UNPKG

10.3 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 = fn => {
85 /** @type {WeakMap<object, Map<string, Map<string, string>>>} */
86 const cache = new WeakMap();
87
88 /**
89 * @param {string} context context used to create relative path
90 * @param {string} identifier identifier used to create relative path
91 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
92 * @returns {string} the returned relative path
93 */
94 const cachedFn = (context, identifier, associatedObjectForCache) => {
95 if (!associatedObjectForCache) return fn(context, identifier);
96
97 let innerCache = cache.get(associatedObjectForCache);
98 if (innerCache === undefined) {
99 innerCache = new Map();
100 cache.set(associatedObjectForCache, innerCache);
101 }
102
103 let cachedResult;
104 let innerSubCache = innerCache.get(context);
105 if (innerSubCache === undefined) {
106 innerCache.set(context, (innerSubCache = new Map()));
107 } else {
108 cachedResult = innerSubCache.get(identifier);
109 }
110
111 if (cachedResult !== undefined) {
112 return cachedResult;
113 } else {
114 const result = fn(context, identifier);
115 innerSubCache.set(identifier, result);
116 return result;
117 }
118 };
119
120 /**
121 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
122 * @returns {function(string, string): string} cached function
123 */
124 cachedFn.bindCache = associatedObjectForCache => {
125 let innerCache;
126 if (associatedObjectForCache) {
127 innerCache = cache.get(associatedObjectForCache);
128 if (innerCache === undefined) {
129 innerCache = new Map();
130 cache.set(associatedObjectForCache, innerCache);
131 }
132 } else {
133 innerCache = new Map();
134 }
135
136 /**
137 * @param {string} context context used to create relative path
138 * @param {string} identifier identifier used to create relative path
139 * @returns {string} the returned relative path
140 */
141 const boundFn = (context, identifier) => {
142 let cachedResult;
143 let innerSubCache = innerCache.get(context);
144 if (innerSubCache === undefined) {
145 innerCache.set(context, (innerSubCache = new Map()));
146 } else {
147 cachedResult = innerSubCache.get(identifier);
148 }
149
150 if (cachedResult !== undefined) {
151 return cachedResult;
152 } else {
153 const result = fn(context, identifier);
154 innerSubCache.set(identifier, result);
155 return result;
156 }
157 };
158
159 return boundFn;
160 };
161
162 /**
163 * @param {string} context context used to create relative path
164 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
165 * @returns {function(string): string} cached function
166 */
167 cachedFn.bindContextCache = (context, associatedObjectForCache) => {
168 let innerSubCache;
169 if (associatedObjectForCache) {
170 let innerCache = cache.get(associatedObjectForCache);
171 if (innerCache === undefined) {
172 innerCache = new Map();
173 cache.set(associatedObjectForCache, innerCache);
174 }
175
176 innerSubCache = innerCache.get(context);
177 if (innerSubCache === undefined) {
178 innerCache.set(context, (innerSubCache = new Map()));
179 }
180 } else {
181 innerSubCache = new Map();
182 }
183
184 /**
185 * @param {string} identifier identifier used to create relative path
186 * @returns {string} the returned relative path
187 */
188 const boundFn = identifier => {
189 const cachedResult = innerSubCache.get(identifier);
190 if (cachedResult !== undefined) {
191 return cachedResult;
192 } else {
193 const result = fn(context, identifier);
194 innerSubCache.set(identifier, result);
195 return result;
196 }
197 };
198
199 return boundFn;
200 };
201
202 return cachedFn;
203};
204
205/**
206 *
207 * @param {string} context context for relative path
208 * @param {string} identifier identifier for path
209 * @returns {string} a converted relative path
210 */
211const _makePathsRelative = (context, identifier) => {
212 return identifier
213 .split(SEGMENTS_SPLIT_REGEXP)
214 .map(str => absoluteToRequest(context, str))
215 .join("");
216};
217
218exports.makePathsRelative = makeCacheable(_makePathsRelative);
219
220/**
221 *
222 * @param {string} context context for relative path
223 * @param {string} identifier identifier for path
224 * @returns {string} a converted relative path
225 */
226const _makePathsAbsolute = (context, identifier) => {
227 return identifier
228 .split(SEGMENTS_SPLIT_REGEXP)
229 .map(str => requestToAbsolute(context, str))
230 .join("");
231};
232
233exports.makePathsAbsolute = makeCacheable(_makePathsAbsolute);
234
235/**
236 * @param {string} context absolute context path
237 * @param {string} request any request string may containing absolute paths, query string, etc.
238 * @returns {string} a new request string avoiding absolute paths when possible
239 */
240const _contextify = (context, request) => {
241 return request
242 .split("!")
243 .map(r => absoluteToRequest(context, r))
244 .join("!");
245};
246
247const contextify = makeCacheable(_contextify);
248exports.contextify = contextify;
249
250/**
251 * @param {string} context absolute context path
252 * @param {string} request any request string
253 * @returns {string} a new request string using absolute paths when possible
254 */
255const _absolutify = (context, request) => {
256 return request
257 .split("!")
258 .map(r => requestToAbsolute(context, r))
259 .join("!");
260};
261
262const absolutify = makeCacheable(_absolutify);
263exports.absolutify = absolutify;
264
265const PATH_QUERY_FRAGMENT_REGEXP =
266 /^((?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/;
267
268/** @typedef {{ resource: string, path: string, query: string, fragment: string }} ParsedResource */
269
270/**
271 * @param {string} str the path with query and fragment
272 * @returns {ParsedResource} parsed parts
273 */
274const _parseResource = str => {
275 const match = PATH_QUERY_FRAGMENT_REGEXP.exec(str);
276 return {
277 resource: str,
278 path: match[1].replace(/\0(.)/g, "$1"),
279 query: match[2] ? match[2].replace(/\0(.)/g, "$1") : "",
280 fragment: match[3] || ""
281 };
282};
283exports.parseResource = (realFn => {
284 /** @type {WeakMap<object, Map<string, ParsedResource>>} */
285 const cache = new WeakMap();
286
287 const getCache = associatedObjectForCache => {
288 const entry = cache.get(associatedObjectForCache);
289 if (entry !== undefined) return entry;
290 /** @type {Map<string, ParsedResource>} */
291 const map = new Map();
292 cache.set(associatedObjectForCache, map);
293 return map;
294 };
295
296 /**
297 * @param {string} str the path with query and fragment
298 * @param {Object=} associatedObjectForCache an object to which the cache will be attached
299 * @returns {ParsedResource} parsed parts
300 */
301 const fn = (str, associatedObjectForCache) => {
302 if (!associatedObjectForCache) return realFn(str);
303 const cache = getCache(associatedObjectForCache);
304 const entry = cache.get(str);
305 if (entry !== undefined) return entry;
306 const result = realFn(str);
307 cache.set(str, result);
308 return result;
309 };
310
311 fn.bindCache = associatedObjectForCache => {
312 const cache = getCache(associatedObjectForCache);
313 return str => {
314 const entry = cache.get(str);
315 if (entry !== undefined) return entry;
316 const result = realFn(str);
317 cache.set(str, result);
318 return result;
319 };
320 };
321
322 return fn;
323})(_parseResource);
324
325/**
326 * @param {string} filename the filename which should be undone
327 * @param {string} outputPath the output path that is restored (only relevant when filename contains "..")
328 * @param {boolean} enforceRelative true returns ./ for empty paths
329 * @returns {string} repeated ../ to leave the directory of the provided filename to be back on output dir
330 */
331exports.getUndoPath = (filename, outputPath, enforceRelative) => {
332 let depth = -1;
333 let append = "";
334 outputPath = outputPath.replace(/[\\/]$/, "");
335 for (const part of filename.split(/[/\\]+/)) {
336 if (part === "..") {
337 if (depth > -1) {
338 depth--;
339 } else {
340 const i = outputPath.lastIndexOf("/");
341 const j = outputPath.lastIndexOf("\\");
342 const pos = i < 0 ? j : j < 0 ? i : Math.max(i, j);
343 if (pos < 0) return outputPath + "/";
344 append = outputPath.slice(pos + 1) + "/" + append;
345 outputPath = outputPath.slice(0, pos);
346 }
347 } else if (part !== ".") {
348 depth++;
349 }
350 }
351 return depth > 0
352 ? `${"../".repeat(depth)}${append}`
353 : enforceRelative
354 ? `./${append}`
355 : append;
356};