@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 +66 -32
- package/lib/runtime-rewrite.js +8 -12
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +1 -1
- package/templates/.cursorrules +1 -1
- package/templates/.github/copilot-instructions.md +2 -2
- package/templates/AGENTS.md +12 -7
- package/templates/CONVENTIONS.md +3 -3
- package/templates/Dockerfile +2 -2
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
|
|
322
|
-
//
|
|
323
|
-
//
|
|
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
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
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
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
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.
|
|
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' } : {
|
|
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
|
-
|
|
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
|
|
654
|
-
// Bun,
|
|
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 {
|
|
670
|
-
const { drizzle } = await import('drizzle-orm/
|
|
671
|
-
return drizzle({ client: new
|
|
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',
|
package/lib/runtime-rewrite.js
CHANGED
|
@@ -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
|
|
62
|
-
//
|
|
63
|
-
//
|
|
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
|
|
108
|
-
*
|
|
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\)\.
|
|
130
|
-
'# The Debian-based oven/bun image ships ca-certificates for outbound TLS (e.g. a\n# managed Postgres).
|
|
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.
|
|
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
|
@@ -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 `
|
|
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
|
package/templates/.cursorrules
CHANGED
|
@@ -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 `
|
|
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
|
|
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
|
|
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'
|
package/templates/AGENTS.md
CHANGED
|
@@ -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**.
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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 (`
|
|
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
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -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 (`
|
|
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
|
|
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
|
|
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
|
|
package/templates/Dockerfile
CHANGED
|
@@ -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).
|
|
25
|
-
#
|
|
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
|