Skip to content

Commit 0f1496f

Browse files
Process package.json exports with auto-import provider (#47092)
* Have auto-import provider pull in `exports` * Revert filtering of node_modules relative paths, to do in separate PR * Do @types and JS prioritization correctly * Cache entrypoints on PackageJsonInfo * Add one more test * Delete unused function * Fix other tests - dependencies need package.json files * Do two passes of exports resolution * Fix missed refactor * Apply suggestions from code review Co-authored-by: Nathan Shively-Sanders <[email protected]> * Uncomment rest of test * Handle array targets Co-authored-by: Nathan Shively-Sanders <[email protected]>
1 parent 935c05c commit 0f1496f

20 files changed

+975
-63
lines changed

src/compiler/moduleNameResolver.ts

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,7 @@ namespace ts {
340340
}
341341

342342
const failedLookupLocations: string[] = [];
343-
const features =
344-
getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 ? NodeResolutionFeatures.Node12Default :
345-
getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault :
346-
NodeResolutionFeatures.None;
343+
const features = getDefaultNodeResolutionFeatures(options);
347344
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features, conditions: ["node", "require", "types"] };
348345
let resolved = primaryLookup();
349346
let primary = true;
@@ -433,6 +430,42 @@ namespace ts {
433430
}
434431
}
435432

433+
function getDefaultNodeResolutionFeatures(options: CompilerOptions) {
434+
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 ? NodeResolutionFeatures.Node12Default :
435+
getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault :
436+
NodeResolutionFeatures.None;
437+
}
438+
439+
/**
440+
* @internal
441+
* Does not try `@types/${packageName}` - use a second pass if needed.
442+
*/
443+
export function resolvePackageNameToPackageJson(
444+
packageName: string,
445+
containingDirectory: string,
446+
options: CompilerOptions,
447+
host: ModuleResolutionHost,
448+
cache: ModuleResolutionCache | undefined,
449+
): PackageJsonInfo | undefined {
450+
const moduleResolutionState: ModuleResolutionState = {
451+
compilerOptions: options,
452+
host,
453+
traceEnabled: isTraceEnabled(options, host),
454+
failedLookupLocations: [],
455+
packageJsonInfoCache: cache?.getPackageJsonInfoCache(),
456+
conditions: emptyArray,
457+
features: NodeResolutionFeatures.None,
458+
};
459+
460+
return forEachAncestorDirectory(containingDirectory, ancestorDirectory => {
461+
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
462+
const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules");
463+
const candidate = combinePaths(nodeModulesFolder, packageName);
464+
return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState);
465+
}
466+
});
467+
}
468+
436469
/**
437470
* Given a set of options, returns the set of type directive names
438471
* that should be included for this program automatically.
@@ -1171,11 +1204,6 @@ namespace ts {
11711204
return resolvedModule.resolvedFileName;
11721205
}
11731206

1174-
/* @internal */
1175-
export function tryResolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost) {
1176-
return tryResolveJSModuleWorker(moduleName, initialDir, host).resolvedModule;
1177-
}
1178-
11791207
/* @internal */
11801208
enum NodeResolutionFeatures {
11811209
None = 0,
@@ -1536,11 +1564,124 @@ namespace ts {
15361564
return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths));
15371565
}
15381566

