Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ interface InternalOptions {
disableCodeSplitting?: boolean;

/**
* An array of files to restrict the TypeScript compilation root names to.
* An array of files to exclude from the TypeScript compilation root names.
*/
rootFiles?: string[];
excludeRootFiles?: string[];
}

/** Full set of options for `application` builder. */
Expand Down Expand Up @@ -456,7 +456,7 @@ export async function normalizeOptions(
verbose,
watch,
progress = true,
rootFiles,
excludeRootFiles,
externalPackages,
namedChunks,
budgets,
Expand Down Expand Up @@ -504,7 +504,7 @@ export async function normalizeOptions(
workspaceRoot,
entryPoints,
disableCodeSplitting,
rootFiles: rootFiles?.map((file: string) => path.resolve(workspaceRoot, file)),
excludeRootFiles: excludeRootFiles?.map((file: string) => path.resolve(workspaceRoot, file)),
optimizationOptions,
outputOptions,
outExtension,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { type NormalizedUnitTestBuilderOptions, injectTestingPolyfills } from '.
import { findTests, getTestEntrypoints } from '../../test-discovery';
import { RunnerOptions } from '../api';

const DEFAULT_TEST_PATTERNS = ['**/*.spec.ts', '**/*.test.ts'];

/**
* Creates the virtual file contents to initialize the Angular testing environment (TestBed).
*
Expand Down Expand Up @@ -210,14 +212,7 @@ export async function getVitestBuildOptions(
removeTestExtension: true,
});

const rootFiles = [...testFiles];
if (providersFile) {
rootFiles.push(providersFile);
}

if (setupFiles?.length) {
rootFiles.push(...setupFiles);

const setupEntryPoints = getTestEntrypoints(setupFiles, {
projectSourceRoot,
workspaceRoot,
Expand All @@ -230,6 +225,34 @@ export async function getVitestBuildOptions(
}
}

let excludeRootFiles: string[] | undefined;
const isDefaultInclude =
exclude.length === 0 &&
include.length === DEFAULT_TEST_PATTERNS.length &&
include.every((val: string) => DEFAULT_TEST_PATTERNS.includes(val));

if (!isDefaultInclude) {
const allTestFiles = await findTests(
DEFAULT_TEST_PATTERNS,
[],
workspaceRoot,
projectSourceRoot,
);
const testFilesSet = new Set(testFiles);
if (providersFile) {
testFilesSet.add(toPosixPath(providersFile));
}

if (setupFiles?.length) {
for (const setupFile of setupFiles) {
testFilesSet.add(toPosixPath(setupFile));
}
}

const unselectedTestFiles = allTestFiles.filter((file) => !testFilesSet.has(file));
excludeRootFiles = unselectedTestFiles.length > 0 ? unselectedTestFiles : undefined;
}

// The Angular compiler facade must be loaded in a dedicated setup file before TestBed initialization.
// This ensures the compiler facade is published before 'init-testbed' or any shared code-split chunks
// (such as those from a configured 'providersFile') are evaluated.
Expand Down Expand Up @@ -267,7 +290,7 @@ export async function getVitestBuildOptions(
optimization: false,
namedChunks: false,
entryPoints,
rootFiles,
excludeRootFiles,
// Vitest's Node-based module loading emulation (vite-node) is not fully spec compliant and lacks
// live ESM bindings across chunk boundaries. This can cause uninitialized exports or break mocking.
// Disabling code splitting avoids shared chunks, but increases build and coverage memory/time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,77 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
});

it('should compile NgModule declared components when using include in an NgModule app', async () => {
await harness.writeFiles({
'src/tsconfig.spec.json': JSON.stringify({
extends: '../tsconfig.json',
compilerOptions: {
outDir: '../out-tsc/spec',
types: ['vitest/globals'],
},
include: ['**/*.d.ts', '**/*.ts'],
}),
'src/app/app.module.ts': `
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';

@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, RouterModule],
})
export class AppModule {}
`,
'src/app/app.component.ts': `
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
standalone: false,
templateUrl: './app.component.html',
})
export class AppComponent {}
`,
'src/app/app.component.html': '<router-outlet />',
'src/app/app.component.spec.ts': `
import { TestBed } from '@angular/core/testing';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterModule.forRoot([])],
declarations: [AppComponent],
}).compileComponents();
});

it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});
`,
'src/app/broken.service.spec.ts': `
// This test has a TypeScript type error that would fail compilation if compiled
const invalidNumber: number = 'not a number';
describe('BrokenService', () => {
it('should fail compilation', () => {
expect(invalidNumber).toBe(1);
});
});`,
});

harness.useTarget('test', {
...BASE_OPTIONS,
include: ['src/app/app.component.spec.ts'],
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,68 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
expect(result?.success).toBeFalse();
expectLog(logs, `The specified tsConfig file 'random/tsconfig.spec.json' does not exist.`);
});

it('should compile NgModule declared components when tsConfig includes application files', async () => {
await harness.writeFiles({
'src/tsconfig.spec.json': JSON.stringify({
extends: '../tsconfig.json',
compilerOptions: {
outDir: '../out-tsc/spec',
types: ['vitest/globals'],
},
include: ['**/*.d.ts', '**/*.ts'],
}),
'src/app/app.module.ts': `
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';

@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, RouterModule],
})
export class AppModule {}
`,
'src/app/app.component.ts': `
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
standalone: false,
templateUrl: './app.component.html',
})
export class AppComponent {}
`,
'src/app/app.component.html': '<router-outlet />',
'src/app/app.component.spec.ts': `
import { TestBed } from '@angular/core/testing';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterModule.forRoot([])],
declarations: [AppComponent],
}).compileComponents();
});

it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});
`,
});

harness.useTarget('test', {
...BASE_OPTIONS,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface CompilerOptionOverrides {
includeTestMetadata?: boolean;
customConditions?: string[];
rootFiles?: string[];
excludeRootFiles?: string[];
declarationMap?: boolean;
compilationMode?: 'full' | 'partial';
paths?: Record<string, string[]>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,24 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
const rootFilesSet = new Set(
compilerOptionOverrides.rootFiles.map((file) => canonicalizePath(toPosixPath(file))),
);

for (const file of originalRootNames) {
if (/\.d\.[cm]?ts$/i.test(file)) {
rootFilesSet.add(canonicalizePath(toPosixPath(file)));
}
}

rootNames = [...rootFilesSet];
}

if (compilerOptionOverrides?.excludeRootFiles?.length) {
const excludeSet = new Set(
compilerOptionOverrides.excludeRootFiles.map((file) => canonicalizePath(toPosixPath(file))),
);

rootNames = rootNames.filter((file) => !excludeSet.has(canonicalizePath(toPosixPath(file))));
}

const { compilerOptions, warnings } = transformCompilerOptions(
ts,
originalCompilerOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface CompilerPluginOptions {
externalRuntimeStyles?: boolean;
instrumentForCoverage?: (request: string) => boolean;
templateUpdates?: Map<string, string>;
rootFiles?: string[];
excludeRootFiles?: string[];
}

// eslint-disable-next-line max-lines-per-function
Expand Down Expand Up @@ -330,7 +330,7 @@ export function createCompilerPlugin(
instrumentForCoverage: !!pluginOptions.instrumentForCoverage,
includeTestMetadata: !!pluginOptions.includeTestMetadata,
customConditions: build.initialOptions.conditions,
rootFiles: pluginOptions.rootFiles,
excludeRootFiles: pluginOptions.excludeRootFiles,
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function createCompilerPluginOptions(
externalRuntimeStyles,
instrumentForCoverage,
optimizationOptions,
rootFiles,
excludeRootFiles,
} = options;
const incremental = !!options.watch;

Expand All @@ -46,6 +46,6 @@ export function createCompilerPluginOptions(
instrumentForCoverage,
templateUpdates,
includeTestMetadata: !optimizationOptions.scripts,
rootFiles,
excludeRootFiles,
};
}
Loading