@webjsdev/cli 0.10.23 → 0.10.25

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
@@ -33,7 +33,7 @@ npx @webjsdev/cli create my-app
33
33
  cd my-app && npm run dev
34
34
  ```
35
35
 
36
- Both `webjs create` and `create-webjs-app` auto-install dependencies in the new directory using your detected package manager (npm / pnpm / yarn / bun). Pass `--no-install` to opt out.
36
+ `webjs create` installs dependencies in the new directory by default on **Node** (it needs `node_modules` to run). On **Bun** it **skips** the install (zero-install: `bun run dev` resolves deps on the fly). Pass `--install` to force the install, or `--no-install` to skip it, on either runtime.
37
37
 
38
38
  ## Commands
39
39
 
package/bin/webjs.js CHANGED
@@ -50,12 +50,12 @@ const USAGE = `webjs commands:
50
50
  webjs doctor Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook, page/layout elision)
51
51
  webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
52
52
  webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
53
- webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
53
+ webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--install|--no-install] Scaffold a new webjs app
54
54
  (only 3 templates exist. default: full-stack, Drizzle, --db sqlite, --runtime node)
55
55
  --runtime bun emits a Bun-flavored app (bun.lock, bun Dockerfile/CI, bun docs);
56
56
  also auto-detected when run via "bun create webjs".
57
- Auto-runs the detected package manager's install in the new dir
58
- unless --no-install is passed.
57
+ Install default is per runtime: Node installs; Bun skips (zero-install,
58
+ "bun run dev" resolves deps on the fly). --install / --no-install override.
59
59
  webjs db generate Generate a SQL migration from the schema (drizzle-kit generate)
60
60
  webjs db migrate Apply pending migrations (drizzle-kit migrate)
61
61
  webjs db push Push the schema straight to the dev DB (drizzle-kit push)
@@ -521,7 +521,11 @@ files.
521
521
  Full docs: https://docs.webjs.com`);
522
522
  process.exit(1);
523
523
  }
524
+ // Install policy (#682). Default per runtime: Node installs (needs
525
+ // node_modules to run); Bun skips (zero-install, `bun run dev` resolves
526
+ // on the fly). `--install` / `--no-install` override either way.
524
527
  const noInstall = rest.includes('--no-install');
528
+ const explicitInstall = rest.includes('--install');
525
529
  // --db picks the database dialect: sqlite (default) or postgres.
526
530
  const db = flag(rest, '--db', 'sqlite');
527
531
  // --runtime picks the target runtime: node (default) or bun. Orthogonal
@@ -532,8 +536,9 @@ Full docs: https://docs.webjs.com`);
532
536
  console.error(`Error: unknown --runtime '${runtime}'. Only node / bun are supported.`);
533
537
  process.exit(1);
534
538
  }
535
- const { scaffoldApp } = await import('../lib/create.js');
536
- await scaffoldApp(name, process.cwd(), { template, db, runtime, install: !noInstall });
539
+ const { scaffoldApp, resolveCreateInstall } = await import('../lib/create.js');
540
+ const install = resolveCreateInstall({ runtime, explicitInstall, noInstall });
541
+ await scaffoldApp(name, process.cwd(), { template, db, runtime, install });
537
542
  break;
538
543
  }
