path.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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 CHAR_HASH = "#".charCodeAt(0);
  8. const CHAR_SLASH = "/".charCodeAt(0);
  9. const CHAR_BACKSLASH = "\\".charCodeAt(0);
  10. const CHAR_A = "A".charCodeAt(0);
  11. const CHAR_Z = "Z".charCodeAt(0);
  12. const CHAR_LOWER_A = "a".charCodeAt(0);
  13. const CHAR_LOWER_Z = "z".charCodeAt(0);
  14. const CHAR_DOT = ".".charCodeAt(0);
  15. const CHAR_COLON = ":".charCodeAt(0);
  16. const CHAR_QUESTION = "?".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 known to start with `\\`
  36. * @returns {PathType} AbsoluteWin for `\\?\…` / `\\.\…`, otherwise Normal
  37. */
  38. const getDosDeviceType = (maybePath) => {
  39. if (maybePath.length >= 4 && maybePath.charCodeAt(3) === CHAR_BACKSLASH) {
  40. const c2 = maybePath.charCodeAt(2);
  41. if (c2 === CHAR_QUESTION || c2 === CHAR_DOT) {
  42. return PathType.AbsoluteWin;
  43. }
  44. }
  45. return PathType.Normal;
  46. };
  47. /**
  48. * @param {string} maybePath a path
  49. * @returns {PathType} type of path
  50. */
  51. const getType = (maybePath) => {
  52. switch (maybePath.length) {
  53. case 0:
  54. return PathType.Empty;
  55. case 1: {
  56. const c0 = maybePath.charCodeAt(0);
  57. switch (c0) {
  58. case CHAR_DOT:
  59. return PathType.Relative;
  60. case CHAR_SLASH:
  61. return PathType.AbsolutePosix;
  62. case CHAR_HASH:
  63. return PathType.Internal;
  64. }
  65. return PathType.Normal;
  66. }
  67. case 2: {
  68. const c0 = maybePath.charCodeAt(0);
  69. switch (c0) {
  70. case CHAR_DOT: {
  71. const c1 = maybePath.charCodeAt(1);
  72. switch (c1) {
  73. case CHAR_DOT:
  74. case CHAR_SLASH:
  75. return PathType.Relative;
  76. }
  77. return PathType.Normal;
  78. }
  79. case CHAR_SLASH:
  80. return PathType.AbsolutePosix;
  81. case CHAR_HASH:
  82. return PathType.Internal;
  83. }
  84. const c1 = maybePath.charCodeAt(1);
  85. if (
  86. c1 === CHAR_COLON &&
  87. ((c0 >= CHAR_A && c0 <= CHAR_Z) ||
  88. (c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z))
  89. ) {
  90. return PathType.AbsoluteWin;
  91. }
  92. return PathType.Normal;
  93. }
  94. }
  95. const c0 = maybePath.charCodeAt(0);
  96. switch (c0) {
  97. case CHAR_DOT: {
  98. const c1 = maybePath.charCodeAt(1);
  99. switch (c1) {
  100. case CHAR_SLASH:
  101. return PathType.Relative;
  102. case CHAR_DOT: {
  103. const c2 = maybePath.charCodeAt(2);
  104. if (c2 === CHAR_SLASH) return PathType.Relative;
  105. return PathType.Normal;
  106. }
  107. }
  108. return PathType.Normal;
  109. }
  110. case CHAR_SLASH:
  111. return PathType.AbsolutePosix;
  112. case CHAR_HASH:
  113. return PathType.Internal;
  114. }
  115. const c1 = maybePath.charCodeAt(1);
  116. if (c1 === CHAR_COLON) {
  117. const c2 = maybePath.charCodeAt(2);
  118. if (
  119. (c2 === CHAR_BACKSLASH || c2 === CHAR_SLASH) &&
  120. ((c0 >= CHAR_A && c0 <= CHAR_Z) ||
  121. (c0 >= CHAR_LOWER_A && c0 <= CHAR_LOWER_Z))
  122. ) {
  123. return PathType.AbsoluteWin;
  124. }
  125. }
  126. // DOS device paths (`\\?\…`, `\\.\…`) are handled in a cold helper so
  127. // this function stays small — inlining the full check here regressed
  128. // `description-files-multi` under `--no-opt` interpretation. Here we
  129. // only pay the two-byte gate for non-DOS inputs.
  130. if (c0 === CHAR_BACKSLASH && c1 === CHAR_BACKSLASH) {
  131. return getDosDeviceType(maybePath);
  132. }
  133. return PathType.Normal;
  134. };
  135. /**
  136. * @param {string} maybePath a path
  137. * @returns {string} the normalized path
  138. */
  139. const normalize = (maybePath) => {
  140. switch (getType(maybePath)) {
  141. case PathType.Empty:
  142. return maybePath;
  143. case PathType.AbsoluteWin:
  144. return winNormalize(maybePath);
  145. case PathType.Relative: {
  146. const r = posixNormalize(maybePath);
  147. return getType(r) === PathType.Relative ? r : `./${r}`;
  148. }
  149. }
  150. return posixNormalize(maybePath);
  151. };
  152. /**
  153. * @param {string} rootPath the root path
  154. * @param {string | undefined} request the request path
  155. * @returns {string} the joined path
  156. */
  157. const join = (rootPath, request) => {
  158. if (!request) return normalize(rootPath);
  159. const requestType = getType(request);
  160. switch (requestType) {
  161. case PathType.AbsolutePosix:
  162. return posixNormalize(request);
  163. case PathType.AbsoluteWin:
  164. return winNormalize(request);
  165. }
  166. switch (getType(rootPath)) {
  167. case PathType.Normal:
  168. case PathType.Relative:
  169. case PathType.AbsolutePosix:
  170. return posixNormalize(`${rootPath}/${request}`);
  171. case PathType.AbsoluteWin:
  172. return winNormalize(`${rootPath}\\${request}`);
  173. }
  174. switch (requestType) {
  175. case PathType.Empty:
  176. return rootPath;
  177. case PathType.Relative: {
  178. const r = posixNormalize(rootPath);
  179. return getType(r) === PathType.Relative ? r : `./${r}`;
  180. }
  181. }
  182. return posixNormalize(rootPath);
  183. };
  184. /**
  185. * @param {string} maybePath a path
  186. * @returns {string} the directory name
  187. */
  188. const dirname = (maybePath) => {
  189. switch (getType(maybePath)) {
  190. case PathType.AbsoluteWin:
  191. return path.win32.dirname(maybePath);
  192. }
  193. return path.posix.dirname(maybePath);
  194. };
  195. /** @typedef {{ fn: (rootPath: string, request: string) => string, cache: Map<string, Map<string, string | undefined>> }} CachedJoin */
  196. /**
  197. * @returns {CachedJoin} cached join
  198. */
  199. const createCachedJoin = () => {
  200. /** @type {CachedJoin["cache"]} */
  201. const cache = new Map();
  202. /** @type {CachedJoin["fn"]} */
  203. const fn = (rootPath, request) => {
  204. /** @type {string | undefined} */
  205. let cacheEntry;
  206. let inner = cache.get(rootPath);
  207. if (inner === undefined) {
  208. cache.set(rootPath, (inner = new Map()));
  209. } else {
  210. cacheEntry = inner.get(request);
  211. if (cacheEntry !== undefined) return cacheEntry;
  212. }
  213. cacheEntry = join(rootPath, request);
  214. inner.set(request, cacheEntry);
  215. return cacheEntry;
  216. };
  217. return { fn, cache };
  218. };
  219. /** @typedef {{ fn: (maybePath: string) => string, cache: Map<string, string> }} CachedDirname */
  220. /**
  221. * @returns {CachedDirname} cached dirname
  222. */
  223. const createCachedDirname = () => {
  224. /** @type {CachedDirname["cache"]} */
  225. const cache = new Map();
  226. /** @type {CachedDirname["fn"]} */
  227. const fn = (maybePath) => {
  228. const cacheEntry = cache.get(maybePath);
  229. if (cacheEntry !== undefined) return cacheEntry;
  230. const result = dirname(maybePath);
  231. cache.set(maybePath, result);
  232. return result;
  233. };
  234. return { fn, cache };
  235. };
  236. /** @typedef {{ fn: (maybePath: string, suffix?: string) => string, cache: Map<string, Map<string | undefined, string | undefined>> }} CachedBasename */
  237. /**
  238. * @returns {CachedBasename} cached basename
  239. */
  240. const createCachedBasename = () => {
  241. /** @type {CachedBasename["cache"]} */
  242. const cache = new Map();
  243. /** @type {CachedBasename["fn"]} */
  244. const fn = (maybePath, suffix) => {
  245. /** @type {string | undefined} */
  246. let cacheEntry;
  247. let inner = cache.get(maybePath);
  248. if (inner === undefined) {
  249. cache.set(maybePath, (inner = new Map()));
  250. } else {
  251. cacheEntry = inner.get(suffix);
  252. if (cacheEntry !== undefined) return cacheEntry;
  253. }
  254. cacheEntry = path.basename(maybePath, suffix);
  255. inner.set(suffix, cacheEntry);
  256. return cacheEntry;
  257. };
  258. return { fn, cache };
  259. };
  260. /**
  261. * Whether `request` is a relative request — i.e. matches `^\.\.?(?:\/|$)`.
  262. *
  263. * This is called on every `doResolve` via `UnsafeCachePlugin` and
  264. * `getInnerRequest`, so the char-code form is meaningfully faster than the
  265. * equivalent regex test: no regex state machine, no string object churn.
  266. * @param {string} request request string
  267. * @returns {boolean} true if request is relative
  268. */
  269. const isRelativeRequest = (request) => {
  270. const len = request.length;
  271. if (len === 0 || request.charCodeAt(0) !== CHAR_DOT) return false;
  272. if (len === 1) return true; // "."
  273. const c1 = request.charCodeAt(1);
  274. if (c1 === CHAR_SLASH) return true; // "./..."
  275. if (c1 !== CHAR_DOT) return false; // ".x..."
  276. if (len === 2) return true; // ".."
  277. return request.charCodeAt(2) === CHAR_SLASH; // "../..."
  278. };
  279. /**
  280. * Check if childPath is a subdirectory of parentPath.
  281. *
  282. * Called from `TsconfigPathsPlugin._selectPathsDataForContext` inside a loop
  283. * over every tsconfig-paths context on every resolve, so it's worth keeping
  284. * cheap. Compared to the previous `startsWith(normalize(parent + "/"))`
  285. * version, this: checks the last char with `charCodeAt` instead of two
  286. * `endsWith` calls; and skips `normalize()` entirely in the common case
  287. * (parent has no trailing separator), since all we really need is the same
  288. * anchoring effect — a cheap `startsWith` plus a separator char check on the
  289. * byte immediately after `parentPath.length`.
  290. * @param {string} parentPath parent directory path
  291. * @param {string} childPath child path to check
  292. * @returns {boolean} true if childPath is under parentPath
  293. */
  294. const isSubPath = (parentPath, childPath) => {
  295. const parentLen = parentPath.length;
  296. if (parentLen === 0) {
  297. // Match the old `normalize("" + "/") === "/"` fallback: an empty
  298. // parent only "contains" a child that starts with a forward slash.
  299. return childPath.length > 0 && childPath.charCodeAt(0) === CHAR_SLASH;
  300. }
  301. const lastChar = parentPath.charCodeAt(parentLen - 1);
  302. if (lastChar === CHAR_SLASH || lastChar === CHAR_BACKSLASH) {
  303. // Parent already ends with a separator — a plain prefix test is enough.
  304. return childPath.startsWith(parentPath);
  305. }
  306. if (childPath.length <= parentLen) return false;
  307. if (!childPath.startsWith(parentPath)) return false;
  308. // Must be followed by a separator so "/app" doesn't match "/app-other".
  309. const nextChar = childPath.charCodeAt(parentLen);
  310. return nextChar === CHAR_SLASH || nextChar === CHAR_BACKSLASH;
  311. };
  312. module.exports.PathType = PathType;
  313. module.exports.createCachedBasename = createCachedBasename;
  314. module.exports.createCachedDirname = createCachedDirname;
  315. module.exports.createCachedJoin = createCachedJoin;
  316. module.exports.deprecatedInvalidSegmentRegEx = deprecatedInvalidSegmentRegEx;
  317. module.exports.dirname = dirname;
  318. module.exports.getType = getType;
  319. module.exports.invalidSegmentRegEx = invalidSegmentRegEx;
  320. module.exports.isRelativeRequest = isRelativeRequest;
  321. module.exports.isSubPath = isSubPath;
  322. module.exports.join = join;
  323. module.exports.normalize = normalize;