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.
		
		
		
		
		
			
		
			
	
	
		
			382 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
		
		
			
		
	
	
			382 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
| 
											9 months ago
										 | /* | ||
|  | 	MIT License http://www.opensource.org/licenses/mit-license.php
 | ||
|  | */ | ||
|  | 
 | ||
|  | "use strict"; | ||
|  | 
 | ||
|  | const path = require("path"); | ||
|  | 
 | ||
|  | const WINDOWS_ABS_PATH_REGEXP = /^[a-zA-Z]:[\\/]/; | ||
|  | const SEGMENTS_SPLIT_REGEXP = /([|!])/; | ||
|  | const WINDOWS_PATH_SEPARATOR_REGEXP = /\\/g; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @typedef {Object} MakeRelativePathsCache | ||
|  |  * @property {Map<string, Map<string, string>>=} relativePaths | ||
|  |  */ | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param {string} relativePath relative path | ||
|  |  * @returns {string} request | ||
|  |  */ | ||
|  | const relativePathToRequest = relativePath => { | ||
|  | 	if (relativePath === "") return "./."; | ||
|  | 	if (relativePath === "..") return "../."; | ||
|  | 	if (relativePath.startsWith("../")) return relativePath; | ||
|  | 	return `./${relativePath}`; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param {string} context context for relative path | ||
|  |  * @param {string} maybeAbsolutePath path to make relative | ||
|  |  * @returns {string} relative path in request style | ||
|  |  */ | ||
|  | const absoluteToRequest = (context, maybeAbsolutePath) => { | ||
|  | 	if (maybeAbsolutePath[0] === "/") { | ||
|  | 		if ( | ||
|  | 			maybeAbsolutePath.length > 1 && | ||
|  | 			maybeAbsolutePath[maybeAbsolutePath.length - 1] === "/" | ||
|  | 		) { | ||
|  | 			// this 'path' is actually a regexp generated by dynamic requires.
 | ||
|  | 			// Don't treat it as an absolute path.
 | ||
|  | 			return maybeAbsolutePath; | ||
|  | 		} | ||
|  | 
 | ||
|  | 		const querySplitPos = maybeAbsolutePath.indexOf("?"); | ||
|  | 		let resource = | ||
|  | 			querySplitPos === -1 | ||
|  | 				? maybeAbsolutePath | ||
|  | 				: maybeAbsolutePath.slice(0, querySplitPos); | ||
|  | 		resource = relativePathToRequest(path.posix.relative(context, resource)); | ||
|  | 		return querySplitPos === -1 | ||
|  | 			? resource | ||
|  | 			: resource + maybeAbsolutePath.slice(querySplitPos); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	if (WINDOWS_ABS_PATH_REGEXP.test(maybeAbsolutePath)) { | ||
|  | 		const querySplitPos = maybeAbsolutePath.indexOf("?"); | ||
|  | 		let resource = | ||
|  | 			querySplitPos === -1 | ||
|  | 				? maybeAbsolutePath | ||
|  | 				: maybeAbsolutePath.slice(0, querySplitPos); | ||
|  | 		resource = path.win32.relative(context, resource); | ||
|  | 		if (!WINDOWS_ABS_PATH_REGEXP.test(resource)) { | ||
|  | 			resource = relativePathToRequest( | ||
|  | 				resource.replace(WINDOWS_PATH_SEPARATOR_REGEXP, "/") | ||
|  | 			); | ||
|  | 		} | ||
|  | 		return querySplitPos === -1 | ||
|  | 			? resource | ||
|  | 			: resource + maybeAbsolutePath.slice(querySplitPos); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	// not an absolute path
 | ||
|  | 	return maybeAbsolutePath; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param {string} context context for relative path | ||
|  |  * @param {string} relativePath path | ||
|  |  * @returns {string} absolute path | ||
|  |  */ | ||
|  | const requestToAbsolute = (context, relativePath) => { | ||
|  | 	if (relativePath.startsWith("./") || relativePath.startsWith("../")) | ||
|  | 		return path.join(context, relativePath); | ||
|  | 	return relativePath; | ||
|  | }; | ||
|  | 
 | ||
|  | const makeCacheable = realFn => { | ||
|  | 	/** @type {WeakMap<object, Map<string, ParsedResource>>} */ | ||
|  | 	const cache = new WeakMap(); | ||
|  | 
 | ||
|  | 	const getCache = associatedObjectForCache => { | ||
|  | 		const entry = cache.get(associatedObjectForCache); | ||
|  | 		if (entry !== undefined) return entry; | ||
|  | 		/** @type {Map<string, ParsedResource>} */ | ||
|  | 		const map = new Map(); | ||
|  | 		cache.set(associatedObjectForCache, map); | ||
|  | 		return map; | ||
|  | 	}; | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {string} str the path with query and fragment | ||
|  | 	 * @param {Object=} associatedObjectForCache an object to which the cache will be attached | ||
|  | 	 * @returns {ParsedResource} parsed parts | ||
|  | 	 */ | ||
|  | 	const fn = (str, associatedObjectForCache) => { | ||
|  | 		if (!associatedObjectForCache) return realFn(str); | ||
|  | 		const cache = getCache(associatedObjectForCache); | ||
|  | 		const entry = cache.get(str); | ||
|  | 		if (entry !== undefined) return entry; | ||
|  | 		const result = realFn(str); | ||
|  | 		cache.set(str, result); | ||
|  | 		return result; | ||
|  | 	}; | ||
|  | 
 | ||
|  | 	fn.bindCache = associatedObjectForCache => { | ||
|  | 		const cache = getCache(associatedObjectForCache); | ||
|  | 		return str => { | ||
|  | 			const entry = cache.get(str); | ||
|  | 			if (entry !== undefined) return entry; | ||
|  | 			const result = realFn(str); | ||
|  | 			cache.set(str, result); | ||
|  | 			return result; | ||
|  | 		}; | ||
|  | 	}; | ||
|  | 
 | ||
|  | 	return fn; | ||
|  | }; | ||
|  | 
 | ||
|  | const makeCacheableWithContext = fn => { | ||
|  | 	/** @type {WeakMap<object, Map<string, Map<string, string>>>} */ | ||
|  | 	const cache = new WeakMap(); | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {string} context context used to create relative path | ||
|  | 	 * @param {string} identifier identifier used to create relative path | ||
|  | 	 * @param {Object=} associatedObjectForCache an object to which the cache will be attached | ||
|  | 	 * @returns {string} the returned relative path | ||
|  | 	 */ | ||
|  | 	const cachedFn = (context, identifier, associatedObjectForCache) => { | ||
|  | 		if (!associatedObjectForCache) return fn(context, identifier); | ||
|  | 
 | ||
|  | 		let innerCache = cache.get(associatedObjectForCache); | ||
|  | 		if (innerCache === undefined) { | ||
|  | 			innerCache = new Map(); | ||
|  | 			cache.set(associatedObjectForCache, innerCache); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		let cachedResult; | ||
|  | 		let innerSubCache = innerCache.get(context); | ||
|  | 		if (innerSubCache === undefined) { | ||
|  | 			innerCache.set(context, (innerSubCache = new Map())); | ||
|  | 		} else { | ||
|  | 			cachedResult = innerSubCache.get(identifier); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if (cachedResult !== undefined) { | ||
|  | 			return cachedResult; | ||
|  | 		} else { | ||
|  | 			const result = fn(context, identifier); | ||
|  | 			innerSubCache.set(identifier, result); | ||
|  | 			return result; | ||
|  | 		} | ||
|  | 	}; | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {Object=} associatedObjectForCache an object to which the cache will be attached | ||
|  | 	 * @returns {function(string, string): string} cached function | ||
|  | 	 */ | ||
|  | 	cachedFn.bindCache = associatedObjectForCache => { | ||
|  | 		let innerCache; | ||
|  | 		if (associatedObjectForCache) { | ||
|  | 			innerCache = cache.get(associatedObjectForCache); | ||
|  | 			if (innerCache === undefined) { | ||
|  | 				innerCache = new Map(); | ||
|  | 				cache.set(associatedObjectForCache, innerCache); | ||
|  | 			} | ||
|  | 		} else { | ||
|  | 			innerCache = new Map(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/** | ||
|  | 		 * @param {string} context context used to create relative path | ||
|  | 		 * @param {string} identifier identifier used to create relative path | ||
|  | 		 * @returns {string} the returned relative path | ||
|  | 		 */ | ||
|  | 		const boundFn = (context, identifier) => { | ||
|  | 			let cachedResult; | ||
|  | 			let innerSubCache = innerCache.get(context); | ||
|  | 			if (innerSubCache === undefined) { | ||
|  | 				innerCache.set(context, (innerSubCache = new Map())); | ||
|  | 			} else { | ||
|  | 				cachedResult = innerSubCache.get(identifier); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if (cachedResult !== undefined) { | ||
|  | 				return cachedResult; | ||
|  | 			} else { | ||
|  | 				const result = fn(context, identifier); | ||
|  | 				innerSubCache.set(identifier, result); | ||
|  | 				return result; | ||
|  | 			} | ||
|  | 		}; | ||
|  | 
 | ||
|  | 		return boundFn; | ||
|  | 	}; | ||
|  | 
 | ||
|  | 	/** | ||
|  | 	 * @param {string} context context used to create relative path | ||
|  | 	 * @param {Object=} associatedObjectForCache an object to which the cache will be attached | ||
|  | 	 * @returns {function(string): string} cached function | ||
|  | 	 */ | ||
|  | 	cachedFn.bindContextCache = (context, associatedObjectForCache) => { | ||
|  | 		let innerSubCache; | ||
|  | 		if (associatedObjectForCache) { | ||
|  | 			let innerCache = cache.get(associatedObjectForCache); | ||
|  | 			if (innerCache === undefined) { | ||
|  | 				innerCache = new Map(); | ||
|  | 				cache.set(associatedObjectForCache, innerCache); | ||
|  | 			} | ||
|  | 
 | ||
|  | 			innerSubCache = innerCache.get(context); | ||
|  | 			if (innerSubCache === undefined) { | ||
|  | 				innerCache.set(context, (innerSubCache = new Map())); | ||
|  | 			} | ||
|  | 		} else { | ||
|  | 			innerSubCache = new Map(); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/** | ||
|  | 		 * @param {string} identifier identifier used to create relative path | ||
|  | 		 * @returns {string} the returned relative path | ||
|  | 		 */ | ||
|  | 		const boundFn = identifier => { | ||
|  | 			const cachedResult = innerSubCache.get(identifier); | ||
|  | 			if (cachedResult !== undefined) { | ||
|  | 				return cachedResult; | ||
|  | 			} else { | ||
|  | 				const result = fn(context, identifier); | ||
|  | 				innerSubCache.set(identifier, result); | ||
|  | 				return result; | ||
|  | 			} | ||
|  | 		}; | ||
|  | 
 | ||
|  | 		return boundFn; | ||
|  | 	}; | ||
|  | 
 | ||
|  | 	return cachedFn; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * | ||
|  |  * @param {string} context context for relative path | ||
|  |  * @param {string} identifier identifier for path | ||
|  |  * @returns {string} a converted relative path | ||
|  |  */ | ||
|  | const _makePathsRelative = (context, identifier) => { | ||
|  | 	return identifier | ||
|  | 		.split(SEGMENTS_SPLIT_REGEXP) | ||
|  | 		.map(str => absoluteToRequest(context, str)) | ||
|  | 		.join(""); | ||
|  | }; | ||
|  | 
 | ||
|  | exports.makePathsRelative = makeCacheableWithContext(_makePathsRelative); | ||
|  | 
 | ||
|  | /** | ||
|  |  * | ||
|  |  * @param {string} context context for relative path | ||
|  |  * @param {string} identifier identifier for path | ||
|  |  * @returns {string} a converted relative path | ||
|  |  */ | ||
|  | const _makePathsAbsolute = (context, identifier) => { | ||
|  | 	return identifier | ||
|  | 		.split(SEGMENTS_SPLIT_REGEXP) | ||
|  | 		.map(str => requestToAbsolute(context, str)) | ||
|  | 		.join(""); | ||
|  | }; | ||
|  | 
 | ||
|  | exports.makePathsAbsolute = makeCacheableWithContext(_makePathsAbsolute); | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param {string} context absolute context path | ||
|  |  * @param {string} request any request string may containing absolute paths, query string, etc. | ||
|  |  * @returns {string} a new request string avoiding absolute paths when possible | ||
|  |  */ | ||
|  | const _contextify = (context, request) => { | ||
|  | 	return request | ||
|  | 		.split("!") | ||
|  | 		.map(r => absoluteToRequest(context, r)) | ||
|  | 		.join("!"); | ||
|  | }; | ||
|  | 
 | ||
|  | const contextify = makeCacheableWithContext(_contextify); | ||
|  | exports.contextify = contextify; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param {string} context absolute context path | ||
|  |  * @param {string} request any request string | ||
|  |  * @returns {string} a new request string using absolute paths when possible | ||
|  |  */ | ||
|  | const _absolutify = (context, request) => { | ||
|  | 	return request | ||
|  | 		.split("!") | ||
|  | 		.map(r => requestToAbsolute(context, r)) | ||
|  | 		.join("!"); | ||
|  | }; | ||
|  | 
 | ||
|  | const absolutify = makeCacheableWithContext(_absolutify); | ||
|  | exports.absolutify = absolutify; | ||
|  | 
 | ||
|  | const PATH_QUERY_FRAGMENT_REGEXP = | ||
|  | 	/^((?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/; | ||
|  | const PATH_QUERY_REGEXP = /^((?:\0.|[^?\0])*)(\?.*)?$/; | ||
|  | 
 | ||
|  | /** @typedef {{ resource: string, path: string, query: string, fragment: string }} ParsedResource */ | ||
|  | /** @typedef {{ resource: string, path: string, query: string }} ParsedResourceWithoutFragment */ | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param {string} str the path with query and fragment | ||
|  |  * @returns {ParsedResource} parsed parts | ||
|  |  */ | ||
|  | const _parseResource = str => { | ||
|  | 	const match = PATH_QUERY_FRAGMENT_REGEXP.exec(str); | ||
|  | 	return { | ||
|  | 		resource: str, | ||
|  | 		path: match[1].replace(/\0(.)/g, "$1"), | ||
|  | 		query: match[2] ? match[2].replace(/\0(.)/g, "$1") : "", | ||
|  | 		fragment: match[3] || "" | ||
|  | 	}; | ||
|  | }; | ||
|  | exports.parseResource = makeCacheable(_parseResource); | ||
|  | 
 | ||
|  | /** | ||
|  |  * Parse resource, skips fragment part | ||
|  |  * @param {string} str the path with query and fragment | ||
|  |  * @returns {ParsedResourceWithoutFragment} parsed parts | ||
|  |  */ | ||
|  | const _parseResourceWithoutFragment = str => { | ||
|  | 	const match = PATH_QUERY_REGEXP.exec(str); | ||
|  | 	return { | ||
|  | 		resource: str, | ||
|  | 		path: match[1].replace(/\0(.)/g, "$1"), | ||
|  | 		query: match[2] ? match[2].replace(/\0(.)/g, "$1") : "" | ||
|  | 	}; | ||
|  | }; | ||
|  | exports.parseResourceWithoutFragment = makeCacheable( | ||
|  | 	_parseResourceWithoutFragment | ||
|  | ); | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param {string} filename the filename which should be undone | ||
|  |  * @param {string} outputPath the output path that is restored (only relevant when filename contains "..") | ||
|  |  * @param {boolean} enforceRelative true returns ./ for empty paths | ||
|  |  * @returns {string} repeated ../ to leave the directory of the provided filename to be back on output dir | ||
|  |  */ | ||
|  | exports.getUndoPath = (filename, outputPath, enforceRelative) => { | ||
|  | 	let depth = -1; | ||
|  | 	let append = ""; | ||
|  | 	outputPath = outputPath.replace(/[\\/]$/, ""); | ||
|  | 	for (const part of filename.split(/[/\\]+/)) { | ||
|  | 		if (part === "..") { | ||
|  | 			if (depth > -1) { | ||
|  | 				depth--; | ||
|  | 			} else { | ||
|  | 				const i = outputPath.lastIndexOf("/"); | ||
|  | 				const j = outputPath.lastIndexOf("\\"); | ||
|  | 				const pos = i < 0 ? j : j < 0 ? i : Math.max(i, j); | ||
|  | 				if (pos < 0) return outputPath + "/"; | ||
|  | 				append = outputPath.slice(pos + 1) + "/" + append; | ||
|  | 				outputPath = outputPath.slice(0, pos); | ||
|  | 			} | ||
|  | 		} else if (part !== ".") { | ||
|  | 			depth++; | ||
|  | 		} | ||
|  | 	} | ||
|  | 	return depth > 0 | ||
|  | 		? `${"../".repeat(depth)}${append}` | ||
|  | 		: enforceRelative | ||
|  | 		? `./${append}` | ||
|  | 		: append; | ||
|  | }; |