Skip to content

Commit d5731ec

Browse files
authored
Merge pull request #351 from webpack/extension-alias
add extension alias
2 parents 8d05604 + 16225de commit d5731ec

File tree

13 files changed

+203
-9
lines changed

13 files changed

+203
-9
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ myResolver.resolve({}, lookupStartPath, request, resolveContext, (
7777
#### Resolver Options
7878

7979
| Field | Default | Description |
80-
| ---------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
80+
|------------------|-----------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------- |
8181
| alias | [] | A list of module alias configurations or an object which maps key to value |
8282
| aliasFields | [] | A list of alias fields in description files |
83+
| extensionAlias | {} | An object which maps extension to extension aliases |
8384
| cachePredicate | function() { return true }; | A function which decides whether a request should be cached or not. An object is passed to the function with `path` and `request` properties. |
8485
| cacheWithContext | true | If unsafe cache is enabled, includes `request.context` in the cache key |
8586
| conditionNames | ["node"] | A list of exports field condition names |
@@ -142,7 +143,7 @@ enhanced-resolve will try to resolve requests containing `#` as path and as frag
142143
## Tests
143144

144145
```javascript
145-
npm test
146+
yarn test
146147
```
147148

148149
[![Build Status](https://secure.travis-ci.org/webpack/enhanced-resolve.png?branch=main)](http://travis-ci.org/webpack/enhanced-resolve)

lib/ExtensionAliasPlugin.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Ivan Kopeykin @vankop
4+
*/
5+
6+
"use strict";
7+
8+
const forEachBail = require("./forEachBail");
9+
10+
/** @typedef {import("./Resolver")} Resolver */
11+
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
12+
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
13+
/** @typedef {{ alias: string|string[], extension: string }} ExtensionAliasOption */
14+
15+
module.exports = class ExtensionAliasPlugin {
16+
/**
17+
* @param {string | ResolveStepHook} source source
18+
* @param {ExtensionAliasOption} options options
19+
* @param {string | ResolveStepHook} target target
20+
*/
21+
constructor(source, options, target) {
22+
this.source = source;
23+
this.options = options;
24+
this.target = target;
25+
}
26+
27+
/**
28+
* @param {Resolver} resolver the resolver
29+
* @returns {void}
30+
*/
31+
apply(resolver) {
32+
const target = resolver.ensureHook(this.target);
33+
const { extension, alias } = this.options;
34+
resolver
35+
.getHook(this.source)
36+
.tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => {
37+
const requestPath = request.request;
38+
if (!requestPath || !requestPath.endsWith(extension)) return callback();
39+
const resolve = (alias, callback) => {
40+
resolver.doResolve(
41+
target,
42+
{
43+
...request,
44+
request: `${requestPath.slice(0, -extension.length)}${alias}`,
45+
fullySpecified: true
46+
},
47+
`aliased from extension alias with mapping '${extension}' to '${alias}'`,
48+
resolveContext,
49+
callback
50+
);
51+
};
52+
53+
const stoppingCallback = (err, result) => {
54+
if (err) return callback(err);
55+
if (result) return callback(null, result);
56+
// Don't allow other aliasing or raw request
57+
return callback(null, null);
58+
};
59+
if (typeof alias === "string") {
60+
resolve(alias, stoppingCallback);
61+
} else if (alias.length > 1) {
62+
forEachBail(alias, resolve, stoppingCallback);
63+
} else {
64+
resolve(alias[0], stoppingCallback);
65+
}
66+
});
67+
}
68+
};

lib/ResolverFactory.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const ConditionalPlugin = require("./ConditionalPlugin");
1818
const DescriptionFilePlugin = require("./DescriptionFilePlugin");
1919
const DirectoryExistsPlugin = require("./DirectoryExistsPlugin");
2020
const ExportsFieldPlugin = require("./ExportsFieldPlugin");
21+
const ExtensionAliasPlugin = require("./ExtensionAliasPlugin");
2122
const FileExistsPlugin = require("./FileExistsPlugin");
2223
const ImportsFieldPlugin = require("./ImportsFieldPlugin");
2324
const JoinRequestPartPlugin = require("./JoinRequestPartPlugin");
@@ -38,19 +39,22 @@ const UnsafeCachePlugin = require("./UnsafeCachePlugin");
3839
const UseFilePlugin = require("./UseFilePlugin");
3940

4041
/** @typedef {import("./AliasPlugin").AliasOption} AliasOptionEntry */
42+
/** @typedef {import("./ExtensionAliasPlugin").ExtensionAliasOption} ExtensionAliasOption */
4143
/** @typedef {import("./PnpPlugin").PnpApiImpl} PnpApi */
4244
/** @typedef {import("./Resolver").FileSystem} FileSystem */
4345
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
4446
/** @typedef {import("./Resolver").SyncFileSystem} SyncFileSystem */
4547

4648
/** @typedef {string|string[]|false} AliasOptionNewRequest */
4749
/** @typedef {{[k: string]: AliasOptionNewRequest}} AliasOptions */
50+
/** @typedef {{[k: string]: string|string[] }} ExtensionAliasOptions */
4851
/** @typedef {{apply: function(Resolver): void} | function(this: Resolver, Resolver): void} Plugin */
4952

5053
/**
5154
* @typedef {Object} UserResolveOptions
5255
* @property {(AliasOptions | AliasOptionEntry[])=} alias A list of module alias configurations or an object which maps key to value
5356
* @property {(AliasOptions | AliasOptionEntry[])=} fallback A list of module alias configurations or an object which maps key to value, applied only after modules option
57+
* @property {ExtensionAliasOptions=} extensionAlias An object which maps extension to extension aliases
5458
* @property {(string | string[])[]=} aliasFields A list of alias fields in description files
5559
* @property {(function(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.
5660
* @property {boolean=} cacheWithContext Whether or not the unsafeCache should include request context as part of the cache key.
@@ -83,6 +87,7 @@ const UseFilePlugin = require("./UseFilePlugin");
8387
* @property {AliasOptionEntry[]} alias
8488
* @property {AliasOptionEntry[]} fallback
8589
* @property {Set<string | string[]>} aliasFields
90+
* @property {ExtensionAliasOption[]} extensionAlias
8691
* @property {(function(ResolveRequest): boolean)} cachePredicate
8792
* @property {boolean} cacheWithContext
8893
* @property {Set<string>} conditionNames A list of exports field condition names.
@@ -197,6 +202,14 @@ function createOptions(options) {
197202
: false
198203
: options.enforceExtension,
199204
extensions: new Set(options.extensions || [".js", ".json", ".node"]),
205+
extensionAlias: options.extensionAlias
206+
? Object.keys(options.extensionAlias).map(k => ({
207+
extension: k,
208+
alias: /** @type {ExtensionAliasOptions} */ (options.extensionAlias)[
209+
k
210+
]
211+
}))
212+
: [],
200213
fileSystem: options.useSyncFileSystemCalls
201214
? new SyncAsyncFileSystemDecorator(
202215
/** @type {SyncFileSystem} */ (
@@ -251,6 +264,7 @@ exports.createResolver = function (options) {
251264
descriptionFiles,
252265
enforceExtension,
253266
exportsFields,
267+
extensionAlias,
254268
importsFields,
255269
extensions,
256270
fileSystem,
@@ -283,6 +297,8 @@ exports.createResolver = function (options) {
283297
resolver.ensureHook("newInternalResolve");
284298
resolver.ensureHook("parsedResolve");
285299
resolver.ensureHook("describedResolve");
300+
resolver.ensureHook("rawResolve");
301+
resolver.ensureHook("normalResolve");
286302
resolver.ensureHook("internal");
287303
resolver.ensureHook("rawModule");
288304
resolver.ensureHook("module");
@@ -342,21 +358,28 @@ exports.createResolver = function (options) {
342358
plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
343359

344360
// described-resolve
345-
plugins.push(new NextPlugin("described-resolve", "normal-resolve"));
361+
plugins.push(new NextPlugin("described-resolve", "raw-resolve"));
346362
if (fallback.length > 0) {
347363
plugins.push(
348364
new AliasPlugin("described-resolve", fallback, "internal-resolve")
349365
);
350366
}
351367

352-
// normal-resolve
353-
if (alias.length > 0)
354-
plugins.push(new AliasPlugin("normal-resolve", alias, "internal-resolve"));
368+
// raw-resolve
369+
if (alias.length > 0) {
370+
plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve"));
371+
}
355372
aliasFields.forEach(item => {
356-
plugins.push(
357-
new AliasFieldPlugin("normal-resolve", item, "internal-resolve")
358-
);
373+
plugins.push(new AliasFieldPlugin("raw-resolve", item, "internal-resolve"));
359374
});
375+
extensionAlias.forEach(item =>
376+
plugins.push(
377+
new ExtensionAliasPlugin("raw-resolve", item, "normal-resolve")
378+
)
379+
);
380+
plugins.push(new NextPlugin("raw-resolve", "normal-resolve"));
381+
382+
// normal-resolve
360383
if (preferRelative) {
361384
plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative"));
362385
}

test/extension-alias.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
const path = require("path");
2+
const fs = require("fs");
3+
const should = require("should");
4+
5+
const CachedInputFileSystem = require("../lib/CachedInputFileSystem");
6+
const ResolverFactory = require("../lib/ResolverFactory");
7+
8+
/** @typedef {import("../lib/util/entrypoints").ImportsField} ImportsField */
9+
10+
describe("extension-alias", () => {
11+
const fixture = path.resolve(__dirname, "fixtures", "extension-alias");
12+
const nodeFileSystem = new CachedInputFileSystem(fs, 4000);
13+
14+
const resolver = ResolverFactory.createResolver({
15+
extensions: [".js"],
16+
fileSystem: nodeFileSystem,
17+
mainFiles: ["index.js"],
18+
extensionAlias: {
19+
".js": [".ts", ".js"],
20+
".mjs": ".mts"
21+
}
22+
});
23+
24+
it("should alias fully specified file", done => {
25+
resolver.resolve({}, fixture, "./index.js", {}, (err, result) => {
26+
if (err) return done(err);
27+
should(result).be.eql(path.resolve(fixture, "index.ts"));
28+
done();
29+
});
30+
});
31+
32+
it("should alias fully specified file when there are two alternatives", done => {
33+
resolver.resolve({}, fixture, "./dir/index.js", {}, (err, result) => {
34+
if (err) return done(err);
35+
should(result).be.eql(path.resolve(fixture, "dir", "index.ts"));
36+
done();
37+
});
38+
});
39+
40+
it("should also allow the second alternative", done => {
41+
resolver.resolve({}, fixture, "./dir2/index.js", {}, (err, result) => {
42+
if (err) return done(err);
43+
should(result).be.eql(path.resolve(fixture, "dir2", "index.js"));
44+
done();
45+
});
46+
});
47+
48+
it("should support alias option without an array", done => {
49+
resolver.resolve({}, fixture, "./dir2/index.mjs", {}, (err, result) => {
50+
if (err) return done(err);
51+
should(result).be.eql(path.resolve(fixture, "dir2", "index.mts"));
52+
done();
53+
});
54+
});
55+
56+
it("should not allow to fallback to the original extension or add extensions", done => {
57+
resolver.resolve({}, fixture, "./index.mjs", {}, (err, result) => {
58+
should(err).be.instanceOf(Error);
59+
done();
60+
});
61+
});
62+
63+
describe("should not apply extension alias to extensions or mainFiles field", () => {
64+
const resolver = ResolverFactory.createResolver({
65+
extensions: [".js"],
66+
fileSystem: nodeFileSystem,
67+
mainFiles: ["index.js"],
68+
extensionAlias: {
69+
".js": []
70+
}
71+
});
72+
73+
it("directory", done => {
74+
resolver.resolve({}, fixture, "./dir2", {}, (err, result) => {
75+
if (err) return done(err);
76+
should(result).be.eql(path.resolve(fixture, "dir2", "index.js"));
77+
done();
78+
});
79+
});
80+
81+
it("file", done => {
82+
resolver.resolve({}, fixture, "./dir2/index", {}, (err, result) => {
83+
if (err) return done(err);
84+
should(result).be.eql(path.resolve(fixture, "dir2", "index.js"));
85+
done();
86+
});
87+
});
88+
});
89+
});

test/fixtures/extension-alias/dir/index.js

Whitespace-only changes.

test/fixtures/extension-alias/dir/index.ts

Whitespace-only changes.

test/fixtures/extension-alias/dir2/index.js

Whitespace-only changes.

test/fixtures/extension-alias/dir2/index.mts

Whitespace-only changes.

test/fixtures/extension-alias/index.js

Whitespace-only changes.

test/fixtures/extension-alias/index.mjs

Whitespace-only changes.

test/fixtures/extension-alias/index.mts.js

Whitespace-only changes.

test/fixtures/extension-alias/index.ts

Whitespace-only changes.

types.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ declare class CloneBasenamePlugin {
9191
target: any;
9292
apply(resolver: Resolver): void;
9393
}
94+
declare interface ExtensionAliasOption {
95+
alias: string | string[];
96+
extension: string;
97+
}
98+
declare interface ExtensionAliasOptions {
99+
[index: string]: string | string[];
100+
}
94101
declare interface FileSystem {
95102
readFile: {
96103
(arg0: string, arg1: FileSystemCallback<string | Buffer>): void;
@@ -214,6 +221,7 @@ declare interface ResolveOptions {
214221
alias: AliasOption[];
215222
fallback: AliasOption[];
216223
aliasFields: Set<string | string[]>;
224+
extensionAlias: ExtensionAliasOption[];
217225
cachePredicate: (arg0: ResolveRequest) => boolean;
218226
cacheWithContext: boolean;
219227

@@ -322,6 +330,11 @@ declare interface UserResolveOptions {
322330
*/
323331
fallback?: AliasOptions | AliasOption[];
324332

333+
/**
334+
* An object which maps extension to extension aliases
335+
*/
336+
extensionAlias?: ExtensionAliasOptions;
337+
325338
/**
326339
* A list of alias fields in description files
327340
*/

0 commit comments

Comments
 (0)