@w5s/eslint-config-ignore 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@ import fs from "node:fs";
2
2
  import nodePath from "node:path";
3
3
  import process from "node:process";
4
4
  import parseGitignore from "parse-gitignore";
5
- import { findUp } from "find-up";
5
+ import fs$1 from "node:fs/promises";
6
6
  //#region src/meta.ts
7
7
  const meta = Object.freeze({
8
8
  name: "@w5s/eslint-config-ignore",
9
- version: "1.1.0",
9
+ version: "1.2.0",
10
10
  buildNumber: 1
11
11
  });
12
12
  //#endregion
@@ -50,15 +50,6 @@ function ignoreFileParse(input) {
50
50
  return parseGitignore.parse(input).patterns;
51
51
  }
52
52
  //#endregion
53
- //#region src/internal/ignoreFileFind.ts
54
- async function ignoreFileFind(cwd) {
55
- return (await Promise.all([
56
- findUp(nodePath.join("", ".gitignore"), { cwd }),
57
- findUp(nodePath.join("android", ".gitignore"), { cwd }),
58
- findUp(nodePath.join("ios", ".gitignore"), { cwd })
59
- ])).filter((filePath) => filePath !== void 0).map((filePath) => nodePath.relative(cwd, filePath));
60
- }
61
- //#endregion
62
53
  //#region src/internal/ignoreRuleResolve.ts
63
54
  /**
64
55
  * Resolve a raw ignore rule from a `.gitignore` file into a path
@@ -87,6 +78,114 @@ function ignoreRuleResolve(prefix, rule) {
87
78
  return negated ? `!${relativeIgnorePath}` : relativeIgnorePath;
88
79
  }
89
80
  //#endregion
81
+ //#region src/internal/ignoreFileFind.ts
82
+ const GITIGNORE_FILE = ".gitignore";
83
+ /**
84
+ * Find `.gitignore` files relevant to `rootDir`.
85
+ *
86
+ * Behavior:
87
+ * - Searches downwards from `rootDir` (BFS) for `.gitignore` files, skipping `excludeDirs`.
88
+ * - Walks ancestors from `rootDir` up to the filesystem root (and stops at a `.git` folder when `stopAtGitRoot` is true).
89
+ * - Returns relative paths to `rootDir`.
90
+ *
91
+ * @param rootDir
92
+ * @param options
93
+ */
94
+ async function ignoreFileFind(rootDir, options) {
95
+ const excludeDirs = new Set(options?.excludeDirs ?? [
96
+ "node_modules",
97
+ ".git",
98
+ "dist",
99
+ "build",
100
+ "out"
101
+ ]);
102
+ const maxDepth = options?.maxDepth ?? 8;
103
+ const stopAtGitRoot = options?.stopAtGitRoot ?? true;
104
+ const absoluteRootDir = nodePath.resolve(rootDir);
105
+ const found = /* @__PURE__ */ new Set();
106
+ const normalize = (p) => p.replaceAll("\\", "/");
107
+ function lastMatchWins(patterns, candidateRel) {
108
+ const normCandidate = normalize(candidateRel);
109
+ let lastMatch = null;
110
+ for (const p of patterns) {
111
+ const patNorm = normalize(p.startsWith("!") ? p.slice(1) : p);
112
+ if (!patNorm) continue;
113
+ if (normCandidate === patNorm || normCandidate.startsWith(patNorm + "/")) lastMatch = p;
114
+ }
115
+ return lastMatch;
116
+ }
117
+ function isIgnored(patterns, candidateRel) {
118
+ const m = lastMatchWins(patterns, candidateRel);
119
+ if (!m) return false;
120
+ return !m.startsWith("!");
121
+ }
122
+ async function collectAncestorGitignores(startDir) {
123
+ const files = [];
124
+ let dir = startDir;
125
+ while (true) {
126
+ const gi = nodePath.join(dir, GITIGNORE_FILE);
127
+ try {
128
+ const stat = await fs$1.stat(gi).catch(() => null);
129
+ if (stat && stat.isFile()) files.push(gi);
130
+ } catch {}
131
+ if (stopAtGitRoot) try {
132
+ const gitStat = await fs$1.stat(nodePath.join(dir, ".git")).catch(() => null);
133
+ if (gitStat && gitStat.isDirectory()) break;
134
+ } catch {}
135
+ const parent = nodePath.dirname(dir);
136
+ if (parent === dir) break;
137
+ dir = parent;
138
+ }
139
+ return files.reverse();
140
+ }
141
+ async function parseAndResolve(giPath) {
142
+ try {
143
+ const parsed = ignoreFileParse(String(await fs$1.readFile(giPath, "utf8")));
144
+ const prefixRel = nodePath.relative(rootDir, nodePath.dirname(giPath));
145
+ return parsed.map((p) => ignoreRuleResolve(prefixRel, p));
146
+ } catch {
147
+ return [];
148
+ }
149
+ }
150
+ const ancestorFiles = await collectAncestorGitignores(absoluteRootDir);
151
+ const initialPatterns = [];
152
+ for (const gi of ancestorFiles) {
153
+ const parsed = await parseAndResolve(gi);
154
+ if (parsed.length > 0) initialPatterns.push(...parsed);
155
+ found.add(gi);
156
+ }
157
+ const queue = [{
158
+ dir: absoluteRootDir,
159
+ depth: 0,
160
+ patterns: [...initialPatterns]
161
+ }];
162
+ while (queue.length > 0) {
163
+ const { dir: currentDir, depth, patterns } = queue.shift();
164
+ if (depth > maxDepth) continue;
165
+ let entries;
166
+ try {
167
+ entries = await fs$1.readdir(currentDir, { withFileTypes: true });
168
+ } catch {
169
+ continue;
170
+ }
171
+ const local = entries.find((e) => e.isFile() && e.name === GITIGNORE_FILE);
172
+ const combinedPatterns = local ? [...patterns, ...await parseAndResolve(nodePath.join(currentDir, GITIGNORE_FILE))] : patterns;
173
+ if (local) found.add(nodePath.join(currentDir, GITIGNORE_FILE));
174
+ for (const ent of entries) {
175
+ if (!ent.isDirectory()) continue;
176
+ if (excludeDirs.has(ent.name)) continue;
177
+ const subdir = nodePath.join(currentDir, ent.name);
178
+ if (isIgnored(combinedPatterns, nodePath.relative(rootDir, subdir))) continue;
179
+ queue.push({
180
+ dir: subdir,
181
+ depth: depth + 1,
182
+ patterns: combinedPatterns
183
+ });
184
+ }
185
+ }
186
+ return [...found].map((p) => nodePath.relative(rootDir, p));
187
+ }
188
+ //#endregion
90
189
  //#region src/eslintIgnores.ts
