Skip to content

Commit 5e427cc

Browse files
committed
fix(@angular/build): fix setup files duplicate modules
This includes setup files in the initial build and avoids lazy discovery and thus module duplicates. Module duplicates can break many things such as dependency injection. Closes #31732
1 parent 633e44b commit 5e427cc

File tree

4 files changed

+86
-19
lines changed

4 files changed

+86
-19
lines changed

packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { toPosixPath } from '../../../../utils/path';
1111
import type { ApplicationBuilderInternalOptions } from '../../../application/options';
1212
import { OutputHashing } from '../../../application/schema';
1313
import { NormalizedUnitTestBuilderOptions, injectTestingPolyfills } from '../../options';
14-
import { findTests, getTestEntrypoints } from '../../test-discovery';
14+
import { findTests, getSetupEntrypoints, getTestEntrypoints } from '../../test-discovery';
1515
import { RunnerOptions } from '../api';
1616

1717
function createTestBedInitVirtualFile(
@@ -88,12 +88,19 @@ export async function getVitestBuildOptions(
8888
);
8989
}
9090

91-
const entryPoints = getTestEntrypoints(testFiles, {
91+
const testEntryPoints = getTestEntrypoints(testFiles, {
9292
projectSourceRoot,
9393
workspaceRoot,
9494
removeTestExtension: true,
9595
});
96-
entryPoints.set('init-testbed', 'angular:test-bed-init');
96+
const setupEntryPoints = getSetupEntrypoints(options.setupFiles, {
97+
projectSourceRoot,
98+
workspaceRoot,
99+
removeTestExtension: true,
100+
});
101+
setupEntryPoints.set('init-testbed', 'angular:test-bed-init');
102+
103+
const entryPoints = new Map([...testEntryPoints, ...setupEntryPoints]);
97104

98105
// The 'vitest' package is always external for testing purposes
99106
const externalDependencies = ['vitest'];
@@ -138,6 +145,6 @@ export async function getVitestBuildOptions(
138145
virtualFiles: {
139146
'angular:test-bed-init': testBedInitContents,
140147
},
141-
testEntryPointMappings: entryPoints,
148+
testEntryPointMappings: testEntryPoints,
142149
};
143150
}

packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type { TestExecutor } from '../api';
2626
import { setupBrowserConfiguration } from './browser-provider';
2727
import { findVitestBaseConfig } from './configuration';
2828
import { createVitestConfigPlugin, createVitestPlugins } from './plugins';
29+
import { getSetupEntrypoints } from '../../test-discovery';
2930

3031
export class VitestExecutor implements TestExecutor {
3132
private vitest: Vitest | undefined;
@@ -127,9 +128,19 @@ export class VitestExecutor implements TestExecutor {
127128
}
128129

129130
private prepareSetupFiles(): string[] {
130-
const { setupFiles } = this.options;
131+
const { setupFiles, workspaceRoot } = this.options;
132+
133+
const setupFilesEntrypoints = getSetupEntrypoints(setupFiles, {
134+
projectSourceRoot: this.options.projectSourceRoot,
135+
workspaceRoot,
136+
removeTestExtension: true,
137+
});
138+
const setupFileNames = Array.from(setupFilesEntrypoints.keys()).map(
139+
(entrypoint) => `${entrypoint}.js`,
140+
);
141+
131142
// Add setup file entries for TestBed initialization and project polyfills
132-
const testSetupFiles = ['init-testbed.js', ...setupFiles];
143+
const testSetupFiles = ['init-testbed.js', ...setupFileNames];
133144

134145
// TODO: Provide additional result metadata to avoid needing to extract based on filename
135146
if (this.buildResultFiles.has('polyfills.js')) {

packages/angular/build/src/builders/unit-test/test-discovery.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export async function findTests(
7373
return [...resolvedTestFiles];
7474
}
7575

76-
interface TestEntrypointsOptions {
76+
interface EntrypointsOptions {
7777
projectSourceRoot: string;
7878
workspaceRoot: string;
7979
removeTestExtension?: boolean;
@@ -89,15 +89,49 @@ interface TestEntrypointsOptions {
8989
*/
9090
export function getTestEntrypoints(
9191
testFiles: string[],
92-
{ projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions,
92+
{ projectSourceRoot, workspaceRoot, removeTestExtension }: EntrypointsOptions,
93+
): Map<string, string> {
94+
return getEntrypoints(testFiles, {
95+
projectSourceRoot,
96+
workspaceRoot,
97+
removeTestExtension,
98+
prefix: 'spec',
99+
});
100+
}
101+
102+
/**
103+
* @param setupFiles An array of absolute paths to setup files.
104+
* @param options Configuration options for generating entry points.
105+
* @returns A map where keys are the generated unique bundle names and values are the original file paths.
106+
*/
107+
export function getSetupEntrypoints(
108+
setupFiles: string[],
109+
{ projectSourceRoot, workspaceRoot, removeTestExtension }: EntrypointsOptions,
110+
): Map<string, string> {
111+
return getEntrypoints(setupFiles, {
112+
projectSourceRoot,
113+
workspaceRoot,
114+
removeTestExtension,
115+
prefix: 'setup',
116+
});
117+
}
118+
119+
function getEntrypoints(
120+
files: string[],
121+
{
122+
projectSourceRoot,
123+
workspaceRoot,
124+
removeTestExtension,
125+
prefix,
126+
}: EntrypointsOptions & { prefix: string },
93127
): Map<string, string> {
94128
const seen = new Set<string>();
95129
const roots = [projectSourceRoot, workspaceRoot];
96130

97131
return new Map(
98-
Array.from(testFiles, (testFile) => {
99-
const fileName = generateNameFromPath(testFile, roots, !!removeTestExtension);
100-
const baseName = `spec-${fileName}`;
132+
Array.from(files, (setupFile) => {
133+
const fileName = generateNameFromPath(setupFile, roots, !!removeTestExtension);
134+
const baseName = `${prefix}-${fileName}`;
101135
let uniqueName = baseName;
102136
let suffix = 2;
103137
while (seen.has(uniqueName)) {
@@ -106,7 +140,7 @@ export function getTestEntrypoints(
106140
}
107141
seen.add(uniqueName);
108142

109-
return [uniqueName, testFile];
143+
return [uniqueName, setupFile];
110144
}),
111145
);
112146
}
@@ -136,7 +170,7 @@ function generateNameFromPath(
136170
let endIndex = relativePath.length;
137171
if (removeTestExtension) {
138172
const infixes = TEST_FILE_INFIXES.map((p) => p.substring(1)).join('|');
139-
const match = relativePath.match(new RegExp(`\\.(${infixes})\\.[^.]+$`));
173+
const match = relativePath.match(new RegExp(`\\.((${infixes})\\.)?[^.]+$`));
140174

141175
if (match?.index) {
142176
endIndex = match.index;

packages/angular/build/src/builders/unit-test/tests/options/setup-files_spec.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import { execute } from '../../index';
1010
import {
1111
BASE_OPTIONS,
1212
describeBuilder,
13-
UNIT_TEST_BUILDER_INFO,
1413
setupApplicationTarget,
15-
expectLog,
14+
UNIT_TEST_BUILDER_INFO,
1615
} from '../setup';
1716

1817
describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
@@ -45,6 +44,13 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
4544
});`,
4645
});
4746

47+
await harness.modifyFile('src/tsconfig.spec.json', (content) => {
48+
const tsConfig = JSON.parse(content);
49+
tsConfig.files ??= [];
50+
tsConfig.files.push('setup.ts');
51+
return JSON.stringify(tsConfig);
52+
});
53+
4854
harness.useTarget('test', {
4955
...BASE_OPTIONS,
5056
setupFiles: ['src/setup.ts'],
@@ -55,14 +61,16 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
5561
});
5662

5763
it('should allow setup files to configure testing module', async () => {
58-
pending('failing');
5964
await harness.writeFiles({
6065
'src/setup.ts': `
6166
import { TestBed } from '@angular/core/testing';
67+
import { beforeEach } from 'vitest';
6268
import { SETUP_LOADED_TOKEN } from './setup-loaded-token';
6369
64-
TestBed.configureTestingModule({
65-
providers: [{provide: SETUP_LOADED_TOKEN, useValue: true}],
70+
beforeEach(() => {
71+
TestBed.configureTestingModule({
72+
providers: [{provide: SETUP_LOADED_TOKEN, useValue: true}],
73+
});
6674
});
6775
`,
6876
'src/setup-loaded-token.ts': `
@@ -71,7 +79,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
7179
export const SETUP_LOADED_TOKEN = new InjectionToken<boolean>('SETUP_LOADED_TOKEN');
7280
`,
7381
'src/app/app.component.spec.ts': `
74-
import { describe, expect, test } from 'vitest';
82+
import { beforeEach, describe, expect, test } from 'vitest';
7583
import { TestBed } from '@angular/core/testing';
7684
import { SETUP_LOADED_TOKEN } from '../setup-loaded-token';
7785
@@ -82,6 +90,13 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
8290
});`,
8391
});
8492

93+
await harness.modifyFile('src/tsconfig.spec.json', (content) => {
94+
const tsConfig = JSON.parse(content);
95+
tsConfig.files ??= [];
96+
tsConfig.files.push('setup.ts');
97+
return JSON.stringify(tsConfig);
98+
});
99+
85100
harness.useTarget('test', {
86101
...BASE_OPTIONS,
87102
setupFiles: ['src/setup.ts'],

0 commit comments

Comments
 (0)