Skip to content

Commit 93fa9ea

Browse files
committed
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.
1 parent 0fd4404 commit 93fa9ea

3 files changed

Lines changed: 160 additions & 6 deletions

File tree

‎packages/angular/build/src/builders/library/options.ts‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { supportColor } from '../../utils/color';
1313
import { assertIsError } from '../../utils/error';
1414
import { normalizeAssetPatterns } from '../../utils/normalize-asset-patterns';
1515
import { normalizeCacheOptions } from '../../utils/normalize-cache';
16-
import { toPosixPath } from '../../utils/path';
16+
import { canonicalizePath, isSubDirectory, toPosixPath } from '../../utils/path';
1717
import {
1818
generateSearchDirectories,
1919
getTailwindConfig,
@@ -39,14 +39,15 @@ export async function normalizeLibraryOptions(
3939
projectName: string,
4040
options: LibraryBuilderOptions,
4141
): Promise<NormalizedLibraryOptions> {
42-
const workspaceRoot = toPosixPath(context.workspaceRoot);
42+
const workspaceRoot = toPosixPath(canonicalizePath(context.workspaceRoot));
4343
const projectMetadata = await context.getProjectMetadata(projectName);
4444
const projectRootPaths = getProjectRootPaths(workspaceRoot, projectMetadata);
4545
const projectRoot = toPosixPath(projectRootPaths.projectRoot);
4646
const projectSourceRoot = toPosixPath(projectRootPaths.projectSourceRoot);
4747

4848
const {
4949
tsConfig,
50+
outputPath: rawOutputPath,
5051
assets: rawAssets,
5152
stylePreprocessorOptions,
5253
inlineStyleLanguage = 'css',
@@ -62,6 +63,19 @@ export async function normalizeLibraryOptions(
6263
clearScreen,
6364
} = options;
6465

66+
const outputPath = toPosixPath(
67+
path.resolve(workspaceRoot, rawOutputPath ?? path.posix.join('dist', projectName)),
68+
);
69+
if (
70+
outputPath === workspaceRoot ||
71+
outputPath === projectRoot ||
72+
isSubDirectory(outputPath, projectRoot)
73+
) {
74+
throw new Error(
75+
`The 'outputPath' option ('${outputPath}') cannot be the workspace root, project root, or a parent directory of the project root.`,
76+
);
77+
}
78+
6579
const resolvedTsConfigPath = toPosixPath(path.resolve(workspaceRoot, tsConfig));
6680
const packageJsonPath = path.posix.join(projectRoot, 'package.json');
6781

@@ -105,13 +119,31 @@ export async function normalizeLibraryOptions(
105119
{ glob: 'README.md', input: projectRoot, output: '.' },
106120
];
107121

122+
const primaryEntryPoint = entryPoints.get('.');
123+
const primaryEntryDir = primaryEntryPoint
124+
? path.posix.dirname(primaryEntryPoint.entryFilePath)
125+
: undefined;
126+
const seenReadmeAssets = new Set<string>();
127+
108128
for (const entryPoint of entryPoints.values()) {
109129
if (entryPoint.isPrimary) {
110130
continue;
111131
}
132+
133+
const entryDir = path.posix.dirname(entryPoint.entryFilePath);
134+
if (entryDir === projectRoot || entryDir === primaryEntryDir) {
135+
continue;
136+
}
137+
138+
const readmeKey = `${entryDir}:${entryPoint.name}`;
139+
if (seenReadmeAssets.has(readmeKey)) {
140+
continue;
141+
}
142+
seenReadmeAssets.add(readmeKey);
143+
112144
defaultAssets.push({
113145
glob: 'README.md',
114-
input: path.posix.dirname(entryPoint.entryFilePath),
146+
input: entryDir,
115147
output: entryPoint.name,
116148
});
117149
}
@@ -149,9 +181,7 @@ export async function normalizeLibraryOptions(
149181
projectRoot,
150182
packageName,
151183
packageJson,
152-
outputPath: toPosixPath(
153-
path.resolve(workspaceRoot, options.outputPath ?? path.posix.join('dist', projectName)),
154-
),
184+
outputPath,
155185
deleteOutputPath,
156186
packageJsonPath,
157187
tsConfigPath: resolvedTsConfigPath,

‎packages/angular/build/src/builders/library/tests/options/assets_spec.ts‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,76 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
5353

5454
expect(harness.readFile('dist/lib/docs/README.md')).toBe('# Library Docs');
5555
});
56+
57+
it('should not copy root README.md into secondary entry point output when secondary entry point is in projectRoot', async () => {
58+
await harness.writeFiles({
59+
'projects/lib/README.md': '# Root Library README',
60+
'projects/lib/testing.ts': 'export const TESTING = true;',
61+
});
62+
63+
await harness.modifyFile('projects/lib/package.json', (content) => {
64+
const pkg = JSON.parse(content);
65+
pkg.exports = {
66+
'.': './src/public-api.ts',
67+
'./testing': './testing.ts',
68+
};
69+
70+
return JSON.stringify(pkg, null, 2);
71+
});
72+
73+
const { result } = await harness.executeOnce();
74+
expect(result?.success).toBeTrue();
75+
76+
expect(harness.readFile('dist/lib/README.md')).toBe('# Root Library README');
77+
expect(harness.hasFile('dist/lib/testing/README.md')).toBeFalse();
78+
});
79+
80+
it('should not copy README.md into secondary entry point output when sharing directory with primary entry point', async () => {
81+
await harness.writeFiles({
82+
'projects/lib/README.md': '# Root Library README',
83+
'projects/lib/src/README.md': '# Src README',
84+
'projects/lib/src/testing.ts': 'export const TESTING = true;',
85+
});
86+
87+
await harness.modifyFile('projects/lib/package.json', (content) => {
88+
const pkg = JSON.parse(content);
89+
pkg.exports = {
90+
'.': './src/public-api.ts',
91+
'./testing': './src/testing.ts',
92+
};
93+
94+
return JSON.stringify(pkg, null, 2);
95+
});
96+
97+
const { result } = await harness.executeOnce();
98+
expect(result?.success).toBeTrue();
99+
100+
expect(harness.readFile('dist/lib/README.md')).toBe('# Root Library README');
101+
expect(harness.hasFile('dist/lib/testing/README.md')).toBeFalse();
102+
});
103+
104+
it('should copy secondary entry point README.md when located in its own directory', async () => {
105+
await harness.writeFiles({
106+
'projects/lib/README.md': '# Root Library README',
107+
'projects/lib/testing/README.md': '# Testing Entry Point README',
108+
'projects/lib/testing/public-api.ts': 'export const TESTING = true;',
109+
});
110+
111+
await harness.modifyFile('projects/lib/package.json', (content) => {
112+
const pkg = JSON.parse(content);
113+
pkg.exports = {
114+
'.': './src/public-api.ts',
115+
'./testing': './testing/public-api.ts',
116+
};
117+
118+
return JSON.stringify(pkg, null, 2);
119+
});
120+
121+
const { result } = await harness.executeOnce();
122+
expect(result?.success).toBeTrue();
123+
124+
expect(harness.readFile('dist/lib/README.md')).toBe('# Root Library README');
125+
expect(harness.readFile('dist/lib/testing/README.md')).toBe('# Testing Entry Point README');
126+
});
56127
});
57128
});

