.cursorrules · git:20260821.fd1eb37 · 2026-08-21 · sha256 8ee86975065d2cc6
.cursorrules git:20260821.fd1eb37A
Immutable. This exact content never changes and is served at /api/v1/blob/8ee86975065d2cc6.
# 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://webjs.dev/docs**.
## 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.
How a PR gets REVIEWED is deliberately not specified here. Use whatever your
team already does. WebJs has opinions about the code (the conventions above,
`webjs check`, the test layers) and none about your review process.
The user should never have to ask for tests or documentation.
## 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