Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/angular/build/src/builders/library/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ function normalizeEntryPoints(
const exportsRecord = typeof rawExports === 'string' ? { '.': rawExports } : rawExports;

const entryPoints = new Map<string, NormalizedEntryPoint>();
// Bundle names flatten '/' to '-', so two distinct subpaths can produce one.
// Keyed case-insensitively: names differing only in case are distinct keys in
// the bundler input map but one file on a case-insensitive filesystem.
const bundleNameOrigins = new Map<string, string>();
let hasPrimary = false;

for (const [key, value] of Object.entries(exportsRecord)) {
Expand Down Expand Up @@ -346,6 +350,17 @@ function normalizeEntryPoints(
);
}

const bundleNameKey = entryPoint.bundleName.toLowerCase();
const collidingKey = bundleNameOrigins.get(bundleNameKey);
if (collidingKey !== undefined) {
throw new Error(
`Entry points '${collidingKey}' and '${key}' both produce the bundle name ` +
`'${entryPoint.bundleName}'. Entry point subpaths must not differ only by ` +
`'/' versus '-', or by case.`,
);
}

bundleNameOrigins.set(bundleNameKey, key);
entryPoints.set(entryPoint.name, entryPoint);

if (entryPoint.isPrimary) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,86 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
expect(result?.success).toBeTrue();
});

it('should succeed with a nested entry point beside a similarly named flat one', async () => {
await harness.writeFiles({
'projects/lib/zz/child/public-api.ts': 'export const NESTED = 42;\n',
'projects/lib/zz-sibling/public-api.ts': 'export const FLAT = 7;\n',
});
await harness.modifyFile('projects/lib/package.json', (content) => {
const pkg = JSON.parse(content);
pkg.exports = {
'.': './src/public-api.ts',
'./zz/child': './zz/child/public-api.ts',
'./zz-sibling': './zz-sibling/public-api.ts',
};

return JSON.stringify(pkg, null, 2);
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
harness.expectFile('dist/lib/fesm2022/lib-zz-child.mjs').toExist();
harness.expectFile('dist/lib/fesm2022/lib-zz-sibling.mjs').toExist();
});

it('should fail when two entry points produce the same bundle name', async () => {
// A bundle name flattens '/' to '-', so './zz/child' and './zz-child' both
// become 'lib-zz-child'. Without a guard the second silently overwrites the
// first in the bundler input map: the build exits 0, one entry point's code
// is gone, and both 'exports' keys resolve to the surviving file.
await harness.writeFiles({
'projects/lib/zz/child/public-api.ts': 'export const NESTED = 42;\n',
'projects/lib/zz-child/public-api.ts': 'export const FLAT = 7;\n',
});
await harness.modifyFile('projects/lib/package.json', (content) => {
const pkg = JSON.parse(content);
pkg.exports = {
'.': './src/public-api.ts',
'./zz/child': './zz/child/public-api.ts',
'./zz-child': './zz-child/public-api.ts',
};

return JSON.stringify(pkg, null, 2);
});

const { result, error } = await harness.executeOnce({
outputLogsOnException: false,
outputLogsOnFailure: false,
});
expect(result).toBeUndefined();
expect(error).toBeDefined();
expect((error as Error).message).toMatch(
/both produce the bundle name 'lib-zz-child'/,
);
});

it('should fail when two entry points differ only by case', async () => {
// Distinct keys in the bundler input map, but one file on a case-insensitive
// filesystem, so the emitted bundle depends on which OS ran the build.
await harness.writeFiles({
'projects/lib/Zz/public-api.ts': 'export const UPPER = 42;\n',
'projects/lib/zz/public-api.ts': 'export const LOWER = 7;\n',
});
await harness.modifyFile('projects/lib/package.json', (content) => {
const pkg = JSON.parse(content);
pkg.exports = {
'.': './src/public-api.ts',
'./Zz': './Zz/public-api.ts',
'./zz': './zz/public-api.ts',
};

return JSON.stringify(pkg, null, 2);
});

const { result, error } = await harness.executeOnce({
outputLogsOnException: false,
outputLogsOnFailure: false,
});
expect(result).toBeUndefined();
expect(error).toBeDefined();
expect((error as Error).message).toMatch(/both produce the bundle name/);
});

it('should fail when entry point is not a .ts or .mts file', async () => {
await harness.modifyFile('projects/lib/package.json', (content) => {
const pkg = JSON.parse(content);
Expand Down
Loading