From 6d0c07a2a3522362362663b0549b54bc451b4e89 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 25 Sep 2018 14:04:01 -0700 Subject: [PATCH 1/3] Add support for configuration inheritance via packages --- src/compiler/commandLineParser.ts | 28 +++--- src/compiler/moduleNameResolver.ts | 57 +++++++---- .../unittests/configurationExtension.ts | 94 +++++++++++++++++-- 3 files changed, 144 insertions(+), 35 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 3ebb865a64c32..6d30bc434d5be 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -2163,20 +2163,24 @@ namespace ts { errors: Push, createDiagnostic: (message: DiagnosticMessage, arg1?: string) => Diagnostic) { extendedConfig = normalizeSlashes(extendedConfig); - // If the path isn't a rooted or relative path, don't try to resolve it (we reserve the right to special case module-id like paths in the future) - if (!(isRootedDiskPath(extendedConfig) || startsWith(extendedConfig, "./") || startsWith(extendedConfig, "../"))) { - errors.push(createDiagnostic(Diagnostics.A_path_in_an_extends_option_must_be_relative_or_rooted_but_0_is_not, extendedConfig)); - return undefined; - } - let extendedConfigPath = getNormalizedAbsolutePath(extendedConfig, basePath); - if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, Extension.Json)) { - extendedConfigPath = `${extendedConfigPath}.json`; - if (!host.fileExists(extendedConfigPath)) { - errors.push(createDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig)); - return undefined; + if (isRootedDiskPath(extendedConfig) || startsWith(extendedConfig, "./") || startsWith(extendedConfig, "../")) { + let extendedConfigPath = getNormalizedAbsolutePath(extendedConfig, basePath); + if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, Extension.Json)) { + extendedConfigPath = `${extendedConfigPath}.json`; + if (!host.fileExists(extendedConfigPath)) { + errors.push(createDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig)); + return undefined; + } } + return extendedConfigPath; + } + // If the path isn't a rooted or relative path, resolve like a module + const resolved = nodeModuleNameResolver(extendedConfig, combinePaths(basePath, "tsconfig.json"), { moduleResolution: ModuleResolutionKind.NodeJs }, host, /*cache*/ undefined, /*lookupConfig*/ true); + if (resolved.resolvedModule) { + return resolved.resolvedModule.resolvedFileName; } - return extendedConfigPath; + errors.push(createDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig)); + return undefined; } function getExtendedConfig( diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 1d912faa1161c..fecbd2c851dd6 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -62,6 +62,7 @@ namespace ts { TypeScript, /** '.ts', '.tsx', or '.d.ts' */ JavaScript, /** '.js' or '.jsx' */ Json, /** '.json' */ + TSConfig, /** '.json' with `tsconfig` used instead of `index` */ DtsOnly /** Only '.d.ts' */ } @@ -98,6 +99,7 @@ namespace ts { types?: string; typesVersions?: MapLike>; main?: string; + tsconfig?: string; } interface PackageJson extends PackageJsonPathFields { @@ -126,7 +128,7 @@ namespace ts { return value; } - function readPackageJsonPathField(jsonContent: PackageJson, fieldName: K, baseDirectory: string, state: ModuleResolutionState): PackageJson[K] | undefined { + function readPackageJsonPathField(jsonContent: PackageJson, fieldName: K, baseDirectory: string, state: ModuleResolutionState): PackageJson[K] | undefined { const fileName = readPackageJsonField(jsonContent, fieldName, "string", state); if (fileName === undefined) return; const path = normalizePath(combinePaths(baseDirectory, fileName)); @@ -141,6 +143,10 @@ namespace ts { || readPackageJsonPathField(jsonContent, "types", baseDirectory, state); } + function readPackageJsonTSConfigField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { + return readPackageJsonPathField(jsonContent, "tsconfig", baseDirectory, state); + } + function readPackageJsonMainField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { return readPackageJsonPathField(jsonContent, "main", baseDirectory, state); } @@ -792,25 +798,27 @@ namespace ts { return resolvedModule && resolvedModule.resolvedFileName; } + const jsOnlyExtensions = [Extensions.JavaScript]; + const tsExtensions = [Extensions.TypeScript, Extensions.JavaScript]; + const tsPlusJsonExtensions = [...tsExtensions, Extensions.Json]; + const tsconfigExtensions = [Extensions.TSConfig]; function tryResolveJSModuleWorker(moduleName: string, initialDir: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { - return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, /*jsOnly*/ true); + return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, jsOnlyExtensions); } - export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations { - return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, /*jsOnly*/ false); + export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; + /* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; + export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations { + return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, lookupConfig ? tsconfigExtensions : (compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions)); } - function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, jsOnly: boolean): ResolvedModuleWithFailedLookupLocations { + function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[]): ResolvedModuleWithFailedLookupLocations { const traceEnabled = isTraceEnabled(compilerOptions, host); const failedLookupLocations: string[] = []; const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations }; - const result = jsOnly ? - tryResolve(Extensions.JavaScript) : - (tryResolve(Extensions.TypeScript) || - tryResolve(Extensions.JavaScript) || - (compilerOptions.resolveJsonModule ? tryResolve(Extensions.Json) : undefined)); + const result = forEach(extensions, ext => tryResolve(ext)); if (result && result.value) { const { resolved, isExternalLibraryImport } = result.value; return createResolvedModuleWithFailedLookupLocations(resolved, isExternalLibraryImport, failedLookupLocations); @@ -954,9 +962,9 @@ namespace ts { * in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations. */ function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { - if (extensions === Extensions.Json) { + if (extensions === Extensions.Json || extensions === Extensions.TSConfig) { const extensionLess = tryRemoveExtension(candidate, Extension.Json); - return extensionLess === undefined ? undefined : tryAddingExtensions(extensionLess, extensions, onlyRecordFailures, state); + return (extensionLess === undefined && extensions === Extensions.Json) ? undefined : tryAddingExtensions(extensionLess || candidate, extensions, onlyRecordFailures, state); } // First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts" @@ -994,6 +1002,7 @@ namespace ts { return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts); case Extensions.JavaScript: return tryExtension(Extension.Js) || tryExtension(Extension.Jsx); + case Extensions.TSConfig: case Extensions.Json: return tryExtension(Extension.Json); } @@ -1037,7 +1046,7 @@ namespace ts { return fromPackageJson; } const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, state.host); - return loadModuleFromFile(extensions, combinePaths(candidate, "index"), !directoryExists, state); + return loadModuleFromFile(extensions, combinePaths(candidate, extensions === Extensions.TSConfig ? "tsconfig" : "index"), !directoryExists, state); } interface PackageJsonInfo { @@ -1100,9 +1109,22 @@ namespace ts { } function loadModuleFromPackageJson(jsonContent: PackageJsonPathFields, versionPaths: VersionPaths | undefined, extensions: Extensions, candidate: string, state: ModuleResolutionState): PathAndExtension | undefined { - let file = extensions !== Extensions.JavaScript && extensions !== Extensions.Json - ? readPackageJsonTypesFields(jsonContent, candidate, state) - : readPackageJsonMainField(jsonContent, candidate, state); + let file: string | undefined; + switch (extensions) { + case Extensions.JavaScript: + case Extensions.Json: + file = readPackageJsonMainField(jsonContent, candidate, state); + break; + case Extensions.TypeScript: + case Extensions.DtsOnly: + file = readPackageJsonTypesFields(jsonContent, candidate, state); + break; + case Extensions.TSConfig: + file = readPackageJsonTSConfigField(jsonContent, candidate, state); + break; + default: + return Debug.assertNever(extensions); + } if (!file) { if (extensions === Extensions.TypeScript) { // When resolving typescript modules, try resolving using main field as well @@ -1162,6 +1184,7 @@ namespace ts { switch (extensions) { case Extensions.JavaScript: return extension === Extension.Js || extension === Extension.Jsx; + case Extensions.TSConfig: case Extensions.Json: return extension === Extension.Json; case Extensions.TypeScript: @@ -1213,7 +1236,7 @@ namespace ts { if (packageResult) { return packageResult; } - if (extensions !== Extensions.JavaScript && extensions !== Extensions.Json) { + if (extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) { const nodeModulesAtTypes = combinePaths(nodeModulesFolder, "@types"); let nodeModulesAtTypesExists = nodeModulesFolderExists; if (nodeModulesFolderExists && !directoryProbablyExists(nodeModulesAtTypes, state.host)) { diff --git a/src/testRunner/unittests/configurationExtension.ts b/src/testRunner/unittests/configurationExtension.ts index b9567b3b54d39..ca64bc9130961 100644 --- a/src/testRunner/unittests/configurationExtension.ts +++ b/src/testRunner/unittests/configurationExtension.ts @@ -4,6 +4,83 @@ namespace ts { cwd, files: { [root]: { + "dev/node_modules/config-box/package.json": JSON.stringify({ + name: "config-box", + version: "1.0.0", + tsconfig: "./strict.json" + }), + "dev/node_modules/config-box/strict.json": JSON.stringify({ + compilerOptions: { + strict: true, + } + }), + "dev/node_modules/config-box/unstrict.json": JSON.stringify({ + compilerOptions: { + strict: false, + } + }), + "dev/tsconfig.extendsBox.json": JSON.stringify({ + extends: "config-box", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsStrict.json": JSON.stringify({ + extends: "config-box/strict", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsUnStrict.json": JSON.stringify({ + extends: "config-box/unstrict", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsStrictExtension.json": JSON.stringify({ + extends: "config-box/strict.json", + files: [ + "main.ts", + ] + }), + "dev/node_modules/config-box-implied/package.json": JSON.stringify({ + name: "config-box-implied", + version: "1.0.0", + }), + "dev/node_modules/config-box-implied/tsconfig.json": JSON.stringify({ + compilerOptions: { + strict: true, + } + }), + "dev/node_modules/config-box-implied/unstrict/tsconfig.json": JSON.stringify({ + compilerOptions: { + strict: false, + } + }), + "dev/tsconfig.extendsBoxImplied.json": JSON.stringify({ + extends: "config-box-implied", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsBoxImpliedUnstrict.json": JSON.stringify({ + extends: "config-box-implied/unstrict", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsBoxImpliedUnstrictExtension.json": JSON.stringify({ + extends: "config-box-implied/unstrict/tsconfig", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsBoxImpliedPath.json": JSON.stringify({ + extends: "config-box-implied/tsconfig.json", + files: [ + "main.ts", + ] + }), "dev/tsconfig.json": JSON.stringify({ extends: "./configs/base", files: [ @@ -221,12 +298,6 @@ namespace ts { messageText: `Compiler option 'extends' requires a value of type string.` }]); - testFailure("can error when 'extends' is neither relative nor rooted.", "extends2.json", [{ - code: 18001, - category: DiagnosticCategory.Error, - messageText: `A path in an 'extends' option must be relative or rooted, but 'configs/base' is not.` - }]); - testSuccess("can overwrite compiler options using extended 'null'", "configs/third.json", { allowJs: true, noImplicitAny: true, @@ -245,6 +316,17 @@ namespace ts { combinePaths(basePath, "main.ts") ]); + describe("finding extended configs from node_modules", () => { + testSuccess("can lookup via tsconfig field", "tsconfig.extendsBox.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via package-relative path", "tsconfig.extendsStrict.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via non-redirected-to package-relative path", "tsconfig.extendsUnStrict.json", { strict: false }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via package-relative path with extension", "tsconfig.extendsStrictExtension.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig", "tsconfig.extendsBoxImplied.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory", "tsconfig.extendsBoxImpliedUnstrict.json", { strict: false }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory with name", "tsconfig.extendsBoxImpliedUnstrictExtension.json", { strict: false }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory with extension", "tsconfig.extendsBoxImpliedPath.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + }); + it("adds extendedSourceFiles only once", () => { const sourceFile = readJsonConfigFile("configs/fourth.json", (path) => host.readFile(path)); const dir = combinePaths(basePath, "configs"); From 1d29ab91bed19bff0742164ab5059d7698db67b2 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Thu, 27 Sep 2018 13:26:32 -0700 Subject: [PATCH 2/3] Fix lint --- src/compiler/moduleNameResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index fecbd2c851dd6..2674337b6a9ce 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -807,7 +807,7 @@ namespace ts { } export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations; - /* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; + /* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; // tslint:disable-line unified-signatures export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations { return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, lookupConfig ? tsconfigExtensions : (compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions)); } From 0dfec21522472483bce62dcbb7ace4e2b7f780dc Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 22 Oct 2018 14:45:52 -0700 Subject: [PATCH 3/3] Propagate trace into config parse hosts --- src/compiler/program.ts | 3 ++- src/compiler/types.ts | 1 + src/compiler/watch.ts | 3 ++- tests/baselines/reference/api/tsserverlibrary.d.ts | 1 + tests/baselines/reference/api/typescript.d.ts | 1 + 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 3c3c1ff5db2c8..375e77088c07d 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -2977,7 +2977,8 @@ namespace ts { readFile: f => host.readFile(f), useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(), getCurrentDirectory: () => host.getCurrentDirectory(), - onUnRecoverableConfigFileDiagnostic: () => undefined + onUnRecoverableConfigFileDiagnostic: () => undefined, + trace: host.trace ? (s) => host.trace!(s) : undefined }; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1c2009bc19f78..7d4d36f3b21b0 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2802,6 +2802,7 @@ namespace ts { fileExists(path: string): boolean; readFile(path: string): string | undefined; + trace?(s: string): void; } /** diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 261013e29b3c8..1c2e490ca4264 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -484,7 +484,8 @@ namespace ts { fileExists: path => host.fileExists(path), readFile, getCurrentDirectory, - onUnRecoverableConfigFileDiagnostic: host.onUnRecoverableConfigFileDiagnostic + onUnRecoverableConfigFileDiagnostic: host.onUnRecoverableConfigFileDiagnostic, + trace: host.trace ? s => host.trace!(s) : undefined }; // From tsc we want to get already parsed result and hence check for rootFileNames diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 1680826b31fc4..5dccf338fb644 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1762,6 +1762,7 @@ declare namespace ts { */ fileExists(path: string): boolean; readFile(path: string): string | undefined; + trace?(s: string): void; } /** * Branded string for keeping track of when we've turned an ambiguous path diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index a7c3f9cd902f1..47bdb0eba7615 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1762,6 +1762,7 @@ declare namespace ts { */ fileExists(path: string): boolean; readFile(path: string): string | undefined; + trace?(s: string): void; } /** * Branded string for keeping track of when we've turned an ambiguous path