Skip to content

Commit 5e05373

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 cea7feb commit 5e05373

7 files changed

Lines changed: 76 additions & 5 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ 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 files to restrict the TypeScript compilation root names to.
144+
*/
145+
rootFiles?: string[];
141146
}
142147

143148
/** Full set of options for `application` builder. */
@@ -447,6 +452,7 @@ export async function normalizeOptions(
447452
verbose,
448453
watch,
449454
progress = true,
455+
rootFiles,
450456
externalPackages,
451457
namedChunks,
452458
budgets,
@@ -494,6 +500,7 @@ export async function normalizeOptions(
494500
workspaceRoot,
495501
entryPoints,
496502
disableCodeSplitting,
503+
rootFiles: rootFiles?.map((file: string) => path.resolve(workspaceRoot, file)),
497504
optimizationOptions,
498505
outputOptions,
499506
outExtension,

‎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 '../../../utils/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?.length) {
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$/i.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
@@ -61,6 +61,7 @@ export interface CompilerPluginOptions {
6161
externalRuntimeStyles?: boolean;
6262
instrumentForCoverage?: (request: string) => boolean;
6363
templateUpdates?: Map<string, string>;
64+
rootFiles?: string[];
6465
}
6566

6667
// eslint-disable-next-line max-lines-per-function
@@ -329,17 +330,21 @@ export function createCompilerPlugin(
329330
instrumentForCoverage: !!pluginOptions.instrumentForCoverage,
330331
includeTestMetadata: !!pluginOptions.includeTestMetadata,
331332
customConditions: build.initialOptions.conditions,
333+
rootFiles: pluginOptions.rootFiles,
332334
},
333335
);
336+
334337
if (initializationResult.warnings?.length) {
335338
setupWarnings?.push(...initializationResult.warnings);
336339
}
340+
337341
angularCompilationContext.setCompilerOptions(initializationResult.compilerOptions);
338342
shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
339343
useTypeScriptTranspilation =
340344
!!initializationResult.compilerOptions['_useTypeScriptTranspilation'];
341345
referencedFiles = initializationResult.referencedFiles;
342346
externalStylesheets = initializationResult.externalStylesheets;
347+
343348
if (initializationResult.templateUpdates) {
344349
// Propagate any template updates
345350
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)