feat(48665): tsconfig "extends" field ignores "exports" field of source package (#50955)

* feat(48665): resolve configs from the exports field of the source package

* add missed property

* rename loadFileName to loadFileNameFromPackageJsonField

* Apply suggestions from code review

Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>

Co-authored-by: Andrew Branch <andrewbranch@users.noreply.github.com>
Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
This commit is contained in:
Oleksandr T
2022-12-20 02:40:12 +02:00
committed by GitHub
parent 0f5e037e1d
commit 5951ee9cff
3 changed files with 41 additions and 6 deletions

View File

@@ -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;
}

View File

@@ -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)));
}
}
}

View File

@@ -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", () => {