Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit fdcc98c

Browse filesBrowse files
mattlewis92dgp1130
authored andcommitted
perf(@angular/build): skip semantic affected-file walk when type checking is disabled
When type checking is disabled (`NG_BUILD_TYPE_CHECK=0`), the AOT build still ran a full semantic type-check as a side effect of computing the affected-files set, then discarded the result. `useTypeChecking` already suppresses the *consumption* of semantic diagnostics via the compiler-plugin `diagnoseFiles` mask, but the *production* -- the `findAffectedFiles` -> `getSemanticDiagnosticsOfNextAffectedFile` walk -- was never gated, so disabling type checking still paid for a full semantic pass whose results were never read. The affected set has three consumers: Angular template diagnostics (behind the `semantic` flag, already suppressed when type checking is off), the single-file vs whole-program diagnostics optimization (also diagnostics only), and the emit gate in `emitAffectedFiles()`. The emit gate only matters in the slow TypeScript emit path where cross-file types can change a file's JS; in the isolatedModules fast path emit is per-file/type-erased, so an empty set is safe. Gate the walk so it only runs when type checking is on OR the slow emit path is used, and skip it in the (type-check-off + fast-path) combination. (cherry picked from commit 2462709)
1 parent 19cda74 commit fdcc98c
Copy full SHA for fdcc98c

2 files changed

+110-3Lines changed: 110 additions & 3 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎packages/angular/build/src/tools/angular/compilation/aot-compilation.ts‎

Copy file name to clipboardExpand all lines: packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
+14-3Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type * as ng from '@angular/compiler-cli';
1010
import assert from 'node:assert';
1111
import { relative } from 'node:path';
1212
import ts from 'typescript';
13+
import { useTypeChecking } from '../../../utils/environment-options';
1314
import { profileAsync, profileSync } from '../../esbuild/profiling';
1415
import {
1516
AngularHostOptions,
@@ -175,9 +176,19 @@ export class AotCompilation extends AngularCompilation {
175176
}
176177
}
177178

178-
const affectedFiles = profileSync('NG_FIND_AFFECTED', () =>
179-
findAffectedFiles(typeScriptProgram, angularCompiler, usingBuildInfo),
180-
);
179+
// The affected files walk runs a full semantic type-check as a side effect
180+
// (via `getSemanticDiagnosticsOfNextAffectedFile`). Its result is only consumed to
181+
// scope Angular template diagnostics and to force re-emit of TS-affected files in the
182+
// slow TypeScript emit path. When type-checking is disabled, template/semantic
183+
// diagnostics are already suppressed at the consumption layer (see the compiler-plugin
184+
// `diagnoseFiles` mask), and in the isolatedModules fast path emit is per-file, so the
185+
// set is never read. Skip the walk in that case to avoid a discarded semantic pass.
186+
const affectedFiles =
187+
useTypeChecking || useTypeScriptTranspilation
188+
? profileSync('NG_FIND_AFFECTED', () =>
189+
findAffectedFiles(typeScriptProgram, angularCompiler, usingBuildInfo),
190+
)
191+
: new Set<ts.SourceFile>();
181192

182193
const componentResourcesDependencies = new Map<string, string[]>();
183194

Collapse file
+96Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import assert from 'node:assert/strict';
2+
import { readdir } from 'node:fs/promises';
3+
import { readFile, writeFile } from '../../utils/fs';
4+
import { execWithEnv, ng } from '../../utils/process';
5+
import { updateJsonFile } from '../../utils/project';
6+
7+
const OUTPUT_DIR = 'dist/test-project/browser';
8+
9+
async function readEmittedJs(): Promise<Map<string, string>> {
10+
const contents = new Map<string, string>();
11+
for (const file of (await readdir(OUTPUT_DIR)).sort()) {
12+
if (file.endsWith('.js')) {
13+
contents.set(file, await readFile(`${OUTPUT_DIR}/${file}`));
14+
}
15+
}
16+
17+
return contents;
18+
}
19+
20+
/**
21+
* Verifies that disabling type checking (`NG_BUILD_TYPE_CHECK=0`) does not change the
22+
* emitted JavaScript. When type checking is disabled, the AOT compilation skips the
23+
* semantic "affected files" walk in the isolatedModules fast path (its only remaining
24+
* consumers are diagnostics, which are already suppressed). The emitted output must remain
25+
* byte-for-byte identical to a type-checked build.
26+
*/
27+
export default async function () {
28+
// Disable the persistent disk cache so both builds are cold and independent, keeping the
29+
// comparison free of incremental state carried over between the two runs.
30+
await updateJsonFile('angular.json', (config) => {
31+
config.cli ??= {};
32+
config.cli.cache = { enabled: false };
33+
});
34+
35+
// A type-only cross-file dependency. The interface is used solely as a type (and in the
36+
// template via `user.name`), so it is fully erased from the emitted JS. This is exactly the
37+
// kind of cross-file type relationship that the affected-file walk would type-check.
38+
await writeFile(
39+
'src/app/user.model.ts',
40+
'export interface User {\n id: number;\n name: string;\n}\n',
41+
);
42+
43+
// Root component with both a template and the type-only dependency above.
44+
await writeFile(
45+
'src/app/app.ts',
46+
[
47+
`import { Component, signal } from '@angular/core';`,
48+
`import { RouterOutlet } from '@angular/router';`,
49+
`import type { User } from './user.model';`,
50+
``,
51+
`@Component({`,
52+
` selector: 'app-root',`,
53+
` imports: [RouterOutlet],`,
54+
` template: '<h1>Hello, {{ user.name }}</h1><router-outlet />',`,
55+
`})`,
56+
`export class App {`,
57+
` protected readonly title = signal('test-project');`,
58+
` protected readonly user: User = { id: 1, name: 'Angular' };`,
59+
`}`,
60+
``,
61+
].join('\n'),
62+
);
63+
64+
// Baseline: type checking enabled (default). The affected-file walk runs.
65+
await ng('build', '--configuration=development', '--output-hashing=none');
66+
const withTypeChecking = await readEmittedJs();
67+
68+
// Sanity check that the component (and therefore real emit) is present in the baseline,
69+
// so an all-empty comparison cannot pass trivially.
70+
assert.ok(
71+
[...withTypeChecking.values()].some((contents) => contents.includes('Angular')),
72+
'Expected the baseline build to emit the component.',
73+
);
74+
75+
// Type checking disabled: the affected-file walk is skipped in the isolatedModules fast
76+
// path. The emitted output must be byte-for-byte identical to the baseline.
77+
await execWithEnv('ng', ['build', '--configuration=development', '--output-hashing=none'], {
78+
...process.env,
79+
NG_BUILD_TYPE_CHECK: '0',
80+
});
81+
const withoutTypeChecking = await readEmittedJs();
82+
83+
assert.deepStrictEqual(
84+
[...withoutTypeChecking.keys()],
85+
[...withTypeChecking.keys()],
86+
'Disabling type checking must not change the set of emitted JS files.',
87+
);
88+
89+
for (const [file, baseline] of withTypeChecking) {
90+
assert.strictEqual(
91+
withoutTypeChecking.get(file),
92+
baseline,
93+
`Emitted output for "${file}" changed when type checking was disabled.`,
94+
);
95+
}
96+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.