@pyreon/zero 0.15.0 → 0.18.0
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/{api-routes-DANluJic.js → api-routes-Ci0kVmM4.js} +2 -2
- package/lib/client.js +4 -1
- package/lib/env.js +6 -6
- package/lib/font.js +3 -3
- package/lib/{fs-router-ZebyutPa.js → fs-router-MewHc5SB.js} +25 -30
- package/lib/i18n-routing.js +112 -1
- package/lib/image.js +140 -58
- package/lib/index.js +252 -82
- package/lib/og-image.js +5 -5
- package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
- package/lib/script.js +114 -25
- package/lib/seo.js +186 -15
- package/lib/server.js +274 -564
- package/lib/types/config.d.ts +307 -3
- package/lib/types/env.d.ts +2 -2
- package/lib/types/i18n-routing.d.ts +193 -2
- package/lib/types/image.d.ts +105 -5
- package/lib/types/index.d.ts +666 -182
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +607 -72
- package/lib/vite-plugin-y0NmCLJA.js +2476 -0
- package/package.json +11 -10
- package/src/adapters/bun.ts +20 -1
- package/src/adapters/cloudflare.ts +78 -1
- package/src/adapters/index.ts +25 -3
- package/src/adapters/netlify.ts +63 -1
- package/src/adapters/node.ts +25 -1
- package/src/adapters/static.ts +26 -1
- package/src/adapters/validate.ts +8 -1
- package/src/adapters/vercel.ts +76 -1
- package/src/adapters/warn-missing-env.ts +49 -0
- package/src/app.ts +14 -0
- package/src/client.ts +18 -0
- package/src/entry-server.ts +55 -5
- package/src/env.ts +7 -7
- package/src/font.ts +3 -3
- package/src/fs-router.ts +72 -3
- package/src/i18n-routing.ts +246 -12
- package/src/image.tsx +242 -91
- package/src/index.ts +4 -4
- package/src/isr.ts +24 -6
- package/src/manifest.ts +675 -0
- package/src/og-image.ts +5 -5
- package/src/script.tsx +159 -36
- package/src/seo.ts +346 -15
- package/src/server.ts +10 -2
- package/src/ssg-plugin.ts +1211 -54
- package/src/types.ts +333 -10
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +171 -41
- package/lib/vite-plugin-E4BHYvYW.js +0 -855
package/src/vite-plugin.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { resolveConfig } from './config'
|
|
|
14
14
|
// Used in the dev-mode SSR catch handler to convert loader-thrown
|
|
15
15
|
// `redirect()` errors into real HTTP redirects (302/307/308).
|
|
16
16
|
import { getRedirectInfo } from '@pyreon/router'
|
|
17
|
+
import { matchPattern } from './entry-server'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Scan node_modules/@pyreon/ to discover all installed Pyreon packages.
|
|
@@ -46,7 +47,6 @@ function resolveNestedPackage(root: string, name: string): string | undefined {
|
|
|
46
47
|
if (existsSync(nested)) return nested
|
|
47
48
|
return undefined
|
|
48
49
|
}
|
|
49
|
-
import { matchPattern } from "./entry-server";
|
|
50
50
|
import { renderErrorOverlay } from "./error-overlay";
|
|
51
51
|
import {
|
|
52
52
|
generateMiddlewareModule,
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
scanRouteFiles,
|
|
55
55
|
scanRouteFilesWithExports,
|
|
56
56
|
} from "./fs-router";
|
|
57
|
+
import { expandRoutesForLocales } from "./i18n-routing";
|
|
57
58
|
import { render404Page } from "./not-found";
|
|
58
59
|
import { ssgPlugin } from "./ssg-plugin";
|
|
59
60
|
import type { ZeroConfig } from "./types";
|
|
@@ -67,6 +68,49 @@ const RESOLVED_VIRTUAL_MIDDLEWARE_ID = `\0${VIRTUAL_MIDDLEWARE_ID}`;
|
|
|
67
68
|
const VIRTUAL_API_ROUTES_ID = "virtual:zero/api-routes";
|
|
68
69
|
const RESOLVED_VIRTUAL_API_ROUTES_ID = `\0${VIRTUAL_API_ROUTES_ID}`;
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Per-plugin-instance storage for the user-supplied ZeroConfig. Lets
|
|
73
|
+
* downstream consumers (e.g. `@pyreon/zero-cli`'s `build` command, which
|
|
74
|
+
* loads the user's `vite.config.ts` and inspects its plugin list)
|
|
75
|
+
* recover the original config without us attaching internal state to
|
|
76
|
+
* the public Plugin object via an underscore-prefixed property.
|
|
77
|
+
*
|
|
78
|
+
* Exported via `getZeroPluginConfig(plugin)` so the WeakMap itself
|
|
79
|
+
* stays an implementation detail — callers can't enumerate or mutate
|
|
80
|
+
* the table, only read by Plugin identity.
|
|
81
|
+
*/
|
|
82
|
+
const zeroPluginConfigMap = new WeakMap<Plugin, ZeroConfig>();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retrieve the `ZeroConfig` that was passed to `zeroPlugin(userConfig)`
|
|
86
|
+
* when the plugin was created. Returns `undefined` if the argument
|
|
87
|
+
* isn't a recognized pyreon-zero main plugin instance.
|
|
88
|
+
*/
|
|
89
|
+
export function getZeroPluginConfig(plugin: Plugin): ZeroConfig | undefined {
|
|
90
|
+
return zeroPluginConfigMap.get(plugin);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detects `--port` / `--port=N` / `-p N` / `-p=N` in `process.argv`.
|
|
95
|
+
* Used by the plugin's `config()` hook to decide whether to apply the
|
|
96
|
+
* default port — when the CLI was invoked with `--port`, the plugin
|
|
97
|
+
* must skip its default so the CLI flag wins (see the comment at the
|
|
98
|
+
* port-handling block in `zeroPlugin()` for the full precedence model).
|
|
99
|
+
*
|
|
100
|
+
* Exported for testing only (the plugin uses it internally).
|
|
101
|
+
*
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
export function argvHasPortFlag(argv: readonly string[] = process.argv): boolean {
|
|
105
|
+
for (let i = 0; i < argv.length; i++) {
|
|
106
|
+
const a = argv[i];
|
|
107
|
+
if (a === "--port" || a === "-p") return true;
|
|
108
|
+
if (a !== undefined && (a.startsWith("--port=") || a.startsWith("-p=")))
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
70
114
|
/**
|
|
71
115
|
* Zero Vite plugin — adds file-based routing and zero-config conventions
|
|
72
116
|
* on top of @pyreon/vite-plugin.
|
|
@@ -85,10 +129,9 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
85
129
|
let routesDir: string;
|
|
86
130
|
let root: string;
|
|
87
131
|
|
|
88
|
-
const mainPlugin: Plugin
|
|
132
|
+
const mainPlugin: Plugin = {
|
|
89
133
|
name: "pyreon-zero",
|
|
90
134
|
enforce: "pre",
|
|
91
|
-
_zeroConfig: userConfig,
|
|
92
135
|
|
|
93
136
|
configResolved(resolvedConfig) {
|
|
94
137
|
root = resolvedConfig.root;
|
|
@@ -109,9 +152,30 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
109
152
|
// • lazy() for routes that only export `default` (best code splitting)
|
|
110
153
|
// • Direct mod.loader/.guard/.meta access for routes with metadata
|
|
111
154
|
// • No spurious IMPORT_IS_UNDEFINED warnings from Rolldown
|
|
112
|
-
const
|
|
155
|
+
const baseRoutes = await scanRouteFilesWithExports(routesDir, config.mode);
|
|
156
|
+
// PR H — fan routes into per-locale variants when `i18n` is
|
|
157
|
+
// configured. No-op when unset; identity-returns the input
|
|
158
|
+
// otherwise so existing apps see byte-identical output.
|
|
159
|
+
const routes = config.i18n
|
|
160
|
+
? expandRoutesForLocales(baseRoutes, config.i18n)
|
|
161
|
+
: baseRoutes;
|
|
162
|
+
// SSG mode: lazy() route splitting by default (parity with
|
|
163
|
+
// SSR/SPA). Opt-out via `ssg.splitChunks: false` for tiny
|
|
164
|
+
// sites that prefer single-chunk + instant navigation.
|
|
165
|
+
//
|
|
166
|
+
// Pre-2026-Q3: SSG was hardcoded to `staticImports: true`
|
|
167
|
+
// (bundle everything). Trade-off was instant post-hydration
|
|
168
|
+
// nav, but the initial bundle grew linearly with route
|
|
169
|
+
// count — a 50-route docs site shipped all 50 route
|
|
170
|
+
// components on first paint. Lazy splitting (now the
|
|
171
|
+
// default for SSG) fixes that: only the landing route +
|
|
172
|
+
// deps load up front, the rest fetch on navigation. See
|
|
173
|
+
// `ssg.splitChunks` JSDoc in types.ts for the crossover-
|
|
174
|
+
// point rationale.
|
|
175
|
+
const ssgSplitDisabled =
|
|
176
|
+
config.mode === "ssg" && config.ssg?.splitChunks === false;
|
|
113
177
|
return generateRouteModuleFromRoutes(routes, routesDir, {
|
|
114
|
-
staticImports:
|
|
178
|
+
staticImports: ssgSplitDisabled,
|
|
115
179
|
});
|
|
116
180
|
} catch (_err) {
|
|
117
181
|
return `export const routes = []`;
|
|
@@ -203,10 +267,10 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
203
267
|
renderSsr(server, root, req.originalUrl ?? pathname, pathname, webReq).then(
|
|
204
268
|
(result) => {
|
|
205
269
|
if (result === null) return next();
|
|
206
|
-
res.statusCode =
|
|
270
|
+
res.statusCode = result.status;
|
|
207
271
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
208
|
-
res.setHeader("Content-Length", Buffer.byteLength(result));
|
|
209
|
-
res.end(result);
|
|
272
|
+
res.setHeader("Content-Length", Buffer.byteLength(result.html));
|
|
273
|
+
res.end(result.html);
|
|
210
274
|
},
|
|
211
275
|
(err: unknown) => {
|
|
212
276
|
// Loader-thrown `redirect()` — convert to a real HTTP redirect
|
|
@@ -327,15 +391,15 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
327
391
|
// Discover all @pyreon/* packages installed in node_modules.
|
|
328
392
|
// The "bun" export condition points to TS source — esbuild's
|
|
329
393
|
// dep optimizer would compile them with the wrong JSX runtime.
|
|
330
|
-
const
|
|
331
|
-
const pyreonExclude = scanPyreonPackages(
|
|
394
|
+
const cwd = viteUserConfig.root ?? process.cwd()
|
|
395
|
+
const pyreonExclude = scanPyreonPackages(cwd)
|
|
332
396
|
|
|
333
397
|
// `@pyreon/runtime-server` is only imported by zero's dev SSR
|
|
334
398
|
// middleware and the production server entry — apps rarely list it
|
|
335
399
|
// as a direct dep. Resolve it to the copy nested under zero so
|
|
336
400
|
// `ssrLoadModule("@pyreon/runtime-server")` works uniformly.
|
|
337
401
|
const runtimeServerAlias = resolveNestedPackage(
|
|
338
|
-
|
|
402
|
+
cwd,
|
|
339
403
|
"@pyreon/runtime-server",
|
|
340
404
|
)
|
|
341
405
|
|
|
@@ -361,16 +425,51 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
361
425
|
optimizeDeps: {
|
|
362
426
|
exclude: pyreonExclude,
|
|
363
427
|
},
|
|
364
|
-
//
|
|
365
|
-
// `zero
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
// `
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
428
|
+
// Port handling — the zero-canonical default is 3000 (matches
|
|
429
|
+
// `zero dev` / `zero preview` / the runtime adapter, and
|
|
430
|
+
// matches Next.js / Remix / Astro convention).
|
|
431
|
+
//
|
|
432
|
+
// Apply the default UNLESS Vite's CLI was invoked with
|
|
433
|
+
// `--port`/`-p` (in which case the CLI flag must win — see
|
|
434
|
+
// memory: vite cli port doesnt override plugin). PR #579
|
|
435
|
+
// proved this empirically: returning `server: { port: 3000 }`
|
|
436
|
+
// unconditionally clobbered `vite --port 517N --strictPort`
|
|
437
|
+
// in the e2e playwright config and every webServer timed
|
|
438
|
+
// out. argv detection here lets the CLI win at the source.
|
|
439
|
+
//
|
|
440
|
+
// Precedence (CLI > user vite.config > zero({port}) > 3000):
|
|
441
|
+
// 1. `vite --port N` → argvHasPortFlag() === true → plugin
|
|
442
|
+
// omits `server.port` entirely → CLI value wins
|
|
443
|
+
// 2. User `vite.config.ts server: { port: N }` → user
|
|
444
|
+
// config beats plugin in Vite's merge order
|
|
445
|
+
// 3. `zero({ port: N })` → resolved into `config.port`
|
|
446
|
+
// 4. Default 3000 — when no other source set a port
|
|
447
|
+
//
|
|
448
|
+
// `process.argv` is populated by the time Vite invokes the
|
|
449
|
+
// plugin's config() hook (Vite calls plugins synchronously
|
|
450
|
+
// during CLI bootstrap before applying inline overrides).
|
|
451
|
+
...(userConfig.port === undefined && argvHasPortFlag()
|
|
452
|
+
? {}
|
|
453
|
+
: { server: { port: config.port } }),
|
|
454
|
+
// Propagate `zero({ base })` to Vite's `base` config — that's
|
|
455
|
+
// what controls asset URL rewriting in the built HTML/JS
|
|
456
|
+
// (`<script src="/blog/assets/…">`). Pre-fix this was a
|
|
457
|
+
// typed-but-unimplemented field: `__ZERO_BASE__` was defined
|
|
458
|
+
// as a Vite global but no consumer existed, AND Vite's own
|
|
459
|
+
// `base` had to be set manually in vite.config.ts. Setting
|
|
460
|
+
// it here makes `zero({ base: '/blog/' })` the canonical
|
|
461
|
+
// single-source-of-truth surface; the value flows through
|
|
462
|
+
// to (a) Vite's HTML/asset URL rewriter, (b) `createRouter`
|
|
463
|
+
// via `__ZERO_BASE__` in `startClient` / `createApp`, (c)
|
|
464
|
+
// the SSG entry's `createApp({ base })` call.
|
|
465
|
+
//
|
|
466
|
+
// Vite's config-merge semantics: plugin-returned config is
|
|
467
|
+
// the BASE; user's `vite.config.ts` top-level overrides.
|
|
468
|
+
// So a user who sets BOTH `zero({ base: '/blog/' })` AND
|
|
469
|
+
// `vite.config.base: '/foo/'` gets the latter — the user's
|
|
470
|
+
// explicit override wins. The default `/` is a no-op
|
|
471
|
+
// (matches Vite's default), so always-setting is safe.
|
|
472
|
+
base: config.base,
|
|
374
473
|
define: {
|
|
375
474
|
__ZERO_MODE__: JSON.stringify(config.mode),
|
|
376
475
|
__ZERO_BASE__: JSON.stringify(config.base),
|
|
@@ -379,6 +478,12 @@ export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin[] {
|
|
|
379
478
|
},
|
|
380
479
|
};
|
|
381
480
|
|
|
481
|
+
// Stash the original user config keyed by plugin identity so the CLI
|
|
482
|
+
// (which loads vite.config.ts and inspects the plugin list) can
|
|
483
|
+
// recover it via `getZeroPluginConfig(plugin)` without us hanging a
|
|
484
|
+
// `_`-prefixed property off the public Plugin object.
|
|
485
|
+
zeroPluginConfigMap.set(mainPlugin, userConfig);
|
|
486
|
+
|
|
382
487
|
// SSG mode auto-wires the static-site generation hook. Other modes get
|
|
383
488
|
// just the main plugin. The SSG plugin internally no-ops when
|
|
384
489
|
// `mode !== 'ssg'`, but skipping it entirely keeps the plugin chain
|
|
@@ -488,13 +593,24 @@ async function dispatchApiRoute(
|
|
|
488
593
|
}
|
|
489
594
|
|
|
490
595
|
/**
|
|
491
|
-
*
|
|
492
|
-
* Returns true if the 404 was handled (response sent), false otherwise.
|
|
596
|
+
* Static-page 404 fallback for apps WITHOUT `_404.tsx` in the routes tree.
|
|
493
597
|
*
|
|
494
|
-
*
|
|
495
|
-
* the
|
|
496
|
-
*
|
|
497
|
-
*
|
|
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
|
+
*
|
|
606
|
+
* Returns true if the 404 was handled (response sent), false if the path
|
|
607
|
+
* 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.
|
|
498
614
|
*/
|
|
499
615
|
async function handle404(
|
|
500
616
|
server: import("vite").ViteDevServer,
|
|
@@ -510,10 +626,11 @@ async function handle404(
|
|
|
510
626
|
return false; // Route matches — not a 404
|
|
511
627
|
}
|
|
512
628
|
|
|
513
|
-
// No route matched —
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
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.
|
|
517
634
|
const html = await render404Page(undefined);
|
|
518
635
|
|
|
519
636
|
res.statusCode = 404;
|
|
@@ -541,18 +658,12 @@ async function renderSsr(
|
|
|
541
658
|
originalUrl: string,
|
|
542
659
|
pathname: string,
|
|
543
660
|
req?: Request,
|
|
544
|
-
): Promise<string | null> {
|
|
545
|
-
// Pattern check FIRST — otherwise SSR would try (and likely crash) on
|
|
546
|
-
// asset paths that happened to accept text/html (e.g. curl-style).
|
|
661
|
+
): Promise<{ html: string; status: number } | null> {
|
|
547
662
|
const routesMod = await server.ssrLoadModule(VIRTUAL_ROUTES_ID);
|
|
548
663
|
const routes = routesMod.routes as Array<{
|
|
549
664
|
path?: string;
|
|
550
665
|
children?: unknown[];
|
|
551
666
|
}>;
|
|
552
|
-
const patterns = flattenRoutePatterns(routes);
|
|
553
|
-
if (!patterns.some((pattern) => matchPattern(pattern, pathname))) {
|
|
554
|
-
return null;
|
|
555
|
-
}
|
|
556
667
|
|
|
557
668
|
// Read + transform index.html (Vite injects the HMR client / JSX prelude).
|
|
558
669
|
let template = await readFile(join(root, "index.html"), "utf-8");
|
|
@@ -565,7 +676,7 @@ async function renderSsr(
|
|
|
565
676
|
// `@pyreon/runtime-server` isn't a direct dep of most apps, so zero's
|
|
566
677
|
// `config()` hook registers an alias that points it at the copy under
|
|
567
678
|
// zero's own `node_modules` — same path → same Vite module → same instance.
|
|
568
|
-
const [core,
|
|
679
|
+
const [core, _headPkg, headSsr, routerPkg, runtimeServer] = await Promise.all(
|
|
569
680
|
[
|
|
570
681
|
server.ssrLoadModule("@pyreon/core") as Promise<
|
|
571
682
|
typeof import("@pyreon/core")
|
|
@@ -620,8 +731,25 @@ async function renderSsr(
|
|
|
620
731
|
// `preload` loads lazy route components AND runs loaders for `pathname` so
|
|
621
732
|
// the synchronous render pass produces final HTML — no loading fallbacks,
|
|
622
733
|
// no `useLoaderData() === undefined`.
|
|
734
|
+
//
|
|
735
|
+
// M1.2 — Unmatched URLs no longer bail to a static 404 page. The router's
|
|
736
|
+
// `resolveRoute` (PR L5) walks the route tree and, if a parent layout has
|
|
737
|
+
// `notFoundComponent` AND the URL is under that layout's prefix, builds a
|
|
738
|
+
// synthetic chain `[...ancestorLayouts, syntheticLeaf]` with
|
|
739
|
+
// `isNotFound: true`. The render then produces 404 HTML INSIDE the
|
|
740
|
+
// layout's chrome. If the routes tree has no reachable `notFoundComponent`,
|
|
741
|
+
// `matched` stays empty — fall through to `handle404` for the static
|
|
742
|
+
// fallback (preserves backward compat for apps without `_404.tsx`).
|
|
623
743
|
await routerInst.preload(pathname, req);
|
|
624
744
|
|
|
745
|
+
const resolved = routerInst.currentRoute() as
|
|
746
|
+
| { matched?: unknown[]; isNotFound?: boolean }
|
|
747
|
+
| undefined;
|
|
748
|
+
if (!resolved?.matched || resolved.matched.length === 0) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
const status = resolved.isNotFound === true ? 404 : 200;
|
|
752
|
+
|
|
625
753
|
return runtimeServer.runWithRequestContext(async () => {
|
|
626
754
|
const app = core.h(App as Parameters<typeof core.h>[0], null);
|
|
627
755
|
|
|
@@ -630,14 +758,16 @@ async function renderSsr(
|
|
|
630
758
|
routerInst as Parameters<typeof routerPkg.serializeLoaderData>[0],
|
|
631
759
|
);
|
|
632
760
|
const hasData = loaderData && Object.keys(loaderData).length > 0;
|
|
761
|
+
// M2.2 — safe serializer (parity with production handler / SSG entry).
|
|
633
762
|
const loaderScript = hasData
|
|
634
|
-
? `<script>window.__PYREON_LOADER_DATA__=${
|
|
763
|
+
? `<script>window.__PYREON_LOADER_DATA__=${routerPkg.stringifyLoaderData(loaderData)}</script>`
|
|
635
764
|
: "";
|
|
636
765
|
|
|
637
|
-
|
|
766
|
+
const html = template
|
|
638
767
|
.replace("<!--pyreon-head-->", head)
|
|
639
768
|
.replace("<!--pyreon-app-->", appHtml)
|
|
640
769
|
.replace("<!--pyreon-scripts-->", loaderScript);
|
|
770
|
+
return { html, status };
|
|
641
771
|
});
|
|
642
772
|
}
|
|
643
773
|
|