Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add extension alias #351

Merged
merged 2 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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)
Expand Down
68 changes: 68 additions & 0 deletions lib/ExtensionAliasPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
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 } = this.options;
resolver
.getHook(this.source)
.tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => {
const requestPath = request.request;
if (!requestPath || !requestPath.endsWith(extension)) return callback();
const resolve = (alias, callback) => {
resolver.doResolve(
target,
{
...request,
request: `${requestPath.slice(0, -extension.length)}${alias}`,
fullySpecified: true
},
`aliased from extension alias with mapping '${extension}' to '${alias}'`,
resolveContext,
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(alias[0], stoppingCallback);
}
});
}
};
37 changes: 30 additions & 7 deletions lib/ResolverFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -38,19 +39,22 @@ 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 */
/** @typedef {import("./Resolver").SyncFileSystem} SyncFileSystem */

/** @typedef {string|string[]|false} AliasOptionNewRequest */
/** @typedef {{[k: string]: AliasOptionNewRequest}} AliasOptions */
/** @typedef {{[k: string]: 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.
Expand Down Expand Up @@ -83,6 +87,7 @@ const UseFilePlugin = require("./UseFilePlugin");
* @property {AliasOptionEntry[]} alias
* @property {AliasOptionEntry[]} fallback
* @property {Set<string | string[]>} aliasFields
* @property {ExtensionAliasOption[]} extensionAlias
* @property {(function(ResolveRequest): boolean)} cachePredicate
* @property {boolean} cacheWithContext
* @property {Set<string>} conditionNames A list of exports field condition names.
Expand Down Expand Up @@ -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} */ (
Expand Down Expand Up @@ -251,6 +264,7 @@ exports.createResolver = function (options) {
descriptionFiles,
enforceExtension,
exportsFields,
extensionAlias,
importsFields,
extensions,
fileSystem,
Expand Down Expand Up @@ -283,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");
Expand Down Expand Up @@ -342,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"));
}
Expand Down
89 changes: 89 additions & 0 deletions test/extension-alias.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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"],
".mjs": ".mts"
}
});

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 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 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 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.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 not apply extension alias to extensions or mainFiles field", () => {
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", {}, (err, result) => {
if (err) return done(err);
should(result).be.eql(path.resolve(fixture, "dir2", "index.js"));
done();
});
});
});
});
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
13 changes: 13 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 | string[];
}
declare interface FileSystem {
readFile: {
(arg0: string, arg1: FileSystemCallback<string | Buffer>): void;
Expand Down Expand Up @@ -214,6 +221,7 @@ declare interface ResolveOptions {
alias: AliasOption[];
fallback: AliasOption[];
aliasFields: Set<string | string[]>;
extensionAlias: ExtensionAliasOption[];
cachePredicate: (arg0: ResolveRequest) => boolean;
cacheWithContext: boolean;

Expand Down Expand Up @@ -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
*/
Expand Down