From 93fa9ea666bf9fe20a34796df49f67fa692293bb Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:01:22 +0000 Subject: [PATCH 1/4] fix(@angular/build): validate library outputPath and avoid duplicate secondary README assets - Prevent secondary entry-point default `README.md` assets from copying the root or primary `README.md` when sharing `projectRoot` or the primary entry-point directory. - Validate in `normalizeLibraryOptions` that `outputPath` is not equal to `workspaceRoot`, `projectRoot`, or a parent directory of `projectRoot` before `deleteOutputDir` executes. - Canonicalize `workspaceRoot` via `canonicalizePath` to ensure consistent Windows drive-letter casing across compilation and file watching. --- .../build/src/builders/library/options.ts | 42 +++++++++-- .../library/tests/options/assets_spec.ts | 71 +++++++++++++++++++ .../library/tests/options/output-path_spec.ts | 53 ++++++++++++++ 3 files changed, 160 insertions(+), 6 deletions(-) diff --git a/packages/angular/build/src/builders/library/options.ts b/packages/angular/build/src/builders/library/options.ts index 23d18d6f4d8e..e8aae6e34665 100644 --- a/packages/angular/build/src/builders/library/options.ts +++ b/packages/angular/build/src/builders/library/options.ts @@ -13,7 +13,7 @@ import { supportColor } from '../../utils/color'; import { assertIsError } from '../../utils/error'; import { normalizeAssetPatterns } from '../../utils/normalize-asset-patterns'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; -import { toPosixPath } from '../../utils/path'; +import { canonicalizePath, isSubDirectory, toPosixPath } from '../../utils/path'; import { generateSearchDirectories, getTailwindConfig, @@ -39,7 +39,7 @@ export async function normalizeLibraryOptions( projectName: string, options: LibraryBuilderOptions, ): Promise { - const workspaceRoot = toPosixPath(context.workspaceRoot); + const workspaceRoot = toPosixPath(canonicalizePath(context.workspaceRoot)); const projectMetadata = await context.getProjectMetadata(projectName); const projectRootPaths = getProjectRootPaths(workspaceRoot, projectMetadata); const projectRoot = toPosixPath(projectRootPaths.projectRoot); @@ -47,6 +47,7 @@ export async function normalizeLibraryOptions( const { tsConfig, + outputPath: rawOutputPath, assets: rawAssets, stylePreprocessorOptions, inlineStyleLanguage = 'css', @@ -62,6 +63,19 @@ export async function normalizeLibraryOptions( clearScreen, } = options; + const outputPath = toPosixPath( + path.resolve(workspaceRoot, rawOutputPath ?? path.posix.join('dist', projectName)), + ); + if ( + outputPath === workspaceRoot || + outputPath === projectRoot || + isSubDirectory(outputPath, projectRoot) + ) { + throw new Error( + `The 'outputPath' option ('${outputPath}') cannot be the workspace root, project root, or a parent directory of the project root.`, + ); + } + const resolvedTsConfigPath = toPosixPath(path.resolve(workspaceRoot, tsConfig)); const packageJsonPath = path.posix.join(projectRoot, 'package.json'); @@ -105,13 +119,31 @@ export async function normalizeLibraryOptions( { glob: 'README.md', input: projectRoot, output: '.' }, ]; + const primaryEntryPoint = entryPoints.get('.'); + const primaryEntryDir = primaryEntryPoint + ? path.posix.dirname(primaryEntryPoint.entryFilePath) + : undefined; + const seenReadmeAssets = new Set(); + for (const entryPoint of entryPoints.values()) { if (entryPoint.isPrimary) { continue; } + + const entryDir = path.posix.dirname(entryPoint.entryFilePath); + if (entryDir === projectRoot || entryDir === primaryEntryDir) { + continue; + } + + const readmeKey = `${entryDir}:${entryPoint.name}`; + if (seenReadmeAssets.has(readmeKey)) { + continue; + } + seenReadmeAssets.add(readmeKey); + defaultAssets.push({ glob: 'README.md', - input: path.posix.dirname(entryPoint.entryFilePath), + input: entryDir, output: entryPoint.name, }); } @@ -149,9 +181,7 @@ export async function normalizeLibraryOptions( projectRoot, packageName, packageJson, - outputPath: toPosixPath( - path.resolve(workspaceRoot, options.outputPath ?? path.posix.join('dist', projectName)), - ), + outputPath, deleteOutputPath, packageJsonPath, tsConfigPath: resolvedTsConfigPath, diff --git a/packages/angular/build/src/builders/library/tests/options/assets_spec.ts b/packages/angular/build/src/builders/library/tests/options/assets_spec.ts index a596cbfe4e62..57480fd1c0b8 100644 --- a/packages/angular/build/src/builders/library/tests/options/assets_spec.ts +++ b/packages/angular/build/src/builders/library/tests/options/assets_spec.ts @@ -53,5 +53,76 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(harness.readFile('dist/lib/docs/README.md')).toBe('# Library Docs'); }); + + it('should not copy root README.md into secondary entry point output when secondary entry point is in projectRoot', async () => { + await harness.writeFiles({ + 'projects/lib/README.md': '# Root Library README', + 'projects/lib/testing.ts': 'export const TESTING = true;', + }); + + await harness.modifyFile('projects/lib/package.json', (content) => { + const pkg = JSON.parse(content); + pkg.exports = { + '.': './src/public-api.ts', + './testing': './testing.ts', + }; + + return JSON.stringify(pkg, null, 2); + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + expect(harness.readFile('dist/lib/README.md')).toBe('# Root Library README'); + expect(harness.hasFile('dist/lib/testing/README.md')).toBeFalse(); + }); + + it('should not copy README.md into secondary entry point output when sharing directory with primary entry point', async () => { + await harness.writeFiles({ + 'projects/lib/README.md': '# Root Library README', + 'projects/lib/src/README.md': '# Src README', + 'projects/lib/src/testing.ts': 'export const TESTING = true;', + }); + + await harness.modifyFile('projects/lib/package.json', (content) => { + const pkg = JSON.parse(content); + pkg.exports = { + '.': './src/public-api.ts', + './testing': './src/testing.ts', + }; + + return JSON.stringify(pkg, null, 2); + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + expect(harness.readFile('dist/lib/README.md')).toBe('# Root Library README'); + expect(harness.hasFile('dist/lib/testing/README.md')).toBeFalse(); + }); + + it('should copy secondary entry point README.md when located in its own directory', async () => { + await harness.writeFiles({ + 'projects/lib/README.md': '# Root Library README', + 'projects/lib/testing/README.md': '# Testing Entry Point README', + 'projects/lib/testing/public-api.ts': 'export const TESTING = true;', + }); + + await harness.modifyFile('projects/lib/package.json', (content) => { + const pkg = JSON.parse(content); + pkg.exports = { + '.': './src/public-api.ts', + './testing': './testing/public-api.ts', + }; + + return JSON.stringify(pkg, null, 2); + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + expect(harness.readFile('dist/lib/README.md')).toBe('# Root Library README'); + expect(harness.readFile('dist/lib/testing/README.md')).toBe('# Testing Entry Point README'); + }); }); }); diff --git a/packages/angular/build/src/builders/library/tests/options/output-path_spec.ts b/packages/angular/build/src/builders/library/tests/options/output-path_spec.ts index 2969692fa6ac..f102ed0eef4f 100644 --- a/packages/angular/build/src/builders/library/tests/options/output-path_spec.ts +++ b/packages/angular/build/src/builders/library/tests/options/output-path_spec.ts @@ -32,5 +32,58 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => harness.expectFile('dist/custom-output/fesm2022/lib.mjs').toExist(); harness.expectFile('dist/custom-output/package.json').toExist(); }); + + it('should reject outputPath equal to projectRoot', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + outputPath: 'projects/lib', + }); + + const { result, error } = await harness.executeOnce({ + outputLogsOnException: false, + outputLogsOnFailure: false, + }); + expect(result).toBeUndefined(); + expect(error).toBeDefined(); + expect((error as Error).message).toMatch( + /The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/, + ); + harness.expectFile('projects/lib/package.json').toExist(); + }); + + it('should reject outputPath equal to workspaceRoot', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + outputPath: '.', + }); + + const { result, error } = await harness.executeOnce({ + outputLogsOnException: false, + outputLogsOnFailure: false, + }); + expect(result).toBeUndefined(); + expect(error).toBeDefined(); + expect((error as Error).message).toMatch( + /The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/, + ); + }); + + it('should reject outputPath when it is a parent directory of projectRoot', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + outputPath: 'projects', + }); + + const { result, error } = await harness.executeOnce({ + outputLogsOnException: false, + outputLogsOnFailure: false, + }); + expect(result).toBeUndefined(); + expect(error).toBeDefined(); + expect((error as Error).message).toMatch( + /The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/, + ); + harness.expectFile('projects/lib/package.json').toExist(); + }); }); }); From 58fbe7580bd5aa4a5538c37b25601bc48a271998 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:08:13 +0000 Subject: [PATCH 2/4] fixup! fix(@angular/build): validate library outputPath and avoid duplicate secondary README assets --- .../build/src/builders/library/options.ts | 20 ++----- .../library/tests/options/output-path_spec.ts | 53 ------------------- 2 files changed, 4 insertions(+), 69 deletions(-) diff --git a/packages/angular/build/src/builders/library/options.ts b/packages/angular/build/src/builders/library/options.ts index e8aae6e34665..a6ad882da5fc 100644 --- a/packages/angular/build/src/builders/library/options.ts +++ b/packages/angular/build/src/builders/library/options.ts @@ -13,7 +13,7 @@ import { supportColor } from '../../utils/color'; import { assertIsError } from '../../utils/error'; import { normalizeAssetPatterns } from '../../utils/normalize-asset-patterns'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; -import { canonicalizePath, isSubDirectory, toPosixPath } from '../../utils/path'; +import { canonicalizePath, toPosixPath } from '../../utils/path'; import { generateSearchDirectories, getTailwindConfig, @@ -47,7 +47,6 @@ export async function normalizeLibraryOptions( const { tsConfig, - outputPath: rawOutputPath, assets: rawAssets, stylePreprocessorOptions, inlineStyleLanguage = 'css', @@ -63,19 +62,6 @@ export async function normalizeLibraryOptions( clearScreen, } = options; - const outputPath = toPosixPath( - path.resolve(workspaceRoot, rawOutputPath ?? path.posix.join('dist', projectName)), - ); - if ( - outputPath === workspaceRoot || - outputPath === projectRoot || - isSubDirectory(outputPath, projectRoot) - ) { - throw new Error( - `The 'outputPath' option ('${outputPath}') cannot be the workspace root, project root, or a parent directory of the project root.`, - ); - } - const resolvedTsConfigPath = toPosixPath(path.resolve(workspaceRoot, tsConfig)); const packageJsonPath = path.posix.join(projectRoot, 'package.json'); @@ -181,7 +167,9 @@ export async function normalizeLibraryOptions( projectRoot, packageName, packageJson, - outputPath, + outputPath: toPosixPath( + path.resolve(workspaceRoot, options.outputPath ?? path.posix.join('dist', projectName)), + ), deleteOutputPath, packageJsonPath, tsConfigPath: resolvedTsConfigPath, diff --git a/packages/angular/build/src/builders/library/tests/options/output-path_spec.ts b/packages/angular/build/src/builders/library/tests/options/output-path_spec.ts index f102ed0eef4f..2969692fa6ac 100644 --- a/packages/angular/build/src/builders/library/tests/options/output-path_spec.ts +++ b/packages/angular/build/src/builders/library/tests/options/output-path_spec.ts @@ -32,58 +32,5 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => harness.expectFile('dist/custom-output/fesm2022/lib.mjs').toExist(); harness.expectFile('dist/custom-output/package.json').toExist(); }); - - it('should reject outputPath equal to projectRoot', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - outputPath: 'projects/lib', - }); - - const { result, error } = await harness.executeOnce({ - outputLogsOnException: false, - outputLogsOnFailure: false, - }); - expect(result).toBeUndefined(); - expect(error).toBeDefined(); - expect((error as Error).message).toMatch( - /The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/, - ); - harness.expectFile('projects/lib/package.json').toExist(); - }); - - it('should reject outputPath equal to workspaceRoot', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - outputPath: '.', - }); - - const { result, error } = await harness.executeOnce({ - outputLogsOnException: false, - outputLogsOnFailure: false, - }); - expect(result).toBeUndefined(); - expect(error).toBeDefined(); - expect((error as Error).message).toMatch( - /The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/, - ); - }); - - it('should reject outputPath when it is a parent directory of projectRoot', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - outputPath: 'projects', - }); - - const { result, error } = await harness.executeOnce({ - outputLogsOnException: false, - outputLogsOnFailure: false, - }); - expect(result).toBeUndefined(); - expect(error).toBeDefined(); - expect((error as Error).message).toMatch( - /The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/, - ); - harness.expectFile('projects/lib/package.json').toExist(); - }); }); }); From 8b12f35d9eb58b33974636d44075ab76d557e259 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:21:48 +0000 Subject: [PATCH 3/4] fixup! fix(@angular/build): validate library outputPath and avoid duplicate secondary README assets --- .../build/src/builders/library/options.ts | 29 ------------------- .../library/tests/options/assets_spec.ts | 4 +-- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/angular/build/src/builders/library/options.ts b/packages/angular/build/src/builders/library/options.ts index a6ad882da5fc..5c47e3dcc6cc 100644 --- a/packages/angular/build/src/builders/library/options.ts +++ b/packages/angular/build/src/builders/library/options.ts @@ -105,35 +105,6 @@ export async function normalizeLibraryOptions( { glob: 'README.md', input: projectRoot, output: '.' }, ]; - const primaryEntryPoint = entryPoints.get('.'); - const primaryEntryDir = primaryEntryPoint - ? path.posix.dirname(primaryEntryPoint.entryFilePath) - : undefined; - const seenReadmeAssets = new Set(); - - for (const entryPoint of entryPoints.values()) { - if (entryPoint.isPrimary) { - continue; - } - - const entryDir = path.posix.dirname(entryPoint.entryFilePath); - if (entryDir === projectRoot || entryDir === primaryEntryDir) { - continue; - } - - const readmeKey = `${entryDir}:${entryPoint.name}`; - if (seenReadmeAssets.has(readmeKey)) { - continue; - } - seenReadmeAssets.add(readmeKey); - - defaultAssets.push({ - glob: 'README.md', - input: entryDir, - output: entryPoint.name, - }); - } - const assets = normalizeAssetPatterns( [...defaultAssets, ...(rawAssets ?? [])], workspaceRoot, diff --git a/packages/angular/build/src/builders/library/tests/options/assets_spec.ts b/packages/angular/build/src/builders/library/tests/options/assets_spec.ts index 57480fd1c0b8..32d2dc1ab507 100644 --- a/packages/angular/build/src/builders/library/tests/options/assets_spec.ts +++ b/packages/angular/build/src/builders/library/tests/options/assets_spec.ts @@ -101,7 +101,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(harness.hasFile('dist/lib/testing/README.md')).toBeFalse(); }); - it('should copy secondary entry point README.md when located in its own directory', async () => { + it('should only copy the root README.md by default and not secondary entry point README.md files', async () => { await harness.writeFiles({ 'projects/lib/README.md': '# Root Library README', 'projects/lib/testing/README.md': '# Testing Entry Point README', @@ -122,7 +122,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(result?.success).toBeTrue(); expect(harness.readFile('dist/lib/README.md')).toBe('# Root Library README'); - expect(harness.readFile('dist/lib/testing/README.md')).toBe('# Testing Entry Point README'); + expect(harness.hasFile('dist/lib/testing/README.md')).toBeFalse(); }); }); }); From ef64cd16ecc0a4f2d10e71c1a40ceb2739baadd5 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:31:41 +0000 Subject: [PATCH 4/4] fixup! fix(@angular/build): validate library outputPath and avoid duplicate secondary README assets --- packages/angular/build/src/builders/library/options.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/builders/library/options.ts b/packages/angular/build/src/builders/library/options.ts index 5c47e3dcc6cc..c4df9fe3996a 100644 --- a/packages/angular/build/src/builders/library/options.ts +++ b/packages/angular/build/src/builders/library/options.ts @@ -13,7 +13,7 @@ import { supportColor } from '../../utils/color'; import { assertIsError } from '../../utils/error'; import { normalizeAssetPatterns } from '../../utils/normalize-asset-patterns'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; -import { canonicalizePath, toPosixPath } from '../../utils/path'; +import { toPosixPath } from '../../utils/path'; import { generateSearchDirectories, getTailwindConfig, @@ -39,7 +39,7 @@ export async function normalizeLibraryOptions( projectName: string, options: LibraryBuilderOptions, ): Promise { - const workspaceRoot = toPosixPath(canonicalizePath(context.workspaceRoot)); + const workspaceRoot = toPosixPath(context.workspaceRoot); const projectMetadata = await context.getProjectMetadata(projectName); const projectRootPaths = getProjectRootPaths(workspaceRoot, projectMetadata); const projectRoot = toPosixPath(projectRootPaths.projectRoot);