@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.
@@ -32,18 +32,18 @@ enforced by `webjs check`; follow them by judgment.
32
32
 
33
33
  - **Server actions and queries live in `modules/<feature>/actions/` and
34
34
  `modules/<feature>/queries/`** (`*.server.{js,ts}`), not loose in the
35
- app root. Cross-cutting server infrastructure (the Prisma singleton,
36
- session helpers, auth config) lives in `lib/`.
35
+ app root. The DB connection lives in `db/connection.server.ts`; other
36
+ cross-cutting server infrastructure (session helpers, auth config) lives in `lib/`.
37
37
  - **One exported function per action/query file.** Name the file after
38
38
  the function (`create-post.server.ts` exports `createPost`). It keeps
39
39
  the action surface greppable.
40
40
  - **Every feature has tests.** A `modules/<feature>/` directory should
41
41
  have matching test files under `test/<feature>/`. A unit test for
42
42
  logic, a browser/e2e test for user-facing behaviour.
43
- - **Persist data with Prisma + SQLite, never JSON files.** The scaffold
44
- wires up `prisma/schema.prisma` and `lib/prisma.server.ts`. A
43
+ - **Persist data with Drizzle + SQLite, never JSON files.** The scaffold
44
+ wires up `db/schema.server.ts` and `db/connection.server.ts`. A
45
45
  `data/todos.json` or `db.json` used as a database resets on reload and
46
- cannot scale; define a Prisma model instead.
46
+ cannot scale; define a Drizzle table instead.
47
47
 
48
48
  ---
49
49
 
@@ -254,40 +254,45 @@ docs". That is the agent's default behavior in a webjs project.
254
254
 
255
255
  ---
256
256
 
257
- ## Data persistence: Prisma + SQLite, never JSON files
257
+ ## Data persistence: Drizzle + SQLite, never JSON files
258
258
 
259
259
  <!-- OVERRIDE -->
260
260
 
261
- Every webjs app uses **Prisma + SQLite** for persistence by default. The
262
- scaffold ships `prisma/schema.prisma`, `lib/prisma.server.ts` (singleton), the
263
- `predev` / `prestart` hooks that run `prisma generate` / `prisma migrate
264
- deploy`, and `npm run db:migrate` / `db:generate` / `db:studio` scripts.
261
+ Every webjs app uses **Drizzle + SQLite** for persistence by default. The
262
+ scaffold ships the `db/` folder (`schema.server.ts`, `columns.server.ts`,
263
+ `connection.server.ts`), the `webjs.start.before` step that runs
264
+ `webjs db migrate` inside `webjs start` (#550), and the
265
+ `npm run db:generate` / `db:migrate` / `db:push` / `db:studio` / `db:seed`
266
+ scripts (which route through `webjs db` to drizzle-kit). Drizzle has no
267
+ codegen, so there is no dev `before` step.
265
268
 
266
269
  **AI agents: these rules are absolute.**
267
270
 
268
271
  1. For ANY data the app stores (todos, posts, messages, products,
269
- comments, users…), define a Prisma model in `prisma/schema.prisma`
272
+ comments, users…), define a Drizzle table in `db/schema.server.ts`
270
273
  and persist there.
271
274
  2. **NEVER** create JSON files under `data/`, `db.json`, `posts.json`,
272
275
  `todos.json`, etc. as a fake database. It resets on reload and cannot
273
276
  scale; this is a project convention (see the conventions section above).
274
277
  3. **NEVER** use module-scope arrays or `Map`s as a "store". They
275
278
  reset on every dev-server reload and can't scale beyond one process.
276
- 4. **NEVER** use `localStorage` / `sessionStorage` to persist app data -
279
+ 4. **NEVER** use `localStorage` / `sessionStorage` to persist app data,
277
280
  it's per-browser and never reaches the server. Use it only for UI
278
281
  preferences (theme, sidebar collapsed, etc.).
279
- 5. To add a model: edit `prisma/schema.prisma`, then `npm run db:migrate
280
- -- --name <description>`. Access via `import { prisma } from
281
- '../../../lib/prisma.server.ts'` **only inside `.server.{js,ts}` files,
282
- `route.ts` handlers, or `middleware.ts`**. Never new `PrismaClient()`.
283
- Components, pages, and layouts call into the wrapped server query
284
- instead; the framework rewrites that import to an RPC stub on the
285
- browser side, so prisma source never reaches the client.
286
-
287
- To switch to Postgres or MySQL: change `provider` in
288
- `prisma/schema.prisma` and the `DATABASE_URL` in `.env`. Do this only
289
- if the user explicitly asks for it. SQLite is the right default for
290
- dev and small production workloads.
282
+ 5. To add a model: edit `db/schema.server.ts`, then `npm run db:generate`
283
+ and `npm run db:migrate`. Access via `import { db } from
284
+ '../../../db/connection.server.ts'` (and the tables from
285
+ `db/schema.server.ts`) **only inside `.server.{js,ts}` files,
286
+ `route.ts` handlers, or `middleware.ts`**. Components, pages, and
287
+ layouts call into the wrapped server query instead; the framework
288
+ rewrites that import to an RPC stub on the browser side, so the DB
289
+ driver never reaches the client.
290
+
291
+ To switch to Postgres: scaffold with `--db postgres`, or swap
292
+ `db/columns.server.ts` + `db/connection.server.ts` for the Postgres
293
+ variants and point `DATABASE_URL` at Postgres. The schema, queries, and
294
+ actions are unchanged. SQLite is the right default for dev and small
295
+ production workloads.
291
296
 
292
297
  ---
293
298
 
@@ -302,9 +307,9 @@ saas templates) is a **starting point**.
302
307
 
303
308
  When the user asks the agent to build their actual app:
304
309
 
305
- 1. **Replace the example `User` model** in `prisma/schema.prisma` with
306
- the real domain models the app needs (e.g. `Todo`, `Post`, `Message`)
307
- - unless the app actually has users.
310
+ 1. **Replace the example `User` model** in `db/schema.server.ts` with
311
+ the real domain models the app needs (e.g. `Todo`, `Post`, `Message`),
312
+ unless the app actually has users.
308
313
  2. **Replace `app/page.ts`** with the app's real homepage. Don't ship
309
314
  "Hello from …" as the deliverable.
310
315
  3. **Delete or replace `components/theme-toggle.ts`** if the app doesn't
@@ -319,10 +324,11 @@ When the user asks the agent to build their actual app:
319
324
  dashboard, or board, or a wide layout overflows into an unnecessary
320
325
  horizontal scrollbar. Keep the design tokens and theme setup, those
321
326
  are infrastructure.
322
- 6. **Keep:** the Prisma setup, the test config, the agent config files
327
+ 6. **Keep:** the Drizzle setup, the test config, the agent config files
323
328
  (`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
324
- `lib/prisma.server.ts`, the directory conventions, the design tokens in
325
- `app/layout.ts`. These are the infrastructure, not the example app.
329
+ `db/connection.server.ts` + `db/columns.server.ts`, the directory
330
+ conventions, the design tokens in `app/layout.ts`. These are the
331
+ infrastructure, not the example app.
326
332
 
327
333
  This is enforced, not just advised. The example `app/page.ts` and
328
334
  `app/layout.ts` carry a `webjs-scaffold-placeholder` marker comment, and
@@ -334,7 +340,7 @@ line. So the delivered app contains only what the user asked for, never
334
340
  leftover scaffold code.
335
341
 
336
342
  The scaffold exists so the agent doesn't reinvent the directory layout,
337
- the Prisma wiring, the test runner config, or the convention files. It
343
+ the Drizzle wiring, the test runner config, or the convention files. It
338
344
  does NOT exist so the agent ships the example homepage.
339
345
 
340
346
  ---
@@ -385,7 +391,7 @@ modules/
385
391
  - One exported function per server action/query file
386
392
  - Server actions need BOTH the `.server.{js,ts}` extension AND a `'use server'` directive at the top. Extension alone marks a server-only utility (source-protected, not RPC-callable). Directive alone is a lint violation (`use-server-needs-extension`).
387
393
  - Components must call `Class.register('tag')`
388
- - **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of `@prisma/client` or `node:*` from pages, layouts, or components crash the browser at module load. Wrap in a `.server.{js,ts}` file; the framework rewrites that import to an RPC stub on the browser side. `lib/` holds both server-only infra (`lib/prisma.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with `cn`); the convention is "if a `lib/` file needs Node APIs, only import it from server-only files."
394
+ - **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of a DB driver (`better-sqlite3` / `pg`) or `node:*` from pages, layouts, or components crash the browser at module load. Wrap in a `.server.{js,ts}` file; the framework rewrites that import to an RPC stub on the browser side. The DB lives in `db/*.server.ts`; `lib/` holds other server-only infra and browser-safe utilities (`lib/utils/cn.ts` with `cn`); the convention is "if a `lib/` file needs Node APIs, only import it from server-only files."
389
395
  - Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
390
396
  - **Fetch server data in the component that needs it, with an `async render()`, not by prop-drilling.** A leaf component can write `const u = await getUser(this.uid)` directly in `render()`; SSR awaits it so the data is in the first paint, and the client uses stale-while-revalidate on a re-fetch. Reach for `renderFallback()` only to show a re-fetch loading state, and `Task` / signals only for genuinely client-only data (a `Task` shows its pending state at SSR, losing first-paint data). Do not put `await getData()` in a page / layout when a leaf component can own it (page fetches run sequentially, a route-level waterfall).
391
397
 
@@ -919,8 +925,11 @@ route imports and calls it.
919
925
  ```ts
920
926
  // modules/posts/actions/create-post.server.ts
921
927
  'use server';
928
+ import { db } from '../../../db/connection.server.ts';
929
+ import { posts } from '../../../db/schema.server.ts';
922
930
  export async function createPost({ title, body }) {
923
- return prisma.post.create({ data: { title, body } });
931
+ const [post] = await db.insert(posts).values({ title, body }).returning();
932
+ return post;
924
933
  }
925
934
  ```
926
935
 
@@ -1030,7 +1039,8 @@ Where the data lives, where to read it:
1030
1039
  ```ts
1031
1040
  // modules/posts/actions/create-post.server.ts
1032
1041
  'use server';
1033
- import { prisma } from '../../../lib/prisma.server.ts';
1042
+ import { db } from '../../../db/connection.server.ts';
1043
+ import { posts } from '../../../db/schema.server.ts';
1034
1044
  import type { ActionResult } from '../types.ts';
1035
1045
 
1036
1046
  export async function createPost(input: {
@@ -1148,7 +1158,7 @@ Create new projects with `webjs create`:
1148
1158
  ```sh
1149
1159
  webjs create <name> # full-stack (default)
1150
1160
  webjs create <name> --template api # backend-only API
1151
- webjs create <name> --template saas # auth + dashboard + Prisma User model
1161
+ webjs create <name> --template saas # auth + dashboard + Drizzle User model
1152
1162
  ```
1153
1163
 
1154
1164
  **Route-wrapping pattern (especially for `--template api` apps):**
@@ -21,8 +21,9 @@
21
21
  # framework AGENTS.md "Secure response headers" section.
22
22
  FROM node:24-alpine
23
23
 
24
- # openssl + ca-certificates are required by Prisma's query engine at runtime.
25
- RUN apk add --no-cache openssl ca-certificates
24
+ # ca-certificates for outbound TLS (e.g. a managed Postgres). better-sqlite3
25
+ # is a prebuilt native module, so no build toolchain is needed here.
26
+ RUN apk add --no-cache ca-certificates
26
27
 
27
28
  WORKDIR /app
28
29
 
@@ -35,9 +36,9 @@ RUN npm install --no-audit --no-fund
35
36
  # App source. node_modules and local state are excluded via .dockerignore.
36
37
  COPY . .
37
38
 
38
- # Generate the Prisma client at build time (every scaffold ships a
39
- # prisma/schema.prisma). If you remove Prisma from the app, delete this line.
40
- RUN npx prisma generate
39
+ # Drizzle has no client-codegen step, so there is nothing to build here. The
40
+ # database is migrated at boot via `webjs start` (the `webjs.start.before`
41
+ # step runs `webjs db migrate`). See the CMD note below.
41
42
 
42
43
  ENV NODE_ENV=production
43
44
  # webjs start reads $PORT (default 8080). compose / uncloud / Railway set it.
@@ -55,6 +56,8 @@ EXPOSE 8080
55
56
  HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=5 \
56
57
  CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||8080)+'/__webjs/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
57
58
 
58
- # `npm start` runs `prestart: prisma migrate deploy` (idempotent, a no-op when
59
- # there are no migrations yet) and then `webjs start`, which serves on $PORT.
59
+ # `npm start` is a thin alias for `webjs start` (#550). `webjs start` runs the
60
+ # `webjs.start.before` step (`webjs db migrate`, idempotent / a no-op with no
61
+ # pending migrations) IN-PROCESS, then serves on $PORT. `CMD ["webjs", "start"]`
62
+ # is now equivalent: the migrate no longer depends on an npm `prestart` hook.
60
63
  CMD ["npm", "start"]
@@ -12,9 +12,9 @@ services:
12
12
  - "8080:8080"
13
13
  environment:
14
14
  PORT: 8080
15
- # SQLite on a volume for local dev. For production, point DATABASE_URL at
16
- # your managed Postgres and switch prisma/schema.prisma's provider to
17
- # "postgresql".
15
+ # SQLite on a volume for local dev. For production, scaffold with
16
+ # --db postgres (or swap db/columns.server.ts + db/connection.server.ts
17
+ # for the pg variant) and point DATABASE_URL at your managed Postgres.
18
18
  DATABASE_URL: file:/data/dev.db
19
19
  # Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
20
20
  AUTH_SECRET: ${AUTH_SECRET:-change-me-please-at-least-32-characters!}
@@ -1,168 +0,0 @@
1
- /**
2
- * Prisma-client preflight for `webjs dev` (#452).
3
- *
4
- * The scaffold's `dev` npm script is `webjs dev`, and `npm run dev` runs the
5
- * `predev` hook (`prisma generate`) FIRST. Invoking the `webjs dev` binary
6
- * directly (easy to do, and tempting for an AI/CLI) skips `predev`, so the dev
7
- * server boots against an ungenerated `@prisma/client` and crashes with a raw
8
- * "did not initialize yet" error and no hint that the canonical command is
9
- * `npm run dev`. This turns that crash into a one-line, actionable message.
10
- *
11
- * Scope is deliberately narrow: it only fires for an app that actually uses
12
- * Prisma (a `prisma/schema.prisma` OR an `@prisma/client` dependency), and it
13
- * only HINTS. It never auto-runs an arbitrary `predev` script and never shells
14
- * out to `prisma generate` on its own, keeping the no-build promise intact.
15
- *
16
- * Detection (verified against a real Prisma 6 install): the GENERATED
17
- * `.prisma/client` target is resolved through standard Node resolution from the
18
- * app (so a hoisted monorepo, where the client lives at a PARENT `node_modules`,
19
- * resolves correctly), then read. An ABSENT target, or a present-but-stub target
20
- * (the ungenerated client whose `PrismaClient` constructor throws the init
21
- * error), is "ungenerated". A real generated target older than the schema is
22
- * "stale". We do NOT grep the static `@prisma/client` re-export shim: it is
23
- * present in both states and never carries the init-error string itself.
24
- */
25
- import { existsSync, statSync, readFileSync } from 'node:fs';
26
- import { join, dirname } from 'node:path';
27
- import { createRequire } from 'node:module';
28
-
29
- /**
30
- * Does this app use Prisma? True if a schema is checked in OR `@prisma/client`
31
- * is a declared dependency. Either alone is enough; a non-Prisma app has
32
- * neither and gets no warning.
33
- *
34
- * @param {string} cwd
35
- * @returns {boolean}
36
- */
37
- export function usesPrisma(cwd) {
38
- if (existsSync(join(cwd, 'prisma', 'schema.prisma'))) return true;
39
- try {
40
- const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
41
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
42
- return Boolean(deps && deps['@prisma/client']);
43
- } catch {
44
- return false;
45
- }
46
- }
47
-
48
- // Marker the ungenerated `prisma-client-js` stub embeds in its generated target
49
- // (`node_modules/.prisma/client/index.js`). Verified against a real Prisma 6
50
- // install: after `npm i @prisma/client` but before `prisma generate`, the
51
- // generated `.prisma/client` entry IS present but its `PrismaClient` constructor
52
- // throws `@prisma/client did not initialize yet. Please run "prisma generate"`.
53
- // A real `prisma generate` replaces that stub with the generated client, which
54
- // does NOT contain this string. So the marker, read from the GENERATED target
55
- // (not the static `@prisma/client` shim), is the reliable ungenerated signal.
56
- const UNGENERATED_MARKER = 'did not initialize yet';
57
-
58
- /**
59
- * Resolve the GENERATED Prisma client entry (`.prisma/client/index.js`) for an
60
- * app, following standard Node resolution so a hoisted monorepo layout (the
61
- * generated client at a PARENT `node_modules`, the app under `apps/<x>`) still
62
- * resolves. Returns a discriminated result so the caller can tell the three
63
- * cases apart:
64
- * - `{ kind: 'unresolved' }` - `@prisma/client` itself is not resolvable.
65
- * - `{ kind: 'no-target' }` - the package resolves but `.prisma/client`
66
- * does not (a custom `output`, ambiguous).
67
- * - `{ kind: 'target', path }` - the generated target resolves.
68
- *
69
- * @param {string} cwd
70
- * @returns {{ kind: 'unresolved' } | { kind: 'no-target' } | { kind: 'target', path: string }}
71
- */
72
- function resolveGeneratedClient(cwd) {
73
- let clientDir;
74
- try {
75
- // Resolve @prisma/client AS THE APP would (hoisting-aware), then locate its
76
- // package dir. The shim itself loads `.prisma/client/default` relative to
77
- // here, so resolving from this dir follows the same (possibly hoisted) path.
78
- const appRequire = createRequire(join(cwd, 'noop.js'));
79
- clientDir = dirname(appRequire.resolve('@prisma/client'));
80
- } catch {
81
- return { kind: 'unresolved' };
82
- }
83
- const shimRequire = createRequire(join(clientDir, 'noop.js'));
84
- for (const entry of ['.prisma/client/index.js', '.prisma/client/default.js']) {
85
- try {
86
- return { kind: 'target', path: shimRequire.resolve(entry) };
87
- } catch { /* try the next entry */ }
88
- }
89
- return { kind: 'no-target' };
90
- }
91
-
92
- /**
93
- * Inspect the generated Prisma client state for a Prisma app.
94
- *
95
- * Returns one of:
96
- * - `{ status: 'ok' }` - client generated and not older than the schema.
97
- * - `{ status: 'missing' }` - schema/dep present but no generated client.
98
- * - `{ status: 'stale' }` - client exists but the schema is newer than it.
99
- *
100
- * Detection resolves the GENERATED `.prisma/client` target through standard Node
101
- * resolution (so hoisted monorepos are handled) and reads it: an absent target,
102
- * or a present-but-stub target (the ungenerated `PrismaClient` that throws on
103
- * construction), is `missing`. A real generated client that is older than the
104
- * schema is `stale`. A custom-`output` generator whose target Node cannot
105
- * resolve falls back to `ok` rather than nag a working app (false positives are
106
- * worse than a missed hint here).
107
- *
108
- * @param {string} cwd
109
- * @returns {{ status: 'ok' | 'missing' | 'stale' }}
110
- */
111
- export function prismaClientState(cwd) {
112
- const resolved = resolveGeneratedClient(cwd);
113
-
114
- // @prisma/client not resolvable: the app declared the dep (usesPrisma gated
115
- // us here) but it is not installed/generated. That is the boot-crash case.
116
- if (resolved.kind === 'unresolved') return { status: 'missing' };
117
-
118
- // The package resolves but the default `.prisma/client` target does not: a
119
- // custom `output` whose location we cannot cheaply verify. Fall back to `ok`
120
- // rather than nag a working app (false positives are worse than a missed hint).
121
- if (resolved.kind === 'no-target') return { status: 'ok' };
122
-
123
- const generatedIndex = resolved.path;
124
-
125
- // The generated target exists. Is it still the ungenerated stub (its
126
- // PrismaClient constructor throws the init error)?
127
- try {
128
- const body = readFileSync(generatedIndex, 'utf8');
129
- if (body.includes(UNGENERATED_MARKER)) return { status: 'missing' };
130
- } catch { /* unreadable: fall through to the stale check, then ok */ }
131
-
132
- // Generated for real. Is it older than the schema (a stale client)?
133
- const schema = join(cwd, 'prisma', 'schema.prisma');
134
- try {
135
- if (existsSync(schema)) {
136
- const schemaMtime = statSync(schema).mtimeMs;
137
- const clientMtime = statSync(generatedIndex).mtimeMs;
138
- if (schemaMtime > clientMtime) return { status: 'stale' };
139
- }
140
- } catch { /* if we can't stat, treat as ok */ }
141
-
142
- return { status: 'ok' };
143
- }
144
-
145
- /**
146
- * Build the actionable hint for an ungenerated/stale client, or `null` when the
147
- * app is fine or does not use Prisma. The caller prints it (a warning, not a
148
- * hard exit) before booting the dev server.
149
- *
150
- * @param {string} cwd
151
- * @returns {string | null}
152
- */
153
- export function prismaDevHint(cwd) {
154
- if (!usesPrisma(cwd)) return null;
155
- const { status } = prismaClientState(cwd);
156
- if (status === 'ok') return null;
157
-
158
- const reason =
159
- status === 'stale'
160
- ? 'Your Prisma client looks stale (the schema changed since it was generated).'
161
- : 'Your Prisma client is not generated yet.';
162
- return (
163
- `webjs: ${reason}\n` +
164
- ` The dev server will crash on an ungenerated client. Fix it with either:\n` +
165
- ` npm run dev # canonical: runs \`prisma generate\` (predev) first\n` +
166
- ` webjs db generate # just regenerate the client, then re-run\n`
167
- );
168
- }