@webjsdev/cli 0.10.4 → 0.10.6
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 +57 -0
- package/lib/create.js +41 -0
- package/lib/node-preflight.js +46 -0
- package/lib/saas-template.js +32 -5
- package/package.json +1 -1
- package/templates/.vscode/settings.json +15 -0
- package/templates/AGENTS.md +39 -3
- 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.
|
|
@@ -16,6 +42,7 @@ const USAGE = `webjs commands:
|
|
|
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
44
|
webjs check Run correctness checks on the app
|
|
45
|
+
webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
|
|
19
46
|
webjs create <name> [--template full-stack|api|saas] [--no-install] Scaffold a new webjs app
|
|
20
47
|
(only 3 templates exist. default: full-stack with Prisma+SQLite)
|
|
21
48
|
Auto-runs the detected package manager's install in the new dir
|
|
@@ -41,6 +68,15 @@ function flag(args, name, def) {
|
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
async function main() {
|
|
71
|
+
// Preflight: webjs needs Node 24+ (built-in TS strip + recursive fs.watch).
|
|
72
|
+
// Run before any subcommand so an older Node fails fast with a clear,
|
|
73
|
+
// actionable message naming the found + required version, exiting non-zero
|
|
74
|
+
// instead of crashing cryptically later. `help` is exempt so a user on an
|
|
75
|
+
// old Node can still read usage.
|
|
76
|
+
if (cmd !== 'help' && cmd !== undefined) {
|
|
77
|
+
const { assertNodeVersion } = await import('@webjsdev/server');
|
|
78
|
+
assertNodeVersion({ onFail: 'exit' });
|
|
79
|
+
}
|
|
44
80
|
switch (cmd) {
|
|
45
81
|
case 'dev': {
|
|
46
82
|
// If we're already inside the --watch child, start the server directly.
|
|
@@ -242,6 +278,27 @@ async function main() {
|
|
|
242
278
|
}
|
|
243
279
|
break;
|
|
244
280
|
}
|
|
281
|
+
case 'types': {
|
|
282
|
+
// Generate `.webjs/routes.d.ts` from the app's `app/` routes (#258),
|
|
283
|
+
// narrowing the @webjsdev/core `Route` href union + per-route `params`.
|
|
284
|
+
// Opt-in codegen: the static types in @webjsdev/core work without it
|
|
285
|
+
// (un-generated apps see `Route = string`).
|
|
286
|
+
const { generateRouteTypes } = await import('@webjsdev/server');
|
|
287
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
288
|
+
const appDir = process.cwd();
|
|
289
|
+
const text = await generateRouteTypes(appDir);
|
|
290
|
+
const outDir = join(appDir, '.webjs');
|
|
291
|
+
await mkdir(outDir, { recursive: true });
|
|
292
|
+
const outFile = join(outDir, 'routes.d.ts');
|
|
293
|
+
await writeFile(outFile, text);
|
|
294
|
+
// Count the typed routes (each `WebjsRoutes` key is one route literal).
|
|
295
|
+
const count = (text.match(/^\s+".*": true;$/gm) || []).length;
|
|
296
|
+
console.log(
|
|
297
|
+
`webjs types: wrote .webjs/routes.d.ts (${count} route${count === 1 ? '' : 's'} typed). ` +
|
|
298
|
+
`Ensure tsconfig "include" lists ".webjs/routes.d.ts" so tsserver picks it up.`,
|
|
299
|
+
);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
245
302
|
case 'create': {
|
|
246
303
|
const name = rest[0];
|
|
247
304
|
if (!name || name.startsWith('-')) {
|
package/lib/create.js
CHANGED
|
@@ -346,6 +346,22 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
346
346
|
{ name: '@webjsdev/ts-plugin' },
|
|
347
347
|
],
|
|
348
348
|
},
|
|
349
|
+
// `.webjs/routes.d.ts` is the OPT-IN generated route-types overlay (#258):
|
|
350
|
+
// run `webjs types` (or `webjs dev`, which emits it) to narrow the
|
|
351
|
+
// @webjsdev/core `Route` href union + per-route `params`. Listed in
|
|
352
|
+
// `include` so tsserver picks it up; it is gitignored (regenerated per
|
|
353
|
+
// machine), so a fresh clone runs `webjs dev` / `webjs types` to recreate
|
|
354
|
+
// it, and the static @webjsdev/core types work even when it is absent.
|
|
355
|
+
include: [
|
|
356
|
+
'app/**/*',
|
|
357
|
+
'components/**/*',
|
|
358
|
+
'modules/**/*',
|
|
359
|
+
'lib/**/*',
|
|
360
|
+
'middleware.js',
|
|
361
|
+
'middleware.ts',
|
|
362
|
+
'.webjs/routes.d.ts',
|
|
363
|
+
],
|
|
364
|
+
exclude: ['node_modules', '.webjs/vendor', 'prisma/migrations'],
|
|
349
365
|
}, null, 2) + '\n');
|
|
350
366
|
|
|
351
367
|
// --- Templates (AGENTS.md, CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
|
|
@@ -397,6 +413,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
397
413
|
// to main, mirroring the webjs framework's own CI.
|
|
398
414
|
'.github/workflows/ci.yml',
|
|
399
415
|
'.editorconfig',
|
|
416
|
+
// VS Code: associate the published webjs-config JSON Schema with the
|
|
417
|
+
// package.json `webjs` block, so an unknown / typo'd key (#259) is
|
|
418
|
+
// flagged natively in the editor instead of silently dropped.
|
|
419
|
+
'.vscode/settings.json',
|
|
400
420
|
// Production / deploy scaffolding. `docker compose up --build` runs
|
|
401
421
|
// the app locally with the same Dockerfile production builds from.
|
|
402
422
|
'Dockerfile',
|
|
@@ -494,6 +514,27 @@ if (process.env.NODE_ENV !== 'production') g.__prisma = prisma;
|
|
|
494
514
|
if (isApi) {
|
|
495
515
|
// API-only template: no layout, no page, no components.
|
|
496
516
|
// Just a health route and an example module with route wrapper.
|
|
517
|
+
|
|
518
|
+
// Root middleware applying CORS to every route. An API consumed by a
|
|
519
|
+
// browser from another origin needs this; the `cors()` primitive
|
|
520
|
+
// handles origin reflection, the OPTIONS preflight, Vary: Origin, and
|
|
521
|
+
// the credentials rule, so route handlers stay focused on data.
|
|
522
|
+
await writeFile(join(appDir, 'middleware.ts'), `import { cors } from '@webjsdev/server';
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* App-wide CORS policy. Replace the allow-list with your real frontend
|
|
526
|
+
* origins. With \`credentials: true\` a wildcard origin is invalid per the
|
|
527
|
+
* CORS spec, so list explicit origins (never \`'*'\` + credentials).
|
|
528
|
+
*/
|
|
529
|
+
export default cors({
|
|
530
|
+
origin: ['http://localhost:3000', 'https://app.example.com'],
|
|
531
|
+
credentials: true,
|
|
532
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
533
|
+
allowedHeaders: ['content-type', 'authorization'],
|
|
534
|
+
maxAge: 86400,
|
|
535
|
+
});
|
|
536
|
+
`);
|
|
537
|
+
|
|
497
538
|
await mkdir(join(appDir, 'app', 'api', 'health'), { recursive: true });
|
|
498
539
|
await mkdir(join(appDir, 'app', 'api', 'users'), { recursive: true });
|
|
499
540
|
await writeFile(join(appDir, 'app', 'api', 'health', 'route.ts'), `export async function GET() {
|
|
@@ -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
|
@@ -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
package/templates/AGENTS.md
CHANGED
|
@@ -112,6 +112,15 @@ layered on top:
|
|
|
112
112
|
See [docs.webjs.com → Editor setup](https://docs.webjs.com/docs/editor-setup)
|
|
113
113
|
for the full walkthrough.
|
|
114
114
|
|
|
115
|
+
**Config validation in `package.json`.** The scaffold ships
|
|
116
|
+
`.vscode/settings.json`, which associates the published webjs-config JSON
|
|
117
|
+
Schema (`@webjsdev/server/webjs-config.schema.json`) with the `webjs` block
|
|
118
|
+
of `package.json`. In VS Code an unknown / typo'd `webjs.*` key (`redirect`
|
|
119
|
+
for `redirects`, say) is then flagged inline instead of silently dropped to
|
|
120
|
+
the default. The same shape is typed by the `WebjsConfig` type from
|
|
121
|
+
`@webjsdev/core` (`import type { WebjsConfig } from '@webjsdev/core'`) for a
|
|
122
|
+
typed reference.
|
|
123
|
+
|
|
115
124
|
## UI components: Webjs UI (preinstalled)
|
|
116
125
|
|
|
117
126
|
This scaffold ships with the standard Webjs UI component kit
|
|
@@ -267,6 +276,28 @@ test/<feature>/ feature-scoped tests, one folder per concern
|
|
|
267
276
|
middleware.ts root middleware (optional, outermost)
|
|
268
277
|
```
|
|
269
278
|
|
|
279
|
+
### Typed page / layout / route-handler props
|
|
280
|
+
|
|
281
|
+
Type page / layout / route-handler arguments with the exported helpers so a
|
|
282
|
+
param typo is a compile-time error:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import type { PageProps, LayoutProps, RouteHandlerContext } from '@webjsdev/core';
|
|
286
|
+
|
|
287
|
+
export default function Post({ params }: PageProps<'/blog/[slug]'>) {
|
|
288
|
+
return html`<h1>${params.slug}</h1>`; // params typed { slug: string }
|
|
289
|
+
}
|
|
290
|
+
export default function RootLayout({ children }: LayoutProps) { /* ... */ }
|
|
291
|
+
export async function GET(req: Request, ctx: RouteHandlerContext) { /* ctx.params */ }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Run `webjs types` once (and ensure `tsconfig.json` `include` lists
|
|
295
|
+
`.webjs/routes.d.ts`, the scaffold already does) to generate the route union:
|
|
296
|
+
`PageProps<'/blog/[slug]'>['params']` then narrows to `{ slug: string }` and
|
|
297
|
+
`navigate()` only accepts real app routes. `webjs dev` regenerates the file on
|
|
298
|
+
startup, so it stays current. Without it, `params` is `Record<string, string>`
|
|
299
|
+
and `navigate()` accepts any string (non-breaking).
|
|
300
|
+
|
|
270
301
|
## Database (Prisma + SQLite by default)
|
|
271
302
|
|
|
272
303
|
Every scaffold includes a Prisma setup pointed at a local SQLite file.
|
|
@@ -440,7 +471,7 @@ import { createContext } from '@webjsdev/core/context';
|
|
|
440
471
|
import { Task } from '@webjsdev/core/task';
|
|
441
472
|
import { fixture, waitForUpdate } from '@webjsdev/core/testing';
|
|
442
473
|
|
|
443
|
-
import { rateLimit, cache, createAuth, Credentials, Session } from '@webjsdev/server';
|
|
474
|
+
import { rateLimit, cors, cache, createAuth, Credentials, Session } from '@webjsdev/server';
|
|
444
475
|
```
|
|
445
476
|
|
|
446
477
|
## Environment variables (server vs browser)
|
|
@@ -709,8 +740,13 @@ return html`
|
|
|
709
740
|
```
|
|
710
741
|
|
|
711
742
|
The router's `closest('webjs-frame')` detection takes precedence over
|
|
712
|
-
layout markers. Only the frame's content swaps. Use this sparingly
|
|
713
|
-
folder-based layouts handle 99% of cases.
|
|
743
|
+
layout markers. Only the frame's content swaps. Use this sparingly,
|
|
744
|
+
folder-based layouts handle 99% of cases. When a frame nav's response
|
|
745
|
+
lacks the matching `<webjs-frame id>` (e.g. an auth redirect), the router
|
|
746
|
+
fires a cancelable, bubbling `webjs:frame-missing` event (detail
|
|
747
|
+
`{ frameId, url, document }`) and leaves the frame unchanged rather than
|
|
748
|
+
silently swapping the whole page; call `preventDefault()` to take over
|
|
749
|
+
the outcome (e.g. `location.assign(e.detail.url)`).
|
|
714
750
|
|
|
715
751
|
### 5. `loading.ts` for per-segment skeletons
|
|
716
752
|
|
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.
|