@dreamtree-org/config
v0.2.0
Published
Shared ESLint (incl. Dreamtree design-lint), TypeScript, Prettier, Vitest, knip and Playwright presets for every Dreamtree product — one line per tool, per-stack defaults.
Maintainers
Readme
@dreamtree-org/config
Replaces the ESLint, TypeScript, Prettier, Vitest, knip and Playwright config files (and the qc:* scripts) that were copy-pasted into every Dreamtree product. Each tool gets one line per app.
60-second start
npm i -D @dreamtree-org/config| Tool | In the app |
|---|---|
| ESLint | eslint.config.mjs: import dreamtree from '@dreamtree-org/config/eslint'; export default dreamtree({ stack: 'next' }); |
| TypeScript | tsconfig.json: { "extends": "@dreamtree-org/config/tsconfig/next.json" } (or node.json, vite-react.json) |
| Prettier | package.json: "prettier": "@dreamtree-org/config/prettier" |
| Vitest | vitest.config.ts: import { dreamtreeVitest } from '@dreamtree-org/config/vitest'; export default dreamtreeVitest(); |
| knip | knip.config.ts: import { dreamtreeKnip } from '@dreamtree-org/config/knip'; export default dreamtreeKnip({ stack: 'next' }); |
| Playwright | playwright.config.js: export default dreamtreePlaywright({ baseURL, webServer: { env } }) |
| qc scripts | "qc:cycles": "dreamtree-qc cycles", "qc:baseline": "dreamtree-qc baseline", "qc:deadcode": "knip" |
The tools themselves are optional peer dependencies: install only what your stack uses.
| Stack | Peers it needs |
|---|---|
| next | eslint, eslint-config-next, eslint-config-prettier, typescript |
| express | eslint, @eslint/js, globals; eslint-plugin-sonarjs (used when installed) |
| vite-react | eslint, @eslint/js, globals, eslint-plugin-react; eslint-plugin-react-hooks (used when installed) |
| any + typescript: true | typescript-eslint |
| Vitest / Playwright / knip / Prettier / qc | vitest / @playwright/test / knip / prettier / madge |
A missing peer fails with the command to fix it: dreamtree({ stack: 'next' }): "eslint-config-next" is not installed — run \npm i -D eslint-config-next` in the app.`
ESLint — dreamtree(options)
| Key | Type | Default | Meaning |
|---|---|---|---|
| stack | 'next' \| 'express' \| 'vite-react' | required | Which preset. |
| ignores | string[] | [] | Appended to the stack's global ignores. |
| overrides | object[] | [] | Flat-config objects appended after the preset, e.g. { files: ['lib/mock/**'], rules: { 'max-lines': 'off' } }. |
| legacyFiles | string[] | [] | Files whose error rules drop to warnings (your eslint.legacy.mjs list). Never add to it. |
| relaxedFiles | string[] | tests, scripts, commands | Files with size/complexity/any/console rules off. Replaces the default. |
| nativeDialogs | boolean | on for next, vite-react | Ban alert/confirm/prompt, including window.confirm, self.prompt and const w = window. |
| dialogHint | string | 'use the twreact-ui Dialog and Toast' | What the lint message tells people to use instead. |
| serverOnlyImports | string[] | [] | next: appended to the modules client code may not import (knex, mysql2, ioredis, bullmq, @/lib/tenancy/*, @/lib/platform/*, @/lib/jobs/*, @/models/*). |
| serverDirs | string[] | ['lib','models','app/api'] | next: no-console is an error here. |
| domainDirs | string[] | ['lib'] | next: .ts functions capped at 40 lines (components keep 60). |
| clientDirs | string[] | ['components','lib/hooks'] | next: may not import server-only modules. |
| sonar | boolean | true | express: add sonarjs quality gates when the plugin is installed. |
| module | 'cjs' \| 'esm' | 'cjs' | express: source type of server code. |
| client | string | — | express: a Vite/React folder in the same repo (e.g. 'client'); gets the vite-react rules. |
| typescript | boolean | false | express / vite-react: add typescript-eslint strict rules. |
| design | boolean \| object | false | Add the Dreamtree design rules (see below). true, or the dreamtreeDesign options. |
Design rules — design: true or @dreamtree-org/config/eslint/design
The rules in twreact-ui's DESIGN.md, as lint. Every message names the fix, so people and AI agents (Dreamtree Studio's validate_screen) can correct the code from the lint output alone.
| Rule | Catches | Message suggests |
|---|---|---|
| dreamtree-design/no-raw-colors | bg-blue-600, text-slate-500, border-[#19a249], style={{ color: '#f00' }} in class strings (className, cn(), clsx(), cva()) | the token class (bg-primary-600, text-secondary-500) or a role (bg-surface, text-muted) |
| dreamtree-design/no-arbitrary-values | text-[11px], w-[312px] (grid templates allowed) | text-micro, text-xs, rounded-card, or the Tailwind scale |
| dreamtree-design/allowed-imports | other UI kits (@mui, antd, @radix-ui, react-icons, …), or anything outside allowImports when set | twreact-ui components and blocks, lucide-react |
| dreamtree-design/require-states (warn) | a default-exported page that renders a list with no loading, empty or error branch | LoadingState, EmptyState, ErrorState |
| native dialogs | alert / confirm / prompt | ConfirmDialog and Toast |
// Product: opt in next to the stack preset (start with severity 'warn' and ratchet).
export default dreamtree({ stack: 'next', design: { severity: 'warn' } });
// Standalone (Dreamtree Studio prototypes): strict import allowlist.
import { dreamtreeDesign } from '@dreamtree-org/config/eslint/design';
export default dreamtreeDesign({ allowImports: ['react', '@dreamtree-org/twreact-ui', 'lucide-react'] });Spot check on 20 Oct 2026: school360 → 4,163 raw colours, 1,322 arbitrary values, 30 alert(); PracticePro → 1,646 raw colours, 40 arbitrary values.
What each stack enforces
| | next | express | vite-react |
|---|---|---|---|
| Base | eslint-config-next core-web-vitals + typescript, eslint-config-prettier | @eslint/js recommended, sonarjs recommended, Node globals | @eslint/js recommended, React JSX, browser globals |
| Function size | 60 lines (40 in lib/**/*.ts) | 120 | 120 |
| Complexity | 12, max-params 3, max-depth 3 | 15 (+ sonar cognitive 15) | 15 |
| File size | 400 lines, 250 for .tsx | — | — |
| Empty catch | error | allowed (best-effort swallows) | allowed |
| any | error | — (error with typescript: true) | — (error with typescript: true) |
| Native dialogs | error | — | error |
| Other | no as unknown as, no thrown object literals, no server-only imports in client code, no console in server code | no-eval family, no thrown literals | no module.exports in Vite src, no-eval family |
TypeScript
tsconfig/next.json (Next apps), tsconfig/node.json (TS services: strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes), tsconfig/vite-react.json (node.json + DOM, react-jsx, vite/client, noEmit).
paths, include and exclude resolve relative to the file that declares them, so they stay in the app:
{
"extends": "@dreamtree-org/config/tsconfig/next.json",
"compilerOptions": { "paths": { "@/*": ["./*"] } },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}Prettier
"prettier": "@dreamtree-org/config/prettier" — printWidth 120, double quotes, trailing commas (Next apps).
"prettier": "@dreamtree-org/config/prettier/single-quote" — printWidth 100, single quotes (TS monorepos).
Keep .prettierignore in the app; Prettier has no shared ignore.
Vitest — dreamtreeVitest(options)
| Key | Type | Default | Meaning |
|---|---|---|---|
| root | string | process.cwd() | Alias targets are relative to this. Pass __dirname. |
| environment | string | 'node' | 'jsdom' for React client tests. |
| include | string[] | ['**/*.test.ts','**/*.test.tsx'] | |
| exclude | string[] | [] | Appended to node_modules, .next, dist, coverage, scripts, graphify-out. |
| alias | Record<string,string> | { '@': '.' } | Mirror your vite.config.js aliases here. |
| serverOnlyStub | boolean | true | Alias server-only to an empty module shipped by this package. |
| globals, setupFiles, plugins | | — | Passed through. |
| timeout | number | — | Sets both testTimeout and hookTimeout. |
| coverage | object \| false | v8 over lib/**, models/** | Merged into the defaults. |
| test, resolve | object | — | Any other Vitest keys, merged last. |
knip — dreamtreeKnip(options)
knip.json cannot extend, so use knip.config.ts:
import { dreamtreeKnip } from '@dreamtree-org/config/knip';
export default dreamtreeKnip({ stack: 'next', entry: ['korm-mcp.config.cjs'], ignoreDependencies: ['archiver'] });entry and ignore are appended to the stack base (next: models/**/*.js, lib/api.ts); any other knip key passes through.
Playwright — dreamtreePlaywright(options)
import { dreamtreePlaywright } from '@dreamtree-org/config/playwright';
const PORT = process.env.E2E_PORT || 5002;
export default dreamtreePlaywright({
baseURL: `http://localhost:${PORT}`,
webServer: { env: { PORT: String(PORT), NODE_ENV: 'test' } },
});Defaults: testDir: './tests/e2e', 30 s timeout, serial, github reporter on CI else list, trace: 'on-first-retry', desktop (1440×900) + mobile (Pixel 7) projects, webServer npm start waiting on ${baseURL}/health. Every key can be overridden (mobile: false, projects, use, …).
Recipes
Next (firepro, CampaignPro, procurementpro)
import dreamtree from '@dreamtree-org/config/eslint';
import legacyFiles from './eslint.legacy.mjs';
export default dreamtree({ stack: 'next', serverOnlyImports: ['firebase-admin'], legacyFiles });Express + Vite client (taskpro, venturepro, billpro, ai-agent-builder) — CommonJS root:
const { dreamtree } = require('@dreamtree-org/config/eslint');
module.exports = dreamtree({ stack: 'express', client: 'client', ignores: ['schema/sync.json'] });client/vitest.config.js:
import react from '@vitejs/plugin-react';
import { dreamtreeVitest } from '@dreamtree-org/config/vitest';
export default dreamtreeVitest({
root: __dirname, environment: 'jsdom', globals: true, plugins: [react()],
include: ['src/**/*.test.{js,jsx}'], alias: { '@': 'src', '@api': 'src/api' },
serverOnlyStub: false, coverage: false, timeout: 30_000,
});TS monorepo (fitweavepro, runloompro) — dreamtree({ stack: 'express', module: 'esm', typescript: true, sonar: false }), extends: "@dreamtree-org/config/tsconfig/node.json", "prettier": "@dreamtree-org/config/prettier/single-quote".
What it deliberately does NOT do
- No product rules. Ticket-specific bans (appforge's
PRE-110import walls, runloompro's Temporal determinism rules, ai-agent-builder's Chrome-extension globals) stay in the app asoverrides. - No
.prettierignore,paths,includeor knipentrylists: they are paths into the app. - No Jest preset and no
vite.config.js— only the test half of Vite. - It never relaxes a rule silently. If an app was looser than the preset, the difference shows up as lint findings, not as a hidden override.
