globUtils.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Natsu @xiaoxiaojx
  4. */
  5. "use strict";
  6. const path = require("path");
  7. const { join: joinPath } = require("./fs");
  8. const {
  9. ABSOLUTE_PATH_REGEXP,
  10. WINDOWS_PATH_SEPARATOR_REGEXP
  11. } = require("./identifier");
  12. const memoize = require("./memoize");
  13. // watchpack's glob-to-regexp core, loaded only when a glob is translated
  14. const getGlobToRegExpSource = memoize(
  15. () =>
  16. /** @type {{ util: { globToRegExp: (glob: string) => string } }} */ (
  17. /** @type {unknown} */ (require("watchpack"))
  18. ).util.globToRegExp
  19. );
  20. /** @typedef {{ caseSensitive?: boolean, requireLiteralLeadingDot?: boolean }} GlobMatchOptions */
  21. /**
  22. * @param {string} s string
  23. * @returns {string} escaped glob pattern
  24. */
  25. const escapeGlobPattern = (s) => {
  26. let result = "";
  27. for (const c of s) {
  28. result +=
  29. c === "*" || c === "?" || c === "[" || c === "]" || c === "{" || c === "}"
  30. ? `\\${c}`
  31. : c;
  32. }
  33. return result;
  34. };
  35. /**
  36. * @param {string} s string
  37. * @returns {string} normalized path separators for glob patterns
  38. */
  39. const normalizePathSeparators = (s) => {
  40. let result = "";
  41. const chars = [...s];
  42. for (let i = 0; i < chars.length; i++) {
  43. const c = chars[i];
  44. if (c === "\\") {
  45. const next = chars[i + 1];
  46. if (
  47. next === "*" ||
  48. next === "?" ||
  49. next === "[" ||
  50. next === "]" ||
  51. next === "{" ||
  52. next === "}"
  53. ) {
  54. result += c;
  55. } else {
  56. result += "/";
  57. }
  58. } else {
  59. result += c;
  60. }
  61. }
  62. return result;
  63. };
  64. /**
  65. * @param {string} s string
  66. * @returns {string} normalized path separators for filesystem paths
  67. */
  68. const normalizePathSeparatorsForPath = (s) =>
  69. // the scan is cheaper than the replace, and most paths have no `\`
  70. s.includes("\\") ? s.replace(WINDOWS_PATH_SEPARATOR_REGEXP, "/") : s;
  71. /**
  72. * @param {string} s string
  73. * @returns {string} unescaped glob path
  74. */
  75. const unescapeGlobPath = (s) => {
  76. let result = "";
  77. const chars = [...s];
  78. for (let i = 0; i < chars.length; i++) {
  79. const c = chars[i];
  80. if (c === "\\") {
  81. const next = chars[i + 1];
  82. if (
  83. next === "*" ||
  84. next === "?" ||
  85. next === "[" ||
  86. next === "]" ||
  87. next === "{" ||
  88. next === "}"
  89. ) {
  90. result += next;
  91. i++;
  92. } else {
  93. result += c;
  94. }
  95. } else {
  96. result += c;
  97. }
  98. }
  99. return result;
  100. };
  101. /**
  102. * @param {string} c character
  103. * @returns {boolean} is glob metacharacter
  104. */
  105. const isGlobMetacharacter = (c) =>
  106. c === "*" || c === "?" || c === "[" || c === "{";
  107. /**
  108. * @param {string} pattern pattern
  109. * @returns {number} end index of base directory
  110. */
  111. const globBaseDirEnd = (pattern) => {
  112. let escaped = false;
  113. let idx = pattern.length;
  114. for (let byteIdx = 0; byteIdx < pattern.length; byteIdx++) {
  115. const c = pattern[byteIdx];
  116. if (escaped) {
  117. escaped = false;
  118. continue;
  119. }
  120. if (c === "\\") {
  121. escaped = true;
  122. continue;
  123. }
  124. if (isGlobMetacharacter(c)) {
  125. idx = byteIdx;
  126. break;
  127. }
  128. }
  129. const slashIdx = pattern.lastIndexOf("/", idx - 1);
  130. return slashIdx === -1 ? 0 : slashIdx + 1;
  131. };
  132. /**
  133. * @param {string} pattern pattern
  134. * @returns {string} base directory
  135. */
  136. const extractGlobBaseDir = (pattern) => {
  137. const end = globBaseDirEnd(pattern);
  138. return end === 0 ? "./" : pattern.slice(0, end);
  139. };
  140. /**
  141. * @param {string} c character
  142. * @returns {string} regexp-escaped character
  143. */
  144. const quoteRegExpChar = (c) => (/[$()*+\-./?[\\\]^{|}]/.test(c) ? `\\${c}` : c);
  145. /**
  146. * Skips a character class: a `]` right after the `[`, or after its `!`/`^`, is
  147. * a member rather than the terminator.
  148. * @param {string} str string
  149. * @param {number} start index of the `[`
  150. * @returns {number} index of the closing `]`, or `start` when unterminated
  151. */
  152. const skipCharacterClass = (str, start) => {
  153. let i = start + 1;
  154. if (str[i] === "!" || str[i] === "^") i++;
  155. if (str[i] === "]") i++;
  156. while (i < str.length && str[i] !== "]") i += str[i] === "\\" ? 2 : 1;
  157. return i < str.length ? i : start;
  158. };
  159. /**
  160. * Expands `{a,b}` alternations (escape- and class-aware, nested) into
  161. * brace-free globs; unmatched braces are kept as literal characters. A `,`
  162. * inside a class is a member, as `braces` and picomatch read it, so
  163. * `import.meta.glob` patterns mean here what they mean to fast-glob.
  164. * @param {string} glob glob
  165. * @returns {string[]} brace-free globs
  166. */
  167. const expandGlobBraces = (glob) => {
  168. let braceStart = -1;
  169. let braceEnd = -1;
  170. let depth = 0;
  171. for (let i = 0; i < glob.length; i++) {
  172. const c = glob[i];
  173. if (c === "\\") {
  174. i++;
  175. } else if (c === "[") {
  176. i = skipCharacterClass(glob, i);
  177. } else if (c === "{") {
  178. if (depth === 0) braceStart = i;
  179. depth++;
  180. } else if (c === "}") {
  181. if (depth === 0) {
  182. return expandGlobBraces(`${glob.slice(0, i)}\\}${glob.slice(i + 1)}`);
  183. }
  184. if (--depth === 0) {
  185. braceEnd = i;
  186. break;
  187. }
  188. }
  189. }
  190. if (braceStart === -1) return [glob];
  191. if (braceEnd === -1) {
  192. return expandGlobBraces(
  193. `${glob.slice(0, braceStart)}\\{${glob.slice(braceStart + 1)}`
  194. );
  195. }
  196. const prefix = glob.slice(0, braceStart);
  197. const suffix = glob.slice(braceEnd + 1);
  198. const inner = glob.slice(braceStart + 1, braceEnd);
  199. /** @type {string[]} */
  200. const alternatives = [];
  201. let altStart = 0;
  202. let altDepth = 0;
  203. for (let i = 0; i < inner.length; i++) {
  204. const c = inner[i];
  205. if (c === "\\") {
  206. i++;
  207. } else if (c === "[") {
  208. i = skipCharacterClass(inner, i);
  209. } else if (c === "{") {
  210. altDepth++;
  211. } else if (c === "}") {
  212. altDepth--;
  213. } else if (c === "," && altDepth === 0) {
  214. alternatives.push(inner.slice(altStart, i));
  215. altStart = i + 1;
  216. }
  217. }
  218. alternatives.push(inner.slice(altStart));
  219. /** @type {string[]} */
  220. const result = [];
  221. for (const alt of alternatives) {
  222. for (const expanded of expandGlobBraces(`${prefix}${alt}${suffix}`)) {
  223. result.push(expanded);
  224. }
  225. }
  226. return result;
  227. };
  228. /**
  229. * watchpack compiles `?` to a bare `.` (its only unescaped dot in runs
  230. * without classes); restrict it to a single non-separator character.
  231. * @param {string} source regexp source
  232. * @returns {string} fixed regexp source
  233. */
  234. const fixupGlobRegExpSource = (source) =>
  235. source.replace(/\\[\s\S]|\./g, (m) => (m === "." ? "[^/]" : m));
  236. /**
  237. * Converts a brace-free glob into a regexp source. Escapes and character
  238. * classes are translated here (watchpack mishandles `?`/`!` inside classes);
  239. * remaining runs go through watchpack's glob-to-regexp core.
  240. * @param {string} glob brace-free glob
  241. * @returns {string} regexp source without anchors
  242. */
  243. const braceFreeGlobToRegExpSource = (glob) => {
  244. let source = "";
  245. let runStart = 0;
  246. /**
  247. * @param {number} end run end index
  248. */
  249. const flushRun = (end) => {
  250. if (runStart < end) {
  251. source += fixupGlobRegExpSource(
  252. getGlobToRegExpSource()(glob.slice(runStart, end))
  253. );
  254. }
  255. };
  256. for (let i = 0; i < glob.length; i++) {
  257. const c = glob[i];
  258. if (c === "\\") {
  259. flushRun(i);
  260. source += quoteRegExpChar(i + 1 < glob.length ? glob[i + 1] : "\\");
  261. i++;
  262. runStart = i + 1;
  263. } else if (c === "[") {
  264. const negated = glob[i + 1] === "!" || glob[i + 1] === "^";
  265. const bodyStart = i + (negated ? 2 : 1);
  266. const end = skipCharacterClass(glob, i);
  267. flushRun(i);
  268. if (end === i) {
  269. // unterminated class: literal `[`
  270. source += "\\[";
  271. runStart = i + 1;
  272. continue;
  273. }
  274. let body = "";
  275. for (let j = bodyStart; j < end; j++) {
  276. const bc = glob[j];
  277. if (bc === "\\" && j + 1 < end) {
  278. body += quoteRegExpChar(glob[j + 1]);
  279. j++;
  280. } else if (bc === "]" || bc === "\\" || bc === "[") {
  281. body += `\\${bc}`;
  282. } else {
  283. body += bc;
  284. }
  285. }
  286. // negated glob classes must not cross a path separator
  287. source += `[${negated ? "^/" : ""}${body}]`;
  288. i = end;
  289. runStart = end + 1;
  290. }
  291. }
  292. flushRun(glob.length);
  293. return source;
  294. };
  295. /**
  296. * @param {string} pattern glob pattern
  297. * @param {boolean} caseSensitive case sensitive
  298. * @returns {RegExp | null} anchored regexp, null for un-compilable patterns
  299. */
  300. const globToRegExp = (pattern, caseSensitive) => {
  301. const sources = expandGlobBraces(pattern).map(braceFreeGlobToRegExpSource);
  302. const source = sources.length === 1 ? sources[0] : `(?:${sources.join("|")})`;
  303. try {
  304. return new RegExp(`^${source}$`, caseSensitive ? "" : "i");
  305. } catch (_err) {
  306. return null;
  307. }
  308. };
  309. /**
  310. * @param {string} pattern pattern
  311. * @param {string} str path
  312. * @param {GlobMatchOptions=} options options
  313. * @returns {boolean} matches
  314. */
  315. const globMatchWithOptions = (pattern, str, options = {}) => {
  316. const regexp = globToRegExp(pattern, options.caseSensitive !== false);
  317. return regexp !== null && regexp.test(str);
  318. };
  319. // a segment starting with a dot is only reachable by a pattern spelling it out
  320. const NON_DOT_PREFIX = "(?!\\.)";
  321. const NON_DOT_SEGMENT = "(?!\\.)[^/]+";
  322. const CONSECUTIVE_STARS_REGEXP = /\*{2,}/g;
  323. // a `//` or `/./` the path has to lose before it can be matched
  324. const UNNORMALIZED_PATH_REGEXP = /\/\/|(?:^|\/)\.(?:\/|$)/;
  325. /**
  326. * Drops the `.` and empty segments of a pattern or a path — they always name
  327. * the same file. The first and last segment stay, so a leading `./`, a leading
  328. * `/` and a trailing `/` keep their meaning. A `..` is left alone: resolving it
  329. * textually is what `path.matchesGlob` does, and it is wrong through a symlink,
  330. * where `dir/link/../b` is not `dir/b`.
  331. * @param {string[]} parts segments
  332. * @returns {string[]} normalized segments
  333. */
  334. const optimizeGlobSegments = (parts) => {
  335. let changed = false;
  336. do {
  337. changed = false;
  338. for (let i = 1; i < parts.length - 1; i++) {
  339. if (parts[i] === "." || parts[i] === "") {
  340. parts.splice(i, 1);
  341. i--;
  342. changed = true;
  343. }
  344. }
  345. if (
  346. parts[0] === "." &&
  347. parts.length === 2 &&
  348. (parts[1] === "." || parts[1] === "")
  349. ) {
  350. parts.pop();
  351. changed = true;
  352. }
  353. } while (changed);
  354. return parts.length === 0 ? [""] : parts;
  355. };
  356. /**
  357. * @param {string} segment segment
  358. * @param {number} start index of the `(`
  359. * @returns {number} index of the matching `)`, -1 when unmatched
  360. */
  361. const findExtendedGlobEnd = (segment, start) => {
  362. let depth = 0;
  363. for (let i = start; i < segment.length; i++) {
  364. const c = segment[i];
  365. if (c === "[") {
  366. i = skipCharacterClass(segment, i);
  367. } else if (c === "(") {
  368. depth++;
  369. } else if (c === ")" && --depth === 0) {
  370. return i;
  371. }
  372. }
  373. return -1;
  374. };
  375. /**
  376. * @param {string} inner group body
  377. * @returns {string[]} top-level `|` alternatives
  378. */
  379. const splitExtendedGlobAlternatives = (inner) => {
  380. /** @type {string[]} */
  381. const alternatives = [];
  382. let start = 0;
  383. let depth = 0;
  384. for (let i = 0; i < inner.length; i++) {
  385. const c = inner[i];
  386. if (c === "[") {
  387. i = skipCharacterClass(inner, i);
  388. } else if (c === "(") {
  389. depth++;
  390. } else if (c === ")") {
  391. depth--;
  392. } else if (c === "|" && depth === 0) {
  393. alternatives.push(inner.slice(start, i));
  394. start = i + 1;
  395. }
  396. }
  397. alternatives.push(inner.slice(start));
  398. return alternatives;
  399. };
  400. /**
  401. * @param {string} segment segment
  402. * @returns {number} index of the first extended glob prefix, -1 when there is none
  403. */
  404. const findExtendedGlobStart = (segment) => {
  405. for (let i = 0; i < segment.length - 1; i++) {
  406. const c = segment[i];
  407. if (c === "[") {
  408. i = skipCharacterClass(segment, i);
  409. } else if (
  410. segment[i + 1] === "(" &&
  411. (c === "?" || c === "*" || c === "+" || c === "@" || c === "!")
  412. ) {
  413. return i;
  414. }
  415. }
  416. return -1;
  417. };
  418. /**
  419. * Whether the segment can match a leading dot, which it does when the dot is
  420. * literal in the pattern — after an extended glob that matched nothing
  421. * included, as `*(a).b` matching `.b`.
  422. * @param {string} segment segment without separators
  423. * @returns {boolean} segment may match a dot segment
  424. */
  425. const segmentGlobMatchesLeadingDot = (segment) => {
  426. if (segment.startsWith(".")) return true;
  427. if (findExtendedGlobStart(segment) !== 0) return false;
  428. const end = findExtendedGlobEnd(segment, 1);
  429. if (end === -1) return false;
  430. const type = segment[0];
  431. const emptyMatch =
  432. type === "*" ||
  433. type === "?" ||
  434. ((type === "@" || type === "+") &&
  435. splitExtendedGlobAlternatives(segment.slice(2, end)).includes(""));
  436. return emptyMatch && segmentGlobMatchesLeadingDot(segment.slice(end + 1));
  437. };
  438. /**
  439. * Whether the segment matches an empty path segment, which only the extended
  440. * globs that quantify their group do — `!(x)/a` matches `/a`, `@(a|)/a` does
  441. * not, and neither does a bare `*`.
  442. * @param {string} segment segment without separators
  443. * @returns {boolean} segment may match nothing
  444. */
  445. const segmentGlobMatchesEmpty = (segment) => {
  446. if (segment === "") return true;
  447. if (findExtendedGlobStart(segment) !== 0) return false;
  448. const type = segment[0];
  449. if (type !== "!" && type !== "*" && type !== "?") return false;
  450. const end = findExtendedGlobEnd(segment, 1);
  451. return end !== -1 && segmentGlobMatchesEmpty(segment.slice(end + 1));
  452. };
  453. /**
  454. * Compiles one path segment, extended globs (`+(a|b)`, `!(a)`, …) included.
  455. * `!(a)` is "anything but `a`", so it is a lookahead over what the rest of the
  456. * segment matches, not over `a` alone.
  457. * @param {string} segment segment without separators
  458. * @returns {string} regexp source
  459. */
  460. const segmentGlobToRegExpSource = (segment) => {
  461. const start = findExtendedGlobStart(segment);
  462. const end = start === -1 ? -1 : findExtendedGlobEnd(segment, start + 1);
  463. if (end === -1) {
  464. // no extended glob here, and `**` within a segment is a plain `*`
  465. return braceFreeGlobToRegExpSource(
  466. segment.replace(CONSECUTIVE_STARS_REGEXP, "*")
  467. );
  468. }
  469. const prefix = segmentGlobToRegExpSource(segment.slice(0, start));
  470. const rest = segmentGlobToRegExpSource(segment.slice(end + 1));
  471. const group = `(?:${splitExtendedGlobAlternatives(
  472. segment.slice(start + 2, end)
  473. )
  474. .map(segmentGlobToRegExpSource)
  475. .join("|")})`;
  476. switch (segment[start]) {
  477. case "?":
  478. return `${prefix}${group}?${rest}`;
  479. case "*":
  480. return `${prefix}${group}*${rest}`;
  481. case "+":
  482. return `${prefix}${group}+${rest}`;
  483. case "@":
  484. return `${prefix}${group}${rest}`;
  485. default:
  486. return `${prefix}(?!${group}${rest}(?:/|$))[^/]*?${rest}`;
  487. }
  488. };
  489. /**
  490. * Compiles one brace-free `/`-separated glob to a regexp source with
  491. * `path.matchesGlob` semantics: `*`, `?` and classes stay within a segment,
  492. * a whole-segment `**` spans segments, and neither crosses a dot segment.
  493. * @param {string} glob brace-free glob
  494. * @returns {string} regexp source without anchors
  495. */
  496. const braceFreeGlobToPathRegExpSource = (glob) => {
  497. const segments = optimizeGlobSegments(glob.split("/"));
  498. let source = "";
  499. let needSeparator = false;
  500. for (let i = 0; i < segments.length; i++) {
  501. const segment = segments[i];
  502. const isLast = i === segments.length - 1;
  503. if (segment === "") {
  504. // leading separator (absolute) and trailing one (directory)
  505. source += "/";
  506. continue;
  507. }
  508. if (segment === "**") {
  509. source += isLast
  510. ? `${needSeparator ? "/" : ""}(?:${NON_DOT_SEGMENT}(?:/${NON_DOT_SEGMENT})*)?`
  511. : needSeparator
  512. ? `(?:/${NON_DOT_SEGMENT})*`
  513. : `/?(?:${NON_DOT_SEGMENT}/)*`;
  514. needSeparator = !isLast && needSeparator;
  515. continue;
  516. }
  517. if (needSeparator) source += "/";
  518. // a segment matches at least one character
  519. if (!segmentGlobMatchesEmpty(segment)) source += "(?=[^/])";
  520. if (!segmentGlobMatchesLeadingDot(segment)) source += NON_DOT_PREFIX;
  521. source += segmentGlobToRegExpSource(segment);
  522. needSeparator = true;
  523. }
  524. // a pattern without a trailing separator still matches a directory path
  525. return glob.endsWith("/") ? source : `${source}/?`;
  526. };
  527. /**
  528. * Compiles a glob into a matcher over OS-independent paths: `\` is a path
  529. * separator in both the pattern and the tested path (never an escape, so a
  530. * pattern built with `path.resolve` works everywhere; use `[*]` to match a
  531. * literal `*`), and a relative pattern matches at any depth. Matching follows
  532. * `path.matchesGlob`, except where its minimatch shows through — see the
  533. * divergences listed above the corpus in `test/globUtils.unittest.js`.
  534. * @param {string} pattern glob pattern
  535. * @param {GlobMatchOptions=} options options
  536. * @returns {((str: string) => boolean) | null} matcher, null for un-compilable patterns
  537. */
  538. const createPathGlobMatcher = (pattern, options = {}) => {
  539. let normalizedPattern = normalizePathSeparatorsForPath(pattern);
  540. while (normalizedPattern.startsWith("./")) {
  541. normalizedPattern = normalizedPattern.slice(2);
  542. }
  543. if (
  544. !ABSOLUTE_PATH_REGEXP.test(normalizedPattern) &&
  545. !normalizedPattern.startsWith("**/")
  546. ) {
  547. normalizedPattern = `**/${normalizedPattern}`;
  548. }
  549. const sources = expandGlobBraces(normalizedPattern).map(
  550. braceFreeGlobToPathRegExpSource
  551. );
  552. const source = sources.length === 1 ? sources[0] : `(?:${sources.join("|")})`;
  553. /** @type {RegExp} */
  554. let regexp;
  555. try {
  556. regexp = new RegExp(
  557. `^${source}$`,
  558. options.caseSensitive === false ? "i" : ""
  559. );
  560. } catch (_err) {
  561. return null;
  562. }
  563. return (str) => {
  564. const normalizedPath = normalizePathSeparatorsForPath(str);
  565. return regexp.test(
  566. UNNORMALIZED_PATH_REGEXP.test(normalizedPath)
  567. ? optimizeGlobSegments(normalizedPath.split("/")).join("/")
  568. : normalizedPath
  569. );
  570. };
  571. };
  572. /**
  573. * @param {string} path path
  574. * @param {string} baseDir base directory
  575. * @returns {boolean} has dot component
  576. */
  577. const pathHasDotComponent = (path, baseDir) => {
  578. const relative = path.startsWith(baseDir) ? path.slice(baseDir.length) : path;
  579. for (const segment of relative.split("/").filter(Boolean)) {
  580. if (segment.startsWith(".")) return true;
  581. }
  582. return false;
  583. };
  584. /**
  585. * @param {string[]} patterns pattern segments
  586. * @param {string[]} paths path segments
  587. * @param {GlobMatchOptions} options options
  588. * @returns {boolean} matches
  589. */
  590. const matchesExplicitDotSegments = (patterns, paths, options) => {
  591. if (patterns.length === 0) return paths.length === 0;
  592. if (paths.length === 0) return false;
  593. const [patternHead, ...patternRest] = patterns;
  594. const [pathHead, ...pathRest] = paths;
  595. if (patternHead === "**") {
  596. return (
  597. matchesExplicitDotSegments(patternRest, paths, options) ||
  598. (!pathHead.startsWith(".") &&
  599. matchesExplicitDotSegments(patterns, pathRest, options))
  600. );
  601. }
  602. if (pathHead.startsWith(".") && !patternHead.startsWith(".")) {
  603. return false;
  604. }
  605. return (
  606. globMatchWithOptions(patternHead, pathHead, options) &&
  607. matchesExplicitDotSegments(patternRest, pathRest, options)
  608. );
  609. };
  610. /**
  611. * @param {string} pattern pattern
  612. * @param {string} baseDir base directory
  613. * @param {string} path path
  614. * @param {GlobMatchOptions} options options
  615. * @returns {boolean} has explicit dot
  616. */
  617. const patternHasExplicitDotFor = (pattern, baseDir, path, options) => {
  618. const escapedBaseDir = escapeGlobPattern(baseDir);
  619. const patternSuffix =
  620. (pattern.startsWith(baseDir) && pattern.slice(baseDir.length)) ||
  621. (pattern.startsWith(escapedBaseDir) &&
  622. pattern.slice(escapedBaseDir.length)) ||
  623. pattern;
  624. const relative = path.startsWith(baseDir) ? path.slice(baseDir.length) : path;
  625. const patternSegments = patternSuffix.split("/").filter(Boolean);
  626. const pathSegments = relative.split("/").filter(Boolean);
  627. return matchesExplicitDotSegments(patternSegments, pathSegments, options);
  628. };
  629. /**
  630. * @param {string} pattern pattern
  631. * @param {string} path path
  632. * @param {string} baseDir base directory
  633. * @param {GlobMatchOptions=} options options
  634. * @returns {boolean} matches
  635. */
  636. const globMatchWithExplicitDot = (pattern, path, baseDir, options = {}) => {
  637. const normalizedPattern = normalizePathSeparators(pattern);
  638. const normalizedPath = normalizePathSeparatorsForPath(path);
  639. const normalizedBaseDir = normalizePathSeparatorsForPath(baseDir);
  640. return globMatchNormalizedWithExplicitDot(
  641. normalizedPattern,
  642. normalizedPath,
  643. normalizedBaseDir,
  644. options
  645. );
  646. };
  647. /**
  648. * @param {string} normalizedPattern pattern
  649. * @param {string} normalizedPath path
  650. * @param {string} normalizedBaseDir base directory
  651. * @param {GlobMatchOptions=} options options
  652. * @returns {boolean} matches
  653. */
  654. const globMatchNormalizedWithExplicitDot = (
  655. normalizedPattern,
  656. normalizedPath,
  657. normalizedBaseDir,
  658. options = {}
  659. ) => {
  660. const requireLiteralLeadingDot = options.requireLiteralLeadingDot !== false;
  661. if (
  662. requireLiteralLeadingDot &&
  663. pathHasDotComponent(normalizedPath, normalizedBaseDir) &&
  664. !patternHasExplicitDotFor(
  665. normalizedPattern,
  666. normalizedBaseDir,
  667. normalizedPath,
  668. options
  669. )
  670. ) {
  671. return false;
  672. }
  673. return globMatchWithOptions(normalizedPattern, normalizedPath, options);
  674. };
  675. /**
  676. * @param {string} base base path
  677. * @param {string} subPath sub path
  678. * @returns {string} joined path
  679. */
  680. const joinImportMetaGlobPath = (base, subPath) => {
  681. let normalizedSubPath = normalizePathSeparators(subPath);
  682. if (normalizedSubPath.startsWith("./")) {
  683. normalizedSubPath = normalizedSubPath.slice(2);
  684. }
  685. if (base.endsWith("/")) {
  686. return normalizePathSeparators(`${base}${normalizedSubPath}`);
  687. }
  688. return normalizePathSeparators(`${base}/${normalizedSubPath}`);
  689. };
  690. /**
  691. * @param {string} base base path
  692. * @param {string} subPath sub path
  693. * @returns {string} joined filesystem path
  694. */
  695. const joinImportMetaGlobFsPath = (base, subPath) =>
  696. normalizePathSeparatorsForPath(joinPath(undefined, base, subPath));
  697. /**
  698. * @param {string} context context
  699. * @param {string} compilerContext compiler context
  700. * @param {string} globPath path
  701. * @returns {[string, string]} base and path parts
  702. */
  703. const importMetaGlobPathParts = (context, compilerContext, globPath) => {
  704. if (globPath.startsWith("/")) {
  705. return [compilerContext, globPath.slice(1)];
  706. }
  707. return [context, globPath];
  708. };
  709. /**
  710. * @typedef {object} ResolvedContextModuleGlobPattern
  711. * @property {string} absolutePattern
  712. * @property {string} base
  713. * @property {string} absoluteBase
  714. * @property {boolean} negative
  715. */
  716. /**
  717. * @param {string} pattern pattern
  718. * @param {string} context context
  719. * @param {string} commonBase common base
  720. * @returns {ResolvedContextModuleGlobPattern} resolved pattern
  721. */
  722. const resolveContextModuleGlobPattern = (pattern, context, commonBase) => {
  723. let negative = false;
  724. let normalizedPattern = pattern;
  725. if (normalizedPattern.startsWith("!")) {
  726. negative = true;
  727. normalizedPattern = normalizedPattern.slice(1);
  728. }
  729. normalizedPattern = normalizePathSeparators(normalizedPattern);
  730. /** @type {string} */
  731. let base;
  732. /** @type {string} */
  733. let patternToJoin;
  734. if (normalizedPattern.startsWith("/")) {
  735. base = inferGlobRootContext(
  736. commonBase,
  737. extractGlobBaseDir(normalizedPattern.slice(1))
  738. );
  739. patternToJoin = normalizedPattern.slice(1);
  740. } else {
  741. base = context || commonBase;
  742. patternToJoin = normalizedPattern;
  743. }
  744. base = normalizePathSeparatorsForPath(base);
  745. const escapedBase = escapeGlobPattern(base);
  746. const absolutePattern = normalizePathSeparators(
  747. path.posix.normalize(path.posix.join(escapedBase, patternToJoin))
  748. );
  749. const patternBase = extractGlobBaseDir(normalizedPattern);
  750. const absoluteBase = unescapeGlobPath(extractGlobBaseDir(absolutePattern));
  751. return {
  752. absolutePattern,
  753. base: patternBase,
  754. absoluteBase,
  755. negative
  756. };
  757. };
  758. /**
  759. * @param {string} commonBase common base
  760. * @param {string} patternBase pattern base
  761. * @returns {string} inferred root context
  762. */
  763. const inferGlobRootContext = (commonBase, patternBase) => {
  764. let normalizedCommonBase = normalizePathSeparatorsForPath(commonBase);
  765. if (!normalizedCommonBase.endsWith("/")) {
  766. normalizedCommonBase += "/";
  767. }
  768. const trimmedPatternBase = patternBase.replace(/^\/+/, "");
  769. let matchedLen = 0;
  770. const indices = [];
  771. for (let i = 0; i <= trimmedPatternBase.length; i++) {
  772. indices.push(i);
  773. }
  774. for (const idx of indices) {
  775. if (
  776. !trimmedPatternBase.slice(0, idx).endsWith("/") &&
  777. idx !== trimmedPatternBase.length
  778. ) {
  779. continue;
  780. }
  781. if (normalizedCommonBase.endsWith(trimmedPatternBase.slice(0, idx))) {
  782. matchedLen = idx;
  783. }
  784. }
  785. return normalizedCommonBase.slice(
  786. 0,
  787. normalizedCommonBase.length - matchedLen
  788. );
  789. };
  790. /**
  791. * @param {ResolvedContextModuleGlobPattern[]} patterns patterns
  792. * @param {string} fallback fallback
  793. * @returns {string} common base directory
  794. */
  795. const commonGlobBaseDir = (patterns, fallback) => {
  796. const positivePatterns = patterns.filter((p) => !p.negative);
  797. if (positivePatterns.length === 0) return fallback;
  798. let commonBase = positivePatterns[0].absoluteBase;
  799. for (const pattern of positivePatterns.slice(1)) {
  800. const base = pattern.absoluteBase;
  801. while (!base.startsWith(commonBase)) {
  802. const parent = path.posix.dirname(commonBase);
  803. if (parent === commonBase) return fallback;
  804. commonBase = parent.endsWith("/") ? parent : `${parent}/`;
  805. }
  806. }
  807. return commonBase.endsWith("/")
  808. ? normalizePathSeparatorsForPath(commonBase)
  809. : normalizePathSeparatorsForPath(`${commonBase}/`);
  810. };
  811. /**
  812. * @param {ResolvedContextModuleGlobPattern} pattern pattern
  813. * @param {string} normalizedPath path
  814. * @param {boolean} exhaustive exhaustive
  815. * @param {boolean} caseSensitive case sensitive
  816. * @returns {boolean} matches
  817. */
  818. const globPatternMatches = (
  819. pattern,
  820. normalizedPath,
  821. exhaustive,
  822. caseSensitive
  823. ) =>
  824. globMatchNormalizedWithExplicitDot(
  825. pattern.absolutePattern,
  826. normalizedPath,
  827. pattern.absoluteBase,
  828. { requireLiteralLeadingDot: !exhaustive, caseSensitive }
  829. );
  830. /**
  831. * @param {ResolvedContextModuleGlobPattern[]} patterns patterns
  832. * @param {string} filePath path
  833. * @param {boolean} exhaustive exhaustive
  834. * @param {boolean=} caseSensitive case sensitive (default true)
  835. * @returns {string | undefined} user request
  836. */
  837. const globUserRequest = (
  838. patterns,
  839. filePath,
  840. exhaustive,
  841. caseSensitive = true
  842. ) => {
  843. const normalizedPath = normalizePathSeparatorsForPath(filePath);
  844. const matched = patterns
  845. .filter((pattern) => !pattern.negative)
  846. .find((pattern) =>
  847. globPatternMatches(pattern, normalizedPath, exhaustive, caseSensitive)
  848. );
  849. if (!matched) return;
  850. if (
  851. patterns
  852. .filter((pattern) => pattern.negative)
  853. .some((pattern) =>
  854. globPatternMatches(pattern, normalizedPath, exhaustive, caseSensitive)
  855. )
  856. ) {
  857. return;
  858. }
  859. const suffix = (
  860. normalizedPath.startsWith(matched.absoluteBase)
  861. ? normalizedPath.slice(matched.absoluteBase.length)
  862. : normalizedPath
  863. ).replace(/^\/+/, "");
  864. return joinImportMetaGlobPath(matched.base, suffix);
  865. };
  866. /**
  867. * @param {ResolvedContextModuleGlobPattern[]} patterns patterns
  868. * @param {string} commonBaseDir common base
  869. * @returns {boolean} recursive
  870. */
  871. const globPatternsAreRecursive = (patterns, commonBaseDir) => {
  872. const normalizedCommonBase = normalizePathSeparatorsForPath(commonBaseDir);
  873. const normalizedBase = normalizedCommonBase.endsWith("/")
  874. ? normalizedCommonBase
  875. : `${normalizedCommonBase}/`;
  876. return patterns
  877. .filter((pattern) => !pattern.negative)
  878. .some((pattern) => {
  879. const unescapedPattern = unescapeGlobPath(pattern.absolutePattern);
  880. if (unescapedPattern.includes("**")) return true;
  881. const suffix = unescapedPattern.startsWith(normalizedBase)
  882. ? unescapedPattern.slice(normalizedBase.length)
  883. : unescapedPattern.startsWith(normalizedCommonBase)
  884. ? unescapedPattern.slice(normalizedCommonBase.length)
  885. : unescapedPattern;
  886. return suffix.includes("/");
  887. });
  888. };
  889. /**
  890. * @param {string} dirname directory name
  891. * @returns {boolean} skipped in non-exhaustive mode
  892. */
  893. const isNonExhaustiveImportMetaGlobSkippedDir = (dirname) =>
  894. dirname === "node_modules" || dirname.startsWith(".");
  895. /**
  896. * A dot/node_modules directory is still traversed in non-exhaustive mode when
  897. * it lies within the literal base of some positive pattern — the user named it
  898. * explicitly — so combined patterns behave like that pattern used alone.
  899. * @param {ResolvedContextModuleGlobPattern[]} patterns patterns
  900. * @param {string} dirPath absolute directory path
  901. * @returns {boolean} directory is within a positive pattern's literal base
  902. */
  903. const globPatternBaseReachesDir = (patterns, dirPath) => {
  904. const normalizedDir = normalizePathSeparatorsForPath(dirPath);
  905. const dirWithSlash = normalizedDir.endsWith("/")
  906. ? normalizedDir
  907. : `${normalizedDir}/`;
  908. return patterns.some((pattern) => {
  909. if (pattern.negative) return false;
  910. const base = pattern.absoluteBase.endsWith("/")
  911. ? pattern.absoluteBase
  912. : `${pattern.absoluteBase}/`;
  913. return base.startsWith(dirWithSlash);
  914. });
  915. };
  916. module.exports = {
  917. commonGlobBaseDir,
  918. createPathGlobMatcher,
  919. escapeGlobPattern,
  920. extractGlobBaseDir,
  921. getGlobToRegExpSource,
  922. globMatchNormalizedWithExplicitDot,
  923. globMatchWithExplicitDot,
  924. globMatchWithOptions,
  925. globPatternBaseReachesDir,
  926. globPatternsAreRecursive,
  927. globUserRequest,
  928. importMetaGlobPathParts,
  929. inferGlobRootContext,
  930. isNonExhaustiveImportMetaGlobSkippedDir,
  931. joinImportMetaGlobFsPath,
  932. joinImportMetaGlobPath,
  933. normalizePathSeparators,
  934. normalizePathSeparatorsForPath,
  935. patternHasExplicitDotFor,
  936. resolveContextModuleGlobPattern,
  937. unescapeGlobPath
  938. };