@pyreon/zero 0.24.0 → 0.24.2

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/lib/server.js CHANGED
@@ -2299,7 +2299,7 @@ function zeroPlugin(userConfig = {}) {
2299
2299
  const pathname = req.url?.split("?")[0] ?? "/";
2300
2300
  if (pathname.startsWith("/@") || pathname.startsWith("/__")) return next();
2301
2301
  if (/\.\w+$/.test(pathname)) return next();
2302
- handle404(server, routesDir, pathname, res).then((handled) => {
2302
+ handle404(server, routesDir, pathname, res, root, req.originalUrl ?? pathname).then((handled) => {
2303
2303
  if (!handled) next();
2304
2304
  }, (err) => {
2305
2305
  console.error("[Pyreon] Error in 404 handler:", err);
@@ -2423,28 +2423,53 @@ async function dispatchApiRoute(server, req, res) {
2423
2423
  return true;
2424
2424
  }
2425
2425
  /**
2426
- * Static-page 404 fallback for apps WITHOUT `_404.tsx` in the routes tree.
2427
- *
2428
- * For `mode: 'ssr'` apps with `_404.tsx`, the SSR middleware's `renderSsr`
2429
- * routes unmatched URLs through the router-driven path (PR L5 + M1.2) — that
2430
- * produces a layout-wrapped 404 with HTTP status 404, never reaching here.
2431
- * This function is the LEGACY fallback that fires only when:
2432
- * - The app is in `mode: 'spa'` / `mode: 'ssg'` (no dev SSR middleware), OR
2433
- * - The app has no reachable `notFoundComponent` in its routes tree (so the
2434
- * SSR middleware's `resolveRoute` returns matched: [] and falls through).
2426
+ * 404 handler for unmatched URLs in dev. Three behaviours:
2427
+ *
2428
+ * 1. If the URL matches a real route pattern, return false (caller falls
2429
+ * through to the next middleware Vite's SPA shell etc.).
2430
+ * 2. Otherwise, try `renderSsr`. Even for `mode: 'ssg'` / `mode: 'spa'`
2431
+ * apps (no upstream SSR middleware registered) this works in dev: the
2432
+ * router's `findNotFoundFallback` (PR L5 / M1.2) walks the routes
2433
+ * tree, finds a `notFoundComponent` (`_404.tsx` / `_not-found.tsx`)
2434
+ * attached to the deepest matching parent layout, builds a synthetic
2435
+ * chain `[...layouts, syntheticLeaf]`, and renderSsr produces 404
2436
+ * HTML INSIDE the layout's chrome — matching what `dist/404.html`
2437
+ * ships at build time.
2438
+ * 3. If renderSsr returns null (no `notFoundComponent` reachable from
2439
+ * any layout), fall back to a bare static HTML page so the user
2440
+ * gets SOMETHING.
2441
+ *
2442
+ * **Pre-fix this function ALWAYS emitted the bare static page in step 3**,
2443
+ * ignoring any user-provided `_404.tsx` / `_not-found.tsx`. For
2444
+ * `mode: 'ssr'` apps the upstream SSR middleware caught the 404 first
2445
+ * (so a `_404.tsx` worked there), but for `mode: 'ssg'` / `mode: 'spa'`
2446
+ * apps the SSR middleware never registered and unmatched URLs fell
2447
+ * through here directly — dev showed the bare fallback while the
2448
+ * SSG-built `dist/404.html` shipped the branded version. Production-
2449
+ * vs-dev drift; no warning.
2450
+ *
2451
+ * For `mode: 'ssr'` apps the upstream SSR middleware is still the
2452
+ * primary path (cheap when matched). renderSsr may be called twice on a
2453
+ * truly-unmatched URL (once by the upstream middleware, once here as
2454
+ * fallback). The duplicate cost is purely a no-op `resolveRoute` call
2455
+ * returning `matched: []` again — no extra render work.
2435
2456
  *
2436
2457
  * Returns true if the 404 was handled (response sent), false if the path
2437
2458
  * actually matches a route (caller continues to next middleware).
2438
- *
2439
- * Pre-M1.2 a stale comment claimed `_404.tsx` "cannot be SSR-rendered because
2440
- * the compiler emits _tpl() calls that require document". That was wrong — the
2441
- * SSR runtime renders compiler-emitted components fine via `renderToString`
2442
- * (no document needed). The static fallback exists for backward compat with
2443
- * apps that don't ship `_404.tsx`, not because SSR-rendering it is impossible.
2444
2459
  */
2445
- async function handle404(server, _routesDir, pathname, res) {
2460
+ async function handle404(server, _routesDir, pathname, res, root, originalUrl) {
2446
2461
  const routes = (await server.ssrLoadModule(VIRTUAL_ROUTES_ID)).routes;
2447
2462
  if (flattenRoutePatterns(routes).some((pattern) => matchPattern(pattern, pathname))) return false;
2463
+ try {
2464
+ const result = await renderSsr(server, root, originalUrl, pathname);
2465
+ if (result !== null) {
2466
+ res.statusCode = result.status;
2467
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2468
+ res.setHeader("Content-Length", Buffer.byteLength(result.html));
2469
+ res.end(result.html);
2470
+ return true;
2471
+ }
2472
+ } catch {}
2448
2473
  const html = await render404Page(void 0);
2449
2474
  res.statusCode = 404;
2450
2475
  res.setHeader("Content-Type", "text/html; charset=utf-8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
@@ -173,15 +173,15 @@
173
173
  "lint": "oxlint ."
174
174
  },
175
175
  "dependencies": {
176
- "@pyreon/core": "^0.24.0",
177
- "@pyreon/head": "^0.24.0",
178
- "@pyreon/meta": "^0.24.0",
179
- "@pyreon/reactivity": "^0.24.0",
180
- "@pyreon/router": "^0.24.0",
181
- "@pyreon/runtime-dom": "^0.24.0",
182
- "@pyreon/runtime-server": "^0.24.0",
183
- "@pyreon/server": "^0.24.0",
184
- "@pyreon/vite-plugin": "^0.24.0",
176
+ "@pyreon/core": "^0.24.2",
177
+ "@pyreon/head": "^0.24.2",
178
+ "@pyreon/meta": "^0.24.2",
179
+ "@pyreon/reactivity": "^0.24.2",
180
+ "@pyreon/router": "^0.24.2",
181
+ "@pyreon/runtime-dom": "^0.24.2",
182
+ "@pyreon/runtime-server": "^0.24.2",
183
+ "@pyreon/server": "^0.24.2",
184
+ "@pyreon/vite-plugin": "^0.24.2",
185
185
  "vite": "^8.0.0"
186
186
  },
187
187
  "devDependencies": {
@@ -312,7 +312,14 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
312
312
  return next();
313
313
  if (/\.\w+$/.test(pathname)) return next();
314
314
 
315
- handle404(server, routesDir, pathname, res).then(
315
+ handle404(
316
+ server,
317
+ routesDir,
318
+ pathname,
319
+ res,
320
+ root,
321
+ req.originalUrl ?? pathname,
322
+ ).then(
316
323
  (handled) => {
317
324
  if (!handled) next();
318
325
  },
@@ -593,30 +600,47 @@ async function dispatchApiRoute(
593
600
  }
594
601
 
595
602
  /**
596
- * Static-page 404 fallback for apps WITHOUT `_404.tsx` in the routes tree.
603
+ * 404 handler for unmatched URLs in dev. Three behaviours:
597
604
  *
598
- * For `mode: 'ssr'` apps with `_404.tsx`, the SSR middleware's `renderSsr`
599
- * routes unmatched URLs through the router-driven path (PR L5 + M1.2) — that
600
- * produces a layout-wrapped 404 with HTTP status 404, never reaching here.
601
- * This function is the LEGACY fallback that fires only when:
602
- * - The app is in `mode: 'spa'` / `mode: 'ssg'` (no dev SSR middleware), OR
603
- * - The app has no reachable `notFoundComponent` in its routes tree (so the
604
- * SSR middleware's `resolveRoute` returns matched: [] and falls through).
605
+ * 1. If the URL matches a real route pattern, return false (caller falls
606
+ * through to the next middleware Vite's SPA shell etc.).
607
+ * 2. Otherwise, try `renderSsr`. Even for `mode: 'ssg'` / `mode: 'spa'`
608
+ * apps (no upstream SSR middleware registered) this works in dev: the
609
+ * router's `findNotFoundFallback` (PR L5 / M1.2) walks the routes
610
+ * tree, finds a `notFoundComponent` (`_404.tsx` / `_not-found.tsx`)
611
+ * attached to the deepest matching parent layout, builds a synthetic
612
+ * chain `[...layouts, syntheticLeaf]`, and renderSsr produces 404
613
+ * HTML INSIDE the layout's chrome — matching what `dist/404.html`
614
+ * ships at build time.
615
+ * 3. If renderSsr returns null (no `notFoundComponent` reachable from
616
+ * any layout), fall back to a bare static HTML page so the user
617
+ * gets SOMETHING.
618
+ *
619
+ * **Pre-fix this function ALWAYS emitted the bare static page in step 3**,
620
+ * ignoring any user-provided `_404.tsx` / `_not-found.tsx`. For
621
+ * `mode: 'ssr'` apps the upstream SSR middleware caught the 404 first
622
+ * (so a `_404.tsx` worked there), but for `mode: 'ssg'` / `mode: 'spa'`
623
+ * apps the SSR middleware never registered and unmatched URLs fell
624
+ * through here directly — dev showed the bare fallback while the
625
+ * SSG-built `dist/404.html` shipped the branded version. Production-
626
+ * vs-dev drift; no warning.
627
+ *
628
+ * For `mode: 'ssr'` apps the upstream SSR middleware is still the
629
+ * primary path (cheap when matched). renderSsr may be called twice on a
630
+ * truly-unmatched URL (once by the upstream middleware, once here as
631
+ * fallback). The duplicate cost is purely a no-op `resolveRoute` call
632
+ * returning `matched: []` again — no extra render work.
605
633
  *
606
634
  * Returns true if the 404 was handled (response sent), false if the path
607
635
  * actually matches a route (caller continues to next middleware).
608
- *
609
- * Pre-M1.2 a stale comment claimed `_404.tsx` "cannot be SSR-rendered because
610
- * the compiler emits _tpl() calls that require document". That was wrong — the
611
- * SSR runtime renders compiler-emitted components fine via `renderToString`
612
- * (no document needed). The static fallback exists for backward compat with
613
- * apps that don't ship `_404.tsx`, not because SSR-rendering it is impossible.
614
636
  */
615
637
  async function handle404(
616
638
  server: import("vite").ViteDevServer,
617
639
  _routesDir: string,
618
640
  pathname: string,
619
641
  res: import("http").ServerResponse,
642
+ root: string,
643
+ originalUrl: string,
620
644
  ): Promise<boolean> {
621
645
  const mod = await server.ssrLoadModule(VIRTUAL_ROUTES_ID);
622
646
  const routes = mod.routes as Array<{ path?: string; children?: unknown[] }>;
@@ -626,11 +650,32 @@ async function handle404(
626
650
  return false; // Route matches — not a 404
627
651
  }
628
652
 
629
- // No route matched + no `_404.tsx` reachable — emit a minimal static page
630
- // so the user gets SOMETHING. Apps that want branded 404s should add
631
- // `_404.tsx` to their routes tree (canonical pattern); the SSR middleware
632
- // then routes through the router-driven path with layout chrome instead
633
- // of landing here.
653
+ // Try the router-driven path: renderSsr resolveRoute
654
+ // findNotFoundFallback. Returns layout-wrapped 404 HTML + status 404 if
655
+ // any reachable `notFoundComponent` matches; returns null only when no
656
+ // `_404.tsx` / `_not-found.tsx` exists anywhere in the routes tree.
657
+ //
658
+ // Try/catch protects against ssrLoadModule failures (e.g. the user's
659
+ // `app.ts` has a syntax error in dev): we'd rather serve the bare
660
+ // fallback than crash the 404 handler. The caller's error path catches
661
+ // `next(err)` if renderSsr rejects in a way we can't recover from.
662
+ try {
663
+ const result = await renderSsr(server, root, originalUrl, pathname);
664
+ if (result !== null) {
665
+ res.statusCode = result.status;
666
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
667
+ res.setHeader("Content-Length", Buffer.byteLength(result.html));
668
+ res.end(result.html);
669
+ return true;
670
+ }
671
+ } catch {
672
+ // Fall through to bare HTML below.
673
+ }
674
+
675
+ // No `notFoundComponent` reachable + renderSsr returned null — emit a
676
+ // minimal static page so the user gets SOMETHING. Apps that want
677
+ // branded 404s should add `_404.tsx` (or `_not-found.tsx`) to their
678
+ // routes tree.
634
679
  const html = await render404Page(undefined);
635
680
 
636
681
  res.statusCode = 404;