path.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const path = require("path");
  7. const { fileURLToPath } = require("url");
  8. const CHAR_HASH = "#".charCodeAt(0);
  9. const CHAR_SLASH = "/".charCodeAt(0);
  10. const CHAR_BACKSLASH = "\\".charCodeAt(0);
  11. const CHAR_A = "A".charCodeAt(0);
  12. const CHAR_Z = "Z".charCodeAt(0);
  13. const CHAR_LOWER_A = "a".charCodeAt(0);
  14. const CHAR_LOWER_Z = "z".charCodeAt(0);
  15. const CHAR_DOT = ".".charCodeAt(0);
  16. const CHAR_COLON = ":".charCodeAt(0);
  17. const posixNormalize = path.posix.normalize;
  18. const winNormalize = path.win32.normalize;
  19. /**
  20. * @enum {number}
  21. */
  22. const PathType = Object.freeze({
  23. Empty: 0,
  24. Normal: 1,
  25. Relative: 2,
  26. AbsoluteWin: 3,
  27. AbsolutePosix: 4,
  28. Internal: 5,
  29. });
  30. const deprecatedInvalidSegmentRegEx =
  31. /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
  32. const invalidSegmentRegEx =
  33. /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i;
  34. /**
  35. * @param {string} maybePath a path
  36. * @returns {PathType} type of path
  37. */
  38. const getType = (maybePath) => {
  39. switch (maybePath.length) {
  40. case 0:
  41. return PathType.Empty;
  42. case 1: {
  43. const c0 = maybePath.charCodeAt(0);
  44. switch (c0) {
  45. case CHAR_DOT:
  46. return PathType.Relative;
  47. case CHAR_SLASH:
  48. return PathType.AbsolutePosix;
  49. case CHAR_HASH:
  50. return PathType.Internal;
  51. }
  52. return PathType.Normal;
  53. }
  54. case 2: {
  55. const c0 = maybePath.charCodeAt(0);
  56. switch (c0) {
  57. case CHAR_DOT: {
  58. const c1 = maybePath.charCodeAt(1);
  59. switch (c1) {
  60. case CHAR_DOT:
  61. case CHAR_SLASH:
  62. return PathType.Relative;
  63. }
  64. return PathType.Normal;
  65. }
  66. case CHAR_SLASH:
  67. return PathType.AbsolutePosix;
  68. case CHAR_HASH:
  69. return PathType.Internal;
  70. }
  71. const c1 = maybePath.charCodeAt(1);
  72. if (
  73. c1 === CHAR_COLON &&
  74. ((c0 >= CHAR_A && c0 <= CHAR_Z) ||
  75. (c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z))
  76. ) {
  77. return PathType.AbsoluteWin;
  78. }
  79. if (c0 === CHAR_BACKSLASH && c1 === CHAR_BACKSLASH) {
  80. return PathType.AbsoluteWin;
  81. }
  82. return PathType.Normal;
  83. }
  84. }
  85. const c0 = maybePath.charCodeAt(0);
  86. switch (c0) {
  87. case CHAR_DOT: {
  88. const c1 = maybePath.charCodeAt(1);
  89. switch (c1) {
  90. case CHAR_SLASH:
  91. return PathType.Relative;
  92. case CHAR_DOT: {
  93. const c2 = maybePath.charCodeAt(2);
  94. if (c2 === CHAR_SLASH) return PathType.Relative;
  95. return PathType.Normal;
  96. }
  97. }
  98. return PathType.Normal;
  99. }
  100. case CHAR_SLASH:
  101. return PathType.AbsolutePosix;
  102. case CHAR_HASH:
  103. return PathType.Internal;
  104. }
  105. const c1 = maybePath.charCodeAt(1);
  106. if (c1 === CHAR_COLON) {
  107. const c2 = maybePath.charCodeAt(2);
  108. if (
  109. (c2 === CHAR_BACKSLASH || c2 === CHAR_SLASH) &&
  110. ((c0 >= CHAR_A && c0 <= CHAR_Z) ||
  111. (c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z))
  112. ) {
  113. return PathType.AbsoluteWin;
  114. }
  115. }
  116. // Two leading backslashes root a UNC share (`\\server\share`) or a DOS
  117. // device path (`\\?\…`, `\\.\…`); `path.win32` reads either as absolute,
  118. // so both belong on `path.win32` and not on posix.
  119. if (c0 === CHAR_BACKSLASH && c1 === CHAR_BACKSLASH) {
  120. return PathType.AbsoluteWin;
  121. }
  122. return PathType.Normal;
  123. };
  124. /**
  125. * @param {string} maybePath a path
  126. * @returns {string} the normalized path
  127. */
  128. const normalize = (maybePath) => {
  129. switch (getType(maybePath)) {
  130. case PathType.Empty:
  131. return maybePath;
  132. case PathType.AbsoluteWin:
  133. return winNormalize(maybePath);
  134. case PathType.Relative: {
  135. const r = posixNormalize(maybePath);
  136. return getType(r) === PathType.Relative ? r : `./${r}`;
  137. }
  138. }
  139. return posixNormalize(maybePath);
  140. };
  141. /**
  142. * @param {string} rootPath the root path
  143. * @param {string | undefined} request the request path
  144. * @returns {string} the joined path
  145. */
  146. const join = (rootPath, request) => {
  147. if (!request) return normalize(rootPath);
  148. const requestType = getType(request);
  149. switch (requestType) {
  150. case PathType.AbsolutePosix:
  151. return posixNormalize(request);
  152. case PathType.AbsoluteWin:
  153. return winNormalize(request);
  154. }
  155. switch (getType(rootPath)) {
  156. case PathType.Normal:
  157. case PathType.Relative:
  158. case PathType.AbsolutePosix:
  159. return posixNormalize(`${rootPath}/${request}`);
  160. case PathType.AbsoluteWin:
  161. return winNormalize(`${rootPath}\\${request}`);
  162. }
  163. switch (requestType) {
  164. case PathType.Empty:
  165. return rootPath;
  166. case PathType.Relative: {
  167. const r = posixNormalize(rootPath);
  168. return getType(r) === PathType.Relative ? r : `./${r}`;
  169. }
  170. }
  171. return posixNormalize(rootPath);
  172. };
  173. /**
  174. * @param {string} maybePath a path
  175. * @returns {string} the directory name
  176. */
  177. const dirname = (maybePath) => {
  178. switch (getType(maybePath)) {
  179. case PathType.AbsoluteWin:
  180. return path.win32.dirname(maybePath);
  181. }
  182. return path.posix.dirname(maybePath);
  183. };
  184. /** @typedef {{ fn: (rootPath: string, request: string) => string, cache: Map<string, Map<string, string | undefined>> }} CachedJoin */
  185. /**
  186. * @returns {CachedJoin} cached join
  187. */
  188. const createCachedJoin = () => {
  189. /** @type {CachedJoin["cache"]} */
  190. const cache = new Map();
  191. /** @type {CachedJoin["fn"]} */
  192. const fn = (rootPath, request) => {
  193. /** @type {string | undefined} */
  194. let cacheEntry;
  195. let inner = cache.get(rootPath);
  196. if (inner === undefined) {
  197. cache.set(rootPath, (inner = new Map()));
  198. } else {
  199. cacheEntry = inner.get(request);
  200. if (cacheEntry !== undefined) return cacheEntry;
  201. }
  202. cacheEntry = join(rootPath, request);
  203. inner.set(request, cacheEntry);
  204. return cacheEntry;
  205. };
  206. return { fn, cache };
  207. };
  208. /** @typedef {{ fn: (maybePath: string) => string, cache: Map<string, string> }} CachedDirname */
  209. /**
  210. * @returns {CachedDirname} cached dirname
  211. */
  212. const createCachedDirname = () => {
  213. /** @type {CachedDirname["cache"]} */
  214. const cache = new Map();
  215. /** @type {CachedDirname["fn"]} */
  216. const fn = (maybePath) => {
  217. const cacheEntry = cache.get(maybePath);
  218. if (cacheEntry !== undefined) return cacheEntry;
  219. const result = dirname(maybePath);
  220. cache.set(maybePath, result);
  221. return result;
  222. };
  223. return { fn, cache };
  224. };
  225. /** @typedef {{ fn: (maybePath: string, suffix?: string) => string, cache: Map<string, Map<string | undefined, string | undefined>> }} CachedBasename */
  226. /**
  227. * @returns {CachedBasename} cached basename
  228. */
  229. const createCachedBasename = () => {
  230. /** @type {CachedBasename["cache"]} */
  231. const cache = new Map();
  232. /** @type {CachedBasename["fn"]} */
  233. const fn = (maybePath, suffix) => {
  234. /** @type {string | undefined} */
  235. let cacheEntry;
  236. let inner = cache.get(maybePath);
  237. if (inner === undefined) {
  238. cache.set(maybePath, (inner = new Map()));
  239. } else {
  240. cacheEntry = inner.get(suffix);
  241. if (cacheEntry !== undefined) return cacheEntry;
  242. }
  243. cacheEntry = path.basename(maybePath, suffix);
  244. inner.set(suffix, cacheEntry);
  245. return cacheEntry;
  246. };
  247. return { fn, cache };
  248. };
  249. /**
  250. * Whether `request` is a relative request — i.e. matches `^\.\.?(?:\/|$)`.
  251. *
  252. * This is called on every `doResolve` via `UnsafeCachePlugin` and
  253. * `getInnerRequest`, so the char-code form is meaningfully faster than the
  254. * equivalent regex test: no regex state machine, no string object churn.
  255. * @param {string} request request string
  256. * @returns {boolean} true if request is relative
  257. */
  258. const isRelativeRequest = (request) => {
  259. const len = request.length;
  260. if (len === 0 || request.charCodeAt(0) !== CHAR_DOT) return false;
  261. if (len === 1) return true; // "."
  262. const c1 = request.charCodeAt(1);
  263. if (c1 === CHAR_SLASH) return true; // "./..."
  264. if (c1 !== CHAR_DOT) return false; // ".x..."
  265. if (len === 2) return true; // ".."
  266. return request.charCodeAt(2) === CHAR_SLASH; // "../..."
  267. };
  268. /**
  269. * Whether this is a Windows path, in which `/` and `\` are interchangeable and
  270. * paths compare case-insensitively, as opposed to a posix path, in which `\` is
  271. * an ordinary filename character. Decided by the root — a drive letter or a
  272. * leading `\` — which is where `path.win32` and `path.posix` disagree about
  273. * `parse(maybePath).root`, and never by the host platform, since Windows paths
  274. * are resolved on posix hosts and in browsers too. A path starting with `//`
  275. * stays posix: `path.win32` reads it as a UNC root, but here it cannot be told
  276. * apart from a posix path, where `\` has to keep being a filename character.
  277. * @param {string} maybePath a path
  278. * @returns {boolean} true, when the path is a Windows path
  279. */
  280. const isWindowsPath = (maybePath) => {
  281. const c0 = maybePath.charCodeAt(0);
  282. if (c0 === CHAR_BACKSLASH) return true;
  283. if (maybePath.charCodeAt(1) !== CHAR_COLON) return false;
  284. return (
  285. (c0 >= CHAR_A && c0 <= CHAR_Z) || (c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z)
  286. );
  287. };
  288. /**
  289. * @param {number} charCode a char code
  290. * @param {boolean} windowsPath whether `\` separates segments
  291. * @returns {boolean} true, when the char code separates path segments
  292. */
  293. const isSeparator = (charCode, windowsPath) =>
  294. charCode === CHAR_SLASH || (windowsPath && charCode === CHAR_BACKSLASH);
  295. /**
  296. * @param {string} parentPath parent directory path
  297. * @param {boolean} windowsPath whether `parentPath` is a Windows path
  298. * @returns {number} length of `parentPath` without its trailing separators
  299. */
  300. const parentPathLength = (parentPath, windowsPath) => {
  301. let end = parentPath.length;
  302. while (end > 0 && isSeparator(parentPath.charCodeAt(end - 1), windowsPath)) {
  303. end--;
  304. }
  305. return end;
  306. };
  307. /**
  308. * Cold path of `startsWithPath`: Node lowercases whole paths rather than single
  309. * characters, which only makes a difference outside of ASCII.
  310. * @param {string} parentPath parent directory path
  311. * @param {number} length number of characters to compare
  312. * @param {string} childPath child path to check
  313. * @returns {boolean} true, when both prefixes name the same Windows path
  314. */
  315. const equalsWindowsPrefix = (parentPath, length, childPath) =>
  316. childPath.slice(0, length).replace(/\//g, "\\").toLowerCase() ===
  317. parentPath.slice(0, length).replace(/\//g, "\\").toLowerCase();
  318. /**
  319. * @param {string} parentPath parent directory path
  320. * @param {number} length number of characters of `parentPath` to compare
  321. * @param {string} childPath child path to check
  322. * @param {boolean} windowsPath whether `parentPath` is a Windows path
  323. * @returns {boolean} true, when `childPath` starts with that prefix
  324. */
  325. const startsWithPath = (parentPath, length, childPath, windowsPath) => {
  326. if (childPath.length < length) return false;
  327. // The common case is an exact prefix of a parent without trailing
  328. // separators, which `startsWith` answers natively and without a slice. For
  329. // a posix parent that is the whole answer, only a Windows one has more
  330. // spellings of the same path to try.
  331. if (length === parentPath.length) {
  332. if (childPath.startsWith(parentPath)) return true;
  333. if (!windowsPath) return false;
  334. }
  335. for (let i = 0; i < length; i++) {
  336. const childCharCode = childPath.charCodeAt(i);
  337. const parentCharCode = parentPath.charCodeAt(i);
  338. if (childCharCode === parentCharCode) continue;
  339. if (!windowsPath) return false;
  340. // Windows mixes `/` and `\` freely and compares case-insensitively.
  341. if (isSeparator(childCharCode, true) && isSeparator(parentCharCode, true)) {
  342. continue;
  343. }
  344. if (childCharCode > 127 || parentCharCode > 127) {
  345. return equalsWindowsPrefix(parentPath, length, childPath);
  346. }
  347. const childLower =
  348. childCharCode >= CHAR_A && childCharCode <= CHAR_Z
  349. ? childCharCode + 32
  350. : childCharCode;
  351. const parentLower =
  352. parentCharCode >= CHAR_A && parentCharCode <= CHAR_Z
  353. ? parentCharCode + 32
  354. : parentCharCode;
  355. if (childLower !== parentLower) return false;
  356. }
  357. return true;
  358. };
  359. /**
  360. * Whether childPath is parentPath itself or a path under it, the answer node's
  361. * `relative(parentPath, childPath)` gives: not escaping upward and not
  362. * absolute. A trailing separator on the parent is not part of the boundary, so
  363. * `/a/b/` contains exactly what `/a/b` contains.
  364. * @param {string} parentPath parent directory path
  365. * @param {string} childPath child path to check
  366. * @returns {boolean} true if childPath is parentPath or is under it
  367. */
  368. const isInside = (parentPath, childPath) => {
  369. const windowsPath = isWindowsPath(parentPath);
  370. const length = parentPathLength(parentPath, windowsPath);
  371. if (!startsWithPath(parentPath, length, childPath, windowsPath)) return false;
  372. // The parent itself, or a segment boundary right after it so that `/a/b`
  373. // does not contain the sibling `/a/b-other`.
  374. return (
  375. childPath.length === length ||
  376. isSeparator(childPath.charCodeAt(length), windowsPath)
  377. );
  378. };
  379. /**
  380. * Check if childPath is a subdirectory of parentPath. Compares like `isInside`,
  381. * except that a path is not a subpath of itself.
  382. *
  383. * Called from `TsconfigPathsPlugin._selectPathsDataForContext` inside a loop
  384. * over every tsconfig-paths context on every resolve, so it's worth keeping
  385. * cheap: a native `startsWith` plus a separator char check answers it, and the
  386. * character loop only runs for a Windows path that the prefix test missed.
  387. * @param {string} parentPath parent directory path
  388. * @param {string} childPath child path to check
  389. * @returns {boolean} true if childPath is under parentPath
  390. */
  391. const isSubPath = (parentPath, childPath) => {
  392. const windowsPath = isWindowsPath(parentPath);
  393. const length = parentPathLength(parentPath, windowsPath);
  394. if (childPath.length <= length) return false;
  395. if (!startsWithPath(parentPath, length, childPath, windowsPath)) return false;
  396. return isSeparator(childPath.charCodeAt(length), windowsPath);
  397. };
  398. /**
  399. * Convert a `file:` `URL` instance to a filesystem path; any other input
  400. * (including plain strings) is returned unchanged. Mirrors Node's `fs`, which
  401. * treats strings as literal paths and only `URL` objects as URLs (see
  402. * nodejs/node#17658) — so a directory literally named `file:` is never
  403. * mistaken for a URL.
  404. * @param {string | URL} maybeURL a path string or a `file:` `URL` instance
  405. * @returns {string} a filesystem path
  406. */
  407. const toPath = (maybeURL) =>
  408. maybeURL instanceof URL ? fileURLToPath(maybeURL) : maybeURL;
  409. module.exports.PathType = PathType;
  410. module.exports.createCachedBasename = createCachedBasename;
  411. module.exports.createCachedDirname = createCachedDirname;
  412. module.exports.createCachedJoin = createCachedJoin;
  413. module.exports.deprecatedInvalidSegmentRegEx = deprecatedInvalidSegmentRegEx;
  414. module.exports.dirname = dirname;
  415. module.exports.getType = getType;
  416. module.exports.invalidSegmentRegEx = invalidSegmentRegEx;
  417. module.exports.isInside = isInside;
  418. module.exports.isRelativeRequest = isRelativeRequest;
  419. module.exports.isSubPath = isSubPath;
  420. module.exports.isWindowsPath = isWindowsPath;
  421. module.exports.join = join;
  422. module.exports.normalize = normalize;
  423. module.exports.toPath = toPath;