@pyreon/lint — API Reference
Generated from
lint'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see lint.
Pyreon-specific lint rules powered by oxc-parser. Covers reactivity (16), JSX (11), lifecycle (6), performance (5), SSR (5), architecture (11), store (3), form (4), styling (5), hooks (3), accessibility (3), router (5), SSG (3), security (3), frontend (12), query (2), rx (1), i18n (1), storage (1), http (2), isomorphic (6), backend (7), web-perf (7), portable (6), js (3) — 132 rules total. Programmatic API (lint sync, lintAsync parallel across a worker pool for large runs, lintFile), CLI (pyreon-lint), watch mode (fs.watch + 100ms debounce + AstCache), LSP server, and .pyreonlintrc.json config with per-rule options via ESLint-style tuple form. Every rule belongs to one of ten GROUPS — the axes the categories do not capture (what knowledge a rule requires, WHERE a file runs, and which platforms it must survive): pyreon (52, framework semantics), pkg (27, per-library and dependency-gated), a11y (15, accessibility), internal (6, encodes the Pyreon repo and is never on in a shipped preset), isomorphic (6, the hydration contract for files that render on both sides), security (3, exploitable shapes), backend (7, server-role files), web-perf (7, client-role files), portable (6, must survive iOS + Android), js (3, language shapes oxlint cannot express). A rule declares appliesTo (a list of file ROLES) and the RUNNER gates it — resolved by resolveFileRole, which reads node: imports, fs-router api routes, island declarations and entry files, and defaults to shared because an isomorphic file must satisfy both sides. Categories live underneath, so a query rule is group pkg, category query. A rule may also declare scanTarget ('source' default, 'test', 'packageConfig', or a LIST of them) — the files it is ABOUT — so a health gate can collect them; without it a rule whose subject is a test file or a package-root config can never fire against a source-only scan. The list form exists because a rule can be about more than one surface: no-require-in-esm declares ['source', 'test'], since a .test.ts in a "type": "module" package throws require is not defined under real Node exactly as src/ does. Resolve it through scanTargetsOf / targetsScan rather than comparing with ===, which silently matches nothing against a list. A whole group can be set in one line via the groups config key, applied after the preset and before per-rule entries so an explicit rule always wins. Options shared by more than one rule go in the top-level settings key: portablePaths names the directories that must lower to SwiftUI and Compose, and five of the six portable rules need that same answer, so repeating it per rule makes the config a hand-maintained subset of the registry. A settings key is seeded into the options of a rule only when that rule DECLARES it in meta.schema, per-rule options win, and a key no rule declares is reported as a config error. Two independent reasons a rule can be off in a shipped preset. Opt-in best-practice rules (meta.optIn — the frontend/query/rx/i18n/storage categories plus the form/router opt-in rules): off in recommended/strict/app/lib, enabled wholesale by the best-practices preset or per-rule config. Monorepo-scoped rules (meta.scope: 'monorepo' — no-circular-import, no-cross-layer-import, no-error-without-prefix, no-query-selector-cast-in-test, require-browser-smoke-test, vitest-config-uses-shared): these encode the Pyreon repository itself — its layer order, its private internal packages, its [Pyreon] error prefix — so EVERY consumer preset forces them off, best-practices included, and the Pyreon repo re-enables them by id in its own .pyreonlintrc.json. Library-scoped opt-in rules auto-gate on the project’s package.json dependencies (a project that doesn’t use @pyreon/query never sees query rules). Notable rules: pyreon/no-process-dev-gate (auto-fixable), pyreon/query-options-as-function (auto-fixable — wraps the options object literal in () => (...); also a proactive MCP validate detector), pyreon/require-img-alt / pyreon/img-requires-dimensions / pyreon/no-discarded-optimize-fields (a11y + CLS — the last flags a raw <img src={x.src}> that discards a ?optimize descriptor), pyreon/heading-order (a11y — flags a skipped heading level), pyreon/color-contrast (a11y — literal-hex contrast pairs), pyreon/i18n-prefer-trans-for-rich-jsx, pyreon/prefer-typed-search-params.
Features
132 rules across 25 categories
lint(options) programmatic API + lintFile() low-level entry
CLI: pyreon-lint with --preset / --fix / --watch / --format / --rule-options
4 presets: recommended, strict, app, lib
Per-rule options via tuple form in config or
--rule-options id='{json}'AstCache (FNV-1a hash) for repeat runs
LSP server for IDE integration (startLspServer)
Inline suppression: // pyreon-lint-ignore <rule> OR // pyreon-lint-disable-next-line <rule> — the same comment also silences a
pyreon doctor/ MCPvalidateDETECTOR finding, by its bare code or the prefixed id the tool prints (pyreon-patterns/as-unknown-as-vnodechild)
Complete example
A full, end-to-end usage of the package:
import { lint, lintFile, allRules, getPreset, AstCache } from '@pyreon/lint'
// Programmatic — typical CI usage
const result = lint({ paths: ['src/'], preset: 'recommended' })
console.log(result.totalErrors, result.totalWarnings)
for (const d of result.configDiagnostics) console.log(d.ruleId, d.message)
// Per-rule overrides on a single run
lint({
paths: ['.'],
ruleOverrides: { 'pyreon/no-classname': 'off' },
ruleOptionsOverrides: {
'pyreon/no-window-in-ssr': { exemptPaths: ['src/foundation/'] },
},
})
// Low-level single-file API with AST cache (watch mode)
const cache = new AstCache()
const config = getPreset('recommended')
const fileResult = lintFile('app.tsx', source, allRules, config, cache)
// CLI
// pyreon-lint --preset strict --quiet # CI mode
// pyreon-lint --fix # auto-fix
// pyreon-lint --watch src/ # watch mode
// pyreon-lint --list # list all 132 rules
// pyreon-lint --why-off pyreon/rx-prefer-pipe # explain a silent rule
// pyreon-lint --rule-options 'pyreon/no-window-in-ssr={"exemptPaths":["src/foundation/"]}' src/Exports
| Symbol | Kind | Summary |
|---|---|---|
lint | function | 132 rules across 25 categories. |
lintFile | function | Low-level single-file API. |
cli | function | CLI entry. |
no-process-dev-gate | constant | The typeof process !== 'undefined' && process.env.NODE_ENV !== 'production' pattern works in vitest (Node, process i |
require-browser-smoke-test | constant | Locks in the durability of the T1.1 browser smoke harness (PRs #224, #227, #229, #231). |
API
lint function
lint(options?: LintOptions): LintResult132 rules across 25 categories. Auto-loads .pyreonlintrc.json. Presets: recommended, strict, app, lib. Per-rule options via tuple form in config (["error", { exemptPaths: [...] }]) or ruleOptionsOverrides. exemptPaths is honoured CENTRALLY for every rule (the runner skips an exempt file before the rule runs), so it means the same thing everywhere rather than only in rules that opted in. Wrong-typed options surface on result.configDiagnostics, as does a rules/groups/settings key that names nothing — a mistyped rule id used to be silently ignored, which is indistinguishable from working. Uses oxc-parser with AST caching.
Example
import { lint } from "@pyreon/lint"
const result = lint({ paths: ["src/"], preset: "recommended" })
console.log(result.totalErrors, result.totalWarnings)
// Config-level diagnostics (malformed rule options, etc.)
for (const d of result.configDiagnostics) console.log(d.ruleId, d.message)
// Severity overrides + per-rule options overrides
lint({
paths: ["."],
ruleOverrides: { "pyreon/no-classname": "off" },
ruleOptionsOverrides: {
"pyreon/no-window-in-ssr": { exemptPaths: ["src/foundation/"] },
},
})See also: lintFile · getPreset · AstCache
lintFile function
lintFile(filePath: string, sourceText: string, rules: Rule[], config: LintConfig, cache?: AstCache, configDiagnosticsSink?: ConfigDiagnostic[]): LintFileResultLow-level single-file API. Optional AstCache for repeat runs (FNV-1a hash keyed). Optional configDiagnosticsSink collects malformed-option diagnostics; without it they print to stderr.
Example
import { lintFile, allRules, getPreset, AstCache } from "@pyreon/lint"
const cache = new AstCache()
const config = getPreset("recommended")
const configSink: ConfigDiagnostic[] = []
const result = lintFile("app.tsx", source, allRules, config, cache, configSink)See also: lint · AstCache
cli function
pyreon-lint [--preset name] [--fix] [--format text|json|compact] [--quiet] [--watch] [--list] [--config path] [--ignore path] [--rule id=severity] [--rule-options id='{json}'] [path...]CLI entry. Config: .pyreonlintrc.json (reference schema/pyreonlintrc.schema.json for IDE autocomplete) or package.json's 'pyreonlint' field. Ignore: .pyreonlintignore + .gitignore. Watch: fs.watch recursive with 100ms debounce. --rule-options id='{json}' passes per-rule options on a single run.
Example
pyreon-lint --preset strict --quiet # CI mode
pyreon-lint --fix # auto-fix
pyreon-lint --watch src/ # watch mode
pyreon-lint --init # scaffold .pyreonlintrc.json for this project
pyreon-lint --list # list all 132 rules
pyreon-lint --why-off no-window-in-ssr # why a rule will (or will not) run here
pyreon-lint --format json # machine-readable
pyreon-lint --rule-options 'pyreon/no-window-in-ssr={"exemptPaths":["src/foundation/"]}' src/See also: lint
no-process-dev-gate constant
rule: pyreon/no-process-dev-gate (architecture, error, auto-fixable)The typeof process !== 'undefined' && process.env.NODE_ENV !== 'production' pattern works in vitest (Node, process is defined) but is silently dead code in real Vite browser bundles because Vite does NOT polyfill process for the client. Every console.warn gated on the broken constant never fires for real users in dev mode — unit tests pass while users get nothing. Use import.meta.env.DEV instead — Vite/Rolldown literal-replace it at build time, prod tree-shakes the warning to zero bytes, and vitest sets it to true automatically. Server-only packages (zero, core/server, core/runtime-server, vite-plugin, cli, lint, mcp, storybook, typescript) and test files are exempt. Reference implementation: packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions. The rule has an auto-fix that replaces the broken expression with import.meta.env?.DEV === true.
Example
// ❌ Wrong — dead code in real Vite browser bundles
const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
if (__DEV__) console.warn('hello')
// ✅ Correct — Vite literal-replaces import.meta.env.DEV at build time
// @ts-ignore — provided by Vite/Rolldown at build time
const __DEV__ = import.meta.env?.DEV === true
if (__DEV__) console.warn('hello')Common mistakes
Copying the
typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'pattern from existing codebases — it works in Node but is dead in browser bundlesTrying to test with
delete globalThis.process— vitest's ownimport.meta.envdepends onprocess, so deleting it breaks the FIXED gate too (not because the gate is wrong, but because vitest can't resolve it)Adding
process: { env: { ... } }polyfills to vite.config.ts as a workaround — fix the source insteadUsing the rule for server-only packages — they're correctly exempt because Node always has
process
See also: require-browser-smoke-test
require-browser-smoke-test constant
rule: pyreon/require-browser-smoke-test (architecture, error in recommended/strict/lib, off in app)Locks in the durability of the T1.1 browser smoke harness (PRs #224, #227, #229, #231). Every browser-categorized package MUST ship at least one *.browser.test.{ts,tsx} file under src/. Without this rule, new browser packages can quietly ship without smoke coverage and we drift back to the world before T1.1 — happy-dom silently masks environment-divergence bugs (PR #197 mock-vnode metadata drop, PR #200 typeof process dead code, multi-word event delegation bug). Default browser-package list mirrors .claude/rules/test-environment-parity.md. The rule fires once per package on its src/index.ts, walks the package directory looking for *.browser.test.*, and reports if none are found. Off in app preset because apps don't ship as packages with smoke obligations.
Example
// Per-package config (optional — defaults cover all known browser packages)
{
"rules": {
"pyreon/require-browser-smoke-test": [
"error",
{
"additionalPackages": ["@my-org/my-browser-pkg"],
"exemptPaths": ["packages/experimental/"]
}
]
}
}Common mistakes
Adding a new browser-running package without a browser test — the rule will fail your PR
Hardcoding the browser-package list in the rule — the list lives in
.claude/rules/browser-packages.json(single source of truth), not in the rule sourceDisabling the rule globally — use
exemptPathsto exempt specific packages still under constructionShipping a
sanity.browser.test.tswithexpect(1).toBe(1)just to satisfy the rule — it passes but provides zero signal. The rule is a GATE, not a quality check; review actual contents on PR
See also: no-process-dev-gate