Architecture-enforcing ESLint rules. The kind of thing you'd normally write a wiki page about and then watch everyone ignore — except these fail the build.
Each rule encodes one architectural decision that's expensive to relitigate in
code review every week: keep wall-clock reads out of domain logic, never lose
the original error in a re-throw, no hardcoded English in the render layer, no
process.env reaching into a class method. Static analysis instead of tribal
knowledge.
11 rules, 94 tests, zero false-positive tolerance. ESLint 9 flat-config
native, TypeScript-aware (uses @typescript-eslint/utils).
npm i -D eslint-plugin-ghost// eslint.config.js (flat config, ESLint 9+)
import ghost from 'eslint-plugin-ghost';
export default [
{
files: ['**/*.{ts,tsx}'],
plugins: { ghost },
rules: { ...ghost.configs.recommended.rules },
},
];See eslint.config.example.js for picking rules
individually.
| Rule | Recommended | What it enforces |
|---|---|---|
no-date-in-domain |
error | No wall-clock reads (new Date(), Date.now()) in domain files — inject a clock so logic stays replayable and testable. |
no-process-env-in-method |
error | No process.env access inside a class member — read config at the module boundary, not deep in behavior. |
no-platform-branch |
error | No runtime platform branching (Platform.OS, isWeb) inside render functions — split the file per platform instead. |
no-test-only |
error | No focused test markers (.only, fit, fdescribe) in committed code — the classic silent green CI. |
no-catch-rewrap |
warn | A re-throw inside catch must reference the caught error (cause) — never sever the stack. |
require-error-logged |
warn | A swallowed catch must log the caught variable at warn/error — no silent catch {}. |
no-hardcoded-string-in-jsx |
warn | No hardcoded user-visible strings in JSX — route copy through i18n. |
no-hardcoded-user-message |
warn | Exception constructors must not receive raw English literals — keep user-facing copy out of throw sites. |
no-locale-method-without-locale |
warn | Intl constructors must receive an explicit locale — no implicit machine-locale formatting. |
no-timezone-naive-date |
warn | No .toISOString() in display-layer files — render in the user's zone, not UTC. |
no-bare-api-fetch |
off | No bare fetch() to internal API routes — go through the typed client so request/response stay contract-checked. |
configs.recommended wires the severities in the column above. no-bare-api-fetch
ships off by default because the "internal route" shape is project-specific —
turn it on and adapt as needed.
They came out of a large event-sourced TypeScript monorepo where the same review comments kept recurring. Every rule here replaced a recurring human nit with a machine one:
- Replayability (
no-date-in-domain,no-process-env-in-method) — domain logic that reads ambient state can't be deterministically replayed. Inject it. - Error causality (
no-catch-rewrap,require-error-logged) — the two ways teams lose stack traces: re-throwing withoutcause, and swallowing silently. - Localization integrity (
no-hardcoded-string-in-jsx,no-hardcoded-user-message,no-locale-method-without-locale,no-timezone-naive-date) — user-facing copy and formatting belong behind a boundary, not sprinkled through components and throw sites. - Platform hygiene (
no-platform-branch) — one reference implementation, thin per-platform shells; noif (platform)ternaries in shared render code. - CI honesty (
no-test-only) — a focused test passes green while skipping everything else. Ban it at the lint layer.
npm install
npm test # vitest, 94 tests via @typescript-eslint/rule-tester
npm run build # emits dist/ (ESM + .d.ts)
npm run typecheckMIT © ghostON3