Permalink
Cannot retrieve contributors at this time
667 lines (559 sloc)
16.3 KB
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
codeql-action/node_modules/enhanced-resolve/lib/util/entrypoints.js
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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 */ | |
/** | |
* @typedef {Object} PathTreeNode | |
* @property {Map<string, PathTreeNode>|null} children | |
* @property {MappingValue} folder | |
* @property {Map<string, MappingValue>|null} wildcards | |
* @property {Map<string, MappingValue>} files | |
*/ | |
/** | |
* 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); | |
/** | |
* @param {ExportsField} exportsField the exports field | |
* @returns {FieldProcessor} process callback | |
*/ | |
module.exports.processExportsField = function processExportsField( | |
exportsField | |
) { | |
return createFieldProcessor( | |
buildExportsFieldPathTree(exportsField), | |
assertExportsFieldRequest, | |
assertExportTarget | |
); | |
}; | |
/** | |
* @param {ImportsField} importsField the exports field | |
* @returns {FieldProcessor} process callback | |
*/ | |
module.exports.processImportsField = function processImportsField( | |
importsField | |
) { | |
return createFieldProcessor( | |
buildImportsFieldPathTree(importsField), | |
assertImportsFieldRequest, | |
assertImportTarget | |
); | |
}; | |
/** | |
* @param {PathTreeNode} treeRoot root | |
* @param {(s: string) => string} assertRequest assertRequest | |
* @param {(s: string, f: boolean) => void} assertTarget assertTarget | |
* @returns {FieldProcessor} field processor | |
*/ | |
function createFieldProcessor(treeRoot, assertRequest, assertTarget) { | |
return function fieldProcessor(request, conditionNames) { | |
request = assertRequest(request); | |
const match = findMatch(request, treeRoot); | |
if (match === null) return []; | |
const [mapping, remainRequestIndex] = 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); | |
} | |
const remainingRequest = | |
remainRequestIndex === request.length + 1 | |
? undefined | |
: remainRequestIndex < 0 | |
? request.slice(-remainRequestIndex - 1) | |
: request.slice(remainRequestIndex); | |
return directMapping( | |
remainingRequest, | |
remainRequestIndex < 0, | |
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 "/"` | |
); | |
} | |
} | |
/** | |
* 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 | |
*/ | |
function findMatch(request, treeRoot) { | |
if (request.length === 0) { | |
const value = treeRoot.files.get(""); | |
return value ? [value, 1] : null; | |
} | |
if ( | |
treeRoot.children === null && | |
treeRoot.folder === null && | |
treeRoot.wildcards === null | |
) { | |
const value = treeRoot.files.get(request); | |
return value ? [value, request.length + 1] : null; | |
} | |
let node = treeRoot; | |
let lastNonSlashIndex = 0; | |
let slashIndex = request.indexOf("/", 0); | |
/** @type {[MappingValue, number]|null} */ | |
let lastFolderMatch = null; | |
const applyFolderMapping = () => { | |
const folderMapping = node.folder; | |
if (folderMapping) { | |
if (lastFolderMatch) { | |
lastFolderMatch[0] = folderMapping; | |
lastFolderMatch[1] = -lastNonSlashIndex - 1; | |
} else { | |
lastFolderMatch = [folderMapping, -lastNonSlashIndex - 1]; | |
} | |
} | |
}; | |
const applyWildcardMappings = (wildcardMappings, remainingRequest) => { | |
if (wildcardMappings) { | |
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[0] = target; | |
lastFolderMatch[1] = lastNonSlashIndex + 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; | |
const value = node.files.get(remainingRequest); | |
if (value) { | |
return [value, request.length + 1]; | |
} | |
applyFolderMapping(); | |
applyWildcardMappings(node.wildcards, remainingRequest); | |
return lastFolderMatch; | |
} | |
/** | |
* @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} subpathMapping 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, | |
subpathMapping, | |
mappingTarget, | |
conditionNames, | |
assert | |
) { | |
if (mappingTarget === null) return []; | |
if (typeof mappingTarget === "string") { | |
return [ | |
targetMapping(remainingRequest, subpathMapping, mappingTarget, assert) | |
]; | |
} | |
const targets = []; | |
for (const exp of mappingTarget) { | |
if (typeof exp === "string") { | |
targets.push( | |
targetMapping(remainingRequest, subpathMapping, exp, assert) | |
); | |
continue; | |
} | |
const mapping = conditionalMapping(exp, conditionNames); | |
if (!mapping) continue; | |
const innerExports = directMapping( | |
remainingRequest, | |
subpathMapping, | |
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} subpathMapping 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, | |
mappingTarget, | |
assert | |
) { | |
if (remainingRequest === undefined) { | |
assert(mappingTarget, false); | |
return mappingTarget; | |
} | |
if (subpathMapping) { | |
assert(mappingTarget, true); | |
return mappingTarget + remainingRequest; | |
} | |
assert(mappingTarget, false); | |
return mappingTarget.replace(/\*/g, remainingRequest.replace(/\$/g, "$$")); | |
} | |
/** | |
* @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; | |
} | |
/** | |
* 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 looks 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 (node.children === null) { | |
newNode = createNode(); | |
node.children = new Map(); | |
node.children.set(folder, newNode); | |
} else { | |
newNode = node.children.get(folder); | |
if (!newNode) { | |
newNode = createNode(); | |
node.children.set(folder, newNode); | |
} | |
} | |
node = newNode; | |
lastNonSlashIndex = slashIndex + 1; | |
slashIndex = path.indexOf("/", lastNonSlashIndex); | |
} | |
if (lastNonSlashIndex >= path.length) { | |
node.folder = target; | |
} else { | |
const file = lastNonSlashIndex > 0 ? path.slice(lastNonSlashIndex) : path; | |
if (file.endsWith("*")) { | |
if (node.wildcards === null) node.wildcards = new Map(); | |
node.wildcards.set(file.slice(0, -1), target); | |
} else { | |
node.files.set(file, target); | |
} | |
} | |
} | |
/** | |
* @param {ExportsField} field exports field | |
* @returns {PathTreeNode} tree root | |
*/ | |
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; | |
} | |
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++; | |
} | |
root.files.set("", field); | |
return root; | |
} | |
throw new Error( | |
`Exports field key should be relative path and start with "." (key: ${JSON.stringify( | |
key | |
)})` | |
); | |
} | |
if (key.length === 1) { | |
root.files.set("", field[key]); | |
continue; | |
} | |
if (key.charCodeAt(1) !== slashCode) { | |
throw new Error( | |
`Exports field key should be relative path and start with "./" (key: ${JSON.stringify( | |
key | |
)})` | |
); | |
} | |
walkPath(root, key.slice(2), field[key]); | |
} | |
return root; | |
} | |
/** | |
* @param {ImportsField} field imports field | |
* @returns {PathTreeNode} root | |
*/ | |
function buildImportsFieldPathTree(field) { | |
const root = createNode(); | |
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 | |
)})` | |
); | |
} | |
walkPath(root, key.slice(1), field[key]); | |
} | |
return root; | |
} |