@webjsdev/cli 0.10.17 → 0.10.19
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 +3 -3
- package/bin/webjs.js +90 -22
- package/lib/app-tasks.js +62 -0
- package/lib/create.js +234 -82
- package/lib/run-tasks.js +100 -0
- package/lib/saas-template.js +65 -73
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +5 -5
- package/templates/.env.example +2 -2
- package/templates/.github/copilot-instructions.md +4 -4
- package/templates/.github/workflows/ci.yml +4 -7
- package/templates/AGENTS.md +88 -72
- package/templates/CONVENTIONS.md +52 -41
- package/templates/Dockerfile +10 -7
- package/templates/compose.yaml +3 -3
- package/templates/test/hello/hello.test.ts +1 -1
- package/lib/prisma-preflight.js +0 -168
package/templates/AGENTS.md
CHANGED
|
@@ -11,7 +11,7 @@ companion and reach for docs.webjs.com whenever you need more detail.
|
|
|
11
11
|
|
|
12
12
|
This project was created with `webjs create`. The files you see right
|
|
13
13
|
now (`app/page.ts` printing "Hello from {{APP_NAME}}", the example `User`
|
|
14
|
-
model in `
|
|
14
|
+
model in `db/schema.server.ts`, the `theme-toggle` component, the
|
|
15
15
|
example users module in api/saas templates) are **starting-point
|
|
16
16
|
references, not the final product**. Your job is to replace them with
|
|
17
17
|
the app the user actually asked for. That includes adapting
|
|
@@ -30,11 +30,11 @@ user asked for, never leftover scaffold code.
|
|
|
30
30
|
|
|
31
31
|
**Non-negotiables for every webjs app:**
|
|
32
32
|
|
|
33
|
-
1. **Use
|
|
34
|
-
(`
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
1. **Use Drizzle + SQLite for persistence.** It's already wired up
|
|
34
|
+
(`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate`
|
|
35
|
+
+ `npm run db:migrate`). For any data the app stores (todos, posts,
|
|
36
|
+
messages, products, comments, anything), define a Drizzle table and
|
|
37
|
+
persist there.
|
|
38
38
|
- **NEVER** store app data in JSON files (`data/todos.json`,
|
|
39
39
|
`db.json`, …). It resets on reload and cannot scale. This is a project convention,
|
|
40
40
|
and the user's prompt explicitly forbids it.
|
|
@@ -47,10 +47,10 @@ user asked for, never leftover scaffold code.
|
|
|
47
47
|
`full-stack` (default), `--template api`, `--template saas`. Don't
|
|
48
48
|
reach for a `--template blog` / `--template todo` / `--template
|
|
49
49
|
ecommerce`. They don't exist and the CLI will reject them.
|
|
50
|
-
3. **First step after scaffolding:** edit `
|
|
50
|
+
3. **First step after scaffolding:** edit `db/schema.server.ts` to the
|
|
51
51
|
app's real domain models (delete the example `User` model unless the
|
|
52
|
-
app actually needs users), run `webjs db
|
|
53
|
-
pages / actions / queries against
|
|
52
|
+
app actually needs users), run `webjs db generate` then
|
|
53
|
+
`webjs db migrate`, then build pages / actions / queries against them.
|
|
54
54
|
|
|
55
55
|
**Picking the right scaffold from the user's prompt** (you do this BEFORE
|
|
56
56
|
running `webjs create`; if you're reading this you've already scaffolded.
|
|
@@ -202,10 +202,10 @@ Pure functions that return Tailwind class strings. You apply them to
|
|
|
202
202
|
import {
|
|
203
203
|
cardClass, cardHeaderClass, cardTitleClass,
|
|
204
204
|
cardContentClass, cardFooterClass,
|
|
205
|
-
} from '
|
|
206
|
-
import { inputClass } from '
|
|
207
|
-
import { labelClass } from '
|
|
208
|
-
import { buttonClass } from '
|
|
205
|
+
} from '#components/ui/card.ts';
|
|
206
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
207
|
+
import { labelClass } from '#components/ui/label.ts';
|
|
208
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
209
209
|
|
|
210
210
|
return html`
|
|
211
211
|
<div class=${cardClass()}>
|
|
@@ -237,13 +237,13 @@ elements. Import them once (typically in `app/layout.ts`) and use
|
|
|
237
237
|
|
|
238
238
|
```ts
|
|
239
239
|
// app/layout.ts (registers the custom elements for every page)
|
|
240
|
-
import '
|
|
241
|
-
import '
|
|
240
|
+
import '#components/ui/dialog.ts';
|
|
241
|
+
import '#components/ui/tabs.ts';
|
|
242
242
|
```
|
|
243
243
|
|
|
244
244
|
```ts
|
|
245
245
|
// app/some-page/page.ts (uses the registered elements)
|
|
246
|
-
import { buttonClass } from '
|
|
246
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
247
247
|
|
|
248
248
|
return html`
|
|
249
249
|
<ui-dialog>
|
|
@@ -322,12 +322,15 @@ modules/<feature>/
|
|
|
322
322
|
utils/*.ts feature-scoped helpers
|
|
323
323
|
types.ts feature types
|
|
324
324
|
lib/
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
325
|
+
... cross-cutting infra (session, auth config, etc.)
|
|
326
|
+
db/
|
|
327
|
+
schema.server.ts Drizzle models + relations (your data layer)
|
|
328
|
+
columns.server.ts column helpers (dialect-specific; the only file to swap for Postgres)
|
|
329
|
+
connection.server.ts opens the driver, exports the \`db\` singleton (import \`db\` from here)
|
|
330
|
+
seed.server.ts optional seed (run via \`webjs db seed\`)
|
|
331
|
+
dev.db SQLite file (gitignored); run \`npm run db:migrate\` to create
|
|
332
|
+
migrations/ generated migration SQL (committed)
|
|
333
|
+
drizzle.config.ts drizzle-kit config (root; SQLite by default, --db postgres to switch)
|
|
331
334
|
public/ static assets, served at /public/*
|
|
332
335
|
test/<feature>/ feature-scoped tests, one folder per concern
|
|
333
336
|
<name>.test.ts node unit / integration test (node --test)
|
|
@@ -359,39 +362,44 @@ Run `webjs types` once (and ensure `tsconfig.json` `include` lists
|
|
|
359
362
|
startup, so it stays current. Without it, `params` is `Record<string, string>`
|
|
360
363
|
and `navigate()` accepts any string (non-breaking).
|
|
361
364
|
|
|
362
|
-
## Database (
|
|
365
|
+
## Database (Drizzle + SQLite by default)
|
|
363
366
|
|
|
364
|
-
Every scaffold includes a
|
|
367
|
+
Every scaffold includes a Drizzle setup pointed at a local SQLite file,
|
|
368
|
+
under a `db/` folder (`schema.server.ts`, `columns.server.ts`,
|
|
369
|
+
`connection.server.ts`). Drizzle has no codegen and no engine binary.
|
|
365
370
|
First-run workflow:
|
|
366
371
|
|
|
367
372
|
```sh
|
|
368
373
|
cp .env.example .env # DATABASE_URL is pre-filled for SQLite
|
|
369
|
-
npm run db:
|
|
370
|
-
npm run
|
|
374
|
+
npm run db:generate # schema -> SQL migration (drizzle-kit)
|
|
375
|
+
npm run db:migrate # apply it (creates db/dev.db)
|
|
376
|
+
npm run dev # webjs dev, then serves
|
|
371
377
|
```
|
|
372
378
|
|
|
373
|
-
###
|
|
379
|
+
### `npm run dev` / `npm start` and `webjs dev` / `webjs start` behave identically
|
|
374
380
|
|
|
375
|
-
`
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
381
|
+
`npm run dev` and `npm start` are the documented entrypoints, and they
|
|
382
|
+
are thin aliases for `webjs dev` / `webjs start`. The start orchestration
|
|
383
|
+
(applying migrations, and any parallel watcher like the Tailwind CLI)
|
|
384
|
+
lives in the `webjs` block of `package.json` and runs INSIDE
|
|
385
|
+
`webjs dev` / `webjs start`:
|
|
379
386
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
an unmigrated database in production, etc.
|
|
387
|
+
```jsonc
|
|
388
|
+
"webjs": {
|
|
389
|
+
"start": { "before": ["webjs db migrate"] }
|
|
390
|
+
}
|
|
391
|
+
```
|
|
386
392
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
393
|
+
Drizzle has no codegen, so there is no dev `before` step. An app that
|
|
394
|
+
adds the Tailwind CLI puts its `--watch` command under
|
|
395
|
+
`webjs.dev.parallel` and it runs alongside the server, torn down on exit.
|
|
396
|
+
`before` steps run to completion first; a failed `webjs db migrate`
|
|
397
|
+
aborts the boot with a clear message rather than serving a stale schema.
|
|
391
398
|
|
|
392
|
-
In Docker / Railway,
|
|
393
|
-
|
|
394
|
-
|
|
399
|
+
In Docker / Railway, `CMD ["npm", "start"]` and `CMD ["webjs", "start"]`
|
|
400
|
+
are equivalent: `webjs start` runs `webjs.start.before` (`webjs db
|
|
401
|
+
migrate`) in-process before serving, so the migrate no longer depends on
|
|
402
|
+
an npm `prestart` hook.
|
|
395
403
|
|
|
396
404
|
### Running on Bun instead of Node
|
|
397
405
|
|
|
@@ -407,14 +415,16 @@ bun --bun run dev # or: bun --bun run start
|
|
|
407
415
|
On Node the `.ts` type-stripping is the built-in `module.stripTypeScriptTypes`;
|
|
408
416
|
on Bun (which has no built-in) it comes from `amaro` automatically, so the same
|
|
409
417
|
source serves identically. SSR action-result seeding (an internal hydration
|
|
410
|
-
optimization)
|
|
411
|
-
an async-render component re-
|
|
418
|
+
optimization) works on both runtimes: Node installs it via `module.registerHooks`,
|
|
419
|
+
Bun via a `Bun.plugin` `onLoad`, so an async-render component does not re-fetch
|
|
420
|
+
on hydration on either runtime.
|
|
412
421
|
|
|
413
422
|
**Containerized deploy ships with the scaffold.** `Dockerfile`,
|
|
414
423
|
`compose.yaml`, and `.dockerignore` are scaffolded at the app root. The
|
|
415
424
|
Dockerfile pins `node:24-alpine` (the same Node major CI uses), installs
|
|
416
|
-
deps,
|
|
417
|
-
|
|
425
|
+
deps (no build step, since Drizzle has no codegen), and starts via
|
|
426
|
+
`npm start` (`webjs start` runs `webjs.start.before` = `webjs db migrate`
|
|
427
|
+
before serving). Run it locally with `docker compose up --build` (the
|
|
418
428
|
app comes up on http://localhost:8080 against a SQLite file on a named
|
|
419
429
|
volume). For production, point `DATABASE_URL` at managed Postgres and set
|
|
420
430
|
`AUTH_SECRET`. The `.dockerignore` keeps the `.webjs/vendor/` importmap in
|
|
@@ -439,24 +449,28 @@ live DB ping), add an optional `readiness.{js,ts}` at the app root that
|
|
|
439
449
|
default-exports an async check; `/__webjs/ready` runs it once warm and reports
|
|
440
450
|
503 if it returns `false` or throws.
|
|
441
451
|
|
|
442
|
-
Scripts:
|
|
452
|
+
Scripts (all wrap `drizzle-kit`):
|
|
443
453
|
|
|
444
|
-
- `npm run db:
|
|
445
|
-
- `npm run db:
|
|
446
|
-
- `npm run db:
|
|
447
|
-
- `
|
|
448
|
-
- `
|
|
454
|
+
- `npm run db:generate`: `webjs db generate` (schema -> SQL migration)
|
|
455
|
+
- `npm run db:migrate`: `webjs db migrate` (apply pending migrations)
|
|
456
|
+
- `npm run db:push`: `webjs db push` (push the schema straight to the dev DB)
|
|
457
|
+
- `npm run db:studio`: `webjs db studio` (visual DB browser)
|
|
458
|
+
- `npm run db:seed`: `webjs db seed` (run `db/seed.server.ts`)
|
|
459
|
+
- `webjs.start.before` runs `webjs db migrate` inside `webjs start` (idempotent; replaces the old `prestart` hook). No dev `before` step (no codegen).
|
|
449
460
|
|
|
450
|
-
Always import
|
|
451
|
-
|
|
461
|
+
Always import `db` from `db/connection.server.ts` (the globalThis-cached
|
|
462
|
+
singleton avoids opening a new connection on every dev-server reload), and
|
|
463
|
+
the tables from `db/schema.server.ts`:
|
|
452
464
|
|
|
453
465
|
```ts
|
|
454
|
-
import {
|
|
455
|
-
const users = await
|
|
466
|
+
import { db } from '#db/connection.server.ts';
|
|
467
|
+
const users = await db.query.users.findMany();
|
|
456
468
|
```
|
|
457
469
|
|
|
458
|
-
To switch to Postgres
|
|
459
|
-
|
|
470
|
+
To switch to Postgres: scaffold with `--db postgres`, or swap
|
|
471
|
+
`db/columns.server.ts` + `db/connection.server.ts` for the Postgres
|
|
472
|
+
variants and point `DATABASE_URL` at Postgres. The schema, queries, and
|
|
473
|
+
actions are unchanged.
|
|
460
474
|
|
|
461
475
|
## NPM packages (vendor pipeline)
|
|
462
476
|
|
|
@@ -521,9 +535,9 @@ committed manifest, optional `--download` for full offline capability,
|
|
|
521
535
|
and a `--from` knob to swap the resolver CDN if jspm.io has an
|
|
522
536
|
incident.
|
|
523
537
|
|
|
524
|
-
**Don't auto-run `webjs vendor pin` in `
|
|
525
|
-
would silently churn the committed importmap.json as jspm.io
|
|
526
|
-
URLs or transitive deps drift. Pin is a deliberate developer action,
|
|
538
|
+
**Don't auto-run `webjs vendor pin` in a `webjs.dev.before` / `webjs.start.before`
|
|
539
|
+
step.** Auto-pin would silently churn the committed importmap.json as jspm.io
|
|
540
|
+
resolves URLs or transitive deps drift. Pin is a deliberate developer action,
|
|
527
541
|
like `npm install` itself.
|
|
528
542
|
|
|
529
543
|
**Do NOT modify the `.webjs/` lines in `.gitignore` / `.dockerignore`.**
|
|
@@ -740,11 +754,12 @@ legitimately use `static styles = css\`\`` for scoped CSS.
|
|
|
740
754
|
```ts
|
|
741
755
|
// modules/posts/actions/create-post.server.ts
|
|
742
756
|
'use server';
|
|
743
|
-
import {
|
|
757
|
+
import { db } from '#db/connection.server.ts';
|
|
758
|
+
import { posts } from '#db/schema.server.ts';
|
|
744
759
|
|
|
745
760
|
export async function createPost(input: { title: string; body: string }) {
|
|
746
761
|
if (!input.title) return { success: false, error: 'title required', status: 400 };
|
|
747
|
-
const post = await
|
|
762
|
+
const [post] = await db.insert(posts).values(input).returning();
|
|
748
763
|
return { success: true, data: post };
|
|
749
764
|
}
|
|
750
765
|
```
|
|
@@ -791,7 +806,7 @@ with a `route.ts` POST handler:
|
|
|
791
806
|
```ts
|
|
792
807
|
// app/posts/route.ts
|
|
793
808
|
import { redirect, html } from '@webjsdev/core';
|
|
794
|
-
import { createPost } from '
|
|
809
|
+
import { createPost } from '#modules/posts/actions/create-post.server.ts';
|
|
795
810
|
|
|
796
811
|
export async function POST(req: Request) {
|
|
797
812
|
const form = await req.formData();
|
|
@@ -1117,13 +1132,14 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
1117
1132
|
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.
|
|
1118
1133
|
2. **Server-only code goes in `.server.{js,ts}` files, `route.ts`
|
|
1119
1134
|
handlers, or `middleware.ts`. Never in pages, layouts, or
|
|
1120
|
-
components.** Direct imports of
|
|
1121
|
-
server-only dependency from a page, layout, loading.ts,
|
|
1122
|
-
not-found.ts, or component will crash the browser at module load.
|
|
1135
|
+
components.** Direct imports of a DB driver (`better-sqlite3` / `pg`),
|
|
1136
|
+
`node:*`, or any server-only dependency from a page, layout, loading.ts,
|
|
1137
|
+
error.ts, not-found.ts, or component will crash the browser at module load.
|
|
1123
1138
|
Wrap the access in a `.server.{js,ts}` file; the framework
|
|
1124
|
-
rewrites that import into an RPC stub for the browser.
|
|
1125
|
-
|
|
1126
|
-
|
|
1139
|
+
rewrites that import into an RPC stub for the browser. Server-only
|
|
1140
|
+
infra lives in `db/*.server.ts` (the DB) and `lib/*.server.ts`
|
|
1141
|
+
(`lib/session.server.ts`); browser-safe utilities live in
|
|
1142
|
+
`lib/utils/cn.ts` with `cn`, design-
|
|
1127
1143
|
system helpers). Server-only `lib/*` files must only be imported
|
|
1128
1144
|
from `.server.ts`/`route.ts`/`middleware.ts`; browser-safe `lib/*`
|
|
1129
1145
|
files (like `lib/utils/cn.ts`) can be imported anywhere.
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -32,18 +32,18 @@ enforced by `webjs check`; follow them by judgment.
|
|
|
32
32
|
|
|
33
33
|
- **Server actions and queries live in `modules/<feature>/actions/` and
|
|
34
34
|
`modules/<feature>/queries/`** (`*.server.{js,ts}`), not loose in the
|
|
35
|
-
app root.
|
|
36
|
-
session helpers, auth config) lives in `lib/`.
|
|
35
|
+
app root. The DB connection lives in `db/connection.server.ts`; other
|
|
36
|
+
cross-cutting server infrastructure (session helpers, auth config) lives in `lib/`.
|
|
37
37
|
- **One exported function per action/query file.** Name the file after
|
|
38
38
|
the function (`create-post.server.ts` exports `createPost`). It keeps
|
|
39
39
|
the action surface greppable.
|
|
40
40
|
- **Every feature has tests.** A `modules/<feature>/` directory should
|
|
41
41
|
have matching test files under `test/<feature>/`. A unit test for
|
|
42
42
|
logic, a browser/e2e test for user-facing behaviour.
|
|
43
|
-
- **Persist data with
|
|
44
|
-
wires up `
|
|
43
|
+
- **Persist data with Drizzle + SQLite, never JSON files.** The scaffold
|
|
44
|
+
wires up `db/schema.server.ts` and `db/connection.server.ts`. A
|
|
45
45
|
`data/todos.json` or `db.json` used as a database resets on reload and
|
|
46
|
-
cannot scale; define a
|
|
46
|
+
cannot scale; define a Drizzle table instead.
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
@@ -254,40 +254,45 @@ docs". That is the agent's default behavior in a webjs project.
|
|
|
254
254
|
|
|
255
255
|
---
|
|
256
256
|
|
|
257
|
-
## Data persistence:
|
|
257
|
+
## Data persistence: Drizzle + SQLite, never JSON files
|
|
258
258
|
|
|
259
259
|
<!-- OVERRIDE -->
|
|
260
260
|
|
|
261
|
-
Every webjs app uses **
|
|
262
|
-
scaffold ships `
|
|
263
|
-
`
|
|
264
|
-
|
|
261
|
+
Every webjs app uses **Drizzle + SQLite** for persistence by default. The
|
|
262
|
+
scaffold ships the `db/` folder (`schema.server.ts`, `columns.server.ts`,
|
|
263
|
+
`connection.server.ts`), the `webjs.start.before` step that runs
|
|
264
|
+
`webjs db migrate` inside `webjs start` (#550), and the
|
|
265
|
+
`npm run db:generate` / `db:migrate` / `db:push` / `db:studio` / `db:seed`
|
|
266
|
+
scripts (which route through `webjs db` to drizzle-kit). Drizzle has no
|
|
267
|
+
codegen, so there is no dev `before` step.
|
|
265
268
|
|
|
266
269
|
**AI agents: these rules are absolute.**
|
|
267
270
|
|
|
268
271
|
1. For ANY data the app stores (todos, posts, messages, products,
|
|
269
|
-
comments, users…), define a
|
|
272
|
+
comments, users…), define a Drizzle table in `db/schema.server.ts`
|
|
270
273
|
and persist there.
|
|
271
274
|
2. **NEVER** create JSON files under `data/`, `db.json`, `posts.json`,
|
|
272
275
|
`todos.json`, etc. as a fake database. It resets on reload and cannot
|
|
273
276
|
scale; this is a project convention (see the conventions section above).
|
|
274
277
|
3. **NEVER** use module-scope arrays or `Map`s as a "store". They
|
|
275
278
|
reset on every dev-server reload and can't scale beyond one process.
|
|
276
|
-
4. **NEVER** use `localStorage` / `sessionStorage` to persist app data
|
|
279
|
+
4. **NEVER** use `localStorage` / `sessionStorage` to persist app data,
|
|
277
280
|
it's per-browser and never reaches the server. Use it only for UI
|
|
278
281
|
preferences (theme, sidebar collapsed, etc.).
|
|
279
|
-
5. To add a model: edit `
|
|
280
|
-
|
|
281
|
-
'
|
|
282
|
-
`
|
|
283
|
-
Components, pages, and
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
282
|
+
5. To add a model: edit `db/schema.server.ts`, then `npm run db:generate`
|
|
283
|
+
and `npm run db:migrate`. Access via `import { db } from
|
|
284
|
+
'#db/connection.server.ts'` (and the tables from
|
|
285
|
+
`db/schema.server.ts`) **only inside `.server.{js,ts}` files,
|
|
286
|
+
`route.ts` handlers, or `middleware.ts`**. Components, pages, and
|
|
287
|
+
layouts call into the wrapped server query instead; the framework
|
|
288
|
+
rewrites that import to an RPC stub on the browser side, so the DB
|
|
289
|
+
driver never reaches the client.
|
|
290
|
+
|
|
291
|
+
To switch to Postgres: scaffold with `--db postgres`, or swap
|
|
292
|
+
`db/columns.server.ts` + `db/connection.server.ts` for the Postgres
|
|
293
|
+
variants and point `DATABASE_URL` at Postgres. The schema, queries, and
|
|
294
|
+
actions are unchanged. SQLite is the right default for dev and small
|
|
295
|
+
production workloads.
|
|
291
296
|
|
|
292
297
|
---
|
|
293
298
|
|
|
@@ -302,9 +307,9 @@ saas templates) is a **starting point**.
|
|
|
302
307
|
|
|
303
308
|
When the user asks the agent to build their actual app:
|
|
304
309
|
|
|
305
|
-
1. **Replace the example `User` model** in `
|
|
306
|
-
the real domain models the app needs (e.g. `Todo`, `Post`, `Message`)
|
|
307
|
-
|
|
310
|
+
1. **Replace the example `User` model** in `db/schema.server.ts` with
|
|
311
|
+
the real domain models the app needs (e.g. `Todo`, `Post`, `Message`),
|
|
312
|
+
unless the app actually has users.
|
|
308
313
|
2. **Replace `app/page.ts`** with the app's real homepage. Don't ship
|
|
309
314
|
"Hello from …" as the deliverable.
|
|
310
315
|
3. **Delete or replace `components/theme-toggle.ts`** if the app doesn't
|
|
@@ -319,10 +324,11 @@ When the user asks the agent to build their actual app:
|
|
|
319
324
|
dashboard, or board, or a wide layout overflows into an unnecessary
|
|
320
325
|
horizontal scrollbar. Keep the design tokens and theme setup, those
|
|
321
326
|
are infrastructure.
|
|
322
|
-
6. **Keep:** the
|
|
327
|
+
6. **Keep:** the Drizzle setup, the test config, the agent config files
|
|
323
328
|
(`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
|
|
324
|
-
`
|
|
325
|
-
`app/layout.ts`. These are the
|
|
329
|
+
`db/connection.server.ts` + `db/columns.server.ts`, the directory
|
|
330
|
+
conventions, the design tokens in `app/layout.ts`. These are the
|
|
331
|
+
infrastructure, not the example app.
|
|
326
332
|
|
|
327
333
|
This is enforced, not just advised. The example `app/page.ts` and
|
|
328
334
|
`app/layout.ts` carry a `webjs-scaffold-placeholder` marker comment, and
|
|
@@ -334,7 +340,7 @@ line. So the delivered app contains only what the user asked for, never
|
|
|
334
340
|
leftover scaffold code.
|
|
335
341
|
|
|
336
342
|
The scaffold exists so the agent doesn't reinvent the directory layout,
|
|
337
|
-
the
|
|
343
|
+
the Drizzle wiring, the test runner config, or the convention files. It
|
|
338
344
|
does NOT exist so the agent ships the example homepage.
|
|
339
345
|
|
|
340
346
|
---
|
|
@@ -382,10 +388,11 @@ modules/
|
|
|
382
388
|
```
|
|
383
389
|
|
|
384
390
|
**Rules:**
|
|
391
|
+
- **Prefer the `#` root alias over deep relatives.** Write `import { db } from '#db/connection.server.ts'`, `import { Button } from '#components/ui/button.ts'`, `#lib/...`, `#modules/...` instead of `../../../`. It is native `package.json "imports"` (the single `"#*": "./*"` key covers every top-level folder, so a new folder needs no config), resolved by Node and Bun with no build step. There is no slash after the `#` (`#lib/...`, not `#/lib/...`). A same-directory import stays relative (`./sibling.ts`).
|
|
385
392
|
- One exported function per server action/query file
|
|
386
393
|
- 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`).
|
|
387
394
|
- Components must call `Class.register('tag')`
|
|
388
|
-
- **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** Direct imports of
|
|
395
|
+
- **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."
|
|
389
396
|
- Routes (`app/**/page.ts`, `app/**/route.ts`) must be thin: import logic from modules
|
|
390
397
|
- **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).
|
|
391
398
|
|
|
@@ -582,8 +589,8 @@ the module once (typically in `app/layout.ts`) and use the tag.
|
|
|
582
589
|
```ts
|
|
583
590
|
// Tier 1: class helpers on native elements (use this for forms,
|
|
584
591
|
// dashboards, cards, layouts, anywhere the value is purely visual)
|
|
585
|
-
import { buttonClass } from '
|
|
586
|
-
import { inputClass } from '
|
|
592
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
593
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
587
594
|
return html`
|
|
588
595
|
<button class=${buttonClass({ size: 'lg' })}>Save</button>
|
|
589
596
|
<input class=${inputClass()} placeholder="Email">
|
|
@@ -767,7 +774,7 @@ Consume:
|
|
|
767
774
|
|
|
768
775
|
```ts
|
|
769
776
|
// app/page.ts
|
|
770
|
-
import { rubric } from '
|
|
777
|
+
import { rubric } from '#lib/utils/ui.ts';
|
|
771
778
|
|
|
772
779
|
export default function Home() {
|
|
773
780
|
return html`
|
|
@@ -919,15 +926,18 @@ route imports and calls it.
|
|
|
919
926
|
```ts
|
|
920
927
|
// modules/posts/actions/create-post.server.ts
|
|
921
928
|
'use server';
|
|
929
|
+
import { db } from '#db/connection.server.ts';
|
|
930
|
+
import { posts } from '#db/schema.server.ts';
|
|
922
931
|
export async function createPost({ title, body }) {
|
|
923
|
-
|
|
932
|
+
const [post] = await db.insert(posts).values({ title, body }).returning();
|
|
933
|
+
return post;
|
|
924
934
|
}
|
|
925
935
|
```
|
|
926
936
|
|
|
927
937
|
```ts
|
|
928
938
|
// app/api/posts/route.ts
|
|
929
939
|
import { route } from '@webjsdev/server';
|
|
930
|
-
import { createPost } from '
|
|
940
|
+
import { createPost } from '#modules/posts/actions/create-post.server.ts';
|
|
931
941
|
// The route() adapter merges query + route params + JSON body into one input
|
|
932
942
|
// object and JSON-responds the result. Pass { validate } to guard the input.
|
|
933
943
|
export const POST = route(createPost);
|
|
@@ -1030,7 +1040,8 @@ Where the data lives, where to read it:
|
|
|
1030
1040
|
```ts
|
|
1031
1041
|
// modules/posts/actions/create-post.server.ts
|
|
1032
1042
|
'use server';
|
|
1033
|
-
import {
|
|
1043
|
+
import { db } from '#db/connection.server.ts';
|
|
1044
|
+
import { posts } from '#db/schema.server.ts';
|
|
1034
1045
|
import type { ActionResult } from '../types.ts';
|
|
1035
1046
|
|
|
1036
1047
|
export async function createPost(input: {
|
|
@@ -1148,7 +1159,7 @@ Create new projects with `webjs create`:
|
|
|
1148
1159
|
```sh
|
|
1149
1160
|
webjs create <name> # full-stack (default)
|
|
1150
1161
|
webjs create <name> --template api # backend-only API
|
|
1151
|
-
webjs create <name> --template saas # auth + dashboard +
|
|
1162
|
+
webjs create <name> --template saas # auth + dashboard + Drizzle User model
|
|
1152
1163
|
```
|
|
1153
1164
|
|
|
1154
1165
|
**Route-wrapping pattern (especially for `--template api` apps):**
|
|
@@ -1157,8 +1168,8 @@ Routes are thin wrappers over typed server actions. Business logic lives in
|
|
|
1157
1168
|
|
|
1158
1169
|
```ts
|
|
1159
1170
|
// app/api/users/route.ts: thin wrapper
|
|
1160
|
-
import { listUsers } from '
|
|
1161
|
-
import { createUser } from '
|
|
1171
|
+
import { listUsers } from '#modules/users/queries/list-users.server.ts';
|
|
1172
|
+
import { createUser } from '#modules/users/actions/create-user.server.ts';
|
|
1162
1173
|
|
|
1163
1174
|
export async function GET() { return Response.json(await listUsers()); }
|
|
1164
1175
|
export async function POST(req: Request) {
|
package/templates/Dockerfile
CHANGED
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
# framework AGENTS.md "Secure response headers" section.
|
|
22
22
|
FROM node:24-alpine
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
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.
|
|
26
|
+
RUN apk add --no-cache ca-certificates
|
|
26
27
|
|
|
27
28
|
WORKDIR /app
|
|
28
29
|
|
|
@@ -35,9 +36,9 @@ RUN npm install --no-audit --no-fund
|
|
|
35
36
|
# App source. node_modules and local state are excluded via .dockerignore.
|
|
36
37
|
COPY . .
|
|
37
38
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
39
|
+
# Drizzle has no client-codegen step, so there is nothing to build here. The
|
|
40
|
+
# database is migrated at boot via `webjs start` (the `webjs.start.before`
|
|
41
|
+
# step runs `webjs db migrate`). See the CMD note below.
|
|
41
42
|
|
|
42
43
|
ENV NODE_ENV=production
|
|
43
44
|
# webjs start reads $PORT (default 8080). compose / uncloud / Railway set it.
|
|
@@ -55,6 +56,8 @@ EXPOSE 8080
|
|
|
55
56
|
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=5 \
|
|
56
57
|
CMD ["node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||8080)+'/__webjs/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
57
58
|
|
|
58
|
-
# `npm start`
|
|
59
|
-
#
|
|
59
|
+
# `npm start` is a thin alias for `webjs start` (#550). `webjs start` runs the
|
|
60
|
+
# `webjs.start.before` step (`webjs db migrate`, idempotent / a no-op with no
|
|
61
|
+
# pending migrations) IN-PROCESS, then serves on $PORT. `CMD ["webjs", "start"]`
|
|
62
|
+
# is now equivalent: the migrate no longer depends on an npm `prestart` hook.
|
|
60
63
|
CMD ["npm", "start"]
|
package/templates/compose.yaml
CHANGED
|
@@ -12,9 +12,9 @@ services:
|
|
|
12
12
|
- "8080:8080"
|
|
13
13
|
environment:
|
|
14
14
|
PORT: 8080
|
|
15
|
-
# SQLite on a volume for local dev. For production,
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# SQLite on a volume for local dev. For production, scaffold with
|
|
16
|
+
# --db postgres (or swap db/columns.server.ts + db/connection.server.ts
|
|
17
|
+
# for the pg variant) and point DATABASE_URL at your managed Postgres.
|
|
18
18
|
DATABASE_URL: file:/data/dev.db
|
|
19
19
|
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
20
20
|
AUTH_SECRET: ${AUTH_SECRET:-change-me-please-at-least-32-characters!}
|
|
@@ -16,7 +16,7 @@ test('html template renders correctly', async () => {
|
|
|
16
16
|
|
|
17
17
|
test('example: your first server action test', async () => {
|
|
18
18
|
// Import your server action:
|
|
19
|
-
// import { createPost } from '
|
|
19
|
+
// import { createPost } from '#modules/posts/actions/create-post.server.ts';
|
|
20
20
|
//
|
|
21
21
|
// const result = await createPost({ title: 'Test', body: 'Content' });
|
|
22
22
|
// assert.equal(result.success, true);
|