UNPKG

11.1 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3*/
4
5"use strict";
6
7const path = require("path");
8
9const WINDOWS_ABS_PATH_REGEXP = /^[a-zA-Z]:[\\/]/;
10const SEGMENTS_SPLIT_REGEXP = /([|!])/;
11const WINDOWS_PATH_SEPARATOR_REGEXP = /\\/g;
12
13/**
14 * @typedef {Object} MakeRelativePathsCache
15 * @property {Map<string, Map<string, string>>=} relativePaths
16 */
17
18const relativePathToRequest = relativePath => {
19 if (relativePath === "") return "./.";
20 if (relativePath === "..") return "../.";
21 if (relativePath.startsWith("../")) return relativePath;
22 return `./${relativePath}`;
23};
24
25/**
26 * @param {string} context context for relative path
27 * @param {string} maybeAbsolutePath path to make relative
28 * @returns {string} relative path in request style
29 */
30const absoluteToRequest = (context, maybeAbsolutePath) => {
31 if (maybeAbsolutePath[0] === "/") {
32 if (
33 maybeAbsolutePath.length > 1 &&
34 maybeAbsolutePath[maybeAbsolutePath.length - 1] === "/"
35 ) {
36 // this 'path' is actually a regexp generated by dynamic requires.
37 // Don't treat it as an absolute path.
38 return maybeAbsolutePath;
39 }
40
41 const querySplitPos = maybeAbsolutePath.indexOf("?");
42 let resource =
43 querySplitPos === -1
44 ? maybeAbsolutePath
45 : maybeAbsolutePath.slice(0, querySplitPos);
46 resource = relativePathToRequest(path.posix.relative(context, resource));
47 return querySplitPos === -1
48 ? resource
49 : resource + maybeAbsolutePath.slice(querySplitPos);
50 }
51
52 if (WINDOWS_ABS_PATH_REGEXP.test(maybeAbsolutePath)) {
53 const querySplitPos = maybeAbsolutePath.indexOf("?");
54 let resource =
55 querySplitPos === -1
56 ? maybeAbsolutePath
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, "/")
62 );
63 }
64 return querySplitPos === -1
65 ? resource
66 : resource + maybeAbsolutePath.slice(querySplitPos);
67 }
68
69 // not an absolute path
70 return maybeAbsolutePath;
71};
72
73/**
74 * @param {string} context context for relative path
75 * @param {string} relativePath path
76 * @returns {string} absolute path
77 */
78const requestToAbsolute = (context, relativePath) => {
79 if (relativePath.startsWith("./") || relativePath.startsWith("../"))
80 return path.join(context, relativePath);
81 return relativePath;
82};
83
84const makeCacheable = realFn => {
85 /** @type {WeakMap<object, Map<string, ParsedResource>>} */
86 const cache = new WeakMap();
87
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);
94 return map;
95 };
96
97 /**
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
101 */
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);
109 return result;
110 };
111
112 fn.bindCache = associatedObjectForCache => {
113 const cache = getCache(associatedObjectForCache);
114 return str => {
115 const entry = cache.get(str);
116 if (entry !== undefined) return entry;
117 const result = realFn(str);
118 cache.set(str, result);
119 return result;
120 };
121 };
122
123 return fn;
124};
125
126const makeCacheableWithContext = fn => {
127 /** @type {WeakMap<object, Map<string, Map<string, string>>>} */
128 const cache = new WeakMap();
129
130 /**
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
135 */
136 const cachedFn = (context, identifier, associatedObjectForCache) => {
137 if (!associatedObjectForCache) return fn(context, identifier);
138
139 let innerCache = cache.get(associatedObjectForCache);
140 if (innerCache === undefined) {
141 innerCache = new Map();
142 cache.set(associatedObjectForCache, innerCache);
143 }
144
145 let cachedResult;
146 let innerSubCache = innerCache.get(context);
147 if (innerSubCache === undefined) {
148 innerCache.set(context, (innerSubCache = new Map()));
149 } else {
150 cachedResult = innerSubCache.get(identifier);
151 }
152
153 if (cachedResult !== undefined) {
154 return cachedResult;
155 } else {
156 const result = fn(context, identifier);
157 innerSubCache.set(identifier, result);
158 return result;
159 }
160 };
161
162 /**
163 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
164 * @returns {function(string, string): string} cached function
165 */
166 cachedFn.bindCache = associatedObjectForCache => {
167 let innerCache;
168 if (associatedObjectForCache) {
169 innerCache = cache.get(associatedObjectForCache);
170 if (innerCache === undefined) {
171 innerCache = new Map();
172 cache.set(associatedObjectForCache, innerCache);
173 }
174 } else {
175 innerCache = new Map();
176 }
177
178 /**
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
182 */
183 const boundFn = (context, identifier) => {
184 let cachedResult;
185 let innerSubCache = innerCache.get(context);
186 if (innerSubCache === undefined) {
187 innerCache.set(context, (innerSubCache = new Map()));
188 } else {
189 cachedResult = innerSubCache.get(identifier);
190 }
191
192 if (cachedResult !== undefined) {
193 return cachedResult;
194 } else {
195 const result = fn(context, identifier);
196 innerSubCache.set(identifier, result);
197 return result;
198 }
199 };
200
201 return boundFn;
202 };
203
204 /**
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
208 */
209 cachedFn.bindContextCache = (context, associatedObjectForCache) => {
210 let innerSubCache;
211 if (associatedObjectForCache) {
212 let innerCache = cache.get(associatedObjectForCache);
213 if (innerCache === undefined) {
214 innerCache = new Map();
215 cache.set(associatedObjectForCache, innerCache);
216 }
217
218 innerSubCache = innerCache.get(context);
219 if (innerSubCache === undefined) {
220 innerCache.set(context, (innerSubCache = new Map()));
221 }
222 } else {
223 innerSubCache = new Map();
224 }
225
226 /**
227 * @param {string} identifier identifier used to create relative path
228 * @returns {string} the returned relative path
229 */
230 const boundFn = identifier => {
231 const cachedResult = innerSubCache.get(identifier);
232 if (cachedResult !== undefined) {
233 return cachedResult;
234 } else {
235 const result = fn(context, identifier);
236 innerSubCache.set(identifier, result);
237 return result;
238 }
239 };
240
241 return boundFn;
242 };
243
244 return cachedFn;
245};
246
247/**
248 *
249 * @param {string} context context for relative path
250 * @param {string} identifier identifier for path
251 * @returns {string} a converted relative path
252 */
253const _makePathsRelative = (context, identifier) => {
254 return identifier
255 .split(SEGMENTS_SPLIT_REGEXP)
256 .map(str => absoluteToRequest(context, str))
257 .join("");
258};
259
260exports.makePathsRelative = makeCacheableWithContext(_makePathsRelative);
261
262/**
263 *
264 * @param {string} context context for relative path
265 * @param {string} identifier identifier for path
266 * @returns {string} a converted relative path
267 */
268const _makePathsAbsolute = (context, identifier) => {
269 return identifier
270 .split(SEGMENTS_SPLIT_REGEXP)
271 .map(str => requestToAbsolute(context, str))
272 .join("");
273};
274
275exports.makePathsAbsolute = makeCacheableWithContext(_makePathsAbsolute);
276
277/**
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
281 */
282const _contextify = (context, request) => {
283 return request
284 .split("!")
285 .map(r => absoluteToRequest(context, r))
286 .join("!");
287};
288
289const contextify = makeCacheableWithContext(_contextify);
290exports.contextify = contextify;
291
292/**
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
296 */
297const _absolutify = (context, request) => {
298 return request
299 .split("!")
300 .map(r => requestToAbsolute(context, r))
301 .join("!");
302};
303
304const absolutify = makeCacheableWithContext(_absolutify);
305exports.absolutify = absolutify;
306
307const PATH_QUERY_FRAGMENT_REGEXP =
308 /^((?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/;
309const PATH_QUERY_REGEXP = /^((?:\0.|[^?\0])*)(\?.*)?$/;
310
311/** @typedef {{ resource: string, path: string, query: string, fragment: string }} ParsedResource */
312/** @typedef {{ resource: string, path: string, query: string }} ParsedResourceWithoutFragment */
313
314/**
315 * @param {string} str the path with query and fragment
316 * @returns {ParsedResource} parsed parts
317 */
318const _parseResource = str => {
319 const match = PATH_QUERY_FRAGMENT_REGEXP.exec(str);
320 return {
321 resource: str,
322 path: match[1].replace(/\0(.)/g, "$1"),
323 query: match[2] ? match[2].replace(/\0(.)/g, "$1") : "",
324 fragment: match[3] || ""
325 };
326};
327exports.parseResource = makeCacheable(_parseResource);
328
329/**
330 * Parse resource, skips fragment part
331 * @param {string} str the path with query and fragment
332 * @returns {ParsedResourceWithoutFragment} parsed parts
333 */
334const _parseResourceWithoutFragment = str => {
335 const match = PATH_QUERY_REGEXP.exec(str);
336 return {
337 resource: str,
338 path: match[1].replace(/\0(.)/g, "$1"),
339 query: match[2] ? match[2].replace(/\0(.)/g, "$1") : ""
340 };
341};
342exports.parseResourceWithoutFragment = makeCacheable(
343 _parseResourceWithoutFragment
344);
345
346/**
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
351 */
352exports.getUndoPath = (filename, outputPath, enforceRelative) => {
353 let depth = -1;
354 let append = "";
355 outputPath = outputPath.replace(/[\\/]$/, "");
356 for (const part of filename.split(/[/\\]+/)) {
357 if (part === "..") {
358 if (depth > -1) {
359 depth--;
360 } else {
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);
367 }
368 } else if (part !== ".") {
369 depth++;
370 }
371 }
372 return depth > 0
373 ? `${"../".repeat(depth)}${append}`
374 : enforceRelative
375 ? `./${append}`
376 : append;
377};