Files
suaveplan/packages/devtools/tooling/linter/src/glob-resolver/glob-resolver.md
2026-05-27 23:29:53 +01:00

4.1 KiB
Raw Blame History

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

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 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<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 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).