@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 +1 -1
- package/bin/webjs.js +10 -5
- package/lib/create.js +152 -39
- 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 +16 -7
- package/templates/CONVENTIONS.md +3 -3
- package/templates/Dockerfile +2 -2
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
322
|
-
//
|
|
323
|
-
//
|
|
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
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
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
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
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.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
654
|
-
// Bun,
|
|
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 {
|
|
670
|
-
const { drizzle } = await import('drizzle-orm/
|
|
671
|
-
return drizzle({ client: new
|
|
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
|
-
|
|
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
|
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,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**.
|
|
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 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
|
-
|
|
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 (`
|
|
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
|
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
|