diff --git a/packages/angular_devkit/build_angular/src/builders/application/build-action.ts b/packages/angular_devkit/build_angular/src/builders/application/build-action.ts index fb0ed18a8dbf..21037c2be33c 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/build-action.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/build-action.ts @@ -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; diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/options/delete-output-path_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/delete-output-path_spec.ts index a5f0b18a051c..1a7a11b3d4e0 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/tests/options/delete-output-path_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/delete-output-path_spec.ts @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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(); }); }); }); diff --git a/packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts b/packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts index 20e8b2c14b1a..9ca6bc091d18 100644 --- a/packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts +++ b/packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts @@ -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 { +export async function deleteOutputDir( + root: string, + outputPath: string, + emptyOnlyDirectories?: string[], +): Promise { 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; @@ -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 }); } }