Files
2026-05-27 23:29:53 +01:00

7.1 KiB

Design — @suaveplan/linter

Tier

Server-tier — uses node:fs and node:child_process. Marked via "engines.node" and the absence of a browser field. The package never imports from @suaveplan/* runtime code other than @suaveplan/error and @suaveplan/types.

Layout

packages/devtools/tooling/linter/
  package.json                  v0.1.0 — subpath exports per feature
  tsconfig.lib.json
  vite.config.ts                ESM-first, dts plugin
  vitest.config.ts              forks pool, istanbul, 100/100/100/100
  README.md
  tests/setup/polyfills.ts
  src/
    index.ts                    Root barrel — explicit named re-exports
    bin.ts                      One-line CLI binary entry: process.exit(await runCli(...))
    error-codes/
      error-codes.ts            defineCodes("LINTER", ...) + LinterError
      error-codes.md
      error-codes.test.ts
      index.ts
    telemetry/
      telemetry.ts              OTel counters + histogram + record helpers
      telemetry.md
      telemetry.test.ts
      index.ts
    types/
      linter.types.ts           Shared interfaces (no .md required)
      index.ts
    markdown-fixer/
      markdown-fixer.ts         fixMarkdown + processMarkdownFiles
      markdown-fixer.md
      markdown-fixer.test.ts
      index.ts
    biome-runner/
      biome-runner.ts           runBiome via CommandRunner seam
      biome-runner.md
      biome-runner.test.ts
      index.ts
    glob-resolver/
      glob-resolver.ts          resolveMarkdownTargets({ patterns, fs, glob })
      glob-resolver.md
      glob-resolver.test.ts
      index.ts
    unified-lint/
      unified-lint.ts           runUnifiedLint orchestrator
      unified-lint.md
      unified-lint.test.ts
      index.ts
    cli/
      cli.ts                    runCli(argv, opts?) — pure
      cli.md
      cli.test.ts
      index.ts

Public types

export interface CommandRunner {
    run(input: CommandInput): Promise<CommandResult>;
}
export interface CommandInput {
    command: string;
    args: ReadonlyArray<string>;
    cwd?: string;
    env?: Readonly<Record<string, string>>;
}
export interface CommandResult {
    exitCode: number;
    stdout: string;
    stderr: string;
}

export interface FsAdapter {
    readFile(path: string): Promise<string>;
    writeFile(path: string, data: string): Promise<void>;
    exists(path: string): Promise<boolean>;
}

export interface GlobAdapter {
    find(pattern: string, opts: GlobOptions): Promise<ReadonlyArray<string>>;
}
export interface GlobOptions {
    ignore: ReadonlyArray<string>;
    cwd: string;
}

export interface MarkdownFixReport {
    file: string;
    changed: boolean;
    rulesApplied: ReadonlyArray<string>;
}

export interface BiomeReport {
    mode: "check" | "check-write" | "format-write";
    exitCode: number;
    stdout: string;
    stderr: string;
    success: boolean;
    durationMs: number;
}

export interface LintReport {
    biome?: BiomeReport;
    markdown: ReadonlyArray<MarkdownFixReport>;
    success: boolean;
    durationMs: number;
}

Error codes

defineCodes("LINTER", [...]):

  • INVALID_OPTIONS — Zod validation fails at the public surface.
  • FILE_NOT_FOUND — single-file path passed to fixMarkdown / resolveMarkdownTargets does not exist.
  • NO_TARGETS_FOUND — glob expansion produced an empty list.
  • READ_FAILED — fs.readFile threw (e.g. permissions, EISDIR).
  • WRITE_FAILED — fs.writeFile threw.
  • MARKDOWN_PARSE_FAILED — unified().process(...) threw (malformed input).
  • BIOME_UNAVAILABLE — biome --version probe via CommandRunner returned non-zero.
  • BIOME_FAILED — biome run returned a non-zero exit AND we are in check mode (a non-zero exit in check-write mode is informational and surfaces via the report).
  • RUNNER_FAILED — CommandRunner.run rejected (process spawn failed before exit).
  • UNKNOWN_FLAG — CLI argv contains an unrecognised flag.

Every code has at least one Verified-by scenario in the spec delta.

Telemetry

  • Counter linter_files_fixed_total{rule} — incremented per rule application within fixMarkdown.
  • Counter linter_files_processed_total{result} — result ∈ {"changed", "unchanged"}.
  • Counter linter_biome_runs_total{mode, outcome} — outcome ∈ {"success", "failure"}.
  • Histogram linter_run_duration_ms{phase} — phase ∈ {"markdown", "biome", "total"}.

All instruments are obtained via metrics.getMeter("@suaveplan/linter") — when no MeterProvider is registered the API returns no-op instruments so production callers who do not opt into telemetry pay zero cost.

CommandRunner seam

The default exported runner wraps node:child_process.spawn (NOT execSync) so it is cancellation-friendly and async-native:

export const nodeCommandRunner: CommandRunner = {
    async run({ command, args, cwd, env }) {
        return await new Promise<CommandResult>((resolve, reject) => {
            const child = spawn(command, args, {
                cwd,
                env: env ? { ...process.env, ...env } : process.env,
                stdio: ["ignore", "pipe", "pipe"],
            });
            let stdout = "";
            let stderr = "";
            child.stdout?.on("data", (d) => {
                stdout += String(d);
            });
            child.stderr?.on("data", (d) => {
                stderr += String(d);
            });
            child.on("error", reject);
            child.on("close", (exitCode) => {
                resolve({
                    exitCode: exitCode ?? 1,
                    stdout,
                    stderr,
                });
            });
        });
    },
};

Tests pass a CommandRunner whose run is a real async function returning canned results — not a vi.fn mock. This honours rule 2 (no mocks) while keeping the unit-test runtime hermetic.

FsAdapter and GlobAdapter defaults

export const nodeFsAdapter: FsAdapter = {
    async readFile(path) {
        return fs.readFile(path, "utf-8");
    },
    async writeFile(path, data) {
        await fs.writeFile(path, data, "utf-8");
    },
    async exists(path) {
        try {
            await fs.access(path);
            return true;
        } catch {
            return false;
        }
    },
};

export const nodeGlobAdapter: GlobAdapter = {
    async find(pattern, { ignore, cwd }) {
        return glob(pattern, { ignore: [...ignore], cwd });
    },
};

Tests pass in-memory adapters (a Map<string, string> for fs; a function returning a canned list for glob) — again, real fakes, not vi.mock.

CLI argv parsing

Trivial hand-rolled parser — no commander dependency for one binary with five flags. Returns { mode, fix, patterns, cwd, showHelp }. Unknown flags raise LinterError(LINTER.UNKNOWN_FLAG) so the binary wrapper prints the message and exits non-zero rather than silently treating the flag as a positional pattern.

Subpath exports

Every feature folder gets its own subpath entry so consumers can import only what they need (@suaveplan/linter/markdown-fixer, /biome-runner, etc.) and pay zero cost for the features they don't use. The root barrel re-exports every public symbol so the default @suaveplan/linter import still works.