1567+
/* @internal */
1568+
export function getEntrypointsFromPackageJsonInfo(
1569+
packageJsonInfo: PackageJsonInfo,
1570+
options: CompilerOptions,
1571+
host: ModuleResolutionHost,
1572+
cache: ModuleResolutionCache | undefined,
1573+
resolveJs?: boolean,
1574+
): string[] | false {
1575+
if (!resolveJs && packageJsonInfo.resolvedEntrypoints !== undefined) {
1576+
// Cached value excludes resolutions to JS files - those could be
1577+
// cached separately, but they're used rarely.
1578+
return packageJsonInfo.resolvedEntrypoints;
1579+
}
1580+
1581+
let entrypoints: string[] | undefined;
1582+
const extensions = resolveJs ? Extensions.JavaScript : Extensions.TypeScript;
1583+
const features = getDefaultNodeResolutionFeatures(options);
1584+
const requireState: ModuleResolutionState = {
1585+
compilerOptions: options,
1586+
host,
1587+
traceEnabled: isTraceEnabled(options, host),
1588+
failedLookupLocations: [],
1589+
packageJsonInfoCache: cache?.getPackageJsonInfoCache(),
1590+
conditions: ["node", "require", "types"],
1591+
features,
1592+
};
1593+
const requireResolution = loadNodeModuleFromDirectoryWorker(
1594+
extensions,
1595+
packageJsonInfo.packageDirectory,
1596+
/*onlyRecordFailures*/ false,
1597+
requireState,
1598+
packageJsonInfo.packageJsonContent,
1599+
packageJsonInfo.versionPaths);
1600+
entrypoints = append(entrypoints, requireResolution?.path);
1601+
1602+
if (features & NodeResolutionFeatures.Exports && packageJsonInfo.packageJsonContent.exports) {
1603+
for (const conditions of [["node", "import", "types"], ["node", "require", "types"]]) {
1604+
const exportState = { ...requireState, failedLookupLocations: [], conditions };
1605+
const exportResolutions = loadEntrypointsFromExportMap(
1606+
packageJsonInfo,
1607+
packageJsonInfo.packageJsonContent.exports,
1608+
exportState,
1609+
extensions);
1610+
if (exportResolutions) {
1611+
for (const resolution of exportResolutions) {
1612+
entrypoints = appendIfUnique(entrypoints, resolution.path);
1613+
}
1614+
}
1615+
}
1616+
}
1617+
1618+
return packageJsonInfo.resolvedEntrypoints = entrypoints || false;
1619+
}
1620+
1621+
function loadEntrypointsFromExportMap(
1622+
scope: PackageJsonInfo,
1623+
exports: object,
1624+
state: ModuleResolutionState,
1625+
extensions: Extensions,
1626+
): PathAndExtension[] | undefined {
1627+
let entrypoints: PathAndExtension[] | undefined;
1628+
if (isArray(exports)) {
1629+
for (const target of exports) {
1630+
loadEntrypointsFromTargetExports(target);
1631+
}
1632+
}
1633+
// eslint-disable-next-line no-null/no-null
1634+
else if (typeof exports === "object" && exports !== null && allKeysStartWithDot(exports as MapLike<unknown>)) {
1635+
for (const key in exports) {
1636+
loadEntrypointsFromTargetExports((exports as MapLike<unknown>)[key]);
1637+
}
1638+
}
1639+
else {
1640+
loadEntrypointsFromTargetExports(exports);
1641+
}
1642+
return entrypoints;
1643+
1644+
function loadEntrypointsFromTargetExports(target: unknown): boolean | undefined {
1645+
if (typeof target === "string" && startsWith(target, "./") && target.indexOf("*") === -1) {
1646+
const partsAfterFirst = getPathComponents(target).slice(2);
1647+
if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) {
1648+
return false;
1649+
}
1650+
const resolvedTarget = combinePaths(scope.packageDirectory, target);
1651+
const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.());
1652+
const result = loadJSOrExactTSFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state);
1653+
if (result) {
1654+
entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path);
1655+
return true;
1656+
}
1657+
}
1658+
else if (Array.isArray(target)) {
1659+
for (const t of target) {
1660+
const success = loadEntrypointsFromTargetExports(t);
1661+
if (success) {
1662+
return true;
1663+
}
1664+
}
1665+
}
1666+
// eslint-disable-next-line no-null/no-null
1667+
else if (typeof target === "object" && target !== null) {
1668+
return forEach(getOwnKeys(target as MapLike<unknown>), key => {
1669+
if (key === "default" || contains(state.conditions, key) || isApplicableVersionedTypesKey(state.conditions, key)) {
1670+
loadEntrypointsFromTargetExports((target as MapLike<unknown>)[key]);
1671+
return true;
1672+
}
1673+
});
1674+
}
1675+
}
1676+
}
1677+
15391678
/*@internal*/
15401679
interface PackageJsonInfo {
15411680
packageDirectory: string;
15421681
packageJsonContent: PackageJsonPathFields;
15431682
versionPaths: VersionPaths | undefined;
1683+
/** false: resolved to nothing. undefined: not yet resolved */
1684+
resolvedEntrypoints: string[] | false | undefined;
15441685
}
15451686

