summaryrefslogtreecommitdiff
path: root/node_modules/enhanced-resolve/lib/util/entrypoints.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/enhanced-resolve/lib/util/entrypoints.js')
-rw-r--r--node_modules/enhanced-resolve/lib/util/entrypoints.js604
1 files changed, 604 insertions, 0 deletions
diff --git a/node_modules/enhanced-resolve/lib/util/entrypoints.js b/node_modules/enhanced-resolve/lib/util/entrypoints.js
new file mode 100644
index 00000000..392898ce
--- /dev/null
+++ b/node_modules/enhanced-resolve/lib/util/entrypoints.js
@@ -0,0 +1,604 @@
+/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Ivan Kopeykin @vankop
+*/
+
+"use strict";
+
+/** @typedef {string|(string|ConditionalMapping)[]} DirectMapping */
+/** @typedef {{[k: string]: MappingValue}} ConditionalMapping */
+/** @typedef {ConditionalMapping|DirectMapping|null} MappingValue */
+/** @typedef {Record<string, MappingValue>|ConditionalMapping|DirectMapping} ExportsField */
+/** @typedef {Record<string, MappingValue>} ImportsField */
+
+/**
+ * Processing exports/imports field
+ * @callback FieldProcessor
+ * @param {string} request request
+ * @param {Set<string>} conditionNames condition names
+ * @returns {string[]} resolved paths
+ */
+
+/*
+Example exports field:
+{
+ ".": "./main.js",
+ "./feature": {
+ "browser": "./feature-browser.js",
+ "default": "./feature.js"
+ }
+}
+Terminology:
+
+Enhanced-resolve name keys ("." and "./feature") as exports field keys.
+
+If value is string or string[], mapping is called as a direct mapping
+and value called as a direct export.
+
+If value is key-value object, mapping is called as a conditional mapping
+and value called as a conditional export.
+
+Key in conditional mapping is called condition name.
+
+Conditional mapping nested in another conditional mapping is called nested mapping.
+
+----------
+
+Example imports field:
+{
+ "#a": "./main.js",
+ "#moment": {
+ "browser": "./moment/index.js",
+ "default": "moment"
+ },
+ "#moment/": {
+ "browser": "./moment/",
+ "default": "moment/"
+ }
+}
+Terminology:
+
+Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys.
+
+If value is string or string[], mapping is called as a direct mapping
+and value called as a direct export.
+
+If value is key-value object, mapping is called as a conditional mapping
+and value called as a conditional export.
+
+Key in conditional mapping is called condition name.
+
+Conditional mapping nested in another conditional mapping is called nested mapping.
+
+*/
+
+const slashCode = "/".charCodeAt(0);
+const dotCode = ".".charCodeAt(0);
+const hashCode = "#".charCodeAt(0);
+const patternRegEx = /\*/g;
+
+/**
+ * @param {ExportsField} exportsField the exports field
+ * @returns {FieldProcessor} process callback
+ */
+module.exports.processExportsField = function processExportsField(
+ exportsField
+) {
+ return createFieldProcessor(
+ buildExportsField(exportsField),
+ request => (request.length === 0 ? "." : "./" + request),
+ assertExportsFieldRequest,
+ assertExportTarget
+ );
+};
+
+/**
+ * @param {ImportsField} importsField the exports field
+ * @returns {FieldProcessor} process callback
+ */
+module.exports.processImportsField = function processImportsField(
+ importsField
+) {
+ return createFieldProcessor(
+ buildImportsField(importsField),
+ request => "#" + request,
+ assertImportsFieldRequest,
+ assertImportTarget
+ );
+};
+
+/**
+ * @param {ExportsField | ImportsField} field root
+ * @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,
+ normalizeRequest,
+ assertRequest,
+ assertTarget
+) {
+ return function fieldProcessor(request, conditionNames) {
+ request = assertRequest(request);
+
+ const match = findMatch(normalizeRequest(request), field);
+
+ if (match === null) return [];
+
+ const [mapping, remainingRequest, isSubpathMapping, isPattern] = match;
+
+ /** @type {DirectMapping|null} */
+ let direct = null;
+
+ if (isConditionalMapping(mapping)) {
+ direct = conditionalMapping(
+ /** @type {ConditionalMapping} */ (mapping),
+ conditionNames
+ );
+
+ // matching not found
+ if (direct === null) return [];
+ } else {
+ direct = /** @type {DirectMapping} */ (mapping);
+ }
+
+ return directMapping(
+ remainingRequest,
+ isPattern,
+ isSubpathMapping,
+ direct,
+ conditionNames,
+ assertTarget
+ );
+ };
+}
+
+/**
+ * @param {string} request request
+ * @returns {string} updated request
+ */
+function assertExportsFieldRequest(request) {
+ if (request.charCodeAt(0) !== dotCode) {
+ throw new Error('Request should be relative path and start with "."');
+ }
+ if (request.length === 1) return "";
+ if (request.charCodeAt(1) !== slashCode) {
+ throw new Error('Request should be relative path and start with "./"');
+ }
+ if (request.charCodeAt(request.length - 1) === slashCode) {
+ throw new Error("Only requesting file allowed");
+ }
+
+ return request.slice(2);
+}
+
+/**
+ * @param {string} request request
+ * @returns {string} updated request
+ */
+function assertImportsFieldRequest(request) {
+ if (request.charCodeAt(0) !== hashCode) {
+ throw new Error('Request should start with "#"');
+ }
+ if (request.length === 1) {
+ throw new Error("Request should have at least 2 characters");
+ }
+ if (request.charCodeAt(1) === slashCode) {
+ throw new Error('Request should not start with "#/"');
+ }
+ if (request.charCodeAt(request.length - 1) === slashCode) {
+ throw new Error("Only requesting file allowed");
+ }
+
+ return request.slice(1);
+}
+
+/**
+ * @param {string} exp export target
+ * @param {boolean} expectFolder is folder expected
+ */
+function assertExportTarget(exp, expectFolder) {
+ if (
+ exp.charCodeAt(0) === slashCode ||
+ (exp.charCodeAt(0) === dotCode && exp.charCodeAt(1) !== slashCode)
+ ) {
+ throw new Error(
+ `Export should be relative path and start with "./", got ${JSON.stringify(
+ exp
+ )}.`
+ );
+ }
+
+ const isFolder = exp.charCodeAt(exp.length - 1) === slashCode;
+
+ if (isFolder !== expectFolder) {
+ throw new Error(
+ expectFolder
+ ? `Expecting folder to folder mapping. ${JSON.stringify(
+ exp
+ )} should end with "/"`
+ : `Expecting file to file mapping. ${JSON.stringify(
+ exp
+ )} should not end with "/"`
+ );
+ }
+}
+
+/**
+ * @param {string} imp import target
+ * @param {boolean} expectFolder is folder expected
+ */
+function assertImportTarget(imp, expectFolder) {
+ const isFolder = imp.charCodeAt(imp.length - 1) === slashCode;
+
+ if (isFolder !== expectFolder) {
+ throw new Error(
+ expectFolder
+ ? `Expecting folder to folder mapping. ${JSON.stringify(
+ imp
+ )} should end with "/"`
+ : `Expecting file to file mapping. ${JSON.stringify(
+ imp
+ )} should not end with "/"`
+ );
+ }
+}
+
+/**
+ * @param {string} a first string
+ * @param {string} b second string
+ * @returns {number} compare result
+ */
+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;
+}
+
+/**
+ * Trying to match request to field
+ * @param {string} request request
+ * @param {ExportsField | ImportsField} field exports or import field
+ * @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 (
+ Object.prototype.hasOwnProperty.call(field, request) &&
+ !request.includes("*") &&
+ !request.endsWith("/")
+ ) {
+ const target = /** @type {{[k: string]: MappingValue}} */ (field)[request];
+
+ return [target, "", false, false];
+ }
+
+ /** @type {string} */
+ let bestMatch = "";
+ /** @type {string|undefined} */
+ let bestMatchSubpath;
+
+ const keys = Object.getOwnPropertyNames(field);
+
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ 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
+ );
+ }
+ }
+ // For legacy `./foo/`
+ else if (
+ key[key.length - 1] === "/" &&
+ request.startsWith(key) &&
+ patternKeyCompare(bestMatch, key) === 1
+ ) {
+ bestMatch = key;
+ bestMatchSubpath = request.slice(key.length);
+ }
+ }
+
+ if (bestMatch === "") return null;
+
+ const target = /** @type {{[k: string]: MappingValue}} */ (field)[bestMatch];
+ const isSubpathMapping = bestMatch.endsWith("/");
+ const isPattern = bestMatch.includes("*");
+
+ return [
+ target,
+ /** @type {string} */ (bestMatchSubpath),
+ isSubpathMapping,
+ isPattern
+ ];
+}
+
+/**
+ * @param {ConditionalMapping|DirectMapping|null} mapping mapping
+ * @returns {boolean} is conditional mapping
+ */
+function isConditionalMapping(mapping) {
+ return (
+ mapping !== null && typeof mapping === "object" && !Array.isArray(mapping)
+ );
+}
+
+/**
+ * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file 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<string>} conditionNames condition names
+ * @param {(d: string, f: boolean) => void} assert asserting direct value
+ * @returns {string[]} mapping result
+ */
+function directMapping(
+ remainingRequest,
+ isPattern,
+ isSubpathMapping,
+ mappingTarget,
+ conditionNames,
+ assert
+) {
+ if (mappingTarget === null) return [];
+
+ if (typeof mappingTarget === "string") {
+ return [
+ targetMapping(
+ remainingRequest,
+ isPattern,
+ isSubpathMapping,
+ mappingTarget,
+ assert
+ )
+ ];
+ }
+
+ /** @type {string[]} */
+ const targets = [];
+
+ for (const exp of mappingTarget) {
+ if (typeof exp === "string") {
+ targets.push(
+ targetMapping(
+ remainingRequest,
+ isPattern,
+ isSubpathMapping,
+ exp,
+ assert
+ )
+ );
+ continue;
+ }
+
+ const mapping = conditionalMapping(exp, conditionNames);
+ if (!mapping) continue;
+ const innerExports = directMapping(
+ remainingRequest,
+ isPattern,
+ isSubpathMapping,
+ mapping,
+ conditionNames,
+ assert
+ );
+ for (const innerExport of innerExports) {
+ targets.push(innerExport);
+ }
+ }
+
+ return targets;
+}
+
+/**
+ * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file 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,
+ isPattern,
+ isSubpathMapping,
+ mappingTarget,
+ assert
+) {
+ if (remainingRequest === undefined) {
+ assert(mappingTarget, false);
+
+ return mappingTarget;
+ }
+
+ if (isSubpathMapping) {
+ assert(mappingTarget, true);
+
+ return mappingTarget + remainingRequest;
+ }
+
+ assert(mappingTarget, false);
+
+ let result = mappingTarget;
+
+ if (isPattern) {
+ result = result.replace(
+ patternRegEx,
+ remainingRequest.replace(/\$/g, "$$")
+ );
+ }
+
+ return result;
+}
+
+/**
+ * @param {ConditionalMapping} conditionalMapping_ conditional mapping
+ * @param {Set<string>} conditionNames condition names
+ * @returns {DirectMapping|null} direct mapping if found
+ */
+function conditionalMapping(conditionalMapping_, conditionNames) {
+ /** @type {[ConditionalMapping, string[], number][]} */
+ let lookup = [[conditionalMapping_, Object.keys(conditionalMapping_), 0]];
+
+ loop: while (lookup.length > 0) {
+ const [mapping, conditions, j] = lookup[lookup.length - 1];
+ const last = conditions.length - 1;
+
+ for (let i = j; i < conditions.length; i++) {
+ const condition = conditions[i];
+
+ // assert default. Could be last only
+ if (i !== last) {
+ if (condition === "default") {
+ throw new Error("Default condition should be last one");
+ }
+ } else if (condition === "default") {
+ const innerMapping = mapping[condition];
+ // is nested
+ if (isConditionalMapping(innerMapping)) {
+ const conditionalMapping = /** @type {ConditionalMapping} */ (
+ innerMapping
+ );
+ lookup[lookup.length - 1][2] = i + 1;
+ lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
+ continue loop;
+ }
+
+ return /** @type {DirectMapping} */ (innerMapping);
+ }
+
+ if (conditionNames.has(condition)) {
+ const innerMapping = mapping[condition];
+ // is nested
+ if (isConditionalMapping(innerMapping)) {
+ const conditionalMapping = /** @type {ConditionalMapping} */ (
+ innerMapping
+ );
+ lookup[lookup.length - 1][2] = i + 1;
+ lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
+ continue loop;
+ }
+
+ return /** @type {DirectMapping} */ (innerMapping);
+ }
+ }
+
+ lookup.pop();
+ }
+
+ return null;
+}
+
+/**
+ * @param {ExportsField} field exports field
+ * @returns {ExportsField} normalized exports field
+ */
+function buildExportsField(field) {
+ // handle syntax sugar, if exports field is direct mapping for "."
+ if (typeof field === "string" || Array.isArray(field)) {
+ return { ".": field };
+ }
+
+ const keys = Object.keys(field);
+
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+
+ if (key.charCodeAt(0) !== dotCode) {
+ // handle syntax sugar, if exports field is conditional mapping for "."
+ if (i === 0) {
+ while (i < keys.length) {
+ const charCode = keys[i].charCodeAt(0);
+ if (charCode === dotCode || charCode === slashCode) {
+ throw new Error(
+ `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
+ key
+ )})`
+ );
+ }
+ i++;
+ }
+
+ return { ".": field };
+ }
+
+ throw new Error(
+ `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
+ key
+ )})`
+ );
+ }
+
+ if (key.length === 1) {
+ continue;
+ }
+
+ if (key.charCodeAt(1) !== slashCode) {
+ throw new Error(
+ `Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
+ key
+ )})`
+ );
+ }
+ }
+
+ return field;
+}
+
+/**
+ * @param {ImportsField} field imports field
+ * @returns {ImportsField} normalized imports field
+ */
+function buildImportsField(field) {
+ const keys = Object.keys(field);
+
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+
+ if (key.charCodeAt(0) !== hashCode) {
+ throw new Error(
+ `Imports field key should start with "#" (key: ${JSON.stringify(key)})`
+ );
+ }
+
+ if (key.length === 1) {
+ throw new Error(
+ `Imports field key should have at least 2 characters (key: ${JSON.stringify(
+ key
+ )})`
+ );
+ }
+
+ if (key.charCodeAt(1) === slashCode) {
+ throw new Error(
+ `Imports field key should not start with "#/" (key: ${JSON.stringify(
+ key
+ )})`
+ );
+ }
+ }
+
+ return field;
+}