Skip to content

Commit f9f6d57

Browse files
Merge pull request #554 from webpack/claude/fix-extension-alias-alignment-xC6sZ
2 parents f5adeee + c1319d1 commit f9f6d57

8 files changed

Lines changed: 145 additions & 33 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"enhanced-resolve": minor
3+
---
4+
5+
Add `extensionAliasForExports` option. When `true`, `extensionAlias` also applies to paths resolved through the `package.json` `exports` field. Off by default to match Node.js; opt in for full TypeScript-resolver parity with packages that ship `.ts` sources alongside the compiled `.js` they declare in `exports`.

README.md

Lines changed: 42 additions & 32 deletions
Large diffs are not rendered by default.

lib/ResolverFactory.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const { PathType, getType } = require("./util/path");
6767
* @property {(AliasOptions | AliasOptionEntry[])=} alias A list of module alias configurations or an object which maps key to value
6868
* @property {(AliasOptions | AliasOptionEntry[])=} fallback A list of module alias configurations or an object which maps key to value, applied only after modules option
6969
* @property {ExtensionAliasOptions=} extensionAlias An object which maps extension to extension aliases
70+
* @property {boolean=} extensionAliasForExports Also apply `extensionAlias` to paths resolved through the package.json `exports` field. Off by default (Node.js-aligned); when enabled, matches TypeScript's behavior for packages that ship TS sources alongside compiled JS.
7071
* @property {(string | string[])[]=} aliasFields A list of alias fields in description files
7172
* @property {((predicate: ResolveRequest) => boolean)=} cachePredicate A function which decides whether a request should be cached or not. An object is passed with at least `path` and `request` properties.
7273
* @property {boolean=} cacheWithContext Whether or not the unsafeCache should include request context as part of the cache key.
@@ -101,6 +102,7 @@ const { PathType, getType } = require("./util/path");
101102
* @property {AliasOptionEntry[]} fallback fallback
102103
* @property {Set<string | string[]>} aliasFields alias fields
103104
* @property {ExtensionAliasOption[]} extensionAlias extension alias
105+
* @property {boolean} extensionAliasForExports apply extension alias to exports field targets
104106
* @property {(predicate: ResolveRequest) => boolean} cachePredicate cache predicate
105107
* @property {boolean} cacheWithContext cache with context
106108
* @property {Set<string>} conditionNames A list of exports field condition names.
@@ -270,6 +272,7 @@ function createOptions(options) {
270272
],
271273
}))
272274
: [],
275+
extensionAliasForExports: options.extensionAliasForExports || false,
273276
fileSystem: options.useSyncFileSystemCalls
274277
? new SyncAsyncFileSystemDecorator(
275278
/** @type {SyncFileSystem} */ (
@@ -320,6 +323,7 @@ module.exports.createResolver = function createResolver(options) {
320323
alias,
321324
fallback,
322325
aliasFields,
326+
extensionAliasForExports,
323327
cachePredicate,
324328
cacheWithContext,
325329
conditionNames,
@@ -371,6 +375,9 @@ module.exports.createResolver = function createResolver(options) {
371375
resolver.ensureHook("resolveInPackage");
372376
resolver.ensureHook("resolveInExistingDirectory");
373377
resolver.ensureHook("importsFieldRelative");
378+
if (extensionAliasForExports) {
379+
resolver.ensureHook("exportsFieldRelative");
380+
}
374381
resolver.ensureHook("relative");
375382
resolver.ensureHook("describedRelative");
376383
resolver.ensureHook("directory");
@@ -595,20 +602,37 @@ module.exports.createResolver = function createResolver(options) {
595602
);
596603

597604
// resolve-in-package
605+
const exportsFieldTarget = extensionAliasForExports
606+
? "exports-field-relative"
607+
: "relative";
598608
for (const exportsField of exportsFields) {
599609
plugins.push(
600610
new ExportsFieldPlugin(
601611
"resolve-in-package",
602612
conditionNames,
603613
exportsField,
604-
"relative",
614+
exportsFieldTarget,
605615
),
606616
);
607617
}
608618
plugins.push(
609619
new NextPlugin("resolve-in-package", "resolve-in-existing-directory"),
610620
);
611621

622+
// exports-field-relative (opt-in via `extensionAliasForExports`):
623+
// apply `extensionAlias` to paths produced by the exports field. This is
624+
// off by default to match Node.js (which does not substitute extensions on
625+
// bare-module targets), and on opt-in aligns with TypeScript for packages
626+
// that ship TS sources alongside the compiled JS they list in `exports`.
627+
if (extensionAliasForExports) {
628+
for (const item of extensionAlias) {
629+
plugins.push(
630+
new ExtensionAliasPlugin("exports-field-relative", item, "relative"),
631+
);
632+
}
633+
plugins.push(new NextPlugin("exports-field-relative", "relative"));
634+
}
635+
612636
// resolve-in-existing-directory
613637
plugins.push(
614638
new JoinRequestPlugin("resolve-in-existing-directory", "relative"),

test/extension-alias.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,61 @@ describe("extension-alias", () => {
131131
});
132132
});
133133

134+
describe("exports field (extensionAliasForExports)", () => {
135+
const exportsFixture = path.resolve(
136+
__dirname,
137+
"fixtures",
138+
"exports-field-extension-alias-opt-in",
139+
);
140+
141+
it("should not apply extension alias to exports-field targets by default (Node.js-aligned)", (done) => {
142+
const defaultResolver = ResolverFactory.createResolver({
143+
extensions: [".js"],
144+
extensionAlias: { ".js": [".ts", ".js"] },
145+
fileSystem: nodeFileSystem,
146+
fullySpecified: true,
147+
conditionNames: ["default"],
148+
});
149+
defaultResolver.resolve(
150+
{},
151+
exportsFixture,
152+
"pkg/string.js",
153+
{},
154+
(err, result) => {
155+
if (err) return done(err);
156+
expect(result).toEqual(
157+
path.resolve(exportsFixture, "./node_modules/pkg/dist/string.js"),
158+
);
159+
done();
160+
},
161+
);
162+
});
163+
164+
it("should prefer the TS source over the exports-declared JS target when the option is enabled", (done) => {
165+
const tsResolver = ResolverFactory.createResolver({
166+
extensions: [".js"],
167+
extensionAlias: { ".js": [".ts", ".js"] },
168+
extensionAliasForExports: true,
169+
fileSystem: nodeFileSystem,
170+
fullySpecified: true,
171+
conditionNames: ["default"],
172+
});
173+
tsResolver.resolve(
174+
{},
175+
exportsFixture,
176+
"pkg/string.js",
177+
{},
178+
(err, result) => {
179+
if (err) return done(err);
180+
expect(result).toEqual(
181+
path.resolve(exportsFixture, "./node_modules/pkg/dist/string.ts"),
182+
);
183+
done();
184+
},
185+
);
186+
});
187+
});
188+
134189
describe("should not apply extension alias to extensions or mainFiles field", () => {
135190
const resolver = ResolverFactory.createResolver({
136191
extensions: [".js"],

test/fixtures/exports-field-extension-alias-opt-in/node_modules/pkg/dist/string.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/exports-field-extension-alias-opt-in/node_modules/pkg/dist/string.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/exports-field-extension-alias-opt-in/node_modules/pkg/package.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

types.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,11 @@ declare interface ResolveOptionsResolverFactoryObject_1 {
12811281
*/
12821282
extensionAlias: ExtensionAliasOption[];
12831283

1284+
/**
1285+
* apply extension alias to exports field targets
1286+
*/
1287+
extensionAliasForExports: boolean;
1288+
12841289
/**
12851290
* cache predicate
12861291
*/
@@ -1417,6 +1422,11 @@ declare interface ResolveOptionsResolverFactoryObject_2 {
14171422
*/
14181423
extensionAlias?: ExtensionAliasOptions;
14191424

1425+
/**
1426+
* Also apply `extensionAlias` to paths resolved through the package.json `exports` field. Off by default (Node.js-aligned); when enabled, matches TypeScript's behavior for packages that ship TS sources alongside compiled JS.
1427+
*/
1428+
extensionAliasForExports?: boolean;
1429+
14201430
/**
14211431
* A list of alias fields in description files
14221432
*/

0 commit comments

Comments
 (0)