15461687
/**
@@ -1606,7 +1747,7 @@ namespace ts {
16061747
trace(host, Diagnostics.Found_package_json_at_0, packageJsonPath);
16071748
}
16081749
const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state);
1609-
const result = { packageDirectory, packageJsonContent, versionPaths };
1750+
const result = { packageDirectory, packageJsonContent, versionPaths, resolvedEntrypoints: undefined };
16101751
state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, result);
16111752
return result;
16121753
}

src/compiler/moduleSpecifiers.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,44 +59,55 @@ namespace ts.moduleSpecifiers {
5959
};
6060
}
6161

62+
// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`?
63+
// Because when this is called by the file renamer, `importingSourceFile` is the file being renamed,
64+
// while `importingSourceFileName` its *new* name. We need a source file just to get its
65+
// `impliedNodeFormat` and to detect certain preferences from existing import module specifiers.
6266
export function updateModuleSpecifier(
6367
compilerOptions: CompilerOptions,
68+
importingSourceFile: SourceFile,
6469
importingSourceFileName: Path,
6570
toFileName: string,
6671
host: ModuleSpecifierResolutionHost,
6772
oldImportSpecifier: string,
6873
): string | undefined {
69-
const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {});
74+
const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {});
7075
if (res === oldImportSpecifier) return undefined;
7176
return res;
7277
}
7378

74-
// Note: importingSourceFile is just for usesJsExtensionOnImports
79+
// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`?
80+
// Because when this is called by the declaration emitter, `importingSourceFile` is the implementation
81+
// file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the
82+
// one currently being produced; the latter to the one being imported). We need an implementation file
83+
// just to get its `impliedNodeFormat` and to detect certain preferences from existing import module
84+
// specifiers.
7585
export function getModuleSpecifier(
7686
compilerOptions: CompilerOptions,
7787
importingSourceFile: SourceFile,
7888
importingSourceFileName: Path,
7989
toFileName: string,
8090
host: ModuleSpecifierResolutionHost,
8191
): string {
82-
return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {});
92+
return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {});
8393
}
8494

8595
export function getNodeModulesPackageName(
8696
compilerOptions: CompilerOptions,
87-
importingSourceFileName: Path,
97+
importingSourceFile: SourceFile,
8898
nodeModulesFileName: string,
8999
host: ModuleSpecifierResolutionHost,
90100
preferences: UserPreferences,
91101
): string | undefined {
92-
const info = getInfo(importingSourceFileName, host);
93-
const modulePaths = getAllModulePaths(importingSourceFileName, nodeModulesFileName, host, preferences);
102+
const info = getInfo(importingSourceFile.path, host);
103+
const modulePaths = getAllModulePaths(importingSourceFile.path, nodeModulesFileName, host, preferences);
94104
return firstDefined(modulePaths,
95-
modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions, /*packageNameOnly*/ true));
105+
modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, /*packageNameOnly*/ true));
96106
}
97107

98108
function getModuleSpecifierWorker(
99109
compilerOptions: CompilerOptions,
110+
importingSourceFile: SourceFile,
100111
importingSourceFileName: Path,
101112
toFileName: string,
102113
host: ModuleSpecifierResolutionHost,
@@ -105,7 +116,7 @@ namespace ts.moduleSpecifiers {
105116
): string {
106117
const info = getInfo(importingSourceFileName, host);
107118
const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host, userPreferences);
108-
return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions)) ||
119+
return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions)) ||
109120
getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences);
110121
}
111122

