Skip to content

Commit dcf906a

Browse files
committed
perf(@angular/build): reuse extendedConfigCache in TypeScript compilation
Reusing TypeScript's extendedConfigCache across watch rebuilds eliminates redundant reading and re-parsing of extended tsconfig files from disk. The referenced configuration files are also tracked so that entries in the cache are invalidated when those files change on disk. See: angular/angular#71034
1 parent b975da4 commit dcf906a

4 files changed

Lines changed: 118 additions & 39 deletions

File tree

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,61 @@ describe('AngularCompilation', () => {
158158
suppressOutputPathCheck: true,
159159
outDir: undefined,
160160
}),
161+
undefined,
162+
jasmine.any(Map),
161163
);
162164
expect(result.rootNames).toEqual(['/src/main.ts']);
163165
expect(result.compilerOptions.target).toBe(ts.ScriptTarget.ES2022);
164166
expect(result.compilerOptions.inlineSources).toBe(true);
165167
expect(result.warnings.length).toBeGreaterThan(0);
168+
expect(result.tsConfigFiles).toEqual(['tsconfig.json']);
169+
});
170+
171+
it('passes extendedConfigCache to readConfiguration and invalidates entries on config change', async () => {
172+
const compilation = new MockTypeScriptCompilation();
173+
let passedCache: Map<string, ts.ExtendedConfigCacheEntry> | undefined;
174+
const mockReadConfig = jasmine
175+
.createSpy('readConfiguration')
176+
.and.callFake((_project, _options, _host, extendedConfigCache) => {
177+
passedCache = extendedConfigCache;
178+
extendedConfigCache?.set(
179+
'/path/to/tsconfig.base.json',
180+
{} as ts.ExtendedConfigCacheEntry,
181+
);
182+
183+
return {
184+
options: { target: ts.ScriptTarget.ES2020 },
185+
rootNames: ['/src/main.ts'],
186+
errors: [],
187+
};
188+
});
189+
spyOn(TypeScriptCompilation, 'loadCompilerCli').and.resolveTo({
190+
readConfiguration: mockReadConfig,
191+
} as unknown as typeof import('@angular/compiler-cli'));
192+
193+
const result1 = await compilation.testLoadConfiguration('tsconfig.json');
194+
expect(mockReadConfig).toHaveBeenCalledTimes(1);
195+
expect(passedCache).toBeDefined();
196+
expect(passedCache?.has('/path/to/tsconfig.base.json')).toBeTrue();
197+
expect(result1.tsConfigFiles).toEqual(['tsconfig.json', '/path/to/tsconfig.base.json']);
198+
199+
// Invalidation of non-config file should keep extended config cache intact
200+
await compilation.update?.(new Set(['/src/main.ts']));
201+
expect(passedCache?.has('/path/to/tsconfig.base.json')).toBeTrue();
202+
203+
// Invalidation of extended tsconfig file should remove it from cache
204+
await compilation.update?.(new Set(['/path/to/tsconfig.base.json']));
205+
expect(passedCache?.has('/path/to/tsconfig.base.json')).toBeFalse();
206+
207+
// Invalidation with different casing should also remove it from cache
208+
passedCache?.set('/path/to/tsconfig.base.json', {} as ts.ExtendedConfigCacheEntry);
209+
await compilation.update?.(new Set(['/PATH/TO/TSCONFIG.BASE.JSON']));
210+
expect(passedCache?.has('/path/to/tsconfig.base.json')).toBeFalse();
211+
212+
// Subsequent configuration load re-uses the same cache instance
213+
await compilation.testLoadConfiguration('tsconfig.json');
214+
expect(mockReadConfig).toHaveBeenCalledTimes(2);
215+
expect(mockReadConfig.calls.argsFor(1)[3]).toBe(passedCache);
166216
});
167217
});
168218

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

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export class AotCompilation extends TypeScriptCompilation {
7777
rootNames,
7878
errors: configurationDiagnostics,
7979
warnings,
80+
tsConfigFiles,
8081
} = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType);
8182

8283
const useTypeScriptTranspilation =
@@ -209,28 +210,31 @@ export class AotCompilation extends TypeScriptCompilation {
209210
const componentResourcesDependencies = new Map<string, string[]>();
210211

211212
// Get all files referenced in the TypeScript/Angular program including component resources
212-
const referencedFiles = typeScriptProgram
213-
.getSourceFiles()
214-
.filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile))
215-
.flatMap((sourceFile) => {
216-
const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile);
217-
componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies);
218-
// Also invalidate Angular diagnostics for a source file if component resources are modified
219-
if (this.#state && hostOptions.modifiedFiles?.size) {
220-
for (const resourceDependency of resourceDependencies) {
221-
if (
222-
hostOptions.modifiedFiles.has(resourceDependency) &&
223-
!/\.(?:css|scss|sass|less)$/i.test(resourceDependency)
224-
) {
225-
this.#state.diagnosticCache.delete(sourceFile);
226-
// Also mark as affected in case changed template affects diagnostics
227-
affectedFiles.add(sourceFile);
213+
const referencedFiles = [
214+
...tsConfigFiles,
215+
...typeScriptProgram
216+
.getSourceFiles()
217+
.filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile))
218+
.flatMap((sourceFile) => {
219+
const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile);
220+
componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies);
221+
// Also invalidate Angular diagnostics for a source file if component resources are modified
222+
if (this.#state && hostOptions.modifiedFiles?.size) {
223+
for (const resourceDependency of resourceDependencies) {
224+
if (
225+
hostOptions.modifiedFiles.has(resourceDependency) &&
226+
!/\.(?:css|scss|sass|less)$/i.test(resourceDependency)
227+
) {
228+
this.#state.diagnosticCache.delete(sourceFile);
229+
// Also mark as affected in case changed template affects diagnostics
230+
affectedFiles.add(sourceFile);
231+
}
228232
}
229233
}
230-
}
231234

232-
return [sourceFile.fileName, ...resourceDependencies];
233-
});
235+
return [sourceFile.fileName, ...resourceDependencies];
236+
}),
237+
];
234238

