Skip to content

Commit e54e4ef

Browse files
author
Sean Callahan
committed
Fix wildcard exports extensionless resolution in bundler mode
When using moduleResolution: 'bundler' with wildcard exports patterns like "./*": "./src/*", extensionless imports (e.g., import from "pkg/utils") now correctly resolve to TypeScript files (e.g., ./src/utils.ts). Previously, loadFileNameFromPackageJsonField would return undefined for extensionless paths without attempting to add extensions, even in bundler mode where extensionless imports are supposed to work. This fix adds extension probing (.ts, .tsx, .d.ts, .js, .jsx) for extensionless paths when not in ESM mode, mirroring the behavior of loadModuleFromFile for regular imports. Fixes #62909
1 parent b78f089 commit e54e4ef

20 files changed

+508
-12
lines changed

src/compiler/moduleNameResolver.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2114,13 +2114,28 @@ function loadFileNameFromPackageJsonField(extensions: Extensions, candidate: str
21142114
return result !== undefined ? { path: candidate, ext, resolvedUsingTsExtension: packageJsonValue ? !endsWith(packageJsonValue, ext) : undefined } : undefined;
21152115
}
21162116

2117-
if (state.isConfigLookup && extensions === Extensions.Json && fileExtensionIs(candidate, Extension.Json)) {
2118-
const result = tryFile(candidate, onlyRecordFailures, state);
2119-
return result !== undefined ? { path: candidate, ext: Extension.Json, resolvedUsingTsExtension: undefined } : undefined;
2120-
}
2121-
2122-
return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state);
2123-
}
2117+
if (state.isConfigLookup && extensions === Extensions.Json && fileExtensionIs(candidate, Extension.Json)) {
2118+
const result = tryFile(candidate, onlyRecordFailures, state);
2119+
return result !== undefined ? { path: candidate, ext: Extension.Json, resolvedUsingTsExtension: undefined } : undefined;
2120+
}
2121+
2122+
// First, try replacing an existing JS extension with a TS extension (e.g., ./foo.js -> ./foo.ts)
2123+
const resolvedByReplacingExtension = loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state);
2124+
if (resolvedByReplacingExtension) {
2125+
return resolvedByReplacingExtension;
2126+
}
2127+
2128+
// For extensionless paths in bundler mode (non-ESM), try adding extensions (e.g., ./foo -> ./foo.ts)
2129+
// This mirrors the behavior of loadModuleFromFile for regular imports.
2130+
if (!(state.features & NodeResolutionFeatures.EsmMode)) {
2131+
const filename = getBaseFileName(candidate);
2132+
if (!filename.includes(".")) {
2133+
return tryAddingExtensions(candidate, extensions, "", onlyRecordFailures, state);
2134+
}
2135+
}
2136+
2137+
return undefined;
2138+
}
21242139