539
544
  case 'vendor': {
package/lib/create.js CHANGED
@@ -14,7 +14,7 @@
14
14
  import { mkdir, writeFile, readFile, cp } from 'node:fs/promises';
15
15
  import { join, resolve, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
- import { existsSync } from 'node:fs';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
18
  import { createRequire } from 'node:module';
19
19
  import { spawnSync } from 'node:child_process';
20
20
  import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi } from './runtime-rewrite.js';
@@ -35,6 +35,68 @@ function detectPackageManager() {
35
35
  return 'npm';
36
36
  }
37
37
 
38
+ /**
39
+ * Decide whether `webjs create` runs the post-scaffold install, per target
40
+ * runtime (#682). Node installs by default (it needs `node_modules` to run);
41
+ * Bun SKIPS by default (zero-install: `bun run dev` resolves deps on the fly,
42
+ * #675). Under Bun zero-install deps resolve to their LATEST version (ranges
43
+ * and any lockfile are ignored at runtime, #690), so `bun install` is the path
44
+ * to pinned, reproducible versions. Explicit flags win: `--install` forces it
45
+ * on, `--no-install` forces it off. The CLI entry points call this and pass an
46
+ * explicit boolean to `scaffoldApp`, so the library default (no install unless
47
+ * `install: true`) is unchanged for programmatic callers.
48
+ *
49
+ * @param {{ runtime?: string, explicitInstall?: boolean, noInstall?: boolean }} [o]
50
+ * @returns {boolean}
51
+ */
52
+ export function resolveCreateInstall({ runtime, explicitInstall, noInstall } = {}) {
53
+ if (explicitInstall) return true;
54
+ if (noInstall) return false;
55
+ const isBun = runtime === 'bun' || (!runtime && detectPackageManager() === 'bun');
56
+ return !isBun;
57
+ }
58
+
59
+ /**
60
+ * Read the EXACT version of `@webjsdev/<pkg>` the scaffolding CLI ships with, so
61
+ * the generated `package.json` pins it precisely and BOTH npm and bun resolve
62
+ * the same version (#692). Walks the `require.resolve` node_modules search paths
63
+ * and fs-reads `<pkg>/package.json` directly: `@webjsdev/server` (and `ui`) hide
64
+ * `./package.json` behind `exports`, so a bare `require('<pkg>/package.json')`
65
+ * fails (same constraint #687 hit). Falls back to `'latest'` when the package is
66
+ * not resolvable (defensive; the CLI's own dependency closure is normally
67
+ * present), which keeps the scaffold working rather than emitting a bad pin.
68
+ * @param {string} pkg e.g. 'cli', 'core', 'server'
69
+ * @returns {string} an exact version, or 'latest'
70
+ */
71
+ function webjsdevVersion(pkg) {
72
+ const req = createRequire(import.meta.url);
73
+ for (const base of (req.resolve.paths(`@webjsdev/${pkg}`) || [])) {
74
+ const pj = join(base, '@webjsdev', pkg, 'package.json');
75
+ if (existsSync(pj)) {
76
+ try {
77
+ const v = JSON.parse(readFileSync(pj, 'utf8')).version;
78
+ if (v) return v;
79
+ } catch { /* unreadable; keep looking, then fall back */ }
80
+ }
81
+ }
82
+ return 'latest';
83
+ }
84
+
85
+ /**
86
+ * Exact third-party dep versions the scaffold pins (#692). These are template
87
+ * deps the CLI does NOT itself depend on, so they cannot be read from the CLI's
88
+ * closure (unlike `@webjsdev/*`). Pinned EXACT so npm and bun resolve identically
89
+ * (a `^` range diverges: npm takes latest-in-range, bun zero-install takes
90
+ * absolute latest, #690). Drizzle is the 1.0 relations-v2 RC the scaffold's db
91
+ * code targets (its npm `latest` tag is a 0.x line, so a range would resolve the
92
+ * wrong major under bun). Refresh on a deliberate bump, same as the old ranges.
93
+ */
94
+ const SCAFFOLD_DEP_VERSIONS = {
95
+ 'drizzle-orm': '1.0.0-rc.3',
96
+ 'drizzle-kit': '1.0.0-rc.3',
97
+ pg: '8.22.0',
98
+ };
99
+
38
100
  /**
39
101
  * Run `<pm> install` inside the scaffolded app. Returns true on success.
40
102
  * Inherits stdio so the user sees the install progress live. Caller decides
@@ -273,6 +335,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
273
335
  throw new Error(`Unknown --runtime '${runtime}'. Only ${VALID_RUNTIMES.join(' / ')} are supported.`);
274
336
  }
275
337
  const isBun = runtime === 'bun';
338
+ // Zero-install Bun entry (#675): the app-local `webjs-bun.mjs` bootstrap, run
339
+ // under `bun --bun`, so the server resolves the CLI + deps via Bun auto-install
340
+ // (no `bun install` required). App-local so it is resolvable with no node_modules.
341
+ const bunBoot = 'bun --bun webjs-bun.mjs';
276
342
  const appDir = join(cwd, name);
277
343
  if (existsSync(appDir)) {
278
344
  console.error(`Error: directory '${name}' already exists.`);
@@ -318,18 +384,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
318
384
  // so `npm run start` (a thin alias) behaves identically. Drizzle has no
319
385
  // codegen, so there is no dev `before` step.
320
386
  //
321
- // Bun runtime (#541): the long-running server scripts (`dev` / `start`)
322
- // are prefixed `bun --bun` so the app SERVES on Bun. The `--bun` overrides
323
- // the `webjs` bin's `#!/usr/bin/env node` shebang (without it `bun run dev`
387
+ // Bun runtime (#541, zero-install #675): the server + DB scripts run via
388
+ // the `webjs-bun.mjs` bootstrap under `bun --bun`. `--bun` overrides the
389
+ // `webjs` bin's `#!/usr/bin/env node` shebang (without it `bun run dev`
324
390
  // would exec webjs under Node, silently running the "bun" app on Node).
325
- // Baking it into the script body means a plain `bun run dev` (or even
326
- // `npm run dev`) starts on Bun, so a user never has to remember the flag.
327
- // The runtime-neutral tooling scripts below (test / db / check / typecheck
391
+ // Routing through the bootstrap file (which imports the CLI by bare
392
+ // specifier) instead of the `webjs` bin means Bun's auto-install resolves
393
+ // `@webjsdev/*` and your deps ON DEMAND, so `bun run dev` / `start` work
394
+ // with NO `bun install` (install becomes optional, for editor types /
395
+ // offline). The runtime-neutral tooling scripts (test / check / typecheck
328
396
  // / doctor) stay plain `webjs ...`: they spawn node tooling (`node --test`,
329
- // drizzle-kit, tsc) and forcing `--bun` there buys nothing (and `webjs
330
- // test` shells `node --test`, which a `bun --test` would not be).
331
- dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
332
- start: isBun ? 'bun --bun webjs start' : 'webjs start',
397
+ // tsc), which needs an install, so they are not part of the zero-install path.
398
+ dev: isBun ? `${bunBoot} dev` : 'webjs dev',
399
+ start: isBun ? `${bunBoot} start` : 'webjs start',
333
400
  test: 'webjs test',
334
401
  'test:server': 'webjs test --server',
335
402
  'test:browser': 'webjs test --browser',
@@ -340,23 +407,33 @@ export async function scaffoldApp(name, cwd, opts = {}) {
340
407
  // vendor pins, @webjsdev versions, git hook). Local tool, NOT a CI gate
341
408
  // (its env-drift + network pin-freshness checks would make CI flaky).
342
409
  doctor: 'webjs doctor',
343
- 'db:generate': 'webjs db generate',
344
- 'db:migrate': 'webjs db migrate',
345
- 'db:push': 'webjs db push',
346
- 'db:studio': 'webjs db studio',
347
- 'db:seed': 'webjs db seed',
410
+ 'db:generate': isBun ? `${bunBoot} db generate` : 'webjs db generate',
411
+ 'db:migrate': isBun ? `${bunBoot} db migrate` : 'webjs db migrate',
412
+ 'db:push': isBun ? `${bunBoot} db push` : 'webjs db push',
413
+ 'db:studio': isBun ? `${bunBoot} db studio` : 'webjs db studio',
414
+ 'db:seed': isBun ? `${bunBoot} db seed` : 'webjs db seed',
348
415
  },
349
416
  dependencies: {
350
417
  // Drizzle ORM (no codegen, no engine binary). Pinned to the 1.0 line
351
- // for relations v2. The SQLite/Postgres driver below is dialect-picked.
352
- 'drizzle-orm': '^1.0.0-rc.3',
353
- ...(dialect === 'postgres' ? { pg: '^8.13.0' } : { 'better-sqlite3': '^12.11.1' }),
354
- '@webjsdev/cli': 'latest',
355
- '@webjsdev/core': 'latest',
356
- '@webjsdev/server': 'latest',
418
+ // for relations v2. SQLite needs NO driver dependency: the connection
419
+ // uses the built-in node:sqlite (Node) / bun:sqlite (Bun) via Drizzle's
420
+ // node-sqlite / bun-sqlite adapters. Postgres still needs the pg driver.
421
+ // Exact pins (#692): npm and bun must resolve identical versions. A `^`
422
+ // range diverges (npm = latest-in-range; bun zero-install = absolute
423
+ // latest, #690), and drizzle's npm `latest` tag is a 0.x line, so a range
424
+ // would pull the wrong major under bun. @webjsdev/* are pinned to the
425
+ // versions the scaffolding CLI itself ships with.
426
+ 'drizzle-orm': SCAFFOLD_DEP_VERSIONS['drizzle-orm'],
427
+ ...(dialect === 'postgres' ? { pg: SCAFFOLD_DEP_VERSIONS.pg } : {}),
428
+ '@webjsdev/cli': webjsdevVersion('cli'),
429
+ '@webjsdev/core': webjsdevVersion('core'),
430
+ '@webjsdev/server': webjsdevVersion('server'),
357
431
  },
358
432
  devDependencies: {
359
- 'drizzle-kit': '^1.0.0-rc.3',
433
+ // Exact pin (#692): drizzle-kit is resolved under bun zero-install via
434
+ // `bun run db:generate` / `db:migrate`, so it must match drizzle-orm's
435
+ // exact version across runtimes (a range would diverge, #690).
436
+ 'drizzle-kit': SCAFFOLD_DEP_VERSIONS['drizzle-kit'],
360
437
  ...(dialect === 'postgres' ? { '@types/pg': '^8.11.0' } : {}),
361
438
  // The TypeScript compiler, for `npm run typecheck` (webjs typecheck runs
362
439
  // tsc --noEmit). Not needed at runtime (Node strips types in place), only
@@ -394,18 +471,28 @@ export async function scaffoldApp(name, cwd, opts = {}) {
394
471
  webjs: {
395
472
  // Drizzle has no codegen, so there is no dev `before` step. Production
396
473
  // applies pending migrations at boot via `webjs db migrate` (drizzle-kit).
397
- start: { before: ['webjs db migrate'] },
474
+ // On Bun this runs through the same zero-install bootstrap as `start`, so
475
+ // the boot-time migrate needs no `webjs` bin in node_modules (#675).
476
+ start: { before: [isBun ? `${bunBoot} db migrate` : 'webjs db migrate'] },
398
477
  },
399
- // Bun runtime (#541): Bun does NOT run a dependency's postinstall by default
400
- // (a security default); a package must be listed here for its install script
401
- // to run on `bun install`. The sqlite driver `better-sqlite3` fetches its
402
- // native prebuild in a postinstall, so without this the native binding is
403
- // missing and the app crashes at first DB access. Postgres `pg` is pure JS
404
- // (no postinstall), so the list is sqlite-only. Omitted entirely on Node
405
- // (npm runs postinstalls), keeping the node-mode package.json byte-identical.
406
- ...(isBun && dialect !== 'postgres' ? { trustedDependencies: ['better-sqlite3'] } : {}),
407
478
  }, null, 2) + '\n');
408
479
 
480
+ // The zero-install Bun entry (#675). `bun run dev` / `start` invoke this via
481
+ // `bun --bun` (see the scripts above). Importing the webjs CLI by bare
482
+ // specifier lets Bun auto-install resolve `@webjsdev/*` and your deps on
483
+ // demand, so a fresh app serves with NO `bun install`. The CLI reads its
484
+ // command (dev / start / db ...) and flags straight from argv. Node apps do
485
+ // not get this file; they run the `webjs` bin directly.
486
+ if (isBun) {
487
+ await writeFile(join(appDir, 'webjs-bun.mjs'),
488
+ '// Zero-install Bun entry (webjs #675). Run via `bun --bun webjs-bun.mjs <cmd>`\n' +
489
+ '// (the dev / start / db npm scripts do this). Importing the CLI by bare\n' +
490
+ '// specifier lets Bun auto-install resolve @webjsdev/* and your deps on\n' +
491
+ '// demand, so the app serves with no `bun install` (install stays optional,\n' +
492
+ '// for editor types and offline runs). Args pass through to the CLI.\n' +
493
+ "await import('@webjsdev/cli/bin/webjs.js');\n");
494
+ }
495
+
409
496
  await writeFile(join(appDir, 'tsconfig.json'), JSON.stringify({
410
497
  compilerOptions: {
411
498
  target: 'ES2022',
@@ -650,8 +737,9 @@ export type User = typeof users.$inferSelect;
650
737
  import { fileURLToPath } from 'node:url';
651
738
  import * as schema from './schema.server.ts';
652
739
 
653
- // The only file that opens the driver. Runtime-neutral: native bun:sqlite on
654
- // Bun, better-sqlite3 on Node. Cached on globalThis across dev reloads.
740
+ // The only file that opens the driver. Runtime-neutral and ZERO native deps:
741
+ // built-in bun:sqlite on Bun, built-in node:sqlite on Node. Cached on
742
+ // globalThis across dev reloads.
655
743
  // A relative SQLite path resolves against the app root (the parent of db/), not
656
744
  // process.cwd(), so the connection works under \`webjs dev\` AND when the app is
657
745
  // embedded via createRequestHandler from a different working directory.
@@ -660,15 +748,25 @@ const raw = process.env.DATABASE_URL?.replace(/^file:/, '') ?? 'db/dev.db';
660
748
  const url = raw === ':memory:' || isAbsolute(raw) ? raw : resolve(appRoot, raw);
661
749
  const g = globalThis as unknown as { __webjs_db?: unknown };
662
750
 
751
+ // Both node:sqlite and bun:sqlite default \`busy_timeout\` to 0, so a concurrent
752
+ // writer throws \`database is locked\` immediately. Restore a 5s wait (the old
753
+ // better-sqlite3 default) so contended access waits, and WAL so readers proceed
754
+ // alongside one writer.
755
+ function tune<T extends { exec(sql: string): unknown }>(client: T): T {
756
+ client.exec('PRAGMA busy_timeout = 5000');
757
+ client.exec('PRAGMA journal_mode = WAL');
758
+ return client;
759
+ }
760
+
663
761
  async function open() {
664
762
  if ((globalThis as { Bun?: unknown }).Bun) {
665
763
  const { Database } = await import('bun:sqlite');
666
764
  const { drizzle } = await import('drizzle-orm/bun-sqlite');
667
- return drizzle({ client: new Database(url), relations: schema.relations });
765
+ return drizzle({ client: tune(new Database(url)), relations: schema.relations });
668
766
  }
669
- const { default: Database } = await import('better-sqlite3');
670
- const { drizzle } = await import('drizzle-orm/better-sqlite3');
671
- return drizzle({ client: new Database(url), relations: schema.relations });
767
+ const { DatabaseSync } = await import('node:sqlite');
768
+ const { drizzle } = await import('drizzle-orm/node-sqlite');
769
+ return drizzle({ client: tune(new DatabaseSync(url)), relations: schema.relations });
672
770
  }
673
771
 
674
772
  export const db = (g.__webjs_db ??= await open()) as Awaited<ReturnType<typeof open>>;
@@ -701,6 +799,12 @@ export default defineConfig({
701
799
  `
702
800
  : `import { defineConfig } from 'drizzle-kit';
703
801
 
802
+ // No 'driver' is set: drizzle-kit auto-selects the SQLite driver for
803
+ // migrate/push/studio from the runtime, picking the built-in node:sqlite on
804
+ // Node and bun:sqlite on Bun (it only reaches for better-sqlite3 when that
805
+ // package is present, which this app does not install). That auto-selection is
806
+ // what keeps \`webjs db migrate\` free of a native driver, matching the runtime
807
+ // connection in db/connection.server.ts.
704
808
  export default defineConfig({
705
809
  dialect: 'sqlite',
706
810
  schema: './db/schema.server.ts',
@@ -1370,6 +1474,12 @@ For AI agents, read this before editing scaffolded files:
1370
1474
  if (!installed) {
1371
1475
  console.log(`\n[warn] ${pm} install failed. Run '${pm} install' manually in ${name}/ to finish setup.\n`);
1372
1476
  }
1477
+ } else if (isBun) {
1478
+ // Bun zero-install (#675): no install needed; `bun run dev` resolves deps on
1479
+ // the fly. They resolve to LATEST (ranges + lockfile ignored at runtime,
1480
+ // #690), so point at `bun install` for pinned, reproducible versions.
1481
+ console.log(`Skipped install. Bun resolves dependencies on the fly, so 'bun run dev' and 'bun run start' work as-is (no node_modules).`);
1482
+ console.log(`These resolve to each dependency's LATEST version. Run 'bun install' in ${name}/ when you want pinned, reproducible versions (and editor type intelligence).\n`);
1373
1483
  }
1374
1484
 
1375
1485
  // Next-steps banner prints LAST so the actionable command is the
@@ -1381,7 +1491,10 @@ For AI agents, read this before editing scaffolded files:
1381
1491
  // generate + migrate before the first run (the example User model wants
1382
1492
  // its table to exist). Drizzle splits Prisma's `migrate dev` into
1383
1493
  // `db:generate` (schema to SQL) then `db:migrate` (apply).
1384
- const installSegment = installed ? '' : `${pm} install && `;
1494
+ // Omit the install step from the next-steps line for a deliberate Bun
1495
+ // zero-install (#682): `bun run dev` resolves deps on the fly, so it works
1496
+ // without an install. Otherwise (Node, or an install that did not run) keep it.
1497
+ const installSegment = (installed || (isBun && !shouldInstall)) ? '' : `${pm} install && `;
1385
1498
  const dbSegment = isSaas ? `${pm} run db:generate && ${pm} run db:migrate && ` : '';
1386
1499
  const runCommand = `cd ${name} && ${installSegment}${dbSegment}${pm} run dev`;
1387
1500
  // Use `npx webjsdev ui ...` here, not `npx webjs ui ...`. The bare
@@ -58,14 +58,10 @@ export function bunifyProse(s) {
58
58
  'Dockerfile pins `node:24-alpine` (the same Node major CI uses), installs\ndeps (no build step, since Drizzle has no codegen), and starts via',
59
59
  'Dockerfile is a pure `oven/bun:1` image (no Node, since `webjs db migrate`\nresolves drizzle-kit and runs under Bun with no `npx`, #570), installs deps\nwith `bun install` (no build step, since Drizzle has no codegen), and starts via',
60
60
  )
61
- // The "Running on Bun" section frames Bun as opt-in ("force it with --bun").
62
- // In a bun-flavored app the dev/start scripts ALREADY embed --bun, so reframe
63
- // it as the configured default.
61
+ // The "Running on Bun" section heading is reframed for a bun-flavored app.
62
+ // Its body already describes the webjs-bun.mjs bootstrap + zero-install in the
63
+ // template (#675), so only the heading needs the runtime reframe.
64
64
  .replaceAll('### Running on Bun instead of Node', '### Runtime: this app runs on Bun')
65
- .replaceAll(
66
- 'The same `package.json` scripts work on\neither; to run under Bun, force it with `--bun` so the server executes on Bun\nrather than the `webjs` bin\'s Node shebang:',
67
- 'This app is configured for Bun. Its `dev` / `start` scripts already force\n`--bun` (which overrides the `webjs` bin\'s Node shebang), so a plain `bun run dev`\nserves on Bun. The other scripts (test / db / check) run on Node, the runtime\nthe `webjs` tooling targets:',
68
- )
69
65
  // Invocation styles first, so "npm create webjs@latest" does not get
70
66
  // mangled by the generic "npm <x>" rules below.
71
67
  .replaceAll('npm create webjs@latest', 'bun create webjs')
@@ -104,8 +100,8 @@ export function bunifyProse(s) {
104
100
  * `node:24-alpine` base with a copied Bun binary, since the installed CLI could
105
101
  * still shell `npx`, which a pure Bun image lacks. #595 flipped it once the
106
102
  * npx-free CLI shipped.) `oven/bun:1` is Debian-based: `ca-certificates` ship in
107
- * the image and better-sqlite3 fetches its glibc prebuild on `bun install`
108
- * (gated by `trustedDependencies`), so no build toolchain is needed.
103
+ * the image, and SQLite uses the built-in bun:sqlite (no native module), so no
104
+ * build toolchain is needed.
109
105
  *
110
106
  * @param {string} s
111
107
  * @returns {string}
@@ -126,13 +122,13 @@ export function bunifyDockerfile(s) {
126
122
  .replace('FROM node:24-alpine', 'FROM oven/bun:1')
127
123
  // Debian base: ca-certificates already present, no `apk`. Drop the alpine line.
128
124
  .replace(
129
- /# ca-certificates for outbound TLS \(e\.g\. a managed Postgres\)\. better-sqlite3\n# is a prebuilt native module, so no build toolchain is needed here\.\nRUN apk add --no-cache ca-certificates\n\n/,
130
- '# The Debian-based oven/bun image ships ca-certificates for outbound TLS (e.g. a\n# managed Postgres). better-sqlite3 fetches its glibc prebuild on `bun install`\n# (gated by trustedDependencies), so no build toolchain is needed.\n\n',
125
+ /# ca-certificates for outbound TLS \(e\.g\. a managed Postgres\)\. SQLite uses the\n# built-in node:sqlite \(no native module, no build toolchain needed\)\.\nRUN apk add --no-cache ca-certificates\n\n/,
126
+ '# The Debian-based oven/bun image ships ca-certificates for outbound TLS (e.g. a\n# managed Postgres). SQLite uses the built-in bun:sqlite (no native module), so no\n# build toolchain is needed.\n\n',
131
127
  )
132
128
  // Lockfile + install (bun.lock, bun install).
133
129
  .replace(
134
130
  '# package-lock.json is optional (it\'s absent when the app was scaffolded with\n# --no-install); the glob keeps the COPY working with or without it.\nCOPY package.json package-lock.json* ./\nRUN npm install --no-audit --no-fund',
135
- '# bun.lock is optional (absent when scaffolded with --no-install); the glob keeps\n# the COPY working with or without it. trustedDependencies in package.json lets\n# better-sqlite3\'s native-prebuild postinstall run (bun skips postinstalls).\nCOPY package.json bun.lock* ./\nRUN bun install',
131
+ '# bun.lock is optional (absent when scaffolded with --no-install); the glob keeps\n# the COPY working with or without it. SQLite uses the built-in bun:sqlite, so no\n# native dependency or postinstall is involved.\nCOPY package.json bun.lock* ./\nRUN bun install',
136
132
  )
137
133
  // Healthcheck: the pure Bun image has no node; use `bun -e`. Keep the
138
134
  // dependency-free-probe comment accurate (the probe runs under Bun now).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.23",
3
+ "version": "0.10.25",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -139,7 +139,7 @@ self-review loop.
139
139
  a static field on the class.
140
140
  - **Never extend raw HTMLElement directly for app components.** Always subclass `WebComponent` (or the factory form `WebComponent({...})`) to hook into SSR, lifecycle, elision, and the reactive property system. Extend raw HTMLElement only for rare native-API edge cases (like form-associated `ElementInternals` or customized built-in elements), and add a `webjs-allow-htmlelement: <reason>` comment to acknowledge the exception.
141
141
  - One function per server action file (`*.server.ts`).
142
- - Server-only code (a DB driver like `better-sqlite3`/`pg`, `node:*`, anything that needs Node APIs)
142
+ - Server-only code (a DB driver like `pg`, `node:*`, anything that needs Node APIs)
143
143
  goes only in `.server.{js,ts}` files, `route.ts` handlers, or
144
144
  `middleware.ts`. Never in pages, layouts, or components. Wrap the access in
145
145
  a `.server.{js,ts}` file; the framework rewrites that import into an RPC
@@ -114,7 +114,7 @@ self-review loop.
114
114
  - One function per server action file (*.server.ts)
115
115
  - Components must call customElements.define('tag', Class)
116
116
  - **Never extend raw HTMLElement directly for app components.** Always subclass `WebComponent` (or the factory form `WebComponent({...})`) to hook into SSR, lifecycle, elision, and the reactive property system. Extend raw HTMLElement only for rare native-API edge cases (like form-associated `ElementInternals` or customized built-in elements), and add a `webjs-allow-htmlelement: <reason>` comment to acknowledge the exception.
117
- - Server-only code (the DB driver `better-sqlite3` / `pg`, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. The DB lives in db/*.server.ts (db/connection.server.ts); lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file: if a lib/ file needs Node APIs, only import it from server-only files.
117
+ - Server-only code (the DB driver `pg`, node:*, anything that needs Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap the access in a .server.{js,ts} file; the framework rewrites that import into an RPC stub for the browser. The DB lives in db/*.server.ts (db/connection.server.ts); lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); follow the same rule per file: if a lib/ file needs Node APIs, only import it from server-only files.
118
118
  - Directives: webjs exports the lit directives with no clean native equivalent (`repeat`, `unsafeHTML`, `live`, `keyed`, `guard`, `cache`, `until`, `ref` / `createRef`, `templateContent`, `asyncAppend` / `asyncReplace`, `watch`). Lit's `classMap` / `styleMap` / `ifDefined` / `when` / `choose` are NOT exported. For those, use plain template-literal expressions (`class=${cond ? 'a' : 'b'}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead.
119
119
  - **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.
120
120
  - **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.
@@ -107,10 +107,10 @@ each change must include.
107
107
  - **Erasable TypeScript only.** The runtime strips types via `module.stripTypeScriptTypes` (Node's built-in, or `amaro` on Bun) (whitespace replacement, byte-exact position preservation, no sourcemap). The scaffold's tsconfig.json sets `erasableSyntaxOnly: true`, so the TS compiler rejects `enum`, `namespace` with values, constructor parameter properties, legacy decorators with `emitDecoratorMetadata`, and `import = require`. Use erasable equivalents: `const X = { ... } as const` plus a derived union type instead of `enum`; explicit fields plus constructor body assignments instead of parameter properties. If `erasableSyntaxOnly` is disabled and non-erasable syntax is used, the dev server fails at strip time and returns a 500 pointing at the `no-non-erasable-typescript` lint rule. webjs is buildless end-to-end and has no bundler fallback.
108
108
  - Tagged template: html`<div>${value}</div>` with css`...` for styles.
109
109
  - **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 (`static shadow = true`) legitimately author `static styles = css\`...\`` for scoped CSS; don't use inline `style="..."` there.
110
- - Components: extend the `WebComponent({ ... })` factory to declare reactive properties (e.g. `extends WebComponent({ count: Number })`; per-prop options via `prop(Number, { reflect: true })`), add `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. A hand-written `static properties` throws at construction (`no-static-properties`); set defaults via the `default` option or the constructor, never a class-field initializer (`reactive-props-no-class-field`). Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`), flagged by `array-prop-uses-array-type`. **Never extend raw HTMLElement directly for app components.** Always subclass `WebComponent` (or the factory form `WebComponent({...})`) to hook into SSR, lifecycle, elision, and the reactive property system. Extend raw HTMLElement only for rare native-API edge cases (like form-associated `ElementInternals` or customized built-in elements), and add a `webjs-allow-htmlelement: <reason>` comment to acknowledge the exception.
110
+ - Components: extend the `WebComponent({ ... })` factory to declare reactive properties (e.g. `extends WebComponent({ count: Number })`; per-prop options via `prop(Number, { reflect: true })`), add `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. A hand-written `static properties` throws at construction (`no-static-properties`); set defaults in the constructor, never a class-field initializer (`reactive-props-no-class-field`). Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`), flagged by `array-prop-uses-array-type`. **Never extend raw HTMLElement directly for app components.** Always subclass `WebComponent` (or the factory form `WebComponent({...})`) to hook into SSR, lifecycle, elision, and the reactive property system. Extend raw HTMLElement only for rare native-API edge cases (like form-associated `ElementInternals` or customized built-in elements), and add a `webjs-allow-htmlelement: <reason>` comment to acknowledge the exception.
111
111
  - 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.
112
112
  - Server actions: *.server.ts files with one exported async function each.
113
- - 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.
113
+ - Server-only code (a DB driver like 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.
114
114
  - Directives: webjs exports the lit directives with no clean native equivalent (`repeat`, `unsafeHTML`, `live`, `keyed`, `guard`, `cache`, `until`, `ref` / `createRef`, `templateContent`, `asyncAppend` / `asyncReplace`, `watch`). Lit's `classMap` / `styleMap` / `ifDefined` / `when` / `choose` are NOT exported; use plain template-literal expressions instead.
115
115
  - Context: import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context'
116
116
  - Task: import { Task, TaskStatus } from '@webjsdev/core/task'
@@ -404,15 +404,24 @@ an npm `prestart` hook.
404
404
 
405
405
  ### Running on Bun instead of Node
406
406
 
407
- webjs runs on **Node 24+ or Bun**. The same `package.json` scripts work on
408
- either; to run under Bun, force it with `--bun` so the server executes on Bun
409
- rather than the `webjs` bin's Node shebang:
407
+ webjs runs on **Node 24+ or Bun**. A `--runtime bun` app routes its `dev` /
408
+ `start` / `db` scripts through a `webjs-bun.mjs` bootstrap under `bun --bun`
409
+ (which overrides the `webjs` bin's Node shebang so the server runs on Bun). The
410
+ bootstrap imports the CLI by bare specifier, so Bun auto-install resolves deps on
411
+ demand and **no `bun install` is needed**:
410
412
 
411
413
  ```sh
412
- bun install
413
- bun --bun run dev # or: bun --bun run start
414
+ bun run dev # or: bun run start (no install step required)
414
415
  ```
415
416
 
417
+ `bun create` does not run an install on Bun, so a fresh app serves immediately.
418
+ Under zero-install, deps resolve to their LATEST version: ranges and any
419
+ `bun.lock` are ignored by the runtime auto-install (only an exact `package.json`
420
+ pin is honored, via an `onLoad` rewrite). Run `bun install` when you want pinned,
421
+ reproducible versions (it materializes `node_modules` from the lockfile) or
422
+ editor type intelligence. (To run a Node-flavored app on Bun instead, force `bun --bun run
423
+ dev`, which still expects an install.)
424
+
416
425
  On Node the `.ts` type-stripping is the built-in `module.stripTypeScriptTypes`;
417
426
  on Bun (which has no built-in) it comes from `amaro` automatically, so the same
418
427
  source serves identically. SSR action-result seeding (an internal hydration
@@ -632,7 +641,7 @@ the click handler is inert). Two consequences for how you write code:
632
641
  reactivity) and never in `connectedCallback` (which the server
633
642
  doesn't run). For reactive properties declared via the
634
643
  `WebComponent({ ... })` factory, set the default in the constructor
635
- or pass the `default` option (e.g. `prop(Number, { default: 0 })`).
644
+ after `super()`.
636
645
  2. **`connectedCallback` is browser-only.** Use it for
637
646
  `localStorage`, viewport size, online status, or anything that
638
647
  genuinely can't be known on the server. Read the value, then
@@ -1153,7 +1162,7 @@ composition, so a nested shell ends up dropped by the HTML parser.
1153
1162
  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.
1154
1163
  2. **Server-only code goes in `.server.{js,ts}` files, `route.ts`
1155
1164
  handlers, or `middleware.ts`. Never in pages, layouts, or
1156
- components.** Direct imports of a DB driver (`better-sqlite3` / `pg`),
1165
+ components.** Direct imports of a DB driver (`pg`),
1157
1166
  `node:*`, or any server-only dependency from a page, layout, loading.ts,
1158
1167
  error.ts, not-found.ts, or component will crash the browser at module load.
1159
1168
  Wrap the access in a `.server.{js,ts}` file; the framework
@@ -400,7 +400,7 @@ modules/
400
400
  - One exported function per server action/query file
401
401
  - 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`).
402
402
  - Components must call `Class.register('tag')`
403
- - **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."
403
+ - **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 (`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."
404
404
  - Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
405
405
  - **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).
406
406
  - **Keep pages and layouts as pure carriers, so their modules stay out of the network tab.** A page/layout never hydrates; the framework drops its module from the browser as long as its only browser-relevant job is registering the components it imports. It starts shipping its own module (invisible in tests) the moment its closure does any OTHER client work. So do not give a page/layout module-scope client work (a top-level call, a `window` / `document` / `customElements` access, a bare side-effect import, or a `@webjsdev/core/client-router` import: routing is automatic), and do not import a client-global-touching non-component util into it. Put client behaviour in a component, server-only code in `.server.{js,ts}`. Self-check: `page.ts` / `layout.ts` should not appear in the browser's network tab.
@@ -671,7 +671,7 @@ export class MyWidget extends WebComponent({
671
671
  MyWidget.register('my-widget');
672
672
  ```
673
673
 
674
- Reactive properties are declared one way: pass the properties shape directly to the base-class factory `WebComponent({ ... })` (e.g. `label: String`). The property types flow to `this.<prop>` with no `declare` lines needed, and the factory installs the reactive accessors so a class-field initializer can never clobber them. For per-property options use the `prop()` helper inside the shape (`count: prop(Number, { reflect: true })`, `mode: prop({ state: true })`); narrow a type with `prop<Student>(Object)`. Set defaults via the `default` option (`prop(Number, { default: 0 })`) or by assigning in the constructor after `super()`. A hand-written `static properties = { ... }` THROWS at construction (`no-static-properties`). The factory gives you full intelligence in any tsserver-backed editor. See the Editor Setup docs for the standalone `@webjsdev/intellisense` (no Lit dependency) that extends this to tag / attribute intelligence inside `html\`…\`` templates (go-to-definition, binding-aware completions, value/binding diagnostics, hover); in VS Code / Cursor / Windsurf the `webjs` extension bundles it automatically.
674
+ Reactive properties are declared one way: pass the properties shape directly to the base-class factory `WebComponent({ ... })` (e.g. `label: String`). The property types flow to `this.<prop>` with no `declare` lines needed, and the factory installs the reactive accessors so a class-field initializer can never clobber them. For per-property options use the `prop()` helper inside the shape (`count: prop(Number, { reflect: true })`, `mode: prop({ state: true })`); narrow a type with `prop<Student>(Object)`. Set defaults by assigning in the constructor after `super()`. A hand-written `static properties = { ... }` THROWS at construction (`no-static-properties`). The factory gives you full intelligence in any tsserver-backed editor. See the Editor Setup docs for the standalone `@webjsdev/intellisense` (no Lit dependency) that extends this to tag / attribute intelligence inside `html\`…\`` templates (go-to-definition, binding-aware completions, value/binding diagnostics, hover); in VS Code / Cursor / Windsurf the `webjs` extension bundles it automatically.
675
675
 
676
676
  **Rules:**
677
677
  - One component per file
@@ -683,7 +683,7 @@ Reactive properties are declared one way: pass the properties shape directly to
683
683
  - **Never extend raw HTMLElement directly for app components.** Always subclass `WebComponent` (or the factory form `WebComponent({...})`) to hook into SSR, lifecycle, elision, and the reactive property system. Extend raw HTMLElement only for rare native-API edge cases (like form-associated `ElementInternals` or customized built-in elements), and add a `webjs-allow-htmlelement: <reason>` comment to acknowledge the exception.
684
684
  - Tag name must contain a hyphen (HTML spec)
685
685
  - Always call `Class.register('tag')`. That's the standard DOM API.
686
- - **Reactive props are declared via the base-class factory `WebComponent({ ... })`.** A hand-written `static properties = { ... }` throws at construction (`webjs check` flags it via `no-static-properties`). Never write `propName = value` or `propName: Type = value` as a class-field initializer on a reactive prop. It compiles to `Object.defineProperty(this, …)` after `super()` and clobbers the framework's reactive accessor, silently breaking re-renders (`webjs check` flags this via `reactive-props-no-class-field`). Set defaults via the `default` option or in the constructor. Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`): the two share one JSON converter so neither crashes, but `Array` states the shape and `webjs check` flags the `Object` form via `array-prop-uses-array-type`.
686
+ - **Reactive props are declared via the base-class factory `WebComponent({ ... })`.** A hand-written `static properties = { ... }` throws at construction (`webjs check` flags it via `no-static-properties`). Never write `propName = value` or `propName: Type = value` as a class-field initializer on a reactive prop. It compiles to `Object.defineProperty(this, …)` after `super()` and clobbers the framework's reactive accessor, silently breaking re-renders (`webjs check` flags this via `reactive-props-no-class-field`). Set defaults in the constructor. Declare an array-typed prop with the `Array` constructor, not `Object` (`items: prop<Tag[]>(Array)`): the two share one JSON converter so neither crashes, but `Array` states the shape and `webjs check` flags the `Object` form via `array-prop-uses-array-type`.
687
687
  - Component state lives in signals. Import `signal` from `@webjsdev/core`, read via `signal.get()` inside `render()`, write via `signal.set(value)`. Module-scope signals share state across components; instance signals (created in the constructor) carry component-local state. Reactive properties (declared via the factory) wrap HTML attributes, attribute reflection, and `.prop=${value}` SSR hydration.
688
688
  - Use lifecycle hooks (`firstUpdated`, `updated`) only when needed
689
689
 
@@ -21,8 +21,8 @@
21
21
  # framework AGENTS.md "Secure response headers" section.
22
22
  FROM node:24-alpine
23
23
 
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.
24
+ # ca-certificates for outbound TLS (e.g. a managed Postgres). SQLite uses the
25
+ # built-in node:sqlite (no native module, no build toolchain needed).
26
26
  RUN apk add --no-cache ca-certificates
27
27
 
28
28
  WORKDIR /app