@@ -76,54 +76,13 @@ export function readConfiguration(
7676 project : string ,
7777 existingOptions ?: api . CompilerOptions ,
7878 host : ConfigurationHost = getFileSystem ( ) ,
79+ extendedConfigCache = new Map < string , ts . ExtendedConfigCacheEntry > ( ) ,
7980) : ParsedConfiguration {
8081 try {
8182 const fs = getFileSystem ( ) ;
82-
83- const readConfigFile = ( configFile : string ) =>
84- ts . readConfigFile ( configFile , ( file ) => host . readFile ( host . resolve ( file ) ) ) ;
85- const readAngularCompilerOptions = (
86- configFile : string ,
87- parentOptions : NgCompilerOptions = { } ,
88- ) : NgCompilerOptions => {
89- const { config, error} = readConfigFile ( configFile ) ;
90-
91- if ( error ) {
92- // Errors are handled later on by 'parseJsonConfigFileContent'
93- return parentOptions ;
94- }
95-
96- // Note: In Google, `angularCompilerOptions` are stored in `bazelOptions`.
97- // This function typically doesn't run for actual Angular compilations, but
98- // tooling like Tsurge, or schematics may leverage this helper, so we account
99- // for this here.
100- const angularCompilerOptions =
101- config . angularCompilerOptions ?? config . bazelOptions ?. angularCompilerOptions ;
102-
103- // we are only interested into merging 'angularCompilerOptions' as
104- // other options like 'compilerOptions' are merged by TS
105- let existingNgCompilerOptions = { ...angularCompilerOptions , ...parentOptions } ;
106- if ( ! config . extends ) {
107- return existingNgCompilerOptions ;
108- }
109-
110- const extendsPaths : string [ ] =
111- typeof config . extends === 'string' ? [ config . extends ] : config . extends ;
112-
113- // Call readAngularCompilerOptions recursively to merge NG Compiler options
114- // Reverse the array so the overrides happen from right to left.
115- return [ ...extendsPaths ] . reverse ( ) . reduce ( ( prevOptions , extendsPath ) => {
116- const extendedConfigPath = getExtendedConfigPath ( configFile , extendsPath , host , fs ) ;
117-
118- return extendedConfigPath === null
119- ? prevOptions
120- : readAngularCompilerOptions ( extendedConfigPath , prevOptions ) ;
121- } , existingNgCompilerOptions ) ;
122- } ;
123-
12483 const { projectFile, basePath} = calcProjectFileAndBasePath ( project , host ) ;
125- const configFileName = host . resolve ( host . pwd ( ) , projectFile ) ;
126- const { config, error} = readConfigFile ( projectFile ) ;
84+ const parseConfigHost = createParseConfigHost ( host , fs ) ;
85+ const { config, error} = readConfigFile ( projectFile , host , fs , extendedConfigCache ) ;
12786
12887 if ( error ) {
12988 return {
@@ -138,31 +97,45 @@ export function readConfiguration(
13897 const existingCompilerOptions : api . CompilerOptions = {
13998 genDir : basePath ,
14099 basePath,
141- ...readAngularCompilerOptions ( configFileName ) ,
142100 ...existingOptions ,
143101 } ;
144102
145- const parseConfigHost = createParseConfigHost ( host , fs ) ;
146103 const {
147104 options,
148105 errors,
149106 fileNames : rootNames ,
150107 projectReferences,
151108 } = ts . parseJsonConfigFileContent (
109+ /* json */ config ,
110+ /* host */ parseConfigHost ,
111+ /* basePath */ basePath ,
112+ /* existingOptions */ existingCompilerOptions ,
113+ /* configFileName */ projectFile ,
114+ /* resolutionStack */ undefined ,
115+ /* extraFileExtensions */ undefined ,
116+ /* extendedConfigCache */ extendedConfigCache ,
117+ ) ;
118+
119+ const angularCompilerOptions = readAngularCompilerOptions (
120+ projectFile ,
152121 config ,
122+ host ,
123+ fs ,
153124 parseConfigHost ,
154- basePath ,
155- existingCompilerOptions ,
156- configFileName ,
125+ extendedConfigCache ,
157126 ) ;
158127
128+ Object . assign ( options , angularCompilerOptions , existingOptions ) ;
129+
159130 let emitFlags = api . EmitFlags . Default ;
160- if ( ! ( options [ 'skipMetadataEmit' ] || options [ 'flatModuleOutFile' ] ) ) {
131+ if ( ! options [ 'skipMetadataEmit' ] && ! options [ 'flatModuleOutFile' ] ) {
161132 emitFlags |= api . EmitFlags . Metadata ;
162133 }
134+
163135 if ( options [ 'skipTemplateCodegen' ] ) {
164136 emitFlags = emitFlags & ~ api . EmitFlags . Codegen ;
165137 }
138+
166139 return { project : projectFile , rootNames, projectReferences, options, errors, emitFlags} ;
167140 } catch ( e ) {
168141 const errors : ts . Diagnostic [ ] = [
@@ -180,6 +153,113 @@ export function readConfiguration(
180153 }
181154}
182155
156+ function readConfigFile (
157+ configFile : string ,
158+ host : ConfigurationHost ,
159+ fs : FileSystem ,
160+ extendedConfigCache ?: Map < string , ts . ExtendedConfigCacheEntry > ,
161+ ) : { config ?: any ; error ?: ts . Diagnostic } {
162+ const cacheKey = fs . isCaseSensitive ( ) ? configFile : configFile . toLowerCase ( ) ;
163+ const cacheEntry = extendedConfigCache ?. get ( cacheKey ) ;
164+
165+ if ( cacheEntry ) {
166+ return {
167+ config : cacheEntry . extendedConfig ?. raw ,
168+ error : ( cacheEntry . extendedResult as { parseDiagnostics ?: ts . Diagnostic [ ] } )
169+ . parseDiagnostics ?. [ 0 ] ,
170+ } ;
171+ }
172+
173+ return ts . readConfigFile ( configFile , ( file ) => host . readFile ( host . resolve ( file ) ) ) ;
174+ }
175+
176+ function readAngularCompilerOptions (
177+ configFile : string ,
178+ config : any ,
179+ host : ConfigurationHost ,
180+ fs : FileSystem ,
181+ parseConfigHost : ts . ParseConfigHost ,
182+ extendedConfigCache : Map < string , ts . ExtendedConfigCacheEntry > ,
183+ ) : NgCompilerOptions {
184+ const cacheKey = fs . isCaseSensitive ( ) ? configFile : configFile . toLowerCase ( ) ;
185+ const cacheEntry = extendedConfigCache . get ( cacheKey ) ;
186+ const cachedExtendedConfig = cacheEntry ?. extendedConfig as
187+ | ( ts . ExtendedConfigCacheEntry [ 'extendedConfig' ] & {
188+ angularCompilerOptions ?: NgCompilerOptions ;
189+ } )
190+ | undefined ;
191+
192+ // If the merged angularCompilerOptions for this config are already cached, reuse them.
193+ if ( cachedExtendedConfig ?. angularCompilerOptions ) {
194+ return cachedExtendedConfig . angularCompilerOptions ;
195+ }
196+
197+ // Note: In Google, `angularCompilerOptions` are stored in `bazelOptions`.
198+ // This function typically doesn't run for actual Angular compilations, but
199+ // tooling like Tsurge, or schematics may leverage this helper, so we account
200+ // for this here.
201+ const angularCompilerOptions =
202+ config . angularCompilerOptions ?? config . bazelOptions ?. angularCompilerOptions ?? { } ;
203+
204+ if ( ! config . extends ) {
205+ if ( cachedExtendedConfig ) {
206+ cachedExtendedConfig . angularCompilerOptions = angularCompilerOptions ;
207+ }
208+ return angularCompilerOptions ;
209+ }
210+
211+ const resolvedExtendedConfigPath = cachedExtendedConfig ?. extendedConfigPath ;
212+
213+ const extendsPaths : string [ ] = resolvedExtendedConfigPath
214+ ? typeof resolvedExtendedConfigPath === 'string'
215+ ? [ resolvedExtendedConfigPath ]
216+ : resolvedExtendedConfigPath
217+ : typeof config . extends === 'string'
218+ ? [ config . extends ]
219+ : Array . isArray ( config . extends )
220+ ? config . extends
221+ : [ ] ;
222+
223+ // Recursively merge extended configurations from base to leaf (left-to-right).
224+ const inheritedOptions = extendsPaths . reduce ( ( prevOptions , extendsPath ) => {
225+ const extendedConfigPath = resolvedExtendedConfigPath
226+ ? absoluteFrom ( extendsPath )
227+ : getExtendedConfigPath ( configFile , extendsPath , host , fs , parseConfigHost ) ;
228+ if ( extendedConfigPath === null ) {
229+ return prevOptions ;
230+ }
231+
232+ const { config : extendedConfig , error} = readConfigFile (
233+ extendedConfigPath ,
234+ host ,
235+ fs ,
236+ extendedConfigCache ,
237+ ) ;
238+
239+ if ( error || ! extendedConfig ) {
240+ return prevOptions ;
241+ }
242+
243+ const options = readAngularCompilerOptions (
244+ extendedConfigPath ,
245+ extendedConfig ,
246+ host ,
247+ fs ,
248+ parseConfigHost ,
249+ extendedConfigCache ,
250+ ) ;
251+
252+ return { ...prevOptions , ...options } ;
253+ } , { } as NgCompilerOptions ) ;
254+
255+ const mergedOptions = { ...inheritedOptions , ...angularCompilerOptions } ;
256+ if ( cachedExtendedConfig ) {
257+ cachedExtendedConfig . angularCompilerOptions = mergedOptions ;
258+ }
259+
260+ return mergedOptions ;
261+ }
262+
183263function createParseConfigHost ( host : ConfigurationHost , fs = getFileSystem ( ) ) : ts . ParseConfigHost {
184264 return {
185265 fileExists : host . exists . bind ( host ) ,
@@ -194,41 +274,25 @@ function getExtendedConfigPath(
194274 extendsValue : string ,
195275 host : ConfigurationHost ,
196276 fs : FileSystem ,
277+ parseConfigHost : ts . ParseConfigHost ,
197278) : AbsoluteFsPath | null {
198- const result = getExtendedConfigPathWorker ( configFile , extendsValue , host , fs ) ;
199- if ( result !== null ) {
200- return result ;
201- }
202-
203- // Try to resolve the paths with a json extension append a json extension to the file in case if
204- // it is missing and the resolution failed. This is to replicate TypeScript behaviour, see:
205- // https://github.com/microsoft/TypeScript/blob/294a5a7d784a5a95a8048ee990400979a6bc3a1c/src/compiler/commandLineParser.ts#L2806
206- return getExtendedConfigPathWorker ( configFile , `${ extendsValue } .json` , host , fs ) ;
207- }
208-
209- function getExtendedConfigPathWorker (
210- configFile : string ,
211- extendsValue : string ,
212- host : ConfigurationHost ,
213- fs : FileSystem ,
214- ) : AbsoluteFsPath | null {
215- if ( extendsValue . startsWith ( '.' ) || fs . isRooted ( extendsValue ) ) {
216- const extendedConfigPath = host . resolve ( host . dirname ( configFile ) , extendsValue ) ;
217- if ( host . exists ( extendedConfigPath ) ) {
218- return extendedConfigPath ;
219- }
220- } else {
221- const parseConfigHost = createParseConfigHost ( host , fs ) ;
222-
223- // Path isn't a rooted or relative path, resolve like a module.
224- const { resolvedModule} = ts . nodeModuleNameResolver (
225- extendsValue ,
226- configFile ,
227- { moduleResolution : ts . ModuleResolutionKind . NodeNext , resolveJsonModule : true } ,
228- parseConfigHost ,
229- ) ;
230- if ( resolvedModule ) {
231- return absoluteFrom ( resolvedModule . resolvedFileName ) ;
279+ for ( const candidate of [ extendsValue , `${ extendsValue } .json` ] ) {
280+ if ( candidate [ 0 ] === '.' || fs . isRooted ( candidate ) ) {
281+ const extendedConfigPath = host . resolve ( host . dirname ( configFile ) , candidate ) ;
282+ if ( host . exists ( extendedConfigPath ) ) {
283+ return extendedConfigPath ;
284+ }
285+ } else {
286+ // Path isn't a rooted or relative path, resolve like a module.
287+ const { resolvedModule} = ts . nodeModuleNameResolver (
288+ candidate ,
289+ configFile ,
290+ { moduleResolution : ts . ModuleResolutionKind . NodeNext , resolveJsonModule : true } ,
291+ parseConfigHost ,
292+ ) ;
293+ if ( resolvedModule ) {
294+ return absoluteFrom ( resolvedModule . resolvedFileName ) ;
295+ }
232296 }
233297 }
234298
0 commit comments