|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"use strict"; |
|
|
|
|
|
const { parseIdentifier } = require("./identifier"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const slashCode = "/".charCodeAt(0); |
|
|
const dotCode = ".".charCodeAt(0); |
|
|
const hashCode = "#".charCodeAt(0); |
|
|
const patternRegEx = /\*/g; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 findMatch(request, field) { |
|
|
if ( |
|
|
Object.prototype.hasOwnProperty.call(field, request) && |
|
|
!request.includes("*") && |
|
|
!request.endsWith("/") |
|
|
) { |
|
|
const target = (field)[request]; |
|
|
|
|
|
return [target, "", false, false, request]; |
|
|
} |
|
|
|
|
|
|
|
|
let bestMatch = ""; |
|
|
|
|
|
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, |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
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 = (field)[bestMatch]; |
|
|
const isSubpathMapping = bestMatch.endsWith("/"); |
|
|
const isPattern = bestMatch.includes("*"); |
|
|
|
|
|
return [ |
|
|
target, |
|
|
(bestMatchSubpath), |
|
|
isSubpathMapping, |
|
|
isPattern, |
|
|
bestMatch, |
|
|
]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isConditionalMapping(mapping) { |
|
|
return ( |
|
|
mapping !== null && typeof mapping === "object" && !Array.isArray(mapping) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function conditionalMapping(conditionalMapping_, conditionNames) { |
|
|
|
|
|
const lookup = [[conditionalMapping_, Object.keys(conditionalMapping_), 0]]; |
|
|
|
|
|
loop: while (lookup.length > 0) { |
|
|
const [mapping, conditions, j] = lookup[lookup.length - 1]; |
|
|
|
|
|
for (let i = j; i < conditions.length; i++) { |
|
|
const condition = conditions[i]; |
|
|
|
|
|
if (condition === "default") { |
|
|
const innerMapping = mapping[condition]; |
|
|
|
|
|
if (isConditionalMapping(innerMapping)) { |
|
|
const conditionalMapping = ( |
|
|
innerMapping |
|
|
); |
|
|
lookup[lookup.length - 1][2] = i + 1; |
|
|
lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]); |
|
|
continue loop; |
|
|
} |
|
|
|
|
|
return (innerMapping); |
|
|
} |
|
|
|
|
|
if (conditionNames.has(condition)) { |
|
|
const innerMapping = mapping[condition]; |
|
|
|
|
|
if (isConditionalMapping(innerMapping)) { |
|
|
const conditionalMapping = ( |
|
|
innerMapping |
|
|
); |
|
|
lookup[lookup.length - 1][2] = i + 1; |
|
|
lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]); |
|
|
continue loop; |
|
|
} |
|
|
|
|
|
return (innerMapping); |
|
|
} |
|
|
} |
|
|
|
|
|
lookup.pop(); |
|
|
} |
|
|
|
|
|
return null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function directMapping( |
|
|
remainingRequest, |
|
|
isPattern, |
|
|
isSubpathMapping, |
|
|
mappingTarget, |
|
|
conditionNames, |
|
|
assert, |
|
|
) { |
|
|
if (mappingTarget === null) return []; |
|
|
|
|
|
if (typeof mappingTarget === "string") { |
|
|
return [ |
|
|
targetMapping( |
|
|
remainingRequest, |
|
|
isPattern, |
|
|
isSubpathMapping, |
|
|
mappingTarget, |
|
|
assert, |
|
|
), |
|
|
]; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createFieldProcessor( |
|
|
field, |
|
|
normalizeRequest, |
|
|
assertRequest, |
|
|
assertTarget, |
|
|
) { |
|
|
return function fieldProcessor(request, conditionNames) { |
|
|
request = assertRequest(request); |
|
|
|
|
|
const match = findMatch(normalizeRequest(request), field); |
|
|
|
|
|
if (match === null) return [[], null]; |
|
|
|
|
|
const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] = |
|
|
match; |
|
|
|
|
|
|
|
|
let direct = null; |
|
|
|
|
|
if (isConditionalMapping(mapping)) { |
|
|
direct = conditionalMapping( |
|
|
(mapping), |
|
|
conditionNames, |
|
|
); |
|
|
|
|
|
|
|
|
if (direct === null) return [[], null]; |
|
|
} else { |
|
|
direct = (mapping); |
|
|
} |
|
|
|
|
|
return [ |
|
|
directMapping( |
|
|
remainingRequest, |
|
|
isPattern, |
|
|
isSubpathMapping, |
|
|
direct, |
|
|
conditionNames, |
|
|
assertTarget, |
|
|
), |
|
|
usedField, |
|
|
]; |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildExportsField(field) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function assertExportTarget(exp, expectFolder) { |
|
|
const parsedIdentifier = parseIdentifier(exp); |
|
|
|
|
|
if (!parsedIdentifier) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const [relativePath] = parsedIdentifier; |
|
|
const isFolder = |
|
|
relativePath.charCodeAt(relativePath.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 "/"`, |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
module.exports.processExportsField = function processExportsField( |
|
|
exportsField, |
|
|
) { |
|
|
return createFieldProcessor( |
|
|
buildExportsField(exportsField), |
|
|
(request) => (request.length === 0 ? "." : `./${request}`), |
|
|
assertExportsFieldRequest, |
|
|
assertExportTarget, |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function assertImportTarget(imp, expectFolder) { |
|
|
const parsedIdentifier = parseIdentifier(imp); |
|
|
|
|
|
if (!parsedIdentifier) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const [relativePath] = parsedIdentifier; |
|
|
const isFolder = |
|
|
relativePath.charCodeAt(relativePath.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 "/"`, |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
module.exports.processImportsField = function processImportsField( |
|
|
importsField, |
|
|
) { |
|
|
return createFieldProcessor( |
|
|
importsField, |
|
|
(request) => `#${request}`, |
|
|
assertImportsFieldRequest, |
|
|
assertImportTarget, |
|
|
); |
|
|
}; |
|
|
|