235239
this.#state = new AngularCompilationState(
236240
angularProgram,

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class JitCompilation extends TypeScriptCompilation {
5555
rootNames,
5656
errors: configurationDiagnostics,
5757
warnings,
58+
tsConfigFiles,
5859
} = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType);
5960

6061
if (hostOptions.modifiedFiles) {
@@ -89,9 +90,10 @@ export class JitCompilation extends TypeScriptCompilation {
8990
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
9091
);
9192

92-
const referencedFiles = typeScriptProgram
93-
.getSourceFiles()
94-
.map((sourceFile) => sourceFile.fileName);
93+
const referencedFiles = [
94+
...tsConfigFiles,
95+
...typeScriptProgram.getSourceFiles().map((sourceFile) => sourceFile.fileName),
96+
];
9597

9698
return { compilerOptions, referencedFiles, warnings };
9799
}

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

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ export interface TransformedConfiguration {
2020
rootNames: string[];
2121
errors: ts.Diagnostic[];
2222
warnings: PartialMessage[];
23+
tsConfigFiles: readonly string[];
2324
}
2425

2526
export abstract class TypeScriptCompilation extends AngularCompilation {
2627
static #angularCompilerCliModule?: typeof ng;
28+
readonly #extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>();
2729

2830
static async loadCompilerCli(): Promise<typeof ng> {
2931
TypeScriptCompilation.#angularCompilerCliModule ??= await import('@angular/compiler-cli');
@@ -43,24 +45,31 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
4345
rootNames: originalRootNames,
4446
errors,
4547
} = profileSync('NG_READ_CONFIG', () =>
46-
readConfiguration(tsconfig, {
47-
// Angular specific configuration defaults and overrides to ensure a functioning compilation.
48-
suppressOutputPathCheck: true,
49-
outDir: undefined,
50-
sourceMap: false,
51-
declaration: false,
52-
declarationMap: false,
53-
allowEmptyCodegenFiles: false,
54-
annotationsAs: 'decorators',
55-
enableResourceInlining: false,
56-
supportTestBed: false,
57-
supportJitMode: false,
58-
// Disable removing of comments as TS is quite aggressive with these and can
59-
// remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */.
60-
removeComments: false,
61-
}),
48+
readConfiguration(
49+
tsconfig,
50+
{
51+
// Angular specific configuration defaults and overrides to ensure a functioning compilation.
52+
suppressOutputPathCheck: true,
53+
outDir: undefined,
54+
sourceMap: false,
55+
declaration: false,
56+
declarationMap: false,
57+
allowEmptyCodegenFiles: false,
58+
annotationsAs: 'decorators',
59+
enableResourceInlining: false,
60+
supportTestBed: false,
61+
supportJitMode: false,
62+
// Disable removing of comments as TS is quite aggressive with these and can
63+
// remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */.
64+
removeComments: false,
65+
},
66+
undefined,
67+
this.#extendedConfigCache,
68+
),
6269
);
6370

71+
const tsConfigFiles = [toPosixPath(tsconfig), ...this.#extendedConfigCache.keys()];
72+
6473
let rootNames = originalRootNames;
6574
if (compilerOptionOverrides?.rootFiles?.length) {
6675
const rootFilesSet = new Set(
@@ -87,14 +96,28 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
8796
rootNames,
8897
errors,
8998
warnings,
99+
tsConfigFiles,
90100
};
91101
}
92102

93103
protected readonly sourceFiles = new Map<string, ts.SourceFile>();
94104

95105
protected invalidateFiles(files: Iterable<string>): void {
96106
for (const file of files) {
97-
this.sourceFiles.delete(toPosixPath(file));
107+
const posixFile = toPosixPath(file);
108+
this.sourceFiles.delete(posixFile);
109+
110+
if (this.#extendedConfigCache.size === 0) {
111+
continue;
112+
}
113+
114+
if (this.#extendedConfigCache.delete(posixFile)) {
115+
continue;
116+
}
117+
118+
// Check with lowercased key because TypeScript lowercases the keys
119+
// of the extended config cache on case-insensitive operating systems.
120+
this.#extendedConfigCache.delete(posixFile.toLowerCase());
98121
}
99122
}
100123

0 commit comments

Comments
 (0)