.cursorrules · git:20260619.6688c92 · 2026-06-19 · sha256 443e37ede44e8392
.cursorrules git:20260619.6688c92A
Immutable. This exact content never changes and is served at /api/v1/blob/443e37ede44e8392.
# Cursor Rules: webjs app
You are working on a webjs app, an AI-first, no-build, web-components-first
framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
project-specific conventions before writing any code. When AGENTS.md doesn't
cover what you need, the full hosted docs are at **https://docs.webjs.com**.
## Persistence + scaffold rules (non-negotiable)
- **Use Drizzle + SQLite for data, never JSON files.** It's already wired up
(`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate` + `npm run db:migrate`). For ANY
data the app stores (todos, posts, messages, products, comments…),
define a Drizzle table. NEVER create `data/*.json`, `db.json`, or any
JSON file as a fake database. NEVER use module-scope arrays / Maps as
a substitute. NEVER use localStorage for app data. These are project conventions in CONVENTIONS.md (a JSON file used as a
database resets on reload and cannot scale).
- **The scaffold is reference, not the final product.** Replace
`app/page.ts`, the example `User` model, the example users module, etc.
with the app the user actually asked for. Don't ship "Hello from
<app-name>" as the deliverable.
- **Only three templates exist:** `webjs create <name>` (default
full-stack), `--template api`, `--template saas`. The CLI rejects any
other `--template` value. Pick:
- Any product UI (todo, blog, dashboard, marketplace, social…) → default
- HTTP/JSON API only, no UI → `--template api`
- Auth / login / signup / SaaS → `--template saas`
## Before starting ANY work
FIRST, before writing any code:
1. Run `git branch --show-current` to check the branch.
- If on main/master: STOP. Ask the user which branch to use, or create
one with `git checkout -b feature/<name>`.
- If on a feature branch: verify it matches the current task. Ask if unsure.
2. Sync with parent: `git fetch origin && git log HEAD..origin/main --oneline`
- If upstream has new commits: `git rebase origin/main` before starting.
- Resolve any conflicts before proceeding with the task.
## Autonomous mode (sandbox / no-prompt)
If running without interactive approval, auto-decide:
- On main? Auto-create feature/<task-slug> branch
- Parent has new commits? Auto-rebase before starting
- Merge? Auto-merge in autonomous mode, delete feature branches after
- Commit message? Auto-generate (meaningful, no AI attribution)
- Tests failing? Fix them. Convention violations? Fix them.
Quality bar stays the same, no blocking on questions.
## Mandatory workflow (never skip)
Every code change must include:
1. Server tests in `test/<feature>/*.test.ts` (node:test).
2. Browser tests in `test/<feature>/browser/*.test.js` (WTR + Playwright, real Chromium).
3. Documentation updates. Walk every surface in the **Definition of done**
section of CONVENTIONS.md (AGENTS.md, CONVENTIONS.md, README.md, docs/,
website/, scaffold scripts) and either update it or write
"N/A because <reason>" in the PR body. Docs land on the same PR as the
code, never as a follow-up.
4. Convention check: `webjs check` must pass.
5. Pre-merge self-review loop. Before saying the PR is ready for merge, run
fresh-context review rounds until one round finds zero issues. Cursor
primitive: open a NEW composer tab and prompt the review there so the
reviewer has no prior context on your decisions. Minimum two rounds;
rotate focus each round. Skip the loop only for one-line trivial
changes; skipping on a change that touches logic, public surface, build,
security, or multiple files is the exact failure mode the loop exists
to prevent. The full rule, prompt template, and reporting contract live
in the **Pre-merge self-review loop** section of CONVENTIONS.md.
The user should never have to ask for tests, documentation, or the
self-review loop.
## Git rules
- COMMIT AND PUSH PER LOGICAL UNIT, NOT AT THE END. One feature, one fix,
one rename, one doc rewrite per commit. Always `git push` after
committing. The user should never have to ask for a commit.
- HARD LIMIT: if you have 5+ unstaged files spanning different concerns,
commit before continuing. Cursor 1.7+ has its own
`.cursor/hooks/nudge-uncommitted.sh` (afterFileEdit) firing at threshold
4; the same enforcement runs for Claude users via
`.claude/hooks/nudge-uncommitted.sh`. On older Cursor versions without
the hook, self-enforce the same rule. Batching multiple logical units
into one commit is the failure mode this rule exists to prevent.
- Write meaningful commit messages: what changed and why, not "update files"
- NEVER add "Co-Authored-By", "Generated by", "AI-assisted" or similar
attribution trailers to commits
- NEVER use em-dashes (U+2014), a hyphen-as-pause (` - `), or a
semicolon-as-pause (` ; `) in commit messages or anywhere else.
Rewrite the sentence so no pause-punctuation crutch is needed.
Use a period, comma, colon, parentheses, or a restructured phrasing.
Plain hyphens stay fine in compound words, CLI flags, filenames,
and ranges. Semicolons stay fine inside code
- Work on feature branches, not main
- NEVER push directly to main. Create a pull request instead.
- NEVER merge any branch without explicit user permission. Always ask:
"Ready to merge <branch> into <target>? Delete or keep <branch> after?"
Wait for approval AND the delete/keep preference before proceeding.
This applies to ALL merges, not just merges into main.
- Run tests before every commit
## Framework rules
- No build step: source files are served as ES modules
- **Erasable TypeScript only.** The runtime strips types via `module.stripTypeScriptTypes` (Node's built-in, or `amaro` on Bun) (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and has no bundler fallback.
- Web components with shadow DOM: use `static styles = css` not inline styles
- One function per server action file (*.server.ts)
- Components must call customElements.define('tag', Class)
- Server-only code (the DB driver, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. The DB lives in db/*.server.ts; lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file: if a lib/ file needs Node APIs, only import it from server-only files.
- Directives: webjs exports the lit directives with no clean native equivalent (`repeat`, `unsafeHTML`, `live`, `keyed`, `guard`, `cache`, `until`, `ref` / `createRef`, `templateContent`, `asyncAppend` / `asyncReplace`, `watch`). Lit's `classMap` / `styleMap` / `ifDefined` / `when` / `choose` are NOT exported. For those, use plain template-literal expressions (`class=${cond ? 'a' : 'b'}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead.
- **Progressive enhancement is the default.** Pages AND every web component are SSR'd to real HTML. Write components so the first paint is the right content. Read SSR-meaningful defaults in `constructor()`. `connectedCallback` is never called on the server, so anything there only runs after hydration. Initial data for components comes from the page function (server-side fetch plus pass as attribute/property), NOT from `fetch` calls in `connectedCallback`. For write-paths, prefer `<form action=...>` plus server action over `fetch` plus click handler. The framework upgrades plain forms to partial-swap submissions automatically.
- **Client navigation is auto-magic.** Real `<a href>` and `<form action>` get partial-swap behavior with no opt-in. Because layouts persist across navigation, put shared chrome (sidenav, header) in `layout.ts` and page-specific content in `page.ts`. For validation errors, return 4xx HTML from a `route.ts` POST handler; the router renders it in place preserving the user's input. For non-layout swap regions, wrap in `<webjs-frame id="...">`. See "Client navigation patterns" in AGENTS.md.
- See AGENTS.md for the complete directive decision guide