| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent dcf4ab8 commit fe9ebf5
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -76,54 +76,13 @@ export function readConfiguration( | |||
| 76 | 76 | project: string, | |
| 77 | 77 | existingOptions?: api.CompilerOptions, | |
| 78 | 78 | host: ConfigurationHost = getFileSystem(), | |
| 79 | + extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>(), | ||
| 79 | 80 | ): ParsedConfiguration { | |
| 80 | 81 | try { | |
| 81 | 82 | 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 | - | ||
| 124 | 83 | 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); | ||
| 127 | 86 | ||
| 128 | 87 | if (error) { | |
| 129 | 88 | return { | |
@@ -138,31 +97,45 @@ export function readConfiguration( | |||
| 138 | 97 | const existingCompilerOptions: api.CompilerOptions = { | |
| 139 | 98 | genDir: basePath, | |
| 140 | 99 | basePath, | |
| 141 | - ...readAngularCompilerOptions(configFileName), | ||
| 142 | 100 | ...existingOptions, | |
| 143 | 101 | }; | |
| 144 | 102 | ||
| 145 | - const parseConfigHost = createParseConfigHost(host, fs); | ||
| 146 | 103 | const { | |
| 147 | 104 | options, | |
| 148 | 105 | errors, | |
| 149 | 106 | fileNames: rootNames, | |
| 150 | 107 | projectReferences, | |
| 151 | 108 | } = 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, | ||
| 152 | 121 | config, | |
| 122 | + host, | ||
| 123 | + fs, | ||
| 153 | 124 | parseConfigHost, | |
| 154 | - basePath, | ||
| 155 | - existingCompilerOptions, | ||
| 156 | - configFileName, | ||
| 125 | + extendedConfigCache, | ||
| 157 | 126 | ); | |
| 158 | 127 | ||
| 128 | + Object.assign(options, angularCompilerOptions, existingOptions); | ||
| 129 | + | ||
| 159 | 130 | let emitFlags = api.EmitFlags.Default; | |
| 160 | - if (!(options['skipMetadataEmit'] || options['flatModuleOutFile'])) { | ||
| 131 | + if (!options['skipMetadataEmit'] && !options['flatModuleOutFile']) { | ||
| 161 | 132 | emitFlags |= api.EmitFlags.Metadata; | |
| 162 | 133 | } | |
| 134 | + | ||
| 163 | 135 | if (options['skipTemplateCodegen']) { | |
| 164 | 136 | emitFlags = emitFlags & ~api.EmitFlags.Codegen; | |
| 165 | 137 | } | |
| 138 | + | ||
| 166 | 139 | return {project: projectFile, rootNames, projectReferences, options, errors, emitFlags}; | |
| 167 | 140 | } catch (e) { | |
| 168 | 141 | const errors: ts.Diagnostic[] = [ | |
@@ -180,6 +153,113 @@ export function readConfiguration( | |||
| 180 | 153 | } | |
| 181 | 154 | } | |
| 182 | 155 | ||
| 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 | + | ||
| 183 | 263 | function createParseConfigHost(host: ConfigurationHost, fs = getFileSystem()): ts.ParseConfigHost { | |
| 184 | 264 | return { | |
| 185 | 265 | fileExists: host.exists.bind(host), | |
@@ -194,41 +274,25 @@ function getExtendedConfigPath( | |||
| 194 | 274 | extendsValue: string, | |
| 195 | 275 | host: ConfigurationHost, | |
| 196 | 276 | fs: FileSystem, | |
| 277 | + parseConfigHost: ts.ParseConfigHost, | ||
| 197 | 278 | ): 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 | + } | ||
| 232 | 296 | } | |
| 233 | 297 | } | |
| 234 | 298 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -298,4 +298,76 @@ describe('perform_compile', () => { | |||
| 298 | 298 | }), | |
| 299 | 299 | ); | |
| 300 | 300 | }); | |
| 301 | + | ||
| 302 | + it('should reuse cached configuration populated by extendedConfigCache across calls', () => { | ||
| 303 | + writeSomeConfigs(); | ||
| 304 | + | ||
| 305 | + const extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>(); | ||
| 306 | + const {options: firstRunOptions} = readConfiguration( | ||
| 307 | + path.resolve(basePath, 'tsconfig-level-1.json'), | ||
| 308 | + undefined, | ||
| 309 | + undefined, | ||
| 310 | + extendedConfigCache, | ||
| 311 | + ); | ||
| 312 | + | ||
| 313 | + expect(firstRunOptions.annotateForClosureCompiler).toBeTrue(); | ||
| 314 | + expect(firstRunOptions.skipMetadataEmit).toBeTrue(); | ||
| 315 | + expect(extendedConfigCache.size).toBeGreaterThan(0); | ||
| 316 | + | ||
| 317 | + // Overwrite the file on disk with invalid JSON. If readConfiguration uses cache, it will not fail. | ||
| 318 | + support.writeFiles({ | ||
| 319 | + 'tsconfig-level-2.json': `INVALID_JSON`, | ||
| 320 | + }); | ||
| 321 | + | ||
| 322 | + const {options: secondRunOptions} = readConfiguration( | ||
| 323 | + path.resolve(basePath, 'tsconfig-level-1.json'), | ||
| 324 | + undefined, | ||
| 325 | + undefined, | ||
| 326 | + extendedConfigCache, | ||
| 327 | + ); | ||
| 328 | + | ||
| 329 | + expect(secondRunOptions.annotateForClosureCompiler).toBeTrue(); | ||
| 330 | + expect(secondRunOptions.skipMetadataEmit).toBeTrue(); | ||
| 331 | + }); | ||
| 332 | + | ||
| 333 | + it('should honor changed angularCompilerOptions in an extended tsconfig across calls when cache is invalidated', () => { | ||
| 334 | + writeSomeConfigs(); | ||
| 335 | + | ||
| 336 | + const extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>(); | ||
| 337 | + const level1Path = path.resolve(basePath, 'tsconfig-level-1.json'); | ||
| 338 | + const level2Path = path.resolve(basePath, 'tsconfig-level-2.json'); | ||
| 339 | + | ||
| 340 | + // First run: initial values | ||
| 341 | + const {options: firstRunOptions} = readConfiguration( | ||
| 342 | + level1Path, | ||
| 343 | + undefined, | ||
| 344 | + undefined, | ||
| 345 | + extendedConfigCache, | ||
| 346 | + ); | ||
| 347 | + | ||
| 348 | + expect(firstRunOptions.skipMetadataEmit).toBeTrue(); | ||
| 349 | + | ||
| 350 | + // Modify the angular-specific option in the extended config file | ||
| 351 | + support.writeFiles({ | ||
| 352 | + 'tsconfig-level-2.json': `{ | ||
| 353 | + "extends": "./tsconfig-level-3.json", | ||
| 354 | + "angularCompilerOptions": { | ||
| 355 | + "skipMetadataEmit": false | ||
| 356 | + } | ||
| 357 | + }`, | ||
| 358 | + }); | ||
| 359 | + | ||
| 360 | + // Invalidate the cache entry for the modified extended configuration (mimicking watch mode file watcher behavior) | ||
| 361 | + extendedConfigCache.delete(level2Path); | ||
| 362 | + | ||
| 363 | + // Second run: verify updated option is picked up and honored | ||
| 364 | + const {options: secondRunOptions} = readConfiguration( | ||
| 365 | + level1Path, | ||
| 366 | + undefined, | ||
| 367 | + undefined, | ||
| 368 | + extendedConfigCache, | ||
| 369 | + ); | ||
| 370 | + | ||
| 371 | + expect(secondRunOptions.skipMetadataEmit).toBeFalse(); | ||
| 372 | + }); | ||
| 301 | 373 | }); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments