@webjsdev/cli 0.10.17 → 0.10.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { delimiter, dirname, join } from 'node:path';
3
+
4
+ /**
5
+ * Build a PATH the way `npm run` does: prepend every ANCESTOR
6
+ * `node_modules/.bin` (the app's, then up to the repo root for a hoisted
7
+ * monorepo) so a `before` / `parallel` command naming a LOCAL-only binary
8
+ * (`drizzle-kit`, `tailwindcss`) resolves under a bare `webjs dev` / `start`, exactly
9
+ * as it does under `npm run dev`. Without this a bare `webjs dev` exits 127 on
10
+ * the first such step and aborts the boot, defeating the whole #550 point.
11
+ *
12
+ * @param {string} cwd
13
+ * @param {NodeJS.ProcessEnv} [env]
14
+ */
15
+ function envWithLocalBin(cwd, env = process.env) {
16
+ const bins = [];
17
+ let dir = cwd;
18
+ // Walk up to the filesystem root, collecting each node_modules/.bin.
19
+ for (;;) {
20
+ bins.push(join(dir, 'node_modules', '.bin'));
21
+ const parent = dirname(dir);
22
+ if (parent === dir) break;
23
+ dir = parent;
24
+ }
25
+ return { ...env, PATH: [...bins, env.PATH || ''].join(delimiter) };
26
+ }
27
+
28
+ /**
29
+ * Run the configured `before` steps (#550) sequentially to completion. Returns
30
+ * the FIRST failure so the caller can abort the boot, or `{ ok: true }`. Pure of
31
+ * `process.exit` and `console` (the bin owns the exit code + logging via the
32
+ * `onStep` hook) so the orchestration is deterministically unit-testable, with
33
+ * `spawn` injectable for tests.
34
+ *
35
+ * @param {string[]} steps
36
+ * @param {string} cwd
37
+ * @param {{ spawn?: typeof nodeSpawn, onStep?: (step: string) => void }} [opts]
38
+ * @returns {Promise<{ ok: true } | { ok: false, step: string, code: number }>}
39
+ */
40
+ export async function runBeforeSteps(steps, cwd, opts = {}) {
41
+ const spawn = opts.spawn || nodeSpawn;
42
+ const env = envWithLocalBin(cwd);
43
+ for (const step of steps) {
44
+ if (opts.onStep) opts.onStep(step);
45
+ const code = await new Promise((res) => {
46
+ const c = spawn(step, { shell: true, stdio: 'inherit', cwd, env });
47
+ c.on('exit', (code) => res(code ?? 0));
48
+ c.on('error', () => res(1));
49
+ });
50
+ if (code !== 0) return { ok: false, step, code };
51
+ }
52
+ return { ok: true };
53
+ }
54
+
55
+ /**
56
+ * Spawn the configured dev `parallel` tasks (#550) as long-lived children and
57
+ * return a killer that tears them ALL down (idempotent), so a watcher cannot
58
+ * leak past the dev server. `spawn` is injectable for tests.
59
+ *
60
+ * @param {string[]} commands
61
+ * @param {string} cwd
62
+ * @param {{ spawn?: typeof nodeSpawn, onStart?: (cmd: string) => void }} [opts]
63
+ * @returns {() => void}
64
+ */
65
+ export function startParallelTasks(commands, cwd, opts = {}) {
66
+ const spawn = opts.spawn || nodeSpawn;
67
+ const env = envWithLocalBin(cwd);
68
+ const children = commands.map((cmd) => {
69
+ if (opts.onStart) opts.onStart(cmd);
70
+ // `detached: true` puts the child in its OWN process group, so the killer
71
+ // can take down the whole tree (the `sh -c` wrapper AND the watcher it
72
+ // spawns, e.g. tailwindcss) rather than just the shell, which would leak the
73
+ // watcher as an orphan.
74
+ return spawn(cmd, { shell: true, stdio: 'inherit', cwd, env, detached: true });
75
+ });
76
+ let killed = false;
77
+ return () => {
78
+ if (killed) return;
79
+ killed = true;
80
+ for (const c of children) killChildTree(c);
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Tear down a shell-spawned child's whole process GROUP. A `sh -c '<watcher>'`
86
+ * child run with `detached: true` is a group leader, so a NEGATIVE pid signals
87
+ * the group (the shell + the watcher). Falls back to a direct `kill()` when
88
+ * there is no numeric pid (a fake child in a test) or the group kill is
89
+ * unsupported (a non-POSIX runtime), so the killer never throws.
90
+ *
91
+ * @param {import('node:child_process').ChildProcess} child
92
+ */
93
+ function killChildTree(child) {
94
+ try {
95
+ if (typeof child.pid === 'number') process.kill(-child.pid, 'SIGTERM');
96
+ else child.kill();
97
+ } catch {
98
+ try { child.kill(); } catch {}
99
+ }
100
+ }
@@ -50,16 +50,10 @@ export async function writeSaasFiles(appDir) {
50
50
  // the saas auth pages use raw <form> + label/input class helpers instead.
51
51
  await copyUiComponents(appDir, ['dialog', 'switch', 'checkbox']);
52
52
 
53
- // lib/prisma.server.ts
53
+ // The db/ layer (columns/connection) is written by the full-stack scaffold
54
+ // already; this template overwrites db/schema.server.ts below to add the
55
+ // User.passwordHash column auth needs.
54
56
  await mkdir(join(appDir, 'lib'), { recursive: true });
55
- await writeFile(join(appDir, 'lib', 'prisma.server.ts'), [
56
- "import { PrismaClient } from '@prisma/client';",
57
- "",
58
- "const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };",
59
- "export const prisma = globalForPrisma.prisma || new PrismaClient();",
60
- "if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;",
61
- "",
62
- ].join('\n'));
63
57
 
64
58
  // lib/password.server.ts
65
59
  await writeFile(join(appDir, 'lib', 'password.server.ts'), [
@@ -85,14 +79,14 @@ export async function writeSaasFiles(appDir) {
85
79
  // lib/auth.server.ts
86
80
  await writeFile(join(appDir, 'lib', 'auth.server.ts'), [
87
81
  "import { createAuth, Credentials } from '@webjsdev/server';",
88
- "import { prisma } from './prisma.server.ts';",
82
+ "import { db } from '../db/connection.server.ts';",
89
83
  "import { compare } from './password.server.ts';",
90
84
  "",
91
85
  "export const { auth, signIn, signOut, handlers } = createAuth({",
92
86
  " providers: [",
93
87
  " Credentials({",
94
88
  " async authorize(credentials: { email: string; password: string }) {",
95
- " const user = await prisma.user.findUnique({ where: { email: credentials.email } });",
89
+ " const user = await db.query.users.findFirst({ where: { email: credentials.email } });",
96
90
  " if (!user || !await compare(credentials.password, user.passwordHash)) return null;",
97
91
  " return { id: String(user.id), name: user.name, email: user.email };",
98
92
  " },",
@@ -103,26 +97,24 @@ export async function writeSaasFiles(appDir) {
103
97
  "",
104
98
  ].join('\n'));
105
99
 
106
- // prisma/schema.prisma
107
- await mkdir(join(appDir, 'prisma'), { recursive: true });
108
- await writeFile(join(appDir, 'prisma', 'schema.prisma'), [
109
- 'datasource db {',
110
- ' provider = "sqlite"',
111
- ' url = env("DATABASE_URL")',
112
- '}',
113
- '',
114
- 'generator client {',
115
- ' provider = "prisma-client-js"',
116
- '}',
117
- '',
118
- 'model User {',
119
- ' id Int @id @default(autoincrement())',
120
- ' email String @unique',
121
- ' name String?',
122
- ' passwordHash String',
123
- ' createdAt DateTime @default(now())',
124
- '}',
125
- '',
100
+ // db/schema.server.ts: overwrite the full-stack scaffold's example User to
101
+ // add passwordHash (the column auth needs). Drizzle, dialect-agnostic.
102
+ await writeFile(join(appDir, 'db', 'schema.server.ts'), [
103
+ "import { defineRelations } from 'drizzle-orm';",
104
+ "import { table, pk, text, createdAt } from './columns.server.ts';",
105
+ "",
106
+ "export const users = table('users', {",
107
+ " id: pk(),",
108
+ " email: text().notNull().unique(),",
109
+ " name: text(),",
110
+ " passwordHash: text().notNull(),",
111
+ " createdAt: createdAt(),",
112
+ "});",
113
+ "",
114
+ "export const relations = defineRelations({ users }, () => ({}));",
115
+ "",
116
+ "export type User = typeof users.$inferSelect;",
117
+ "",
126
118
  ].join('\n'));
127
119
 
128
120
  // modules/auth/actions/signup.server.ts
@@ -132,15 +124,14 @@ export async function writeSaasFiles(appDir) {
132
124
  await writeFile(join(appDir, 'modules', 'auth', 'actions', 'signup.server.ts'), [
133
125
  "'use server';",
134
126
  "",
135
- "import { prisma } from '../../../lib/prisma.server.ts';",
127
+ "import { db } from '../../../db/connection.server.ts';",
128
+ "import { users } from '../../../db/schema.server.ts';",
136
129
  "import { hash } from '../../../lib/password.server.ts';",
137
130
  "",
138
131
  "export async function signup(input: { name: string; email: string; password: string }) {",
139
- " const exists = await prisma.user.findUnique({ where: { email: input.email } });",
132
+ " const exists = await db.query.users.findFirst({ where: { email: input.email }, columns: { id: true } });",
140
133
  " if (exists) return { success: false as const, error: 'Email already registered', status: 409 };",
141
- " const user = await prisma.user.create({",
142
- " data: { name: input.name, email: input.email, passwordHash: await hash(input.password) },",
143
- " });",
134
+ " const [user] = await db.insert(users).values({ name: input.name, email: input.email, passwordHash: await hash(input.password) }).returning();",
144
135
  " return { success: true as const, data: { id: user.id, name: user.name, email: user.email } };",
145
136
  "}",
146
137
  "",
@@ -183,11 +174,10 @@ export async function writeSaasFiles(appDir) {
183
174
  // - The protected-route gate (unauthenticated /dashboard -> 302 /login) runs
184
175
  // ALWAYS once the app modules import: auth() only reads a cookie, no DB
185
176
  // query. This is the headline security assertion and it is REAL.
186
- // - The signup -> login -> protected-route flow writes + reads a user, so it
187
- // needs Prisma generated AND migrated (`npm run db:generate` +
188
- // `npm run db:migrate`). When the Prisma client is not yet generated the
189
- // app modules can't import at all, so the whole suite skips with a clear
190
- // message instead of crashing. After you set up the DB it runs for real.
177
+ // The signup, login, and protected-route flow writes + reads a user, so it
178
+ // needs the DB migrated (`npm run db:generate` then `npm run db:migrate`).
179
+ // Until the users table exists those flows error, so the suite skips with a
180
+ // clear message instead of crashing. After DB setup it runs for real.
191
181
  await mkdir(join(appDir, 'test', 'auth'), { recursive: true });
192
182
  await writeFile(join(appDir, 'test', 'auth', 'auth.test.ts'), [
193
183
  "import { test } from 'node:test';",
@@ -200,20 +190,20 @@ export async function writeSaasFiles(appDir) {
200
190
  "",
201
191
  "const appDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');",
202
192
  "",
203
- "// The auth pages + dashboard middleware import lib/prisma.server.ts, which",
204
- "// imports @prisma/client. Until `npm run db:generate` (prisma generate) has",
205
- "// run, that import is missing, so a request hitting those modules 500s. We",
206
- "// detect that at the RESPONSE level (a 5xx on the dashboard) and SKIP with a",
207
- "// clear message rather than reporting a misleading failure. After you run",
193
+ "// The auth pages + dashboard middleware query the users table via Drizzle.",
194
+ "// Until `npm run db:generate` + `npm run db:migrate` have created it, a",
195
+ "// request hitting those modules 500s. We detect that at the RESPONSE level",
196
+ "// (a 5xx on the dashboard) and SKIP with a clear message rather than report",
197
+ "// a misleading failure. After you run",
208
198
  "// npm install && npm run db:generate && npm run db:migrate",
209
199
  "// every assertion below runs for real.",
210
200
  "process.env.DATABASE_URL ||= 'file:./dev.db';",
211
201
  "process.env.AUTH_SECRET ||= 'test-secret-at-least-32-characters-long!!';",
212
202
  "",
213
203
  "function makeHandler() {",
214
- " // createRequestHandler builds lazily, so it succeeds even before prisma is",
215
- " // generated; the missing dependency only surfaces when a request reaches",
216
- " // the prisma-importing module. That is why readiness is probed per-response.",
204
+ " // createRequestHandler builds lazily, so it succeeds even before the DB",
205
+ " // is migrated; the missing table only surfaces when a request reaches a",
206
+ " // module that queries it. That is why readiness is probed per-response.",
217
207
  " return createRequestHandler({ appDir, dev: true });",
218
208
  "}",
219
209
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.17",
3
+ "version": "0.10.18",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -7,10 +7,10 @@ cover what you need, the full hosted docs are at **https://docs.webjs.com**.
7
7
 
8
8
  ## Persistence + scaffold rules (non-negotiable)
9
9
 
10
- - **Use Prisma + SQLite for data, never JSON files.** It is already wired up
11
- (`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`). For
10
+ - **Use Drizzle + SQLite for data, never JSON files.** It is already wired up
11
+ (`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate` + `npm run db:migrate`). For
12
12
  ANY data the app stores (todos, posts, messages, products, comments), define
13
- a Prisma model. NEVER create `data/*.json`, `db.json`, or any JSON file as a
13
+ a Drizzle table. NEVER create `data/*.json`, `db.json`, or any JSON file as a
14
14
  fake database. NEVER use module-scope arrays / Maps as a substitute. NEVER
15
15
  use localStorage for app data. These are project conventions in
16
16
  CONVENTIONS.md (a JSON file used as a database resets on reload and
@@ -131,12 +131,12 @@ self-review loop.
131
131
  - Custom-element tag names are passed to `.register('tag-name')`. They are NOT
132
132
  a static field on the class.
133
133
  - One function per server action file (`*.server.ts`).
134
- - Server-only code (`@prisma/client`, `node:*`, anything that needs Node APIs)
134
+ - Server-only code (a DB driver like `better-sqlite3`/`pg`, `node:*`, anything that needs Node APIs)
135
135
  goes only in `.server.{js,ts}` files, `route.ts` handlers, or
136
136
  `middleware.ts`. Never in pages, layouts, or components. Wrap the access in
137
137
  a `.server.{js,ts}` file; the framework rewrites that import into an RPC
138
138
  stub for the browser. `lib/` holds both server-only infra
139
- (`lib/prisma.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with
139
+ (the DB in `db/*.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with
140
140
  `cn`); follow the same rule per file.
141
141
  - Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat`
142
142
  ship. Use plain template-literal expressions
@@ -23,5 +23,5 @@ AUTH_SECRET=
23
23
  # REDIS_URL=redis://localhost:6379
24
24
 
25
25
  # ── Database ────────────────────────────────────────────────────────
26
- # Used by Prisma. SQLite for dev, PostgreSQL/MySQL for production.
27
- DATABASE_URL=file:./dev.db
26
+ # Used by Drizzle. SQLite for dev, PostgreSQL for production (--db postgres).
27
+ DATABASE_URL=file:./db/dev.db
@@ -7,10 +7,10 @@ the full hosted docs are at **https://docs.webjs.com**.
7
7
 
8
8
  ## Persistence + scaffold rules (non-negotiable)
9
9
 
10
- - **Use Prisma + SQLite for data, never JSON files.** It's already wired up
11
- (`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`). For ANY
10
+ - **Use Drizzle + SQLite for data, never JSON files.** It's already wired up
11
+ (`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate` + `npm run db:migrate`). For ANY
12
12
  data the app stores (todos, posts, messages, products, comments…),
13
- define a Prisma model. NEVER create `data/*.json`, `db.json`, or any
13
+ define a Drizzle table. NEVER create `data/*.json`, `db.json`, or any
14
14
  JSON file as a fake database. NEVER use module-scope arrays / Maps as
15
15
  a substitute. NEVER use localStorage for app data. It resets on reload and cannot scale. This is a project convention
16
16
  (CONVENTIONS.md).
@@ -104,7 +104,7 @@ each change must include.
104
104
  - Components: extend WebComponent, declare `static properties` (and `static styles` for shadow-DOM components), call `Class.register('tag-name')` at the bottom of the file. The tag name is the argument to `.register()`, not a static field.
105
105
  - Async data in a component: prefer an `async render()` (`const u = await getUser(this.uid)`), which SSR awaits so the data is in the first paint, over prop-drilling from the page or fetching in `connectedCallback`. `renderFallback()` is the optional re-fetch loading state (never first paint); error isolation is automatic (`renderError()` customizes it); a `Task` is for genuinely client-only data.
106
106
  - Server actions: *.server.ts files with one exported async function each.
107
- - Server-only code (@prisma/client, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to 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); apply the same rule per file.
107
+ - Server-only code (a DB driver like better-sqlite3/pg, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to 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); apply the same rule per file.
108
108
  - Directives: webjs ships only `unsafeHTML`, `live`, and `repeat`. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported. Use plain template-literal expressions and lifecycle hooks instead.
109
109
  - Context: import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context'
110
110
  - Task: import { Task, TaskStatus } from '@webjsdev/core/task'
@@ -45,9 +45,8 @@ jobs:
45
45
  node-version: '24'
46
46
  cache: npm
47
47
  - run: npm ci
48
- - run: npx prisma generate
49
- - name: Apply migrations to the test database
50
- run: npx prisma migrate deploy
48
+ - name: Set up the database (generate + apply migrations)
49
+ run: npm run db:generate && npm run db:migrate
51
50
  env:
52
51
  DATABASE_URL: file:./ci.db
53
52
  # --server keeps this job to node:test (the browser layer is its own
@@ -66,7 +65,6 @@ jobs:
66
65
  node-version: '24'
67
66
  cache: npm
68
67
  - run: npm ci
69
- - run: npx prisma generate
70
68
  - name: Install Playwright Chromium
71
69
  run: npx playwright install --with-deps chromium
72
70
  - run: npm run test:browser
@@ -81,9 +79,8 @@ jobs:
81
79
  node-version: '24'
82
80
  cache: npm
83
81
  - run: npm ci
84
- - run: npx prisma generate
85
- - name: Apply migrations to the test database
86
- run: npx prisma migrate deploy
82
+ - name: Set up the database (generate + apply migrations)
83
+ run: npm run db:generate && npm run db:migrate
87
84
  env:
88
85
  DATABASE_URL: file:./ci.db
89
86
  # The scaffold's e2e test (test/hello/e2e/) drives a real browser
@@ -11,7 +11,7 @@ companion and reach for docs.webjs.com whenever you need more detail.
11
11
 
12
12
  This project was created with `webjs create`. The files you see right
13
13
  now (`app/page.ts` printing "Hello from {{APP_NAME}}", the example `User`
14
- model in `prisma/schema.prisma`, the `theme-toggle` component, the
14
+ model in `db/schema.server.ts`, the `theme-toggle` component, the
15
15
  example users module in api/saas templates) are **starting-point
16
16
  references, not the final product**. Your job is to replace them with
17
17
  the app the user actually asked for. That includes adapting
@@ -30,11 +30,11 @@ user asked for, never leftover scaffold code.
30
30
 
31
31
  **Non-negotiables for every webjs app:**
32
32
 
33
- 1. **Use Prisma + SQLite for persistence.** It's already wired up
34
- (`prisma/schema.prisma`, `lib/prisma.server.ts`, `npm run db:migrate`,
35
- `predev` hook running `prisma generate`). For any data the app
36
- stores (todos, posts, messages, products, comments, anything),
37
- define a Prisma model and persist there.
33
+ 1. **Use Drizzle + SQLite for persistence.** It's already wired up
34
+ (`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate`
35
+ + `npm run db:migrate`). For any data the app stores (todos, posts,
36
+ messages, products, comments, anything), define a Drizzle table and
37
+ persist there.
38
38
  - **NEVER** store app data in JSON files (`data/todos.json`,
39
39
  `db.json`, …). It resets on reload and cannot scale. This is a project convention,
40
40
  and the user's prompt explicitly forbids it.
@@ -47,10 +47,10 @@ user asked for, never leftover scaffold code.
47
47
  `full-stack` (default), `--template api`, `--template saas`. Don't
48
48
  reach for a `--template blog` / `--template todo` / `--template
49
49
  ecommerce`. They don't exist and the CLI will reject them.
50
- 3. **First step after scaffolding:** edit `prisma/schema.prisma` to the
50
+ 3. **First step after scaffolding:** edit `db/schema.server.ts` to the
51
51
  app's real domain models (delete the example `User` model unless the
52
- app actually needs users), run `webjs db migrate <name>`, then build
53
- pages / actions / queries against those models.
52
+ app actually needs users), run `webjs db generate` then
53
+ `webjs db migrate`, then build pages / actions / queries against them.
54
54
 
55
55
  **Picking the right scaffold from the user's prompt** (you do this BEFORE
56
56
  running `webjs create`; if you're reading this you've already scaffolded.
@@ -322,12 +322,15 @@ modules/<feature>/
322
322
  utils/*.ts feature-scoped helpers
323
323
  types.ts feature types
324
324
  lib/
325
- prisma.ts PrismaClient singleton (import from here, never `new PrismaClient()`)
326
- ... other cross-cutting infra (session, auth config, etc.)
327
- prisma/
328
- schema.prisma Prisma schema, SQLite by default, switch provider for Postgres/MySQL
329
- dev.db SQLite file (gitignored); run `npm run db:migrate` to create
330
- migrations/ generated migration SQL
325
+ ... cross-cutting infra (session, auth config, etc.)
326
+ db/
327
+ schema.server.ts Drizzle models + relations (your data layer)
328
+ columns.server.ts column helpers (dialect-specific; the only file to swap for Postgres)
329
+ connection.server.ts opens the driver, exports the \`db\` singleton (import \`db\` from here)
330
+ seed.server.ts optional seed (run via \`webjs db seed\`)
331
+ dev.db SQLite file (gitignored); run \`npm run db:migrate\` to create
332
+ migrations/ generated migration SQL (committed)
333
+ drizzle.config.ts drizzle-kit config (root; SQLite by default, --db postgres to switch)
331
334
  public/ static assets, served at /public/*
332
335
  test/<feature>/ feature-scoped tests, one folder per concern
333
336
  <name>.test.ts node unit / integration test (node --test)
@@ -359,39 +362,44 @@ Run `webjs types` once (and ensure `tsconfig.json` `include` lists
359
362
  startup, so it stays current. Without it, `params` is `Record<string, string>`
360
363
  and `navigate()` accepts any string (non-breaking).
361
364
 
362
- ## Database (Prisma + SQLite by default)
365
+ ## Database (Drizzle + SQLite by default)
363
366
 
364
- Every scaffold includes a Prisma setup pointed at a local SQLite file.
367
+ Every scaffold includes a Drizzle setup pointed at a local SQLite file,
368
+ under a `db/` folder (`schema.server.ts`, `columns.server.ts`,
369
+ `connection.server.ts`). Drizzle has no codegen and no engine binary.
365
370
  First-run workflow:
366
371
 
367
372
  ```sh
368
373
  cp .env.example .env # DATABASE_URL is pre-filled for SQLite
369
- npm run db:migrate # creates prisma/dev.db + migration
370
- npm run dev # webjs dev + prisma generate via predev
374
+ npm run db:generate # schema -> SQL migration (drizzle-kit)
375
+ npm run db:migrate # apply it (creates db/dev.db)
376
+ npm run dev # webjs dev, then serves
371
377
  ```
372
378
 
373
- ### Always `npm run dev` / `npm start`, never `webjs dev` / `webjs start` directly
379
+ ### `npm run dev` / `npm start` and `webjs dev` / `webjs start` behave identically
374
380
 
375
- `webjs dev` and `webjs start` are framework primitives, they only run
376
- the webjs server. They do **not** run `prisma generate`, do **not** run
377
- `prisma migrate deploy`, do **not** spawn the Tailwind watcher, do
378
- **not** run any other per-app process this `package.json` composes.
381
+ `npm run dev` and `npm start` are the documented entrypoints, and they
382
+ are thin aliases for `webjs dev` / `webjs start`. The start orchestration
383
+ (applying migrations, and any parallel watcher like the Tailwind CLI)
384
+ lives in the `webjs` block of `package.json` and runs INSIDE
385
+ `webjs dev` / `webjs start`:
379
386
 
380
- `npm run dev` and `npm start` are the app-level entrypoints. They run
381
- the webjs server **plus** every other process the app needs, wired
382
- together via `predev` / `prestart` hooks and (where present)
383
- `concurrently` for parallel watchers. Skipping the npm wrapper produces
384
- silent breakage: a stale Prisma client, missing `public/tailwind.css`,
385
- an unmigrated database in production, etc.
387
+ ```jsonc
388
+ "webjs": {
389
+ "start": { "before": ["webjs db migrate"] }
390
+ }
391
+ ```
386
392
 
387
- Same split Rails 7+ uses: `bin/rails server` is the framework
388
- primitive, `bin/dev` is the orchestrator. webjs uses npm scripts +
389
- hooks for the same role, because as a no-build framework Tailwind /
390
- Prisma / etc. cannot be bundler plugins.
393
+ Drizzle has no codegen, so there is no dev `before` step. An app that
394
+ adds the Tailwind CLI puts its `--watch` command under
395
+ `webjs.dev.parallel` and it runs alongside the server, torn down on exit.
396
+ `before` steps run to completion first; a failed `webjs db migrate`
397
+ aborts the boot with a clear message rather than serving a stale schema.
391
398
 
392
- In Docker / Railway, prefer `npm start` (or `node node_modules/.bin/npm
393
- start`) as the CMD over `node ... webjs.js start ...`. The npm form
394
- fires `prestart`; the direct binary form skips it.
399
+ In Docker / Railway, `CMD ["npm", "start"]` and `CMD ["webjs", "start"]`
400
+ are equivalent: `webjs start` runs `webjs.start.before` (`webjs db
401
+ migrate`) in-process before serving, so the migrate no longer depends on
402
+ an npm `prestart` hook.
395
403
 
396
404
  ### Running on Bun instead of Node
397
405
 
@@ -407,14 +415,16 @@ bun --bun run dev # or: bun --bun run start
407
415
  On Node the `.ts` type-stripping is the built-in `module.stripTypeScriptTypes`;
408
416
  on Bun (which has no built-in) it comes from `amaro` automatically, so the same
409
417
  source serves identically. SSR action-result seeding (an internal hydration
410
- optimization) is off on Bun (it needs `module.registerHooks`), which only means
411
- an async-render component re-fetches once on hydration, no behavior change.
418
+ optimization) works on both runtimes: Node installs it via `module.registerHooks`,
419
+ Bun via a `Bun.plugin` `onLoad`, so an async-render component does not re-fetch
420
+ on hydration on either runtime.
412
421
 
413
422
  **Containerized deploy ships with the scaffold.** `Dockerfile`,
414
423
  `compose.yaml`, and `.dockerignore` are scaffolded at the app root. The
415
424
  Dockerfile pins `node:24-alpine` (the same Node major CI uses), installs
416
- deps, runs `prisma generate`, and starts via `npm start` so `prestart`
417
- applies migrations. Run it locally with `docker compose up --build` (the
425
+ deps (no build step, since Drizzle has no codegen), and starts via
426
+ `npm start` (`webjs start` runs `webjs.start.before` = `webjs db migrate`
427
+ before serving). Run it locally with `docker compose up --build` (the
418
428
  app comes up on http://localhost:8080 against a SQLite file on a named
419
429
  volume). For production, point `DATABASE_URL` at managed Postgres and set
420
430
  `AUTH_SECRET`. The `.dockerignore` keeps the `.webjs/vendor/` importmap in
@@ -439,24 +449,28 @@ live DB ping), add an optional `readiness.{js,ts}` at the app root that
439
449
  default-exports an async check; `/__webjs/ready` runs it once warm and reports
440
450
  503 if it returns `false` or throws.
441
451
 
442
- Scripts:
452
+ Scripts (all wrap `drizzle-kit`):
443
453
 
444
- - `npm run db:migrate`: `prisma migrate dev` (dev-time schema changes + migration + generate)
445
- - `npm run db:generate`: `prisma generate` (regenerate client only)
446
- - `npm run db:studio`: `prisma studio` (GUI)
447
- - `predev` hook auto-runs `prisma generate` before `npm run dev`
448
- - `prestart` hook runs `prisma migrate deploy` before `npm start` (idempotent in prod)
454
+ - `npm run db:generate`: `webjs db generate` (schema -> SQL migration)
455
+ - `npm run db:migrate`: `webjs db migrate` (apply pending migrations)
456
+ - `npm run db:push`: `webjs db push` (push the schema straight to the dev DB)
457
+ - `npm run db:studio`: `webjs db studio` (visual DB browser)
458
+ - `npm run db:seed`: `webjs db seed` (run `db/seed.server.ts`)
459
+ - `webjs.start.before` runs `webjs db migrate` inside `webjs start` (idempotent; replaces the old `prestart` hook). No dev `before` step (no codegen).
449
460
 
450
- Always import the client from `lib/prisma.server.ts` (never `new PrismaClient()` directly -
451
- the singleton avoids opening a new connection on every dev-server reload):
461
+ Always import `db` from `db/connection.server.ts` (the globalThis-cached
462
+ singleton avoids opening a new connection on every dev-server reload), and
463
+ the tables from `db/schema.server.ts`:
452
464
 
453
465
  ```ts
454
- import { prisma } from '../../../lib/prisma.server.ts';
455
- const users = await prisma.user.findMany();
466
+ import { db } from '../../../db/connection.server.ts';
467
+ const users = await db.query.users.findMany();
456
468
  ```
457
469
 
458
- To switch to Postgres or MySQL: change `provider` in `prisma/schema.prisma`
459
- and the `DATABASE_URL` in `.env`.
470
+ To switch to Postgres: scaffold with `--db postgres`, or swap
471
+ `db/columns.server.ts` + `db/connection.server.ts` for the Postgres
472
+ variants and point `DATABASE_URL` at Postgres. The schema, queries, and
473
+ actions are unchanged.
460
474
 
461
475
  ## NPM packages (vendor pipeline)
462
476
 
@@ -521,9 +535,9 @@ committed manifest, optional `--download` for full offline capability,
521
535
  and a `--from` knob to swap the resolver CDN if jspm.io has an
522
536
  incident.
523
537
 
524
- **Don't auto-run `webjs vendor pin` in `predev` / `prestart`.** Auto-pin
525
- would silently churn the committed importmap.json as jspm.io resolves
526
- URLs or transitive deps drift. Pin is a deliberate developer action,
538
+ **Don't auto-run `webjs vendor pin` in a `webjs.dev.before` / `webjs.start.before`
539
+ step.** Auto-pin would silently churn the committed importmap.json as jspm.io
540
+ resolves URLs or transitive deps drift. Pin is a deliberate developer action,
527
541
  like `npm install` itself.
528
542
 
529
543
  **Do NOT modify the `.webjs/` lines in `.gitignore` / `.dockerignore`.**
@@ -740,11 +754,12 @@ legitimately use `static styles = css\`\`` for scoped CSS.
740
754
  ```ts
741
755
  // modules/posts/actions/create-post.server.ts
742
756
  'use server';
743
- import { prisma } from '../../../lib/prisma.server.ts';
757
+ import { db } from '../../../db/connection.server.ts';
758
+ import { posts } from '../../../db/schema.server.ts';
744
759
 
745
760
  export async function createPost(input: { title: string; body: string }) {
746
761
  if (!input.title) return { success: false, error: 'title required', status: 400 };
747
- const post = await prisma.post.create({ data: input });
762
+ const [post] = await db.insert(posts).values(input).returning();
748
763
  return { success: true, data: post };
749
764
  }
750
765
  ```
@@ -1117,13 +1132,14 @@ composition, so a nested shell ends up dropped by the HTML parser.
1117
1132
  1. Custom element tags must contain a hyphen. Pass the tag to `.register('tag-name')` at the bottom of the file. The tag is not a static field.
1118
1133
  2. **Server-only code goes in `.server.{js,ts}` files, `route.ts`
1119
1134
  handlers, or `middleware.ts`. Never in pages, layouts, or
1120
- components.** Direct imports of `@prisma/client`, `node:*`, or any
1121
- server-only dependency from a page, layout, loading.ts, error.ts,
1122
- not-found.ts, or component will crash the browser at module load.
1135
+ components.** Direct imports of a DB driver (`better-sqlite3` / `pg`),
1136
+ `node:*`, or any server-only dependency from a page, layout, loading.ts,
1137
+ error.ts, not-found.ts, or component will crash the browser at module load.
1123
1138
  Wrap the access in a `.server.{js,ts}` file; the framework
1124
- rewrites that import into an RPC stub for the browser. `lib/`
1125
- holds both server-only infra (`lib/prisma.server.ts`, `lib/session.server.ts`)
1126
- and browser-safe utilities (`lib/utils/cn.ts` with `cn`, design-
1139
+ rewrites that import into an RPC stub for the browser. Server-only
1140
+ infra lives in `db/*.server.ts` (the DB) and `lib/*.server.ts`
1141
+ (`lib/session.server.ts`); browser-safe utilities live in
1142
+ `lib/utils/cn.ts` with `cn`, design-
1127
1143
  system helpers). Server-only `lib/*` files must only be imported
1128
1144
  from `.server.ts`/`route.ts`/`middleware.ts`; browser-safe `lib/*`
1129
1145
  files (like `lib/utils/cn.ts`) can be imported anywhere.