Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): retain symlinks to output platfor…
Browse files Browse the repository at this point in the history
…m directories on builds

The `deleteOutputPath` option will now empty specific build artifact directories instead of
removing them completely. This allows for symlinking or mounting the directories via Docker.
This is similar to the current behavior of emptying the root output path to allow similar
actions. All previous files will still be removed when the `deleteOutputPath` option is enabled.
This is useful in situations were the browser output files may be symlinked onto another
location on disk that is setup as a development server, or a Docker configuration mounts the browser
and server output to different locations on the host machine.

(cherry picked from commit 6a44989)
  • Loading branch information
clydin authored and alan-agius4 committed Dec 11, 2023
1 parent fcd9adc commit 9300248
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function* runEsBuildBuildAction(
} = options;

if (deleteOutputPath && writeToFileSystem) {
await deleteOutputDir(workspaceRoot, outputPath);
await deleteOutputDir(workspaceRoot, outputPath, ['browser', 'server']);
}

const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
// Application code is not needed for asset tests
await harness.writeFile('src/main.ts', 'console.log("TEST");');

// Add file in output
await harness.writeFile('dist/dummy.txt', '');
// Add files in output
await harness.writeFile('dist/a.txt', 'A');
await harness.writeFile('dist/browser/b.txt', 'B');
});

it(`should delete the output files when 'deleteOutputPath' is true`, async () => {
Expand All @@ -27,7 +28,10 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectFile('dist/dummy.txt').toNotExist();
harness.expectDirectory('dist').toExist();
harness.expectFile('dist/a.txt').toNotExist();
harness.expectDirectory('dist/browser').toExist();
harness.expectFile('dist/browser/b.txt').toNotExist();
});

it(`should delete the output files when 'deleteOutputPath' is not set`, async () => {
Expand All @@ -38,7 +42,10 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectFile('dist/dummy.txt').toNotExist();
harness.expectDirectory('dist').toExist();
harness.expectFile('dist/a.txt').toNotExist();
harness.expectDirectory('dist/browser').toExist();
harness.expectFile('dist/browser/b.txt').toNotExist();
});

it(`should not delete the output files when 'deleteOutputPath' is false`, async () => {
Expand All @@ -49,7 +56,23 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectFile('dist/dummy.txt').toExist();
harness.expectFile('dist/a.txt').toExist();
harness.expectFile('dist/browser/b.txt').toExist();
});

it(`should not delete empty only directories when 'deleteOutputPath' is true`, async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
deleteOutputPath: true,
});

// Add an error to prevent the build from writing files
await harness.writeFile('src/main.ts', 'INVALID_CODE');

const { result } = await harness.executeOnce({ outputLogsOnFailure: false });
expect(result?.success).toBeFalse();
harness.expectDirectory('dist').toExist();
harness.expectDirectory('dist/browser').toExist();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ import { join, resolve } from 'node:path';
/**
* Delete an output directory, but error out if it's the root of the project.
*/
export async function deleteOutputDir(root: string, outputPath: string): Promise<void> {
export async function deleteOutputDir(
root: string,
outputPath: string,
emptyOnlyDirectories?: string[],
): Promise<void> {
const resolvedOutputPath = resolve(root, outputPath);
if (resolvedOutputPath === root) {
throw new Error('Output path MUST not be project root directory!');
}

const directoriesToEmpty = emptyOnlyDirectories
? new Set(emptyOnlyDirectories.map((directory) => join(resolvedOutputPath, directory)))
: undefined;

// Avoid removing the actual directory to avoid errors in cases where the output
// directory is mounted or symlinked. Instead the contents are removed.
let entries;
Expand All @@ -31,6 +39,14 @@ export async function deleteOutputDir(root: string, outputPath: string): Promise
}

for (const entry of entries) {
await rm(join(resolvedOutputPath, entry), { force: true, recursive: true, maxRetries: 3 });
const fullEntry = join(resolvedOutputPath, entry);

// Leave requested directories. This allows symlinks to continue to function.
if (directoriesToEmpty?.has(fullEntry)) {
await deleteOutputDir(resolvedOutputPath, fullEntry);
continue;
}

await rm(fullEntry, { force: true, recursive: true, maxRetries: 3 });
}
}

0 comments on commit 9300248

Please sign in to comment.