@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.
package/README.md CHANGED
@@ -40,15 +40,15 @@ Both `webjs create` and `create-webjs-app` auto-install dependencies in the new
40
40
  ```sh
41
41
  webjs create <name> # scaffold a full-stack app (default)
42
42
  webjs create <name> --template api # backend-only API app
43
- webjs create <name> --template saas # auth + dashboard + Prisma User model
43
+ webjs create <name> --template saas # auth + dashboard + Drizzle User model
44
44
 
45
- webjs dev # dev server with live reload (prefer `npm run dev`, which runs the predev prisma generate hook)
45
+ webjs dev # dev server with live reload (runs webjs.dev.before, e.g. webjs db migrate, then serves; npm run dev is a thin alias)
46
46
  webjs start # production server (no build step, serves source directly)
47
47
  webjs check # validate source-code conventions (CI gate)
48
48
  webjs doctor # verify the project/toolchain setup (local onboarding, not CI)
49
49
  webjs test # run server + browser tests
50
50
  webjs vendor pin [--download] # pin client deps to a committable importmap (offline/reproducible)
51
- webjs db <prisma-subcommand> # prisma passthrough (saas template)
51
+ webjs db <generate|migrate|push|studio|seed> # drizzle-kit passthrough (+ seed)
52
52
 
53
53
  webjs ui init # initialise @webjsdev/ui in this project
54
54
  webjs ui add <names...> # copy components from the registry (https://ui.webjs.dev/registry/<name>.json)
package/bin/webjs.js CHANGED
@@ -49,13 +49,15 @@ const USAGE = `webjs commands:
49
49
  webjs doctor Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook)
50
50
  webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
51
51
  webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
52
- webjs create <name> [--template full-stack|api|saas] [--no-install] Scaffold a new webjs app
53
- (only 3 templates exist. default: full-stack with Prisma+SQLite)
52
+ webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--no-install] Scaffold a new webjs app
53
+ (only 3 templates exist. default: full-stack, Drizzle, --db sqlite)
54
54
  Auto-runs the detected package manager's install in the new dir
55
55
  unless --no-install is passed.