21252140
/** Try to return an existing file that adds one of the `extensions` to `candidate`. */
21262141
function tryAddingExtensions(candidate: string, extensions: Extensions, originalExtension: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {

tests/baselines/reference/bundlerRelative1(module=esnext).trace.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
"'package.json' does not have a 'typings' field.",
4646
"'package.json' does not have a 'types' field.",
4747
"'package.json' has 'main' field '../foo' that references '/foo'.",
48+
"File '/foo.ts' does not exist.",
49+
"File '/foo.tsx' does not exist.",
50+
"File '/foo.d.ts' does not exist.",
51+
"File '/foo.js' does not exist.",
52+
"File '/foo.jsx' does not exist.",
4853
"Loading module as file / folder, candidate module location '/foo', target file types: TypeScript, JavaScript, Declaration, JSON.",
4954
"File '/foo.ts' does not exist.",
5055
"File '/foo.tsx' does not exist.",

tests/baselines/reference/bundlerRelative1(module=preserve).trace.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
"'package.json' does not have a 'typings' field.",
4646
"'package.json' does not have a 'types' field.",
4747
"'package.json' has 'main' field '../foo' that references '/foo'.",
48+
"File '/foo.ts' does not exist.",
49+
"File '/foo.tsx' does not exist.",
50+
"File '/foo.d.ts' does not exist.",
51+
"File '/foo.js' does not exist.",
52+
"File '/foo.jsx' does not exist.",
4853
"Loading module as file / folder, candidate module location '/foo', target file types: TypeScript, JavaScript, Declaration, JSON.",
4954
"File '/foo.ts' does not exist.",
5055
"File '/foo.tsx' does not exist.",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//// [tests/cases/compiler/bundlerWildcardExportsExtensionResolution.ts] ////
2+
3+
=== /node_modules/@repo/library/src/utils.ts ===
4+
export function greet(): string {
5+
>greet : Symbol(greet, Decl(utils.ts, 0, 0))
6+
7+
return "hello";
8+
}
9+
10+
=== /node_modules/@repo/library/src/component.tsx ===
11+
export const Component = () => null;
12+
>Component : Symbol(Component, Decl(component.tsx, 0, 12))
13+
14+
=== /node_modules/@repo/library/src/deep/nested/module.ts ===
15+
export const deep = true;
16+
>deep : Symbol(deep, Decl(module.ts, 0, 12))
17+
18+
=== /app.ts ===
19+
// These imports should all resolve successfully in bundler mode
20+
import { greet } from "@repo/library/utils";
21+
>greet : Symbol(greet, Decl(app.ts, 1, 8))
22+
23+
import { Component } from "@repo/library/component";
24+
>Component : Symbol(Component, Decl(app.ts, 2, 8))
25+
26+
import { deep } from "@repo/library/deep/nested/module";
27+
>deep : Symbol(deep, Decl(app.ts, 3, 8))
28+
29+
greet();
30+
>greet : Symbol(greet, Decl(app.ts, 1, 8))
31+
32+
Component;
33+
>Component : Symbol(Component, Decl(app.ts, 2, 8))
34+
35+
deep;
36+
>deep : Symbol(deep, Decl(app.ts, 3, 8))
37+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[
2+
"File '/node_modules/@repo/library/src/package.json' does not exist.",
3+
"Found 'package.json' at '/node_modules/@repo/library/package.json'.",
4+
"File '/node_modules/@repo/library/src/package.json' does not exist according to earlier cached lookups.",
5+
"File '/node_modules/@repo/library/package.json' exists according to earlier cached lookups.",
6+
"File '/node_modules/@repo/library/src/deep/nested/package.json' does not exist.",
7+
"File '/node_modules/@repo/library/src/deep/package.json' does not exist.",
8+
"File '/node_modules/@repo/library/src/package.json' does not exist according to earlier cached lookups.",
9+
"File '/node_modules/@repo/library/package.json' exists according to earlier cached lookups.",
10+
"======== Resolving module '@repo/library/utils' from '/app.ts'. ========",
11+
"Explicitly specified module resolution kind: 'Bundler'.",
12+
"Resolving in CJS mode with conditions 'import', 'types'.",
13+
"File '/package.json' does not exist.",
14+
"Loading module '@repo/library/utils' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.",
15+
"Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration.",
16+
"File '/node_modules/@repo/library/package.json' exists according to earlier cached lookups.",
17+
"Using 'exports' subpath './*' with target './src/utils'.",
18+
"File '/node_modules/@repo/library/src/utils.ts' exists - use it as a name resolution result.",
19+
"Resolving real path for '/node_modules/@repo/library/src/utils.ts', result '/node_modules/@repo/library/src/utils.ts'.",
20+
"======== Module name '@repo/library/utils' was successfully resolved to '/node_modules/@repo/library/src/utils.ts'. ========",
21+
"======== Resolving module '@repo/library/component' from '/app.ts'. ========",
22+
"Explicitly specified module resolution kind: 'Bundler'.",
23+
"Resolving in CJS mode with conditions 'import', 'types'.",
24+
"File '/package.json' does not exist according to earlier cached lookups.",
25+
"Loading module '@repo/library/component' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.",
26+
"Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration.",
27+
"File '/node_modules/@repo/library/package.json' exists according to earlier cached lookups.",
28+
"Using 'exports' subpath './*' with target './src/component'.",
29+
"File '/node_modules/@repo/library/src/component.ts' does not exist.",
30+
"File '/node_modules/@repo/library/src/component.tsx' exists - use it as a name resolution result.",
31+
"Resolving real path for '/node_modules/@repo/library/src/component.tsx', result '/node_modules/@repo/library/src/component.tsx'.",
32+
"======== Module name '@repo/library/component' was successfully resolved to '/node_modules/@repo/library/src/component.tsx'. ========",
33+
"======== Resolving module '@repo/library/deep/nested/module' from '/app.ts'. ========",
34+
"Explicitly specified module resolution kind: 'Bundler'.",
35+
"Resolving in CJS mode with conditions 'import', 'types'.",
36+
"File '/package.json' does not exist according to earlier cached lookups.",
37+
"Loading module '@repo/library/deep/nested/module' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.",
38+
"Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration.",
39+
"File '/node_modules/@repo/library/package.json' exists according to earlier cached lookups.",
40+
"Using 'exports' subpath './*' with target './src/deep/nested/module'.",
41+
"File '/node_modules/@repo/library/src/deep/nested/module.ts' exists - use it as a name resolution result.",
42+
"Resolving real path for '/node_modules/@repo/library/src/deep/nested/module.ts', result '/node_modules/@repo/library/src/deep/nested/module.ts'.",
43+
"======== Module name '@repo/library/deep/nested/module' was successfully resolved to '/node_modules/@repo/library/src/deep/nested/module.ts'. ========"
44+
]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//// [tests/cases/compiler/bundlerWildcardExportsExtensionResolution.ts] ////
2+
3+
=== /node_modules/@repo/library/src/utils.ts ===
4+
export function greet(): string {
5+
>greet : () => string
6+
> : ^^^^^^
7+
8+
return "hello";
9+
>"hello" : "hello"
10+
> : ^^^^^^^
11+
}
12+
13+
=== /node_modules/@repo/library/src/component.tsx ===
14+
export const Component = () => null;
15+
>Component : () => any
16+
> : ^^^^^^^^^
17+
>() => null : () => any
18+
> : ^^^^^^^^^
19+
20+
=== /node_modules/@repo/library/src/deep/nested/module.ts ===
21+
export const deep = true;
22+
>deep : true
23+
> : ^^^^
24+
>true : true
25+
> : ^^^^
26+
27+
=== /app.ts ===
28+
// These imports should all resolve successfully in bundler mode
29+
import { greet } from "@repo/library/utils";
30+
>greet : () => string
31+
> : ^^^^^^
32+
33+
import { Component } from "@repo/library/component";
34+
>Component : () => any
35+
> : ^^^^^^^^^
36+
37+
import { deep } from "@repo/library/deep/nested/module";
38+
>deep : true
39+
> : ^^^^
40+
41+
greet();
42+
>greet() : string
43+
> : ^^^^^^
44+
>greet : () => string
45+
> : ^^^^^^
46+
47+
Component;
48+
>Component : () => any
49+
> : ^^^^^^^^^
50+
51+
deep;
52+
>deep : true
53+
> : ^^^^
54+

tests/baselines/reference/moduleResolution/relative-module-name-as-directory-load-index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ Resolution:: {
2828
"/c/d.d.ts",
2929
"/c/d.js",
3030
"/c/d.jsx",
31+
"/c/d.ts",
32+
"/c/d.tsx",
33+
"/c/d.d.ts",
34+
"/c/d.js",
35+
"/c/d.jsx",
3136
"/c/d/index.ts",
3237
"/c/d/index.tsx",
3338
"/c/d/index.d.ts",
@@ -71,6 +76,11 @@ Resolution:: {
7176
"/c/d.d.ts",
7277
"/c/d.js",
7378
"/c/d.jsx",
79+
"/c/d.ts",
80+
"/c/d.tsx",
81+
"/c/d.d.ts",
82+
"/c/d.js",
83+
"/c/d.jsx",
7484
"/c/d/index.ts",
7585
"/c/d/index.tsx",
7686
"/c/d/index.d.ts",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/app.ts(2,23): error TS2307: Cannot find module '@repo/library/utils' or its corresponding type declarations.
2+
3+
4+
==== /node_modules/@repo/library/package.json (0 errors) ====
5+
{
6+
"name": "@repo/library",
7+
"type": "module",
8+
"exports": {
9+
"./*": "./src/*"
10+
}
11+
}
12+
13+
==== /node_modules/@repo/library/src/utils.ts (0 errors) ====
14+
export function greet(): string {
15+
return "hello";
16+
}
17+
18+
==== /package.json (0 errors) ====
19+
{
20+
"type": "module"
21+
}
22+
23+
==== /app.ts (1 errors) ====
24+
// This import should FAIL in node16 mode - extensionless imports not supported
25+
import { greet } from "@repo/library/utils";
26+
~~~~~~~~~~~~~~~~~~~~~
27+
!!! error TS2307: Cannot find module '@repo/library/utils' or its corresponding type declarations.
28+
29+
greet();
30+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//// [tests/cases/compiler/node16WildcardExportsRequiresExtension.ts] ////
2+
3+
=== /node_modules/@repo/library/src/utils.ts ===
4+
export function greet(): string {
5+
>greet : Symbol(greet, Decl(utils.ts, 0, 0))
6+
7+
return "hello";
8+
}
9+
10+
=== /app.ts ===
11+
// This import should FAIL in node16 mode - extensionless imports not supported
12+
import { greet } from "@repo/library/utils";
13+
>greet : Symbol(greet, Decl(app.ts, 1, 8))
14+
15+
greet();
16+
>greet : Symbol(greet, Decl(app.ts, 1, 8))
17+

0 commit comments

Comments
 (0)