diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 6a03781a758a8..1ec07912adaa6 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1346,7 +1346,12 @@ namespace ts { /** * Reads the config file, reports errors if any and exits if the config file cannot be found */ - export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost): ParsedCommandLine | undefined { + export function getParsedCommandLineOfConfigFile( + configFileName: string, + optionsToExtend: CompilerOptions, + host: ParseConfigFileHost, + extendedConfigCache?: Map + ): ParsedCommandLine | undefined { let configFileText: string | undefined; try { configFileText = host.readFile(configFileName); @@ -1367,7 +1372,16 @@ namespace ts { result.path = toPath(configFileName, cwd, createGetCanonicalFileName(host.useCaseSensitiveFileNames)); result.resolvedPath = result.path; result.originalFileName = result.fileName; - return parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); + return parseJsonSourceFileConfigFileContent( + result, + host, + getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), + optionsToExtend, + getNormalizedAbsolutePath(configFileName, cwd), + /*resolutionStack*/ undefined, + /*extraFileExtension*/ undefined, + extendedConfigCache + ); } /** @@ -1981,8 +1995,8 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine { - return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions); + export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray, /*@internal*/ extendedConfigCache?: Map): ParsedCommandLine { + return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); } /*@internal*/ @@ -2021,11 +2035,12 @@ namespace ts { configFileName?: string, resolutionStack: Path[] = [], extraFileExtensions: ReadonlyArray = [], + extendedConfigCache?: Map ): ParsedCommandLine { Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined)); const errors: Diagnostic[] = []; - const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors); + const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache); const { raw } = parsedConfig; const options = extend(existingOptions, parsedConfig.options || {}); options.configFilePath = configFileName && normalizeSlashes(configFileName); @@ -2173,7 +2188,7 @@ namespace ts { return existingErrors !== configParseDiagnostics.length; } - interface ParsedTsconfig { + export interface ParsedTsconfig { raw: any; options?: CompilerOptions; typeAcquisition?: TypeAcquisition; @@ -2192,13 +2207,14 @@ namespace ts { * It does *not* resolve the included files. */ function parseConfig( - json: any, - sourceFile: TsConfigSourceFile | undefined, - host: ParseConfigHost, - basePath: string, - configFileName: string | undefined, - resolutionStack: string[], - errors: Push, + json: any, + sourceFile: TsConfigSourceFile | undefined, + host: ParseConfigHost, + basePath: string, + configFileName: string | undefined, + resolutionStack: string[], + errors: Push, + extendedConfigCache?: Map ): ParsedTsconfig { basePath = normalizeSlashes(basePath); const resolvedPath = getNormalizedAbsolutePath(configFileName || "", basePath); @@ -2215,7 +2231,7 @@ namespace ts { if (ownConfig.extendedConfigPath) { // copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios. resolutionStack = resolutionStack.concat([resolvedPath]); - const extendedConfig = getExtendedConfig(sourceFile, ownConfig.extendedConfigPath, host, basePath, resolutionStack, errors); + const extendedConfig = getExtendedConfig(sourceFile, ownConfig.extendedConfigPath, host, basePath, resolutionStack, errors, extendedConfigCache); if (extendedConfig && isSuccessfulParsedTsconfig(extendedConfig)) { const baseRaw = extendedConfig.raw; const raw = ownConfig.raw; @@ -2359,6 +2375,11 @@ namespace ts { return undefined; } + export interface ExtendedConfigCacheEntry { + extendedResult: TsConfigSourceFile; + extendedConfig: ParsedTsconfig | undefined; + } + function getExtendedConfig( sourceFile: TsConfigSourceFile | undefined, extendedConfigPath: string, @@ -2366,40 +2387,53 @@ namespace ts { basePath: string, resolutionStack: string[], errors: Push, + extendedConfigCache?: Map ): ParsedTsconfig | undefined { - const extendedResult = readJsonConfigFile(extendedConfigPath, path => host.readFile(path)); + const path = host.useCaseSensitiveFileNames ? extendedConfigPath : toLowerCase(extendedConfigPath); + let value: ExtendedConfigCacheEntry | undefined; + let extendedResult: TsConfigSourceFile; + let extendedConfig: ParsedTsconfig | undefined; + if (extendedConfigCache && (value = extendedConfigCache.get(path))) { + ({ extendedResult, extendedConfig } = value); + } + else { + extendedResult = readJsonConfigFile(extendedConfigPath, path => host.readFile(path)); + if (!extendedResult.parseDiagnostics.length) { + const extendedDirname = getDirectoryPath(extendedConfigPath); + extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, extendedDirname, + getBaseFileName(extendedConfigPath), resolutionStack, errors, extendedConfigCache); + + if (isSuccessfulParsedTsconfig(extendedConfig)) { + // Update the paths to reflect base path + const relativeDifference = convertToRelativePath(extendedDirname, basePath, identity); + const updatePath = (path: string) => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path); + const mapPropertiesInRawIfNotUndefined = (propertyName: string) => { + if (raw[propertyName]) { + raw[propertyName] = map(raw[propertyName], updatePath); + } + }; + + const { raw } = extendedConfig; + mapPropertiesInRawIfNotUndefined("include"); + mapPropertiesInRawIfNotUndefined("exclude"); + mapPropertiesInRawIfNotUndefined("files"); + } + } + if (extendedConfigCache) { + extendedConfigCache.set(path, { extendedResult, extendedConfig }); + } + } if (sourceFile) { sourceFile.extendedSourceFiles = [extendedResult.fileName]; + if (extendedResult.extendedSourceFiles) { + sourceFile.extendedSourceFiles.push(...extendedResult.extendedSourceFiles); + } } if (extendedResult.parseDiagnostics.length) { errors.push(...extendedResult.parseDiagnostics); return undefined; } - - const extendedDirname = getDirectoryPath(extendedConfigPath); - const extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, extendedDirname, - getBaseFileName(extendedConfigPath), resolutionStack, errors); - if (sourceFile && extendedResult.extendedSourceFiles) { - sourceFile.extendedSourceFiles!.push(...extendedResult.extendedSourceFiles); - } - - if (isSuccessfulParsedTsconfig(extendedConfig)) { - // Update the paths to reflect base path - const relativeDifference = convertToRelativePath(extendedDirname, basePath, identity); - const updatePath = (path: string) => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path); - const mapPropertiesInRawIfNotUndefined = (propertyName: string) => { - if (raw[propertyName]) { - raw[propertyName] = map(raw[propertyName], updatePath); - } - }; - - const { raw } = extendedConfig; - mapPropertiesInRawIfNotUndefined("include"); - mapPropertiesInRawIfNotUndefined("exclude"); - mapPropertiesInRawIfNotUndefined("files"); - } - - return extendedConfig; + return extendedConfig!; } function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: Push): boolean { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 358cabcecb1c5..7e01e56f0693d 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -2678,18 +2678,32 @@ namespace ts { return fromCache || undefined; } - // An absolute path pointing to the containing directory of the config file - const basePath = getNormalizedAbsolutePath(getDirectoryPath(refPath), host.getCurrentDirectory()); - const sourceFile = host.getSourceFile(refPath, ScriptTarget.JSON) as JsonSourceFile | undefined; - addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); - if (sourceFile === undefined) { - projectReferenceRedirects.set(sourceFilePath, false); - return undefined; + let commandLine: ParsedCommandLine | undefined; + let sourceFile: JsonSourceFile | undefined; + if (host.getParsedCommandLine) { + commandLine = host.getParsedCommandLine(refPath); + if (!commandLine) { + addFileToFilesByName(/*sourceFile*/ undefined, sourceFilePath, /*redirectedPath*/ undefined); + projectReferenceRedirects.set(sourceFilePath, false); + return undefined; + } + sourceFile = Debug.assertDefined(commandLine.options.configFile); + addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); + } + else { + // An absolute path pointing to the containing directory of the config file + const basePath = getNormalizedAbsolutePath(getDirectoryPath(refPath), host.getCurrentDirectory()); + sourceFile = host.getSourceFile(refPath, ScriptTarget.JSON) as JsonSourceFile | undefined; + addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); + if (sourceFile === undefined) { + projectReferenceRedirects.set(sourceFilePath, false); + return undefined; + } + sourceFile.path = sourceFilePath; + sourceFile.resolvedPath = sourceFilePath; + sourceFile.originalFileName = refPath; + commandLine = parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath); } - sourceFile.path = sourceFilePath; - sourceFile.resolvedPath = sourceFilePath; - sourceFile.originalFileName = refPath; - const commandLine = parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath); const resolvedRef: ResolvedProjectReference = { commandLine, sourceFile }; projectReferenceRedirects.set(sourceFilePath, resolvedRef); if (commandLine.projectReferences) { diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 8bc66e95edc45..aed4ba48e94b0 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -399,12 +399,14 @@ namespace ts { let projectCompilerOptions = baseCompilerOptions; const compilerHost = createCompilerHostFromProgramHost(host, () => projectCompilerOptions); setGetSourceFileAsHashVersioned(compilerHost, host); + compilerHost.getParsedCommandLine = parseConfigFile; compilerHost.resolveModuleNames = maybeBind(host, host.resolveModuleNames); compilerHost.resolveTypeReferenceDirectives = maybeBind(host, host.resolveTypeReferenceDirectives); let moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined; const buildInfoChecked = createFileMap(toPath); + let extendedConfigCache: Map | undefined; // Watch state const builderPrograms = createFileMap(toPath); @@ -481,7 +483,7 @@ namespace ts { let diagnostic: Diagnostic | undefined; parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; - const parsed = getParsedCommandLineOfConfigFile(configFilePath, baseCompilerOptions, parseConfigFileHost); + const parsed = getParsedCommandLineOfConfigFile(configFilePath, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; configFileCache.setValue(configFilePath, parsed || diagnostic!); return parsed; @@ -1395,6 +1397,7 @@ namespace ts { } = changeCompilerHostLikeToUseCache(host, toPath, (...args) => savedGetSourceFile.call(compilerHost, ...args)); readFileWithCache = newReadFileWithCache; compilerHost.getSourceFile = getSourceFileWithCache!; + extendedConfigCache = createMap(); const originalResolveModuleNames = compilerHost.resolveModuleNames; if (!compilerHost.resolveModuleNames) { @@ -1463,6 +1466,7 @@ namespace ts { host.writeFile = originalWriteFile; compilerHost.getSourceFile = savedGetSourceFile; readFileWithCache = savedReadFileWithCache; + extendedConfigCache = undefined; compilerHost.resolveModuleNames = originalResolveModuleNames; moduleResolutionCache = undefined; return anyFailed ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 62d0f27a6981b..e141e2b9daa82 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5131,6 +5131,7 @@ namespace ts { /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; // TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesnt use compilerHost as base /*@internal*/createDirectory?(directory: string): void; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 66b2839b89baf..d09582078be43 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2761,6 +2761,7 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[]; getEnvironmentVariable?(name: string): string | undefined; createHash?(data: string): string; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -3632,7 +3633,7 @@ declare namespace ts { /** * Reads the config file, reports errors if any and exits if the config file cannot be found */ - function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost): ParsedCommandLine | undefined; + function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, extendedConfigCache?: Map): ParsedCommandLine | undefined; /** * Read tsconfig.json file * @param fileName The path to the config file @@ -3674,7 +3675,20 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; + function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray, /*@internal*/ extendedConfigCache?: Map): ParsedCommandLine; + interface ParsedTsconfig { + raw: any; + options?: CompilerOptions; + typeAcquisition?: TypeAcquisition; + /** + * Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet + */ + extendedConfigPath?: string; + } + interface ExtendedConfigCacheEntry { + extendedResult: TsConfigSourceFile; + extendedConfig: ParsedTsconfig | undefined; + } function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions; errors: Diagnostic[]; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index bf3ad5a7e35ec..817276dc5c5c6 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2761,6 +2761,7 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[]; getEnvironmentVariable?(name: string): string | undefined; createHash?(data: string): string; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -3632,7 +3633,7 @@ declare namespace ts { /** * Reads the config file, reports errors if any and exits if the config file cannot be found */ - function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost): ParsedCommandLine | undefined; + function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, extendedConfigCache?: Map): ParsedCommandLine | undefined; /** * Read tsconfig.json file * @param fileName The path to the config file @@ -3674,7 +3675,20 @@ declare namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray): ParsedCommandLine; + function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: ReadonlyArray, /*@internal*/ extendedConfigCache?: Map): ParsedCommandLine; + interface ParsedTsconfig { + raw: any; + options?: CompilerOptions; + typeAcquisition?: TypeAcquisition; + /** + * Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet + */ + extendedConfigPath?: string; + } + interface ExtendedConfigCacheEntry { + extendedResult: TsConfigSourceFile; + extendedConfig: ParsedTsconfig | undefined; + } function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions; errors: Diagnostic[];