@@ -20,10 +20,14 @@ export interface TransformedConfiguration {
2020 rootNames : string [ ] ;
2121 errors : ts . Diagnostic [ ] ;
2222 warnings : PartialMessage [ ] ;
23+ extendedConfigFiles ?: readonly string [ ] ;
2324}
2425
2526export abstract class TypeScriptCompilation extends AngularCompilation {
2627 static #angularCompilerCliModule?: typeof ng ;
28+ #cachedConfiguration?: TransformedConfiguration ;
29+ #cachedRootFiles?: readonly string [ ] ;
30+ readonly #extendedConfigCache = new Map < string , ts . ExtendedConfigCacheEntry > ( ) ;
2731
2832 static async loadCompilerCli ( ) : Promise < typeof ng > {
2933 TypeScriptCompilation . #angularCompilerCliModule ??= await import ( '@angular/compiler-cli' ) ;
@@ -36,29 +40,55 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
3640 compilerOptionOverrides ?: CompilerOptionOverrides ,
3741 buildType : 'application' | 'library' = 'application' ,
3842 ) : Promise < TransformedConfiguration > {
39- const { readConfiguration } = await TypeScriptCompilation . loadCompilerCli ( ) ;
43+ // When `rootFiles` are explicitly provided (e.g., library builder), avoid re-parsing `tsconfig.json`
44+ // and walking the project directory tree via `readConfiguration` on every watch rebuild (~200-350ms on large libraries).
45+ const currentRootFiles = compilerOptionOverrides ?. rootFiles ;
46+ if (
47+ currentRootFiles &&
48+ this . #cachedConfiguration &&
49+ this . #cachedRootFiles &&
50+ this . #cachedRootFiles. length === currentRootFiles . length &&
51+ this . #cachedRootFiles. every ( ( file , index ) => file === currentRootFiles [ index ] )
52+ ) {
53+ return this . #cachedConfiguration;
54+ }
55+
56+ // TODO(alanagius): remove casting when @angular/compiler-cli exports the correct typings.
57+ const { readConfiguration } = ( await TypeScriptCompilation . loadCompilerCli ( ) ) as typeof ng & {
58+ readConfiguration (
59+ project : string ,
60+ existingOptions ?: ng . CompilerOptions ,
61+ host ?: unknown ,
62+ extendedConfigCache ?: Map < string , ts . ExtendedConfigCacheEntry > ,
63+ ) : ng . ParsedConfiguration ;
64+ } ;
4065
4166 const {
4267 options : originalCompilerOptions ,
4368 rootNames : originalRootNames ,
4469 errors,
4570 } = 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- } ) ,
71+ readConfiguration (
72+ tsconfig ,
73+ {
74+ // Angular specific configuration defaults and overrides to ensure a functioning compilation.
75+ suppressOutputPathCheck : true ,
76+ outDir : undefined ,
77+ sourceMap : false ,
78+ declaration : false ,
79+ declarationMap : false ,
80+ allowEmptyCodegenFiles : false ,
81+ annotationsAs : 'decorators' ,
82+ enableResourceInlining : false ,
83+ supportTestBed : false ,
84+ supportJitMode : false ,
85+ // Disable removing of comments as TS is quite aggressive with these and can
86+ // remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */.
87+ removeComments : false ,
88+ } ,
89+ undefined ,
90+ this . #extendedConfigCache,
91+ ) ,
6292 ) ;
6393
6494 let rootNames = originalRootNames ;
@@ -82,19 +112,53 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
82112 buildType ,
83113 ) ;
84114
85- return {
115+ const config : TransformedConfiguration = {
86116 compilerOptions,
87117 rootNames,
88118 errors,
89119 warnings,
120+ extendedConfigFiles : [ ...this . #extendedConfigCache. keys ( ) ] ,
90121 } ;
122+
123+ if ( config . extendedConfigFiles ?. length && currentRootFiles ?. length ) {
124+ this . #cachedConfiguration = config ;
125+ this . #cachedRootFiles = currentRootFiles ;
126+ }
127+
128+ return config ;
91129 }
92130
93131 protected readonly sourceFiles = new Map < string , ts . SourceFile > ( ) ;
94132
95133 protected invalidateFiles ( files : Iterable < string > ) : void {
96134 for ( const file of files ) {
97- this . sourceFiles . delete ( toPosixPath ( file ) ) ;
135+ const posixFile = toPosixPath ( file ) ;
136+ this . sourceFiles . delete ( posixFile ) ;
137+
138+ if ( ! this . #extendedConfigCache. size ) {
139+ continue ;
140+ }
141+
142+ let cacheKey : string | undefined ;
143+ if ( this . #extendedConfigCache. has ( posixFile ) ) {
144+ cacheKey = posixFile ;
145+ } else {
146+ // Check with lowercased key because TypeScript lowercases the keys
147+ // of the extended config cache on case-insensitive operating systems.
148+ const lowerCasedPosixFile = posixFile . toLowerCase ( ) ;
149+ if ( this . #extendedConfigCache. has ( lowerCasedPosixFile ) ) {
150+ cacheKey = lowerCasedPosixFile ;
151+ }
152+ }
153+
154+ if ( ! cacheKey ) {
155+ continue ;
156+ }
157+
158+ // If a tsconfig changes, we need to re-read the configuration.
159+ this . #cachedConfiguration = undefined ;
160+ this . #cachedRootFiles = undefined ;
161+ this . #extendedConfigCache. delete ( cacheKey ) ;
98162 }
99163 }
100164
0 commit comments