Skip to content

Commit 2ec877d

Browse files
committed
fix(@angular-devkit/build-angular): handle basename collisions
(cherry picked from commit 2c9904e)
1 parent cf0228b commit 2ec877d

File tree

3 files changed

+86
-12
lines changed

3 files changed

+86
-12
lines changed

packages/angular/build/src/builders/application/options.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ interface InternalOptions {
6464
* If given a relative path, it is resolved relative to the current workspace and will generate an output at the same relative location
6565
* in the output directory. If given an absolute path, the output will be generated in the root of the output directory with the same base
6666
* name.
67+
*
68+
* If provided a Map, the key is the name of the output bundle and the value is the entry point file.
6769
*/
68-
entryPoints?: Set<string>;
70+
entryPoints?: Set<string> | Map<string, string>;
6971

7072
/** File extension to use for the generated output files. */
7173
outExtension?: 'js' | 'mjs';
@@ -519,7 +521,7 @@ async function getTailwindConfig(
519521
function normalizeEntryPoints(
520522
workspaceRoot: string,
521523
browser: string | undefined,
522-
entryPoints: Set<string> = new Set(),
524+
entryPoints: Set<string> | Map<string, string> = new Set(),
523525
): Record<string, string> {
524526
if (browser === '') {
525527
throw new Error('`browser` option cannot be an empty string.');
@@ -538,6 +540,16 @@ function normalizeEntryPoints(
538540
if (browser) {
539541
// Use `browser` alone.
540542
return { 'main': path.join(workspaceRoot, browser) };
543+
} else if (entryPoints instanceof Map) {
544+
return Object.fromEntries(
545+
Array.from(entryPoints.entries(), ([name, entryPoint]) => {
546+
// Get the full file path to a relative entry point input. Leave bare specifiers alone so they are resolved as modules.
547+
const isRelativePath = entryPoint.startsWith('.');
548+
const entryPointPath = isRelativePath ? path.join(workspaceRoot, entryPoint) : entryPoint;
549+
550+
return [name, entryPointPath];
551+
}),
552+
);
541553
} else {
542554
// Use `entryPoints` alone.
543555
const entryPointPaths: Record<string, string> = {};

packages/angular_devkit/build_angular/src/builders/karma/application_builder.ts

+28-10
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ async function collectEntrypoints(
260260
options: KarmaBuilderOptions,
261261
context: BuilderContext,
262262
projectSourceRoot: string,
263-
): Promise<Set<string>> {
263+
): Promise<Map<string, string>> {
264264
// Glob for files to test.
265265
const testFiles = await findTests(
266266
options.include ?? [],
@@ -269,7 +269,28 @@ async function collectEntrypoints(
269269
projectSourceRoot,
270270
);
271271

272-
return new Set(testFiles);
272+
const seen = new Set<string>();
273+
274+
return new Map(
275+
Array.from(testFiles, (testFile) => {
276+
const relativePath = path
277+
.relative(
278+
testFile.startsWith(projectSourceRoot) ? projectSourceRoot : context.workspaceRoot,
279+
testFile,
280+
)
281+
.replace(/^[./]+/, '_')
282+
.replace(/\//g, '-');
283+
let uniqueName = `spec-${path.basename(relativePath, path.extname(relativePath))}`;
284+
let suffix = 2;
285+
while (seen.has(uniqueName)) {
286+
uniqueName = `${relativePath}-${suffix}`;
287+
++suffix;
288+
}
289+
seen.add(uniqueName);
290+
291+
return [uniqueName, testFile];
292+
}),
293+
);
273294
}
274295

275296
async function initializeApplication(
@@ -298,12 +319,11 @@ async function initializeApplication(
298319
fs.rm(outputPath, { recursive: true, force: true }),
299320
]);
300321

301-
let mainName = 'init_test_bed';
322+
const mainName = 'test_main';
302323
if (options.main) {
303-
entryPoints.add(options.main);
304-
mainName = path.basename(options.main, path.extname(options.main));
324+
entryPoints.set(mainName, options.main);
305325
} else {
306-
entryPoints.add('@angular-devkit/build-angular/src/builders/karma/init_test_bed.js');
326+
entryPoints.set(mainName, '@angular-devkit/build-angular/src/builders/karma/init_test_bed.js');
307327
}
308328

309329
const instrumentForCoverage = options.codeCoverage
@@ -358,6 +378,8 @@ async function initializeApplication(
358378
{ pattern: `${outputPath}/${mainName}.js`, type: 'module', watched: false },
359379
// Serve all source maps.
360380
{ pattern: `${outputPath}/*.map`, included: false, watched: false },
381+
// These are the test entrypoints.
382+
{ pattern: `${outputPath}/spec-*.js`, type: 'module', watched: false },
361383
);
362384

363385
if (hasChunkOrWorkerFiles(buildOutput.files)) {
@@ -371,10 +393,6 @@ async function initializeApplication(
371393
},
372394
);
373395
}
374-
karmaOptions.files.push(
375-
// Serve remaining JS on page load, these are the test entrypoints.
376-
{ pattern: `${outputPath}/*.js`, type: 'module', watched: false },
377-
);
378396

379397
if (options.styles?.length) {
380398
// Serve CSS outputs on page load, these are the global styles.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { execute } from '../../index';
10+
import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup';
11+
12+
describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApp) => {
13+
describe('Behavior: "Specs"', () => {
14+
beforeEach(async () => {
15+
await setupTarget(harness);
16+
});
17+
18+
it('supports multiple spec files with same basename', async () => {
19+
harness.useTarget('test', {
20+
...BASE_OPTIONS,
21+
});
22+
23+
const collidingBasename = 'collision.spec.ts';
24+
25+
// src/app/app.component.spec.ts conflicts with this one:
26+
await harness.writeFiles({
27+
[`src/app/a/${collidingBasename}`]: `/** Success! */`,
28+
[`src/app/b/${collidingBasename}`]: `/** Success! */`,
29+
});
30+
31+
const { result, logs } = await harness.executeOnce();
32+
33+
expect(result?.success).toBeTrue();
34+
35+
if (isApp) {
36+
const bundleLog = logs.find((log) =>
37+
log.message.includes('Application bundle generation complete.'),
38+
);
39+
expect(bundleLog?.message).toContain('spec-app-a-collision.spec.js');
40+
expect(bundleLog?.message).toContain('spec-app-b-collision.spec.js');
41+
}
42+
});
43+
});
44+
});

0 commit comments

Comments
 (0)