Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 20, 2022
4 changes: 2 additions & 2 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ import {
NewLineKind,
Node,
NodeArray,
nodeModuleNameResolver,
nodeNextJsonConfigResolver,
normalizePath,
normalizeSlashes,
NumericLiteral,
Expand Down Expand Up @@ -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;
}
Expand Down
24 changes: 20 additions & 4 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -1868,14 +1873,25 @@ 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)
) {
const result = tryFile(candidate, onlyRecordFailures, state);
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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)));
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/testRunner/unittests/config/configurationExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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", () => {
Expand Down