You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			605 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
		
		
			
		
	
	
			605 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
| 
											9 months ago
										 | /* | ||
|  | 	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; | ||
|  | } |