@webjsdev/cli 0.10.4 → 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 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.
@@ -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.
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() {
@@ -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
+ }
@@ -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
- "export default function SignupPage() {",
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 id=\"signup-form\" class=\"flex flex-col gap-4\">",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {
@@ -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)
@@ -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.