From fd1bcd893e5555cbb937d47580ad8936aba62b8c Mon Sep 17 00:00:00 2001 From: bvanjoi Date: Tue, 26 Jul 2022 19:40:03 +0800 Subject: [PATCH 01/12] support wildcards pattern with common suffix in imports/exports field --- lib/util/entrypoints.js | 24 +++++++-- test/exportsField.js | 54 +++++++++++++++++++ .../node_modules/m/package.json | 9 ++++ .../node_modules/m/src/features/f.js | 0 .../node_modules/m/src/features/y/y.js | 0 .../node_modules/m/src/internal/i.js | 0 test/importsField.js | 13 +++++ 7 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/package.json create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/features/f.js create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/features/y/y.js create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/internal/i.js diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js index b925d626..80d10d67 100644 --- a/lib/util/entrypoints.js +++ b/lib/util/entrypoints.js @@ -430,7 +430,24 @@ function targetMapping( return mappingTarget + remainingRequest; } assert(mappingTarget, false); - return mappingTarget.replace(/\*/g, remainingRequest.replace(/\$/g, "$$")); + 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; + } } /** @@ -549,9 +566,10 @@ function walkPath(root, path, target) { node.folder = target; } else { const file = lastNonSlashIndex > 0 ? path.slice(lastNonSlashIndex) : path; - if (file.endsWith("*")) { + const wildcardsIndex = file.indexOf("*"); + if (wildcardsIndex !== -1) { if (node.wildcards === null) node.wildcards = new Map(); - node.wildcards.set(file.slice(0, -1), target); + node.wildcards.set(file.slice(0, wildcardsIndex), target); } else { node.files.set(file, target); } diff --git a/test/exportsField.js b/test/exportsField.js index d2f2d685..f7ee92af 100644 --- a/test/exportsField.js +++ b/test/exportsField.js @@ -1953,6 +1953,28 @@ describe("Process exports field", function exportsField() { "./a/b/d/c.js", [] ] + }, + { + name: "wildcard pattern with suffix #1", + expect: ["./A/b.js"], + suite: [ + { + "./a/*.js": "./A/*.js" + }, + "./a/b.js", + [] + ] + }, + { + name: "wildcard pattern with suffix #2", + expect: ["./A/b/c.js"], + suite: [ + { + "./a/*.js": "./A/*.js" + }, + "./a/b/c.js", + [] + ] } ]; @@ -2452,4 +2474,36 @@ describe("ExportsFieldPlugin", () => { } ); }); + + it("should resolve with wildcard pattern #1", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve({}, fixture, "m/features/f.js", {}, (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal( + path.resolve(fixture, "./node_modules/m/src/features/f.js") + ); + done(); + }); + }); + + it("should resolve with wildcard pattern #2", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve({}, fixture, "m/features/y/y.js", {}, (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal( + path.resolve(fixture, "./node_modules/m/src/features/y/y.js") + ); + done(); + }); + }); }); diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/package.json b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json new file mode 100644 index 00000000..d610922b --- /dev/null +++ b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json @@ -0,0 +1,9 @@ +{ + "name": "m", + "exports": { + "./features/*.js": "./src/features/*.js" + }, + "imports": { + "#internal/*.js": "./src/internal/*.js" + } +} diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/f.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/y/y.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/y/y.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/internal/i.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/internal/i.js new file mode 100644 index 00000000..e69de29b diff --git a/test/importsField.js b/test/importsField.js index 334cd87a..59eacba4 100644 --- a/test/importsField.js +++ b/test/importsField.js @@ -1357,4 +1357,17 @@ describe("ImportsFieldPlugin", () => { } ); }); + + it("should resolve with wildcard pattern", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/node_modules/m/" + ); + resolver.resolve({}, fixture, "#internal/i.js", {}, (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal(path.resolve(fixture, "./src/internal/i.js")); + done(); + }); + }); }); From f48922ac640985dac49271d267887d1780f52914 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Sun, 16 Apr 2023 22:40:51 +0300 Subject: [PATCH 02/12] test: more --- test/exportsField.js | 60 +++++++++++++++++++ .../node_modules/m/package.json | 4 +- .../m/src/features/internal/file.js | 0 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/features/internal/file.js diff --git a/test/exportsField.js b/test/exportsField.js index f7ee92af..34097ee2 100644 --- a/test/exportsField.js +++ b/test/exportsField.js @@ -2506,4 +2506,64 @@ describe("ExportsFieldPlugin", () => { done(); }); }); + + it("should resolve with wildcard pattern #2", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve({}, fixture, "m/features/y/y.js", {}, (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal( + path.resolve(fixture, "./node_modules/m/src/features/y/y.js") + ); + done(); + }); + }); + + it("should resolve with wildcard pattern #3", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve( + {}, + fixture, + "m/features-no-ext/y/y.js", + {}, + (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal( + path.resolve(fixture, "./node_modules/m/src/features/y/y.js") + ); + done(); + } + ); + }); + + it("should throw error if target is 'null'", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve( + {}, + fixture, + "m/features/internal/file.js", + {}, + (err, result) => { + if (!err) throw new Error(`expect error, got ${result}`); + err.should.be.instanceof(Error); + err.message.should.match( + /Package path \.\/features\/internal\/file\.js is not exported/ + ); + done(); + } + ); + }); }); diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/package.json b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json index d610922b..1c6e157e 100644 --- a/test/fixtures/imports-exports-wildcard/node_modules/m/package.json +++ b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json @@ -1,7 +1,9 @@ { "name": "m", "exports": { - "./features/*.js": "./src/features/*.js" + "./features-no-ext/*": "./src/features/*", + "./features/*.js": "./src/features/*.js", + "./features/internal/*": null }, "imports": { "#internal/*.js": "./src/internal/*.js" diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/internal/file.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/features/internal/file.js new file mode 100644 index 00000000..e69de29b From 125189a945d349b0b4f8f3bd4e31dd4fb3752401 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Sun, 16 Apr 2023 23:24:05 +0300 Subject: [PATCH 03/12] test: not working --- test/exportsField.js | 98 +++++++++++++++++++ .../node_modules/m/package.json | 7 +- .../node_modules/m/src/middle-1/f.js | 0 .../node_modules/m/src/middle-1/nested/f.js | 0 .../m/src/middle-3/nested/f/nested/f.js | 0 .../node_modules/m/src/middle-4/f/f.js | 0 .../node_modules/m/src/middle/f.js | 0 .../node_modules/m/src/middle/nested/f.js | 0 8 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/f.js create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/nested/f.js create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-3/nested/f/nested/f.js create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-4/f/f.js create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/f.js create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/nested/f.js diff --git a/test/exportsField.js b/test/exportsField.js index 34097ee2..b1f6ab8d 100644 --- a/test/exportsField.js +++ b/test/exportsField.js @@ -2545,6 +2545,104 @@ describe("ExportsFieldPlugin", () => { ); }); + it("should resolve with wildcard pattern #4", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve({}, fixture, "m/middle/nested/f.js", {}, (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal( + path.resolve(fixture, "./node_modules/m/src/middle/nested/f.js") + ); + done(); + }); + }); + + it("should resolve with wildcard pattern #5", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve( + {}, + fixture, + "m/middle-1/nested/f.js", + {}, + (err, result) => { + 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") + ); + done(); + } + ); + }); + + it("should resolve with wildcard pattern #6", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve( + {}, + fixture, + "m/middle-2/nested/f.js", + {}, + (err, result) => { + 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") + ); + done(); + } + ); + }); + + it("should resolve with wildcard pattern #7", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve({}, fixture, "m/middle-3/nested/f", {}, (err, result) => { + 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" + ) + ); + done(); + }); + }); + + it("should resolve with wildcard pattern #7", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve({}, fixture, "m/middle-4/f/nested", {}, (err, result) => { + 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" + ) + ); + done(); + }); + }); + it("should throw error if target is 'null'", done => { const fixture = path.resolve( __dirname, diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/package.json b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json index 1c6e157e..552b1bf7 100644 --- a/test/fixtures/imports-exports-wildcard/node_modules/m/package.json +++ b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json @@ -3,7 +3,12 @@ "exports": { "./features-no-ext/*": "./src/features/*", "./features/*.js": "./src/features/*.js", - "./features/internal/*": null + "./features/internal/*": null, + "./middle/nested/f.js": "./src/middle/nested/f.js", + "./middle-1/nested/*.js": "./src/middle-1/nested/*.js", + "./middle-2/*/f.js": "./src/middle-2/*/f.js", + "./middle-3/*": "./src/middle-3/*/*.js", + "./middle-4/*/nested": "./src/middle-4/*/*.js" }, "imports": { "#internal/*.js": "./src/internal/*.js" diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/f.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/nested/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-1/nested/f.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-3/nested/f/nested/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-3/nested/f/nested/f.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-4/f/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-4/f/f.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/f.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/nested/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle/nested/f.js new file mode 100644 index 00000000..e69de29b From 5b875554e7e1578550d51c4b93baa6a09c0e4ab7 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Mon, 17 Apr 2023 22:33:48 +0300 Subject: [PATCH 04/12] refactor: code --- lib/util/entrypoints.js | 58 +++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js index 80d10d67..f0f6ba92 100644 --- a/lib/util/entrypoints.js +++ b/lib/util/entrypoints.js @@ -126,7 +126,7 @@ function createFieldProcessor(treeRoot, assertRequest, assertTarget) { if (match === null) return []; - const [mapping, remainRequestIndex] = match; + const [mapping, remainingRequest, subpathMapping] = match; /** @type {DirectMapping|null} */ let direct = null; @@ -143,16 +143,9 @@ function createFieldProcessor(treeRoot, assertRequest, assertTarget) { direct = /** @type {DirectMapping} */ (mapping); } - const remainingRequest = - remainRequestIndex === request.length + 1 - ? undefined - : remainRequestIndex < 0 - ? request.slice(-remainRequestIndex - 1) - : request.slice(remainRequestIndex); - return directMapping( remainingRequest, - remainRequestIndex < 0, + subpathMapping, direct, conditionNames, assertTarget @@ -251,17 +244,25 @@ function assertImportTarget(imp, expectFolder) { } } +function getRemainingRequest(index, request) { + return index === request.length + 1 + ? undefined + : index < 0 + ? request.slice(-index - 1) + : request.slice(index); +} + /** * Trying to match request to field * @param {string} request request * @param {PathTreeNode} treeRoot path tree root - * @returns {[MappingValue, number]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings + * @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, 1] : null; + return value ? [value, getRemainingRequest(1, request), false] : null; } if ( @@ -271,24 +272,35 @@ function findMatch(request, treeRoot) { ) { const value = treeRoot.files.get(request); - return value ? [value, request.length + 1] : null; + return value + ? [value, getRemainingRequest(request.length + 1, request), false] + : null; } let node = treeRoot; let lastNonSlashIndex = 0; let slashIndex = request.indexOf("/", 0); - /** @type {[MappingValue, number]|null} */ + /** @type {[MappingValue, string, boolean]|null} */ let lastFolderMatch = null; const applyFolderMapping = () => { const folderMapping = node.folder; + if (folderMapping) { if (lastFolderMatch) { lastFolderMatch[0] = folderMapping; - lastFolderMatch[1] = -lastNonSlashIndex - 1; + lastFolderMatch[1] = getRemainingRequest( + -lastNonSlashIndex - 1, + request + ); + lastFolderMatch[2] = -lastNonSlashIndex - 1 < 0; } else { - lastFolderMatch = [folderMapping, -lastNonSlashIndex - 1]; + lastFolderMatch = [ + folderMapping, + getRemainingRequest(-lastNonSlashIndex - 1, request), + -lastNonSlashIndex - 1 < 0 + ]; } } }; @@ -298,10 +310,18 @@ function findMatch(request, treeRoot) { for (const [key, target] of wildcardMappings) { if (remainingRequest.startsWith(key)) { if (!lastFolderMatch) { - lastFolderMatch = [target, lastNonSlashIndex + key.length]; - } else if (lastFolderMatch[1] < lastNonSlashIndex + key.length) { + 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] = lastNonSlashIndex + key.length; + lastFolderMatch[1] = request.slice(lastNonSlashIndex + key.length); + lastFolderMatch[2] = lastNonSlashIndex + key.length < 0; } } } @@ -338,7 +358,7 @@ function findMatch(request, treeRoot) { const value = node.files.get(remainingRequest); if (value) { - return [value, request.length + 1]; + return [value, getRemainingRequest(request.length + 1, request), false]; } applyFolderMapping(); From aaec883338f821bca2a5006cd98a753a90234e53 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 18 Apr 2023 00:54:49 +0300 Subject: [PATCH 05/12] refactor: rewrite implementation --- lib/util/entrypoints.js | 235 ++++++++++++++++++---------------------- test/exportsField.js | 9 +- 2 files changed, 110 insertions(+), 134 deletions(-) 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(); }); From 50d5cfeec10ee9e4b4812f710b5360cc5e19b1ef Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 18 Apr 2023 02:11:37 +0300 Subject: [PATCH 06/12] refactor: rewrite implementation --- lib/util/entrypoints.js | 173 +++++----------------------------------- 1 file changed, 22 insertions(+), 151 deletions(-) diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js index 5172411b..85f1f7c9 100644 --- a/lib/util/entrypoints.js +++ b/lib/util/entrypoints.js @@ -11,14 +11,6 @@ /** @typedef {Record|ConditionalMapping|DirectMapping} ExportsField */ /** @typedef {Record} ImportsField */ -/** - * @typedef {Object} PathTreeNode - * @property {Map|null} children - * @property {MappingValue} folder - * @property {Map|null} wildcards - * @property {Map} files - */ - /** * Processing exports/imports field * @callback FieldProcessor @@ -80,7 +72,6 @@ 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); @@ -94,7 +85,6 @@ module.exports.processExportsField = function processExportsField( ) { return createFieldProcessor( buildExportsFieldPathTree(exportsField), - exportsField, assertExportsFieldRequest, assertExportTarget ); @@ -109,20 +99,18 @@ module.exports.processImportsField = function processImportsField( ) { return createFieldProcessor( buildImportsFieldPathTree(importsField), - importsField, assertImportsFieldRequest, assertImportTarget ); }; /** - * @param {PathTreeNode} treeRoot root - * @param {ExportsField | ImportsField} field exports or import field + * @param {ExportsField | ImportsField} field root * @param {(s: string) => string} assertRequest assertRequest * @param {(s: string, f: boolean) => void} assertTarget assertTarget * @returns {FieldProcessor} field processor */ -function createFieldProcessor(treeRoot, field, assertRequest, assertTarget) { +function createFieldProcessor(field, assertRequest, assertTarget) { return function fieldProcessor(request, conditionNames) { request = assertRequest(request); @@ -264,25 +252,6 @@ function patternKeyCompare(a, b) { 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 @@ -290,20 +259,12 @@ function isConditionalExportsMainSugar(exports) { * @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, field) { - if (isConditionalExportsMainSugar(field)) { - field = { ".": field }; - } - if ( - (Object.prototype.hasOwnProperty.call(field, "./" + request) || - Object.prototype.hasOwnProperty.call(field, "." + request)) && + Object.prototype.hasOwnProperty.call(field, request) && !request.includes("*") && !request.endsWith("/") ) { - const target = field["./" + request] || field["." + request]; - - if (target === "./") return null; - + const target = field[request]; const isDirectory = typeof target === "string" ? target.endsWith("/") : false; @@ -316,8 +277,7 @@ function findMatch(request, field) { const keys = Object.getOwnPropertyNames(field); for (let i = 0; i < keys.length; i++) { - const originalKey = keys[i]; - const key = keys[i].slice(2); + const key = keys[i]; const patternIndex = key.indexOf("*"); if (patternIndex !== -1 && request.startsWith(key.slice(0, patternIndex))) { @@ -336,19 +296,19 @@ function findMatch(request, field) { ); } } else if ( - originalKey[originalKey.length - 1] === "/" && + // For `./foo/` or `./` + key[key.length - 1] === "/" && request.startsWith(key) && patternKeyCompare(bestMatch, key) === 1 ) { - bestMatch = originalKey; + bestMatch = key; bestMatchSubpath = request.slice(key.length); } } if (bestMatch === "") return null; - const target = - field[bestMatch.startsWith("./") ? bestMatch : "./" + bestMatch]; + const target = field[bestMatch]; const pattern = bestMatch.endsWith("/"); return [target, /** @type {string} */ (bestMatchSubpath), pattern]; @@ -494,105 +454,17 @@ function conditionalMapping(conditionalMapping_, conditionNames) { return null; } -/** - * Internal helper to create path tree node - * to ensure that each node gets the same hidden class - * @returns {PathTreeNode} node - */ -function createNode() { - return { - children: null, - folder: null, - wildcards: null, - files: new Map() - }; -} - -/** - * Internal helper for building path tree - * @param {PathTreeNode} root root - * @param {string} path path - * @param {MappingValue} target target - */ -function walkPath(root, path, target) { - if (path.length === 0) { - root.folder = target; - return; - } - - let node = root; - // Typical path tree can look like - // root - // - files: ["a.js", "b.js"] - // - children: - // node1: - // - files: ["a.js", "b.js"] - let lastNonSlashIndex = 0; - let slashIndex = path.indexOf("/", 0); - - while (slashIndex !== -1) { - const folder = path.slice(lastNonSlashIndex, slashIndex); - let 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 { - // 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 = /** @type {PathTreeNode} */ (newNode); - lastNonSlashIndex = slashIndex + 1; - slashIndex = path.indexOf("/", lastNonSlashIndex); - } - - if (lastNonSlashIndex >= path.length) { - node.folder = target; - } else { - const file = lastNonSlashIndex > 0 ? path.slice(lastNonSlashIndex) : path; - const wildcardsIndex = file.indexOf("*"); - if (wildcardsIndex !== -1) { - if (node.wildcards === null) node.wildcards = new Map(); - node.wildcards.set(file.slice(0, wildcardsIndex), target); - } else { - node.files.set(file, target); - } - } -} - /** * @param {ExportsField} field exports field - * @returns {PathTreeNode} tree root + * @returns {ExportsField} normalized exports field */ function buildExportsFieldPathTree(field) { - const root = createNode(); - // handle syntax sugar, if exports field is direct mapping for "." - if (typeof field === "string") { - root.files.set("", field); - - return root; - } else if (Array.isArray(field)) { - root.files.set("", field.slice()); - - return root; + if (typeof field === "string" || Array.isArray(field)) { + return { "": field }; } + const newField = /** @type {ExportsField} */ ({}); const keys = Object.keys(field); for (let i = 0; i < keys.length; i++) { @@ -613,8 +485,7 @@ function buildExportsFieldPathTree(field) { i++; } - root.files.set("", field); - return root; + return { "": field }; } throw new Error( @@ -625,7 +496,8 @@ function buildExportsFieldPathTree(field) { } if (key.length === 1) { - root.files.set("", field[key]); + newField[""] = field[key]; + continue; } @@ -637,20 +509,19 @@ function buildExportsFieldPathTree(field) { ); } - walkPath(root, key.slice(2), field[key]); + newField[key.slice(2)] = field[key]; } - return root; + return newField; } /** * @param {ImportsField} field imports field - * @returns {PathTreeNode} root + * @returns {ImportsField} normalized imports field */ function buildImportsFieldPathTree(field) { - const root = createNode(); - const keys = Object.keys(field); + const newField = /** @type {ImportsField} */ ({}); for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -677,8 +548,8 @@ function buildImportsFieldPathTree(field) { ); } - walkPath(root, key.slice(1), field[key]); + newField[key.slice(1)] = field[key]; } - return root; + return newField; } From f61470792c4fdfdd3bfb049fe124432de133bb52 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 18 Apr 2023 02:14:19 +0300 Subject: [PATCH 07/12] test: fix --- .../node_modules/m/src/middle-2/nested/f.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-2/nested/f.js diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-2/nested/f.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-2/nested/f.js new file mode 100644 index 00000000..e5b70a86 --- /dev/null +++ b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-2/nested/f.js @@ -0,0 +1 @@ +module.exports = { nested: "nested" } From f1fbd8181aa8c7c4500a1db8504065c1a5c50e1c Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 18 Apr 2023 02:29:54 +0300 Subject: [PATCH 08/12] fix: logic --- lib/util/entrypoints.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js index 85f1f7c9..4b8f5a0e 100644 --- a/lib/util/entrypoints.js +++ b/lib/util/entrypoints.js @@ -84,7 +84,7 @@ module.exports.processExportsField = function processExportsField( exportsField ) { return createFieldProcessor( - buildExportsFieldPathTree(exportsField), + buildExportsField(exportsField), assertExportsFieldRequest, assertExportTarget ); @@ -98,7 +98,7 @@ module.exports.processImportsField = function processImportsField( importsField ) { return createFieldProcessor( - buildImportsFieldPathTree(importsField), + buildImportsField(importsField), assertImportsFieldRequest, assertImportTarget ); @@ -295,8 +295,9 @@ function findMatch(request, field) { request.length - patternTrailer.length ); } - } else if ( - // For `./foo/` or `./` + } + // For legacy `./foo/` + else if ( key[key.length - 1] === "/" && request.startsWith(key) && patternKeyCompare(bestMatch, key) === 1 @@ -304,6 +305,15 @@ function findMatch(request, field) { bestMatch = key; bestMatchSubpath = request.slice(key.length); } + // For legacy `./` + else if ( + key === "./" && + patternKeyCompare(bestMatch, key) === 1 && + request.length > 0 + ) { + bestMatch = key; + bestMatchSubpath = request.slice(key.length - 2); + } } if (bestMatch === "") return null; @@ -458,7 +468,7 @@ function conditionalMapping(conditionalMapping_, conditionNames) { * @param {ExportsField} field exports field * @returns {ExportsField} normalized exports field */ -function buildExportsFieldPathTree(field) { +function buildExportsField(field) { // handle syntax sugar, if exports field is direct mapping for "." if (typeof field === "string" || Array.isArray(field)) { return { "": field }; @@ -509,7 +519,8 @@ function buildExportsFieldPathTree(field) { ); } - newField[key.slice(2)] = field[key]; + // Keep "./" for legacy `{ "./": "./" }` + newField[key.slice(2) || "./"] = field[key]; } return newField; @@ -519,7 +530,7 @@ function buildExportsFieldPathTree(field) { * @param {ImportsField} field imports field * @returns {ImportsField} normalized imports field */ -function buildImportsFieldPathTree(field) { +function buildImportsField(field) { const keys = Object.keys(field); const newField = /** @type {ImportsField} */ ({}); From b848e438e21689fe4c87838abfa3d0f487feb6f4 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 18 Apr 2023 02:34:43 +0300 Subject: [PATCH 09/12] test: more --- test/exportsField.js | 60 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/test/exportsField.js b/test/exportsField.js index 79418017..1ffad76a 100644 --- a/test/exportsField.js +++ b/test/exportsField.js @@ -1955,7 +1955,7 @@ describe("Process exports field", function exportsField() { ] }, { - name: "wildcard pattern with suffix #1", + name: "wildcard pattern #1", expect: ["./A/b.js"], suite: [ { @@ -1966,7 +1966,7 @@ describe("Process exports field", function exportsField() { ] }, { - name: "wildcard pattern with suffix #2", + name: "wildcard pattern #2", expect: ["./A/b/c.js"], suite: [ { @@ -1975,6 +1975,62 @@ describe("Process exports field", function exportsField() { "./a/b/c.js", [] ] + }, + { + name: "wildcard pattern #3", + expect: ["./A/b/c.js"], + suite: [ + { + "./a/*/c.js": "./A/*/c.js" + }, + "./a/b/c.js", + [] + ] + }, + { + name: "wildcard pattern #4", + expect: ["./A/b/b.js"], + suite: [ + { + "./a/*/c.js": "./A/*/*.js" + }, + "./a/b/c.js", + [] + ] + }, + { + name: "wildcard pattern #5", + expect: ["./browser/index.js"], // default condition used + suite: [ + { + "./lib/*": { + browser: ["./browser/*"] + }, + "./dist/*.js": { + node: "./*.js", + default: "./browser/*.js" + } + }, + "./dist/index.js", + ["browser"] + ] + }, + { + name: "wildcard pattern #5", + expect: ["./browser/index.js"], // default condition used + suite: [ + { + "./lib/*": { + browser: ["./browser/*"] + }, + "./dist/*.js": { + node: "./*.js", + default: "./browser/*.js" + } + }, + "./lib/index.js", + ["browser"] + ] } ]; From 05659b7516c81a24457429ad4549355e8d57f88a Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 18 Apr 2023 03:25:00 +0300 Subject: [PATCH 10/12] test: more --- lib/util/entrypoints.js | 72 +++++++-- test/exportsField.js | 148 +++++++++++++++++- .../node_modules/m/package.json | 3 +- .../node_modules/m/src/middle-5/f$/$.js | 0 .../node_modules/m/src/middle-5/f/$.js | 0 5 files changed, 203 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f$/$.js create mode 100644 test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f/$.js diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js index 4b8f5a0e..79f0d5d8 100644 --- a/lib/util/entrypoints.js +++ b/lib/util/entrypoints.js @@ -75,6 +75,7 @@ Conditional mapping nested in another conditional mapping is called nested mappi const slashCode = "/".charCodeAt(0); const dotCode = ".".charCodeAt(0); const hashCode = "#".charCodeAt(0); +const patternRegEx = /\*/g; /** * @param {ExportsField} exportsField the exports field @@ -118,7 +119,7 @@ function createFieldProcessor(field, assertRequest, assertTarget) { if (match === null) return []; - const [mapping, remainingRequest, subpathMapping] = match; + const [mapping, remainingRequest, isSubpathMapping, isPattern] = match; /** @type {DirectMapping|null} */ let direct = null; @@ -137,7 +138,8 @@ function createFieldProcessor(field, assertRequest, assertTarget) { return directMapping( remainingRequest, - subpathMapping, + isPattern, + isSubpathMapping, direct, conditionNames, assertTarget @@ -256,7 +258,7 @@ function patternKeyCompare(a, b) { * Trying to match request to field * @param {string} request request * @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 + * @returns {[MappingValue, string, boolean, 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, field) { if ( @@ -265,10 +267,10 @@ function findMatch(request, field) { !request.endsWith("/") ) { const target = field[request]; - const isDirectory = + const isSubpathMapping = typeof target === "string" ? target.endsWith("/") : false; - return [target, "", isDirectory]; + return [target, "", isSubpathMapping, false]; } let bestMatch = ""; @@ -319,9 +321,15 @@ function findMatch(request, field) { if (bestMatch === "") return null; const target = field[bestMatch]; - const pattern = bestMatch.endsWith("/"); - - return [target, /** @type {string} */ (bestMatchSubpath), pattern]; + const isSubpathMapping = bestMatch.endsWith("/"); + const isPattern = bestMatch.includes("*"); + + return [ + target, + /** @type {string} */ (bestMatchSubpath), + isSubpathMapping, + isPattern + ]; } /** @@ -336,7 +344,8 @@ function isConditionalMapping(mapping) { /** * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings - * @param {boolean} subpathMapping true, for subpath mappings + * @param {boolean} isPattern true, if mapping is a pattern (contains "*") + * @param {boolean} isSubpathMapping true, for subpath mappings * @param {DirectMapping|null} mappingTarget direct export * @param {Set} conditionNames condition names * @param {(d: string, f: boolean) => void} assert asserting direct value @@ -344,7 +353,8 @@ function isConditionalMapping(mapping) { */ function directMapping( remainingRequest, - subpathMapping, + isPattern, + isSubpathMapping, mappingTarget, conditionNames, assert @@ -353,7 +363,13 @@ function directMapping( if (typeof mappingTarget === "string") { return [ - targetMapping(remainingRequest, subpathMapping, mappingTarget, assert) + targetMapping( + remainingRequest, + isPattern, + isSubpathMapping, + mappingTarget, + assert + ) ]; } @@ -362,7 +378,13 @@ function directMapping( for (const exp of mappingTarget) { if (typeof exp === "string") { targets.push( - targetMapping(remainingRequest, subpathMapping, exp, assert) + targetMapping( + remainingRequest, + isPattern, + isSubpathMapping, + exp, + assert + ) ); continue; } @@ -371,7 +393,8 @@ function directMapping( if (!mapping) continue; const innerExports = directMapping( remainingRequest, - subpathMapping, + isPattern, + isSubpathMapping, mapping, conditionNames, assert @@ -386,28 +409,43 @@ function directMapping( /** * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings - * @param {boolean} subpathMapping true, for subpath mappings + * @param {boolean} isPattern true, if mapping is a pattern (contains "*") + * @param {boolean} isSubpathMapping true, for subpath mappings * @param {string} mappingTarget direct export * @param {(d: string, f: boolean) => void} assert asserting direct value * @returns {string} mapping result */ function targetMapping( remainingRequest, - subpathMapping, + isPattern, + isSubpathMapping, mappingTarget, assert ) { if (remainingRequest === undefined) { assert(mappingTarget, false); + return mappingTarget; } - if (subpathMapping) { + + if (isSubpathMapping) { assert(mappingTarget, true); + return mappingTarget + remainingRequest; } + assert(mappingTarget, false); - return mappingTarget.replace(/\*/g, remainingRequest.replace(/\$/g, "$$")); + let result = mappingTarget; + + if (isPattern) { + result = result.replace( + patternRegEx, + remainingRequest.replace(/\$/g, "$$") + ); + } + + return result; } /** diff --git a/test/exportsField.js b/test/exportsField.js index 1ffad76a..53646856 100644 --- a/test/exportsField.js +++ b/test/exportsField.js @@ -2000,7 +2000,7 @@ describe("Process exports field", function exportsField() { }, { name: "wildcard pattern #5", - expect: ["./browser/index.js"], // default condition used + expect: ["./browser/index.js"], suite: [ { "./lib/*": { @@ -2017,7 +2017,7 @@ describe("Process exports field", function exportsField() { }, { name: "wildcard pattern #5", - expect: ["./browser/index.js"], // default condition used + expect: ["./browser/index.js"], suite: [ { "./lib/*": { @@ -2031,6 +2031,134 @@ describe("Process exports field", function exportsField() { "./lib/index.js", ["browser"] ] + }, + { + name: "wildcard pattern #6", + expect: ["./browser/foo/bar.js"], + suite: [ + { + "./lib/*/bar.js": { + browser: ["./browser/*/bar.js"] + }, + "./dist/*/bar.js": { + node: "./*.js", + default: "./browser/*.js" + } + }, + "./lib/foo/bar.js", + ["browser"] + ] + }, + { + name: "wildcard pattern #6", + expect: ["./browser/foo.js"], + suite: [ + { + "./lib/*/bar.js": { + browser: ["./browser/*/bar.js"] + }, + "./dist/*/bar.js": { + node: "./*.js", + default: "./browser/*.js" + } + }, + "./dist/foo/bar.js", + ["browser"] + ] + }, + { + name: "wildcard pattern #7", + expect: ["./browser/foo/default.js"], + suite: [ + { + "./lib/*/bar.js": { + browser: ["./browser/*/bar.js"] + }, + "./dist/*/bar.js": { + node: "./*.js", + default: "./browser/*/default.js" + } + }, + "./dist/foo/bar.js", + ["default"] + ] + }, + { + name: "wildcard pattern #8", + expect: ["./A/b/b/b.js"], + suite: [ + { + "./a/*/c.js": "./A/*/*/*.js" + }, + "./a/b/c.js", + [] + ] + }, + { + name: "wildcard pattern #9", + expect: ["./A/b/b/b.js", "./B/b/b/b.js"], + suite: [ + { + "./a/*/c.js": ["./A/*/*/*.js", "./B/*/*/*.js"] + }, + "./a/b/c.js", + [] + ] + }, + { + name: "wildcard pattern #10", + expect: ["./A/b/b/b.js"], + suite: [ + { + "./a/foo-*/c.js": "./A/*/*/*.js" + }, + "./a/foo-b/c.js", + [] + ] + }, + { + name: "wildcard pattern #11", + expect: ["./A/b/b/b.js"], + suite: [ + { + "./a/*-foo/c.js": "./A/*/*/*.js" + }, + "./a/b-foo/c.js", + [] + ] + }, + { + name: "wildcard pattern #12", + expect: ["./A/b/b/b.js"], + suite: [ + { + "./a/foo-*-foo/c.js": "./A/*/*/*.js" + }, + "./a/foo-b-foo/c.js", + [] + ] + }, + { + name: "wildcard pattern #13", + expect: ["./A/b/c/d.js"], + suite: [ + { + "./a/foo-*-foo/c.js": "./A/b/c/d.js" + }, + "./a/foo-b-foo/c.js", + [] + ] + }, + { + name: "wildcard pattern #13", + expect: ["./A/b/c/*.js"], + suite: [ + { + "./a/foo-foo/c.js": "./A/b/c/*.js" + }, + "./a/foo-foo/c.js", + [] + ] } ]; @@ -2696,6 +2824,22 @@ describe("ExportsFieldPlugin", () => { }); }); + it("should resolve with wildcard pattern #9", done => { + const fixture = path.resolve( + __dirname, + "./fixtures/imports-exports-wildcard/" + ); + + resolver.resolve({}, fixture, "m/middle-5/f$/$", {}, (err, result) => { + if (err) return done(err); + if (!result) throw new Error("No result"); + result.should.equal( + path.resolve(fixture, "./node_modules/m/src/middle-5/f$/$.js") + ); + done(); + }); + }); + it("should throw error if target is 'null'", done => { const fixture = path.resolve( __dirname, diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/package.json b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json index 552b1bf7..aa2be496 100644 --- a/test/fixtures/imports-exports-wildcard/node_modules/m/package.json +++ b/test/fixtures/imports-exports-wildcard/node_modules/m/package.json @@ -8,7 +8,8 @@ "./middle-1/nested/*.js": "./src/middle-1/nested/*.js", "./middle-2/*/f.js": "./src/middle-2/*/f.js", "./middle-3/*": "./src/middle-3/*/*.js", - "./middle-4/*/nested": "./src/middle-4/*/*.js" + "./middle-4/*/nested": "./src/middle-4/*/*.js", + "./middle-5/*/$": "./src/middle-5/*/$.js" }, "imports": { "#internal/*.js": "./src/internal/*.js" diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f$/$.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f$/$.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f/$.js b/test/fixtures/imports-exports-wildcard/node_modules/m/src/middle-5/f/$.js new file mode 100644 index 00000000..e69de29b From d8176da92f6522c2c4e9dfd689f388c28b9919b9 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 18 Apr 2023 03:45:41 +0300 Subject: [PATCH 11/12] pref: reduce memory consumation --- lib/util/entrypoints.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js index 79f0d5d8..3bc143c9 100644 --- a/lib/util/entrypoints.js +++ b/lib/util/entrypoints.js @@ -86,6 +86,7 @@ module.exports.processExportsField = function processExportsField( ) { return createFieldProcessor( buildExportsField(exportsField), + "", assertExportsFieldRequest, assertExportTarget ); @@ -100,6 +101,7 @@ module.exports.processImportsField = function processImportsField( ) { return createFieldProcessor( buildImportsField(importsField), + "#", assertImportsFieldRequest, assertImportTarget ); @@ -107,15 +109,16 @@ module.exports.processImportsField = function processImportsField( /** * @param {ExportsField | ImportsField} field root + * @param {string} prefix Request prefix, for `imports` field it is `#`, for `exports` field it is `./`. * @param {(s: string) => string} assertRequest assertRequest * @param {(s: string, f: boolean) => void} assertTarget assertTarget * @returns {FieldProcessor} field processor */ -function createFieldProcessor(field, assertRequest, assertTarget) { +function createFieldProcessor(field, prefix, assertRequest, assertTarget) { return function fieldProcessor(request, conditionNames) { request = assertRequest(request); - const match = findMatch(request, field); + const match = findMatch(prefix + request, field); if (match === null) return []; @@ -570,7 +573,6 @@ function buildExportsField(field) { */ function buildImportsField(field) { const keys = Object.keys(field); - const newField = /** @type {ImportsField} */ ({}); for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -596,9 +598,7 @@ function buildImportsField(field) { )})` ); } - - newField[key.slice(1)] = field[key]; } - return newField; + return field; } From 91edec1f9bdde54c0f87176c634e682626661e47 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 18 Apr 2023 04:27:56 +0300 Subject: [PATCH 12/12] pref: reduce memory consumption --- lib/util/entrypoints.js | 40 ++++++++++++--------------------- test/exportsField.js | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/lib/util/entrypoints.js b/lib/util/entrypoints.js index 3bc143c9..36e335e5 100644 --- a/lib/util/entrypoints.js +++ b/lib/util/entrypoints.js @@ -86,7 +86,7 @@ module.exports.processExportsField = function processExportsField( ) { return createFieldProcessor( buildExportsField(exportsField), - "", + request => (request.length === 0 ? "." : "./" + request), assertExportsFieldRequest, assertExportTarget ); @@ -101,7 +101,7 @@ module.exports.processImportsField = function processImportsField( ) { return createFieldProcessor( buildImportsField(importsField), - "#", + request => "#" + request, assertImportsFieldRequest, assertImportTarget ); @@ -109,16 +109,21 @@ module.exports.processImportsField = function processImportsField( /** * @param {ExportsField | ImportsField} field root - * @param {string} prefix Request prefix, for `imports` field it is `#`, for `exports` field it is `./`. + * @param {(s: string) => string} normalizeRequest Normalize request, for `imports` field it adds `#`, for `exports` field it adds `.` or `./` * @param {(s: string) => string} assertRequest assertRequest * @param {(s: string, f: boolean) => void} assertTarget assertTarget * @returns {FieldProcessor} field processor */ -function createFieldProcessor(field, prefix, assertRequest, assertTarget) { +function createFieldProcessor( + field, + normalizeRequest, + assertRequest, + assertTarget +) { return function fieldProcessor(request, conditionNames) { request = assertRequest(request); - const match = findMatch(prefix + request, field); + const match = findMatch(normalizeRequest(request), field); if (match === null) return []; @@ -270,10 +275,8 @@ function findMatch(request, field) { !request.endsWith("/") ) { const target = field[request]; - const isSubpathMapping = - typeof target === "string" ? target.endsWith("/") : false; - return [target, "", isSubpathMapping, false]; + return [target, "", false, false]; } let bestMatch = ""; @@ -310,15 +313,6 @@ function findMatch(request, field) { bestMatch = key; bestMatchSubpath = request.slice(key.length); } - // For legacy `./` - else if ( - key === "./" && - patternKeyCompare(bestMatch, key) === 1 && - request.length > 0 - ) { - bestMatch = key; - bestMatchSubpath = request.slice(key.length - 2); - } } if (bestMatch === "") return null; @@ -512,10 +506,9 @@ function conditionalMapping(conditionalMapping_, conditionNames) { function buildExportsField(field) { // handle syntax sugar, if exports field is direct mapping for "." if (typeof field === "string" || Array.isArray(field)) { - return { "": field }; + return { ".": field }; } - const newField = /** @type {ExportsField} */ ({}); const keys = Object.keys(field); for (let i = 0; i < keys.length; i++) { @@ -536,7 +529,7 @@ function buildExportsField(field) { i++; } - return { "": field }; + return { ".": field }; } throw new Error( @@ -547,8 +540,6 @@ function buildExportsField(field) { } if (key.length === 1) { - newField[""] = field[key]; - continue; } @@ -559,12 +550,9 @@ function buildExportsField(field) { )})` ); } - - // Keep "./" for legacy `{ "./": "./" }` - newField[key.slice(2) || "./"] = field[key]; } - return newField; + return field; } /** diff --git a/test/exportsField.js b/test/exportsField.js index 53646856..035fc759 100644 --- a/test/exportsField.js +++ b/test/exportsField.js @@ -594,6 +594,56 @@ describe("Process exports field", function exportsField() { [] ] }, + { + name: "Direct mapping #11", + expect: ["./foo.js"], + suite: [ + { + "./": "./", + "./*": "./*", + "./dist/index.js": "./dist/index.js" + }, + "./foo.js", + [] + ] + }, + { + name: "Direct mapping #12", + expect: ["./foo/bar/baz.js"], + suite: [ + { + "./": "./", + "./*": "./*", + "./dist/index.js": "./dist/index.js" + }, + "./foo/bar/baz.js", + [] + ] + }, + { + name: "Direct mapping #13", + expect: ["./foo/bar/baz.js"], + suite: [ + { + "./": "./", + "./dist/index.js": "./dist/index.js" + }, + "./foo/bar/baz.js", + [] + ] + }, + { + name: "Direct mapping #14", + expect: ["./foo/bar/baz.js"], + suite: [ + { + "./*": "./*", + "./dist/index.js": "./dist/index.js" + }, + "./foo/bar/baz.js", + [] + ] + }, //#endregion //#region Direct and conditional mapping