@pyreon/zero 0.15.0 → 0.16.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 +275 -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 +634 -182
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +575 -72
- package/lib/vite-plugin-xjWZwudX.js +2454 -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 +301 -10
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +108 -30
- package/lib/vite-plugin-E4BHYvYW.js +0 -855
|
@@ -1,855 +0,0 @@
|
|
|
1
|
-
import { a as generateRouteModuleFromRoutes, c as scanRouteFilesWithExports, l as __exportAll, o as parseFileRoutes, r as generateMiddlewareModule, s as scanRouteFiles } from "./fs-router-ZebyutPa.js";
|
|
2
|
-
import { i as matchApiRoute, n as createApiMiddleware, r as generateApiRouteModule } from "./api-routes-DANluJic.js";
|
|
3
|
-
import { Fragment, h } from "@pyreon/core";
|
|
4
|
-
import { HeadProvider } from "@pyreon/head";
|
|
5
|
-
import { RouterProvider, RouterView, createRouter, getRedirectInfo } from "@pyreon/router";
|
|
6
|
-
import { createHandler } from "@pyreon/server";
|
|
7
|
-
import { renderToString } from "@pyreon/runtime-server";
|
|
8
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
|
-
import { Readable } from "node:stream";
|
|
12
|
-
import { pathToFileURL } from "node:url";
|
|
13
|
-
|
|
14
|
-
//#region src/app.ts
|
|
15
|
-
/**
|
|
16
|
-
* Create a full Zero app — assembles router, head provider, and root layout.
|
|
17
|
-
*
|
|
18
|
-
* Used internally by entry-server and entry-client.
|
|
19
|
-
*/
|
|
20
|
-
function createApp(options) {
|
|
21
|
-
const router = createRouter({
|
|
22
|
-
routes: options.routes,
|
|
23
|
-
mode: options.routerMode ?? "history",
|
|
24
|
-
...options.url ? { url: options.url } : {},
|
|
25
|
-
scrollBehavior: "top"
|
|
26
|
-
});
|
|
27
|
-
const hasLayoutInRoutes = options.layout !== void 0 && options.routes.some((r) => r.component === options.layout);
|
|
28
|
-
if (hasLayoutInRoutes && process.env.NODE_ENV !== "production") console.warn("[Pyreon] `createApp({ layout })` was passed a component that is ALSO a parent route in the matched chain (likely an fs-router `_layout.tsx`). The explicit `layout` option is being ignored to prevent double-mount. Remove the `layout` argument from `createApp`/`startClient` — the fs-router-emitted route handles it.");
|
|
29
|
-
const Layout = hasLayoutInRoutes ? DefaultLayout : options.layout ?? DefaultLayout;
|
|
30
|
-
function App() {
|
|
31
|
-
return h(HeadProvider, null, h(RouterProvider, { router }, h(Layout, null, h(RouterView, null))));
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
App,
|
|
35
|
-
router
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function DefaultLayout(props) {
|
|
39
|
-
return h(Fragment, null, ...Array.isArray(props.children) ? props.children : [props.children]);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
//#endregion
|
|
43
|
-
//#region src/not-found.ts
|
|
44
|
-
const DEFAULT_404_BODY = "<h1>404 — Not Found</h1><p>The page you requested does not exist.</p>";
|
|
45
|
-
/**
|
|
46
|
-
* Render a 404 component to a full HTML string.
|
|
47
|
-
* If no component is provided, returns a default 404 page.
|
|
48
|
-
*/
|
|
49
|
-
async function render404Page(component, template) {
|
|
50
|
-
let body;
|
|
51
|
-
if (component) body = await renderToString(h(component, null));
|
|
52
|
-
else body = DEFAULT_404_BODY;
|
|
53
|
-
if (template?.includes("<!--pyreon-app-->")) return template.replace("<!--pyreon-app-->", body);
|
|
54
|
-
return `<!DOCTYPE html>
|
|
55
|
-
<html lang="en">
|
|
56
|
-
<head>
|
|
57
|
-
<meta charset="UTF-8">
|
|
58
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
59
|
-
<title>404 — Not Found</title>
|
|
60
|
-
</head>
|
|
61
|
-
<body>
|
|
62
|
-
${body}
|
|
63
|
-
</body>
|
|
64
|
-
</html>`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/entry-server.ts
|
|
69
|
-
/**
|
|
70
|
-
* Create a middleware that dispatches per-route middleware based on URL pattern matching.
|
|
71
|
-
*/
|
|
72
|
-
function createRouteMiddlewareDispatcher(entries) {
|
|
73
|
-
return async (ctx) => {
|
|
74
|
-
for (const entry of entries) if (matchPattern(entry.pattern, ctx.path)) {
|
|
75
|
-
const mw = Array.isArray(entry.middleware) ? entry.middleware : [entry.middleware];
|
|
76
|
-
for (const fn of mw) {
|
|
77
|
-
const result = await fn(ctx);
|
|
78
|
-
if (result) return result;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* URL pattern matcher supporting :param and :param* segments.
|
|
85
|
-
*
|
|
86
|
-
* Rules:
|
|
87
|
-
* - Static segments must match exactly
|
|
88
|
-
* - `:param` matches a single path segment
|
|
89
|
-
* - `:param*` matches all remaining segments (must be last, and path must
|
|
90
|
-
* have matched all preceding segments)
|
|
91
|
-
* - Path length must match pattern length (unless catch-all)
|
|
92
|
-
*/
|
|
93
|
-
function matchPattern(pattern, path) {
|
|
94
|
-
const patternParts = pattern.split("/").filter(Boolean);
|
|
95
|
-
const pathParts = path.split("/").filter(Boolean);
|
|
96
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
97
|
-
const pp = patternParts[i];
|
|
98
|
-
if (pp.endsWith("*")) return i <= pathParts.length;
|
|
99
|
-
if (i >= pathParts.length) return false;
|
|
100
|
-
if (pp.startsWith(":")) continue;
|
|
101
|
-
if (pp !== pathParts[i]) return false;
|
|
102
|
-
}
|
|
103
|
-
return patternParts.length === pathParts.length;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Create the SSR request handler for production.
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* import { routes } from "virtual:zero/routes"
|
|
110
|
-
* import { routeMiddleware } from "virtual:zero/route-middleware"
|
|
111
|
-
* import { createServer } from "@pyreon/zero"
|
|
112
|
-
*
|
|
113
|
-
* export default createServer({ routes, routeMiddleware, apiRoutes })
|
|
114
|
-
*/
|
|
115
|
-
function createServer(options) {
|
|
116
|
-
const config = options.config ?? {};
|
|
117
|
-
const allMiddleware = [];
|
|
118
|
-
if (options.apiRoutes?.length) allMiddleware.push(createApiMiddleware(options.apiRoutes));
|
|
119
|
-
if (options.routeMiddleware?.length) allMiddleware.push(createRouteMiddlewareDispatcher(options.routeMiddleware));
|
|
120
|
-
allMiddleware.push(...config.middleware ?? []);
|
|
121
|
-
allMiddleware.push(...options.middleware ?? []);
|
|
122
|
-
const { App } = createApp({
|
|
123
|
-
routes: options.routes,
|
|
124
|
-
routerMode: "history"
|
|
125
|
-
});
|
|
126
|
-
const handler = createHandler({
|
|
127
|
-
App,
|
|
128
|
-
routes: options.routes,
|
|
129
|
-
middleware: allMiddleware,
|
|
130
|
-
mode: config.ssr?.mode ?? "string",
|
|
131
|
-
...options.template ? { template: options.template } : {},
|
|
132
|
-
...options.clientEntry ? { clientEntry: options.clientEntry } : {}
|
|
133
|
-
});
|
|
134
|
-
if (!options.notFoundComponent) return handler;
|
|
135
|
-
const NotFound = options.notFoundComponent;
|
|
136
|
-
const routePatterns = flattenRoutePatterns$1(options.routes);
|
|
137
|
-
return async (req) => {
|
|
138
|
-
const pathname = new URL(req.url).pathname;
|
|
139
|
-
if (!routePatterns.some((pattern) => matchPattern(pattern, pathname))) {
|
|
140
|
-
const fullHtml = await render404Page(NotFound, options.template);
|
|
141
|
-
return new Response(fullHtml, {
|
|
142
|
-
status: 404,
|
|
143
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
return handler(req);
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
/** Extract all URL patterns from a nested route tree. */
|
|
150
|
-
function flattenRoutePatterns$1(routes, prefix = "") {
|
|
151
|
-
const patterns = [];
|
|
152
|
-
for (const route of routes) {
|
|
153
|
-
const fullPath = route.path === "/" && prefix ? prefix : `${prefix}${route.path}`;
|
|
154
|
-
patterns.push(fullPath);
|
|
155
|
-
if (route.children) patterns.push(...flattenRoutePatterns$1(route.children, fullPath));
|
|
156
|
-
}
|
|
157
|
-
return patterns;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
//#endregion
|
|
161
|
-
//#region src/config.ts
|
|
162
|
-
/**
|
|
163
|
-
* Define a Zero configuration.
|
|
164
|
-
* Used in `zero.config.ts` at the project root.
|
|
165
|
-
*
|
|
166
|
-
* @example
|
|
167
|
-
* import { defineConfig } from "@pyreon/zero/config"
|
|
168
|
-
*
|
|
169
|
-
* export default defineConfig({
|
|
170
|
-
* mode: "ssr",
|
|
171
|
-
* ssr: { mode: "stream" },
|
|
172
|
-
* port: 3000,
|
|
173
|
-
* })
|
|
174
|
-
*/
|
|
175
|
-
function defineConfig(config) {
|
|
176
|
-
return config;
|
|
177
|
-
}
|
|
178
|
-
/** Merge user config with defaults. */
|
|
179
|
-
function resolveConfig(userConfig = {}) {
|
|
180
|
-
return {
|
|
181
|
-
mode: "ssr",
|
|
182
|
-
base: "/",
|
|
183
|
-
port: 3e3,
|
|
184
|
-
adapter: "node",
|
|
185
|
-
...userConfig,
|
|
186
|
-
ssr: {
|
|
187
|
-
mode: "string",
|
|
188
|
-
...userConfig.ssr
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
//#endregion
|
|
194
|
-
//#region src/error-overlay.ts
|
|
195
|
-
/**
|
|
196
|
-
* Dev-only error overlay for SSR/loader errors.
|
|
197
|
-
* Renders a styled HTML page with the error stack trace.
|
|
198
|
-
*/
|
|
199
|
-
function renderErrorOverlay(error) {
|
|
200
|
-
return `<!DOCTYPE html>
|
|
201
|
-
<html lang="en">
|
|
202
|
-
<head>
|
|
203
|
-
<meta charset="UTF-8">
|
|
204
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
205
|
-
<title>SSR Error — Pyreon Zero</title>
|
|
206
|
-
<style>
|
|
207
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
208
|
-
body {
|
|
209
|
-
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
|
|
210
|
-
background: #1a1a2e;
|
|
211
|
-
color: #e0e0e0;
|
|
212
|
-
min-height: 100vh;
|
|
213
|
-
padding: 2rem;
|
|
214
|
-
}
|
|
215
|
-
.overlay {
|
|
216
|
-
max-width: 900px;
|
|
217
|
-
margin: 0 auto;
|
|
218
|
-
}
|
|
219
|
-
.header {
|
|
220
|
-
display: flex;
|
|
221
|
-
align-items: center;
|
|
222
|
-
gap: 0.75rem;
|
|
223
|
-
margin-bottom: 1.5rem;
|
|
224
|
-
}
|
|
225
|
-
.badge {
|
|
226
|
-
background: #e74c3c;
|
|
227
|
-
color: white;
|
|
228
|
-
padding: 0.25rem 0.75rem;
|
|
229
|
-
border-radius: 4px;
|
|
230
|
-
font-size: 0.75rem;
|
|
231
|
-
font-weight: 600;
|
|
232
|
-
text-transform: uppercase;
|
|
233
|
-
letter-spacing: 0.05em;
|
|
234
|
-
}
|
|
235
|
-
.label {
|
|
236
|
-
color: #888;
|
|
237
|
-
font-size: 0.85rem;
|
|
238
|
-
}
|
|
239
|
-
.message {
|
|
240
|
-
font-size: 1.25rem;
|
|
241
|
-
color: #ff6b6b;
|
|
242
|
-
margin-bottom: 1.5rem;
|
|
243
|
-
line-height: 1.5;
|
|
244
|
-
word-break: break-word;
|
|
245
|
-
}
|
|
246
|
-
.stack {
|
|
247
|
-
background: #16213e;
|
|
248
|
-
border: 1px solid #2a2a4a;
|
|
249
|
-
border-radius: 8px;
|
|
250
|
-
padding: 1.25rem;
|
|
251
|
-
overflow-x: auto;
|
|
252
|
-
font-size: 0.8rem;
|
|
253
|
-
line-height: 1.7;
|
|
254
|
-
white-space: pre-wrap;
|
|
255
|
-
word-break: break-all;
|
|
256
|
-
}
|
|
257
|
-
.stack .at { color: #888; }
|
|
258
|
-
.stack .file { color: #4ecdc4; }
|
|
259
|
-
.hint {
|
|
260
|
-
margin-top: 1.5rem;
|
|
261
|
-
padding: 1rem;
|
|
262
|
-
background: #1e2a45;
|
|
263
|
-
border-radius: 6px;
|
|
264
|
-
border-left: 3px solid #3498db;
|
|
265
|
-
font-size: 0.8rem;
|
|
266
|
-
color: #aaa;
|
|
267
|
-
line-height: 1.5;
|
|
268
|
-
}
|
|
269
|
-
</style>
|
|
270
|
-
</head>
|
|
271
|
-
<body>
|
|
272
|
-
<div class="overlay">
|
|
273
|
-
<div class="header">
|
|
274
|
-
<span class="badge">SSR Error</span>
|
|
275
|
-
<span class="label">Pyreon Zero — Dev Mode</span>
|
|
276
|
-
</div>
|
|
277
|
-
<div class="message">${escapeHtml(error.message || "Unknown error")}</div>
|
|
278
|
-
<pre class="stack">${formatStack(escapeHtml(error.stack || ""))}</pre>
|
|
279
|
-
<div class="hint">
|
|
280
|
-
This error occurred during server-side rendering. Check the terminal for
|
|
281
|
-
the full stack trace. This overlay is only shown in development.
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
</body>
|
|
285
|
-
</html>`;
|
|
286
|
-
}
|
|
287
|
-
function escapeHtml(str) {
|
|
288
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
289
|
-
}
|
|
290
|
-
function formatStack(stack) {
|
|
291
|
-
return stack.split("\n").map((line) => {
|
|
292
|
-
if (line.includes("at ")) {
|
|
293
|
-
const fileMatch = line.match(/\(([^)]+)\)/);
|
|
294
|
-
if (fileMatch) return line.replace(fileMatch[0], `(<span class="file">${fileMatch[1]}</span>)`);
|
|
295
|
-
}
|
|
296
|
-
return line;
|
|
297
|
-
}).join("\n");
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
//#endregion
|
|
301
|
-
//#region src/ssg-plugin.ts
|
|
302
|
-
/**
|
|
303
|
-
* SSG (Static Site Generation) build hook for `@pyreon/zero`.
|
|
304
|
-
*
|
|
305
|
-
* Activates when `mode: "ssg"` is set in zero's config. After Vite's client
|
|
306
|
-
* build finishes, this plugin:
|
|
307
|
-
*
|
|
308
|
-
* 1. Triggers a programmatic SSR build via Vite's `build()` API, producing
|
|
309
|
-
* a server bundle in `dist/.zero-ssg-server/` from a synthetic entry
|
|
310
|
-
* that imports `virtual:zero/routes` and `createServer`.
|
|
311
|
-
* 2. Loads the built handler with dynamic `import()`.
|
|
312
|
-
* 3. Resolves the path list from `config.ssg.paths` (string[], async fn,
|
|
313
|
-
* or auto-detected from the static-only routes in the route tree).
|
|
314
|
-
* 4. Calls `prerender()` from `@pyreon/server` to render each path.
|
|
315
|
-
* 5. Cleans up the temporary SSR build directory.
|
|
316
|
-
*
|
|
317
|
-
* Before this PR, `mode: "ssg"` and `ssg.paths` were typed in
|
|
318
|
-
* `types.ts` but had no runtime implementation — the plugin file had zero
|
|
319
|
-
* Rollup build hooks. Apps configured for SSG silently shipped a bare SPA
|
|
320
|
-
* shell with no per-route HTML files, which broke direct-URL deploys to
|
|
321
|
-
* static hosts (no `dist/<path>/index.html`, every URL falls back to the
|
|
322
|
-
* SPA index).
|
|
323
|
-
*/
|
|
324
|
-
const SSG_BUILD_FLAG = "PYREON_ZERO_SSG_INNER_BUILD";
|
|
325
|
-
const SSR_ENTRY_SOURCE = `
|
|
326
|
-
import { routes } from "virtual:zero/routes"
|
|
327
|
-
import { h } from "@pyreon/core"
|
|
328
|
-
import { renderWithHead } from "@pyreon/head/ssr"
|
|
329
|
-
import { serializeLoaderData } from "@pyreon/router"
|
|
330
|
-
import { runWithRequestContext } from "@pyreon/runtime-server"
|
|
331
|
-
import { createApp } from "@pyreon/zero/server"
|
|
332
|
-
|
|
333
|
-
export default async function renderPath(path) {
|
|
334
|
-
const { App, router } = createApp({
|
|
335
|
-
routes,
|
|
336
|
-
routerMode: "history",
|
|
337
|
-
url: path,
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
await router.preload(path)
|
|
341
|
-
|
|
342
|
-
return runWithRequestContext(async () => {
|
|
343
|
-
const app = h(App, null)
|
|
344
|
-
const { html: appHtml, head } = await renderWithHead(app)
|
|
345
|
-
const loaderData = serializeLoaderData(router)
|
|
346
|
-
const hasData = loaderData && Object.keys(loaderData).length > 0
|
|
347
|
-
const loaderScript = hasData
|
|
348
|
-
? \`<script>window.__PYREON_LOADER_DATA__=\${JSON.stringify(loaderData).replace(/<\\//g, "<\\\\/")}<\/script>\`
|
|
349
|
-
: ""
|
|
350
|
-
return { appHtml, head, loaderScript }
|
|
351
|
-
})
|
|
352
|
-
}
|
|
353
|
-
`.trimStart();
|
|
354
|
-
const SSR_ENTRY_FILENAME = "__pyreon-zero-ssg-entry.js";
|
|
355
|
-
/**
|
|
356
|
-
* Auto-detect static paths from the route tree. A "static" path is one with
|
|
357
|
-
* NO dynamic segments (`[id]`, `[...rest]`). Dynamic routes are skipped
|
|
358
|
-
* because we can't enumerate their values at build time without a
|
|
359
|
-
* `getStaticPaths`-style API.
|
|
360
|
-
*/
|
|
361
|
-
async function autoDetectStaticPaths(routesDir) {
|
|
362
|
-
if (!existsSync(routesDir)) return ["/"];
|
|
363
|
-
const fileRoutes = parseFileRoutes(await scanRouteFiles(routesDir));
|
|
364
|
-
const out = [];
|
|
365
|
-
for (const r of fileRoutes) {
|
|
366
|
-
if (r.isLayout || r.isError || r.isLoading || r.isNotFound) continue;
|
|
367
|
-
const path = r.urlPath;
|
|
368
|
-
if (!path) continue;
|
|
369
|
-
if (/[:*]/.test(path)) continue;
|
|
370
|
-
out.push(path);
|
|
371
|
-
}
|
|
372
|
-
return out.length > 0 ? out : ["/"];
|
|
373
|
-
}
|
|
374
|
-
async function resolvePaths(config, routesDir) {
|
|
375
|
-
const explicit = config.ssg?.paths;
|
|
376
|
-
if (typeof explicit === "function") {
|
|
377
|
-
const result = await explicit();
|
|
378
|
-
return Array.isArray(result) ? result : [];
|
|
379
|
-
}
|
|
380
|
-
if (Array.isArray(explicit)) return explicit;
|
|
381
|
-
return autoDetectStaticPaths(routesDir);
|
|
382
|
-
}
|
|
383
|
-
function resolveOutputPath(distDir, path) {
|
|
384
|
-
if (path === "/") return join(distDir, "index.html");
|
|
385
|
-
if (path.endsWith(".html")) return join(distDir, path);
|
|
386
|
-
return join(distDir, path, "index.html");
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Plugin that performs SSG when `mode: "ssg"` is configured. Wires into
|
|
390
|
-
* Vite's `closeBundle` hook so it runs once after the main client build
|
|
391
|
-
* completes. The recursive SSR sub-build is gated by an env flag.
|
|
392
|
-
*/
|
|
393
|
-
function ssgPlugin(userConfig = {}) {
|
|
394
|
-
const config = resolveConfig(userConfig);
|
|
395
|
-
let root = "";
|
|
396
|
-
let distDir = "";
|
|
397
|
-
const isInnerBuild = process.env[SSG_BUILD_FLAG] === "1";
|
|
398
|
-
return {
|
|
399
|
-
name: "pyreon-zero-ssg",
|
|
400
|
-
apply: "build",
|
|
401
|
-
enforce: "post",
|
|
402
|
-
configResolved(resolved) {
|
|
403
|
-
root = resolved.root;
|
|
404
|
-
distDir = resolve(root, resolved.build.outDir);
|
|
405
|
-
},
|
|
406
|
-
async closeBundle() {
|
|
407
|
-
if (config.mode !== "ssg") return;
|
|
408
|
-
if (isInnerBuild) return;
|
|
409
|
-
const ssrOutDir = join(distDir, ".zero-ssg-server");
|
|
410
|
-
const indexHtmlPath = join(distDir, "index.html");
|
|
411
|
-
if (!existsSync(indexHtmlPath)) {
|
|
412
|
-
console.warn(`[zero:ssg] Skipping SSG — ${indexHtmlPath} not found. Did the client build complete?`);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
const entryPath = join(root, SSR_ENTRY_FILENAME);
|
|
416
|
-
await writeFile(entryPath, SSR_ENTRY_SOURCE, "utf-8");
|
|
417
|
-
const { build } = await import("vite");
|
|
418
|
-
process.env[SSG_BUILD_FLAG] = "1";
|
|
419
|
-
try {
|
|
420
|
-
const [{ zeroPlugin }, pyreonModule] = await Promise.all([Promise.resolve().then(() => vite_plugin_exports), import("@pyreon/vite-plugin")]);
|
|
421
|
-
const pyreon = pyreonModule.default;
|
|
422
|
-
await build({
|
|
423
|
-
root,
|
|
424
|
-
mode: "production",
|
|
425
|
-
logLevel: "error",
|
|
426
|
-
configFile: false,
|
|
427
|
-
publicDir: false,
|
|
428
|
-
plugins: [pyreon(), zeroPlugin(userConfig)],
|
|
429
|
-
resolve: { conditions: ["bun"] },
|
|
430
|
-
build: {
|
|
431
|
-
ssr: entryPath,
|
|
432
|
-
outDir: ssrOutDir,
|
|
433
|
-
emptyOutDir: true,
|
|
434
|
-
target: "esnext",
|
|
435
|
-
rollupOptions: {
|
|
436
|
-
input: entryPath,
|
|
437
|
-
output: {
|
|
438
|
-
format: "es",
|
|
439
|
-
entryFileNames: "entry-server.mjs"
|
|
440
|
-
},
|
|
441
|
-
external: [/^node:/]
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
} finally {
|
|
446
|
-
delete process.env[SSG_BUILD_FLAG];
|
|
447
|
-
try {
|
|
448
|
-
await rm(entryPath, { force: true });
|
|
449
|
-
} catch {}
|
|
450
|
-
}
|
|
451
|
-
const handlerPath = join(ssrOutDir, "entry-server.mjs");
|
|
452
|
-
if (!existsSync(handlerPath)) {
|
|
453
|
-
console.warn(`[zero:ssg] SSR build did not produce ${handlerPath} — skipping prerender`);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
const renderPath = (await import(
|
|
457
|
-
/* @vite-ignore */
|
|
458
|
-
pathToFileURL(handlerPath).href
|
|
459
|
-
)).default;
|
|
460
|
-
const template = await readFile(indexHtmlPath, "utf-8");
|
|
461
|
-
const paths = await resolvePaths(config, join(root, "src", "routes"));
|
|
462
|
-
if (paths.length === 0) {
|
|
463
|
-
console.warn("[zero:ssg] No static paths to prerender — set ssg.paths in zero config");
|
|
464
|
-
await rm(ssrOutDir, {
|
|
465
|
-
recursive: true,
|
|
466
|
-
force: true
|
|
467
|
-
});
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
let pages = 0;
|
|
471
|
-
const errors = [];
|
|
472
|
-
const start = Date.now();
|
|
473
|
-
for (const p of paths) try {
|
|
474
|
-
const result = await Promise.race([renderPath(p), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`Prerender timeout for "${p}" (30s)`)), 3e4))]);
|
|
475
|
-
let html = template;
|
|
476
|
-
if (html.includes("<!--pyreon-head-->")) html = html.replace("<!--pyreon-head-->", result.head);
|
|
477
|
-
else if (result.head) html = html.replace("</head>", `${result.head}</head>`);
|
|
478
|
-
if (html.includes("<!--pyreon-app-->")) html = html.replace("<!--pyreon-app-->", result.appHtml);
|
|
479
|
-
else if (result.appHtml) {
|
|
480
|
-
const appDivMatch = html.match(/<div\s+id=["']app["']\s*>([\s\S]*?)<\/div>/);
|
|
481
|
-
if (appDivMatch) html = html.replace(appDivMatch[0], `<div id="app">${result.appHtml}</div>`);
|
|
482
|
-
else html = html.replace("</body>", `<div id="app">${result.appHtml}</div></body>`);
|
|
483
|
-
}
|
|
484
|
-
if (html.includes("<!--pyreon-scripts-->")) html = html.replace("<!--pyreon-scripts-->", result.loaderScript);
|
|
485
|
-
else if (result.loaderScript) html = html.replace("</body>", `${result.loaderScript}</body>`);
|
|
486
|
-
const filePath = resolveOutputPath(distDir, p);
|
|
487
|
-
const resolvedOut = resolve(distDir);
|
|
488
|
-
if (!resolve(filePath).startsWith(resolvedOut)) {
|
|
489
|
-
errors.push({
|
|
490
|
-
path: p,
|
|
491
|
-
error: /* @__PURE__ */ new Error(`Path traversal detected: "${p}"`)
|
|
492
|
-
});
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
496
|
-
await writeFile(filePath, html, "utf-8");
|
|
497
|
-
pages++;
|
|
498
|
-
} catch (error) {
|
|
499
|
-
errors.push({
|
|
500
|
-
path: p,
|
|
501
|
-
error
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
await rm(ssrOutDir, {
|
|
505
|
-
recursive: true,
|
|
506
|
-
force: true
|
|
507
|
-
});
|
|
508
|
-
const elapsed = Date.now() - start;
|
|
509
|
-
console.log(`[zero:ssg] Prerendered ${pages} page(s) in ${elapsed}ms` + (errors.length > 0 ? ` (${errors.length} error(s))` : ""));
|
|
510
|
-
for (const { path: errPath, error } of errors) console.error(`[zero:ssg] Failed to prerender "${errPath}":`, error);
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
//#endregion
|
|
516
|
-
//#region src/vite-plugin.ts
|
|
517
|
-
var vite_plugin_exports = /* @__PURE__ */ __exportAll({ zeroPlugin: () => zeroPlugin });
|
|
518
|
-
/**
|
|
519
|
-
* Scan node_modules/@pyreon/ to discover all installed Pyreon packages.
|
|
520
|
-
* Returns package names to exclude from Vite's dep optimizer.
|
|
521
|
-
*/
|
|
522
|
-
function scanPyreonPackages(root) {
|
|
523
|
-
const pyreonDir = join(root, "node_modules", "@pyreon");
|
|
524
|
-
if (!existsSync(pyreonDir)) return [];
|
|
525
|
-
try {
|
|
526
|
-
return readdirSync(pyreonDir).filter((name) => !name.startsWith(".")).map((name) => `@pyreon/${name}`);
|
|
527
|
-
} catch {
|
|
528
|
-
return [];
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Resolve a package that isn't at the app's top-level `node_modules` but is
|
|
533
|
-
* nested under another `@pyreon/*` package. Used to alias `@pyreon/runtime-server`
|
|
534
|
-
* to the copy under `node_modules/@pyreon/zero/node_modules/@pyreon/runtime-server`
|
|
535
|
-
* so `ssrLoadModule` works without requiring the app to declare it as a
|
|
536
|
-
* direct dep.
|
|
537
|
-
*/
|
|
538
|
-
function resolveNestedPackage(root, name) {
|
|
539
|
-
const direct = join(root, "node_modules", name);
|
|
540
|
-
if (existsSync(direct)) return direct;
|
|
541
|
-
const nested = join(root, "node_modules", "@pyreon", "zero", "node_modules", name);
|
|
542
|
-
if (existsSync(nested)) return nested;
|
|
543
|
-
}
|
|
544
|
-
const VIRTUAL_ROUTES_ID = "virtual:zero/routes";
|
|
545
|
-
const RESOLVED_VIRTUAL_ROUTES_ID = `\0${VIRTUAL_ROUTES_ID}`;
|
|
546
|
-
const VIRTUAL_MIDDLEWARE_ID = "virtual:zero/route-middleware";
|
|
547
|
-
const RESOLVED_VIRTUAL_MIDDLEWARE_ID = `\0${VIRTUAL_MIDDLEWARE_ID}`;
|
|
548
|
-
const VIRTUAL_API_ROUTES_ID = "virtual:zero/api-routes";
|
|
549
|
-
const RESOLVED_VIRTUAL_API_ROUTES_ID = `\0${VIRTUAL_API_ROUTES_ID}`;
|
|
550
|
-
/**
|
|
551
|
-
* Zero Vite plugin — adds file-based routing and zero-config conventions
|
|
552
|
-
* on top of @pyreon/vite-plugin.
|
|
553
|
-
*
|
|
554
|
-
* @example
|
|
555
|
-
* // vite.config.ts
|
|
556
|
-
* import pyreon from "@pyreon/vite-plugin"
|
|
557
|
-
* import zero from "@pyreon/zero"
|
|
558
|
-
*
|
|
559
|
-
* export default {
|
|
560
|
-
* plugins: [pyreon(), zero()],
|
|
561
|
-
* }
|
|
562
|
-
*/
|
|
563
|
-
function zeroPlugin(userConfig = {}) {
|
|
564
|
-
const config = resolveConfig(userConfig);
|
|
565
|
-
let routesDir;
|
|
566
|
-
let root;
|
|
567
|
-
const mainPlugin = {
|
|
568
|
-
name: "pyreon-zero",
|
|
569
|
-
enforce: "pre",
|
|
570
|
-
_zeroConfig: userConfig,
|
|
571
|
-
configResolved(resolvedConfig) {
|
|
572
|
-
root = resolvedConfig.root;
|
|
573
|
-
routesDir = `${root}/src/routes`;
|
|
574
|
-
},
|
|
575
|
-
resolveId(id) {
|
|
576
|
-
if (id === VIRTUAL_ROUTES_ID) return RESOLVED_VIRTUAL_ROUTES_ID;
|
|
577
|
-
if (id === VIRTUAL_MIDDLEWARE_ID) return RESOLVED_VIRTUAL_MIDDLEWARE_ID;
|
|
578
|
-
if (id === VIRTUAL_API_ROUTES_ID) return RESOLVED_VIRTUAL_API_ROUTES_ID;
|
|
579
|
-
},
|
|
580
|
-
async load(id) {
|
|
581
|
-
if (id === RESOLVED_VIRTUAL_ROUTES_ID) try {
|
|
582
|
-
return generateRouteModuleFromRoutes(await scanRouteFilesWithExports(routesDir, config.mode), routesDir, { staticImports: config.mode === "ssg" });
|
|
583
|
-
} catch (_err) {
|
|
584
|
-
return `export const routes = []`;
|
|
585
|
-
}
|
|
586
|
-
if (id === RESOLVED_VIRTUAL_MIDDLEWARE_ID) try {
|
|
587
|
-
return generateMiddlewareModule(await scanRouteFiles(routesDir), routesDir);
|
|
588
|
-
} catch (_err) {
|
|
589
|
-
return `export const routeMiddleware = []`;
|
|
590
|
-
}
|
|
591
|
-
if (id === RESOLVED_VIRTUAL_API_ROUTES_ID) try {
|
|
592
|
-
return generateApiRouteModule(await scanRouteFiles(routesDir), routesDir);
|
|
593
|
-
} catch (_err) {
|
|
594
|
-
return `export const apiRoutes = []`;
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
configureServer(server) {
|
|
598
|
-
server.middlewares.use((req, res, next) => {
|
|
599
|
-
const pathname = req.url?.split("?")[0] ?? "/";
|
|
600
|
-
if (pathname.startsWith("/@") || pathname.startsWith("/__")) return next();
|
|
601
|
-
if (/\.\w+$/.test(pathname)) return next();
|
|
602
|
-
dispatchApiRoute(server, req, res).then((handled) => {
|
|
603
|
-
if (!handled) next();
|
|
604
|
-
}, (err) => {
|
|
605
|
-
console.error("[Pyreon] Error in dev API dispatcher:", err);
|
|
606
|
-
next();
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
if (config.mode === "ssr") server.middlewares.use((req, res, next) => {
|
|
610
|
-
const accept = req.headers.accept ?? "";
|
|
611
|
-
if (!accept.includes("text/html") && !accept.includes("*/*")) return next();
|
|
612
|
-
const pathname = req.url?.split("?")[0] ?? "/";
|
|
613
|
-
if (pathname.startsWith("/@") || pathname.startsWith("/__")) return next();
|
|
614
|
-
if (/\.\w+$/.test(pathname)) return next();
|
|
615
|
-
const reqHost = req.headers.host ?? "localhost";
|
|
616
|
-
const reqUrl = new URL(req.url ?? "/", `http://${reqHost}`);
|
|
617
|
-
const reqHeaders = new Headers();
|
|
618
|
-
for (const [key, value] of Object.entries(req.headers)) if (value !== void 0) reqHeaders.set(key, Array.isArray(value) ? value.join(", ") : String(value));
|
|
619
|
-
const webReq = new Request(reqUrl.href, {
|
|
620
|
-
method: req.method ?? "GET",
|
|
621
|
-
headers: reqHeaders
|
|
622
|
-
});
|
|
623
|
-
renderSsr(server, root, req.originalUrl ?? pathname, pathname, webReq).then((result) => {
|
|
624
|
-
if (result === null) return next();
|
|
625
|
-
res.statusCode = 200;
|
|
626
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
627
|
-
res.setHeader("Content-Length", Buffer.byteLength(result));
|
|
628
|
-
res.end(result);
|
|
629
|
-
}, (err) => {
|
|
630
|
-
const info = getRedirectInfo(err);
|
|
631
|
-
if (info) {
|
|
632
|
-
res.statusCode = info.status;
|
|
633
|
-
res.setHeader("Location", info.url);
|
|
634
|
-
res.end();
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
638
|
-
server.ssrFixStacktrace(error);
|
|
639
|
-
const html = renderErrorOverlay(error);
|
|
640
|
-
res.statusCode = 500;
|
|
641
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
642
|
-
res.setHeader("Content-Length", Buffer.byteLength(html));
|
|
643
|
-
res.end(html);
|
|
644
|
-
});
|
|
645
|
-
});
|
|
646
|
-
server.middlewares.use((req, res, next) => {
|
|
647
|
-
const accept = req.headers.accept ?? "";
|
|
648
|
-
if (!accept.includes("text/html") && !accept.includes("*/*")) return next();
|
|
649
|
-
const pathname = req.url?.split("?")[0] ?? "/";
|
|
650
|
-
if (pathname.startsWith("/@") || pathname.startsWith("/__")) return next();
|
|
651
|
-
if (/\.\w+$/.test(pathname)) return next();
|
|
652
|
-
handle404(server, routesDir, pathname, res).then((handled) => {
|
|
653
|
-
if (!handled) next();
|
|
654
|
-
}, (err) => {
|
|
655
|
-
console.error("[Pyreon] Error in 404 handler:", err);
|
|
656
|
-
next();
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
server.middlewares.use((req, res, next) => {
|
|
660
|
-
if (!(req.headers.accept ?? "").includes("text/html")) return next();
|
|
661
|
-
const originalEnd = res.end.bind(res);
|
|
662
|
-
let errored = false;
|
|
663
|
-
const handleError = (err) => {
|
|
664
|
-
if (errored) return;
|
|
665
|
-
errored = true;
|
|
666
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
667
|
-
server.ssrFixStacktrace(error);
|
|
668
|
-
const html = renderErrorOverlay(error);
|
|
669
|
-
res.statusCode = 500;
|
|
670
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
671
|
-
res.setHeader("Content-Length", Buffer.byteLength(html));
|
|
672
|
-
originalEnd(html);
|
|
673
|
-
};
|
|
674
|
-
res.on("error", handleError);
|
|
675
|
-
try {
|
|
676
|
-
const result = next();
|
|
677
|
-
if (result && typeof result.catch === "function") result.catch(handleError);
|
|
678
|
-
} catch (err) {
|
|
679
|
-
handleError(err);
|
|
680
|
-
}
|
|
681
|
-
});
|
|
682
|
-
server.watcher.add(`${routesDir}/**/*.{tsx,jsx,ts,js}`);
|
|
683
|
-
server.watcher.on("all", (event, path) => {
|
|
684
|
-
if (path.startsWith(routesDir) && (event === "add" || event === "unlink")) {
|
|
685
|
-
for (const resolvedId of [
|
|
686
|
-
RESOLVED_VIRTUAL_ROUTES_ID,
|
|
687
|
-
RESOLVED_VIRTUAL_MIDDLEWARE_ID,
|
|
688
|
-
RESOLVED_VIRTUAL_API_ROUTES_ID
|
|
689
|
-
]) {
|
|
690
|
-
const mod = server.moduleGraph.getModuleById(resolvedId);
|
|
691
|
-
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
692
|
-
}
|
|
693
|
-
server.ws.send({ type: "full-reload" });
|
|
694
|
-
}
|
|
695
|
-
});
|
|
696
|
-
},
|
|
697
|
-
config(viteUserConfig) {
|
|
698
|
-
const root = viteUserConfig.root ?? process.cwd();
|
|
699
|
-
const pyreonExclude = scanPyreonPackages(root);
|
|
700
|
-
const runtimeServerAlias = resolveNestedPackage(root, "@pyreon/runtime-server");
|
|
701
|
-
return {
|
|
702
|
-
resolve: {
|
|
703
|
-
conditions: ["bun"],
|
|
704
|
-
...runtimeServerAlias ? { alias: { "@pyreon/runtime-server": runtimeServerAlias } } : {}
|
|
705
|
-
},
|
|
706
|
-
ssr: { resolve: {
|
|
707
|
-
conditions: ["bun"],
|
|
708
|
-
...runtimeServerAlias ? { alias: { "@pyreon/runtime-server": runtimeServerAlias } } : {}
|
|
709
|
-
} },
|
|
710
|
-
optimizeDeps: { exclude: pyreonExclude },
|
|
711
|
-
...userConfig.port !== void 0 ? { server: { port: config.port } } : {},
|
|
712
|
-
define: {
|
|
713
|
-
__ZERO_MODE__: JSON.stringify(config.mode),
|
|
714
|
-
__ZERO_BASE__: JSON.stringify(config.base)
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
};
|
|
719
|
-
return config.mode === "ssg" ? [mainPlugin, ssgPlugin(userConfig)] : [mainPlugin];
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Dev-mode API-route dispatcher. Loads the `virtual:zero/api-routes` virtual
|
|
723
|
-
* module, builds a Web `Request` from the Node `IncomingMessage`, and invokes
|
|
724
|
-
* the matching route's HTTP-method handler.
|
|
725
|
-
*
|
|
726
|
-
* Returns `true` if the API middleware handled the request (response written).
|
|
727
|
-
* Returns `false` if no route matched (caller falls through to next middleware).
|
|
728
|
-
*
|
|
729
|
-
* Mirrors what `createServer` wires up in production via `createApiMiddleware`,
|
|
730
|
-
* but adapted for Vite's connect-style middleware stack — needs a Node→Web
|
|
731
|
-
* request adapter.
|
|
732
|
-
*/
|
|
733
|
-
async function dispatchApiRoute(server, req, res) {
|
|
734
|
-
let apiRoutes;
|
|
735
|
-
try {
|
|
736
|
-
apiRoutes = (await server.ssrLoadModule(VIRTUAL_API_ROUTES_ID)).apiRoutes ?? [];
|
|
737
|
-
} catch {
|
|
738
|
-
return false;
|
|
739
|
-
}
|
|
740
|
-
if (apiRoutes.length === 0) return false;
|
|
741
|
-
const host = req.headers.host ?? "localhost";
|
|
742
|
-
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
743
|
-
const pathname = url.pathname;
|
|
744
|
-
if (!apiRoutes.some((r) => matchApiRoute(r.pattern, pathname) !== null)) return false;
|
|
745
|
-
const method = (req.method ?? "GET").toUpperCase();
|
|
746
|
-
const headers = new Headers();
|
|
747
|
-
for (const [key, value] of Object.entries(req.headers)) if (value !== void 0) headers.set(key, Array.isArray(value) ? value.join(", ") : String(value));
|
|
748
|
-
const requestInit = {
|
|
749
|
-
method,
|
|
750
|
-
headers
|
|
751
|
-
};
|
|
752
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
753
|
-
requestInit.body = Readable.toWeb(req);
|
|
754
|
-
requestInit.duplex = "half";
|
|
755
|
-
}
|
|
756
|
-
const webReq = new Request(url.href, requestInit);
|
|
757
|
-
const response = await createApiMiddleware(apiRoutes)({
|
|
758
|
-
req: webReq,
|
|
759
|
-
url,
|
|
760
|
-
path: pathname + url.search,
|
|
761
|
-
headers: new Headers(),
|
|
762
|
-
locals: {}
|
|
763
|
-
});
|
|
764
|
-
if (!response) return false;
|
|
765
|
-
res.statusCode = response.status;
|
|
766
|
-
response.headers.forEach((v, k) => {
|
|
767
|
-
res.setHeader(k, v);
|
|
768
|
-
});
|
|
769
|
-
if (response.body) Readable.fromWeb(response.body).pipe(res);
|
|
770
|
-
else res.end();
|
|
771
|
-
return true;
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Check if the requested path matches any route. If not, render a 404 page.
|
|
775
|
-
* Returns true if the 404 was handled (response sent), false otherwise.
|
|
776
|
-
*
|
|
777
|
-
* In dev mode, the _404.tsx component cannot be SSR-rendered because
|
|
778
|
-
* the compiler emits _tpl() calls that require `document`. Instead,
|
|
779
|
-
* we return a static 404 page. The actual component rendering happens
|
|
780
|
-
* on the client side when the SPA loads.
|
|
781
|
-
*/
|
|
782
|
-
async function handle404(server, _routesDir, pathname, res) {
|
|
783
|
-
const routes = (await server.ssrLoadModule(VIRTUAL_ROUTES_ID)).routes;
|
|
784
|
-
if (flattenRoutePatterns(routes).some((pattern) => matchPattern(pattern, pathname))) return false;
|
|
785
|
-
const html = await render404Page(void 0);
|
|
786
|
-
res.statusCode = 404;
|
|
787
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
788
|
-
res.setHeader("Content-Length", Buffer.byteLength(html));
|
|
789
|
-
res.end(html);
|
|
790
|
-
return true;
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Dev-mode SSR render pipeline. Returns the composed HTML string, or `null`
|
|
794
|
-
* if the URL doesn't match any known route (caller falls through to the 404
|
|
795
|
-
* middleware). Mirrors the production `createServer` flow:
|
|
796
|
-
* 1. Load virtual:zero/routes + app.ts via Vite's ssrLoadModule
|
|
797
|
-
* 2. Create a per-request router bound to the request URL
|
|
798
|
-
* 3. Pre-run loaders for the matched route(s)
|
|
799
|
-
* 4. Render app tree with head tag collection
|
|
800
|
-
* 5. Serialize loader data into `window.__PYREON_LOADER_DATA__`
|
|
801
|
-
* 6. Inject everything into the user's transformed index.html (so Vite
|
|
802
|
-
* still gets a chance to inject its HMR client + JSX runtime prelude)
|
|
803
|
-
*/
|
|
804
|
-
async function renderSsr(server, root, originalUrl, pathname, req) {
|
|
805
|
-
const routes = (await server.ssrLoadModule(VIRTUAL_ROUTES_ID)).routes;
|
|
806
|
-
if (!flattenRoutePatterns(routes).some((pattern) => matchPattern(pattern, pathname))) return null;
|
|
807
|
-
let template = await readFile(join(root, "index.html"), "utf-8");
|
|
808
|
-
template = await server.transformIndexHtml(originalUrl, template);
|
|
809
|
-
const [core, headPkg, headSsr, routerPkg, runtimeServer] = await Promise.all([
|
|
810
|
-
server.ssrLoadModule("@pyreon/core"),
|
|
811
|
-
server.ssrLoadModule("@pyreon/head"),
|
|
812
|
-
server.ssrLoadModule("@pyreon/head/ssr"),
|
|
813
|
-
server.ssrLoadModule("@pyreon/router"),
|
|
814
|
-
server.ssrLoadModule("@pyreon/runtime-server")
|
|
815
|
-
]);
|
|
816
|
-
const { App, router: routerInst } = (await server.ssrLoadModule("@pyreon/zero/server")).createApp({
|
|
817
|
-
routes,
|
|
818
|
-
routerMode: "history",
|
|
819
|
-
url: pathname
|
|
820
|
-
});
|
|
821
|
-
await routerInst.preload(pathname, req);
|
|
822
|
-
return runtimeServer.runWithRequestContext(async () => {
|
|
823
|
-
const app = core.h(App, null);
|
|
824
|
-
const { html: appHtml, head } = await headSsr.renderWithHead(app);
|
|
825
|
-
const loaderData = routerPkg.serializeLoaderData(routerInst);
|
|
826
|
-
const loaderScript = loaderData && Object.keys(loaderData).length > 0 ? `<script>window.__PYREON_LOADER_DATA__=${JSON.stringify(loaderData).replace(/<\//g, "<\\/")}<\/script>` : "";
|
|
827
|
-
return template.replace("<!--pyreon-head-->", head).replace("<!--pyreon-app-->", appHtml).replace("<!--pyreon-scripts-->", loaderScript);
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
/**
|
|
831
|
-
* Extract all URL patterns from a nested route tree.
|
|
832
|
-
*
|
|
833
|
-
* The fs-router emits ABSOLUTE paths for every route, including grandchildren —
|
|
834
|
-
* `{ path: "/app/dashboard" }` not `{ path: "dashboard" }`. The matcher reads
|
|
835
|
-
* each route's `path` as-is; no prefix accumulation. Pre-fix, this function
|
|
836
|
-
* concatenated `${prefix}${route.path}` which produced patterns like
|
|
837
|
-
* `///app/app/dashboard` (prefix `'/app'` + path `'/app/dashboard'`). After
|
|
838
|
-
* `path.split('/').filter(Boolean)` those became `['app', 'app', 'dashboard']`
|
|
839
|
-
* — which can't match a real `/app/dashboard` request — so dev-server returned
|
|
840
|
-
* 404 for every nested-layout route. Re-enables PR #411 specs that rely on
|
|
841
|
-
* `/app/*` routing.
|
|
842
|
-
*/
|
|
843
|
-
function flattenRoutePatterns(routes) {
|
|
844
|
-
const patterns = [];
|
|
845
|
-
for (const route of routes) {
|
|
846
|
-
if (!route.path) continue;
|
|
847
|
-
patterns.push(route.path);
|
|
848
|
-
if (route.children) patterns.push(...flattenRoutePatterns(route.children));
|
|
849
|
-
}
|
|
850
|
-
return patterns;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
//#endregion
|
|
854
|
-
export { createServer as a, resolveConfig as i, zeroPlugin as n, render404Page as o, defineConfig as r, createApp as s, vite_plugin_exports as t };
|
|
855
|
-
//# sourceMappingURL=vite-plugin-E4BHYvYW.js.map
|