# 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 Prisma + SQLite for data, never JSON files.** It's already wired up
  (`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`). For ANY
  data the app stores (todos, posts, messages, products, comments…),
  define a Prisma model. 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.** Node 24+ strips types via `module.stripTypeScriptTypes` (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.
- **Tailwind-first styling.** Tailwind utilities are the strong default for pages AND light-DOM components (the default DOM mode): layout, spacing, color (via `@theme` tokens), typography, borders, radius, shadows, interaction states. Light DOM does not scope, so utilities apply directly. The lit reflex to scope CSS (`static styles = css`) or write an inline `<style>` with semantic class names (`.hero`, `.card`) in a light-DOM component is wrong: the scoped block needs `static shadow = true`, and inline class names leak globally. When a utility bundle repeats, extract a `lib/utils/ui.ts` helper returning an `html` fragment, not a CSS class. Reserve raw CSS for the allowlist (design tokens / `@theme`, `@property` + `@keyframes`, `::-webkit-scrollbar`, `prefers-reduced-motion`, complex `color-mix()` / gradients); when unavoidable in a light-DOM component, prefix every class selector with the component tag.
- Shadow-DOM components opt in with `static shadow = true` and use `static styles = css` for scoped CSS, not inline styles. That is the right home for scoped CSS.
- One function per server action file (*.server.ts)
- Components must call customElements.define('tag', Class)
- Server-only code (@prisma/client, 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. lib/ holds both server-only infra (lib/prisma.server.ts) 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 are deliberately minimal: only `unsafeHTML`, `live`, and `repeat` ship. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported. 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) OR from an `async render()` in the component itself (`const u = await getUser(this.uid)`, which SSR awaits so the data is in the first paint), NOT from `fetch` calls in `connectedCallback`. Prefer the co-located `async render()` over prop-drilling; `renderFallback()` is the optional re-fetch loading state (never first paint), and a `Task` is for genuinely client-only data. 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
