Skip to content

Experimental: support resolveModuleName via the TypeScript 7.1 module resolver API - #1715

Open
johnnyreilly wants to merge 2 commits into
nextfrom
experiment/custom-resolve-module-name
Open

johnnyreilly wants to merge 2 commits into
nextfrom
experiment/custom-resolve-module-name

Conversation

@johnnyreilly

Copy link
Copy Markdown
Member

Note

Experimental - we do not actively plan to merge this. It's raised to record what the TypeScript 7.1 API makes possible for the resolveModuleName loader option, and what the trade-offs are.

Summary

Since moving to the TypeScript 7 API, the resolveModuleName loader option has been accepted but ignored. TypeScript 7.1 adds api.createModuleResolver(compilerOptions, { resolveModuleName }), which takes a custom resolver callback. This PR uses it to make the option work again.

How it works

  • Synthetic programs only. The API accepts a resolver only on synthetic programs (createPrograms/reconfigurePrograms), not on configured projects opened from a tsconfig. When resolveModuleName is set, every file compiles in ts-loader's existing synthetic ("orphan") program with the resolver attached. The end-of-build recheck uses that program too.
  • Classic signature kept. The user's function is still called as (moduleName, containingFile, compilerOptions, compilerHost, parentResolver).
  • parentResolver works. It's backed by a second, plain resolver that does TypeScript's default resolution. It's needed because returning nothing from the API's callback means "unresolved", not "use the default".
  • Rebuilds stay consistent. The resolver is recreated when compiler options change.
  • One option still ignored. resolveTypeReferenceDirective has no API equivalent.

Limitations

  • containingFile is a placeholder. The API only reports the importing file's directory, so containingFile is a made-up name in that directory. Resolvers that use path.dirname(containingFile) work; ones that inspect the importing file's name don't.
  • No compilerHost. It's undefined.
  • Results are trimmed. Only resolvedFileName, originalPath and packageId reach the API. Fields like isExternalLibraryImport are dropped.
  • Extra work per build. The tsconfig project is still opened each build (for config diagnostics and config files), so some work is duplicated when the option is set. Not benchmarked.
  • No watch-mode test. Watch mode with the option set hasn't been tested.

Test plan

  • New resolveModuleName comparison test: the only reported error is a deliberate TS2322, which can only appear if foo/baz resolved through the custom resolver. Without this change it reports TS2307: Cannot find module instead (checked by temporarily removing the src/ changes).
  • npm run comparison-tests: all 58 pass
  • npm run lint / npm run build
  • 3.0.1_resolveModuleName execution test passes (it passes without this change too, because Karma doesn't fail on type errors)

🤖 Generated with Claude Code

…he TypeScript 7.1 module resolver API

The 7.1 API accepts a custom module resolver (api.createModuleResolver)
on synthetic programs only. When `resolveModuleName` is set, every file
now compiles in ts-loader's existing synthetic ("orphan") program with
the resolver attached, adapting classic ts-loader's
(moduleName, containingFile, compilerOptions, compilerHost, parentResolver)
signature. `parentResolver` is backed by a second, default resolver.

Limitations: `containingFile` is a placeholder in the importing file's
directory (the API only reports the directory), `compilerHost` is
undefined, and only resolvedFileName/originalPath/packageId are passed
back to the API.

Adds a resolveModuleName comparison test that fails (TS2307) without
this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Benchmark (macOS)

Scenario transpileOnly PR branch median (ms) base branch median (ms) Δ vs base branch
Cold build false 1315.0 1283.8 +2.4%
Incremental rebuild (leaf touch) false 77.1 73.3 +5.3%
Incremental rebuild (hub touch) false 484.2 472.4 +2.5%
Cold build true 311.5 323.2 -3.6%
Incremental rebuild (leaf touch) true 51.8 50.9 +1.7%
Incremental rebuild (hub touch) true 51.3 51.7 -0.8%
Cold build (all orphan files) false 1137.9 1124.2 +1.2%
Incremental rebuild (hub touch, all orphan files) false 376.6 363.4 +3.6%

PR branch = /Users/runner/work/ts-loader/ts-loader, base branch = /Users/runner/work/ts-loader/ts-loader-main. 2 warmup + 10 measured iterations per scenario, median reported. Report-only - no threshold fails this check.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Benchmark (Ubuntu)

Scenario transpileOnly PR branch median (ms) base branch median (ms) Δ vs base branch
Cold build false 651.9 650.9 +0.2%
Incremental rebuild (leaf touch) false 37.1 36.7 +1.0%
Incremental rebuild (hub touch) false 269.9 267.6 +0.8%
Cold build true 235.9 238.5 -1.1%
Incremental rebuild (leaf touch) true 22.8 22.7 +0.6%
Incremental rebuild (hub touch) true 24.2 23.9 +1.3%
Cold build (all orphan files) false 686.2 685.8 +0.1%
Incremental rebuild (hub touch, all orphan files) false 290.1 289.0 +0.4%

PR branch = /home/runner/work/ts-loader/ts-loader, base branch = /home/runner/work/ts-loader/ts-loader-main. 2 warmup + 10 measured iterations per scenario, median reported. Report-only - no threshold fails this check.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Benchmark (Windows)

Scenario transpileOnly PR branch median (ms) base branch median (ms) Δ vs base branch
Cold build false 1794.0 1792.7 +0.1%
Incremental rebuild (leaf touch) false 62.3 66.1 -5.7%
Incremental rebuild (hub touch) false 746.5 750.4 -0.5%
Cold build true 487.8 486.7 +0.2%
Incremental rebuild (leaf touch) true 37.1 36.8 +0.8%
Incremental rebuild (hub touch) true 37.5 38.1 -1.6%
Cold build (all orphan files) false 2052.5 2037.4 +0.7%
Incremental rebuild (hub touch, all orphan files) false 847.9 858.7 -1.3%

PR branch = C:\source\ts-loader, base branch = C:\source\ts-loader-main. 2 warmup + 10 measured iterations per scenario, median reported. Report-only - no threshold fails this check.

@johnnyreilly

Copy link
Copy Markdown
Member Author

@copilot please fix knip issues

Co-authored-by: johnnyreilly <1010525+johnnyreilly@users.noreply.github.com>

Copilot AI commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

@copilot please fix knip issues

Fixed in d16d9ac. The only knip finding from my changes was the unused export CustomResolveModuleName in src/types.ts, which is no longer exported. Knip also lists unused devDependencies and unlisted binaries, but those come from node_modules not being installed in my environment, so I left them. I couldn't run tsc locally for the same reason.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants