@webjsdev/cli 0.10.16 → 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 +118 -52
- package/lib/app-tasks.js +62 -0
- package/lib/create.js +202 -62
- package/lib/dev-supervisor.js +61 -0
- 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/README.md
CHANGED
|
@@ -40,15 +40,15 @@ Both `webjs create` and `create-webjs-app` auto-install dependencies in the new
|
|
|
40
40
|
```sh
|
|
41
41
|
webjs create <name> # scaffold a full-stack app (default)
|
|
42
42
|
webjs create <name> --template api # backend-only API app
|
|
43
|
-
webjs create <name> --template saas # auth + dashboard +
|
|
43
|
+
webjs create <name> --template saas # auth + dashboard + Drizzle User model
|
|
44
44
|
|
|
45
|
-
webjs dev # dev server with live reload (
|
|
45
|
+
webjs dev # dev server with live reload (runs webjs.dev.before, e.g. webjs db migrate, then serves; npm run dev is a thin alias)
|
|
46
46
|
webjs start # production server (no build step, serves source directly)
|
|
47
47
|
webjs check # validate source-code conventions (CI gate)
|
|
48
48
|
webjs doctor # verify the project/toolchain setup (local onboarding, not CI)
|
|
49
49
|
webjs test # run server + browser tests
|
|
50
50
|
webjs vendor pin [--download] # pin client deps to a committable importmap (offline/reproducible)
|
|
51
|
-
webjs db <
|
|
51
|
+
webjs db <generate|migrate|push|studio|seed> # drizzle-kit passthrough (+ seed)
|
|
52
52
|
|
|
53
53
|
webjs ui init # initialise @webjsdev/ui in this project
|
|
54
54
|
webjs ui add <names...> # copy components from the registry (https://ui.webjs.dev/registry/<name>.json)
|
package/bin/webjs.js
CHANGED
|
@@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { checkNodeInline, nodeInlineMessage } from '../lib/node-preflight.js';
|
|
6
6
|
import { loadAppEnv, resolvePort } from '../lib/port.js';
|
|
7
|
+
import { planDevSupervisor } from '../lib/dev-supervisor.js';
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
@@ -39,7 +40,8 @@ if (cmd !== 'help' && cmd !== undefined) {
|
|
|
39
40
|
const TEMPLATES = ['full-stack', 'api', 'saas'];
|
|
40
41
|
|
|
41
42
|
const USAGE = `webjs commands:
|
|
42
|
-
webjs dev [--port 8080]
|
|
43
|
+
webjs dev [--port 8080] [--no-hot] Start dev server with live reload
|
|
44
|
+
(--no-hot: run in-process, no hot-reload supervisor)
|
|
43
45
|
webjs start [--port 8080] Start production server (serves source directly, no build step)
|
|
44
46
|
webjs test [--server|--browser] Run server + browser tests
|
|
45
47
|
webjs check [--json] Run correctness checks on the app (--json emits structured violations)
|
|
@@ -47,13 +49,15 @@ const USAGE = `webjs commands:
|
|
|
47
49
|
webjs doctor Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook)
|
|
48
50
|
webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
|
|
49
51
|
webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
|
|
50
|
-
webjs create <name> [--template full-stack|api|saas] [--no-install] Scaffold a new webjs app
|
|
51
|
-
(only 3 templates exist. default: full-stack
|
|
52
|
+
webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--no-install] Scaffold a new webjs app
|
|
53
|
+
(only 3 templates exist. default: full-stack, Drizzle, --db sqlite)
|
|
52
54
|
Auto-runs the detected package manager's install in the new dir
|
|
53
55
|
unless --no-install is passed.
|
|
54
|
-
webjs db generate
|
|
55
|
-
webjs db migrate
|
|
56
|
-
webjs db
|
|
56
|
+
webjs db generate Generate a SQL migration from the schema (drizzle-kit generate)
|
|
57
|
+
webjs db migrate Apply pending migrations (drizzle-kit migrate)
|
|
58
|
+
webjs db push Push the schema straight to the dev DB (drizzle-kit push)
|
|
59
|
+
webjs db studio Open the database browser (drizzle-kit studio)
|
|
60
|
+
webjs db seed Run the app's db/seed.server.ts
|
|
57
61
|
webjs ui <subcmd> AI-first component library CLI
|
|
58
62
|
(init / add / list / view / diff / info)
|
|
59
63
|
Requires @webjsdev/ui installed in the project
|
|
@@ -71,6 +75,42 @@ function flag(args, name, def) {
|
|
|
71
75
|
return args[i + 1];
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Run the configured `before` steps (#550) for a phase, aborting the boot on
|
|
80
|
+
* the first failure. The orchestration lives in `lib/run-tasks.js` (pure,
|
|
81
|
+
* unit-tested); this owns the phase-prefixed logging + the non-zero exit (a
|
|
82
|
+
* failed generate/migrate must not serve stale code/schema).
|
|
83
|
+
*
|
|
84
|
+
* @param {string} phase 'dev' | 'start' (for the log line)
|
|
85
|
+
* @param {string[]} steps
|
|
86
|
+
* @param {string} cwd
|
|
87
|
+
*/
|
|
88
|
+
async function runPhaseBeforeSteps(phase, steps, cwd) {
|
|
89
|
+
const { runBeforeSteps } = await import('../lib/run-tasks.js');
|
|
90
|
+
const r = await runBeforeSteps(steps, cwd, {
|
|
91
|
+
onStep: (step) => console.log(`webjs ${phase}: running before-step \`${step}\`…`),
|
|
92
|
+
});
|
|
93
|
+
if (!r.ok) {
|
|
94
|
+
console.error(`webjs ${phase}: before-step failed (exit ${r.code}): ${r.step}`);
|
|
95
|
+
process.exit(r.code);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Spawn the configured dev `parallel` tasks (#550) with phase-prefixed logging,
|
|
101
|
+
* delegating the spawn + teardown to `lib/run-tasks.js`'s `startParallelTasks`.
|
|
102
|
+
*
|
|
103
|
+
* @param {string[]} commands
|
|
104
|
+
* @param {string} cwd
|
|
105
|
+
* @returns {Promise<() => void>} the killer
|
|
106
|
+
*/
|
|
107
|
+
async function startDevParallelTasks(commands, cwd) {
|
|
108
|
+
const { startParallelTasks } = await import('../lib/run-tasks.js');
|
|
109
|
+
return startParallelTasks(commands, cwd, {
|
|
110
|
+
onStart: (cmd) => console.log(`webjs dev: starting parallel task \`${cmd}\`…`),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
74
114
|
async function main() {
|
|
75
115
|
// Preflight: webjs needs Node 24+ (built-in TS strip + recursive fs.watch).
|
|
76
116
|
// Run before any subcommand so an older Node fails fast with a clear,
|
|
@@ -83,7 +123,8 @@ async function main() {
|
|
|
83
123
|
}
|
|
84
124
|
switch (cmd) {
|
|
85
125
|
case 'dev': {
|
|
86
|
-
// If we're already inside the
|
|
126
|
+
// If we're already inside the reload child (node --watch or bun --hot),
|
|
127
|
+
// start the server directly.
|
|
87
128
|
if (process.env.__WEBJS_DEV_CHILD === '1') {
|
|
88
129
|
const { startServer } = await import('@webjsdev/server');
|
|
89
130
|
// Load `.env` BEFORE resolving the port so a `PORT` set there is in
|
|
@@ -95,46 +136,46 @@ async function main() {
|
|
|
95
136
|
break;
|
|
96
137
|
}
|
|
97
138
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
139
|
+
// Run the configured dev orchestration in the PARENT only (#550), so a
|
|
140
|
+
// bare `webjs dev` matches `npm run dev`. `dev.before` (one-shot tasks)
|
|
141
|
+
// runs to completion first; `dev.parallel` (Tailwind's
|
|
142
|
+
// watcher, etc.) then runs as children alongside the server. Spawned once
|
|
143
|
+
// here, NOT in the watch child (which re-execs on every restart). Torn
|
|
144
|
+
// down on exit so a watcher cannot outlive the server.
|
|
145
|
+
const { readAppTasks } = await import('../lib/app-tasks.js');
|
|
146
|
+
const devTasks = readAppTasks(process.cwd());
|
|
147
|
+
await runPhaseBeforeSteps('dev', devTasks.dev.before, process.cwd());
|
|
148
|
+
const killTasks = await startDevParallelTasks(devTasks.dev.parallel, process.cwd());
|
|
149
|
+
process.on('SIGINT', () => { killTasks(); process.exit(0); });
|
|
150
|
+
process.on('SIGTERM', () => { killTasks(); process.exit(0); });
|
|
151
|
+
|
|
152
|
+
// Decide how to run: in-process (`--no-hot`), or re-exec'd under the host
|
|
153
|
+
// runtime's hot-reload supervisor (`node --watch` on Node, `bun --hot` on
|
|
154
|
+
// Bun, #514). The branch logic lives in the pure `planDevSupervisor` so it
|
|
155
|
+
// is unit-testable without spawning a process.
|
|
113
156
|
const { existsSync } = await import('node:fs');
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
157
|
+
const plan = planDevSupervisor({
|
|
158
|
+
isBun: !!process.versions.bun,
|
|
159
|
+
argv: process.argv.slice(1),
|
|
160
|
+
noHot: rest.includes('--no-hot'),
|
|
161
|
+
exists: (p) => existsSync(p),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (plan.mode === 'inline') {
|
|
165
|
+
const { startServer } = await import('@webjsdev/server');
|
|
166
|
+
loadAppEnv(process.cwd());
|
|
167
|
+
const port = resolvePort(flag(rest, '--port'));
|
|
168
|
+
await startServer({ appDir: process.cwd(), port, dev: true });
|
|
169
|
+
killTasks();
|
|
170
|
+
break;
|
|
121
171
|
}
|
|
122
172
|
|
|
123
|
-
const child = spawn(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
...process.argv.slice(1),
|
|
130
|
-
],
|
|
131
|
-
{
|
|
132
|
-
stdio: 'inherit',
|
|
133
|
-
cwd: process.cwd(),
|
|
134
|
-
env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
|
|
135
|
-
},
|
|
136
|
-
);
|
|
137
|
-
child.on('exit', (code) => process.exit(code ?? 0));
|
|
173
|
+
const child = spawn(process.execPath, plan.args, {
|
|
174
|
+
stdio: 'inherit',
|
|
175
|
+
cwd: process.cwd(),
|
|
176
|
+
env: { ...process.env, __WEBJS_DEV_CHILD: '1' },
|
|
177
|
+
});
|
|
178
|
+
child.on('exit', (code) => { killTasks(); process.exit(code ?? 0); });
|
|
138
179
|
break;
|
|
139
180
|
}
|
|
140
181
|
case 'start': {
|
|
@@ -142,6 +183,11 @@ async function main() {
|
|
|
142
183
|
// Load `.env` BEFORE resolving the port so a `PORT` set there wins over
|
|
143
184
|
// the 8080 default (#447), same as for `dev`.
|
|
144
185
|
loadAppEnv(process.cwd());
|
|
186
|
+
// Run the configured `start.before` steps (e.g. `webjs db migrate`)
|
|
187
|
+
// before serving (#550), so a bare `webjs start` is not a degraded run
|
|
188
|
+
// that skips the `prestart` hook. Aborts the boot on a failed step.
|
|
189
|
+
const { readAppTasks } = await import('../lib/app-tasks.js');
|
|
190
|
+
await runPhaseBeforeSteps('start', readAppTasks(process.cwd()).start.before, process.cwd());
|
|
145
191
|
const port = resolvePort(flag(rest, '--port'));
|
|
146
192
|
await startServer({ appDir: process.cwd(), port, dev: false });
|
|
147
193
|
break;
|
|
@@ -149,10 +195,28 @@ async function main() {
|
|
|
149
195
|
case 'db': {
|
|
150
196
|
const sub = rest[0];
|
|
151
197
|
const args = rest.slice(1);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
// `webjs db seed` runs the app's own seed script directly (not a
|
|
199
|
+
// drizzle-kit command); Drizzle has no codegen, so there is no
|
|
200
|
+
// `generate`-the-client step, only schema-to-SQL `generate`.
|
|
201
|
+
if (sub === 'seed') {
|
|
202
|
+
const { existsSync } = await import('node:fs');
|
|
203
|
+
const seedFile = ['db/seed.server.ts', 'db/seed.server.js']
|
|
204
|
+
.map((p) => join(process.cwd(), p)).find(existsSync);
|
|
205
|
+
if (!seedFile) {
|
|
206
|
+
console.error('No db/seed.server.ts found in this app.');
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
const child = spawn(process.execPath, [seedFile], { stdio: 'inherit', cwd: process.cwd() });
|
|
210
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
// generate (schema -> SQL migration), migrate (apply), push (dev
|
|
214
|
+
// schema sync), studio. All wrap drizzle-kit; the verbose name stays
|
|
215
|
+
// hidden behind `webjs db`.
|
|
216
|
+
const map = { generate: ['generate'], migrate: ['migrate'], push: ['push'], studio: ['studio'] };
|
|
217
|
+
const kitArgs = map[sub];
|
|
218
|
+
if (!kitArgs) { console.error('Unknown db subcommand.\n' + USAGE); process.exit(1); }
|
|
219
|
+
const child = spawn('npx', ['drizzle-kit', ...kitArgs, ...args], { stdio: 'inherit', cwd: process.cwd() });
|
|
156
220
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
157
221
|
break;
|
|
158
222
|
}
|
|
@@ -407,18 +471,18 @@ async function main() {
|
|
|
407
471
|
console.error(`Error: unknown template '${template}'.
|
|
408
472
|
|
|
409
473
|
Only three scaffolds exist:
|
|
410
|
-
full-stack (default): pages + components + API +
|
|
474
|
+
full-stack (default): pages + components + API + Drizzle/SQLite.
|
|
411
475
|
Pick this for any app the user describes in product terms
|
|
412
476
|
(todo app, blog, dashboard, marketplace, social feed, …).
|
|
413
477
|
api backend-only: route handlers + modules, no pages/SSR.
|
|
414
478
|
Pick this only if the user explicitly asks for an HTTP/JSON
|
|
415
479
|
API with no UI.
|
|
416
|
-
saas auth + login/signup + protected dashboard +
|
|
480
|
+
saas auth + login/signup + protected dashboard + Drizzle User
|
|
417
481
|
model. Pick this only if the user explicitly asks for auth
|
|
418
482
|
or a SaaS-shaped product.
|
|
419
483
|
|
|
420
484
|
The scaffold is a starting point. Replace the example layout/page/
|
|
421
|
-
components/schema with the actual app the user requested. Use
|
|
485
|
+
components/schema with the actual app the user requested. Use Drizzle +
|
|
422
486
|
SQLite for persistence (already wired up). Never store app data in JSON
|
|
423
487
|
files.
|
|
424
488
|
|
|
@@ -426,8 +490,10 @@ Full docs: https://docs.webjs.com`);
|
|
|
426
490
|
process.exit(1);
|
|
427
491
|
}
|
|
428
492
|
const noInstall = rest.includes('--no-install');
|
|
493
|
+
// --db picks the database dialect: sqlite (default) or postgres.
|
|
494
|
+
const db = flag(rest, '--db', 'sqlite');
|
|
429
495
|
const { scaffoldApp } = await import('../lib/create.js');
|
|
430
|
-
await scaffoldApp(name, process.cwd(), { template, install: !noInstall });
|
|
496
|
+
await scaffoldApp(name, process.cwd(), { template, db, install: !noInstall });
|
|
431
497
|
break;
|
|
432
498
|
}
|
|
433
499
|
case 'vendor': {
|
package/lib/app-tasks.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read the dev/start task orchestration from an app's `package.json` `"webjs"`
|
|
6
|
+
* block (#550). This is what lets `webjs dev` / `webjs start` behave identically
|
|
7
|
+
* to `npm run dev` / `npm run start`: the orchestration (Tailwind's watcher, a
|
|
8
|
+
* `db migrate` before prod boot) moves OUT of `concurrently` + `pre*` npm hooks
|
|
9
|
+
* and INTO the framework primitive, so a bare `webjs dev` is not a degraded run.
|
|
10
|
+
*
|
|
11
|
+
* Shape:
|
|
12
|
+
* "webjs": {
|
|
13
|
+
* "dev": {
|
|
14
|
+
* "before": ["webjs db migrate"],
|
|
15
|
+
* "parallel": ["tailwindcss -i ./public/input.css -o ./public/tailwind.css --watch"]
|
|
16
|
+
* },
|
|
17
|
+
* "start": { "before": ["webjs db migrate"] }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* `before` commands run sequentially to completion BEFORE the server boots (the
|
|
21
|
+
* old `predev` / `prestart` hooks: a one-shot `webjs db migrate`).
|
|
22
|
+
* `parallel` (dev only) commands run as long-lived child processes ALONGSIDE the
|
|
23
|
+
* server (the old `concurrently` watchers: Tailwind). Returns normalized arrays
|
|
24
|
+
* (never undefined) so callers iterate without guards, and a missing/empty
|
|
25
|
+
* config yields empty arrays so a plain app runs `webjs dev`/`start` unchanged.
|
|
26
|
+
*
|
|
27
|
+
* Pure (reads one file, never spawns / prints / exits) so it is unit-testable
|
|
28
|
+
* without a process, matching `lib/port.js` and `lib/dev-supervisor.js`.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} appDir
|
|
31
|
+
* @param {(p: string) => string} [readFile] injectable reader for tests
|
|
32
|
+
* @returns {{ dev: { before: string[], parallel: string[] }, start: { before: string[] } }}
|
|
33
|
+
*/
|
|
34
|
+
export function readAppTasks(appDir, readFile) {
|
|
35
|
+
const read = readFile || ((p) => readFileSync(p, 'utf8'));
|
|
36
|
+
let pkg = {};
|
|
37
|
+
try {
|
|
38
|
+
pkg = JSON.parse(read(join(appDir, 'package.json')));
|
|
39
|
+
} catch {
|
|
40
|
+
// No package.json, or unparseable: a plain run with no orchestration.
|
|
41
|
+
return emptyTasks();
|
|
42
|
+
}
|
|
43
|
+
const webjs = pkg && typeof pkg === 'object' ? pkg.webjs : null;
|
|
44
|
+
if (!webjs || typeof webjs !== 'object') return emptyTasks();
|
|
45
|
+
|
|
46
|
+
/** Keep only non-empty string entries; drop anything else defensively. */
|
|
47
|
+
const cmds = (v) =>
|
|
48
|
+
Array.isArray(v) ? v.filter((s) => typeof s === 'string' && s.trim().length > 0) : [];
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
dev: {
|
|
52
|
+
before: cmds(webjs.dev && webjs.dev.before),
|
|
53
|
+
parallel: cmds(webjs.dev && webjs.dev.parallel),
|
|
54
|
+
},
|
|
55
|
+
start: { before: cmds(webjs.start && webjs.start.before) },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @returns {{ dev: { before: string[], parallel: string[] }, start: { before: string[] } }} */
|
|
60
|
+
function emptyTasks() {
|
|
61
|
+
return { dev: { before: [], parallel: [] }, start: { before: [] } };
|
|
62
|
+
}
|