Linter
Refract uses ESLint with the flat config format and Prettier for formatting. Every package has its own config, but they all follow the same conventions. Here’s everything you need to know to stay in sync with the linter — and to make Cursor catch problems before you even save a file.ESLint configuration
Refract uses ESLint’s flat config format, which means each package has aneslint.config.mjs (or .js) file at its root instead of the older .eslintrc format. Flat config gives you explicit, composable control over which rules apply where.
Backend (apps/backend/eslint.config.mjs)
The backend config extends typescript-eslint’s recommended rules, integrates Prettier (via eslint-config-prettier/flat to turn off any rules that conflict with formatting), and adds import ordering enforcement:
import/order rule enforces alphabetical, grouped imports — so Node built-ins come first, then external packages, then your own files. This keeps diffs clean and makes it obvious at a glance what a file depends on.
Frontend (apps/frontend/eslint.config.js)
The frontend adds React-specific rules on top of the TypeScript base:
react-hooks plugin catches things like missing dependencies in useEffect — the kind of bug that causes subtle stale-closure issues that are painful to debug in production.
Tool packages and shared
Tool packages (apps/tools/*/*) and apps/shared use the same minimal config: typescript-eslint recommended + Prettier + project-aware type checking:
Active rules reference
Here’s what’s actually enforced across the codebase, broken down by package.Rules active in all packages (via typescript-eslint recommended)
| Rule | Severity | What it catches |
|---|---|---|
@typescript-eslint/no-explicit-any | warn | Usage of any type — use unknown or a proper type instead |
@typescript-eslint/no-unused-vars | error | Declared variables or parameters that are never read |
@typescript-eslint/no-require-imports | error | require() calls — use import instead |
@typescript-eslint/ban-ts-comment | error | @ts-ignore without a description — use @ts-expect-error with a reason |
@typescript-eslint/no-unused-expressions | error | Expressions whose result is never used (e.g. foo && bar as a statement) |
@typescript-eslint/no-empty-object-type | error | {} as a type — use object or a specific interface |
no-undef | error | References to undeclared variables |
no-console | warn | Direct console.* calls — use tools.logger instead |
Backend-only rules (apps/backend)
| Rule | Severity | What it catches |
|---|---|---|
import/order | warn | Imports not grouped or alphabetized (builtin → external → internal → parent → sibling → index → type) |
Frontend-only rules (apps/frontend)
| Rule | Severity | What it catches |
|---|---|---|
react-hooks/rules-of-hooks | error | Hooks called conditionally or outside of a component/custom hook |
react-hooks/exhaustive-deps | warn | Missing dependencies in useEffect, useCallback, or useMemo |
react-refresh/only-export-components | warn | Non-component exports from files that Vite’s HMR expects to be component-only |
Boiler package-only rules (apps/boiler)
| Rule | Severity | What it catches |
|---|---|---|
@typescript-eslint/explicit-function-return-type | warn | Functions missing an explicit return type annotation |
Prettier configuration
Every package with a.prettierrc uses the same settings:
arrowParens: "avoid"meansx => xinstead of(x) => xfor single-argument arrow functions.trailingComma: "all"adds trailing commas everywhere valid — including function parameters. This makes multi-line diffs cleaner because adding a new argument doesn’t touch the previous line.printWidth: 100is a soft limit. Prettier will still wrap at shorter lengths when it makes sense.
Running the linter
💡 Tip: if you’re seeing a lot ofimport/ordererrors, run the formatter first — Prettier won’t fix import order, but ESLint can auto-fix it:
Wiring ESLint into Cursor
Getting ESLint inline in Cursor means you see problems as you type, not when CI catches them. Here’s how to set it up:1. Install the ESLint extension
Open Cursor’s extension panel and install ESLint (by Microsoft,dbaeumer.vscode-eslint). It’s the same extension as VS Code.
2. Point it at the right config
Because Refract is a monorepo, you want ESLint to resolve configs relative to the file you’re editing, not the workspace root. Add this to your Cursorsettings.json (Cmd+Shift+P → Preferences: Open User Settings (JSON)):
eslint.workingDirectories tells the extension to run ESLint from each package root when editing files in that package — which is what picks up the correct eslint.config.mjs and tsconfig.json.
3. Enable format on save (optional but recommended)
To have Prettier format automatically when you save:esbenp.prettier-vscode) installed as well.
⚠️ Watch out: ESLint runs on your host machine here, which means it needs access to the project’snode_modules. Runmake pnpm-installfirst to ensure the workspace deps are installed in the host-accessiblenode_modules/folders.
What’s next?
- TypeScript — compiler configuration per package.
- Continuous Integration — how linting fits into the CI pipeline.
- Testing — test setup and coverage.