4.1 KiB
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
.mdAND contains no glob magic characters bypasses the glob adapter entirely and is added directly to the result after anfs.existscheck. - 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
FsAdapterandGlobAdapterseams; the defaultnodeGlobAdapterwraps theglobnpm package.
Usage: Basic
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:
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<string[]>
options.patterns: ReadonlyArray<string>— required.options.cwd?: string— defaults to".".options.fs: FsAdapter— required (used for the single-file existence check).options.glob?: GlobAdapter— defaults tonodeGlobAdapter.
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<string>
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 thegloblibrary treatingREADME.mdas a pattern with side effects on a case-insensitive filesystem. - The
cwdoption is forwarded verbatim to the glob adapter. When omitted it falls through as".", which is the behaviour the upstreamprocessMarkdownFilesrelied on implicitly. - The result is materialised into a
Setthen sorted to a regular array. Returning theSetdirectly would force callers to handle iteration order; the explicit[...].sort()keeps the contract simple. - The resolver never reads file contents — only
fs.existsfor the single-file fast path. This keeps the resolver O(patterns) rather than O(matches × file size).