‎packages/angular/build/src/builders/library/tests/options/output-path_spec.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,58 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
3232
harness.expectFile('dist/custom-output/fesm2022/lib.mjs').toExist();
3333
harness.expectFile('dist/custom-output/package.json').toExist();
3434
});
35+
36+
it('should reject outputPath equal to projectRoot', async () => {
37+
harness.useTarget('build', {
38+
...BASE_OPTIONS,
39+
outputPath: 'projects/lib',
40+
});
41+
42+
const { result, error } = await harness.executeOnce({
43+
outputLogsOnException: false,
44+
outputLogsOnFailure: false,
45+
});
46+
expect(result).toBeUndefined();
47+
expect(error).toBeDefined();
48+
expect((error as Error).message).toMatch(
49+
/The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/,
50+
);
51+
harness.expectFile('projects/lib/package.json').toExist();
52+
});
53+
54+
it('should reject outputPath equal to workspaceRoot', async () => {
55+
harness.useTarget('build', {
56+
...BASE_OPTIONS,
57+
outputPath: '.',
58+
});
59+
60+
const { result, error } = await harness.executeOnce({
61+
outputLogsOnException: false,
62+
outputLogsOnFailure: false,
63+
});
64+
expect(result).toBeUndefined();
65+
expect(error).toBeDefined();
66+
expect((error as Error).message).toMatch(
67+
/The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/,
68+
);
69+
});
70+
71+
it('should reject outputPath when it is a parent directory of projectRoot', async () => {
72+
harness.useTarget('build', {
73+
...BASE_OPTIONS,
74+
outputPath: 'projects',
75+
});
76+
77+
const { result, error } = await harness.executeOnce({
78+
outputLogsOnException: false,
79+
outputLogsOnFailure: false,
80+
});
81+
expect(result).toBeUndefined();
82+
expect(error).toBeDefined();
83+
expect((error as Error).message).toMatch(
84+
/The 'outputPath' option .* cannot be the workspace root, project root, or a parent directory of the project root/,
85+
);
86+
harness.expectFile('projects/lib/package.json').toExist();
87+
});
3588
});
3689
});

0 commit comments

Comments
 (0)