Skip to content

Commit 9300248

Browse files
clydinalan-agius4
authored andcommitted
fix(@angular-devkit/build-angular): retain symlinks to output platform 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)
1 parent fcd9adc commit 9300248

File tree

3 files changed

+47
-8
lines changed

3 files changed

+47
-8
lines changed

packages/angular_devkit/build_angular/src/builders/application/build-action.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export async function* runEsBuildBuildAction(
5353
} = options;
5454

5555
if (deleteOutputPath && writeToFileSystem) {
56-
await deleteOutputDir(workspaceRoot, outputPath);
56+
await deleteOutputDir(workspaceRoot, outputPath, ['browser', 'server']);
5757
}
5858

5959
const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;

packages/angular_devkit/build_angular/src/builders/application/tests/options/delete-output-path_spec.ts

+28-5
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
1515
// Application code is not needed for asset tests
1616
await harness.writeFile('src/main.ts', 'console.log("TEST");');
1717

18-
// Add file in output
19-
await harness.writeFile('dist/dummy.txt', '');
18+
// Add files in output
19+
await harness.writeFile('dist/a.txt', 'A');
20+
await harness.writeFile('dist/browser/b.txt', 'B');
2021
});
2122

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

2829
const { result } = await harness.executeOnce();
2930
expect(result?.success).toBeTrue();
30-
harness.expectFile('dist/dummy.txt').toNotExist();
31+
harness.expectDirectory('dist').toExist();
32+
harness.expectFile('dist/a.txt').toNotExist();
33+
harness.expectDirectory('dist/browser').toExist();
34+
harness.expectFile('dist/browser/b.txt').toNotExist();
3135
});
3236

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

3943
const { result } = await harness.executeOnce();
4044
expect(result?.success).toBeTrue();
41-
harness.expectFile('dist/dummy.txt').toNotExist();
45+
harness.expectDirectory('dist').toExist();
46+
harness.expectFile('dist/a.txt').toNotExist();
47+
harness.expectDirectory('dist/browser').toExist();
48+
harness.expectFile('dist/browser/b.txt').toNotExist();
4249
});
4350

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

5057
const { result } = await harness.executeOnce();
5158
expect(result?.success).toBeTrue();
52-
harness.expectFile('dist/dummy.txt').toExist();
59+
harness.expectFile('dist/a.txt').toExist();
60+
harness.expectFile('dist/browser/b.txt').toExist();
61+
});
62+
63+
it(`should not delete empty only directories when 'deleteOutputPath' is true`, async () => {
64+
harness.useTarget('build', {
65+
...BASE_OPTIONS,
66+
deleteOutputPath: true,
67+
});
68+
69+
// Add an error to prevent the build from writing files
70+
await harness.writeFile('src/main.ts', 'INVALID_CODE');
71+
72+
const { result } = await harness.executeOnce({ outputLogsOnFailure: false });
73+
expect(result?.success).toBeFalse();
74+
harness.expectDirectory('dist').toExist();
75+
harness.expectDirectory('dist/browser').toExist();
5376
});
5477
});
5578
});

packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ import { join, resolve } from 'node:path';
1212
/**
1313
* Delete an output directory, but error out if it's the root of the project.
1414
*/
15-
export async function deleteOutputDir(root: string, outputPath: string): Promise<void> {
15+
export async function deleteOutputDir(
16+
root: string,
17+
outputPath: string,
18+
emptyOnlyDirectories?: string[],
19+
): Promise<void> {
1620
const resolvedOutputPath = resolve(root, outputPath);
1721
if (resolvedOutputPath === root) {
1822
throw new Error('Output path MUST not be project root directory!');
1923
}
2024

25+
const directoriesToEmpty = emptyOnlyDirectories
26+
? new Set(emptyOnlyDirectories.map((directory) => join(resolvedOutputPath, directory)))
27+
: undefined;
28+
2129
// Avoid removing the actual directory to avoid errors in cases where the output
2230
// directory is mounted or symlinked. Instead the contents are removed.
2331
let entries;
@@ -31,6 +39,14 @@ export async function deleteOutputDir(root: string, outputPath: string): Promise
3139
}
3240

3341
for (const entry of entries) {
34-
await rm(join(resolvedOutputPath, entry), { force: true, recursive: true, maxRetries: 3 });
42+
const fullEntry = join(resolvedOutputPath, entry);
43+
44+
// Leave requested directories. This allows symlinks to continue to function.
45+
if (directoriesToEmpty?.has(fullEntry)) {
46+
await deleteOutputDir(resolvedOutputPath, fullEntry);
47+
continue;
48+
}
49+
50+
await rm(fullEntry, { force: true, recursive: true, maxRetries: 3 });
3551
}
3652
}

0 commit comments

Comments
 (0)