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 tofixMarkdown/resolveMarkdownTargetsdoes not exist.NO_TARGETS_FOUND— glob expansion produced an empty list.READ_FAILED—fs.readFilethrew (e.g. permissions, EISDIR).WRITE_FAILED—fs.writeFilethrew.MARKDOWN_PARSE_FAILED—unified().process(...)threw (malformed input).BIOME_UNAVAILABLE—biome --versionprobe viaCommandRunnerreturned non-zero.BIOME_FAILED— biome run returned a non-zero exit AND we are incheckmode (a non-zero exit incheck-writemode is informational and surfaces via the report).RUNNER_FAILED—CommandRunner.runrejected (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 withinfixMarkdown. - 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.