@webjsdev/cli 0.10.23 → 0.10.24

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/lib/create.js CHANGED
@@ -273,6 +273,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
273
273
  throw new Error(`Unknown --runtime '${runtime}'. Only ${VALID_RUNTIMES.join(' / ')} are supported.`);
274
274
  }
275
275
  const isBun = runtime === 'bun';
276
+ // Zero-install Bun entry (#675): the app-local `webjs-bun.mjs` bootstrap, run
277
+ // under `bun --bun`, so the server resolves the CLI + deps via Bun auto-install
278
+ // (no `bun install` required). App-local so it is resolvable with no node_modules.
279
+ const bunBoot = 'bun --bun webjs-bun.mjs';
276
280
  const appDir = join(cwd, name);
277
281
  if (existsSync(appDir)) {
278
282
  console.error(`Error: directory '${name}' already exists.`);
@@ -318,18 +322,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
318
322
  // so `npm run start` (a thin alias) behaves identically. Drizzle has no
319
323
  // codegen, so there is no dev `before` step.
320
324
  //
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`
325
+ // Bun runtime (#541, zero-install #675): the server + DB scripts run via
326
+ // the `webjs-bun.mjs` bootstrap under `bun --bun`. `--bun` overrides the
327
+ // `webjs` bin's `#!/usr/bin/env node` shebang (without it `bun run dev`
324
328
  // 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
329
+ // Routing through the bootstrap file (which imports the CLI by bare
330
+ // specifier) instead of the `webjs` bin means Bun's auto-install resolves
331
+ // `@webjsdev/*` and your deps ON DEMAND, so `bun run dev` / `start` work
332
+ // with NO `bun install` (install becomes optional, for editor types /
333
+ // offline). The runtime-neutral tooling scripts (test / check / typecheck
328
334
  // / 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',
335
+ // tsc), which needs an install, so they are not part of the zero-install path.
336
+ dev: isBun ? `${bunBoot} dev` : 'webjs dev',
337
+ start: isBun ? `${bunBoot} start` : 'webjs start',
333
338
  test: 'webjs test',
334
339
  'test:server': 'webjs test --server',
335
340
  'test:browser': 'webjs test --browser',
@@ -340,17 +345,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
340
345
  // vendor pins, @webjsdev versions, git hook). Local tool, NOT a CI gate
341
346
  // (its env-drift + network pin-freshness checks would make CI flaky).
342
347
  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',
348
+ 'db:generate': isBun ? `${bunBoot} db generate` : 'webjs db generate',
349
+ 'db:migrate': isBun ? `${bunBoot} db migrate` : 'webjs db migrate',
350
+ 'db:push': isBun ? `${bunBoot} db push` : 'webjs db push',
351
+ 'db:studio': isBun ? `${bunBoot} db studio` : 'webjs db studio',
352
+ 'db:seed': isBun ? `${bunBoot} db seed` : 'webjs db seed',
348
353
  },
349
354
  dependencies: {
350
355
  // 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.
356
+ // for relations v2. SQLite needs NO driver dependency: the connection
357
+ // uses the built-in node:sqlite (Node) / bun:sqlite (Bun) via Drizzle's
358
+ // node-sqlite / bun-sqlite adapters. Postgres still needs the pg driver.
352
359
  'drizzle-orm': '^1.0.0-rc.3',
353
- ...(dialect === 'postgres' ? { pg: '^8.13.0' } : { 'better-sqlite3': '^12.11.1' }),
360
+ ...(dialect === 'postgres' ? { pg: '^8.13.0' } : {}),
354
361
  '@webjsdev/cli': 'latest',
355
362
  '@webjsdev/core': 'latest',
356
363
  '@webjsdev/server': 'latest',
@@ -394,18 +401,28 @@ export async function scaffoldApp(name, cwd, opts = {}) {
394
401
  webjs: {
395
402
  // Drizzle has no codegen, so there is no dev `before` step. Production
396
403
  // applies pending migrations at boot via `webjs db migrate` (drizzle-kit).
397
- start: { before: ['webjs db migrate'] },
404
+ // On Bun this runs through the same zero-install bootstrap as `start`, so
405
+ // the boot-time migrate needs no `webjs` bin in node_modules (#675).
406
+ start: { before: [isBun ? `${bunBoot} db migrate` : 'webjs db migrate'] },
398
407
  },
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
408
  }, null, 2) + '\n');
408
409
 
