Rule packs
When the gate runs ESLint, it does not use a generic preset. It uses rule packs: groups of rules keyed to the libraries in your project. Stack detection turns packs on from package.json. A profile in tsforge.config.json adjusts default severities and opt-in packs.
Packs apply on every gate check, whether tsforge is editing an existing repo or scaffolding a new one.
How a pack gets enabled
Section titled “How a pack gets enabled”- Always-on safety packs load on every TypeScript project
- tsforge detects your stack from dependencies and enables matching framework packs
- optional
profileintsforge.config.jsonadds packs (e.g.typescript-core,authorization) and sets default severities - optional
packs.include/packs.excludefine-tune the list - the gate runs all enabled pack rules
Always-on packs
Section titled “Always-on packs”These load without waiting for a dependency match:
| ID | What it covers |
|---|---|
env-access | Validated env access, no process.exit in libraries |
module-boundaries | Layering, no React in services |
code-flow | Deterministic time/random, early returns |
comment-hygiene | No narration, PR refs, or historical comments |
security | Command injection, ReDoS, DOM XSS, silent catch blocks, no tokens in storage |
runtime-boundaries | Open redirects, SSRF fetches, prototype pollution, webhook verify, upload limits |
These run on every project alongside tsc; stack detection layers framework packs (react-component-architecture, elysia, nextjs, …) on top. (generic-ts and react in the list below are detection labels only. They name a stack for reporting and carry no rules of their own.)
Pack list
Section titled “Pack list”| ID | Label | Enabled when |
|---|---|---|
fastify | Fastify | fastify in deps |
elysia | Elysia | elysia in deps |
nextjs | Next.js | next in deps |
react-component-architecture | React Component Architecture | react in deps |
tanstack-query | TanStack Query | @tanstack/react-query in deps |
drizzle | Drizzle ORM | drizzle-orm in deps |
bullmq | BullMQ | bullmq in deps |
three | Three.js | three or @react-three/fiber in deps |
structured-logging | Structured Logging | pino or winston in deps |
jwt-cookies | JWT & Cookies | jsonwebtoken or jose in deps |
oauth-security | OAuth Security | OAuth/OIDC libs in deps |
ai-sdk | AI SDK Security | ai, openai, or @anthropic-ai/sdk in deps |
i18n-keys | i18n Keys | i18next in deps |
test-conventions | Test Conventions | vitest/jest/bun test setup |
typescript-core | TypeScript Core | strict profile (fetch .ok, JSON validation, boundary casts) |
authorization | Authorization | security profile (experimental route/action authz heuristics) |
react | React | detection label only |
generic-ts | TypeScript Fundamentals | detection label only |
Every implemented rule with severity and fix text: Rule catalog (grouped by adoption tier).
Bring your own rules
Section titled “Bring your own rules”You are not limited to the built-in packs. Point tsforge.config.json at your own ESLint rule-pack module and tsforge loads it into the gate alongside everything above. No fork required:
{ "plugins": [{ "path": "./tsforge-rules/index.ts" }]}Your pack’s rules run on every validation, exactly like the built-ins. They can’t shadow a built-in rule, and a name collision is reported at startup. This is how you enforce a company’s or repo’s own conventions through tsforge. See Config & external plugins for the full shape.
Web stacks and accessibility
Section titled “Web stacks and accessibility”When stack packs load on a web project, the bundled web ESLint config also enables jsx-a11y rules on .tsx files (alt text, button types, label associations, and similar). This applies to all web stacks, not just Next.js.
React component architecture
Section titled “React component architecture”When the react-component-architecture pack is enabled (React in package.json):
| Rule | What it catches |
|---|---|
no-jsx-computation | .map() / .filter() / arithmetic / chained logic inside JSX {…} |
no-state-in-component-body | useState, useEffect, etc. directly in component .tsx files |
no-inline-jsx-functions | Inline arrow/function handlers in JSX attributes |
no-loading-text-use-skeleton | "Loading…" text / spinners in loading branches. Render a <Skeleton/> instead |
component-file-purity | A component .tsx holds only imports + the component. No inline types, constants, or helper functions |
component-folder-structure | A component .tsx must live in src/views/<Feature>/components/, be the view root src/views/<Feature>/index.tsx, or a shared primitive in src/components/ui/ |
no-cross-feature-imports | Cross-feature runtime imports |
component-file-purity and component-folder-structure enforce a views layout for React code. On a BoringStack build they run at error as part of the project’s own gate. When tsforge edits an existing repo, the architecture rules follow your profile: off in recommended, on in opinionated (or turn individual rules on in rules).
No computation inside JSX
Section titled “No computation inside JSX”Lift .map(), .filter(), arithmetic, and chained logical expressions into a hook or pre-prep variable. JSX is a template.
Disallowed in {…}: array methods (.map, .filter, .reduce, .sort, .find), arithmetic (+, -, *, /), chained logical expressions. Simple ternaries and {show && <Node />} are allowed by default. Story files are exempt.
// ❌<ul>{items.filter((i) => i.visible).map((i) => <li key={i.id}>{i.label}</li>)}</ul>
// ✅ prepare in *.hooks.ts, render a variableconst listItems = useMemo( () => items.filter((i) => i.visible).map((i) => <li key={i.id}>{i.label}</li>), [items]);<ul>{listItems}</ul>State hooks in *.hooks.ts
Section titled “State hooks in *.hooks.ts”In PascalCase component .tsx files, move useState, useEffect, useMemo, useCallback, useLayoutEffect, and useRef to a colocated custom hook. Passthrough hooks (useId, useTransition, useDeferredValue) may stay in the component.
// ❌ useState in Button.tsxexport function Button() { const [open, setOpen] = useState(false); return <button>{open ? "Open" : "Closed"}</button>;}
// ✅ Button.hooks.ts + thin componentexport function useButton() { const [open, setOpen] = useState(false); return { open, setOpen };}Named handler references
Section titled “Named handler references”// ❌ <button onClick={() => doThing(id)} />// ✅ const onClickRow = useCallback(() => doThing(id), [id]); … onClick={onClickRow}Three.js
Section titled “Three.js”The three pack turns on when three or @react-three/fiber is in package.json. Three.js owns the scene graph; application code owns lifecycle. Import from three and three/addons/... only, dispose GPU resources you construct, and mutate hierarchy through add() / remove() rather than children.
// ❌ mixed entrypoints, array mutation, no disposeimport { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";scene.children.push(mesh);
// ✅ canonical addons import, scene APIs, explicit cleanupimport { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";scene.add(mesh);view.dispose();| Rule | What it catches |
|---|---|
no-mixed-three-entrypoints | three/examples/jsm/..., three/src/..., and CDN imports. Autofixes the examples path to three/addons/... |
prefer-named-three-imports | import * as THREE from "three" when every use is a static member |
no-global-three | Bare THREE / require("three") without a package import |
no-direct-children-mutation | object.children.push(...) and other array mutations. Autofixes .push to .add |
require-projection-update | camera.aspect = ... without updateProjectionMatrix() |
require-three-dispose-contract | A class that constructs geometries/materials/textures/renderers with no dispose/destroy |
prefer-three-load-async | Callback loader.load(...) instead of loadAsync() |
require-three-loader-error-path | loader.load(url, onLoad) with no onError |
require-instance-buffer-update | setMatrixAt / setColorAt without needsUpdate |
no-unbounded-device-pixel-ratio | setPixelRatio(window.devicePixelRatio) without a cap |
no-disabled-frustum-culling | frustumCulled = false |
Changing severity
Section titled “Changing severity”Pack rules default to error (gate fails). Profiles pre-set some rules to warn or off. Override any rule in tsforge.config.json:
{ "rules": { "prefer-early-return": "off" } }