@webjsdev/cli 0.10.3 → 0.10.5
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/bin/webjs.js +44 -15
- package/lib/create.js +23 -2
- package/lib/node-preflight.js +46 -0
- package/lib/saas-template.js +33 -6
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +3 -2
- package/templates/.cursorrules +2 -2
- package/templates/.github/copilot-instructions.md +2 -2
- package/templates/AGENTS.md +8 -6
- package/templates/CONVENTIONS.md +58 -70
- package/templates/Dockerfile +8 -0
package/bin/webjs.js
CHANGED
|
@@ -2,10 +2,36 @@
|
|
|
2
2
|
import { resolve, join, dirname } from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { checkNodeInline, nodeInlineMessage } from '../lib/node-preflight.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
8
9
|
|
|
10
|
+
// Node-version preflight (issue #238), INLINE and dependency-free.
|
|
11
|
+
// This MUST run before any `import @webjsdev/server`: importing the server
|
|
12
|
+
// package links `src/dev.js`, which references Node 24+ builtins, so on an old
|
|
13
|
+
// Node that import would LINK-fail before any preflight inside the server
|
|
14
|
+
// package could run. The primary guard is therefore `checkNodeInline` (from
|
|
15
|
+
// `../lib/node-preflight.js`, which imports nothing), depending only on
|
|
16
|
+
// `process.versions.node`. The richer `assertNodeVersion` import inside main()
|
|
17
|
+
// stays as belt-and-suspenders for the link-ok (>= 22.13) cases.
|
|
18
|
+
// `help` / no-arg is exempt so a user on an old Node can still read usage.
|
|
19
|
+
if (cmd !== 'help' && cmd !== undefined) {
|
|
20
|
+
let engines = '>=24.0.0';
|
|
21
|
+
try {
|
|
22
|
+
const { readFileSync } = await import('node:fs');
|
|
23
|
+
const pkg = JSON.parse(
|
|
24
|
+
readFileSync(join(__dirname, '..', 'package.json'), 'utf8'),
|
|
25
|
+
);
|
|
26
|
+
engines = pkg?.engines?.node || engines;
|
|
27
|
+
} catch {}
|
|
28
|
+
const r = checkNodeInline(process.versions.node, engines);
|
|
29
|
+
if (!r.ok) {
|
|
30
|
+
console.error(nodeInlineMessage(r));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
// Exactly three scaffolds exist. Keep this list as the single source of
|
|
10
36
|
// truth. AI-agent docs in README.md / AGENTS.md / .cursorrules /
|
|
11
37
|
// .agents/rules/workflow.md / .github/copilot-instructions.md mirror it.
|
|
@@ -15,7 +41,7 @@ const USAGE = `webjs commands:
|
|
|
15
41
|
webjs dev [--port 8080] Start dev server with live reload
|
|
16
42
|
webjs start [--port 8080] Start production server (serves source directly, no build step)
|
|
17
43
|
webjs test [--server|--browser] Run server + browser tests
|
|
18
|
-
webjs check
|
|
44
|
+
webjs check Run correctness checks on the app
|
|
19
45
|
webjs create <name> [--template full-stack|api|saas] [--no-install] Scaffold a new webjs app
|
|
20
46
|
(only 3 templates exist. default: full-stack with Prisma+SQLite)
|
|
21
47
|
Auto-runs the detected package manager's install in the new dir
|
|
@@ -41,6 +67,15 @@ function flag(args, name, def) {
|
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
async function main() {
|
|
70
|
+
// Preflight: webjs needs Node 24+ (built-in TS strip + recursive fs.watch).
|
|
71
|
+
// Run before any subcommand so an older Node fails fast with a clear,
|
|
72
|
+
// actionable message naming the found + required version, exiting non-zero
|
|
73
|
+
// instead of crashing cryptically later. `help` is exempt so a user on an
|
|
74
|
+
// old Node can still read usage.
|
|
75
|
+
if (cmd !== 'help' && cmd !== undefined) {
|
|
76
|
+
const { assertNodeVersion } = await import('@webjsdev/server');
|
|
77
|
+
assertNodeVersion({ onFail: 'exit' });
|
|
78
|
+
}
|
|
44
79
|
switch (cmd) {
|
|
45
80
|
case 'dev': {
|
|
46
81
|
// If we're already inside the --watch child, start the server directly.
|
|
@@ -212,22 +247,16 @@ async function main() {
|
|
|
212
247
|
break;
|
|
213
248
|
}
|
|
214
249
|
case 'check': {
|
|
215
|
-
const { checkConventions, RULES
|
|
250
|
+
const { checkConventions, RULES } = await import('@webjsdev/server/check');
|
|
216
251
|
|
|
217
252
|
if (rest.includes('--rules')) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
console.log('
|
|
221
|
-
console.log('
|
|
222
|
-
console.log('
|
|
223
|
-
console.log(' to false.\n');
|
|
253
|
+
console.log('webjs check, correctness rules:');
|
|
254
|
+
console.log(' Every rule catches objectively broken code (a crash, a');
|
|
255
|
+
console.log(' security leak, or a build/type-strip failure) and always');
|
|
256
|
+
console.log(' runs. Project conventions (layout, style, process) are');
|
|
257
|
+
console.log(' guidance in CONVENTIONS.md, not rules here.\n');
|
|
224
258
|
for (const r of RULES) {
|
|
225
|
-
|
|
226
|
-
const status = off ? '[disabled by override]' : '[enabled]';
|
|
227
|
-
console.log(` ${r.name.padEnd(30)} ${status.padEnd(24)} ${r.description}`);
|
|
228
|
-
}
|
|
229
|
-
if (!anyOverride) {
|
|
230
|
-
console.log('\n (no overrides found; every rule above is active in this project)');
|
|
259
|
+
console.log(` ${r.name.padEnd(30)} ${r.description}`);
|
|
231
260
|
}
|
|
232
261
|
break;
|
|
233
262
|
}
|
|
@@ -235,7 +264,7 @@ async function main() {
|
|
|
235
264
|
const violations = await checkConventions(process.cwd());
|
|
236
265
|
|
|
237
266
|
if (violations.length === 0) {
|
|
238
|
-
console.log('webjs check: all
|
|
267
|
+
console.log('webjs check: all checks pass ✓');
|
|
239
268
|
} else {
|
|
240
269
|
console.log(`webjs check: ${violations.length} violation(s) found\n`);
|
|
241
270
|
for (const v of violations) {
|
package/lib/create.js
CHANGED
|
@@ -494,6 +494,27 @@ if (process.env.NODE_ENV !== 'production') g.__prisma = prisma;
|
|
|
494
494
|
if (isApi) {
|
|
495
495
|
// API-only template: no layout, no page, no components.
|
|
496
496
|
// Just a health route and an example module with route wrapper.
|
|
497
|
+
|
|
498
|
+
// Root middleware applying CORS to every route. An API consumed by a
|
|
499
|
+
// browser from another origin needs this; the `cors()` primitive
|
|
500
|
+
// handles origin reflection, the OPTIONS preflight, Vary: Origin, and
|
|
501
|
+
// the credentials rule, so route handlers stay focused on data.
|
|
502
|
+
await writeFile(join(appDir, 'middleware.ts'), `import { cors } from '@webjsdev/server';
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* App-wide CORS policy. Replace the allow-list with your real frontend
|
|
506
|
+
* origins. With \`credentials: true\` a wildcard origin is invalid per the
|
|
507
|
+
* CORS spec, so list explicit origins (never \`'*'\` + credentials).
|
|
508
|
+
*/
|
|
509
|
+
export default cors({
|
|
510
|
+
origin: ['http://localhost:3000', 'https://app.example.com'],
|
|
511
|
+
credentials: true,
|
|
512
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
513
|
+
allowedHeaders: ['content-type', 'authorization'],
|
|
514
|
+
maxAge: 86400,
|
|
515
|
+
});
|
|
516
|
+
`);
|
|
517
|
+
|
|
497
518
|
await mkdir(join(appDir, 'app', 'api', 'health'), { recursive: true });
|
|
498
519
|
await mkdir(join(appDir, 'app', 'api', 'users'), { recursive: true });
|
|
499
520
|
await writeFile(join(appDir, 'app', 'api', 'health', 'route.ts'), `export async function GET() {
|
|
@@ -536,7 +557,7 @@ export async function POST(req: Request) {
|
|
|
536
557
|
return Response.json(await createUser(body));
|
|
537
558
|
}
|
|
538
559
|
`);
|
|
539
|
-
// Minimal test
|
|
560
|
+
// Minimal starter test so a freshly scaffolded app ships with a test
|
|
540
561
|
// and `webjs test` runs cleanly. Replace these with real assertions
|
|
541
562
|
// once you wire the action/query to a real data source.
|
|
542
563
|
await writeFile(join(appDir, 'test', 'unit', 'users.test.ts'), `import { test } from 'node:test';
|
|
@@ -820,7 +841,7 @@ export default function Home() {
|
|
|
820
841
|
<p class="text-lede leading-[1.5] text-fg-muted max-w-[56ch] m-0 mb-6">
|
|
821
842
|
Edit <code class="font-mono text-[0.9em]">app/page.ts</code> to get started.
|
|
822
843
|
Run \${accentLink('#', 'webjs test')} to run tests and
|
|
823
|
-
\${accentLink('#', 'webjs check')} to
|
|
844
|
+
\${accentLink('#', 'webjs check')} to catch correctness issues.
|
|
824
845
|
</p>
|
|
825
846
|
<div class="flex gap-3 items-center">
|
|
826
847
|
<button class=\${buttonClass()}>Get started</button>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline, dependency-free Node-version preflight for the CLI (issue #238).
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately SEPARATE from `@webjsdev/server`'s `node-version.js`:
|
|
5
|
+
* importing the server package links `src/dev.js`, which references Node 24+
|
|
6
|
+
* builtins, so on an old Node that import LINK-fails before any preflight could
|
|
7
|
+
* run. The CLI's primary guard must therefore depend on nothing but
|
|
8
|
+
* `process.versions.node`. `bin/webjs.js` imports `checkNodeInline` from here
|
|
9
|
+
* and runs it before any `import @webjsdev/server`. This module imports nothing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pure Node-major check. Reads the minimum from the passed `engines` range
|
|
14
|
+
* (the CLI's own `engines.node`, so the requirement lives in one place) and
|
|
15
|
+
* compares the running major. Fails open (`ok: true`) on an unparseable running
|
|
16
|
+
* version so an unusual runtime is not blocked; falls back to 24 when the
|
|
17
|
+
* engines range carries no integer.
|
|
18
|
+
* @param {string} current running Node version (e.g. `process.versions.node`)
|
|
19
|
+
* @param {string} engines the CLI package's `engines.node` range
|
|
20
|
+
* @returns {{ ok: boolean, current: string, currentMajor: number, requiredMajor: number }}
|
|
21
|
+
*/
|
|
22
|
+
export function checkNodeInline(current, engines) {
|
|
23
|
+
const cm = String(current).match(/^v?(\d+)/);
|
|
24
|
+
const rm = String(engines).match(/(\d+)/);
|
|
25
|
+
const currentMajor = cm ? Number(cm[1]) : NaN;
|
|
26
|
+
const requiredMajor = rm ? Number(rm[1]) : 24;
|
|
27
|
+
const ok = Number.isNaN(currentMajor) || currentMajor >= requiredMajor;
|
|
28
|
+
return { ok, current, currentMajor, requiredMajor };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compose the actionable stderr message for an unsupported Node. Names the found
|
|
33
|
+
* and required version and the reason (the built-in TS strip + recursive
|
|
34
|
+
* fs.watch need Node 24+).
|
|
35
|
+
* @param {{ current: string, currentMajor: number, requiredMajor: number }} r
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function nodeInlineMessage(r) {
|
|
39
|
+
return (
|
|
40
|
+
`webjs requires Node ${r.requiredMajor}+ but found Node ${r.current}. ` +
|
|
41
|
+
`webjs is buildless and relies on Node ${r.requiredMajor}'s built-in ` +
|
|
42
|
+
`TypeScript strip (module.stripTypeScriptTypes) and recursive fs.watch, ` +
|
|
43
|
+
`neither of which exists on Node ${r.currentMajor}. ` +
|
|
44
|
+
`Upgrade to Node ${r.requiredMajor} or newer (see https://nodejs.org).`
|
|
45
|
+
);
|
|
46
|
+
}
|
package/lib/saas-template.js
CHANGED
|
@@ -174,7 +174,7 @@ export async function writeSaasFiles(appDir) {
|
|
|
174
174
|
].join('\n'));
|
|
175
175
|
|
|
176
176
|
// test/unit/auth.test.ts: minimal stub so the scaffold passes
|
|
177
|
-
// `webjs
|
|
177
|
+
// `webjs test` runs cleanly out of the
|
|
178
178
|
// box. The signup/current-user functions import from lib/prisma.server.ts
|
|
179
179
|
// and lib/auth.server.ts, both of which need `prisma generate` to have run before
|
|
180
180
|
// they can be imported, so we deliberately test only the runtime-
|
|
@@ -276,6 +276,7 @@ export async function writeSaasFiles(appDir) {
|
|
|
276
276
|
await mkdir(join(appDir, 'app', 'signup'), { recursive: true });
|
|
277
277
|
await writeFile(join(appDir, 'app', 'signup', 'page.ts'), [
|
|
278
278
|
"import { html } from '@webjsdev/core';",
|
|
279
|
+
"import { signup } from '../../modules/auth/actions/signup.server.ts';",
|
|
279
280
|
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '../../components/ui/card.ts';",
|
|
280
281
|
"import { buttonClass } from '../../components/ui/button.ts';",
|
|
281
282
|
"import { inputClass } from '../../components/ui/input.ts';",
|
|
@@ -283,7 +284,30 @@ export async function writeSaasFiles(appDir) {
|
|
|
283
284
|
"",
|
|
284
285
|
"export const metadata = { title: 'Sign up' };",
|
|
285
286
|
"",
|
|
286
|
-
"
|
|
287
|
+
"// Page server action: handles the POST from the form below. With JS",
|
|
288
|
+
"// disabled this is a plain <form> round-trip; with JS the client router",
|
|
289
|
+
"// swaps the 422 re-render (errors) or follows the 303 (success) in place.",
|
|
290
|
+
"// A validation failure returns fieldErrors + values so the page re-renders",
|
|
291
|
+
"// with messages and the user's typed input preserved (#244).",
|
|
292
|
+
"export async function action({ formData }: { formData: FormData }) {",
|
|
293
|
+
" const name = String(formData.get('name') || '').trim();",
|
|
294
|
+
" const email = String(formData.get('email') || '').trim();",
|
|
295
|
+
" const password = String(formData.get('password') || '');",
|
|
296
|
+
" const values = { name, email };",
|
|
297
|
+
" const fieldErrors: Record<string, string> = {};",
|
|
298
|
+
" if (!name) fieldErrors.name = 'Name is required';",
|
|
299
|
+
" if (!email.includes('@')) fieldErrors.email = 'Enter a valid email';",
|
|
300
|
+
" if (password.length < 8) fieldErrors.password = 'At least 8 characters';",
|
|
301
|
+
" if (Object.keys(fieldErrors).length) return { success: false, fieldErrors, values, status: 422 };",
|
|
302
|
+
" const result = await signup({ name, email, password });",
|
|
303
|
+
" if (!result.success) return { success: false, fieldErrors: { email: result.error }, values, status: result.status };",
|
|
304
|
+
" // Account created. Redirect to login via PRG so a reload will not resubmit.",
|
|
305
|
+
" return { success: true, redirect: '/login' };",
|
|
306
|
+
"}",
|
|
307
|
+
"",
|
|
308
|
+
"export default function SignupPage({ actionData }: { actionData?: { fieldErrors?: Record<string, string>; values?: Record<string, string> } }) {",
|
|
309
|
+
" const errors = actionData?.fieldErrors || {};",
|
|
310
|
+
" const values = actionData?.values || {};",
|
|
287
311
|
" return html`",
|
|
288
312
|
" <div class=\"max-w-sm mx-auto mt-12\">",
|
|
289
313
|
" <div class=${cardClass()}>",
|
|
@@ -292,18 +316,21 @@ export async function writeSaasFiles(appDir) {
|
|
|
292
316
|
" <p class=${cardDescriptionClass()}>Get started with your new workspace.</p>",
|
|
293
317
|
" </div>",
|
|
294
318
|
" <div class=${cardContentClass()}>",
|
|
295
|
-
" <form
|
|
319
|
+
" <form method=\"POST\" class=\"flex flex-col gap-4\">",
|
|
296
320
|
" <div class=\"flex flex-col gap-1.5\">",
|
|
297
321
|
" <label class=${labelClass()} for=\"name\">Name</label>",
|
|
298
|
-
" <input class=${inputClass()} id=\"name\" name=\"name\" type=\"text\" required>",
|
|
322
|
+
" <input class=${inputClass()} id=\"name\" name=\"name\" type=\"text\" value=${values.name || ''} required>",
|
|
323
|
+
" ${errors.name ? html`<p class=\"text-sm text-destructive\">${errors.name}</p>` : ''}",
|
|
299
324
|
" </div>",
|
|
300
325
|
" <div class=\"flex flex-col gap-1.5\">",
|
|
301
326
|
" <label class=${labelClass()} for=\"email\">Email</label>",
|
|
302
|
-
" <input class=${inputClass()} id=\"email\" name=\"email\" type=\"email\" required>",
|
|
327
|
+
" <input class=${inputClass()} id=\"email\" name=\"email\" type=\"email\" value=${values.email || ''} required>",
|
|
328
|
+
" ${errors.email ? html`<p class=\"text-sm text-destructive\">${errors.email}</p>` : ''}",
|
|
303
329
|
" </div>",
|
|
304
330
|
" <div class=\"flex flex-col gap-1.5\">",
|
|
305
331
|
" <label class=${labelClass()} for=\"password\">Password</label>",
|
|
306
|
-
" <input class=${inputClass()} id=\"password\" name=\"password\" type=\"password\" required>",
|
|
332
|
+
" <input class=${inputClass()} id=\"password\" name=\"password\" type=\"password\" minlength=\"8\" required>",
|
|
333
|
+
" ${errors.password ? html`<p class=\"text-sm text-destructive\">${errors.password}</p>` : ''}",
|
|
307
334
|
" </div>",
|
|
308
335
|
" <button class=${buttonClass()} type=\"submit\">Create account</button>",
|
|
309
336
|
" </form>",
|
package/package.json
CHANGED
|
@@ -12,8 +12,9 @@ cover what you need, the full hosted docs are at **https://docs.webjs.com**.
|
|
|
12
12
|
ANY data the app stores (todos, posts, messages, products, comments), define
|
|
13
13
|
a Prisma model. 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
|
-
use localStorage for app data.
|
|
16
|
-
|
|
15
|
+
use localStorage for app data. These are project conventions in
|
|
16
|
+
CONVENTIONS.md (a JSON file used as a database resets on reload and
|
|
17
|
+
cannot scale).
|
|
17
18
|
- **The scaffold is reference, not the final product.** Replace `app/page.ts`,
|
|
18
19
|
the example `User` model, the example users module, etc. with the app the
|
|
19
20
|
user actually asked for. Do not ship "Hello from <app-name>" as the
|
package/templates/.cursorrules
CHANGED
|
@@ -12,8 +12,8 @@ cover what you need, the full hosted docs are at **https://docs.webjs.com**.
|
|
|
12
12
|
data the app stores (todos, posts, messages, products, comments…),
|
|
13
13
|
define a Prisma model. NEVER create `data/*.json`, `db.json`, or any
|
|
14
14
|
JSON file as a fake database. NEVER use module-scope arrays / Maps as
|
|
15
|
-
a substitute. NEVER use localStorage for app data.
|
|
16
|
-
|
|
15
|
+
a substitute. NEVER use localStorage for app data. These are project conventions in CONVENTIONS.md (a JSON file used as a
|
|
16
|
+
database resets on reload and cannot scale).
|
|
17
17
|
- **The scaffold is reference, not the final product.** Replace
|
|
18
18
|
`app/page.ts`, the example `User` model, the example users module, etc.
|
|
19
19
|
with the app the user actually asked for. Don't ship "Hello from
|
|
@@ -12,8 +12,8 @@ the full hosted docs are at **https://docs.webjs.com**.
|
|
|
12
12
|
data the app stores (todos, posts, messages, products, comments…),
|
|
13
13
|
define a Prisma model. NEVER create `data/*.json`, `db.json`, or any
|
|
14
14
|
JSON file as a fake database. NEVER use module-scope arrays / Maps as
|
|
15
|
-
a substitute. NEVER use localStorage for app data.
|
|
16
|
-
|
|
15
|
+
a substitute. NEVER use localStorage for app data. It resets on reload and cannot scale. This is a project convention
|
|
16
|
+
(CONVENTIONS.md).
|
|
17
17
|
- **The scaffold is reference, not the final product.** Replace
|
|
18
18
|
`app/page.ts`, the example `User` model, the example users module, etc.
|
|
19
19
|
with the app the user actually asked for. Don't ship "Hello from
|
package/templates/AGENTS.md
CHANGED
|
@@ -24,8 +24,8 @@ the app the user actually asked for.
|
|
|
24
24
|
stores (todos, posts, messages, products, comments, anything),
|
|
25
25
|
define a Prisma model and persist there.
|
|
26
26
|
- **NEVER** store app data in JSON files (`data/todos.json`,
|
|
27
|
-
`db.json`, …).
|
|
28
|
-
|
|
27
|
+
`db.json`, …). It resets on reload and cannot scale. This is a project convention,
|
|
28
|
+
and the user's prompt explicitly forbids it.
|
|
29
29
|
- **NEVER** use in-memory arrays or `Map`s as a substitute for the
|
|
30
30
|
database. They vanish on every dev-server reload and aren't
|
|
31
31
|
shared across processes.
|
|
@@ -440,7 +440,7 @@ import { createContext } from '@webjsdev/core/context';
|
|
|
440
440
|
import { Task } from '@webjsdev/core/task';
|
|
441
441
|
import { fixture, waitForUpdate } from '@webjsdev/core/testing';
|
|
442
442
|
|
|
443
|
-
import { rateLimit, cache, createAuth, Credentials, Session } from '@webjsdev/server';
|
|
443
|
+
import { rateLimit, cors, cache, createAuth, Credentials, Session } from '@webjsdev/server';
|
|
444
444
|
```
|
|
445
445
|
|
|
446
446
|
## Environment variables (server vs browser)
|
|
@@ -588,7 +588,8 @@ Practical consequences for agents writing webjs code.
|
|
|
588
588
|
| Class-field initializer for a reactive property (`student: Student = {...}`) | Silently breaks reactivity (overwrites the framework accessor) | `declare student: Student` plus constructor default |
|
|
589
589
|
| `@property()` decorator | Banned by invariant 10 (erasable TS) | `static properties = { ... }` plus `declare` |
|
|
590
590
|
| `static styles = css` block without `static shadow = true` | Styles leak globally; the framework warns at runtime | Add `static shadow = true`, or use Tailwind utilities |
|
|
591
|
-
| `willUpdate` computing SSR-visible derived state |
|
|
591
|
+
| `willUpdate` computing SSR-visible derived state | Works (runs at SSR), but overriding it opts the component out of elision | Fine for interactive components; for display-only, derive inline in `render()` |
|
|
592
|
+
| `this.hasAttribute` / `getAttribute` in `render()` | Works (server attribute shim backs the attribute methods at SSR) | Read attributes directly, or via a `static properties` + `declare` reactive prop |
|
|
592
593
|
| `ContextProvider` for server-known data | Default value during SSR, content shift on hydration | Pass via props from the page function |
|
|
593
594
|
|
|
594
595
|
The full annotated catalog with code examples lives in the framework
|
|
@@ -943,5 +944,6 @@ composition, so a nested shell ends up dropped by the HTML parser.
|
|
|
943
944
|
5. When unsure how a framework feature works, `grep` or `cat` the
|
|
944
945
|
relevant `node_modules/@webjsdev/*/src/` file before asking the user.
|
|
945
946
|
|
|
946
|
-
Project
|
|
947
|
-
|
|
947
|
+
Project conventions live in [CONVENTIONS.md](./CONVENTIONS.md) (guidance
|
|
948
|
+
you follow by judgment). `webjs check` is separate: correctness checks
|
|
949
|
+
only, always on, no per-project disabling.
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -9,59 +9,41 @@ Edit the content below the marker to change the convention for your project.
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
This
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
If
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
###
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
webjs check
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"actions-in-modules": false
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Only `false` is meaningful. There's no way to tweak rule *behaviour*
|
|
54
|
-
via config. A rule is either on or off.
|
|
55
|
-
|
|
56
|
-
### Rule for AI agents
|
|
57
|
-
|
|
58
|
-
1. Run `webjs check --rules` to learn the active rule set for this
|
|
59
|
-
project.
|
|
60
|
-
2. Treat every rule not explicitly disabled as binding when writing
|
|
61
|
-
code.
|
|
62
|
-
3. To change which rules are active, edit the `webjs.conventions`
|
|
63
|
-
block in `package.json`. Never inline a rule list into prose, since
|
|
64
|
-
it will drift.
|
|
12
|
+
## `CONVENTIONS.md` vs `webjs check`: two different things
|
|
13
|
+
|
|
14
|
+
This file is the source of truth for **project conventions**: how code
|
|
15
|
+
is organized, named, and tested. They are preferences a reasonable
|
|
16
|
+
project could do differently, so they are guidance (for humans and AI
|
|
17
|
+
agents), not a hard gate. Customize any of them; sections marked
|
|
18
|
+
`<!-- OVERRIDE -->` are explicit customization points.
|
|
19
|
+
|
|
20
|
+
`webjs check` is a separate, narrower tool: **correctness checks** that
|
|
21
|
+
catch objectively broken code (a crash, a security leak, a build or
|
|
22
|
+
type-strip failure). Those always run, there is no per-project
|
|
23
|
+
disabling, and they are not listed here (run `webjs check --rules` to
|
|
24
|
+
see them). The line between the two: *could a sensible app legitimately
|
|
25
|
+
want this to pass?* If yes, it is a convention (this file); if no, it is
|
|
26
|
+
a check (the tool).
|
|
27
|
+
|
|
28
|
+
### Project conventions (follow these)
|
|
29
|
+
|
|
30
|
+
These are the architectural conventions for this app. They are not
|
|
31
|
+
enforced by `webjs check`; follow them by judgment.
|
|
32
|
+
|
|
33
|
+
- **Server actions and queries live in `modules/<feature>/actions/` and
|
|
34
|
+
`modules/<feature>/queries/`** (`*.server.{js,ts}`), not loose in the
|
|
35
|
+
app root. Cross-cutting server infrastructure (the Prisma singleton,
|
|
36
|
+
session helpers, auth config) lives in `lib/`.
|
|
37
|
+
- **One exported function per action/query file.** Name the file after
|
|
38
|
+
the function (`create-post.server.ts` exports `createPost`). It keeps
|
|
39
|
+
the action surface greppable.
|
|
40
|
+
- **Every feature has tests.** A `modules/<feature>/` directory should
|
|
41
|
+
have matching test files under `test/<feature>/`. A unit test for
|
|
42
|
+
logic, a browser/e2e test for user-facing behaviour.
|
|
43
|
+
- **Persist data with Prisma + SQLite, never JSON files.** The scaffold
|
|
44
|
+
wires up `prisma/schema.prisma` and `lib/prisma.server.ts`. A
|
|
45
|
+
`data/todos.json` or `db.json` used as a database resets on reload and
|
|
46
|
+
cannot scale; define a Prisma model instead.
|
|
65
47
|
|
|
66
48
|
---
|
|
67
49
|
|
|
@@ -125,9 +107,9 @@ checklist mirrors this list.
|
|
|
125
107
|
If yes, update it on this PR. Common surfaces (non-exhaustive):
|
|
126
108
|
- `AGENTS.md` (root and every nested one) for API surface, invariants,
|
|
127
109
|
file-routing rules, project-wide agent workflow.
|
|
128
|
-
- `CONVENTIONS.md` (this file) for
|
|
129
|
-
|
|
130
|
-
|
|
110
|
+
- `CONVENTIONS.md` (this file) for project conventions (layout,
|
|
111
|
+
naming, testing). The `webjs check` correctness rules are a separate
|
|
112
|
+
tool surface, not documented here (run `webjs check --rules`).
|
|
131
113
|
- `README.md` (root and any nested ones) for install / use / public
|
|
132
114
|
surface descriptions.
|
|
133
115
|
- `CHANGELOG.md` for any user-visible change, including the SHA / PR
|
|
@@ -267,8 +249,8 @@ deploy`, and `npm run db:migrate` / `db:generate` / `db:studio` scripts.
|
|
|
267
249
|
comments, users…), define a Prisma model in `prisma/schema.prisma`
|
|
268
250
|
and persist there.
|
|
269
251
|
2. **NEVER** create JSON files under `data/`, `db.json`, `posts.json`,
|
|
270
|
-
`todos.json`, etc. as a fake database.
|
|
271
|
-
|
|
252
|
+
`todos.json`, etc. as a fake database. It resets on reload and cannot
|
|
253
|
+
scale; this is a project convention (see the conventions section above).
|
|
272
254
|
3. **NEVER** use module-scope arrays or `Map`s as a "store". They
|
|
273
255
|
reset on every dev-server reload and can't scale beyond one process.
|
|
274
256
|
4. **NEVER** use `localStorage` / `sessionStorage` to persist app data -
|
|
@@ -878,11 +860,15 @@ toggle, the tab switch) requires JS.
|
|
|
878
860
|
JS will fill it in. The first paint must be the right content.
|
|
879
861
|
|
|
880
862
|
**SSR-meaningful component state.** The SSR pipeline constructs the
|
|
881
|
-
component, applies its attributes,
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
863
|
+
component, applies its attributes, runs `willUpdate` and controllers'
|
|
864
|
+
`hostUpdate`, and calls `render()`. It does NOT call `connectedCallback`,
|
|
865
|
+
`firstUpdated`, `updated`, or any other browser-only lifecycle hook.
|
|
866
|
+
Whatever state should appear on first paint MUST be set in the
|
|
867
|
+
constructor (after `super()`), derived in `willUpdate`, or derivable
|
|
868
|
+
from `static properties` + attributes on the rendered tag. Reading
|
|
869
|
+
`this.getAttribute` / `hasAttribute` in `render()` works server-side (a
|
|
870
|
+
server attribute shim backs the attribute methods), but a `Task`'s
|
|
871
|
+
fetch still runs only on the client.
|
|
886
872
|
|
|
887
873
|
```ts
|
|
888
874
|
import { WebComponent, html, signal } from '@webjsdev/core';
|
|
@@ -1019,15 +1005,17 @@ This project enforces a git workflow via agent-specific config files
|
|
|
1019
1005
|
|
|
1020
1006
|
---
|
|
1021
1007
|
|
|
1022
|
-
##
|
|
1008
|
+
## Customizing conventions
|
|
1023
1009
|
|
|
1024
|
-
|
|
1025
|
-
the
|
|
1026
|
-
`package.json`
|
|
1027
|
-
|
|
1010
|
+
The conventions in this file are guidance, so customize them directly:
|
|
1011
|
+
edit the prose under any `<!-- OVERRIDE -->` marker. There is no
|
|
1012
|
+
`package.json` switch and nothing to toggle, because conventions are not
|
|
1013
|
+
enforced by a tool.
|
|
1028
1014
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1015
|
+
`webjs check` is separate: it runs only correctness checks (a crash, a
|
|
1016
|
+
security leak, a build/type-strip failure), always, with no per-project
|
|
1017
|
+
disabling. Run `webjs check` to validate, and `webjs check --rules` to
|
|
1018
|
+
list those checks.
|
|
1031
1019
|
|
|
1032
1020
|
---
|
|
1033
1021
|
|
package/templates/Dockerfile
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
# falls back to esbuild, whose class-declaration transform breaks webjs's SSR
|
|
9
9
|
# walker for multi-class component files. Do not lower this base image
|
|
10
10
|
# below 24 (the same version the CI workflow and the framework pin).
|
|
11
|
+
#
|
|
12
|
+
# Security headers are set by the framework, not the proxy. webjs emits
|
|
13
|
+
# X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and
|
|
14
|
+
# Permissions-Policy on every response, plus Strict-Transport-Security in
|
|
15
|
+
# production over HTTPS (detected from X-Forwarded-Proto on the trusted
|
|
16
|
+
# edge). So the baseline needs no reverse-proxy config. Override or extend
|
|
17
|
+
# per path with package.json "webjs": { "headers": [...] }. See the
|
|
18
|
+
# framework AGENTS.md "Secure response headers" section.
|
|
11
19
|
FROM node:24-alpine
|
|
12
20
|
|
|
13
21
|
# openssl + ca-certificates are required by Prisma's query engine at runtime.
|