/* 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>=} 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; }; /** * @template T * @typedef {function(string, object=): T} MakeCacheableResult */ /** * @template T * @typedef {function(string): T} BindCacheResultFn */ /** * @template T * @typedef {function(object): BindCacheResultFn} BindCache */ /** * @template T * @param {(function(string): T)} realFn real function * @returns {MakeCacheableResult & { bindCache: BindCache }} cacheable function */ const makeCacheable = realFn => { /** * @template T * @typedef {Map} CacheItem */ /** @type {WeakMap>} */ const cache = new WeakMap(); /** * @param {object} associatedObjectForCache an object to which the cache will be attached * @returns {CacheItem} cache item */ const getCache = associatedObjectForCache => { const entry = cache.get(associatedObjectForCache); if (entry !== undefined) return entry; /** @type {Map} */ const map = new Map(); cache.set(associatedObjectForCache, map); return map; }; /** @type {MakeCacheableResult & { bindCache: BindCache }} */ 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; }; /** @type {BindCache} */ fn.bindCache = associatedObjectForCache => { const cache = getCache(associatedObjectForCache); /** * @param {string} str string * @returns {T} value */ return str => { const entry = cache.get(str); if (entry !== undefined) return entry; const result = realFn(str); cache.set(str, result); return result; }; }; return fn; }; /** @typedef {function(string, string, object=): string} MakeCacheableWithContextResult */ /** @typedef {function(string, string): string} BindCacheForContextResultFn */ /** @typedef {function(string): string} BindContextCacheForContextResultFn */ /** @typedef {function(object=): BindCacheForContextResultFn} BindCacheForContext */ /** @typedef {function(string, object=): BindContextCacheForContextResultFn} BindContextCacheForContext */ /** * @param {function(string, string): string} fn function * @returns {MakeCacheableWithContextResult & { bindCache: BindCacheForContext, bindContextCache: BindContextCacheForContext }} cacheable function with context */ const makeCacheableWithContext = fn => { /** @type {WeakMap>>} */ const cache = new WeakMap(); /** @type {MakeCacheableWithContextResult & { bindCache: BindCacheForContext, bindContextCache: BindContextCacheForContext }} */ 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; } const result = fn(context, identifier); innerSubCache.set(identifier, result); return result; }; /** @type {BindCacheForContext} */ 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; } const result = fn(context, identifier); innerSubCache.set(identifier, result); return result; }; return boundFn; }; /** @type {BindContextCacheForContext} */ 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; } 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) => identifier .split(SEGMENTS_SPLIT_REGEXP) .map(str => absoluteToRequest(context, str)) .join(""); module.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) => identifier .split(SEGMENTS_SPLIT_REGEXP) .map(str => requestToAbsolute(context, str)) .join(""); module.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) => request .split("!") .map(r => absoluteToRequest(context, r)) .join("!"); const contextify = makeCacheableWithContext(_contextify); module.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) => request .split("!") .map(r => requestToAbsolute(context, r)) .join("!"); const absolutify = makeCacheableWithContext(_absolutify); module.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 = /** @type {[string, string, string | undefined, string | undefined]} */ (/** @type {unknown} */ (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] || "" }; }; module.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 = /** @type {[string, string, string | undefined]} */ (/** @type {unknown} */ (PATH_QUERY_REGEXP.exec(str))); return { resource: str, path: match[1].replace(/\0(.)/g, "$1"), query: match[2] ? match[2].replace(/\0(.)/g, "$1") : "" }; }; module.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 */ module.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; };