| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667 | /*	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 mappingand value called as a direct export.If value is key-value object, mapping is called as a conditional mappingand 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 mappingand value called as a direct export.If value is key-value object, mapping is called as a conditional mappingand 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;}
 |