Skip to content
ƒtsforgev0.52.0
19

Rule packs

7 min read

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.

  1. Always-on safety packs load on every TypeScript project
  2. tsforge detects your stack from dependencies and enables matching framework packs
  3. optional profile in tsforge.config.json adds packs (e.g. typescript-core, authorization) and sets default severities
  4. optional packs.include / packs.exclude fine-tune the list
  5. the gate runs all enabled pack rules

These load without waiting for a dependency match:

IDWhat it covers
env-accessValidated env access, no process.exit in libraries
module-boundariesLayering, no React in services
code-flowDeterministic time/random, early returns
comment-hygieneNo narration, PR refs, or historical comments
securityCommand injection, ReDoS, DOM XSS, silent catch blocks, no tokens in storage
runtime-boundariesOpen 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.)

IDLabelEnabled when
fastifyFastifyfastify in deps
elysiaElysiaelysia in deps
nextjsNext.jsnext in deps
react-component-architectureReact Component Architecturereact in deps
tanstack-queryTanStack Query@tanstack/react-query in deps
drizzleDrizzle ORMdrizzle-orm in deps
bullmqBullMQbullmq in deps
threeThree.jsthree or @react-three/fiber in deps
structured-loggingStructured Loggingpino or winston in deps
jwt-cookiesJWT & Cookiesjsonwebtoken or jose in deps
oauth-securityOAuth SecurityOAuth/OIDC libs in deps
ai-sdkAI SDK Securityai, openai, or @anthropic-ai/sdk in deps
i18n-keysi18n Keysi18next in deps
test-conventionsTest Conventionsvitest/jest/bun test setup
typescript-coreTypeScript Corestrict profile (fetch .ok, JSON validation, boundary casts)
authorizationAuthorizationsecurity profile (experimental route/action authz heuristics)
reactReactdetection label only
generic-tsTypeScript Fundamentalsdetection label only

Every implemented rule with severity and fix text: Rule catalog (grouped by adoption tier).

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.

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.

When the react-component-architecture pack is enabled (React in package.json):

RuleWhat it catches
no-jsx-computation.map() / .filter() / arithmetic / chained logic inside JSX {…}
no-state-in-component-bodyuseState, useEffect, etc. directly in component .tsx files
no-inline-jsx-functionsInline arrow/function handlers in JSX attributes
no-loading-text-use-skeleton"Loading…" text / spinners in loading branches. Render a <Skeleton/> instead
component-file-purityA component .tsx holds only imports + the component. No inline types, constants, or helper functions
component-folder-structureA 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-importsCross-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).

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 variable
const listItems = useMemo(
() => items.filter((i) => i.visible).map((i) => <li key={i.id}>{i.label}</li>),
[items]
);
<ul>{listItems}</ul>

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.tsx
export function Button() {
const [open, setOpen] = useState(false);
return <button>{open ? "Open" : "Closed"}</button>;
}
// ✅ Button.hooks.ts + thin component
export function useButton() {
const [open, setOpen] = useState(false);
return { open, setOpen };
}
// ❌ <button onClick={() => doThing(id)} />
// ✅ const onClickRow = useCallback(() => doThing(id), [id]); … onClick={onClickRow}

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 dispose
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
scene.children.push(mesh);
// ✅ canonical addons import, scene APIs, explicit cleanup
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
scene.add(mesh);
view.dispose();
RuleWhat it catches
no-mixed-three-entrypointsthree/examples/jsm/..., three/src/..., and CDN imports. Autofixes the examples path to three/addons/...
prefer-named-three-importsimport * as THREE from "three" when every use is a static member
no-global-threeBare THREE / require("three") without a package import
no-direct-children-mutationobject.children.push(...) and other array mutations. Autofixes .push to .add
require-projection-updatecamera.aspect = ... without updateProjectionMatrix()
require-three-dispose-contractA class that constructs geometries/materials/textures/renderers with no dispose/destroy
prefer-three-load-asyncCallback loader.load(...) instead of loadAsync()
require-three-loader-error-pathloader.load(url, onLoad) with no onError
require-instance-buffer-updatesetMatrixAt / setColorAt without needsUpdate
no-unbounded-device-pixel-ratiosetPixelRatio(window.devicePixelRatio) without a cap
no-disabled-frustum-cullingfrustumCulled = false

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" } }

Big picture · Meta-rules · Stack detection