From 97225500f62f9e9d88ed3f7204eda0ffcd208cc7 Mon Sep 17 00:00:00 2001 From: Ivan Kopeykin Date: Mon, 27 Jun 2022 12:12:38 +0300 Subject: [PATCH 1/2] add extension alias add new option extensionAlias which maps extension to extension alias --- README.md | 5 +- lib/ExtensionAliasPlugin.js | 62 ++++++++++++++++ lib/ResolverFactory.js | 17 +++++ test/extension-alias.js | 81 +++++++++++++++++++++ test/fixtures/extension-alias/dir/index.js | 0 test/fixtures/extension-alias/dir/index.ts | 0 test/fixtures/extension-alias/dir2/index.js | 0 test/fixtures/extension-alias/index.js | 0 test/fixtures/extension-alias/index.ts | 0 types.d.ts | 13 ++++ 10 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 lib/ExtensionAliasPlugin.js create mode 100644 test/extension-alias.js create mode 100644 test/fixtures/extension-alias/dir/index.js create mode 100644 test/fixtures/extension-alias/dir/index.ts create mode 100644 test/fixtures/extension-alias/dir2/index.js create mode 100644 test/fixtures/extension-alias/index.js create mode 100644 test/fixtures/extension-alias/index.ts diff --git a/README.md b/README.md index ad512e55..644305c8 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,10 @@ myResolver.resolve({}, lookupStartPath, request, resolveContext, ( #### Resolver Options | Field | Default | Description | -| ---------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +|------------------|-----------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------- | | alias | [] | A list of module alias configurations or an object which maps key to value | | aliasFields | [] | A list of alias fields in description files | +| extensionAlias | {} | An object which maps extension to extension aliases | | 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. | | cacheWithContext | true | If unsafe cache is enabled, includes `request.context` in the cache key | | 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 ## Tests ```javascript -npm test +yarn test ``` [![Build Status](https://secure.travis-ci.org/webpack/enhanced-resolve.png?branch=main)](http://travis-ci.org/webpack/enhanced-resolve) diff --git a/lib/ExtensionAliasPlugin.js b/lib/ExtensionAliasPlugin.js new file mode 100644 index 00000000..78bae8e3 --- /dev/null +++ b/lib/ExtensionAliasPlugin.js @@ -0,0 +1,62 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +const forEachBail = require("./forEachBail"); + +/** @typedef {import("./Resolver")} Resolver */ +/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */ +/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */ +/** @typedef {{ alias: string|string[], extension: string }} ExtensionAliasOption */ + +module.exports = class ExtensionAliasPlugin { + /** + * @param {string | ResolveStepHook} source source + * @param {ExtensionAliasOption} options options + * @param {string | ResolveStepHook} target target + */ + constructor(source, options, target) { + this.source = source; + this.options = options; + this.target = target; + } + + /** + * @param {Resolver} resolver the resolver + * @returns {void} + */ + apply(resolver) { + const target = resolver.ensureHook(this.target); + const { extension, alias: aliasArray } = this.options; + resolver + .getHook(this.source) + .tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => { + const requestPath = request.path; + if (!requestPath || !requestPath.endsWith(extension)) return callback(); + const resolve = (alias, callback) => { + resolver.doResolve( + target, + { + ...request, + path: `${requestPath.slice(0, -extension.length)}${alias}`, + relativePath: request.relativePath + ? `${request.relativePath.slice(0, -extension.length)}${alias}` + : request.relativePath + }, + `aliased from extension alias with mapping '${extension}' to '${alias}'`, + resolveContext, + callback + ); + }; + + if (aliasArray.length > 1) { + forEachBail(aliasArray, resolve, callback); + } else { + resolve(aliasArray[0], callback); + } + }); + } +}; diff --git a/lib/ResolverFactory.js b/lib/ResolverFactory.js index 7a290ded..fa37ae19 100644 --- a/lib/ResolverFactory.js +++ b/lib/ResolverFactory.js @@ -18,6 +18,7 @@ const ConditionalPlugin = require("./ConditionalPlugin"); const DescriptionFilePlugin = require("./DescriptionFilePlugin"); const DirectoryExistsPlugin = require("./DirectoryExistsPlugin"); const ExportsFieldPlugin = require("./ExportsFieldPlugin"); +const ExtensionAliasPlugin = require("./ExtensionAliasPlugin"); const FileExistsPlugin = require("./FileExistsPlugin"); const ImportsFieldPlugin = require("./ImportsFieldPlugin"); const JoinRequestPartPlugin = require("./JoinRequestPartPlugin"); @@ -38,6 +39,7 @@ const UnsafeCachePlugin = require("./UnsafeCachePlugin"); const UseFilePlugin = require("./UseFilePlugin"); /** @typedef {import("./AliasPlugin").AliasOption} AliasOptionEntry */ +/** @typedef {import("./ExtensionAliasPlugin").ExtensionAliasOption} ExtensionAliasOption */ /** @typedef {import("./PnpPlugin").PnpApiImpl} PnpApi */ /** @typedef {import("./Resolver").FileSystem} FileSystem */ /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */ @@ -45,12 +47,14 @@ const UseFilePlugin = require("./UseFilePlugin"); /** @typedef {string|string[]|false} AliasOptionNewRequest */ /** @typedef {{[k: string]: AliasOptionNewRequest}} AliasOptions */ +/** @typedef {{[k: string]: string[] }} ExtensionAliasOptions */ /** @typedef {{apply: function(Resolver): void} | function(this: Resolver, Resolver): void} Plugin */ /** * @typedef {Object} UserResolveOptions * @property {(AliasOptions | AliasOptionEntry[])=} alias A list of module alias configurations or an object which maps key to value * @property {(AliasOptions | AliasOptionEntry[])=} fallback A list of module alias configurations or an object which maps key to value, applied only after modules option + * @property {ExtensionAliasOptions=} extensionAlias An object which maps extension to extension aliases * @property {(string | string[])[]=} aliasFields A list of alias fields in description files * @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. * @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"); * @property {AliasOptionEntry[]} alias * @property {AliasOptionEntry[]} fallback * @property {Set} aliasFields + * @property {ExtensionAliasOption[]} extensionAlias * @property {(function(ResolveRequest): boolean)} cachePredicate * @property {boolean} cacheWithContext * @property {Set} conditionNames A list of exports field condition names. @@ -197,6 +202,14 @@ function createOptions(options) { : false : options.enforceExtension, extensions: new Set(options.extensions || [".js", ".json", ".node"]), + extensionAlias: options.extensionAlias + ? Object.keys(options.extensionAlias).map(k => ({ + extension: k, + alias: /** @type {ExtensionAliasOptions} */ (options.extensionAlias)[ + k + ] + })) + : [], fileSystem: options.useSyncFileSystemCalls ? new SyncAsyncFileSystemDecorator( /** @type {SyncFileSystem} */ ( @@ -251,6 +264,7 @@ exports.createResolver = function (options) { descriptionFiles, enforceExtension, exportsFields, + extensionAlias, importsFields, extensions, fileSystem, @@ -602,6 +616,9 @@ exports.createResolver = function (options) { aliasFields.forEach(item => { plugins.push(new AliasFieldPlugin("file", item, "internal-resolve")); }); + extensionAlias.forEach(item => + plugins.push(new ExtensionAliasPlugin("file", item, "final-file")) + ); plugins.push(new NextPlugin("file", "final-file")); // final-file diff --git a/test/extension-alias.js b/test/extension-alias.js new file mode 100644 index 00000000..ce6abde6 --- /dev/null +++ b/test/extension-alias.js @@ -0,0 +1,81 @@ +const path = require("path"); +const fs = require("fs"); +const should = require("should"); + +const CachedInputFileSystem = require("../lib/CachedInputFileSystem"); +const ResolverFactory = require("../lib/ResolverFactory"); + +/** @typedef {import("../lib/util/entrypoints").ImportsField} ImportsField */ + +describe("extension-alias", () => { + const fixture = path.resolve(__dirname, "fixtures", "extension-alias"); + const nodeFileSystem = new CachedInputFileSystem(fs, 4000); + + const resolver = ResolverFactory.createResolver({ + extensions: [".js"], + fileSystem: nodeFileSystem, + mainFiles: ["index.js"], + extensionAlias: { + ".js": [".ts", ".js"] + } + }); + + it("should alias fully specified file", done => { + resolver.resolve({}, fixture, "./index.js", {}, (err, result) => { + if (err) return done(err); + should(result).be.eql(path.resolve(fixture, "index.ts")); + done(); + }); + }); + + it("should alias specified extension", done => { + resolver.resolve({}, fixture, "./dir", {}, (err, result) => { + if (err) return done(err); + should(result).be.eql(path.resolve(fixture, "dir", "index.ts")); + done(); + }); + }); + + it("should result successfully without aliasing #1", done => { + resolver.resolve({}, fixture, "./dir2", {}, (err, result) => { + if (err) return done(err); + should(result).be.eql(path.resolve(fixture, "dir2", "index.js")); + done(); + }); + }); + + it("should result successfully without aliasing #2", done => { + resolver.resolve({}, fixture, "./dir2/index.js", {}, (err, result) => { + if (err) return done(err); + should(result).be.eql(path.resolve(fixture, "dir2", "index.js")); + done(); + }); + }); + + describe("should result successfully without alias array", () => { + const resolver = ResolverFactory.createResolver({ + extensions: [".js"], + fileSystem: nodeFileSystem, + mainFiles: ["index.js"], + extensionAlias: { + ".js": [] + } + }); + + it("directory", done => { + resolver.resolve({}, fixture, "./dir2", {}, (err, result) => { + if (err) return done(err); + should(result).be.eql(path.resolve(fixture, "dir2", "index.js")); + done(); + }); + }); + + it("file", done => { + resolver.resolve({}, fixture, "./dir2/index.js", {}, (err, result) => { + if (err) return done(err); + should(result).be.eql(path.resolve(fixture, "dir2", "index.js")); + done(); + }); + }); + }); +}); diff --git a/test/fixtures/extension-alias/dir/index.js b/test/fixtures/extension-alias/dir/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/extension-alias/dir/index.ts b/test/fixtures/extension-alias/dir/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/extension-alias/dir2/index.js b/test/fixtures/extension-alias/dir2/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/extension-alias/index.js b/test/fixtures/extension-alias/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/extension-alias/index.ts b/test/fixtures/extension-alias/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/types.d.ts b/types.d.ts index c418c89e..1df19f91 100644 --- a/types.d.ts +++ b/types.d.ts @@ -91,6 +91,13 @@ declare class CloneBasenamePlugin { target: any; apply(resolver: Resolver): void; } +declare interface ExtensionAliasOption { + alias: string | string[]; + extension: string; +} +declare interface ExtensionAliasOptions { + [index: string]: string[]; +} declare interface FileSystem { readFile: { (arg0: string, arg1: FileSystemCallback): void; @@ -214,6 +221,7 @@ declare interface ResolveOptions { alias: AliasOption[]; fallback: AliasOption[]; aliasFields: Set; + extensionAlias: ExtensionAliasOption[]; cachePredicate: (arg0: ResolveRequest) => boolean; cacheWithContext: boolean; @@ -322,6 +330,11 @@ declare interface UserResolveOptions { */ fallback?: AliasOptions | AliasOption[]; + /** + * An object which maps extension to extension aliases + */ + extensionAlias?: ExtensionAliasOptions; + /** * A list of alias fields in description files */ From 16225de118ab1ebf21a0340f4381f62226490936 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 28 Jun 2022 12:09:08 +0200 Subject: [PATCH 2/2] apply extensionAlias before extensions and mainFiles --- lib/ExtensionAliasPlugin.js | 24 ++++++++++------- lib/ResolverFactory.js | 28 ++++++++++++-------- test/extension-alias.js | 28 +++++++++++++------- test/fixtures/extension-alias/dir2/index.mts | 0 test/fixtures/extension-alias/index.mjs | 0 test/fixtures/extension-alias/index.mts.js | 0 types.d.ts | 2 +- 7 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 test/fixtures/extension-alias/dir2/index.mts create mode 100644 test/fixtures/extension-alias/index.mjs create mode 100644 test/fixtures/extension-alias/index.mts.js diff --git a/lib/ExtensionAliasPlugin.js b/lib/ExtensionAliasPlugin.js index 78bae8e3..76ce9b40 100644 --- a/lib/ExtensionAliasPlugin.js +++ b/lib/ExtensionAliasPlugin.js @@ -30,21 +30,19 @@ module.exports = class ExtensionAliasPlugin { */ apply(resolver) { const target = resolver.ensureHook(this.target); - const { extension, alias: aliasArray } = this.options; + const { extension, alias } = this.options; resolver .getHook(this.source) .tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => { - const requestPath = request.path; + const requestPath = request.request; if (!requestPath || !requestPath.endsWith(extension)) return callback(); const resolve = (alias, callback) => { resolver.doResolve( target, { ...request, - path: `${requestPath.slice(0, -extension.length)}${alias}`, - relativePath: request.relativePath - ? `${request.relativePath.slice(0, -extension.length)}${alias}` - : request.relativePath + request: `${requestPath.slice(0, -extension.length)}${alias}`, + fullySpecified: true }, `aliased from extension alias with mapping '${extension}' to '${alias}'`, resolveContext, @@ -52,10 +50,18 @@ module.exports = class ExtensionAliasPlugin { ); }; - if (aliasArray.length > 1) { - forEachBail(aliasArray, resolve, callback); + const stoppingCallback = (err, result) => { + if (err) return callback(err); + if (result) return callback(null, result); + // Don't allow other aliasing or raw request + return callback(null, null); + }; + if (typeof alias === "string") { + resolve(alias, stoppingCallback); + } else if (alias.length > 1) { + forEachBail(alias, resolve, stoppingCallback); } else { - resolve(aliasArray[0], callback); + resolve(alias[0], stoppingCallback); } }); } diff --git a/lib/ResolverFactory.js b/lib/ResolverFactory.js index fa37ae19..6af60a5e 100644 --- a/lib/ResolverFactory.js +++ b/lib/ResolverFactory.js @@ -47,7 +47,7 @@ const UseFilePlugin = require("./UseFilePlugin"); /** @typedef {string|string[]|false} AliasOptionNewRequest */ /** @typedef {{[k: string]: AliasOptionNewRequest}} AliasOptions */ -/** @typedef {{[k: string]: string[] }} ExtensionAliasOptions */ +/** @typedef {{[k: string]: string|string[] }} ExtensionAliasOptions */ /** @typedef {{apply: function(Resolver): void} | function(this: Resolver, Resolver): void} Plugin */ /** @@ -297,6 +297,8 @@ exports.createResolver = function (options) { resolver.ensureHook("newInternalResolve"); resolver.ensureHook("parsedResolve"); resolver.ensureHook("describedResolve"); + resolver.ensureHook("rawResolve"); + resolver.ensureHook("normalResolve"); resolver.ensureHook("internal"); resolver.ensureHook("rawModule"); resolver.ensureHook("module"); @@ -356,21 +358,28 @@ exports.createResolver = function (options) { plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve")); // described-resolve - plugins.push(new NextPlugin("described-resolve", "normal-resolve")); + plugins.push(new NextPlugin("described-resolve", "raw-resolve")); if (fallback.length > 0) { plugins.push( new AliasPlugin("described-resolve", fallback, "internal-resolve") ); } - // normal-resolve - if (alias.length > 0) - plugins.push(new AliasPlugin("normal-resolve", alias, "internal-resolve")); + // raw-resolve + if (alias.length > 0) { + plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve")); + } aliasFields.forEach(item => { - plugins.push( - new AliasFieldPlugin("normal-resolve", item, "internal-resolve") - ); + plugins.push(new AliasFieldPlugin("raw-resolve", item, "internal-resolve")); }); + extensionAlias.forEach(item => + plugins.push( + new ExtensionAliasPlugin("raw-resolve", item, "normal-resolve") + ) + ); + plugins.push(new NextPlugin("raw-resolve", "normal-resolve")); + + // normal-resolve if (preferRelative) { plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative")); } @@ -616,9 +625,6 @@ exports.createResolver = function (options) { aliasFields.forEach(item => { plugins.push(new AliasFieldPlugin("file", item, "internal-resolve")); }); - extensionAlias.forEach(item => - plugins.push(new ExtensionAliasPlugin("file", item, "final-file")) - ); plugins.push(new NextPlugin("file", "final-file")); // final-file diff --git a/test/extension-alias.js b/test/extension-alias.js index ce6abde6..0ebca622 100644 --- a/test/extension-alias.js +++ b/test/extension-alias.js @@ -16,7 +16,8 @@ describe("extension-alias", () => { fileSystem: nodeFileSystem, mainFiles: ["index.js"], extensionAlias: { - ".js": [".ts", ".js"] + ".js": [".ts", ".js"], + ".mjs": ".mts" } }); @@ -28,31 +29,38 @@ describe("extension-alias", () => { }); }); - it("should alias specified extension", done => { - resolver.resolve({}, fixture, "./dir", {}, (err, result) => { + it("should alias fully specified file when there are two alternatives", done => { + resolver.resolve({}, fixture, "./dir/index.js", {}, (err, result) => { if (err) return done(err); should(result).be.eql(path.resolve(fixture, "dir", "index.ts")); done(); }); }); - it("should result successfully without aliasing #1", done => { - resolver.resolve({}, fixture, "./dir2", {}, (err, result) => { + it("should also allow the second alternative", done => { + resolver.resolve({}, fixture, "./dir2/index.js", {}, (err, result) => { if (err) return done(err); should(result).be.eql(path.resolve(fixture, "dir2", "index.js")); done(); }); }); - it("should result successfully without aliasing #2", done => { - resolver.resolve({}, fixture, "./dir2/index.js", {}, (err, result) => { + it("should support alias option without an array", done => { + resolver.resolve({}, fixture, "./dir2/index.mjs", {}, (err, result) => { if (err) return done(err); - should(result).be.eql(path.resolve(fixture, "dir2", "index.js")); + should(result).be.eql(path.resolve(fixture, "dir2", "index.mts")); + done(); + }); + }); + + it("should not allow to fallback to the original extension or add extensions", done => { + resolver.resolve({}, fixture, "./index.mjs", {}, (err, result) => { + should(err).be.instanceOf(Error); done(); }); }); - describe("should result successfully without alias array", () => { + describe("should not apply extension alias to extensions or mainFiles field", () => { const resolver = ResolverFactory.createResolver({ extensions: [".js"], fileSystem: nodeFileSystem, @@ -71,7 +79,7 @@ describe("extension-alias", () => { }); it("file", done => { - resolver.resolve({}, fixture, "./dir2/index.js", {}, (err, result) => { + resolver.resolve({}, fixture, "./dir2/index", {}, (err, result) => { if (err) return done(err); should(result).be.eql(path.resolve(fixture, "dir2", "index.js")); done(); diff --git a/test/fixtures/extension-alias/dir2/index.mts b/test/fixtures/extension-alias/dir2/index.mts new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/extension-alias/index.mjs b/test/fixtures/extension-alias/index.mjs new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/extension-alias/index.mts.js b/test/fixtures/extension-alias/index.mts.js new file mode 100644 index 00000000..e69de29b diff --git a/types.d.ts b/types.d.ts index 1df19f91..01436533 100644 --- a/types.d.ts +++ b/types.d.ts @@ -96,7 +96,7 @@ declare interface ExtensionAliasOption { extension: string; } declare interface ExtensionAliasOptions { - [index: string]: string[]; + [index: string]: string | string[]; } declare interface FileSystem { readFile: {