1/**
2 * Converts gitignore-style rules into glob patterns for tools like tinyglobby's {@link https://superchupu.dev/tinyglobby/documentation#ignore `ignore`} option.
3 *
4 * Gitignore and glob differ (ordering, negation, escapes). This maps the common cases:
5 *
6 * - Rules with no `/` match at any depth → \`**\/name\`, \`**\/name\/\*\/`
7 * - A leading `/` anchors to the ignore file's directory (use the same `cwd` in tinyglobby) → `name`, `name/**`
8 * - A `/` in the pattern (not only leading) uses path-aware matching → `a/b`, `a/b/**`
9 * - A trailing `/` restricts to directories → adds `/**` as needed
10 * - Lines starting with `!` (negation) are returned with a `!` prefix for use in **positive** glob
11 * lists; filter them out when passing only exclusion patterns to an `ignore` array
12 *
13 * Does not implement full gitignore escaping (e.g. `\\ ` for trailing space) or `**` edge cases
14 * identical to git; escape handling can be added later if needed.
15 */
16export function gitignoreRulesToGlobs(rules: ReadonlyArray<string>): string[] {
17 const out: string[] = [];
18 for (const raw of rules) {
19 const line = raw.trim();
20 if (line.length === 0 || line.startsWith("#")) {
21 continue;
22 }
23 let negated = false;
24 let rest = line;
25 if (rest.startsWith("!")) {
26 negated = true;
27 rest = rest.slice(1).trim();
28 if (rest.length === 0 || rest.startsWith("#")) {
29 continue;
30 }
31 }
32 let dirOnly = rest.endsWith("/");
33 if (dirOnly) {
34 rest = rest.slice(0, -1);
35 }
36 rest = rest.replace(/\\/g, "/");
37 if (rest.length === 0) {
38 continue;
39 }
40 const anchored = rest.startsWith("/");
41 if (anchored) {
42 rest = rest.slice(1);
43 }
44 if (rest.length === 0) {
45 continue;
46 }
47 const hasSlash = rest.includes("/");
48 const globs = ruleBodyToGlobs(rest, { anchored, hasSlash, dirOnly });
49 for (const g of globs) {
50 out.push(negated ? `!${g}` : g);
51 }
52 }
53 return out;
54}
55
56function ruleBodyToGlobs(
57 body: string,
58 opts: { anchored: boolean; hasSlash: boolean; dirOnly: boolean },
59): string[] {
60 const { anchored, hasSlash, dirOnly } = opts;
61 if (dirOnly) {
62 if (anchored) {
63 return [`${body}/**`];
64 }
65 if (hasSlash) {
66 return [`${body}/**`];
67 }
68 return [`**/${body}/**`];
69 }
70 if (anchored) {
71 if (hasSlash) {
72 return [body, `${body}/**`];
73 }
74 return [body, `${body}/**`];
75 }
76 if (hasSlash) {
77 return [body, `${body}/**`];
78 }
79 return [`**/${body}`, `**/${body}/**`];
80}