56
- webjs db generate Run \`prisma generate\`
57
- webjs db migrate [name] Run \`prisma migrate dev\`
58
- webjs db studio Run \`prisma studio\`
56
+ webjs db generate Generate a SQL migration from the schema (drizzle-kit generate)
57
+ webjs db migrate Apply pending migrations (drizzle-kit migrate)
58
+ webjs db push Push the schema straight to the dev DB (drizzle-kit push)
59
+ webjs db studio Open the database browser (drizzle-kit studio)
60
+ webjs db seed Run the app's db/seed.server.ts
59
61
  webjs ui <subcmd> AI-first component library CLI
60
62
  (init / add / list / view / diff / info)
61
63
  Requires @webjsdev/ui installed in the project
@@ -73,6 +75,42 @@ function flag(args, name, def) {
73
75
  return args[i + 1];
74
76
  }
75
77
 
78
+ /**
79
+ * Run the configured `before` steps (#550) for a phase, aborting the boot on
80
+ * the first failure. The orchestration lives in `lib/run-tasks.js` (pure,
81
+ * unit-tested); this owns the phase-prefixed logging + the non-zero exit (a
82
+ * failed generate/migrate must not serve stale code/schema).
83
+ *
84
+ * @param {string} phase 'dev' | 'start' (for the log line)
85
+ * @param {string[]} steps
86
+ * @param {string} cwd
87
+ */
88
+ async function runPhaseBeforeSteps(phase, steps, cwd) {
89
+ const { runBeforeSteps } = await import('../lib/run-tasks.js');
90
+ const r = await runBeforeSteps(steps, cwd, {
91
+ onStep: (step) => console.log(`webjs ${phase}: running before-step \`${step}\`…`),
92
+ });
93
+ if (!r.ok) {
94
+ console.error(`webjs ${phase}: before-step failed (exit ${r.code}): ${r.step}`);
95
+ process.exit(r.code);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Spawn the configured dev `parallel` tasks (#550) with phase-prefixed logging,
101
+ * delegating the spawn + teardown to `lib/run-tasks.js`'s `startParallelTasks`.
102
+ *
103
+ * @param {string[]} commands
104
+ * @param {string} cwd
105
+ * @returns {Promise<() => void>} the killer
106
+ */
107
+ async function startDevParallelTasks(commands, cwd) {
108
+ const { startParallelTasks } = await import('../lib/run-tasks.js');
109
+ return startParallelTasks(commands, cwd, {
110
+ onStart: (cmd) => console.log(`webjs dev: starting parallel task \`${cmd}\`…`),
111
+ });
112
+ }
113
+
76
114
  async function main() {
77
115
  // Preflight: webjs needs Node 24+ (built-in TS strip + recursive fs.watch).
78
116
  // Run before any subcommand so an older Node fails fast with a clear,
@@ -98,14 +136,18 @@ async function main() {
98
136
  break;
99
137
  }
100
138
 
101
- // A bare `webjs dev` (not `npm run dev`) skips the `predev` hook, so a
102
- // Prisma app boots against an ungenerated client and crashes with no
103
- // hint (#452). Detect that here, in the PARENT only, so the message
104
- // prints once rather than on every watch restart. Scoped to Prisma apps;
105
- // a non-Prisma app sees nothing. A hint, not an auto-run.
106
- const { prismaDevHint } = await import('../lib/prisma-preflight.js');
107
- const hint = prismaDevHint(process.cwd());
108
- if (hint) console.error(hint);
139
+ // Run the configured dev orchestration in the PARENT only (#550), so a
140
+ // bare `webjs dev` matches `npm run dev`. `dev.before` (one-shot tasks)
141
+ // runs to completion first; `dev.parallel` (Tailwind's
142
+ // watcher, etc.) then runs as children alongside the server. Spawned once
143
+ // here, NOT in the watch child (which re-execs on every restart). Torn
144
+ // down on exit so a watcher cannot outlive the server.
145
+ const { readAppTasks } = await import('../lib/app-tasks.js');
146
+ const devTasks = readAppTasks(process.cwd());
147
+ await runPhaseBeforeSteps('dev', devTasks.dev.before, process.cwd());
148
+ const killTasks = await startDevParallelTasks(devTasks.dev.parallel, process.cwd());
149
+ process.on('SIGINT', () => { killTasks(); process.exit(0); });
150
+ process.on('SIGTERM', () => { killTasks(); process.exit(0); });
109
151
 
110
152
  // Decide how to run: in-process (`--no-hot`), or re-exec'd under the host
111
153
  // runtime's hot-reload supervisor (`node --watch` on Node, `bun --hot` on
@@ -124,6 +166,7 @@ async function main() {
124
166
  loadAppEnv(process.cwd());
125
167
  const port = resolvePort(flag(rest, '--port'));
126
168
  await startServer({ appDir: process.cwd(), port, dev: true });
169
+ killTasks();
127
170
  break;
128
171
  }
129
172
 
@@ -132,7 +175,7 @@ async function main() {
132
175
  cwd: process.cwd(),
133
176
  env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
134
177
  });
135
- child.on('exit', (code) => process.exit(code ?? 0));
178
+ child.on('exit', (code) => { killTasks(); process.exit(code ?? 0); });
136
179
  break;
137
180
  }
138
181
  case 'start': {
@@ -140,6 +183,11 @@ async function main() {
140
183
  // Load `.env` BEFORE resolving the port so a `PORT` set there wins over
141
184
  // the 8080 default (#447), same as for `dev`.
142
185
  loadAppEnv(process.cwd());
186
+ // Run the configured `start.before` steps (e.g. `webjs db migrate`)
187
+ // before serving (#550), so a bare `webjs start` is not a degraded run
188
+ // that skips the `prestart` hook. Aborts the boot on a failed step.
189
+ const { readAppTasks } = await import('../lib/app-tasks.js');
190
+ await runPhaseBeforeSteps('start', readAppTasks(process.cwd()).start.before, process.cwd());
143
191
  const port = resolvePort(flag(rest, '--port'));
144
192
  await startServer({ appDir: process.cwd(), port, dev: false });
145
193
  break;
@@ -147,10 +195,28 @@ async function main() {
147
195
  case 'db': {
148
196
  const sub = rest[0];
149
197
  const args = rest.slice(1);
150
- const map = { generate: ['generate'], migrate: ['migrate', 'dev', ...args], studio: ['studio'] };
151
- const prismaArgs = map[sub];
152
- if (!prismaArgs) { console.error('Unknown db subcommand.\n' + USAGE); process.exit(1); }
153
- const child = spawn('npx', ['prisma', ...prismaArgs], { stdio: 'inherit', cwd: process.cwd() });
198
+ // `webjs db seed` runs the app's own seed script directly (not a
199
+ // drizzle-kit command); Drizzle has no codegen, so there is no
200
+ // `generate`-the-client step, only schema-to-SQL `generate`.
201
+ if (sub === 'seed') {
202
+ const { existsSync } = await import('node:fs');
203
+ const seedFile = ['db/seed.server.ts', 'db/seed.server.js']
204
+ .map((p) => join(process.cwd(), p)).find(existsSync);
205
+ if (!seedFile) {
206
+ console.error('No db/seed.server.ts found in this app.');
207
+ process.exit(1);
208
+ }
209
+ const child = spawn(process.execPath, [seedFile], { stdio: 'inherit', cwd: process.cwd() });
210
+ child.on('exit', (code) => process.exit(code ?? 0));
211
+ break;
212
+ }
213
+ // generate (schema -> SQL migration), migrate (apply), push (dev
214
+ // schema sync), studio. All wrap drizzle-kit; the verbose name stays
215
+ // hidden behind `webjs db`.
216
+ const map = { generate: ['generate'], migrate: ['migrate'], push: ['push'], studio: ['studio'] };
217
+ const kitArgs = map[sub];
218
+ if (!kitArgs) { console.error('Unknown db subcommand.\n' + USAGE); process.exit(1); }
219
+ const child = spawn('npx', ['drizzle-kit', ...kitArgs, ...args], { stdio: 'inherit', cwd: process.cwd() });
154
220
  child.on('exit', (code) => process.exit(code ?? 0));
155
221
  break;
156
222
  }
@@ -405,18 +471,18 @@ async function main() {
405
471
  console.error(`Error: unknown template '${template}'.
406
472
 
407
473
  Only three scaffolds exist:
408
- full-stack (default): pages + components + API + Prisma/SQLite.
474
+ full-stack (default): pages + components + API + Drizzle/SQLite.
409
475
  Pick this for any app the user describes in product terms
410
476
  (todo app, blog, dashboard, marketplace, social feed, …).
411
477
  api backend-only: route handlers + modules, no pages/SSR.
412
478
  Pick this only if the user explicitly asks for an HTTP/JSON
413
479
  API with no UI.
414
- saas auth + login/signup + protected dashboard + Prisma User
480
+ saas auth + login/signup + protected dashboard + Drizzle User
415
481
  model. Pick this only if the user explicitly asks for auth
416
482
  or a SaaS-shaped product.
417
483
 
418
484
  The scaffold is a starting point. Replace the example layout/page/
419
- components/schema with the actual app the user requested. Use Prisma +
485
+ components/schema with the actual app the user requested. Use Drizzle +
420
486
  SQLite for persistence (already wired up). Never store app data in JSON
421
487
  files.
422
488
 
@@ -424,8 +490,10 @@ Full docs: https://docs.webjs.com`);
424
490
  process.exit(1);
425
491
  }
426
492
  const noInstall = rest.includes('--no-install');
493
+ // --db picks the database dialect: sqlite (default) or postgres.
494
+ const db = flag(rest, '--db', 'sqlite');
427
495
  const { scaffoldApp } = await import('../lib/create.js');
428
- await scaffoldApp(name, process.cwd(), { template, install: !noInstall });
496
+ await scaffoldApp(name, process.cwd(), { template, db, install: !noInstall });
429
497
  break;
430
498
  }
431
499
  case 'vendor': {
@@ -0,0 +1,62 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * Read the dev/start task orchestration from an app's `package.json` `"webjs"`
6
+ * block (#550). This is what lets `webjs dev` / `webjs start` behave identically
7
+ * to `npm run dev` / `npm run start`: the orchestration (Tailwind's watcher, a
8
+ * `db migrate` before prod boot) moves OUT of `concurrently` + `pre*` npm hooks
9
+ * and INTO the framework primitive, so a bare `webjs dev` is not a degraded run.
10
+ *
11
+ * Shape:
12
+ * "webjs": {
13
+ * "dev": {
14
+ * "before": ["webjs db migrate"],
15
+ * "parallel": ["tailwindcss -i ./public/input.css -o ./public/tailwind.css --watch"]
16
+ * },
17
+ * "start": { "before": ["webjs db migrate"] }
18
+ * }
19
+ *
20
+ * `before` commands run sequentially to completion BEFORE the server boots (the
21
+ * old `predev` / `prestart` hooks: a one-shot `webjs db migrate`).
22
+ * `parallel` (dev only) commands run as long-lived child processes ALONGSIDE the
23
+ * server (the old `concurrently` watchers: Tailwind). Returns normalized arrays
24
+ * (never undefined) so callers iterate without guards, and a missing/empty
25
+ * config yields empty arrays so a plain app runs `webjs dev`/`start` unchanged.
26
+ *
27
+ * Pure (reads one file, never spawns / prints / exits) so it is unit-testable
28
+ * without a process, matching `lib/port.js` and `lib/dev-supervisor.js`.
29
+ *
30
+ * @param {string} appDir
31
+ * @param {(p: string) => string} [readFile] injectable reader for tests
32
+ * @returns {{ dev: { before: string[], parallel: string[] }, start: { before: string[] } }}
33
+ */
34
+ export function readAppTasks(appDir, readFile) {
35
+ const read = readFile || ((p) => readFileSync(p, 'utf8'));
36
+ let pkg = {};
37
+ try {
38
+ pkg = JSON.parse(read(join(appDir, 'package.json')));
39
+ } catch {
40
+ // No package.json, or unparseable: a plain run with no orchestration.
41
+ return emptyTasks();
42
+ }
43
+ const webjs = pkg && typeof pkg === 'object' ? pkg.webjs : null;
44
+ if (!webjs || typeof webjs !== 'object') return emptyTasks();
45
+
46
+ /** Keep only non-empty string entries; drop anything else defensively. */
47
+ const cmds = (v) =>
48
+ Array.isArray(v) ? v.filter((s) => typeof s === 'string' && s.trim().length > 0) : [];
49
+
50
+ return {
51
+ dev: {
52
+ before: cmds(webjs.dev && webjs.dev.before),
53
+ parallel: cmds(webjs.dev && webjs.dev.parallel),
54
+ },
55
+ start: { before: cmds(webjs.start && webjs.start.before) },
56
+ };
57
+ }
58
+
59
+ /** @returns {{ dev: { before: string[], parallel: string[] }, start: { before: string[] } }} */
60
+ function emptyTasks() {
61
+ return { dev: { before: [], parallel: [] }, start: { before: [] } };
62
+ }
package/lib/create.js CHANGED
@@ -249,6 +249,15 @@ export async function scaffoldApp(name, cwd, opts = {}) {
249
249
  }
250
250
  const isApi = template === 'api';
251
251
  const isSaas = template === 'saas';
252
+
253
+ // Database dialect (#563): sqlite (default) or postgres. Drizzle is the ORM;
254
+ // the schema/queries/actions are identical across dialects, only db/columns
255
+ // + db/connection + the driver dep differ.
256
+ const dialect = opts.db || 'sqlite';
257
+ const VALID_DIALECTS = ['sqlite', 'postgres'];
258
+ if (!VALID_DIALECTS.includes(dialect)) {
259
+ throw new Error(`Unknown --db '${dialect}'. Only ${VALID_DIALECTS.join(' / ')} are supported.`);
260
+ }
252
261
  const appDir = join(cwd, name);
253
262
  if (existsSync(appDir)) {
254
263
  console.error(`Error: directory '${name}' already exists.`);
@@ -264,7 +273,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
264
273
  'modules',
265
274
  'lib',
266
275
  'public',
267
- 'prisma',
276
+ 'db',
268
277
  'test/unit',
269
278
  'test/e2e',
270
279
  ];
@@ -278,8 +287,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
278
287
  type: 'module',
279
288
  private: true,
280
289
  scripts: {
281
- predev: 'prisma generate',
282
- prestart: 'prisma migrate deploy',
290
+ // No `predev` / `prestart` hooks (#550): the `webjs` block below holds
291
+ // the start orchestration (`webjs db migrate`), run INSIDE `webjs start`,
292
+ // so `npm run start` (a thin alias) behaves identically. Drizzle has no
293
+ // codegen, so there is no dev `before` step.
283
294
  dev: 'webjs dev',
284
295
  start: 'webjs start',
285
296
  test: 'webjs test',
@@ -292,18 +303,24 @@ export async function scaffoldApp(name, cwd, opts = {}) {
292
303
  // vendor pins, @webjsdev versions, git hook). Local tool, NOT a CI gate
293
304
  // (its env-drift + network pin-freshness checks would make CI flaky).
294
305
  doctor: 'webjs doctor',
295
- 'db:migrate': 'prisma migrate dev',
296
- 'db:generate': 'prisma generate',
297
- 'db:studio': 'prisma studio',
306
+ 'db:generate': 'webjs db generate',
307
+ 'db:migrate': 'webjs db migrate',
308
+ 'db:push': 'webjs db push',
309
+ 'db:studio': 'webjs db studio',
310
+ 'db:seed': 'webjs db seed',
298
311
  },
299
312
  dependencies: {
300
- '@prisma/client': '^6.0.0',
313
+ // Drizzle ORM (no codegen, no engine binary). Pinned to the 1.0 line
314
+ // for relations v2. The SQLite/Postgres driver below is dialect-picked.
315
+ 'drizzle-orm': '^1.0.0-rc.3',
316
+ ...(dialect === 'postgres' ? { pg: '^8.13.0' } : { 'better-sqlite3': '^12.11.1' }),
301
317
  '@webjsdev/cli': 'latest',
302
318
  '@webjsdev/core': 'latest',
303
319
  '@webjsdev/server': 'latest',
304
320
  },
305
321
  devDependencies: {
306
- prisma: '^6.0.0',
322
+ 'drizzle-kit': '^1.0.0-rc.3',
323
+ ...(dialect === 'postgres' ? { '@types/pg': '^8.11.0' } : {}),
307
324
  // The TypeScript compiler, for `npm run typecheck` (webjs typecheck runs
308
325
  // tsc --noEmit). Not needed at runtime (Node strips types in place), only
309
326
  // to type-check the app.
@@ -330,6 +347,18 @@ export async function scaffoldApp(name, cwd, opts = {}) {
330
347
  // into components/ui/ (they import @webjsdev/core, not the kit), and the
331
348
  // CLI resolves @webjsdev/ui from its own install.
332
349
  },
350
+ // Start task orchestration (#550). `webjs start` reads `start.before` and
351
+ // runs it in-process, so `npm run start` (a thin alias above) behaves
352
+ // identically. Drizzle has no codegen, so there is no dev `before` step;
353
+ // production applies pending migrations via `webjs db migrate`. The scaffold
354
+ // uses the Tailwind browser runtime (no CSS build step), so there is no dev
355
+ // `parallel` watcher here; an app that adds the Tailwind CLI puts its
356
+ // `--watch` command under `webjs.dev.parallel`.
357
+ webjs: {
358
+ // Drizzle has no codegen, so there is no dev `before` step. Production
359
+ // applies pending migrations at boot via `webjs db migrate` (drizzle-kit).
360
+ start: { before: ['webjs db migrate'] },
361
+ },
333
362
  }, null, 2) + '\n');
334
363
 
335
364
  await writeFile(join(appDir, 'tsconfig.json'), JSON.stringify({
@@ -381,7 +410,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
381
410
  'middleware.ts',
382
411
  '.webjs/routes.d.ts',
383
412
  ],
384
- exclude: ['node_modules', '.webjs/vendor', 'prisma/migrations'],
413
+ exclude: ['node_modules', '.webjs/vendor', 'db/migrations'],
385
414
  }, null, 2) + '\n');
386
415
 
387
416
  // --- Templates (AGENTS.md, CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
@@ -398,8 +427,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
398
427
  // Environment variables
399
428
  '.env.example',
400
429
  // Project-level gitignore (node_modules, .webjs, .env, OS junk).
401
- // The Prisma dev.db rule is appended programmatically below so it
402
- // only appears in templates that actually use SQLite.
430
+ // The SQLite dev.db rule is appended programmatically below so it
431
+ // only appears for the sqlite dialect.
403
432
  '.gitignore',
404
433
  // Git hooks (blocks commits on main)
405
434
  '.hooks/pre-commit',
@@ -471,62 +500,172 @@ export async function scaffoldApp(name, cwd, opts = {}) {
471
500
  const preCommitPath = join(appDir, '.hooks', 'pre-commit');
472
501
  if (existsSync(preCommitPath)) await chmod(preCommitPath, 0o755);
473
502
 
474
- // --- Prisma schema + client singleton (all templates) ---
503
+ // --- Drizzle db layer (all templates), dialect-selected (#563) ---
504
+ //
505
+ // The schema, queries, and actions are identical across dialects; only
506
+ // db/columns.server.ts + db/connection.server.ts + drizzle.config.ts + the
507
+ // driver dep differ. Switching dialect (e.g. SQLite in dev, Postgres in
508
+ // prod) is a config + module swap, not a code rewrite. Pinned to drizzle-orm
509
+ // 1.0.0-rc.3 (relations v2). See research #562.
475
510
 
476
- await writeFile(join(appDir, 'prisma', 'schema.prisma'), `generator client {
477
- provider = "prisma-client-js"
478
- }
511
+ const columnsSqlite = `import { sqliteTableCreator, integer, text, real, blob, index as _index } from 'drizzle-orm/sqlite-core';
512
+ import type { SQLiteColumn } from 'drizzle-orm/sqlite-core';
513
+ import { getTableName, type Table } from 'drizzle-orm';
479
514
 
480
- datasource db {
481
- // Defaults to SQLite at ./prisma/dev.db. Switch to postgresql / mysql
482
- // by changing the provider + DATABASE_URL in .env.
483
- provider = "sqlite"
484
- url = env("DATABASE_URL")
485
- }
515
+ // Raw drizzle builders, re-exported so the schema reads like drizzle.
516
+ export { text, integer, real, blob };
517
+
518
+ // Casing factory: column keys map to snake_case SQL names.
519
+ export const table = sqliteTableCreator((name) => name, 'snake_case');
520
+
521
+ export const pk = () => integer().primaryKey({ autoIncrement: true });
522
+ export const uuidPk = () => text().primaryKey().$defaultFn(() => crypto.randomUUID());
523
+ export const uuid = () => text();
524
+ export const bool = () => integer({ mode: 'boolean' });
525
+ export const timestamp = () => integer({ mode: 'timestamp_ms' });
526
+ export const createdAt = () => timestamp().notNull().defaultNow();
527
+ export const updatedAt = () => timestamp().notNull().defaultNow().$onUpdate(() => new Date());
528
+
529
+ // Anonymous-style index helper (rc.3 requires a name; this derives a
530
+ // table-qualified one, matching drizzle-kit's own convention).
531
+ export const index = (...cols: SQLiteColumn[]) =>
532
+ _index(getTableName((cols[0] as unknown as { table: Table }).table) + '_' + cols.map((c) => c.name).join('_') + '_idx').on(...(cols as [SQLiteColumn, ...SQLiteColumn[]]));
533
+ `;
534
+
535
+ const columnsPg = `import { pgTableCreator, serial, uuid as pgUuid, integer, text, real, boolean, timestamp as pgTimestamp, index as _index } from 'drizzle-orm/pg-core';
536
+ import type { PgColumn } from 'drizzle-orm/pg-core';
537
+ import { getTableName, type Table } from 'drizzle-orm';
538
+
539
+ export { text, integer, real };
540
+
541
+ export const table = pgTableCreator((name) => name, 'snake_case');
542
+
543
+ export const pk = () => serial().primaryKey();
544
+ export const uuidPk = () => pgUuid().primaryKey().defaultRandom();
545
+ export const uuid = () => pgUuid();
546
+ export const bool = () => boolean();
547
+ export const timestamp = () => pgTimestamp({ withTimezone: true });
548
+ export const createdAt = () => timestamp().notNull().defaultNow();
549
+ export const updatedAt = () => timestamp().notNull().defaultNow().$onUpdate(() => new Date());
550
+
551
+ export const index = (...cols: PgColumn[]) =>
552
+ _index(getTableName((cols[0] as unknown as { table: Table }).table) + '_' + cols.map((c) => c.name).join('_') + '_idx').on(...(cols as [PgColumn, ...PgColumn[]]));
553
+ `;
554
+
555
+ await writeFile(join(appDir, 'db', 'columns.server.ts'), dialect === 'postgres' ? columnsPg : columnsSqlite);
556
+
557
+ // Example schema (dialect-agnostic). Replace the User model with your own.
558
+ await writeFile(join(appDir, 'db', 'schema.server.ts'), `import { defineRelations } from 'drizzle-orm';
559
+ import { table, pk, text, createdAt } from './columns.server.ts';
486
560
 
487
561
  // Example model. Feel free to delete or extend.
488
- model User {
489
- id Int @id @default(autoincrement())
490
- email String @unique
491
- name String?
492
- createdAt DateTime @default(now())
493
- }
562
+ export const users = table('users', {
563
+ id: pk(),
564
+ email: text().notNull().unique(),
565
+ name: text(),
566
+ createdAt: createdAt(),
567
+ });
568
+
569
+ // Relations live here (one defineRelations for the whole schema). Empty
570
+ // for now; add per-model relations as your schema grows.
571
+ export const relations = defineRelations({ users }, () => ({}));
572
+
573
+ // Derived types, never hand-written.
574
+ export type User = typeof users.$inferSelect;
494
575
  `);
495
576
 
496
- await writeFile(join(appDir, 'lib', 'prisma.server.ts'), `/**
497
- * Prisma client singleton. The \`globalThis\` trick keeps a single
498
- * instance across dev-server module reloads, so we don't open a new
499
- * DB connection on every file change.
500
- */
501
- import { PrismaClient } from '@prisma/client';
577
+ const connSqlite = `import { isAbsolute, resolve, dirname } from 'node:path';
578
+ import { fileURLToPath } from 'node:url';
579
+ import * as schema from './schema.server.ts';
580
+
581
+ // The only file that opens the driver. Runtime-neutral: native bun:sqlite on
582
+ // Bun, better-sqlite3 on Node. Cached on globalThis across dev reloads.
583
+ // A relative SQLite path resolves against the app root (the parent of db/), not
584
+ // process.cwd(), so the connection works under \`webjs dev\` AND when the app is
585
+ // embedded via createRequestHandler from a different working directory.
586
+ const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
587
+ const raw = process.env.DATABASE_URL?.replace(/^file:/, '') ?? 'db/dev.db';
588
+ const url = raw === ':memory:' || isAbsolute(raw) ? raw : resolve(appRoot, raw);
589
+ const g = globalThis as unknown as { __webjs_db?: unknown };
590
+
591
+ async function open() {
592
+ if ((globalThis as { Bun?: unknown }).Bun) {
593
+ const { Database } = await import('bun:sqlite');
594
+ const { drizzle } = await import('drizzle-orm/bun-sqlite');
595
+ return drizzle({ client: new Database(url), relations: schema.relations });
596
+ }
597
+ const { default: Database } = await import('better-sqlite3');
598
+ const { drizzle } = await import('drizzle-orm/better-sqlite3');
599
+ return drizzle({ client: new Database(url), relations: schema.relations });
600
+ }
601
+
602
+ export const db = (g.__webjs_db ??= await open()) as Awaited<ReturnType<typeof open>>;
603
+ `;
502
604
 
503
- const g = globalThis as unknown as { __prisma?: PrismaClient };
605
+ const connPg = `import { drizzle } from 'drizzle-orm/node-postgres';
606
+ import { Pool } from 'pg';
607
+ import * as schema from './schema.server.ts';
504
608
 
505
- export const prisma = g.__prisma ?? new PrismaClient();
506
- if (process.env.NODE_ENV !== 'production') g.__prisma = prisma;
609
+ // The only file that opens the driver. Cached on globalThis across dev reloads.
610
+ const g = globalThis as unknown as { __webjs_db?: unknown };
611
+ function open() {
612
+ return drizzle({ client: new Pool({ connectionString: process.env.DATABASE_URL }), relations: schema.relations });
613
+ }
614
+ export const db = (g.__webjs_db ??= open()) as ReturnType<typeof open>;
615
+ `;
616
+
617
+ await writeFile(join(appDir, 'db', 'connection.server.ts'), dialect === 'postgres' ? connPg : connSqlite);
618
+
619
+ // drizzle-kit config (root, must be this exact filename). DB url from env.
620
+ await writeFile(join(appDir, 'drizzle.config.ts'), dialect === 'postgres'
621
+ ? `import { defineConfig } from 'drizzle-kit';
622
+
623
+ export default defineConfig({
624
+ dialect: 'postgresql',
625
+ schema: './db/schema.server.ts',
626
+ out: './db/migrations',
627
+ dbCredentials: { url: process.env.DATABASE_URL! },
628
+ });
629
+ `
630
+ : `import { defineConfig } from 'drizzle-kit';
631
+
632
+ export default defineConfig({
633
+ dialect: 'sqlite',
634
+ schema: './db/schema.server.ts',
635
+ out: './db/migrations',
636
+ dbCredentials: { url: process.env.DATABASE_URL?.replace(/^file:/, '') ?? 'db/dev.db' },
637
+ });
507
638
  `);
508
639
 
509
- // Env vars: append DATABASE_URL to the .env.example the template
510
- // already copied (if present). The scaffold's root .env.example
511
- // lists auth secrets etc.; we just add the DB line idempotently.
640
+ // Env vars: append DATABASE_URL to the .env.example the template already
641
+ // copied (if present), idempotently.
642
+ const dbUrlLine = dialect === 'postgres'
643
+ ? 'DATABASE_URL=postgres://user:password@localhost:5432/' + name.replace(/[^a-z0-9_]/gi, '_')
644
+ : 'DATABASE_URL=file:./db/dev.db';
512
645
  const envExample = join(appDir, '.env.example');
513
646
  if (existsSync(envExample)) {
514
- const cur = await readFile(envExample, 'utf8');
515
- if (!cur.includes('DATABASE_URL')) {
516
- await writeFile(envExample, cur.replace(/\n?$/, '\n') + '\nDATABASE_URL=file:./prisma/dev.db\n');
647
+ let cur = await readFile(envExample, 'utf8');
648
+ // Replace any existing DATABASE_URL line so it is dialect-correct; else append.
649
+ if (/^DATABASE_URL=.*$/m.test(cur)) {
650
+ cur = cur.replace(/^DATABASE_URL=.*$/m, dbUrlLine);
651
+ } else {
652
+ cur = cur.replace(/\n?$/, '\n') + '\n' + dbUrlLine + '\n';
517
653
  }
654
+ await writeFile(envExample, cur);
518
655
  } else {
519
- await writeFile(envExample, 'DATABASE_URL=file:./prisma/dev.db\n');
656
+ await writeFile(envExample, dbUrlLine + '\n');
520
657
  }
521
658
 
522
- // .gitignore the generated SQLite file.
523
- const gitignore = join(appDir, '.gitignore');
524
- const gitignoreExtra = '\n# SQLite dev database\nprisma/dev.db\nprisma/dev.db-journal\n';
525
- if (existsSync(gitignore)) {
526
- const cur = await readFile(gitignore, 'utf8');
527
- if (!cur.includes('prisma/dev.db')) await writeFile(gitignore, cur + gitignoreExtra);
528
- } else {
529
- await writeFile(gitignore, 'node_modules\n.webjs\n' + gitignoreExtra);
659
+ // .gitignore the generated SQLite file (sqlite only; postgres has no local file).
660
+ if (dialect !== 'postgres') {
661
+ const gitignore = join(appDir, '.gitignore');
662
+ const gitignoreExtra = '\n# SQLite dev database\ndb/dev.db\ndb/dev.db-journal\ndb/dev.db-*\n';
663
+ if (existsSync(gitignore)) {
664
+ const cur = await readFile(gitignore, 'utf8');
665
+ if (!cur.includes('db/dev.db')) await writeFile(gitignore, cur + gitignoreExtra);
666
+ } else {
667
+ await writeFile(gitignore, 'node_modules\n.webjs\n' + gitignoreExtra);
668
+ }
530
669
  }
531
670
 
532
671
  // --- App files (template-specific) ---
@@ -1019,7 +1158,7 @@ ThemeToggle.register('theme-toggle');
1019
1158
  `);
1020
1159
  } // end if (!isApi)
1021
1160
 
1022
- // --- SaaS template extras: auth, dashboard, prisma ---
1161
+ // --- SaaS template extras: auth, dashboard, drizzle User model ---
1023
1162
  if (isSaas) {
1024
1163
  const { writeSaasFiles } = await import('./saas-template.js');
1025
1164
  await writeSaasFiles(appDir);
@@ -1056,9 +1195,9 @@ ThemeToggle.register('theme-toggle');
1056
1195
  dialog,form,field,switch,checkbox}.ts
1057
1196
  components/theme-toggle.ts
1058
1197
  modules/auth/{actions,queries,types.ts}
1059
- lib/{auth,prisma,password}.server.ts
1198
+ lib/{auth,password}.server.ts
1060
1199
  lib/utils/cn.ts ← cn() helper for ui-* components
1061
- prisma/schema.prisma ← User model
1200
+ db/{schema,columns,connection}.server.ts ← Drizzle (User model)
1062
1201
  CONVENTIONS.md, AGENTS.md, CLAUDE.md
1063
1202
  `);
1064
1203
  } else {
@@ -1096,10 +1235,10 @@ For AI agents, read this before editing scaffolded files:
1096
1235
  no-scaffold-placeholder violations (app/page.ts, app/layout.ts).
1097
1236
  That is the signal to replace the example content. Delete each
1098
1237
  marker comment line as you do, and the check goes green.
1099
- • Use Prisma + SQLite for app data. It's already wired up. Define
1100
- real models in prisma/schema.prisma and run \`webjs db migrate\`.
1101
- NEVER store app data in JSON files, in-memory arrays, or
1102
- localStorage as a substitute for the database.
1238
+ • Use Drizzle + SQLite for app data. It's already wired up. Define
1239
+ real models in db/schema.server.ts, then run \`webjs db generate\`
1240
+ and \`webjs db migrate\`. NEVER store app data in JSON files,
1241
+ in-memory arrays, or localStorage as a substitute for the database.
1103
1242
  • Only three scaffolds exist: full-stack (default), api, saas. Don't
1104
1243
  invent template names. If you need a different kind of app, pick
1105
1244
  the closest scaffold and adapt it.
@@ -1129,11 +1268,12 @@ For AI agents, read this before editing scaffolded files:
1129
1268
  // to "dev server up" in one command. The full-stack and saas
1130
1269
  // templates ship with @webjsdev/ui already initialised; the api
1131
1270
  // template has no UI but may add one later. Saas needs a one-time
1132
- // prisma migrate before the first run (the example User model wants
1133
- // its table to exist).
1271
+ // generate + migrate before the first run (the example User model wants
1272
+ // its table to exist). Drizzle splits Prisma's `migrate dev` into
1273
+ // `db:generate` (schema to SQL) then `db:migrate` (apply).
1134
1274
  const installSegment = installed ? '' : `${pm} install && `;
1135
- const prismaSegment = isSaas ? `npx prisma migrate dev --name init && ` : '';
1136
- const runCommand = `cd ${name} && ${installSegment}${prismaSegment}${pm} run dev`;
1275
+ const dbSegment = isSaas ? `${pm} run db:generate && ${pm} run db:migrate && ` : '';
1276
+ const runCommand = `cd ${name} && ${installSegment}${dbSegment}${pm} run dev`;
1137
1277
  // Use `npx webjsdev ui ...` here, not `npx webjs ui ...`. The bare
1138
1278
  // `webjs` npm name is owned by an unrelated package; `npx webjs
1139
1279
  // <cmd>` would fetch THAT package instead of ours when run outside