diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 5a6ee2ec619ab..968e5bb756eaa 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -2191,20 +2191,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, /*projectRefs*/ 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 99b65c8946ea0..4d32cae86f7c1 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); } @@ -853,25 +859,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, /*redirectedReference*/ undefined, /*jsOnly*/ true); + return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, jsOnlyExtensions, /*redirectedReferences*/ undefined); } - export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations { - return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, redirectedReference, /*jsOnly*/ false); + export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations; + /* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; // tslint:disable-line unified-signatures + export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations { + return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, lookupConfig ? tsconfigExtensions : (compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions), redirectedReference); } - function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined, jsOnly: boolean): ResolvedModuleWithFailedLookupLocations { + function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[], redirectedReference: ResolvedProjectReference | undefined): 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); @@ -1019,9 +1027,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" @@ -1059,6 +1067,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); } @@ -1156,11 +1165,27 @@ namespace ts { } function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, jsonContent: PackageJsonPathFields | undefined, versionPaths: VersionPaths | undefined): PathAndExtension | undefined { - const packageFile = jsonContent && (extensions !== Extensions.JavaScript && extensions !== Extensions.Json - ? readPackageJsonTypesFields(jsonContent, candidate, state) || - // When resolving typescript modules, try resolving using main field as well - (extensions === Extensions.TypeScript ? readPackageJsonMainField(jsonContent, candidate, state) : undefined) - : readPackageJsonMainField(jsonContent, candidate, state)); + let packageFile: string | undefined; + if (jsonContent) { + switch (extensions) { + case Extensions.JavaScript: + case Extensions.Json: + packageFile = readPackageJsonMainField(jsonContent, candidate, state); + break; + case Extensions.TypeScript: + // When resolving typescript modules, try resolving using main field as well + packageFile = readPackageJsonTypesFields(jsonContent, candidate, state) || readPackageJsonMainField(jsonContent, candidate, state); + break; + case Extensions.DtsOnly: + packageFile = readPackageJsonTypesFields(jsonContent, candidate, state); + break; + case Extensions.TSConfig: + packageFile = readPackageJsonTSConfigField(jsonContent, candidate, state); + break; + default: + return Debug.assertNever(extensions); + } + } const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => { const fromFile = tryFile(candidate, onlyRecordFailures, state); @@ -1182,7 +1207,7 @@ namespace ts { const onlyRecordFailuresForPackageFile = packageFile ? !directoryProbablyExists(getDirectoryPath(packageFile), state.host) : undefined; const onlyRecordFailuresForIndex = onlyRecordFailures || !directoryProbablyExists(candidate, state.host); - const indexPath = combinePaths(candidate, "index"); + const indexPath = combinePaths(candidate, extensions === Extensions.TSConfig ? "tsconfig" : "index"); if (versionPaths && (!packageFile || containsPath(candidate, packageFile))) { const moduleName = getRelativePathFromDirectory(candidate, packageFile || indexPath, /*ignoreCase*/ false); @@ -1213,6 +1238,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: @@ -1264,7 +1290,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/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/src/testRunner/unittests/configurationExtension.ts b/src/testRunner/unittests/configurationExtension.ts index fbb4950b9a5b8..57c899eb5a8a0 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: [ @@ -226,12 +303,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, @@ -259,6 +330,17 @@ namespace ts { combinePaths(basePath, "tests/utils.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"); 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