Skip to content

Commit 5f80743

Browse files
committed
fix(@angular/build): only compile included test files in unit-test builder
When running the `unit-test` builder with `--include`, TypeScript compilation previously compiled all test files matched by the tsconfig file, causing compilation errors in unrelated test files to fail the test run. This change introduces a `rootFiles` option that restricts the TypeScript program's root files to only the test files to be executed (plus setup files and providers), while preserving ambient `.d.ts` declaration files discovered by `readConfiguration`. Fixes #34089
1 parent cbac34a commit 5f80743

9 files changed

Lines changed: 87 additions & 13 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ interface InternalOptions {
138138
* there. Used exclusively for tests and shouldn't be used for other kinds of builds.
139139
*/
140140
disableCodeSplitting?: boolean;
141+
142+
/**
143+
* An array of root files to compile. When provided, only these files (and ambient declaration files)
144+
* will be included in the TypeScript compilation root names.
145+
*/
146+
rootFiles?: string[];
141147
}
142148

143149
/** Full set of options for `application` builder. */
@@ -447,6 +453,7 @@ export async function normalizeOptions(
447453
verbose,
448454
watch,
449455
progress = true,
456+
rootFiles,
450457
externalPackages,
451458
namedChunks,
452459
budgets,
@@ -494,6 +501,7 @@ export async function normalizeOptions(
494501
workspaceRoot,
495502
entryPoints,
496503
disableCodeSplitting,
504+
rootFiles: rootFiles?.map((file: string) => path.resolve(workspaceRoot, file)),
497505
optimizationOptions,
498506
outputOptions,
499507
outExtension,

packages/angular/build/src/builders/karma/application_builder.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,10 @@ async function setupBuildOptions(
157157
projectSourceRoot: string,
158158
outputPath: string,
159159
): Promise<{ buildOptions: BuildOptions; mainName: string }> {
160-
const entryPoints = await collectEntrypoints(options, context, projectSourceRoot);
160+
const { entryPoints, testFiles } = await collectEntrypoints(options, context, projectSourceRoot);
161161

162162
const mainName = 'test_main';
163-
if (options.main) {
164-
entryPoints.set(mainName, options.main);
165-
} else {
166-
entryPoints.set(mainName, 'angular:test-bed-init');
167-
}
163+
entryPoints.set(mainName, options.main || 'angular:test-bed-init');
168164

169165
const instrumentForCoverage = options.codeCoverage
170166
? createInstrumentationFilter(
@@ -181,6 +177,7 @@ async function setupBuildOptions(
181177
const buildOptions: BuildOptions = {
182178
assets: options.assets,
183179
entryPoints,
180+
rootFiles: options.main ? [...testFiles, options.main] : testFiles,
184181
tsConfig: options.tsConfig,
185182
outputPath,
186183
preserveSymlinks: options.preserveSymlinks,

packages/angular/build/src/builders/karma/utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export async function collectEntrypoints(
5050
options: NormalizedKarmaBuilderOptions,
5151
context: BuilderContext,
5252
projectSourceRoot: string,
53-
): Promise<Map<string, string>> {
53+
): Promise<{ entryPoints: Map<string, string>; testFiles: string[] }> {
5454
// Glob for files to test.
5555
const testFiles = await findTests(
5656
options.include,
@@ -59,7 +59,12 @@ export async function collectEntrypoints(
5959
projectSourceRoot,
6060
);
6161

62-
return getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot });
62+
const entryPoints = getTestEntrypoints(testFiles, {
63+
projectSourceRoot,
64+
workspaceRoot: context.workspaceRoot,
65+
});
66+
67+
return { entryPoints, testFiles };
6368
}
6469

6570
export function hasChunkOrWorkerFiles(files: readonly ResultFile[]): boolean {

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,15 @@ export async function getVitestBuildOptions(
198198
options: NormalizedUnitTestBuilderOptions,
199199
baseBuildOptions: Partial<ApplicationBuilderInternalOptions>,
200200
): Promise<RunnerOptions> {
201-
const { workspaceRoot, projectSourceRoot, include, exclude = [], watch, providersFile } = options;
201+
const {
202+
workspaceRoot,
203+
projectSourceRoot,
204+
include,
205+
exclude = [],
206+
watch,
207+
providersFile,
208+
setupFiles,
209+
} = options;
202210

203211
// Find test files
204212
const testFiles = await findTests(include, exclude, workspaceRoot, projectSourceRoot);
@@ -217,8 +225,15 @@ export async function getVitestBuildOptions(
217225
removeTestExtension: true,
218226
});
219227

220-
if (options.setupFiles?.length) {
221-
const setupEntryPoints = getTestEntrypoints(options.setupFiles, {
228+
const rootFiles = [...testFiles];
229+
if (providersFile) {
230+
rootFiles.push(providersFile);
231+
}
232+
233+
if (setupFiles?.length) {
234+
rootFiles.push(...setupFiles);
235+
236+
const setupEntryPoints = getTestEntrypoints(setupFiles, {
222237
projectSourceRoot,
223238
workspaceRoot,
224239
removeTestExtension: false,
@@ -258,6 +273,7 @@ export async function getVitestBuildOptions(
258273
optimization: false,
259274
namedChunks: false,
260275
entryPoints,
276+
rootFiles,
261277
// Vitest's Node-based module loading emulation (vite-node) is not fully spec compliant and lacks
262278
// live ESM bindings across chunk boundaries. This can cause uninitialized exports or break mocking.
263279
// Disabling code splitting avoids shared chunks, but increases build and coverage memory/time.

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,32 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
8383
expect(result?.success).toBeTrue();
8484
});
8585
});
86+
87+
it('should ignore TypeScript compilation errors in non-included test files', async () => {
88+
await harness.writeFiles({
89+
'src/app/services/test.service.spec.ts': `
90+
describe('TestService', () => {
91+
it('should succeed', () => {
92+
expect(true).toBe(true);
93+
});
94+
});`,
95+
'src/app/broken.service.spec.ts': `
96+
// This test has a TypeScript type error that would fail compilation if compiled
97+
const invalidNumber: number = 'not a number';
98+
describe('BrokenService', () => {
99+
it('should fail compilation', () => {
100+
expect(invalidNumber).toBe(1);
101+
});
102+
});`,
103+
});
104+
105+
harness.useTarget('test', {
106+
...BASE_OPTIONS,
107+
include: ['src/app/services/test.service.spec.ts'],
108+
});
109+
110+
const { result } = await harness.executeOnce();
111+
expect(result?.success).toBeTrue();
112+
});
86113
});
87114
});

packages/angular/build/src/tools/angular/compilation/compiler-options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface CompilerOptionOverrides {
2020
instrumentForCoverage?: boolean;
2121
includeTestMetadata?: boolean;
2222
customConditions?: string[];
23+
rootFiles?: string[];
2324
}
2425

2526
export function transformCompilerOptions(

packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import type * as ng from '@angular/compiler-cli';
1010
import type { PartialMessage } from 'esbuild';
1111
import ts from 'typescript';
12-
import { toPosixPath } from '../../../utils/path';
12+
import { canonicalizePath, toPosixPath } from '../../../utils/path';
1313
import { profileAsync, profileSync } from '../../esbuild/profiling';
1414
import { AngularCompilation, DiagnosticModes } from './angular-compilation';
1515
import { type CompilerOptionOverrides, transformCompilerOptions } from './compiler-options';
@@ -39,7 +39,7 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
3939

4040
const {
4141
options: originalCompilerOptions,
42-
rootNames,
42+
rootNames: originalRootNames,
4343
errors,
4444
} = profileSync('NG_READ_CONFIG', () =>
4545
readConfiguration(tsconfig, {
@@ -60,6 +60,19 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
6060
}),
6161
);
6262

63+
let rootNames = originalRootNames;
64+
if (compilerOptionOverrides?.rootFiles) {
65+
const rootFilesSet = new Set(
66+
compilerOptionOverrides.rootFiles.map((file) => canonicalizePath(toPosixPath(file))),
67+
);
68+
for (const file of originalRootNames) {
69+
if (/\.d\.[cm]?ts$/.test(file)) {
70+
rootFilesSet.add(canonicalizePath(toPosixPath(file)));
71+
}
72+
}
73+
rootNames = [...rootFilesSet];
74+
}
75+
6376
const { compilerOptions, warnings } = transformCompilerOptions(
6477
ts,
6578
originalCompilerOptions,

packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export interface CompilerPluginOptions {
5757
externalRuntimeStyles?: boolean;
5858
instrumentForCoverage?: (request: string) => boolean;
5959
templateUpdates?: Map<string, string>;
60+
rootFiles?: string[];
6061
}
6162

6263
// eslint-disable-next-line max-lines-per-function
@@ -325,17 +326,21 @@ export function createCompilerPlugin(
325326
instrumentForCoverage: !!pluginOptions.instrumentForCoverage,
326327
includeTestMetadata: !!pluginOptions.includeTestMetadata,
327328
customConditions: build.initialOptions.conditions,
329+
rootFiles: pluginOptions.rootFiles,
328330
},
329331
);
332+
330333
if (initializationResult.warnings?.length) {
331334
setupWarnings?.push(...initializationResult.warnings);
332335
}
336+
333337
angularCompilationContext.setCompilerOptions(initializationResult.compilerOptions);
334338
shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
335339
useTypeScriptTranspilation =
336340
!!initializationResult.compilerOptions['_useTypeScriptTranspilation'];
337341
referencedFiles = initializationResult.referencedFiles;
338342
externalStylesheets = initializationResult.externalStylesheets;
343+
339344
if (initializationResult.templateUpdates) {
340345
// Propagate any template updates
341346
initializationResult.templateUpdates.forEach((value, key) =>

packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function createCompilerPluginOptions(
2828
externalRuntimeStyles,
2929
instrumentForCoverage,
3030
optimizationOptions,
31+
rootFiles,
3132
} = options;
3233
const incremental = !!options.watch;
3334

@@ -45,5 +46,6 @@ export function createCompilerPluginOptions(
4546
instrumentForCoverage,
4647
templateUpdates,
4748
includeTestMetadata: !optimizationOptions.scripts,
49+
rootFiles,
4850
};
4951
}

0 commit comments

Comments
 (0)