diff --git a/lib/AliasPlugin.js b/lib/AliasPlugin.js index bbe9724..095f887 100644 --- a/lib/AliasPlugin.js +++ b/lib/AliasPlugin.js @@ -5,14 +5,13 @@ "use strict"; -const forEachBail = require("./forEachBail"); const { PathType, getType } = require("./util/path"); /** @typedef {import("./Resolver")} Resolver */ /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */ /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */ /** @typedef {string | Array | false} Alias */ -/** @typedef {{alias: Alias, name: string, onlyModule?: boolean}} AliasOption */ +/** @typedef {{alias: Alias, name: string, onlyModule?: boolean, nameWithSlash?: string, absolutePath?: string}} AliasOption */ module.exports = class AliasPlugin { /** @@ -23,6 +22,9 @@ module.exports = class AliasPlugin { constructor(source, options, target) { this.source = source; this.options = Array.isArray(options) ? options : [options]; + for (const item of this.options) { + item.nameWithSlash = item.name + "/"; + } this.target = target; } @@ -32,6 +34,7 @@ module.exports = class AliasPlugin { */ apply(resolver) { const target = resolver.ensureHook(this.target); + /** * @param {string} maybeAbsolutePath path * @returns {null|string} absolute path with slash ending @@ -43,112 +46,158 @@ module.exports = class AliasPlugin { } return null; }; + /** * @param {string} path path - * @param {string} maybeSubPath sub path + * @param {AliasOption} item item * @returns {boolean} true, if path is sub path */ - const isSubPath = (path, maybeSubPath) => { - const absolutePath = getAbsolutePathWithSlashEnding(maybeSubPath); - if (!absolutePath) return false; - return path.startsWith(absolutePath); + const isSubPath = (path, item) => { + if (item.absolutePath === undefined) { + item.absolutePath = getAbsolutePathWithSlashEnding(item.name); + } + + if (!item.absolutePath) return false; + return path.startsWith(item.absolutePath); }; + resolver .getHook(this.source) .tapAsync("AliasPlugin", (request, resolveContext, callback) => { const innerRequest = request.request || request.path; if (!innerRequest) return callback(); - forEachBail( - this.options, - (item, callback) => { - /** @type {boolean} */ - let shouldStop = false; - if ( - innerRequest === item.name || - (!item.onlyModule && - (request.request - ? innerRequest.startsWith(`${item.name}/`) - : isSubPath(innerRequest, item.name))) - ) { - /** @type {string} */ - const remainingRequest = innerRequest.slice(item.name.length); - /** - * @param {Alias} alias alias - * @param {(err?: null|Error, result?: null|ResolveRequest) => void} callback callback - * @returns {void} - */ - const resolveWithAlias = (alias, callback) => { - if (alias === false) { - /** @type {ResolveRequest} */ - const ignoreObj = { - ...request, - path: false - }; - if (typeof resolveContext.yield === "function") { - resolveContext.yield(ignoreObj); - return callback(null, null); - } - return callback(null, ignoreObj); - } - if ( - innerRequest !== alias && - !innerRequest.startsWith(alias + "/") - ) { - shouldStop = true; - const newRequestStr = alias + remainingRequest; - /** @type {ResolveRequest} */ - const obj = { - ...request, - request: newRequestStr, - fullySpecified: false - }; - return resolver.doResolve( - target, - obj, - "aliased with mapping '" + - item.name + - "': '" + - alias + - "' to '" + - newRequestStr + - "'", - resolveContext, - (err, result) => { - if (err) return callback(err); - if (result) return callback(null, result); - return callback(); - } - ); - } - return callback(); - }; - /** - * @param {null|Error} [err] error - * @param {null|ResolveRequest} [result] result - * @returns {void} - */ - const stoppingCallback = (err, result) => { - if (err) return callback(err); + let i = 0; + + let resolveCallback; - if (result) return callback(null, result); - // Don't allow other aliasing or raw request - if (shouldStop) return callback(null, null); - return callback(); - }; - if (Array.isArray(item.alias)) { - return forEachBail( - item.alias, - resolveWithAlias, - stoppingCallback + while (i < this.options.length) { + const item = this.options[i]; + i++; + if ( + innerRequest === item.name || + (!item.onlyModule && + (request.request + ? innerRequest.startsWith(item.nameWithSlash) + : isSubPath(innerRequest, item))) + ) { + /** @type {string} */ + const remainingRequest = + innerRequest === item.name + ? undefined + : innerRequest.slice(item.name.length); + /** + * @param {Alias} alias alias + * @param {(err?: null|Error, result?: null|ResolveRequest) => void} callback callback + * @returns {void} + */ + if (Array.isArray(item.alias)) { + for (const alias of item.alias) { + // TODO: maybe shouldBail should be set to true for the last object? + const done = processAlias( + alias, + request, + callback, + innerRequest, + remainingRequest, + resolver, + target, + item, + resolveContext, + resolveCallback, + false ); - } else { - return resolveWithAlias(item.alias, stoppingCallback); + if (done) { + return; + } + } + } else { + const done = processAlias( + item.alias, + request, + callback, + innerRequest, + remainingRequest, + resolver, + target, + item, + resolveContext, + resolveCallback + ); + if (done) { + return; } } - return callback(); - }, - callback - ); + } + } + // No match + callback(); }); } }; + +function processAlias( + alias, + request, + callback, + innerRequest, + remainingRequest, + resolver, + target, + item, + resolveContext, + resolveCallback, + shouldBail = true +) { + if (alias === false) { + /** @type {ResolveRequest} */ + const ignoreObj = { + ...request, + path: false + }; + if (typeof resolveContext.yield === "function") { + resolveContext.yield(ignoreObj); + callback(null, null); + return true; + } + callback(null, ignoreObj); + return true; + } + if (innerRequest !== alias && !innerRequest.startsWith(alias + "/")) { + const newRequestStr = remainingRequest ? alias + remainingRequest : alias; + /** @type {ResolveRequest} */ + const obj = { + ...request, + request: newRequestStr, + fullySpecified: false + }; + let done = false; + if (!resolveCallback) { + resolveCallback = (err, result) => { + if (err || result) { + done = true; + if (err) return callback(err); + return callback(null, result); + } + // TODO: support also logic when this callback is called async + if (shouldBail) { + return callback(null, null); + } + }; + } + resolver.doResolve( + target, + obj, + "aliased with mapping '" + + item.name + + "': '" + + alias + + "' to '" + + newRequestStr + + "'", + resolveContext, + resolveCallback + ); + // the resolveCallback will take care of calling the callback + return shouldBail || done; + } +} diff --git a/test/fallback.test.js b/test/fallback.test.js index a6f7152..d578241 100644 --- a/test/fallback.test.js +++ b/test/fallback.test.js @@ -8,7 +8,8 @@ describe("fallback", function () { const fileSystem = Volume.fromJSON( { "/a/index": "", - "/a/dir/index": "", + "/a/dir/index": "", + "/a/second/index": "", "/recursive/index": "", "/recursive/dir/index": "", "/recursive/dir/file": "", @@ -94,6 +95,11 @@ describe("fallback", function () { expect(resolver.resolveSync({}, "/", "multiAlias/anotherDir")).toBe( "/e/anotherDir/index" ); + // The multiAlias works as expected when the matching alias is the last one + expect(resolver.resolveSync({}, "/", "multiAlias/second")).toBe( + "/a/second/index" + ); + }); it("should log the correct info", done => { const log = []; diff --git a/types.d.ts b/types.d.ts index fc09289..26a2711 100644 --- a/types.d.ts +++ b/types.d.ts @@ -19,6 +19,8 @@ declare interface AliasOption { alias: Alias; name: string; onlyModule?: boolean; + nameWithSlash?: string; + absolutePath?: string; } type AliasOptionNewRequest = string | false | string[]; declare interface AliasOptions {