2 MIT License http://www.opensource.org/licenses/mit-license.php
7 const path = require("path");
9 const WINDOWS_ABS_PATH_REGEXP = /^[a-zA-Z]:[\\/]/;
10 const SEGMENTS_SPLIT_REGEXP = /([|!])/;
11 const WINDOWS_PATH_SEPARATOR_REGEXP = /\\/g;
14 * @typedef {Object} MakeRelativePathsCache
15 * @property {Map<string, Map<string, string>>=} relativePaths
18 const relativePathToRequest = relativePath => {
19 if (relativePath === "") return "./.";
20 if (relativePath === "..") return "../.";
21 if (relativePath.startsWith("../")) return relativePath;
22 return `./${relativePath}`;
26 * @param {string} context context for relative path
27 * @param {string} maybeAbsolutePath path to make relative
28 * @returns {string} relative path in request style
30 const absoluteToRequest = (context, maybeAbsolutePath) => {
31 if (maybeAbsolutePath[0] === "/") {
33 maybeAbsolutePath.length > 1 &&
34 maybeAbsolutePath[maybeAbsolutePath.length - 1] === "/"
36 // this 'path' is actually a regexp generated by dynamic requires.
37 // Don't treat it as an absolute path.
38 return maybeAbsolutePath;
41 const querySplitPos = maybeAbsolutePath.indexOf("?");
45 : maybeAbsolutePath.slice(0, querySplitPos);
46 resource = relativePathToRequest(path.posix.relative(context, resource));
47 return querySplitPos === -1
49 : resource + maybeAbsolutePath.slice(querySplitPos);
52 if (WINDOWS_ABS_PATH_REGEXP.test(maybeAbsolutePath)) {
53 const querySplitPos = maybeAbsolutePath.indexOf("?");
57 : maybeAbsolutePath.slice(0, querySplitPos);
58 resource = path.win32.relative(context, resource);
59 if (!WINDOWS_ABS_PATH_REGEXP.test(resource)) {
60 resource = relativePathToRequest(
61 resource.replace(WINDOWS_PATH_SEPARATOR_REGEXP, "/")
64 return querySplitPos === -1
66 : resource + maybeAbsolutePath.slice(querySplitPos);
69 // not an absolute path
70 return maybeAbsolutePath;
74 * @param {string} context context for relative path
75 * @param {string} relativePath path
76 * @returns {string} absolute path
78 const requestToAbsolute = (context, relativePath) => {
79 if (relativePath.startsWith("./") || relativePath.startsWith("../"))
80 return path.join(context, relativePath);
84 const makeCacheable = realFn => {
85 /** @type {WeakMap<object, Map<string, ParsedResource>>} */
86 const cache = new WeakMap();
88 const getCache = associatedObjectForCache => {
89 const entry = cache.get(associatedObjectForCache);
90 if (entry !== undefined) return entry;
91 /** @type {Map<string, ParsedResource>} */
92 const map = new Map();
93 cache.set(associatedObjectForCache, map);
98 * @param {string} str the path with query and fragment
99 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
100 * @returns {ParsedResource} parsed parts
102 const fn = (str, associatedObjectForCache) => {
103 if (!associatedObjectForCache) return realFn(str);
104 const cache = getCache(associatedObjectForCache);
105 const entry = cache.get(str);
106 if (entry !== undefined) return entry;
107 const result = realFn(str);
108 cache.set(str, result);
112 fn.bindCache = associatedObjectForCache => {
113 const cache = getCache(associatedObjectForCache);
115 const entry = cache.get(str);
116 if (entry !== undefined) return entry;
117 const result = realFn(str);
118 cache.set(str, result);
126 const makeCacheableWithContext = fn => {
127 /** @type {WeakMap<object, Map<string, Map<string, string>>>} */
128 const cache = new WeakMap();
131 * @param {string} context context used to create relative path
132 * @param {string} identifier identifier used to create relative path
133 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
134 * @returns {string} the returned relative path
136 const cachedFn = (context, identifier, associatedObjectForCache) => {
137 if (!associatedObjectForCache) return fn(context, identifier);
139 let innerCache = cache.get(associatedObjectForCache);
140 if (innerCache === undefined) {
141 innerCache = new Map();
142 cache.set(associatedObjectForCache, innerCache);
146 let innerSubCache = innerCache.get(context);
147 if (innerSubCache === undefined) {
148 innerCache.set(context, (innerSubCache = new Map()));
150 cachedResult = innerSubCache.get(identifier);
153 if (cachedResult !== undefined) {
156 const result = fn(context, identifier);
157 innerSubCache.set(identifier, result);
163 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
164 * @returns {function(string, string): string} cached function
166 cachedFn.bindCache = associatedObjectForCache => {
168 if (associatedObjectForCache) {
169 innerCache = cache.get(associatedObjectForCache);
170 if (innerCache === undefined) {
171 innerCache = new Map();
172 cache.set(associatedObjectForCache, innerCache);
175 innerCache = new Map();
179 * @param {string} context context used to create relative path
180 * @param {string} identifier identifier used to create relative path
181 * @returns {string} the returned relative path
183 const boundFn = (context, identifier) => {
185 let innerSubCache = innerCache.get(context);
186 if (innerSubCache === undefined) {
187 innerCache.set(context, (innerSubCache = new Map()));
189 cachedResult = innerSubCache.get(identifier);
192 if (cachedResult !== undefined) {
195 const result = fn(context, identifier);
196 innerSubCache.set(identifier, result);
205 * @param {string} context context used to create relative path
206 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
207 * @returns {function(string): string} cached function
209 cachedFn.bindContextCache = (context, associatedObjectForCache) => {
211 if (associatedObjectForCache) {
212 let innerCache = cache.get(associatedObjectForCache);
213 if (innerCache === undefined) {
214 innerCache = new Map();
215 cache.set(associatedObjectForCache, innerCache);
218 innerSubCache = innerCache.get(context);
219 if (innerSubCache === undefined) {
220 innerCache.set(context, (innerSubCache = new Map()));
223 innerSubCache = new Map();
227 * @param {string} identifier identifier used to create relative path
228 * @returns {string} the returned relative path
230 const boundFn = identifier => {
231 const cachedResult = innerSubCache.get(identifier);
232 if (cachedResult !== undefined) {
235 const result = fn(context, identifier);
236 innerSubCache.set(identifier, result);
249 * @param {string} context context for relative path
250 * @param {string} identifier identifier for path
251 * @returns {string} a converted relative path
253 const _makePathsRelative = (context, identifier) => {
255 .split(SEGMENTS_SPLIT_REGEXP)
256 .map(str => absoluteToRequest(context, str))
260 exports.makePathsRelative = makeCacheableWithContext(_makePathsRelative);
264 * @param {string} context context for relative path
265 * @param {string} identifier identifier for path
266 * @returns {string} a converted relative path
268 const _makePathsAbsolute = (context, identifier) => {
270 .split(SEGMENTS_SPLIT_REGEXP)
271 .map(str => requestToAbsolute(context, str))
275 exports.makePathsAbsolute = makeCacheableWithContext(_makePathsAbsolute);
278 * @param {string} context absolute context path
279 * @param {string} request any request string may containing absolute paths, query string, etc.
280 * @returns {string} a new request string avoiding absolute paths when possible
282 const _contextify = (context, request) => {
285 .map(r => absoluteToRequest(context, r))
289 const contextify = makeCacheableWithContext(_contextify);
290 exports.contextify = contextify;
293 * @param {string} context absolute context path
294 * @param {string} request any request string
295 * @returns {string} a new request string using absolute paths when possible
297 const _absolutify = (context, request) => {
300 .map(r => requestToAbsolute(context, r))
304 const absolutify = makeCacheableWithContext(_absolutify);
305 exports.absolutify = absolutify;
307 const PATH_QUERY_FRAGMENT_REGEXP =
308 /^((?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/;
309 const PATH_QUERY_REGEXP = /^((?:\0.|[^?\0])*)(\?.*)?$/;
311 /** @typedef {{ resource: string, path: string, query: string, fragment: string }} ParsedResource */
312 /** @typedef {{ resource: string, path: string, query: string }} ParsedResourceWithoutFragment */
315 * @param {string} str the path with query and fragment
316 * @returns {ParsedResource} parsed parts
318 const _parseResource = str => {
319 const match = PATH_QUERY_FRAGMENT_REGEXP.exec(str);
322 path: match[1].replace(/\0(.)/g, "$1"),
323 query: match[2] ? match[2].replace(/\0(.)/g, "$1") : "",
324 fragment: match[3] || ""
327 exports.parseResource = makeCacheable(_parseResource);
330 * Parse resource, skips fragment part
331 * @param {string} str the path with query and fragment
332 * @returns {ParsedResourceWithoutFragment} parsed parts
334 const _parseResourceWithoutFragment = str => {
335 const match = PATH_QUERY_REGEXP.exec(str);
338 path: match[1].replace(/\0(.)/g, "$1"),
339 query: match[2] ? match[2].replace(/\0(.)/g, "$1") : ""
342 exports.parseResourceWithoutFragment = makeCacheable(
343 _parseResourceWithoutFragment
347 * @param {string} filename the filename which should be undone
348 * @param {string} outputPath the output path that is restored (only relevant when filename contains "..")
349 * @param {boolean} enforceRelative true returns ./ for empty paths
350 * @returns {string} repeated ../ to leave the directory of the provided filename to be back on output dir
352 exports.getUndoPath = (filename, outputPath, enforceRelative) => {
355 outputPath = outputPath.replace(/[\\/]$/, "");
356 for (const part of filename.split(/[/\\]+/)) {
361 const i = outputPath.lastIndexOf("/");
362 const j = outputPath.lastIndexOf("\\");
363 const pos = i < 0 ? j : j < 0 ? i : Math.max(i, j);
364 if (pos < 0) return outputPath + "/";
365 append = outputPath.slice(pos + 1) + "/" + append;
366 outputPath = outputPath.slice(0, pos);
368 } else if (part !== ".") {
373 ? `${"../".repeat(depth)}${append}`