Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): handle basename collisions
Browse files Browse the repository at this point in the history
(cherry picked from commit 2c9904e)
  • Loading branch information
jkrems committed Nov 6, 2024
1 parent cf0228b commit 2ec877d
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 12 deletions.
16 changes: 14 additions & 2 deletions packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ interface InternalOptions {
* If given a relative path, it is resolved relative to the current workspace and will generate an output at the same relative location
* 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
* name.
*
* If provided a Map, the key is the name of the output bundle and the value is the entry point file.
*/
entryPoints?: Set<string>;
entryPoints?: Set<string> | Map<string, string>;

/** File extension to use for the generated output files. */
outExtension?: 'js' | 'mjs';
Expand Down Expand Up @@ -519,7 +521,7 @@ async function getTailwindConfig(
function normalizeEntryPoints(
workspaceRoot: string,
browser: string | undefined,
entryPoints: Set<string> = new Set(),
entryPoints: Set<string> | Map<string, string> = new Set(),
): Record<string, string> {
if (browser === '') {
throw new Error('`browser` option cannot be an empty string.');
Expand All @@ -538,6 +540,16 @@ function normalizeEntryPoints(
if (browser) {
// Use `browser` alone.
return { 'main': path.join(workspaceRoot, browser) };
} else if (entryPoints instanceof Map) {
return Object.fromEntries(
Array.from(entryPoints.entries(), ([name, entryPoint]) => {
// Get the full file path to a relative entry point input. Leave bare specifiers alone so they are resolved as modules.
const isRelativePath = entryPoint.startsWith('.');
const entryPointPath = isRelativePath ? path.join(workspaceRoot, entryPoint) : entryPoint;

return [name, entryPointPath];
}),
);
} else {
// Use `entryPoints` alone.
const entryPointPaths: Record<string, string> = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ async function collectEntrypoints(
options: KarmaBuilderOptions,
context: BuilderContext,
projectSourceRoot: string,
): Promise<Set<string>> {
): Promise<Map<string, string>> {
// Glob for files to test.
const testFiles = await findTests(
options.include ?? [],
Expand All @@ -269,7 +269,28 @@ async function collectEntrypoints(
projectSourceRoot,
);

return new Set(testFiles);
const seen = new Set<string>();

return new Map(
Array.from(testFiles, (testFile) => {
const relativePath = path
.relative(
testFile.startsWith(projectSourceRoot) ? projectSourceRoot : context.workspaceRoot,
testFile,
)
.replace(/^[./]+/, '_')
.replace(/\//g, '-');
let uniqueName = `spec-${path.basename(relativePath, path.extname(relativePath))}`;
let suffix = 2;
while (seen.has(uniqueName)) {
uniqueName = `${relativePath}-${suffix}`;
++suffix;
}
seen.add(uniqueName);

return [uniqueName, testFile];
}),
);
}

async function initializeApplication(
Expand Down Expand Up @@ -298,12 +319,11 @@ async function initializeApplication(
fs.rm(outputPath, { recursive: true, force: true }),
]);

let mainName = 'init_test_bed';
const mainName = 'test_main';
if (options.main) {
entryPoints.add(options.main);
mainName = path.basename(options.main, path.extname(options.main));
entryPoints.set(mainName, options.main);
} else {
entryPoints.add('@angular-devkit/build-angular/src/builders/karma/init_test_bed.js');
entryPoints.set(mainName, '@angular-devkit/build-angular/src/builders/karma/init_test_bed.js');
}

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

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

if (options.styles?.length) {
// Serve CSS outputs on page load, these are the global styles.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { execute } from '../../index';
import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeKarmaBuilder } from '../setup';

describeKarmaBuilder(execute, KARMA_BUILDER_INFO, (harness, setupTarget, isApp) => {
describe('Behavior: "Specs"', () => {
beforeEach(async () => {
await setupTarget(harness);
});

it('supports multiple spec files with same basename', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
});

const collidingBasename = 'collision.spec.ts';

// src/app/app.component.spec.ts conflicts with this one:
await harness.writeFiles({
[`src/app/a/${collidingBasename}`]: `/** Success! */`,
[`src/app/b/${collidingBasename}`]: `/** Success! */`,
});

const { result, logs } = await harness.executeOnce();

expect(result?.success).toBeTrue();

if (isApp) {
const bundleLog = logs.find((log) =>
log.message.includes('Application bundle generation complete.'),
);
expect(bundleLog?.message).toContain('spec-app-a-collision.spec.js');
expect(bundleLog?.message).toContain('spec-app-b-collision.spec.js');
}
});
});
});

0 comments on commit 2ec877d

Please sign in to comment.