# `glob-resolver` _(no submodules)_ ## Purpose `@suaveplan/linter/glob-resolver` expands a list of patterns (glob expressions or file paths) into a sorted, deduplicated list of markdown targets. It owns the default ignore list (`node_modules`, `dist`), distinguishes between single-file paths and glob expressions, and produces a stable iteration order so the rest of the pipeline is deterministic. The resolver is the single seam where the filesystem and the glob implementation meet, which means every call site downstream can rely on receiving a plain sorted array of paths. ## Features - Single-file fast path — a pattern that ends in `.md` AND contains no glob magic characters bypasses the glob adapter entirely and is added directly to the result after an `fs.exists` check. - Default ignore list — `node_modules/**`, `**/node_modules/**`, `dist/**`, `**/dist/**`. Callers cannot override this list at the resolver layer (the intent is to keep all linter walks consistent across the project). - Set-based deduplication — two patterns that match the same file produce one entry. - Sorted output — paths returned in lexicographic order so reports are reproducible across runs. - Injectable `FsAdapter` and `GlobAdapter` seams; the default `nodeGlobAdapter` wraps the `glob` npm package. ## Usage: Basic ```ts import { nodeGlobAdapter, resolveMarkdownTargets, } from "@suaveplan/linter/glob-resolver"; import { nodeFsAdapter } from "@suaveplan/linter/markdown-fixer"; const files = await resolveMarkdownTargets({ patterns: ["docs/**/*.md", "README.md"], fs: nodeFsAdapter, glob: nodeGlobAdapter, }); ``` ## Usage: Advanced Use an in-memory glob adapter to drive a hermetic test: ```ts import type { FsAdapter, GlobAdapter } from "@suaveplan/linter"; import { resolveMarkdownTargets } from "@suaveplan/linter/glob-resolver"; const tree = ["a.md", "docs/b.md", "node_modules/junk.md"]; const fs: FsAdapter = { async readFile() { return ""; }, async writeFile() {}, async exists(p) { return tree.includes(p); }, }; const glob: GlobAdapter = { async find(pattern, { ignore }) { // ignore-aware filter so the test exercises the same contract return tree.filter( (f) => !ignore.some((i) => f.startsWith(i.replace("/**", ""))) && f.endsWith(".md") ); }, }; const targets = await resolveMarkdownTargets({ patterns: ["**/*.md", "a.md"], fs, glob, }); // targets === ['a.md', 'docs/b.md'] (node_modules ignored, deduped) ``` ## API Reference ### `resolveMarkdownTargets(options): Promise` - `options.patterns: ReadonlyArray` — required. - `options.cwd?: string` — defaults to `"."`. - `options.fs: FsAdapter` — required (used for the single-file existence check). - `options.glob?: GlobAdapter` — defaults to `nodeGlobAdapter`. Throws `LinterError(LINTER.FILE_NOT_FOUND)` when a pattern looks like a file path (ends in `.md` AND has no magic chars) but does not exist. ### `nodeGlobAdapter: GlobAdapter` Default adapter; forwards to `glob(pattern, { ignore, cwd })`. ### `DEFAULT_IGNORE: ReadonlyArray` The unmodifiable ignore list applied on every glob invocation. ## Implementation Notes - The magic-character detection regex (`/[*?[\]{}]/`) matches any of the glob meta-characters. A pattern containing none of them is treated as a literal path — this avoids the `glob` library treating `README.md` as a pattern with side effects on a case-insensitive filesystem. - The `cwd` option is forwarded verbatim to the glob adapter. When omitted it falls through as `"."`, which is the behaviour the upstream `processMarkdownFiles` relied on implicitly. - The result is materialised into a `Set` then sorted to a regular array. Returning the `Set` directly would force callers to handle iteration order; the explicit `[...].sort()` keeps the contract simple. - The resolver never reads file contents — only `fs.exists` for the single-file fast path. This keeps the resolver O(patterns) rather than O(matches × file size).