@webjsdev/cli 0.10.17 → 0.10.18
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 +202 -62
- package/lib/run-tasks.js +100 -0
- package/lib/saas-template.js +39 -49
- 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 +80 -64
- package/templates/CONVENTIONS.md +45 -35
- package/templates/Dockerfile +10 -7
- package/templates/compose.yaml +3 -3
- package/lib/prisma-preflight.js +0 -168
package/lib/run-tasks.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a PATH the way `npm run` does: prepend every ANCESTOR
|
|
6
|
+
* `node_modules/.bin` (the app's, then up to the repo root for a hoisted
|
|
7
|
+
* monorepo) so a `before` / `parallel` command naming a LOCAL-only binary
|
|
8
|
+
* (`drizzle-kit`, `tailwindcss`) resolves under a bare `webjs dev` / `start`, exactly
|
|
9
|
+
* as it does under `npm run dev`. Without this a bare `webjs dev` exits 127 on
|
|
10
|
+
* the first such step and aborts the boot, defeating the whole #550 point.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} cwd
|
|
13
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
14
|
+
*/
|
|
15
|
+
function envWithLocalBin(cwd, env = process.env) {
|
|
16
|
+
const bins = [];
|
|
17
|
+
let dir = cwd;
|
|
18
|
+
// Walk up to the filesystem root, collecting each node_modules/.bin.
|
|
19
|
+
for (;;) {
|
|
20
|
+
bins.push(join(dir, 'node_modules', '.bin'));
|
|
21
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir) break;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
return { ...env, PATH: [...bins, env.PATH || ''].join(delimiter) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run the configured `before` steps (#550) sequentially to completion. Returns
|
|
30
|
+
* the FIRST failure so the caller can abort the boot, or `{ ok: true }`. Pure of
|
|
31
|
+
* `process.exit` and `console` (the bin owns the exit code + logging via the
|
|
32
|
+
* `onStep` hook) so the orchestration is deterministically unit-testable, with
|
|
33
|
+
* `spawn` injectable for tests.
|
|
34
|
+
*
|
|
35
|
+
* @param {string[]} steps
|
|
36
|
+
* @param {string} cwd
|
|
37
|
+
* @param {{ spawn?: typeof nodeSpawn, onStep?: (step: string) => void }} [opts]
|
|
38
|
+
* @returns {Promise<{ ok: true } | { ok: false, step: string, code: number }>}
|
|
39
|
+
*/
|
|
40
|
+
export async function runBeforeSteps(steps, cwd, opts = {}) {
|
|
41
|
+
const spawn = opts.spawn || nodeSpawn;
|
|
42
|
+
const env = envWithLocalBin(cwd);
|
|
43
|
+
for (const step of steps) {
|
|
44
|
+
if (opts.onStep) opts.onStep(step);
|
|
45
|
+
const code = await new Promise((res) => {
|
|
46
|
+
const c = spawn(step, { shell: true, stdio: 'inherit', cwd, env });
|
|
47
|
+
c.on('exit', (code) => res(code ?? 0));
|
|
48
|
+
c.on('error', () => res(1));
|
|
49
|
+
});
|
|
50
|
+
if (code !== 0) return { ok: false, step, code };
|
|
51
|
+
}
|
|
52
|
+
return { ok: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Spawn the configured dev `parallel` tasks (#550) as long-lived children and
|
|
57
|
+
* return a killer that tears them ALL down (idempotent), so a watcher cannot
|
|
58
|
+
* leak past the dev server. `spawn` is injectable for tests.
|
|
59
|
+
*
|
|
60
|
+
* @param {string[]} commands
|
|
61
|
+
* @param {string} cwd
|
|
62
|
+
* @param {{ spawn?: typeof nodeSpawn, onStart?: (cmd: string) => void }} [opts]
|
|
63
|
+
* @returns {() => void}
|
|
64
|
+
*/
|
|
65
|
+
export function startParallelTasks(commands, cwd, opts = {}) {
|
|
66
|
+
const spawn = opts.spawn || nodeSpawn;
|
|
67
|
+
const env = envWithLocalBin(cwd);
|
|
68
|
+
const children = commands.map((cmd) => {
|
|
69
|
+
if (opts.onStart) opts.onStart(cmd);
|
|
70
|
+
// `detached: true` puts the child in its OWN process group, so the killer
|
|
71
|
+
// can take down the whole tree (the `sh -c` wrapper AND the watcher it
|
|
72
|
+
// spawns, e.g. tailwindcss) rather than just the shell, which would leak the
|
|
73
|
+
// watcher as an orphan.
|
|
74
|
+
return spawn(cmd, { shell: true, stdio: 'inherit', cwd, env, detached: true });
|
|
75
|
+
});
|
|
76
|
+
let killed = false;
|
|
77
|
+
return () => {
|
|
78
|
+
if (killed) return;
|
|
79
|
+
killed = true;
|
|
80
|
+
for (const c of children) killChildTree(c);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Tear down a shell-spawned child's whole process GROUP. A `sh -c '<watcher>'`
|
|
86
|
+
* child run with `detached: true` is a group leader, so a NEGATIVE pid signals
|
|
87
|
+
* the group (the shell + the watcher). Falls back to a direct `kill()` when
|
|
88
|
+
* there is no numeric pid (a fake child in a test) or the group kill is
|
|
89
|
+
* unsupported (a non-POSIX runtime), so the killer never throws.
|
|
90
|
+
*
|
|
91
|
+
* @param {import('node:child_process').ChildProcess} child
|
|
92
|
+
*/
|
|
93
|
+
function killChildTree(child) {
|
|
94
|
+
try {
|
|
95
|
+
if (typeof child.pid === 'number') process.kill(-child.pid, 'SIGTERM');
|
|
96
|
+
else child.kill();
|
|
97
|
+
} catch {
|
|
98
|
+
try { child.kill(); } catch {}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/lib/saas-template.js
CHANGED
|
@@ -50,16 +50,10 @@ export async function writeSaasFiles(appDir) {
|
|
|
50
50
|
// the saas auth pages use raw <form> + label/input class helpers instead.
|
|
51
51
|
await copyUiComponents(appDir, ['dialog', 'switch', 'checkbox']);
|
|
52
52
|
|
|
53
|
-
//
|
|
53
|
+
// The db/ layer (columns/connection) is written by the full-stack scaffold
|
|
54
|
+
// already; this template overwrites db/schema.server.ts below to add the
|
|
55
|
+
// User.passwordHash column auth needs.
|
|
54
56
|
await mkdir(join(appDir, 'lib'), { recursive: true });
|
|
55
|
-
await writeFile(join(appDir, 'lib', 'prisma.server.ts'), [
|
|
56
|
-
"import { PrismaClient } from '@prisma/client';",
|
|
57
|
-
"",
|
|
58
|
-
"const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };",
|
|
59
|
-
"export const prisma = globalForPrisma.prisma || new PrismaClient();",
|
|
60
|
-
"if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;",
|
|
61
|
-
"",
|
|
62
|
-
].join('\n'));
|
|
63
57
|
|
|
64
58
|
// lib/password.server.ts
|
|
65
59
|
await writeFile(join(appDir, 'lib', 'password.server.ts'), [
|
|
@@ -85,14 +79,14 @@ export async function writeSaasFiles(appDir) {
|
|
|
85
79
|
// lib/auth.server.ts
|
|
86
80
|
await writeFile(join(appDir, 'lib', 'auth.server.ts'), [
|
|
87
81
|
"import { createAuth, Credentials } from '@webjsdev/server';",
|
|
88
|
-
"import {
|
|
82
|
+
"import { db } from '../db/connection.server.ts';",
|
|
89
83
|
"import { compare } from './password.server.ts';",
|
|
90
84
|
"",
|
|
91
85
|
"export const { auth, signIn, signOut, handlers } = createAuth({",
|
|
92
86
|
" providers: [",
|
|
93
87
|
" Credentials({",
|
|
94
88
|
" async authorize(credentials: { email: string; password: string }) {",
|
|
95
|
-
" const user = await
|
|
89
|
+
" const user = await db.query.users.findFirst({ where: { email: credentials.email } });",
|
|
96
90
|
" if (!user || !await compare(credentials.password, user.passwordHash)) return null;",
|
|
97
91
|
" return { id: String(user.id), name: user.name, email: user.email };",
|
|
98
92
|
" },",
|
|
@@ -103,26 +97,24 @@ export async function writeSaasFiles(appDir) {
|
|
|
103
97
|
"",
|
|
104
98
|
].join('\n'));
|
|
105
99
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
await writeFile(join(appDir, '
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
'}',
|
|
125
|
-
'',
|
|
100
|
+
// db/schema.server.ts: overwrite the full-stack scaffold's example User to
|
|
101
|
+
// add passwordHash (the column auth needs). Drizzle, dialect-agnostic.
|
|
102
|
+
await writeFile(join(appDir, 'db', 'schema.server.ts'), [
|
|
103
|
+
"import { defineRelations } from 'drizzle-orm';",
|
|
104
|
+
"import { table, pk, text, createdAt } from './columns.server.ts';",
|
|
105
|
+
"",
|
|
106
|
+
"export const users = table('users', {",
|
|
107
|
+
" id: pk(),",
|
|
108
|
+
" email: text().notNull().unique(),",
|
|
109
|
+
" name: text(),",
|
|
110
|
+
" passwordHash: text().notNull(),",
|
|
111
|
+
" createdAt: createdAt(),",
|
|
112
|
+
"});",
|
|
113
|
+
"",
|
|
114
|
+
"export const relations = defineRelations({ users }, () => ({}));",
|
|
115
|
+
"",
|
|
116
|
+
"export type User = typeof users.$inferSelect;",
|
|
117
|
+
"",
|
|
126
118
|
].join('\n'));
|
|
127
119
|
|
|
128
120
|
// modules/auth/actions/signup.server.ts
|
|
@@ -132,15 +124,14 @@ export async function writeSaasFiles(appDir) {
|
|
|
132
124
|
await writeFile(join(appDir, 'modules', 'auth', 'actions', 'signup.server.ts'), [
|
|
133
125
|
"'use server';",
|
|
134
126
|
"",
|
|
135
|
-
"import {
|
|
127
|
+
"import { db } from '../../../db/connection.server.ts';",
|
|
128
|
+
"import { users } from '../../../db/schema.server.ts';",
|
|
136
129
|
"import { hash } from '../../../lib/password.server.ts';",
|
|
137
130
|
"",
|
|
138
131
|
"export async function signup(input: { name: string; email: string; password: string }) {",
|
|
139
|
-
" const exists = await
|
|
132
|
+
" const exists = await db.query.users.findFirst({ where: { email: input.email }, columns: { id: true } });",
|
|
140
133
|
" if (exists) return { success: false as const, error: 'Email already registered', status: 409 };",
|
|
141
|
-
" const user = await
|
|
142
|
-
" data: { name: input.name, email: input.email, passwordHash: await hash(input.password) },",
|
|
143
|
-
" });",
|
|
134
|
+
" const [user] = await db.insert(users).values({ name: input.name, email: input.email, passwordHash: await hash(input.password) }).returning();",
|
|
144
135
|
" return { success: true as const, data: { id: user.id, name: user.name, email: user.email } };",
|
|
145
136
|
"}",
|
|
146
137
|
"",
|
|
@@ -183,11 +174,10 @@ export async function writeSaasFiles(appDir) {
|
|
|
183
174
|
// - The protected-route gate (unauthenticated /dashboard -> 302 /login) runs
|
|
184
175
|
// ALWAYS once the app modules import: auth() only reads a cookie, no DB
|
|
185
176
|
// query. This is the headline security assertion and it is REAL.
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
// message instead of crashing. After you set up the DB it runs for real.
|
|
177
|
+
// The signup, login, and protected-route flow writes + reads a user, so it
|
|
178
|
+
// needs the DB migrated (`npm run db:generate` then `npm run db:migrate`).
|
|
179
|
+
// Until the users table exists those flows error, so the suite skips with a
|
|
180
|
+
// clear message instead of crashing. After DB setup it runs for real.
|
|
191
181
|
await mkdir(join(appDir, 'test', 'auth'), { recursive: true });
|
|
192
182
|
await writeFile(join(appDir, 'test', 'auth', 'auth.test.ts'), [
|
|
193
183
|
"import { test } from 'node:test';",
|
|
@@ -200,20 +190,20 @@ export async function writeSaasFiles(appDir) {
|
|
|
200
190
|
"",
|
|
201
191
|
"const appDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');",
|
|
202
192
|
"",
|
|
203
|
-
"// The auth pages + dashboard middleware
|
|
204
|
-
"//
|
|
205
|
-
"//
|
|
206
|
-
"//
|
|
207
|
-
"//
|
|
193
|
+
"// The auth pages + dashboard middleware query the users table via Drizzle.",
|
|
194
|
+
"// Until `npm run db:generate` + `npm run db:migrate` have created it, a",
|
|
195
|
+
"// request hitting those modules 500s. We detect that at the RESPONSE level",
|
|
196
|
+
"// (a 5xx on the dashboard) and SKIP with a clear message rather than report",
|
|
197
|
+
"// a misleading failure. After you run",
|
|
208
198
|
"// npm install && npm run db:generate && npm run db:migrate",
|
|
209
199
|
"// every assertion below runs for real.",
|
|
210
200
|
"process.env.DATABASE_URL ||= 'file:./dev.db';",
|
|
211
201
|
"process.env.AUTH_SECRET ||= 'test-secret-at-least-32-characters-long!!';",
|
|
212
202
|
"",
|
|
213
203
|
"function makeHandler() {",
|
|
214
|
-
" // createRequestHandler builds lazily, so it succeeds even before
|
|
215
|
-
" //
|
|
216
|
-
" //
|
|
204
|
+
" // createRequestHandler builds lazily, so it succeeds even before the DB",
|
|
205
|
+
" // is migrated; the missing table only surfaces when a request reaches a",
|
|
206
|
+
" // module that queries it. That is why readiness is probed per-response.",
|
|
217
207
|
" return createRequestHandler({ appDir, dev: true });",
|
|
218
208
|
"}",
|
|
219
209
|
"",
|
package/package.json
CHANGED
|
@@ -7,10 +7,10 @@ cover what you need, the full hosted docs are at **https://docs.webjs.com**.
|
|
|
7
7
|
|
|
8
8
|
## Persistence + scaffold rules (non-negotiable)
|
|
9
9
|
|
|
10
|
-
- **Use
|
|
11
|
-
(`
|
|
10
|
+
- **Use Drizzle + SQLite for data, never JSON files.** It is already wired up
|
|
11
|
+
(`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate` + `npm run db:migrate`). For
|
|
12
12
|
ANY data the app stores (todos, posts, messages, products, comments), define
|
|
13
|
-
a
|
|
13
|
+
a Drizzle table. NEVER create `data/*.json`, `db.json`, or any JSON file as a
|
|
14
14
|
fake database. NEVER use module-scope arrays / Maps as a substitute. NEVER
|
|
15
15
|
use localStorage for app data. These are project conventions in
|
|
16
16
|
CONVENTIONS.md (a JSON file used as a database resets on reload and
|
|
@@ -131,12 +131,12 @@ self-review loop.
|
|
|
131
131
|
- Custom-element tag names are passed to `.register('tag-name')`. They are NOT
|
|
132
132
|
a static field on the class.
|
|
133
133
|
- One function per server action file (`*.server.ts`).
|
|
134
|
-
- Server-only code (
|
|
134
|
+
- Server-only code (a DB driver like `better-sqlite3`/`pg`, `node:*`, anything that needs Node APIs)
|
|
135
135
|
goes only in `.server.{js,ts}` files, `route.ts` handlers, or
|
|
136
136
|
`middleware.ts`. Never in pages, layouts, or components. Wrap the access in
|
|
137
137
|
a `.server.{js,ts}` file; the framework rewrites that import into an RPC
|
|
138
138
|
stub for the browser. `lib/` holds both server-only infra
|
|
139
|
-
(`
|
|
139
|
+
(the DB in `db/*.server.ts`) and browser-safe utilities (`lib/utils/cn.ts` with
|
|
140
140
|
`cn`); follow the same rule per file.
|
|
141
141
|
- Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat`
|
|
142
142
|
ship. Use plain template-literal expressions
|
package/templates/.env.example
CHANGED
|
@@ -23,5 +23,5 @@ AUTH_SECRET=
|
|
|
23
23
|
# REDIS_URL=redis://localhost:6379
|
|
24
24
|
|
|
25
25
|
# ── Database ────────────────────────────────────────────────────────
|
|
26
|
-
# Used by
|
|
27
|
-
DATABASE_URL=file:./dev.db
|
|
26
|
+
# Used by Drizzle. SQLite for dev, PostgreSQL for production (--db postgres).
|
|
27
|
+
DATABASE_URL=file:./db/dev.db
|
|
@@ -7,10 +7,10 @@ the full hosted docs are at **https://docs.webjs.com**.
|
|
|
7
7
|
|
|
8
8
|
## Persistence + scaffold rules (non-negotiable)
|
|
9
9
|
|
|
10
|
-
- **Use
|
|
11
|
-
(`
|
|
10
|
+
- **Use Drizzle + SQLite for data, never JSON files.** It's already wired up
|
|
11
|
+
(`db/schema.server.ts`, `db/connection.server.ts`, `npm run db:generate` + `npm run db:migrate`). For ANY
|
|
12
12
|
data the app stores (todos, posts, messages, products, comments…),
|
|
13
|
-
define a
|
|
13
|
+
define a Drizzle table. NEVER create `data/*.json`, `db.json`, or any
|
|
14
14
|
JSON file as a fake database. NEVER use module-scope arrays / Maps as
|
|
15
15
|
a substitute. NEVER use localStorage for app data. It resets on reload and cannot scale. This is a project convention
|
|
16
16
|
(CONVENTIONS.md).
|
|
@@ -104,7 +104,7 @@ each change must include.
|
|
|
104
104
|
- Components: extend WebComponent, declare `static properties` (and `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.
|
|
105
105
|
- 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.
|
|
106
106
|
- Server actions: *.server.ts files with one exported async function each.
|
|
107
|
-
- Server-only code (
|
|
107
|
+
- Server-only code (a DB driver like better-sqlite3/pg, node:*, anything needing Node APIs) goes only in .server.{js,ts} files, route.ts handlers, or middleware.ts. Never in pages, layouts, or components. Wrap in a .server.{js,ts} file; the framework rewrites that import to an RPC stub for the browser. The DB lives in db/*.server.ts; lib/ holds other server-only infra and browser-safe utilities (lib/utils/cn.ts with cn); apply the same rule per file.
|
|
108
108
|
- Directives: webjs ships only `unsafeHTML`, `live`, and `repeat`. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported. Use plain template-literal expressions and lifecycle hooks instead.
|
|
109
109
|
- Context: import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context'
|
|
110
110
|
- Task: import { Task, TaskStatus } from '@webjsdev/core/task'
|
|
@@ -45,9 +45,8 @@ jobs:
|
|
|
45
45
|
node-version: '24'
|
|
46
46
|
cache: npm
|
|
47
47
|
- run: npm ci
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
run: npx prisma migrate deploy
|
|
48
|
+
- name: Set up the database (generate + apply migrations)
|
|
49
|
+
run: npm run db:generate && npm run db:migrate
|
|
51
50
|
env:
|
|
52
51
|
DATABASE_URL: file:./ci.db
|
|
53
52
|
# --server keeps this job to node:test (the browser layer is its own
|
|
@@ -66,7 +65,6 @@ jobs:
|
|
|
66
65
|
node-version: '24'
|
|
67
66
|
cache: npm
|
|
68
67
|
- run: npm ci
|
|
69
|
-
- run: npx prisma generate
|
|
70
68
|
- name: Install Playwright Chromium
|
|
71
69
|
run: npx playwright install --with-deps chromium
|
|
72
70
|
- run: npm run test:browser
|
|
@@ -81,9 +79,8 @@ jobs:
|
|
|
81
79
|
node-version: '24'
|
|
82
80
|
cache: npm
|
|
83
81
|
- run: npm ci
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
run: npx prisma migrate deploy
|
|
82
|
+
- name: Set up the database (generate + apply migrations)
|
|
83
|
+
run: npm run db:generate && npm run db:migrate
|
|
87
84
|
env:
|
|
88
85
|
DATABASE_URL: file:./ci.db
|
|
89
86
|
# The scaffold's e2e test (test/hello/e2e/) drives a real browser
|
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.
|
|
@@ -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
|
```
|
|
@@ -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.
|