diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js index f0f6ba92..5172411b 100644 --- a/lib/util/entrypoints.js +++ b/lib/util/entrypoints.js @@ -80,6 +80,7 @@ Conditional mapping nested in another conditional mapping is called nested mappi */ +const { fileURLToPath } = require("url"); const slashCode = "/".charCodeAt(0); const dotCode = ".".charCodeAt(0); const hashCode = "#".charCodeAt(0); @@ -93,6 +94,7 @@ module.exports.processExportsField = function processExportsField( ) { return createFieldProcessor( buildExportsFieldPathTree(exportsField), + exportsField, assertExportsFieldRequest, assertExportTarget ); @@ -107,6 +109,7 @@ module.exports.processImportsField = function processImportsField( ) { return createFieldProcessor( buildImportsFieldPathTree(importsField), + importsField, assertImportsFieldRequest, assertImportTarget ); @@ -114,15 +117,16 @@ module.exports.processImportsField = function processImportsField( /** * @param {PathTreeNode} treeRoot root + * @param {ExportsField | ImportsField} field exports or import field * @param {(s: string) => string} assertRequest assertRequest * @param {(s: string, f: boolean) => void} assertTarget assertTarget * @returns {FieldProcessor} field processor */ -function createFieldProcessor(treeRoot, assertRequest, assertTarget) { +function createFieldProcessor(treeRoot, field, assertRequest, assertTarget) { return function fieldProcessor(request, conditionNames) { request = assertRequest(request); - const match = findMatch(request, treeRoot); + const match = findMatch(request, field); if (match === null) return []; @@ -244,128 +248,110 @@ function assertImportTarget(imp, expectFolder) { } } -function getRemainingRequest(index, request) { - return index === request.length + 1 - ? undefined - : index < 0 - ? request.slice(-index - 1) - : request.slice(index); +function patternKeyCompare(a, b) { + const aPatternIndex = a.indexOf("*"); + const bPatternIndex = b.indexOf("*"); + const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1; + const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1; + + if (baseLenA > baseLenB) return -1; + if (baseLenB > baseLenA) return 1; + if (aPatternIndex === -1) return 1; + if (bPatternIndex === -1) return -1; + if (a.length > b.length) return -1; + if (b.length > a.length) return 1; + + return 0; +} + +function isConditionalExportsMainSugar(exports) { + if (typeof exports === "string" || Array.isArray(exports)) return true; + if (typeof exports !== "object" || exports === null) return false; + + const keys = Object.getOwnPropertyNames(exports); + let isConditionalSugar = false; + let i = 0; + for (let j = 0; j < keys.length; j++) { + const key = keys[j]; + const curIsConditionalSugar = key === "" || key[0] !== "."; + if (i++ === 0) { + isConditionalSugar = curIsConditionalSugar; + } else if (isConditionalSugar !== curIsConditionalSugar) { + // TODO + } + } + return isConditionalSugar; } /** * Trying to match request to field * @param {string} request request - * @param {PathTreeNode} treeRoot path tree root + * @param {ExportsField | ImportsField} field exports or import field * @returns {[MappingValue, string, boolean]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings */ -function findMatch(request, treeRoot) { - if (request.length === 0) { - const value = treeRoot.files.get(""); - - return value ? [value, getRemainingRequest(1, request), false] : null; +function findMatch(request, field) { + if (isConditionalExportsMainSugar(field)) { + field = { ".": field }; } if ( - treeRoot.children === null && - treeRoot.folder === null && - treeRoot.wildcards === null + (Object.prototype.hasOwnProperty.call(field, "./" + request) || + Object.prototype.hasOwnProperty.call(field, "." + request)) && + !request.includes("*") && + !request.endsWith("/") ) { - const value = treeRoot.files.get(request); + const target = field["./" + request] || field["." + request]; - return value - ? [value, getRemainingRequest(request.length + 1, request), false] - : null; - } + if (target === "./") return null; - let node = treeRoot; - let lastNonSlashIndex = 0; - let slashIndex = request.indexOf("/", 0); + const isDirectory = + typeof target === "string" ? target.endsWith("/") : false; - /** @type {[MappingValue, string, boolean]|null} */ - let lastFolderMatch = null; + return [target, "", isDirectory]; + } - const applyFolderMapping = () => { - const folderMapping = node.folder; + let bestMatch = ""; + let bestMatchSubpath; - if (folderMapping) { - if (lastFolderMatch) { - lastFolderMatch[0] = folderMapping; - lastFolderMatch[1] = getRemainingRequest( - -lastNonSlashIndex - 1, - request - ); - lastFolderMatch[2] = -lastNonSlashIndex - 1 < 0; - } else { - lastFolderMatch = [ - folderMapping, - getRemainingRequest(-lastNonSlashIndex - 1, request), - -lastNonSlashIndex - 1 < 0 - ]; - } - } - }; + const keys = Object.getOwnPropertyNames(field); - const applyWildcardMappings = (wildcardMappings, remainingRequest) => { - if (wildcardMappings) { - for (const [key, target] of wildcardMappings) { - if (remainingRequest.startsWith(key)) { - if (!lastFolderMatch) { - lastFolderMatch = [ - target, - request.slice(lastNonSlashIndex + key.length), - lastNonSlashIndex + key.length < 0 - ]; - } else if ( - lastFolderMatch[1].length > - getRemainingRequest(lastNonSlashIndex + key.length, request).length - ) { - lastFolderMatch[0] = target; - lastFolderMatch[1] = request.slice(lastNonSlashIndex + key.length); - lastFolderMatch[2] = lastNonSlashIndex + key.length < 0; - } - } + for (let i = 0; i < keys.length; i++) { + const originalKey = keys[i]; + const key = keys[i].slice(2); + const patternIndex = key.indexOf("*"); + + if (patternIndex !== -1 && request.startsWith(key.slice(0, patternIndex))) { + const patternTrailer = key.slice(patternIndex + 1); + + if ( + request.length >= key.length && + request.endsWith(patternTrailer) && + patternKeyCompare(bestMatch, key) === 1 && + key.lastIndexOf("*") === patternIndex + ) { + bestMatch = key; + bestMatchSubpath = request.slice( + patternIndex, + request.length - patternTrailer.length + ); } + } else if ( + originalKey[originalKey.length - 1] === "/" && + request.startsWith(key) && + patternKeyCompare(bestMatch, key) === 1 + ) { + bestMatch = originalKey; + bestMatchSubpath = request.slice(key.length); } - }; - - while (slashIndex !== -1) { - applyFolderMapping(); - - const wildcardMappings = node.wildcards; - - if (!wildcardMappings && node.children === null) return lastFolderMatch; - - const folder = request.slice(lastNonSlashIndex, slashIndex); - - applyWildcardMappings(wildcardMappings, folder); - - if (node.children === null) return lastFolderMatch; - - const newNode = node.children.get(folder); - - if (!newNode) { - return lastFolderMatch; - } - - node = newNode; - lastNonSlashIndex = slashIndex + 1; - slashIndex = request.indexOf("/", lastNonSlashIndex); } - const remainingRequest = - lastNonSlashIndex > 0 ? request.slice(lastNonSlashIndex) : request; + if (bestMatch === "") return null; - const value = node.files.get(remainingRequest); + const target = + field[bestMatch.startsWith("./") ? bestMatch : "./" + bestMatch]; + const pattern = bestMatch.endsWith("/"); - if (value) { - return [value, getRemainingRequest(request.length + 1, request), false]; - } - - applyFolderMapping(); - - applyWildcardMappings(node.wildcards, remainingRequest); - - return lastFolderMatch; + return [target, /** @type {string} */ (bestMatchSubpath), pattern]; } /** @@ -450,24 +436,8 @@ function targetMapping( return mappingTarget + remainingRequest; } assert(mappingTarget, false); - const wildcardIndex = mappingTarget.indexOf("*"); - - if (wildcardIndex !== -1) { - const maybeCommonSuffix = mappingTarget.slice(wildcardIndex + 1); - if (remainingRequest.endsWith(maybeCommonSuffix)) { - return mappingTarget.replace( - new RegExp(`\\*${maybeCommonSuffix}`, "g"), - remainingRequest.replace(/\$/g, "$$") - ); - } else { - return mappingTarget.replace( - /\*/g, - remainingRequest.replace(/\$/g, "$$") - ); - } - } else { - return mappingTarget; - } + + return mappingTarget.replace(/\*/g, remainingRequest.replace(/\$/g, "$$")); } /** @@ -551,7 +521,7 @@ function walkPath(root, path, target) { } let node = root; - // Typical path tree can looks like + // Typical path tree can look like // root // - files: ["a.js", "b.js"] // - children: @@ -564,20 +534,29 @@ function walkPath(root, path, target) { const folder = path.slice(lastNonSlashIndex, slashIndex); let newNode; - if (node.children === null) { - newNode = createNode(); - node.children = new Map(); - node.children.set(folder, newNode); + // If the folder is a wildcard, create a new wildcard node or get an existing one. + if (folder === "*") { + if (node.wildcards === null) { + newNode = createNode(); + node.wildcards = new Map(); + node.wildcards.set("", newNode); + } else { + newNode = node.wildcards.get(folder) || createNode(); + node.wildcards.set("", newNode); + } } else { - newNode = node.children.get(folder); - - if (!newNode) { + // If the folder is not a wildcard, create a new child node or get an existing one. + if (node.children === null) { newNode = createNode(); + node.children = new Map(); + node.children.set(folder, newNode); + } else { + newNode = node.children.get(folder) || createNode(); node.children.set(folder, newNode); } } - node = newNode; + node = /** @type {PathTreeNode} */ (newNode); lastNonSlashIndex = slashIndex + 1; slashIndex = path.indexOf("/", lastNonSlashIndex); } diff --git a/test/exportsField.js b/test/exportsField.js index b1f6ab8d..79418017 100644 --- a/test/exportsField.js +++ b/test/exportsField.js @@ -2598,7 +2598,7 @@ describe("ExportsFieldPlugin", () => { if (err) return done(err); if (!result) throw new Error("No result"); result.should.equal( - path.resolve(fixture, "./node_modules/m/src/middle-1/nested/f.js") + path.resolve(fixture, "./node_modules/m/src/middle-2/nested/f.js") ); done(); } @@ -2624,7 +2624,7 @@ describe("ExportsFieldPlugin", () => { }); }); - it("should resolve with wildcard pattern #7", done => { + it("should resolve with wildcard pattern #8", done => { const fixture = path.resolve( __dirname, "./fixtures/imports-exports-wildcard/" @@ -2634,10 +2634,7 @@ describe("ExportsFieldPlugin", () => { if (err) return done(err); if (!result) throw new Error("No result"); result.should.equal( - path.resolve( - fixture, - "./node_modules/m/src/middle-3/nested/f/nested/f.js" - ) + path.resolve(fixture, "./node_modules/m/src/middle-4/f/f.js") ); done(); });