From 2fda63e350f988c35c875c4b41e13593562f7b34 Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Mon, 26 Sep 2022 15:22:10 +0300 Subject: [PATCH 1/4] feat(48665): resolve configs from the exports field of the source package --- src/compiler/commandLineParser.ts | 2 +- src/compiler/moduleNameResolver.ts | 17 ++++++++++++----- .../config/configurationExtension.ts | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index b3dcb04c777d9..99995f4719526 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -3153,7 +3153,7 @@ namespace ts { 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); + 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 222d1dee63f4d..c9d10473f037b 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1346,6 +1346,10 @@ namespace ts { return nodeModuleNameResolverWorker(NodeResolutionFeatures.None, moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, extensions, 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, tsconfigExtensions, /*redirectedReference*/ undefined); + } + function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[], redirectedReference: ResolvedProjectReference | undefined): ResolvedModuleWithFailedLookupLocations { const traceEnabled = isTraceEnabled(compilerOptions, host); @@ -1569,12 +1573,15 @@ namespace ts { } } - function loadJSOrExactTSFileName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { + function loadFileName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { if ((extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) && fileExtensionIsOneOf(candidate, supportedTSExtensionsFlat)) { const result = tryFile(candidate, onlyRecordFailures, state); return result !== undefined ? { path: candidate, ext: tryExtractTSExtension(candidate) as Extension } : undefined; } - + if (extensions === Extensions.TSConfig && fileExtensionIs(candidate, Extension.Json)) { + const result = tryFile(candidate, onlyRecordFailures, state); + return result !== undefined ? { path: candidate, ext: Extension.Json } : undefined; + } return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); } @@ -1763,7 +1770,7 @@ namespace ts { } const resolvedTarget = combinePaths(scope.packageDirectory, target); const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); - const result = loadJSOrExactTSFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state); + const result = loadFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state); if (result) { entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); return true; @@ -2186,7 +2193,7 @@ namespace ts { 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, loadFileName(extensions, finalPath, /*onlyRecordFailures*/ false, state))); } else if (typeof target === "object" && target !== null) { // eslint-disable-line no-null/no-null if (!Array.isArray(target)) { @@ -2329,7 +2336,7 @@ namespace ts { continue; } if (state.host.fileExists(possibleInputWithInputExtension)) { - return toSearchResult(withPackageId(scope, loadJSOrExactTSFileName(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); + return toSearchResult(withPackageId(scope, loadFileName(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); } } } diff --git a/src/testRunner/unittests/config/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts index ad906411ffbca..7671f9cfe0711 100644 --- a/src/testRunner/unittests/config/configurationExtension.ts +++ b/src/testRunner/unittests/config/configurationExtension.ts @@ -4,6 +4,24 @@ namespace ts { 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", @@ -333,6 +351,7 @@ namespace 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")]); + testSuccess("can lookup via an package.json exports", "tsconfig.extendsFoo.json", { strict: true }, [combinePaths(basePath, "main.ts")]); }); it("adds extendedSourceFiles only once", () => { From b60569f5d9d0f5b311bc797435541d070dc472e7 Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Fri, 16 Dec 2022 16:16:47 +0200 Subject: [PATCH 2/4] add missed property --- 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 701cb6b7fc0a2..4394c370a3b49 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1883,7 +1883,7 @@ function loadFileName(extensions: Extensions, candidate: string, onlyRecordFailu if (state.isConfigLookup && extensions === Extensions.Json && fileExtensionIs(candidate, Extension.Json)) { const result = tryFile(candidate, onlyRecordFailures, state); - return result !== undefined ? { path: candidate, ext: Extension.Json } : undefined; + return result !== undefined ? { path: candidate, ext: Extension.Json, resolvedUsingTsExtension: undefined } : undefined; } return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); From 744809bd5767c7335da34ad79392f7d82b615b89 Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Fri, 16 Dec 2022 19:28:21 +0200 Subject: [PATCH 3/4] rename loadFileName to loadFileNameFromPackageJsonField --- src/compiler/moduleNameResolver.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 4394c370a3b49..c863a9c62b01f 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1873,7 +1873,12 @@ function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidat } } -function loadFileName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { +/** + * This function is it’s only ever called with paths written in package.json files - never + * module specifiers written in source files - and so consequently 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) ) { @@ -2068,7 +2073,7 @@ function loadEntrypointsFromExportMap( } const resolvedTarget = combinePaths(scope.packageDirectory, target); const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); - const result = loadFileName(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; @@ -2498,7 +2503,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, loadFileName(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)) { @@ -2642,7 +2647,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, loadFileName(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); + return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); } } } From e9efef2d0125e5308107f50e85eebe3467ac11ea Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 16 Dec 2022 17:12:08 -0800 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> --- src/compiler/moduleNameResolver.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index c863a9c62b01f..be8573b075db3 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1874,8 +1874,9 @@ function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidat } /** - * This function is it’s only ever called with paths written in package.json files - never - * module specifiers written in source files - and so consequently it always allows the + * 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 {