Skip to content

Commit 9722550

Browse files
committed
add extension alias
add new option extensionAlias which maps extension to extension alias
1 parent ddc96f8 commit 9722550

File tree

10 files changed

+176
-2
lines changed

10 files changed

+176
-2
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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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: aliasArray } = this.options;
34+
resolver
35+
.getHook(this.source)
36+
.tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => {
37+
const requestPath = request.path;
38+
if (!requestPath || !requestPath.endsWith(extension)) return callback();
39+
const resolve = (alias, callback) => {
40+
resolver.doResolve(
41+
target,
42+
{
43+
...request,
44+
path: `${requestPath.slice(0, -extension.length)}${alias}`,
45+
relativePath: request.relativePath
46+
? `${request.relativePath.slice(0, -extension.length)}${alias}`
47+
: request.relativePath
48+
},
49+
`aliased from extension alias with mapping '${extension}' to '${alias}'`,
50+
resolveContext,
51+
callback
52+
);
53+
};
54+
55+
if (aliasArray.length > 1) {
56+
forEachBail(aliasArray, resolve, callback);
57+
} else {
58+
resolve(aliasArray[0], callback);
59+
}
60+
});
61+
}
62+
};

lib/ResolverFactory.js

Lines changed: 17 additions & 0 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[] }} 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,
@@ -602,6 +616,9 @@ exports.createResolver = function (options) {
602616
aliasFields.forEach(item => {
603617
plugins.push(new AliasFieldPlugin("file", item, "internal-resolve"));
604618
});
619+
extensionAlias.forEach(item =>
620+
plugins.push(new ExtensionAliasPlugin("file", item, "final-file"))
621+
);
605622
plugins.push(new NextPlugin("file", "final-file"));
606623

607624
// final-file

test/extension-alias.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
}
21+
});
22+
23+
it("should alias fully specified file", done => {
24+
resolver.resolve({}, fixture, "./index.js", {}, (err, result) => {
25+
if (err) return done(err);
26+
should(result).be.eql(path.resolve(fixture, "index.ts"));
27+
done();
28+
});
29+
});
30+
31+
it("should alias specified extension", done => {
32+
resolver.resolve({}, fixture, "./dir", {}, (err, result) => {
33+
if (err) return done(err);
34+
should(result).be.eql(path.resolve(fixture, "dir", "index.ts"));
35+
done();
36+
});
37+
});
38+
39+
it("should result successfully without aliasing #1", done => {
40+
resolver.resolve({}, fixture, "./dir2", {}, (err, result) => {
41+
if (err) return done(err);
42+
should(result).be.eql(path.resolve(fixture, "dir2", "index.js"));
43+
done();
44+
});
45+
});
46+
47+
it("should result successfully without aliasing #2", done => {
48+
resolver.resolve({}, fixture, "./dir2/index.js", {}, (err, result) => {
49+
if (err) return done(err);
50+
should(result).be.eql(path.resolve(fixture, "dir2", "index.js"));
51+
done();
52+
});
53+
});
54+
55+
describe("should result successfully without alias array", () => {
56+
const resolver = ResolverFactory.createResolver({
57+
extensions: [".js"],
58+
fileSystem: nodeFileSystem,
59+
mainFiles: ["index.js"],
60+
extensionAlias: {
61+
".js": []
62+
}
63+
});
64+
65+
it("directory", done => {
66+
resolver.resolve({}, fixture, "./dir2", {}, (err, result) => {
67+
if (err) return done(err);
68+
should(result).be.eql(path.resolve(fixture, "dir2", "index.js"));
69+
done();
70+
});
71+
});
72+
73+
it("file", done => {
74+
resolver.resolve({}, fixture, "./dir2/index.js", {}, (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+
});

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/index.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[];
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)