With a custom .foo extension registered in contentMappers, native tsc resolves import "./card.foo", but reports TS2307 for import "./card" under moduleResolution: "bundler". The registered file is already in the program's files list. A bundler configured to resolve .foo handles the same extensionless import successfully.
Demo repo
Cloneable reproduction: https://gist.github.com/leonidaz/ca03b7192f722681735822341f50f741
The mapper is a small standalone Node.js program that returns the .foo source unchanged as TypeScript with an identity span map. No TSRX, Volar, framework, tsserver plugin, or generated declaration shim is involved. Type checking invokes native tsc directly; esbuild is used only for the separate runtime control.
git clone https://gist.github.com/leonidaz/ca03b7192f722681735822341f50f741.git content-mapper-extensionless-repro
cd content-mapper-extensionless-repro
pnpm install --ignore-scripts
pnpm run setup
pnpm exec tsc --version
# Fails: TS2307 for './card', exit 2.
pnpm run check
# Both controls pass, exit 0.
pnpm run check:explicit
pnpm run check:standard
# Same TS2307 even with both allow flags enabled.
pnpm exec tsc --runExternalCode --allowArbitraryExtensions --allowImportingTsExtensions --pretty false -p tsconfig.json
# Configured esbuild bundles the extensionless import; Node prints 42.
pnpm run runtime
setup.mjs only writes a local mapper package manifest under node_modules, pointing at the included mapper.mjs using the current Node executable and checkout path. pnpm run check is exactly tsc --runExternalCode --pretty false -p tsconfig.json.
Version and regression information
- Reproduced with
typescript@7.1.0-dev.20260929.1, the version advertised by typescript@next when checked on 2026-09-29.
pnpm exec tsc --version reports Version 7.1.0-dev.20260929.1.
- npm reports
gitHead: 0681ef7fa3a2378ccf49645b6d5b7463bdca74bb for this nightly and its platform package.
- Tested on macOS 26.6.2 x64, Node.js 24.18.0, pnpm 10.33.4; runtime control uses esbuild 0.28.2.
- Not claiming a regression from an earlier working content-mapper version. The failure and both passing TypeScript controls were also verified from a fresh clone of the linked reproduction.
Which problem is being reported?
The module specifier resolves with the configured runtime build, but not during TypeScript checking under moduleResolution: "bundler".
Code
// card.foo
export const value = 42 as const;
// main.ts
import { value } from "./card";
The full importer adds const check: 42 = value; console.log(check);. The explicit-import control changes only the import target to "./card.foo". A separate control imports a regular .ts file without its extension and passes.
The actual mapper's transform response is simply:
const text = request.params.content;
result = {
text,
extension: ".ts",
mappings: [[0, text.length, 0, text.length, 0]],
};
The complete JSON-RPC transport and manifest setup are included in the reproduction.
Configuration
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"types": []
},
"contentMappers": [
{
"package": "identity-foo-mapper",
"extensions": [
".foo"
]
}
],
"files": [
"main.ts",
"card.foo"
]
}
The runtime-control command is:
pnpm exec esbuild main.ts --bundle --platform=node --format=esm --loader:.foo=ts --resolve-extensions=.foo,.ts,.js --outfile=out.mjs
node out.mjs
# 42
This report concerns the configured bundler environment. It does not request relaxing Node ESM's explicit-extension rules or imply that a content mapper configures the runtime loader.
Actual behavior
main.ts(1,23): error TS2307: Cannot find module './card' or its corresponding type declarations.
| Case |
Result |
Registered/included card.foo, import ./card |
TS2307, exit 2 |
Same mapper and file, import ./card.foo |
Pass, exit 0 |
Regular standard-target.ts, import ./standard-target |
Pass, exit 0 |
Import ./card with allowArbitraryExtensions and allowImportingTsExtensions also enabled |
Same TS2307 |
Bundler configured with .foo in its extension search list |
Bundles and runs; prints 42 |
All checking commands use --runExternalCode. Omitting it correctly produces the separate diagnostic that content mappers require that flag; enabling it does not address the extensionless lookup.
Expected behavior
A way for registered content-mapper extensions to participate in extensionless relative lookup in resolution modes that support it, so the checker can match a bundler configured to resolve those extensions. In this fixture there are no competing card.ts, card.tsx, card.d.ts, or JavaScript files, so the request does not depend on deciding precedence between colliding files.
If explicit-only lookup for mapper extensions is intentional, please treat this as a request for supported/configurable extensionless lookup rather than a regression report. Registering the extension plus selecting bundler resolution currently does not provide it.
tsc --showConfig
Actual output of pnpm exec tsc --runExternalCode --showConfig -p tsconfig.json:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true,
"strict": true,
"target": "es2022",
"types": []
},
"files": [
"./main.ts",
"./card.foo"
]
}
This nightly's --showConfig omits contentMappers; the source config above contains the registration, and the passing explicit-import control verifies that the mapper is loaded.
tsc --traceResolution
Actual output of pnpm exec tsc --runExternalCode --pretty false -p tsconfig.json --traceResolution, with the temporary checkout root normalized to /repro:
======== Resolving module './card' from '/repro/main.ts'. ========
Explicitly specified module resolution kind: 'Bundler'.
Resolving in CJS mode with conditions 'import', 'types'.
Loading module as file / folder, candidate module location '/repro/card', target file types: TypeScript, JavaScript, Declaration, JSON.
File '/repro/card.ts' does not exist.
File '/repro/card.tsx' does not exist.
File '/repro/card.d.ts' does not exist.
File '/repro/card.js' does not exist.
File '/repro/card.jsx' does not exist.
Directory '/repro/card' does not exist, skipping all lookups in it.
======== Module name './card' was not resolved. ========
main.ts(1,23): error TS2307: Cannot find module './card' or its corresponding type declarations.
There is no probe for card.foo.
Importing and target package manifests
Both source files belong to the same reproduction package:
{
"name": "typescript-content-mapper-extensionless-repro",
"private": true,
"type": "module",
"scripts": {
"setup": "node setup.mjs",
"check": "tsc --runExternalCode --pretty false -p tsconfig.json",
"check:explicit": "tsc --runExternalCode --pretty false -p tsconfig.explicit.json",
"check:standard": "tsc --runExternalCode --pretty false -p tsconfig.standard.json",
"trace": "tsc --runExternalCode --pretty false -p tsconfig.json --traceResolution",
"runtime": "esbuild main.ts --bundle --platform=node --format=esm --loader:.foo=ts --resolve-extensions=.foo,.ts,.js --outfile=out.mjs && node out.mjs"
},
"devDependencies": {
"esbuild": "0.28.2",
"typescript": "7.1.0-dev.20260929.1"
}
}
The separate local mapper manifest is generated by the included setup.mjs; it declares typescript.contentMapper.exec and compilerOptions: [].
Source reference and related work
At main-branch commit 299a555c3a91519552b471c5b8ce3eb4247ab044, tryAddingExtensions handles an empty originalExtension using the built-in TypeScript/JavaScript candidates. The subsequent default branch checks extraExtensions when an extension is explicitly present. That appears to explain the trace. This is source inspection of main, distinct from the nightly binary tested above.
I searched issues and open PRs in microsoft/TypeScript and the earlier microsoft/typescript-go repository for content mapper, contentMappers, extensionless, extraExtensions, and related module-resolution terms. I did not find an open PR implementing this lookup:
The reproduction and report were prepared with Codex. The reported compiler and runtime results were executed and verified, including from a clean clone of the reproduction.
With a custom
.fooextension registered incontentMappers, nativetscresolvesimport "./card.foo", but reports TS2307 forimport "./card"undermoduleResolution: "bundler". The registered file is already in the program'sfileslist. A bundler configured to resolve.foohandles the same extensionless import successfully.Demo repo
Cloneable reproduction: https://gist.github.com/leonidaz/ca03b7192f722681735822341f50f741
The mapper is a small standalone Node.js program that returns the
.foosource unchanged as TypeScript with an identity span map. No TSRX, Volar, framework, tsserver plugin, or generated declaration shim is involved. Type checking invokes nativetscdirectly; esbuild is used only for the separate runtime control.setup.mjsonly writes a local mapper package manifest undernode_modules, pointing at the includedmapper.mjsusing the current Node executable and checkout path.pnpm run checkis exactlytsc --runExternalCode --pretty false -p tsconfig.json.Version and regression information
typescript@7.1.0-dev.20260929.1, the version advertised bytypescript@nextwhen checked on 2026-09-29.pnpm exec tsc --versionreportsVersion 7.1.0-dev.20260929.1.gitHead: 0681ef7fa3a2378ccf49645b6d5b7463bdca74bbfor this nightly and its platform package.Which problem is being reported?
The module specifier resolves with the configured runtime build, but not during TypeScript checking under
moduleResolution: "bundler".Code
The full importer adds
const check: 42 = value; console.log(check);. The explicit-import control changes only the import target to"./card.foo". A separate control imports a regular.tsfile without its extension and passes.The actual mapper's
transformresponse is simply:The complete JSON-RPC transport and manifest setup are included in the reproduction.
Configuration
tsconfig.json:{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "noEmit": true, "types": [] }, "contentMappers": [ { "package": "identity-foo-mapper", "extensions": [ ".foo" ] } ], "files": [ "main.ts", "card.foo" ] }The runtime-control command is:
This report concerns the configured bundler environment. It does not request relaxing Node ESM's explicit-extension rules or imply that a content mapper configures the runtime loader.
Actual behavior
card.foo, import./card./card.foostandard-target.ts, import./standard-target./cardwithallowArbitraryExtensionsandallowImportingTsExtensionsalso enabled.fooin its extension search list42All checking commands use
--runExternalCode. Omitting it correctly produces the separate diagnostic that content mappers require that flag; enabling it does not address the extensionless lookup.Expected behavior
A way for registered content-mapper extensions to participate in extensionless relative lookup in resolution modes that support it, so the checker can match a bundler configured to resolve those extensions. In this fixture there are no competing
card.ts,card.tsx,card.d.ts, or JavaScript files, so the request does not depend on deciding precedence between colliding files.If explicit-only lookup for mapper extensions is intentional, please treat this as a request for supported/configurable extensionless lookup rather than a regression report. Registering the extension plus selecting bundler resolution currently does not provide it.
tsc --showConfigActual output of
pnpm exec tsc --runExternalCode --showConfig -p tsconfig.json:{ "compilerOptions": { "module": "esnext", "moduleResolution": "bundler", "noEmit": true, "strict": true, "target": "es2022", "types": [] }, "files": [ "./main.ts", "./card.foo" ] }This nightly's
--showConfigomitscontentMappers; the source config above contains the registration, and the passing explicit-import control verifies that the mapper is loaded.tsc --traceResolutionActual output of
pnpm exec tsc --runExternalCode --pretty false -p tsconfig.json --traceResolution, with the temporary checkout root normalized to/repro:There is no probe for
card.foo.Importing and target package manifests
Both source files belong to the same reproduction package:
{ "name": "typescript-content-mapper-extensionless-repro", "private": true, "type": "module", "scripts": { "setup": "node setup.mjs", "check": "tsc --runExternalCode --pretty false -p tsconfig.json", "check:explicit": "tsc --runExternalCode --pretty false -p tsconfig.explicit.json", "check:standard": "tsc --runExternalCode --pretty false -p tsconfig.standard.json", "trace": "tsc --runExternalCode --pretty false -p tsconfig.json --traceResolution", "runtime": "esbuild main.ts --bundle --platform=node --format=esm --loader:.foo=ts --resolve-extensions=.foo,.ts,.js --outfile=out.mjs && node out.mjs" }, "devDependencies": { "esbuild": "0.28.2", "typescript": "7.1.0-dev.20260929.1" } }The separate local mapper manifest is generated by the included
setup.mjs; it declarestypescript.contentMapper.execandcompilerOptions: [].Source reference and related work
At main-branch commit
299a555c3a91519552b471c5b8ce3eb4247ab044,tryAddingExtensionshandles an emptyoriginalExtensionusing the built-in TypeScript/JavaScript candidates. The subsequent default branch checksextraExtensionswhen an extension is explicitly present. That appears to explain the trace. This is source inspection of main, distinct from the nightly binary tested above.I searched issues and open PRs in
microsoft/TypeScriptand the earliermicrosoft/typescript-gorepository forcontent mapper,contentMappers,extensionless,extraExtensions, and related module-resolution terms. I did not find an open PR implementing this lookup:The reproduction and report were prepared with Codex. The reported compiler and runtime results were executed and verified, including from a clean clone of the reproduction.