microsoft / microsoft/TypeScript
[API] No module resolution API: no counterpart to ts.resolveModuleName
- Dominant language
- Go
- Stars
- 111k
- Forks
- 14.3k
- PR merge metrics
- PR metrics pending
Description
### Description
The 7.x API has no counterpart to `ts.resolveModuleName`, and no other way to ask "what does this specifier resolve to from this file?". The compiler does the resolution internally when it builds a program, but a consumer cannot invoke it on its own.
That matters for tools that resolve outside of, or ahead of, a program:
- Finding a package's declaration entry from its directory, so that `exports` conditions, `typesVersions` and the `types`/`typings` fallbacks all apply — the thing `ts.resolveModuleName(manifest.name, path.join(dir, "__entry__.ts"), options, host)` answered in one call.
- Reporting where an import would resolve to, without materialising a program for it.
- Deciding which files to load before opening a project.
The roadmap in #63875 covers `createProgram`, `createSourceFile`, `transpileModule` and `parseCommandLine` under "top-level utility APIs", but resolution is not among them. microsoft/typescript-go#455 named "resolution behavior" as one of the critical use cases the API was meant to serve, so I may simply have missed where this is tracked — if there is an existing issue, this can be closed as a duplicate.
### Why a workaround isn't sufficient
Reimplementing it means reimplementing Node's `exports` algorithm. I have done so for the narrow case of a package resolving its own name, and it is about 40 lines that handle condition maps, arrays and the `.`/subpath split:
```ts
function selectExportTarget(target: unknown, conditions: ReadonlySet): string | undefined {
if (typeof target === "string") return target;
if (Array.isArray(target)) {
for (const alternative of target) {
const selected = selectExportTarget(alternative, conditions);
if (selected) return selected;
}
return undefined;
}
if (target && typeof target === "object") {
for (const [condition, value] of Object.entries(target)) {
if (conditions.has(condition)) {
const selected = selectExportTarget(value, conditions);
if (selected) return selected;
}
}
}
return undefined;
}
```
That covers the fixtures I have and nothing more. It does not handle `typesVersions`, symlink realpath resolution (pnpm layouts), extension substitution, or `paths`. Every consumer that needs resolution will write a different subset, each wrong in a different way, and none will agree with the compiler that ultimately type-checks the code.
### Suggested shape
Something close to the 6.x signature, answering from the same implementation the compiler uses:
```ts
resolveModuleName(
moduleName: string,
containingFile: string,
compilerOptions: CompilerOptions,
): { resolvedFileName: string; isExternalLibraryImport: boolean; extension: string } | undefined
```
A snapshot-scoped variant (`project.resolveModuleName(...)`) would work equally well for my case and might fit the existing model better, since options and the file system are already established there.
### Use case
Porting OpenRewrite's JavaScript/TypeScript parser from the 6.x API. One component enumerates the public types a package exports, and locates each package's declaration entry by resolving the package's own name from inside its directory. On 6.x that is a single `resolveModuleName` call; on 7.x it is the hand-rolled subset above, which loses `exports` maps that name declarations only under a condition the subset does not model.
Verified against `7.0.2` and `typescript@next` (`7.1.0-dev.20260827.1`).
Contributor guide
Assessment
This issue has not been assessed yet.