91
190
  /**
92
191
  * Create a new eslint configuration object
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/meta.ts","../src/defaultIgnores.ts","../src/internal/ignoreFileParse.ts","../src/internal/ignoreFileFind.ts","../src/internal/ignoreRuleResolve.ts","../src/eslintIgnores.ts"],"sourcesContent":["export const meta = Object.freeze({\n // @ts-ignore - these variables are injected at build time\n name: (typeof __PACKAGE_NAME__ === 'undefined' ? '' : __PACKAGE_NAME__) as string,\n // @ts-ignore - these variables are injected at build time\n version: (typeof __PACKAGE_VERSION__ === 'undefined' ? '' : __PACKAGE_VERSION__) as string,\n // @ts-ignore - these variables are injected at build time\n buildNumber: 1 as number, // (typeof __PACKAGE_BUILD_NUMBER__ === 'undefined' ? 0 : __PACKAGE_BUILD_NUMBER__) as number,\n});\n","export const defaultIgnores = [\n // Lock files\n '**/package-lock.json',\n '**/yarn.lock',\n '**/pnpm-lock.yaml',\n '**/bun.lockb',\n\n // Commonly ignored\n '**/output',\n '**/.output',\n '**/coverage',\n '**/temp',\n '**/.temp',\n '**/tmp',\n '**/.tmp',\n '**/.cache',\n\n // Well known extensions to ignore\n '**/*.min.*',\n '**/*.timestamp-*.mjs',\n\n // Framework specific temporary folder\n '.go/',\n '.pnpm-store/',\n '**/.vitepress/cache',\n '**/.vite-inspect',\n '**/.history',\n '**/.nuxt',\n '**/.next',\n '**/.svelte-kit',\n '**/.vercel',\n '**/.idea',\n '**/.yarn',\n '**/__snapshots__/**',\n\n // git submodules (makefile-core / makefile-ci)\n '.modules/',\n\n // AI related\n '**/.context',\n '**/.claude',\n '**/.agents',\n '**/.*/skills',\n];\n","import parseGitignore from 'parse-gitignore';\n\nexport function ignoreFileParse(input: string): Array<string> {\n return parseGitignore.parse(input).patterns;\n};\n","import nodePath from 'node:path';\nimport { findUp } from 'find-up';\n\nexport async function ignoreFileFind(cwd: string): Promise<Array<string>> {\n const files = await Promise.all(\n [\n findUp(nodePath.join('', '.gitignore'), { cwd }),\n // TODO: refactor as generic exploration\n findUp(nodePath.join('android', '.gitignore'), { cwd }),\n findUp(nodePath.join('ios', '.gitignore'), { cwd }),\n ],\n );\n\n return files\n .filter((filePath): filePath is string => filePath !== undefined)\n .map((filePath) => nodePath.relative(cwd, filePath));\n}\n","import nodePath from 'node:path';\n\n/**\n * Resolve a raw ignore rule from a `.gitignore` file into a path\n * relative to the configured working directory.\n *\n * @example\n * ```ts\n * import { ignoreRuleResolve } from './internal/ignoreRuleResolve.js';\n *\n * ignoreRuleResolve('.', 'dist'); // 'dist'\n * ignoreRuleResolve('.', '/dist'); // 'dist'\n * ignoreRuleResolve('android', 'android-build'); // 'android/android-build'\n * ignoreRuleResolve('android', '!android-build'); // '!android/android-build'\n * ```\n *\n * @internal\n * @param prefix A path prefix that points to the directory containing the `.gitignore` file.\n * @param rule The raw ignore rule parsed from `.gitignore`.\n * @returns A normalized ignore pattern relative to the root `cwd`.\n */\nexport function ignoreRuleResolve(prefix: string, rule: string) {\n const negated = rule.startsWith('!');\n const normalizedPattern = negated ? rule.slice(1) : rule;\n const trimmedPattern = normalizedPattern.startsWith('/')\n ? normalizedPattern.slice(1)\n : normalizedPattern;\n const relativeIgnorePath = nodePath.join(prefix, trimmedPattern);\n\n return negated ? `!${relativeIgnorePath}` : relativeIgnorePath;\n}\n","import fs from 'node:fs';\nimport nodePath from 'node:path';\nimport process from 'node:process';\nimport { defaultIgnores } from './defaultIgnores.js';\nimport { ignoreFileParse } from './internal/ignoreFileParse.js';\nimport { ignoreFileFind } from './internal/ignoreFileFind.js';\nimport { ignoreRuleResolve } from './internal/ignoreRuleResolve.js';\n\n/**\n * Create a new eslint configuration object\n *\n * @example\n * ```ts\n * // eslint.config.js\n * export default [\n * await eslintIgnores({\n * ignores: [\n * // Add custom paths here\n * ]\n * })\n * ];\n * ```\n *\n * @param options\n */\nexport async function eslintIgnores(options: ESLintIgnoreOptions = {}): Promise<ESLintIgnoreConfig> {\n const cwd = options.cwd ?? process.cwd();\n const recommended = options.recommended ?? true;\n const ignoreFilePaths = await ignoreFileFind(cwd);\n const ignoreGlobs = (await Promise.all(ignoreFilePaths.map(async (ignoreFilePathRelative) => {\n const ignoreFilePath = nodePath.join(cwd, ignoreFilePathRelative);\n const ignoreFileContent = String(await fs.promises.readFile(ignoreFilePath));\n const patterns = ignoreFileParse(ignoreFileContent);\n const ignoreDirectoryRelative = nodePath.dirname(ignoreFilePathRelative);\n\n return patterns.map((pattern) => ignoreRuleResolve(ignoreDirectoryRelative, pattern));\n })));\n\n const mergedIgnores = [\n ...(recommended ? defaultIgnores : []),\n ...ignoreGlobs.flat(),\n ];\n\n const ignores = typeof options.ignores === 'function'\n ? options.ignores(mergedIgnores)\n : options.ignores\n ? [...mergedIgnores, ...options.ignores]\n : mergedIgnores;\n\n return {\n name: options.name ?? 'w5s/eslint-ignore',\n ignores,\n };\n}\n\nexport interface ESLintIgnoreConfig {\n /**\n * The configuration name\n */\n name: string;\n\n /**\n * The file globs to ignore\n */\n ignores: Array<string>;\n}\n\nexport interface ESLintIgnoreOptions {\n /**\n * Override configuration name\n */\n name?: ESLintIgnoreConfig['name'];\n\n /**\n * Override current working directory\n */\n cwd?: string;\n\n /**\n * Override or customize ignore patterns.\n * - If passed an array, it appends patterns to the merged ignore list.\n * - If passed a function, it receives the merged list and returns the final array.\n */\n ignores?: ESLintIgnoreConfig['ignores'] | ((ignores: ESLintIgnoreConfig['ignores']) => ESLintIgnoreConfig['ignores']);\n\n /**\n * Include recommended settings and default ignored files\n */\n recommended?: boolean | undefined;\n}\n"],"mappings":";;;;;;AAAA,MAAa,OAAO,OAAO,OAAO;CAEhC,MAAA;CAEA,SAAA;CAEA,aAAa;AACf,CAAC;;;ACPD,MAAa,iBAAiB;CAE5B;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CAGA;CACA;CACA;CACA;AACF;;;ACzCA,SAAgB,gBAAgB,OAA8B;CAC5D,OAAO,eAAe,MAAM,KAAK,CAAC,CAAC;AACrC;;;ACDA,eAAsB,eAAe,KAAqC;CAUxE,QAAO,MATa,QAAQ,IAC1B;EACE,OAAO,SAAS,KAAK,IAAI,YAAY,GAAG,EAAE,IAAI,CAAC;EAE/C,OAAO,SAAS,KAAK,WAAW,YAAY,GAAG,EAAE,IAAI,CAAC;EACtD,OAAO,SAAS,KAAK,OAAO,YAAY,GAAG,EAAE,IAAI,CAAC;CACpD,CACF,EAAA,CAGG,QAAQ,aAAiC,aAAa,KAAA,CAAS,CAAC,CAChE,KAAK,aAAa,SAAS,SAAS,KAAK,QAAQ,CAAC;AACvD;;;;;;;;;;;;;;;;;;;;;;ACKA,SAAgB,kBAAkB,QAAgB,MAAc;CAC9D,MAAM,UAAU,KAAK,WAAW,GAAG;CACnC,MAAM,oBAAoB,UAAU,KAAK,MAAM,CAAC,IAAI;CACpD,MAAM,iBAAiB,kBAAkB,WAAW,GAAG,IACnD,kBAAkB,MAAM,CAAC,IACzB;CACJ,MAAM,qBAAqB,SAAS,KAAK,QAAQ,cAAc;CAE/D,OAAO,UAAU,IAAI,uBAAuB;AAC9C;;;;;;;;;;;;;;;;;;;;ACLA,eAAsB,cAAc,UAA+B,CAAC,GAAgC;CAClG,MAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;CACvC,MAAM,cAAc,QAAQ,eAAe;CAC3C,MAAM,kBAAkB,MAAM,eAAe,GAAG;CAChD,MAAM,cAAe,MAAM,QAAQ,IAAI,gBAAgB,IAAI,OAAO,2BAA2B;EAC3F,MAAM,iBAAiB,SAAS,KAAK,KAAK,sBAAsB;EAEhE,MAAM,WAAW,gBADS,OAAO,MAAM,GAAG,SAAS,SAAS,cAAc,CACzB,CAAC;EAClD,MAAM,0BAA0B,SAAS,QAAQ,sBAAsB;EAEvE,OAAO,SAAS,KAAK,YAAY,kBAAkB,yBAAyB,OAAO,CAAC;CACtF,CAAC,CAAC;CAEF,MAAM,gBAAgB,CACpB,GAAI,cAAc,iBAAiB,CAAC,GACpC,GAAG,YAAY,KAAK,CACtB;CAEA,MAAM,UAAU,OAAO,QAAQ,YAAY,aACvC,QAAQ,QAAQ,aAAa,IAC7B,QAAQ,UACN,CAAC,GAAG,eAAe,GAAG,QAAQ,OAAO,IACrC;CAEN,OAAO;EACL,MAAM,QAAQ,QAAQ;EACtB;CACF;AACF"}
1
+ {"version":3,"file":"index.js","names":["fs"],"sources":["../src/meta.ts","../src/defaultIgnores.ts","../src/internal/ignoreFileParse.ts","../src/internal/ignoreRuleResolve.ts","../src/internal/ignoreFileFind.ts","../src/eslintIgnores.ts"],"sourcesContent":["export const meta = Object.freeze({\n // @ts-ignore - these variables are injected at build time\n name: (typeof __PACKAGE_NAME__ === 'undefined' ? '' : __PACKAGE_NAME__) as string,\n // @ts-ignore - these variables are injected at build time\n version: (typeof __PACKAGE_VERSION__ === 'undefined' ? '' : __PACKAGE_VERSION__) as string,\n // @ts-ignore - these variables are injected at build time\n buildNumber: 1 as number, // (typeof __PACKAGE_BUILD_NUMBER__ === 'undefined' ? 0 : __PACKAGE_BUILD_NUMBER__) as number,\n});\n","export const defaultIgnores = [\n // Lock files\n '**/package-lock.json',\n '**/yarn.lock',\n '**/pnpm-lock.yaml',\n '**/bun.lockb',\n\n // Commonly ignored\n '**/output',\n '**/.output',\n '**/coverage',\n '**/temp',\n '**/.temp',\n '**/tmp',\n '**/.tmp',\n '**/.cache',\n\n // Well known extensions to ignore\n '**/*.min.*',\n '**/*.timestamp-*.mjs',\n\n // Framework specific temporary folder\n '.go/',\n '.pnpm-store/',\n '**/.vitepress/cache',\n '**/.vite-inspect',\n '**/.history',\n '**/.nuxt',\n '**/.next',\n '**/.svelte-kit',\n '**/.vercel',\n '**/.idea',\n '**/.yarn',\n '**/__snapshots__/**',\n\n // git submodules (makefile-core / makefile-ci)\n '.modules/',\n\n // AI related\n '**/.context',\n '**/.claude',\n '**/.agents',\n '**/.*/skills',\n];\n","import parseGitignore from 'parse-gitignore';\n\nexport function ignoreFileParse(input: string): Array<string> {\n return parseGitignore.parse(input).patterns;\n};\n","import nodePath from 'node:path';\n\n/**\n * Resolve a raw ignore rule from a `.gitignore` file into a path\n * relative to the configured working directory.\n *\n * @example\n * ```ts\n * import { ignoreRuleResolve } from './internal/ignoreRuleResolve.js';\n *\n * ignoreRuleResolve('.', 'dist'); // 'dist'\n * ignoreRuleResolve('.', '/dist'); // 'dist'\n * ignoreRuleResolve('android', 'android-build'); // 'android/android-build'\n * ignoreRuleResolve('android', '!android-build'); // '!android/android-build'\n * ```\n *\n * @internal\n * @param prefix A path prefix that points to the directory containing the `.gitignore` file.\n * @param rule The raw ignore rule parsed from `.gitignore`.\n * @returns A normalized ignore pattern relative to the root `cwd`.\n */\nexport function ignoreRuleResolve(prefix: string, rule: string) {\n const negated = rule.startsWith('!');\n const normalizedPattern = negated ? rule.slice(1) : rule;\n const trimmedPattern = normalizedPattern.startsWith('/')\n ? normalizedPattern.slice(1)\n : normalizedPattern;\n const relativeIgnorePath = nodePath.join(prefix, trimmedPattern);\n\n return negated ? `!${relativeIgnorePath}` : relativeIgnorePath;\n}\n","import nodePath from 'node:path';\nimport fs from 'node:fs/promises';\nimport { ignoreFileParse } from './ignoreFileParse.js';\nimport { ignoreRuleResolve } from './ignoreRuleResolve.js';\n\nconst GITIGNORE_FILE = '.gitignore';\n\nexport interface IgnoreFileFindOptions {\n /** Directory names to skip when searching downward from `rootDir`. */\n excludeDirs?: string[];\n /** Maximum recursion depth when searching downward. Defaults to 8. */\n maxDepth?: number;\n /** When true (default), stop ancestor traversal when a `.git` directory is found. */\n stopAtGitRoot?: boolean;\n}\n\n/**\n * Find `.gitignore` files relevant to `rootDir`.\n *\n * Behavior:\n * - Searches downwards from `rootDir` (BFS) for `.gitignore` files, skipping `excludeDirs`.\n * - Walks ancestors from `rootDir` up to the filesystem root (and stops at a `.git` folder when `stopAtGitRoot` is true).\n * - Returns relative paths to `rootDir`.\n *\n * @param rootDir\n * @param options\n */\nexport async function ignoreFileFind(\n rootDir: string,\n options?: IgnoreFileFindOptions,\n): Promise<Array<string>> {\n const excludeDirs = new Set(options?.excludeDirs ?? ['node_modules', '.git', 'dist', 'build', 'out']);\n const maxDepth = options?.maxDepth ?? 8;\n const stopAtGitRoot = options?.stopAtGitRoot ?? true;\n\n const absoluteRootDir = nodePath.resolve(rootDir);\n const found = new Set<string>();\n\n // --- Helpers (internal, not exported) ---------------------------------\n const normalize = (p: string) => p.replaceAll('\\\\', '/');\n\n function lastMatchWins(patterns: string[], candidateRel: string): string | null {\n const normCandidate = normalize(candidateRel);\n let lastMatch: string | null = null;\n for (const p of patterns) {\n const neg = p.startsWith('!');\n const pat = neg ? p.slice(1) : p;\n const patNorm = normalize(pat);\n if (!patNorm) continue;\n\n if (normCandidate === patNorm || normCandidate.startsWith(patNorm + '/')) {\n lastMatch = p;\n }\n }\n return lastMatch;\n }\n\n function isIgnored(patterns: string[], candidateRel: string): boolean {\n const m = lastMatchWins(patterns, candidateRel);\n if (!m) return false;\n return !m.startsWith('!');\n }\n\n async function collectAncestorGitignores(startDir: string): Promise<string[]> {\n const files: string[] = [];\n let dir = startDir;\n while (true) {\n const gi = nodePath.join(dir, GITIGNORE_FILE);\n try {\n const stat = await fs.stat(gi).catch(() => null);\n if (stat && stat.isFile()) files.push(gi);\n } catch {\n // ignore\n }\n\n if (stopAtGitRoot) {\n try {\n const gitStat = await fs.stat(nodePath.join(dir, '.git')).catch(() => null);\n if (gitStat && gitStat.isDirectory()) break;\n } catch {\n // ignore\n }\n }\n\n const parent = nodePath.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n // eslint-disable-next-line unicorn/no-array-reverse\n return files.reverse();\n }\n\n async function parseAndResolve(giPath: string): Promise<string[]> {\n try {\n const content = String(await fs.readFile(giPath, 'utf8'));\n const parsed = ignoreFileParse(content);\n const prefixRel = nodePath.relative(rootDir, nodePath.dirname(giPath));\n return parsed.map((p) => ignoreRuleResolve(prefixRel, p));\n } catch {\n return [];\n }\n }\n\n // --- Build initial patterns from ancestor .gitignore files -------------\n const ancestorFiles = await collectAncestorGitignores(absoluteRootDir);\n const initialPatterns: string[] = [];\n for (const gi of ancestorFiles) {\n const parsed = await parseAndResolve(gi);\n if (parsed.length > 0) initialPatterns.push(...parsed);\n found.add(gi);\n }\n\n // --- BFS downward carrying accumulated patterns -----------------------\n const queue: Array<{ dir: string; depth: number; patterns: string[] }> = [\n { dir: absoluteRootDir, depth: 0, patterns: [...initialPatterns] },\n ];\n\n while (queue.length > 0) {\n // eslint-disable-next-line ts/no-non-null-assertion\n const { dir: currentDir, depth, patterns } = queue.shift()!;\n if (depth > maxDepth) continue;\n\n let entries;\n try {\n entries = await fs.readdir(currentDir, { withFileTypes: true });\n } catch {\n continue; // unreadable\n }\n\n // If local .gitignore exists, parse and extend patterns\n const local = entries.find((e) => e.isFile() && e.name === GITIGNORE_FILE);\n const combinedPatterns = local ? [...patterns, ...(await parseAndResolve(nodePath.join(currentDir, GITIGNORE_FILE)))] : patterns;\n if (local) found.add(nodePath.join(currentDir, GITIGNORE_FILE));\n\n for (const ent of entries) {\n if (!ent.isDirectory()) continue;\n if (excludeDirs.has(ent.name)) continue;\n const subdir = nodePath.join(currentDir, ent.name);\n const rel = nodePath.relative(rootDir, subdir);\n if (isIgnored(combinedPatterns, rel)) continue;\n queue.push({ dir: subdir, depth: depth + 1, patterns: combinedPatterns });\n }\n }\n\n return [...found].map((p) => nodePath.relative(rootDir, p));\n}\n","import fs from 'node:fs';\nimport nodePath from 'node:path';\nimport process from 'node:process';\nimport { defaultIgnores } from './defaultIgnores.js';\nimport { ignoreFileParse } from './internal/ignoreFileParse.js';\nimport { ignoreFileFind } from './internal/ignoreFileFind.js';\nimport { ignoreRuleResolve } from './internal/ignoreRuleResolve.js';\n\n/**\n * Create a new eslint configuration object\n *\n * @example\n * ```ts\n * // eslint.config.js\n * export default [\n * await eslintIgnores({\n * ignores: [\n * // Add custom paths here\n * ]\n * })\n * ];\n * ```\n *\n * @param options\n */\nexport async function eslintIgnores(options: ESLintIgnoreOptions = {}): Promise<ESLintIgnoreConfig> {\n const cwd = options.cwd ?? process.cwd();\n const recommended = options.recommended ?? true;\n const ignoreFilePaths = await ignoreFileFind(cwd);\n const ignoreGlobs = (await Promise.all(ignoreFilePaths.map(async (ignoreFilePathRelative) => {\n const ignoreFilePath = nodePath.join(cwd, ignoreFilePathRelative);\n const ignoreFileContent = String(await fs.promises.readFile(ignoreFilePath));\n const patterns = ignoreFileParse(ignoreFileContent);\n const ignoreDirectoryRelative = nodePath.dirname(ignoreFilePathRelative);\n\n return patterns.map((pattern) => ignoreRuleResolve(ignoreDirectoryRelative, pattern));\n })));\n\n const mergedIgnores = [\n ...(recommended ? defaultIgnores : []),\n ...ignoreGlobs.flat(),\n ];\n\n const ignores = typeof options.ignores === 'function'\n ? options.ignores(mergedIgnores)\n : options.ignores\n ? [...mergedIgnores, ...options.ignores]\n : mergedIgnores;\n\n return {\n name: options.name ?? 'w5s/eslint-ignore',\n ignores,\n };\n}\n\nexport interface ESLintIgnoreConfig {\n /**\n * The configuration name\n */\n name: string;\n\n /**\n * The file globs to ignore\n */\n ignores: Array<string>;\n}\n\nexport interface ESLintIgnoreOptions {\n /**\n * Override configuration name\n */\n name?: ESLintIgnoreConfig['name'];\n\n /**\n * Override current working directory\n */\n cwd?: string;\n\n /**\n * Override or customize ignore patterns.\n * - If passed an array, it appends patterns to the merged ignore list.\n * - If passed a function, it receives the merged list and returns the final array.\n */\n ignores?: ESLintIgnoreConfig['ignores'] | ((ignores: ESLintIgnoreConfig['ignores']) => ESLintIgnoreConfig['ignores']);\n\n /**\n * Include recommended settings and default ignored files\n */\n recommended?: boolean | undefined;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@w5s/eslint-config-ignore",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Shared ESLint ignore configuration generator",
5
5
  "keywords": [
6
6
  "eslint",
@@ -63,5 +63,5 @@
63
63
  "access": "public"
64
64
  },
65
65
  "sideEffect": false,
66
- "gitHead": "fc447207d2af5368f809a9a90d0e4056344b812a"
66
+ "gitHead": "9102f7149f4d1faa4a9c96e7e910cbb296bd63e5"
67
67
  }
@@ -1,17 +1,146 @@
1
1
  import nodePath from 'node:path';
2
- import { findUp } from 'find-up';
3
-
4
- export async function ignoreFileFind(cwd: string): Promise<Array<string>> {
5
- const files = await Promise.all(
6
- [
7
- findUp(nodePath.join('', '.gitignore'), { cwd }),
8
- // TODO: refactor as generic exploration
9
- findUp(nodePath.join('android', '.gitignore'), { cwd }),
10
- findUp(nodePath.join('ios', '.gitignore'), { cwd }),
11
- ],
12
- );
13
-
14
- return files
15
- .filter((filePath): filePath is string => filePath !== undefined)
16
- .map((filePath) => nodePath.relative(cwd, filePath));
2
+ import fs from 'node:fs/promises';
3
+ import { ignoreFileParse } from './ignoreFileParse.js';
4
+ import { ignoreRuleResolve } from './ignoreRuleResolve.js';
5
+
6
+ const GITIGNORE_FILE = '.gitignore';
7
+
8
+ export interface IgnoreFileFindOptions {
9
+ /** Directory names to skip when searching downward from `rootDir`. */
10
+ excludeDirs?: string[];
11
+ /** Maximum recursion depth when searching downward. Defaults to 8. */
12
+ maxDepth?: number;
13
+ /** When true (default), stop ancestor traversal when a `.git` directory is found. */
14
+ stopAtGitRoot?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Find `.gitignore` files relevant to `rootDir`.
19
+ *
20
+ * Behavior:
21
+ * - Searches downwards from `rootDir` (BFS) for `.gitignore` files, skipping `excludeDirs`.
22
+ * - Walks ancestors from `rootDir` up to the filesystem root (and stops at a `.git` folder when `stopAtGitRoot` is true).
23
+ * - Returns relative paths to `rootDir`.
24
+ *
25
+ * @param rootDir
26
+ * @param options
27
+ */
28
+ export async function ignoreFileFind(
29
+ rootDir: string,
30
+ options?: IgnoreFileFindOptions,
31
+ ): Promise<Array<string>> {
32
+ const excludeDirs = new Set(options?.excludeDirs ?? ['node_modules', '.git', 'dist', 'build', 'out']);
33
+ const maxDepth = options?.maxDepth ?? 8;
34
+ const stopAtGitRoot = options?.stopAtGitRoot ?? true;
35
+
36
+ const absoluteRootDir = nodePath.resolve(rootDir);
37
+ const found = new Set<string>();
38
+
39
+ // --- Helpers (internal, not exported) ---------------------------------
40
+ const normalize = (p: string) => p.replaceAll('\\', '/');
41
+
42
+ function lastMatchWins(patterns: string[], candidateRel: string): string | null {
43
+ const normCandidate = normalize(candidateRel);
44
+ let lastMatch: string | null = null;
45
+ for (const p of patterns) {
46
+ const neg = p.startsWith('!');
47
+ const pat = neg ? p.slice(1) : p;
48
+ const patNorm = normalize(pat);
49
+ if (!patNorm) continue;
50
+
51
+ if (normCandidate === patNorm || normCandidate.startsWith(patNorm + '/')) {
52
+ lastMatch = p;
53
+ }
54
+ }
55
+ return lastMatch;
56
+ }
57
+
58
+ function isIgnored(patterns: string[], candidateRel: string): boolean {
59
+ const m = lastMatchWins(patterns, candidateRel);
60
+ if (!m) return false;
61
+ return !m.startsWith('!');
62
+ }
63
+
64
+ async function collectAncestorGitignores(startDir: string): Promise<string[]> {
65
+ const files: string[] = [];
66
+ let dir = startDir;
67
+ while (true) {
68
+ const gi = nodePath.join(dir, GITIGNORE_FILE);
69
+ try {
70
+ const stat = await fs.stat(gi).catch(() => null);
71
+ if (stat && stat.isFile()) files.push(gi);
72
+ } catch {
73
+ // ignore
74
+ }
75
+
76
+ if (stopAtGitRoot) {
77
+ try {
78
+ const gitStat = await fs.stat(nodePath.join(dir, '.git')).catch(() => null);
79
+ if (gitStat && gitStat.isDirectory()) break;
80
+ } catch {
81
+ // ignore
82
+ }
83
+ }
84
+
85
+ const parent = nodePath.dirname(dir);
86
+ if (parent === dir) break;
87
+ dir = parent;
88
+ }
89
+ // eslint-disable-next-line unicorn/no-array-reverse
90
+ return files.reverse();
91
+ }
92
+
93
+ async function parseAndResolve(giPath: string): Promise<string[]> {
94
+ try {
95
+ const content = String(await fs.readFile(giPath, 'utf8'));
96
+ const parsed = ignoreFileParse(content);
97
+ const prefixRel = nodePath.relative(rootDir, nodePath.dirname(giPath));
98
+ return parsed.map((p) => ignoreRuleResolve(prefixRel, p));
99
+ } catch {
100
+ return [];
101
+ }
102
+ }
103
+
104
+ // --- Build initial patterns from ancestor .gitignore files -------------
105
+ const ancestorFiles = await collectAncestorGitignores(absoluteRootDir);
106
+ const initialPatterns: string[] = [];
107
+ for (const gi of ancestorFiles) {
108
+ const parsed = await parseAndResolve(gi);
109
+ if (parsed.length > 0) initialPatterns.push(...parsed);
110
+ found.add(gi);
111
+ }
112
+
113
+ // --- BFS downward carrying accumulated patterns -----------------------
114
+ const queue: Array<{ dir: string; depth: number; patterns: string[] }> = [
115
+ { dir: absoluteRootDir, depth: 0, patterns: [...initialPatterns] },
116
+ ];
117
+
118
+ while (queue.length > 0) {
119
+ // eslint-disable-next-line ts/no-non-null-assertion
120
+ const { dir: currentDir, depth, patterns } = queue.shift()!;
121
+ if (depth > maxDepth) continue;
122
+
123
+ let entries;
124
+ try {
125
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
126
+ } catch {
127
+ continue; // unreadable
128
+ }
129
+
130
+ // If local .gitignore exists, parse and extend patterns
131
+ const local = entries.find((e) => e.isFile() && e.name === GITIGNORE_FILE);
132
+ const combinedPatterns = local ? [...patterns, ...(await parseAndResolve(nodePath.join(currentDir, GITIGNORE_FILE)))] : patterns;
133
+ if (local) found.add(nodePath.join(currentDir, GITIGNORE_FILE));
134
+
135
+ for (const ent of entries) {
136
+ if (!ent.isDirectory()) continue;
137
+ if (excludeDirs.has(ent.name)) continue;
138
+ const subdir = nodePath.join(currentDir, ent.name);
139
+ const rel = nodePath.relative(rootDir, subdir);
140
+ if (isIgnored(combinedPatterns, rel)) continue;
141
+ queue.push({ dir: subdir, depth: depth + 1, patterns: combinedPatterns });
142
+ }
143
+ }
144
+
145
+ return [...found].map((p) => nodePath.relative(rootDir, p));
17
146
  }