@@ -222,7 +233,7 @@ namespace ts.moduleSpecifiers {
222233
let pathsSpecifiers: string[] | undefined;
223234
let relativeSpecifiers: string[] | undefined;
224235
for (const modulePath of modulePaths) {
225-
const specifier = tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions);
236+
const specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions);
226237
nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier);
227238
if (specifier && modulePath.isRedirect) {
228239
// If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar",
@@ -639,7 +650,7 @@ namespace ts.moduleSpecifiers {
639650
: removeFileExtension(relativePath);
640651
}
641652

642-
function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined {
653+
function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile , host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined {
643654
if (!host.fileExists || !host.readFile) {
644655
return undefined;
645656
}
@@ -706,11 +717,19 @@ namespace ts.moduleSpecifiers {
706717
let moduleFileToTry = path;
707718
if (host.fileExists(packageJsonPath)) {
708719
const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!);
709-
// TODO: Inject `require` or `import` condition based on the intended import mode
710720
if (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext) {
711-
const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" ? tryGetModuleNameFromExports(options, path, packageRootPath, packageJsonContent.name, packageJsonContent.exports, ["node", "types"]) : undefined;
721+
// `conditions` *could* be made to go against `importingSourceFile.impliedNodeFormat` if something wanted to generate
722+
// an ImportEqualsDeclaration in an ESM-implied file or an ImportCall in a CJS-implied file. But since this function is
723+
// usually called to conjure an import out of thin air, we don't have an existing usage to call `getModeForUsageAtIndex`
724+
// with, so for now we just stick with the mode of the file.
725+
const conditions = ["node", importingSourceFile.impliedNodeFormat === ModuleKind.ESNext ? "import" : "require", "types"];
726+
const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string"
727+
? tryGetModuleNameFromExports(options, path, packageRootPath, getPackageNameFromTypesPackageName(packageJsonContent.name), packageJsonContent.exports, conditions)
728+
: undefined;
712729
if (fromExports) {
713-
const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry) ? fromExports : { moduleFileToTry: removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) };
730+
const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry)
731+
? fromExports
732+
: { moduleFileToTry: removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) };
714733
return { ...withJsExtension, verbatimFromExports: true };
715734
}
716735
if (packageJsonContent.exports) {

src/compiler/utilities.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6307,8 +6307,6 @@ namespace ts {
63076307
getSymlinkedFiles(): ReadonlyESMap<Path, string> | undefined;
63086308
setSymlinkedDirectory(symlink: string, real: SymlinkedDirectory | false): void;
63096309
setSymlinkedFile(symlinkPath: Path, real: string): void;
6310-
/*@internal*/
6311-
setSymlinkedDirectoryFromSymlinkedFile(symlink: string, real: string): void;
63126310
/**
63136311
* @internal
63146312
* Uses resolvedTypeReferenceDirectives from program instead of from files, since files
@@ -6346,16 +6344,6 @@ namespace ts {
63466344
(symlinkedDirectories || (symlinkedDirectories = new Map())).set(symlinkPath, real);
63476345
}
63486346
},
6349-
setSymlinkedDirectoryFromSymlinkedFile(symlink, real) {
6350-
this.setSymlinkedFile(toPath(symlink, cwd, getCanonicalFileName), real);
6351-
const [commonResolved, commonOriginal] = guessDirectorySymlink(real, symlink, cwd, getCanonicalFileName) || emptyArray;
6352-
if (commonResolved && commonOriginal) {
6353-
this.setSymlinkedDirectory(commonOriginal, {
6354-
real: commonResolved,
6355-
realPath: toPath(commonResolved, cwd, getCanonicalFileName),
6356-
});
6357-
}
6358-
},
63596347
setSymlinksFromResolutions(files, typeReferenceDirectives) {
63606348
Debug.assert(!hasProcessedResolutions);
63616349
hasProcessedResolutions = true;

0 commit comments

Comments
 (0)