410
+ // The zero-install Bun entry (#675). `bun run dev` / `start` invoke this via
411
+ // `bun --bun` (see the scripts above). Importing the webjs CLI by bare
412
+ // specifier lets Bun auto-install resolve `@webjsdev/*` and your deps on
413
+ // demand, so a fresh app serves with NO `bun install`. The CLI reads its
414
+ // command (dev / start / db ...) and flags straight from argv. Node apps do
415
+ // not get this file; they run the `webjs` bin directly.
416
+ if (isBun) {
417
+ await writeFile(join(appDir, 'webjs-bun.mjs'),
418
+ '// Zero-install Bun entry (webjs #675). Run via `bun --bun webjs-bun.mjs <cmd>`\n' +
419
+ '// (the dev / start / db npm scripts do this). Importing the CLI by bare\n' +
420
+ '// specifier lets Bun auto-install resolve @webjsdev/* and your deps on\n' +
421
+ '// demand, so the app serves with no `bun install` (install stays optional,\n' +
422
+ '// for editor types and offline runs). Args pass through to the CLI.\n' +
423
+ "await import('@webjsdev/cli/bin/webjs.js');\n");
424
+ }
425
+
409
426
  await writeFile(join(appDir, 'tsconfig.json'), JSON.stringify({
410
427
  compilerOptions: {
411
428
  target: 'ES2022',
@@ -650,8 +667,9 @@ export type User = typeof users.$inferSelect;
650
667
  import { fileURLToPath } from 'node:url';
651
668
  import * as schema from './schema.server.ts';
652
669
 
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.
670
+ // The only file that opens the driver. Runtime-neutral and ZERO native deps:
671
+ // built-in bun:sqlite on Bun, built-in node:sqlite on Node. Cached on
672
+ // globalThis across dev reloads.
655
673
  // A relative SQLite path resolves against the app root (the parent of db/), not
656
674
  // process.cwd(), so the connection works under \`webjs dev\` AND when the app is
657
675
  // embedded via createRequestHandler from a different working directory.
@@ -660,15 +678,25 @@ const raw = process.env.DATABASE_URL?.replace(/^file:/, '') ?? 'db/dev.db';
660
678
  const url = raw === ':memory:' || isAbsolute(raw) ? raw : resolve(appRoot, raw);
661
679
  const g = globalThis as unknown as { __webjs_db?: unknown };
662
680
 
681
+ // Both node:sqlite and bun:sqlite default \`busy_timeout\` to 0, so a concurrent
682
+ // writer throws \`database is locked\` immediately. Restore a 5s wait (the old
683
+ // better-sqlite3 default) so contended access waits, and WAL so readers proceed
684
+ // alongside one writer.
685
+ function tune<T extends { exec(sql: string): unknown }>(client: T): T {
686
+ client.exec('PRAGMA busy_timeout = 5000');
687
+ client.exec('PRAGMA journal_mode = WAL');
688
+ return client;
689
+ }
690
+
663
691
  async function open() {
664
692
  if ((globalThis as { Bun?: unknown }).Bun) {
665
693
  const { Database } = await import('bun:sqlite');
666
694
  const { drizzle } = await import('drizzle-orm/bun-sqlite');
667
- return drizzle({ client: new Database(url), relations: schema.relations });
695
+ return drizzle({ client: tune(new Database(url)), relations: schema.relations });
668
696
  }
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 });
697
+ const { DatabaseSync } = await import('node:sqlite');
698
+ const { drizzle } = await import('drizzle-orm/node-sqlite');
699
+ return drizzle({ client: tune(new DatabaseSync(url)), relations: schema.relations });
672
700
  }
673
701
 
674
702
  export const db = (g.__webjs_db ??= await open()) as Awaited<ReturnType<typeof open>>;
@@ -701,6 +729,12 @@ export default defineConfig({
701
729
  `
702
730
  : `import { defineConfig } from 'drizzle-kit';
703
731
 
732
+ // No 'driver' is set: drizzle-kit auto-selects the SQLite driver for
733
+ // migrate/push/studio from the runtime, picking the built-in node:sqlite on
734
+ // Node and bun:sqlite on Bun (it only reaches for better-sqlite3 when that
735
+ // package is present, which this app does not install). That auto-selection is
736
+ // what keeps \`webjs db migrate\` free of a native driver, matching the runtime
737
+ // connection in db/connection.server.ts.
704
738
  export default defineConfig({
705
739
  dialect: 'sqlite',
706
740
  schema: './db/schema.server.ts',
@@ -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.24",
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,20 @@ 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 install` is optional here, run it for editor type intelligence or a pinned
418
+ offline install. (To run a Node-flavored app on Bun instead, force `bun --bun run
419
+ dev`, which still expects an install.)
420
+
416
421
  On Node the `.ts` type-stripping is the built-in `module.stripTypeScriptTypes`;
417
422
  on Bun (which has no built-in) it comes from `amaro` automatically, so the same
418
423
  source serves identically. SSR action-result seeding (an internal hydration
@@ -632,7 +637,7 @@ the click handler is inert). Two consequences for how you write code:
632
637
  reactivity) and never in `connectedCallback` (which the server
633
638
  doesn't run). For reactive properties declared via the
634
639
  `WebComponent({ ... })` factory, set the default in the constructor
635
- or pass the `default` option (e.g. `prop(Number, { default: 0 })`).
640
+ after `super()`.
636
641
  2. **`connectedCallback` is browser-only.** Use it for
637
642
  `localStorage`, viewport size, online status, or anything that
638
643
  genuinely can't be known on the server. Read the value, then
@@ -1153,7 +1158,7 @@ composition, so a nested shell ends up dropped by the HTML parser.
1153
1158
  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
1159
  2. **Server-only code goes in `.server.{js,ts}` files, `route.ts`
1155
1160
  handlers, or `middleware.ts`. Never in pages, layouts, or
1156
- components.** Direct imports of a DB driver (`better-sqlite3` / `pg`),
1161
+ components.** Direct imports of a DB driver (`pg`),
1157
1162
  `node:*`, or any server-only dependency from a page, layout, loading.ts,
1158
1163
  error.ts, not-found.ts, or component will crash the browser at module load.
1159
1164
  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