Skip to content

Commit 3317656

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 an `excludeFiles` option that excludes test files not matched by the include patterns from the TypeScript program's root files, while preserving ambient declarations and application source files. Fixes #34089
1 parent cbac34a commit 3317656

9 files changed

Lines changed: 83 additions & 7 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 exclude from the TypeScript compilation root names.
144+
*/
145+
excludeFiles?: 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+
excludeFiles,
450456
externalPackages,
451457
namedChunks,
452458
budgets,
@@ -494,6 +500,7 @@ export async function normalizeOptions(
494500
workspaceRoot,
495501
entryPoints,
496502
disableCodeSplitting,
503+
excludeFiles: excludeFiles?.map((file: string) => path.resolve(workspaceRoot, file)),
497504
optimizationOptions,
498505
outputOptions,
499506
outExtension,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export async function normalizeOptions(
120120
// Target/configuration specified options
121121
buildTarget,
122122
include: options.include ?? ['**/*.spec.ts'],
123+
hasExplicitInclude: options.include !== undefined,
123124
exclude: options.exclude,
124125
filter,
125126
runnerName: runner ?? Runner.Vitest,

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { createProjectResolver } from '../../../../utils/resolve-project';
1717
import type { ApplicationBuilderInternalOptions } from '../../../application/options';
1818
import { OutputHashing } from '../../../application/schema';
1919
import { NormalizedUnitTestBuilderOptions } from '../../options';
20-
import { findTests, getTestEntrypoints } from '../../test-discovery';
20+
import { TEST_FILE_INFIXES, findTests, getTestEntrypoints } from '../../test-discovery';
2121
import { RunnerOptions } from '../api';
2222

2323
/**
@@ -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,20 @@ export async function getVitestBuildOptions(
217225
removeTestExtension: true,
218226
});
219227

220-
if (options.setupFiles?.length) {
221-
const setupEntryPoints = getTestEntrypoints(options.setupFiles, {
228+
let excludeFiles: string[] | undefined;
229+
if (options.hasExplicitInclude) {
230+
const allTestFiles = await findTests(
231+
TEST_FILE_INFIXES.map((infix) => `**/*${infix}.@(ts|tsx)`),
232+
exclude,
233+
workspaceRoot,
234+
projectSourceRoot,
235+
);
236+
const testFilesSet = new Set(testFiles);
237+
excludeFiles = allTestFiles.filter((file) => !testFilesSet.has(file));
238+
}
239+
240+
if (setupFiles?.length) {
241+
const setupEntryPoints = getTestEntrypoints(setupFiles, {
222242
projectSourceRoot,
223243
workspaceRoot,
224244
removeTestExtension: false,
@@ -258,6 +278,7 @@ export async function getVitestBuildOptions(
258278
optimization: false,
259279
namedChunks: false,
260280
entryPoints,
281+
excludeFiles,
261282
// Vitest's Node-based module loading emulation (vite-node) is not fully spec compliant and lacks
262283
// live ESM bindings across chunk boundaries. This can cause uninitialized exports or break mocking.
263284
// Disabling code splitting avoids shared chunks, but increases build and coverage memory/time.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { toPosixPath } from '../../utils/path';
1717
* An array of file infix notations that identify a file as a test file.
1818
* For example, `.spec` in `app.component.spec.ts`.
1919
*/
20-
const TEST_FILE_INFIXES = ['.spec', '.test'];
20+
export const TEST_FILE_INFIXES = ['.spec', '.test'];
2121

2222
/** Maximum length for a generated test entrypoint name. */
2323
const MAX_FILENAME_LENGTH = 128;

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+
excludeFiles?: string[];
2324
}
2425

2526
export function transformCompilerOptions(

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

Lines changed: 14 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,18 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
6060
}),
6161
);
6262

63+
let rootNames = originalRootNames;
64+
if (compilerOptionOverrides?.excludeFiles?.length) {
65+
const excludeFilesSet = new Set(
66+
compilerOptionOverrides.excludeFiles.map((file) => canonicalizePath(toPosixPath(file))),
67+
);
68+
rootNames = originalRootNames.filter((file) => {
69+
const normalizedFile = canonicalizePath(toPosixPath(file));
70+
71+
return !excludeFilesSet.has(normalizedFile);
72+
});
73+
}
74+
6375
const { compilerOptions, warnings } = transformCompilerOptions(
6476
ts,
6577
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+
excludeFiles?: 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+
excludeFiles: pluginOptions.excludeFiles,
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+
excludeFiles,
3132
} = options;
3233
const incremental = !!options.watch;
3334

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

0 commit comments

Comments
 (0)