diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts index 71eacd562259..2d001dfeff7c 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts @@ -158,11 +158,61 @@ describe('AngularCompilation', () => { suppressOutputPathCheck: true, outDir: undefined, }), + undefined, + jasmine.any(Map), ); expect(result.rootNames).toEqual(['/src/main.ts']); expect(result.compilerOptions.target).toBe(ts.ScriptTarget.ES2022); expect(result.compilerOptions.inlineSources).toBe(true); expect(result.warnings.length).toBeGreaterThan(0); + expect(result.tsConfigFiles).toEqual(['tsconfig.json']); + }); + + it('passes extendedConfigCache to readConfiguration and invalidates entries on config change', async () => { + const compilation = new MockTypeScriptCompilation(); + let passedCache: Map | undefined; + const mockReadConfig = jasmine + .createSpy('readConfiguration') + .and.callFake((_project, _options, _host, extendedConfigCache) => { + passedCache = extendedConfigCache; + extendedConfigCache?.set( + '/path/to/tsconfig.base.json', + {} as ts.ExtendedConfigCacheEntry, + ); + + return { + options: { target: ts.ScriptTarget.ES2020 }, + rootNames: ['/src/main.ts'], + errors: [], + }; + }); + spyOn(TypeScriptCompilation, 'loadCompilerCli').and.resolveTo({ + readConfiguration: mockReadConfig, + } as unknown as typeof import('@angular/compiler-cli')); + + const result1 = await compilation.testLoadConfiguration('tsconfig.json'); + expect(mockReadConfig).toHaveBeenCalledTimes(1); + expect(passedCache).toBeDefined(); + expect(passedCache?.has('/path/to/tsconfig.base.json')).toBeTrue(); + expect(result1.tsConfigFiles).toEqual(['tsconfig.json', '/path/to/tsconfig.base.json']); + + // Invalidation of non-config file should keep extended config cache intact + await compilation.update?.(new Set(['/src/main.ts'])); + expect(passedCache?.has('/path/to/tsconfig.base.json')).toBeTrue(); + + // Invalidation of extended tsconfig file should remove it from cache + await compilation.update?.(new Set(['/path/to/tsconfig.base.json'])); + expect(passedCache?.has('/path/to/tsconfig.base.json')).toBeFalse(); + + // Invalidation with different casing should also remove it from cache + passedCache?.set('/path/to/tsconfig.base.json', {} as ts.ExtendedConfigCacheEntry); + await compilation.update?.(new Set(['/PATH/TO/TSCONFIG.BASE.JSON'])); + expect(passedCache?.has('/path/to/tsconfig.base.json')).toBeFalse(); + + // Subsequent configuration load re-uses the same cache instance + await compilation.testLoadConfiguration('tsconfig.json'); + expect(mockReadConfig).toHaveBeenCalledTimes(2); + expect(mockReadConfig.calls.argsFor(1)[3]).toBe(passedCache); }); }); diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts index 9381812a74ce..6f45199d1421 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -77,6 +77,7 @@ export class AotCompilation extends TypeScriptCompilation { rootNames, errors: configurationDiagnostics, warnings, + tsConfigFiles, } = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType); const useTypeScriptTranspilation = @@ -209,28 +210,31 @@ export class AotCompilation extends TypeScriptCompilation { const componentResourcesDependencies = new Map(); // Get all files referenced in the TypeScript/Angular program including component resources - const referencedFiles = typeScriptProgram - .getSourceFiles() - .filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile)) - .flatMap((sourceFile) => { - const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile); - componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies); - // Also invalidate Angular diagnostics for a source file if component resources are modified - if (this.#state && hostOptions.modifiedFiles?.size) { - for (const resourceDependency of resourceDependencies) { - if ( - hostOptions.modifiedFiles.has(resourceDependency) && - !/\.(?:css|scss|sass|less)$/i.test(resourceDependency) - ) { - this.#state.diagnosticCache.delete(sourceFile); - // Also mark as affected in case changed template affects diagnostics - affectedFiles.add(sourceFile); + const referencedFiles = [ + ...tsConfigFiles, + ...typeScriptProgram + .getSourceFiles() + .filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile)) + .flatMap((sourceFile) => { + const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile); + componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies); + // Also invalidate Angular diagnostics for a source file if component resources are modified + if (this.#state && hostOptions.modifiedFiles?.size) { + for (const resourceDependency of resourceDependencies) { + if ( + hostOptions.modifiedFiles.has(resourceDependency) && + !/\.(?:css|scss|sass|less)$/i.test(resourceDependency) + ) { + this.#state.diagnosticCache.delete(sourceFile); + // Also mark as affected in case changed template affects diagnostics + affectedFiles.add(sourceFile); + } } } - } - return [sourceFile.fileName, ...resourceDependencies]; - }); + return [sourceFile.fileName, ...resourceDependencies]; + }), + ]; this.#state = new AngularCompilationState( angularProgram, diff --git a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts index 773279e236a9..ec88987369b0 100644 --- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts @@ -55,6 +55,7 @@ export class JitCompilation extends TypeScriptCompilation { rootNames, errors: configurationDiagnostics, warnings, + tsConfigFiles, } = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType); if (hostOptions.modifiedFiles) { @@ -89,9 +90,10 @@ export class JitCompilation extends TypeScriptCompilation { createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)), ); - const referencedFiles = typeScriptProgram - .getSourceFiles() - .map((sourceFile) => sourceFile.fileName); + const referencedFiles = [ + ...tsConfigFiles, + ...typeScriptProgram.getSourceFiles().map((sourceFile) => sourceFile.fileName), + ]; return { compilerOptions, referencedFiles, warnings }; } diff --git a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts index 2595bc60f55a..5310219d2c1c 100644 --- a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts @@ -20,10 +20,12 @@ export interface TransformedConfiguration { rootNames: string[]; errors: ts.Diagnostic[]; warnings: PartialMessage[]; + tsConfigFiles: readonly string[]; } export abstract class TypeScriptCompilation extends AngularCompilation { static #angularCompilerCliModule?: typeof ng; + readonly #extendedConfigCache = new Map(); static async loadCompilerCli(): Promise { TypeScriptCompilation.#angularCompilerCliModule ??= await import('@angular/compiler-cli'); @@ -43,24 +45,31 @@ export abstract class TypeScriptCompilation extends AngularCompilation { rootNames: originalRootNames, errors, } = profileSync('NG_READ_CONFIG', () => - readConfiguration(tsconfig, { - // Angular specific configuration defaults and overrides to ensure a functioning compilation. - suppressOutputPathCheck: true, - outDir: undefined, - sourceMap: false, - declaration: false, - declarationMap: false, - allowEmptyCodegenFiles: false, - annotationsAs: 'decorators', - enableResourceInlining: false, - supportTestBed: false, - supportJitMode: false, - // Disable removing of comments as TS is quite aggressive with these and can - // remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */. - removeComments: false, - }), + readConfiguration( + tsconfig, + { + // Angular specific configuration defaults and overrides to ensure a functioning compilation. + suppressOutputPathCheck: true, + outDir: undefined, + sourceMap: false, + declaration: false, + declarationMap: false, + allowEmptyCodegenFiles: false, + annotationsAs: 'decorators', + enableResourceInlining: false, + supportTestBed: false, + supportJitMode: false, + // Disable removing of comments as TS is quite aggressive with these and can + // remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */. + removeComments: false, + }, + undefined, + this.#extendedConfigCache, + ), ); + const tsConfigFiles = [toPosixPath(tsconfig), ...this.#extendedConfigCache.keys()]; + let rootNames = originalRootNames; if (compilerOptionOverrides?.rootFiles?.length) { const rootFilesSet = new Set( @@ -87,6 +96,7 @@ export abstract class TypeScriptCompilation extends AngularCompilation { rootNames, errors, warnings, + tsConfigFiles, }; } @@ -94,7 +104,20 @@ export abstract class TypeScriptCompilation extends AngularCompilation { protected invalidateFiles(files: Iterable): void { for (const file of files) { - this.sourceFiles.delete(toPosixPath(file)); + const posixFile = toPosixPath(file); + this.sourceFiles.delete(posixFile); + + if (this.#extendedConfigCache.size === 0) { + continue; + } + + if (this.#extendedConfigCache.delete(posixFile)) { + continue; + } + + // Check with lowercased key because TypeScript lowercases the keys + // of the extended config cache on case-insensitive operating systems. + this.#extendedConfigCache.delete(posixFile.toLowerCase()); } }