diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 1e6935ca223fb..3ae36b06d15fe 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -85,7 +85,7 @@ import { NewLineKind, Node, NodeArray, - nodeModuleNameResolver, + nodeNextJsonConfigResolver, normalizePath, normalizeSlashes, NumericLiteral, @@ -3413,7 +3413,7 @@ function getExtendsConfigPath( 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.Node10 }, host, /*cache*/ undefined, /*projectRefs*/ undefined, /*lookupConfig*/ true); + const resolved = nodeNextJsonConfigResolver(extendedConfig, combinePaths(basePath, "tsconfig.json"), host); if (resolved.resolvedModule) { return resolved.resolvedModule.resolvedFileName; } diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 4bf80337bf530..be8573b075db3 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1629,6 +1629,11 @@ export function nodeModuleNameResolver(moduleName: string, containingFile: strin return nodeModuleNameResolverWorker(NodeResolutionFeatures.None, moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, extensions, !!isConfigLookup, redirectedReference); } +/** @internal */ +export function nodeNextJsonConfigResolver(moduleName: string, containingFile: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { + return nodeModuleNameResolverWorker(NodeResolutionFeatures.Exports, moduleName, getDirectoryPath(containingFile), { moduleResolution: ModuleResolutionKind.NodeNext }, host, /*cache*/ undefined, Extensions.Json, /*isConfigLookup*/ true, /*redirectedReference*/ undefined); +} + function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions, isConfigLookup: boolean, redirectedReference: ResolvedProjectReference | undefined): ResolvedModuleWithFailedLookupLocations { const traceEnabled = isTraceEnabled(compilerOptions, host); @@ -1868,7 +1873,13 @@ function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidat } } -function loadJSOrExactTSFileName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { +/** + * This function is only ever called with paths written in package.json files - never + * module specifiers written in source files - and so it always allows the + + * candidate to end with a TS extension (but will also try substituting a JS extension for a TS extension). + */ +function loadFileNameFromPackageJsonField(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { if (extensions & Extensions.TypeScript && fileExtensionIsOneOf(candidate, supportedTSImplementationExtensions) || extensions & Extensions.Declaration && fileExtensionIsOneOf(candidate, supportedDeclarationExtensions) ) { @@ -1876,6 +1887,11 @@ function loadJSOrExactTSFileName(extensions: Extensions, candidate: string, only return result !== undefined ? { path: candidate, ext: tryExtractTSExtension(candidate) as Extension, resolvedUsingTsExtension: undefined } : undefined; } + if (state.isConfigLookup && extensions === Extensions.Json && fileExtensionIs(candidate, Extension.Json)) { + const result = tryFile(candidate, onlyRecordFailures, state); + return result !== undefined ? { path: candidate, ext: Extension.Json, resolvedUsingTsExtension: undefined } : undefined; + } + return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); } @@ -2058,7 +2074,7 @@ function loadEntrypointsFromExportMap( } const resolvedTarget = combinePaths(scope.packageDirectory, target); const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); - const result = loadJSOrExactTSFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state); + const result = loadFileNameFromPackageJsonField(extensions, finalPath, /*recordOnlyFailures*/ false, state); if (result) { entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); return true; @@ -2488,7 +2504,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo const finalPath = toAbsolutePath(pattern ? resolvedTarget.replace(/\*/g, subpath) : resolvedTarget + subpath); const inputLink = tryLoadInputFileForPath(finalPath, subpath, combinePaths(scope.packageDirectory, "package.json"), isImports); if (inputLink) return inputLink; - return toSearchResult(withPackageId(scope, loadJSOrExactTSFileName(extensions, finalPath, /*onlyRecordFailures*/ false, state))); + return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state))); } else if (typeof target === "object" && target !== null) { // eslint-disable-line no-null/no-null if (!Array.isArray(target)) { @@ -2632,7 +2648,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo if (!extensionIsOk(extensions, possibleExt)) continue; const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames()); if (state.host.fileExists(possibleInputWithInputExtension)) { - return toSearchResult(withPackageId(scope, loadJSOrExactTSFileName(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); + return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); } } } diff --git a/src/testRunner/unittests/config/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts index 8677621a6e514..e4b3e598da75f 100644 --- a/src/testRunner/unittests/config/configurationExtension.ts +++ b/src/testRunner/unittests/config/configurationExtension.ts @@ -7,6 +7,24 @@ function createFileSystem(ignoreCase: boolean, cwd: string, root: string) { cwd, files: { [root]: { + "dev/node_modules/@foo/tsconfig/package.json": JSON.stringify({ + name: "@foo/tsconfig", + version: "1.0.0", + exports: { + ".": "./src/tsconfig.json" + } + }), + "dev/node_modules/@foo/tsconfig/src/tsconfig.json": JSON.stringify({ + compilerOptions: { + strict: true, + } + }), + "dev/tsconfig.extendsFoo.json": JSON.stringify({ + extends: "@foo/tsconfig", + files: [ + "main.ts", + ] + }), "dev/node_modules/config-box/package.json": JSON.stringify({ name: "config-box", version: "1.0.0", @@ -376,6 +394,7 @@ describe("unittests:: config:: configurationExtension", () => { testSuccess("can lookup via an implicit tsconfig in a package-relative directory", "tsconfig.extendsBoxImpliedUnstrict.json", { strict: false }, [ts.combinePaths(basePath, "main.ts")]); testSuccess("can lookup via an implicit tsconfig in a package-relative directory with name", "tsconfig.extendsBoxImpliedUnstrictExtension.json", { strict: false }, [ts.combinePaths(basePath, "main.ts")]); testSuccess("can lookup via an implicit tsconfig in a package-relative directory with extension", "tsconfig.extendsBoxImpliedPath.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an package.json exports", "tsconfig.extendsFoo.json", { strict: true }, [ts.combinePaths(basePath, "main.ts")]); }); it("adds extendedSourceFiles only once", () => {