@pyreon/zero 0.13.1 → 0.15.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 +146 -0
- package/lib/client.js +3 -1
- package/lib/csp.js +19 -9
- package/lib/{fs-router-CQ7Zxeca.js → fs-router-ZebyutPa.js} +43 -6
- package/lib/image-plugin.js +4 -0
- package/lib/image.js +1 -50
- package/lib/index.js +1 -50
- package/lib/link.js +1 -49
- package/lib/script.js +1 -49
- package/lib/server.js +6 -688
- package/lib/theme.js +1 -50
- package/lib/types/i18n-routing.d.ts +4 -4
- package/lib/types/index.d.ts +23 -13
- package/lib/types/link.d.ts +3 -3
- package/lib/types/server.d.ts +28 -5
- package/lib/types/theme.d.ts +2 -2
- package/lib/vite-plugin-E4BHYvYW.js +855 -0
- package/package.json +15 -13
- package/src/app.ts +21 -1
- package/src/csp.ts +28 -12
- package/src/fs-router.ts +53 -3
- package/src/ssg-plugin.ts +366 -0
- package/src/types.ts +28 -9
- package/src/vite-plugin.ts +220 -40
- package/lib/actions.js.map +0 -1
- package/lib/ai.js.map +0 -1
- package/lib/api-routes.js.map +0 -1
- package/lib/cache.js.map +0 -1
- package/lib/client.js.map +0 -1
- package/lib/compression.js.map +0 -1
- package/lib/config.js.map +0 -1
- package/lib/cors.js.map +0 -1
- package/lib/csp.js.map +0 -1
- package/lib/env.js.map +0 -1
- package/lib/favicon.js.map +0 -1
- package/lib/font.js.map +0 -1
- package/lib/fs-router-3xzp-4Wj.js.map +0 -1
- package/lib/fs-router-CQ7Zxeca.js.map +0 -1
- package/lib/i18n-routing.js.map +0 -1
- package/lib/image-plugin.js.map +0 -1
- package/lib/image.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/link.js.map +0 -1
- package/lib/logger.js.map +0 -1
- package/lib/meta.js.map +0 -1
- package/lib/middleware.js.map +0 -1
- package/lib/og-image.js.map +0 -1
- package/lib/rate-limit.js.map +0 -1
- package/lib/script.js.map +0 -1
- package/lib/seo.js.map +0 -1
- package/lib/server.js.map +0 -1
- package/lib/testing.js.map +0 -1
- package/lib/theme.js.map +0 -1
- package/lib/types/actions.d.ts.map +0 -1
- package/lib/types/ai.d.ts.map +0 -1
- package/lib/types/api-routes.d.ts.map +0 -1
- package/lib/types/cache.d.ts.map +0 -1
- package/lib/types/client.d.ts.map +0 -1
- package/lib/types/compression.d.ts.map +0 -1
- package/lib/types/config.d.ts.map +0 -1
- package/lib/types/cors.d.ts.map +0 -1
- package/lib/types/csp.d.ts.map +0 -1
- package/lib/types/env.d.ts.map +0 -1
- package/lib/types/favicon.d.ts.map +0 -1
- package/lib/types/font.d.ts.map +0 -1
- package/lib/types/i18n-routing.d.ts.map +0 -1
- package/lib/types/image-plugin.d.ts.map +0 -1
- package/lib/types/image.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/link.d.ts.map +0 -1
- package/lib/types/logger.d.ts.map +0 -1
- package/lib/types/meta.d.ts.map +0 -1
- package/lib/types/middleware.d.ts.map +0 -1
- package/lib/types/og-image.d.ts.map +0 -1
- package/lib/types/rate-limit.d.ts.map +0 -1
- package/lib/types/script.d.ts.map +0 -1
- package/lib/types/seo.d.ts.map +0 -1
- package/lib/types/server.d.ts.map +0 -1
- package/lib/types/testing.d.ts.map +0 -1
- package/lib/types/theme.d.ts.map +0 -1
package/lib/server.js
CHANGED
|
@@ -1,327 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { createHandler } from "@pyreon/server";
|
|
6
|
-
import { renderToString } from "@pyreon/runtime-server";
|
|
7
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
1
|
+
import { i as generateRouteModule, o as parseFileRoutes, r as generateMiddlewareModule, s as scanRouteFiles, t as filePathToUrlPath } from "./fs-router-ZebyutPa.js";
|
|
2
|
+
import { a as createServer, i as resolveConfig, n as zeroPlugin, o as render404Page, r as defineConfig, s as createApp } from "./vite-plugin-E4BHYvYW.js";
|
|
3
|
+
import { createContext } from "@pyreon/core";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
8
5
|
import { join } from "node:path";
|
|
9
6
|
import { readFile } from "node:fs/promises";
|
|
10
7
|
import { signal } from "@pyreon/reactivity";
|
|
11
8
|
|
|
12
|
-
//#region src/app.ts
|
|
13
|
-
/**
|
|
14
|
-
* Create a full Zero app — assembles router, head provider, and root layout.
|
|
15
|
-
*
|
|
16
|
-
* Used internally by entry-server and entry-client.
|
|
17
|
-
*/
|
|
18
|
-
function createApp(options) {
|
|
19
|
-
const router = createRouter({
|
|
20
|
-
routes: options.routes,
|
|
21
|
-
mode: options.routerMode ?? "history",
|
|
22
|
-
...options.url ? { url: options.url } : {},
|
|
23
|
-
scrollBehavior: "top"
|
|
24
|
-
});
|
|
25
|
-
const Layout = options.layout ?? DefaultLayout;
|
|
26
|
-
function App() {
|
|
27
|
-
return h(HeadProvider, null, h(RouterProvider, { router }, h(Layout, null, h(RouterView, null))));
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
App,
|
|
31
|
-
router
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function DefaultLayout(props) {
|
|
35
|
-
return h(Fragment, null, ...Array.isArray(props.children) ? props.children : [props.children]);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
//#endregion
|
|
39
|
-
//#region src/api-routes.ts
|
|
40
|
-
/**
|
|
41
|
-
* Match a URL path against an API route pattern.
|
|
42
|
-
* Returns extracted params or null if no match.
|
|
43
|
-
*/
|
|
44
|
-
function matchApiRoute(pattern, path) {
|
|
45
|
-
const patternParts = pattern.split("/").filter(Boolean);
|
|
46
|
-
const pathParts = path.split("/").filter(Boolean);
|
|
47
|
-
const params = {};
|
|
48
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
49
|
-
const pp = patternParts[i];
|
|
50
|
-
if (!pp) continue;
|
|
51
|
-
if (pp.endsWith("*")) {
|
|
52
|
-
const paramName = pp.slice(1, -1);
|
|
53
|
-
params[paramName] = pathParts.slice(i).join("/");
|
|
54
|
-
return params;
|
|
55
|
-
}
|
|
56
|
-
if (i >= pathParts.length) return null;
|
|
57
|
-
if (pp.startsWith(":")) {
|
|
58
|
-
params[pp.slice(1)] = pathParts[i];
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (pp !== pathParts[i]) return null;
|
|
62
|
-
}
|
|
63
|
-
return patternParts.length === pathParts.length ? params : null;
|
|
64
|
-
}
|
|
65
|
-
const HTTP_METHODS = [
|
|
66
|
-
"GET",
|
|
67
|
-
"POST",
|
|
68
|
-
"PUT",
|
|
69
|
-
"PATCH",
|
|
70
|
-
"DELETE",
|
|
71
|
-
"HEAD",
|
|
72
|
-
"OPTIONS"
|
|
73
|
-
];
|
|
74
|
-
/**
|
|
75
|
-
* Create a middleware that dispatches API route requests.
|
|
76
|
-
* API routes are matched by URL pattern and HTTP method.
|
|
77
|
-
*/
|
|
78
|
-
function createApiMiddleware(routes) {
|
|
79
|
-
return async (ctx) => {
|
|
80
|
-
for (const route of routes) {
|
|
81
|
-
const params = matchApiRoute(route.pattern, ctx.path);
|
|
82
|
-
if (!params) continue;
|
|
83
|
-
const method = ctx.req.method.toUpperCase();
|
|
84
|
-
const handler = route.module[method];
|
|
85
|
-
if (!handler) {
|
|
86
|
-
const allowed = HTTP_METHODS.filter((m) => route.module[m]).join(", ");
|
|
87
|
-
return new Response(null, {
|
|
88
|
-
status: 405,
|
|
89
|
-
headers: {
|
|
90
|
-
Allow: allowed,
|
|
91
|
-
"Content-Type": "application/json"
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return handler({
|
|
96
|
-
request: ctx.req,
|
|
97
|
-
url: ctx.url,
|
|
98
|
-
path: ctx.path,
|
|
99
|
-
params,
|
|
100
|
-
headers: ctx.req.headers
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Detect whether a route file is an API route.
|
|
107
|
-
* API routes are `.ts` or `.js` files inside an `api/` directory.
|
|
108
|
-
*/
|
|
109
|
-
function isApiRoute(filePath) {
|
|
110
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
111
|
-
return normalized.startsWith("api/") && (normalized.endsWith(".ts") || normalized.endsWith(".js")) && !normalized.endsWith(".tsx") && !normalized.endsWith(".jsx");
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Convert an API route file path to a URL pattern.
|
|
115
|
-
*
|
|
116
|
-
* Examples:
|
|
117
|
-
* "api/posts.ts" → "/api/posts"
|
|
118
|
-
* "api/posts/index.ts" → "/api/posts"
|
|
119
|
-
* "api/posts/[id].ts" → "/api/posts/:id"
|
|
120
|
-
* "api/[...path].ts" → "/api/:path*"
|
|
121
|
-
*/
|
|
122
|
-
function apiFilePathToPattern(filePath) {
|
|
123
|
-
let route = filePath;
|
|
124
|
-
for (const ext of [".ts", ".js"]) if (route.endsWith(ext)) {
|
|
125
|
-
route = route.slice(0, -ext.length);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
const segments = route.split("/");
|
|
129
|
-
const urlSegments = [];
|
|
130
|
-
for (const seg of segments) {
|
|
131
|
-
if (seg === "index") continue;
|
|
132
|
-
const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/);
|
|
133
|
-
if (catchAll) {
|
|
134
|
-
urlSegments.push(`:${catchAll[1]}*`);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const dynamic = seg.match(/^\[(\w+)\]$/);
|
|
138
|
-
if (dynamic) {
|
|
139
|
-
urlSegments.push(`:${dynamic[1]}`);
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
urlSegments.push(seg);
|
|
143
|
-
}
|
|
144
|
-
return `/${urlSegments.join("/")}`;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Generate a virtual module that exports API route entries.
|
|
148
|
-
* Each entry maps a URL pattern to a module with HTTP method handlers.
|
|
149
|
-
*/
|
|
150
|
-
function generateApiRouteModule(files, routesDir) {
|
|
151
|
-
const apiFiles = files.filter(isApiRoute);
|
|
152
|
-
if (apiFiles.length === 0) return "export const apiRoutes = []\n";
|
|
153
|
-
const imports = [];
|
|
154
|
-
const entries = [];
|
|
155
|
-
for (let i = 0; i < apiFiles.length; i++) {
|
|
156
|
-
const name = `_api${i}`;
|
|
157
|
-
const file = apiFiles[i];
|
|
158
|
-
if (!file) continue;
|
|
159
|
-
const fullPath = `${routesDir}/${file}`;
|
|
160
|
-
const pattern = apiFilePathToPattern(file);
|
|
161
|
-
imports.push(`import * as ${name} from "${fullPath}"`);
|
|
162
|
-
entries.push(` { pattern: ${JSON.stringify(pattern)}, module: ${name} }`);
|
|
163
|
-
}
|
|
164
|
-
return [
|
|
165
|
-
...imports,
|
|
166
|
-
"",
|
|
167
|
-
"export const apiRoutes = [",
|
|
168
|
-
entries.join(",\n"),
|
|
169
|
-
"]"
|
|
170
|
-
].join("\n");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
//#endregion
|
|
174
|
-
//#region src/not-found.ts
|
|
175
|
-
const DEFAULT_404_BODY = "<h1>404 — Not Found</h1><p>The page you requested does not exist.</p>";
|
|
176
|
-
/**
|
|
177
|
-
* Render a 404 component to a full HTML string.
|
|
178
|
-
* If no component is provided, returns a default 404 page.
|
|
179
|
-
*/
|
|
180
|
-
async function render404Page(component, template) {
|
|
181
|
-
let body;
|
|
182
|
-
if (component) body = await renderToString(h(component, null));
|
|
183
|
-
else body = DEFAULT_404_BODY;
|
|
184
|
-
if (template?.includes("<!--pyreon-app-->")) return template.replace("<!--pyreon-app-->", body);
|
|
185
|
-
return `<!DOCTYPE html>
|
|
186
|
-
<html lang="en">
|
|
187
|
-
<head>
|
|
188
|
-
<meta charset="UTF-8">
|
|
189
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
190
|
-
<title>404 — Not Found</title>
|
|
191
|
-
</head>
|
|
192
|
-
<body>
|
|
193
|
-
${body}
|
|
194
|
-
</body>
|
|
195
|
-
</html>`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
//#endregion
|
|
199
|
-
//#region src/entry-server.ts
|
|
200
|
-
/**
|
|
201
|
-
* Create a middleware that dispatches per-route middleware based on URL pattern matching.
|
|
202
|
-
*/
|
|
203
|
-
function createRouteMiddlewareDispatcher(entries) {
|
|
204
|
-
return async (ctx) => {
|
|
205
|
-
for (const entry of entries) if (matchPattern(entry.pattern, ctx.path)) {
|
|
206
|
-
const mw = Array.isArray(entry.middleware) ? entry.middleware : [entry.middleware];
|
|
207
|
-
for (const fn of mw) {
|
|
208
|
-
const result = await fn(ctx);
|
|
209
|
-
if (result) return result;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* URL pattern matcher supporting :param and :param* segments.
|
|
216
|
-
*
|
|
217
|
-
* Rules:
|
|
218
|
-
* - Static segments must match exactly
|
|
219
|
-
* - `:param` matches a single path segment
|
|
220
|
-
* - `:param*` matches all remaining segments (must be last, and path must
|
|
221
|
-
* have matched all preceding segments)
|
|
222
|
-
* - Path length must match pattern length (unless catch-all)
|
|
223
|
-
*/
|
|
224
|
-
function matchPattern(pattern, path) {
|
|
225
|
-
const patternParts = pattern.split("/").filter(Boolean);
|
|
226
|
-
const pathParts = path.split("/").filter(Boolean);
|
|
227
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
228
|
-
const pp = patternParts[i];
|
|
229
|
-
if (pp.endsWith("*")) return i <= pathParts.length;
|
|
230
|
-
if (i >= pathParts.length) return false;
|
|
231
|
-
if (pp.startsWith(":")) continue;
|
|
232
|
-
if (pp !== pathParts[i]) return false;
|
|
233
|
-
}
|
|
234
|
-
return patternParts.length === pathParts.length;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Create the SSR request handler for production.
|
|
238
|
-
*
|
|
239
|
-
* @example
|
|
240
|
-
* import { routes } from "virtual:zero/routes"
|
|
241
|
-
* import { routeMiddleware } from "virtual:zero/route-middleware"
|
|
242
|
-
* import { createServer } from "@pyreon/zero"
|
|
243
|
-
*
|
|
244
|
-
* export default createServer({ routes, routeMiddleware, apiRoutes })
|
|
245
|
-
*/
|
|
246
|
-
function createServer(options) {
|
|
247
|
-
const config = options.config ?? {};
|
|
248
|
-
const allMiddleware = [];
|
|
249
|
-
if (options.apiRoutes?.length) allMiddleware.push(createApiMiddleware(options.apiRoutes));
|
|
250
|
-
if (options.routeMiddleware?.length) allMiddleware.push(createRouteMiddlewareDispatcher(options.routeMiddleware));
|
|
251
|
-
allMiddleware.push(...config.middleware ?? []);
|
|
252
|
-
allMiddleware.push(...options.middleware ?? []);
|
|
253
|
-
const { App } = createApp({
|
|
254
|
-
routes: options.routes,
|
|
255
|
-
routerMode: "history"
|
|
256
|
-
});
|
|
257
|
-
const handler = createHandler({
|
|
258
|
-
App,
|
|
259
|
-
routes: options.routes,
|
|
260
|
-
middleware: allMiddleware,
|
|
261
|
-
mode: config.ssr?.mode ?? "string",
|
|
262
|
-
...options.template ? { template: options.template } : {},
|
|
263
|
-
...options.clientEntry ? { clientEntry: options.clientEntry } : {}
|
|
264
|
-
});
|
|
265
|
-
if (!options.notFoundComponent) return handler;
|
|
266
|
-
const NotFound = options.notFoundComponent;
|
|
267
|
-
const routePatterns = flattenRoutePatterns$1(options.routes);
|
|
268
|
-
return async (req) => {
|
|
269
|
-
const pathname = new URL(req.url).pathname;
|
|
270
|
-
if (!routePatterns.some((pattern) => matchPattern(pattern, pathname))) {
|
|
271
|
-
const fullHtml = await render404Page(NotFound, options.template);
|
|
272
|
-
return new Response(fullHtml, {
|
|
273
|
-
status: 404,
|
|
274
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
return handler(req);
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
/** Extract all URL patterns from a nested route tree. */
|
|
281
|
-
function flattenRoutePatterns$1(routes, prefix = "") {
|
|
282
|
-
const patterns = [];
|
|
283
|
-
for (const route of routes) {
|
|
284
|
-
const fullPath = route.path === "/" && prefix ? prefix : `${prefix}${route.path}`;
|
|
285
|
-
patterns.push(fullPath);
|
|
286
|
-
if (route.children) patterns.push(...flattenRoutePatterns$1(route.children, fullPath));
|
|
287
|
-
}
|
|
288
|
-
return patterns;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
//#endregion
|
|
292
|
-
//#region src/config.ts
|
|
293
|
-
/**
|
|
294
|
-
* Define a Zero configuration.
|
|
295
|
-
* Used in `zero.config.ts` at the project root.
|
|
296
|
-
*
|
|
297
|
-
* @example
|
|
298
|
-
* import { defineConfig } from "@pyreon/zero/config"
|
|
299
|
-
*
|
|
300
|
-
* export default defineConfig({
|
|
301
|
-
* mode: "ssr",
|
|
302
|
-
* ssr: { mode: "stream" },
|
|
303
|
-
* port: 3000,
|
|
304
|
-
* })
|
|
305
|
-
*/
|
|
306
|
-
function defineConfig(config) {
|
|
307
|
-
return config;
|
|
308
|
-
}
|
|
309
|
-
/** Merge user config with defaults. */
|
|
310
|
-
function resolveConfig(userConfig = {}) {
|
|
311
|
-
return {
|
|
312
|
-
mode: "ssr",
|
|
313
|
-
base: "/",
|
|
314
|
-
port: 3e3,
|
|
315
|
-
adapter: "node",
|
|
316
|
-
...userConfig,
|
|
317
|
-
ssr: {
|
|
318
|
-
mode: "string",
|
|
319
|
-
...userConfig.ssr
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
//#endregion
|
|
325
9
|
//#region src/isr.ts
|
|
326
10
|
/**
|
|
327
11
|
* In-memory ISR cache with stale-while-revalidate semantics.
|
|
@@ -893,372 +577,6 @@ function getContext(ctx) {
|
|
|
893
577
|
return zctx;
|
|
894
578
|
}
|
|
895
579
|
|
|
896
|
-
//#endregion
|
|
897
|
-
//#region src/error-overlay.ts
|
|
898
|
-
/**
|
|
899
|
-
* Dev-only error overlay for SSR/loader errors.
|
|
900
|
-
* Renders a styled HTML page with the error stack trace.
|
|
901
|
-
*/
|
|
902
|
-
function renderErrorOverlay(error) {
|
|
903
|
-
return `<!DOCTYPE html>
|
|
904
|
-
<html lang="en">
|
|
905
|
-
<head>
|
|
906
|
-
<meta charset="UTF-8">
|
|
907
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
908
|
-
<title>SSR Error — Pyreon Zero</title>
|
|
909
|
-
<style>
|
|
910
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
911
|
-
body {
|
|
912
|
-
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
|
|
913
|
-
background: #1a1a2e;
|
|
914
|
-
color: #e0e0e0;
|
|
915
|
-
min-height: 100vh;
|
|
916
|
-
padding: 2rem;
|
|
917
|
-
}
|
|
918
|
-
.overlay {
|
|
919
|
-
max-width: 900px;
|
|
920
|
-
margin: 0 auto;
|
|
921
|
-
}
|
|
922
|
-
.header {
|
|
923
|
-
display: flex;
|
|
924
|
-
align-items: center;
|
|
925
|
-
gap: 0.75rem;
|
|
926
|
-
margin-bottom: 1.5rem;
|
|
927
|
-
}
|
|
928
|
-
.badge {
|
|
929
|
-
background: #e74c3c;
|
|
930
|
-
color: white;
|
|
931
|
-
padding: 0.25rem 0.75rem;
|
|
932
|
-
border-radius: 4px;
|
|
933
|
-
font-size: 0.75rem;
|
|
934
|
-
font-weight: 600;
|
|
935
|
-
text-transform: uppercase;
|
|
936
|
-
letter-spacing: 0.05em;
|
|
937
|
-
}
|
|
938
|
-
.label {
|
|
939
|
-
color: #888;
|
|
940
|
-
font-size: 0.85rem;
|
|
941
|
-
}
|
|
942
|
-
.message {
|
|
943
|
-
font-size: 1.25rem;
|
|
944
|
-
color: #ff6b6b;
|
|
945
|
-
margin-bottom: 1.5rem;
|
|
946
|
-
line-height: 1.5;
|
|
947
|
-
word-break: break-word;
|
|
948
|
-
}
|
|
949
|
-
.stack {
|
|
950
|
-
background: #16213e;
|
|
951
|
-
border: 1px solid #2a2a4a;
|
|
952
|
-
border-radius: 8px;
|
|
953
|
-
padding: 1.25rem;
|
|
954
|
-
overflow-x: auto;
|
|
955
|
-
font-size: 0.8rem;
|
|
956
|
-
line-height: 1.7;
|
|
957
|
-
white-space: pre-wrap;
|
|
958
|
-
word-break: break-all;
|
|
959
|
-
}
|
|
960
|
-
.stack .at { color: #888; }
|
|
961
|
-
.stack .file { color: #4ecdc4; }
|
|
962
|
-
.hint {
|
|
963
|
-
margin-top: 1.5rem;
|
|
964
|
-
padding: 1rem;
|
|
965
|
-
background: #1e2a45;
|
|
966
|
-
border-radius: 6px;
|
|
967
|
-
border-left: 3px solid #3498db;
|
|
968
|
-
font-size: 0.8rem;
|
|
969
|
-
color: #aaa;
|
|
970
|
-
line-height: 1.5;
|
|
971
|
-
}
|
|
972
|
-
</style>
|
|
973
|
-
</head>
|
|
974
|
-
<body>
|
|
975
|
-
<div class="overlay">
|
|
976
|
-
<div class="header">
|
|
977
|
-
<span class="badge">SSR Error</span>
|
|
978
|
-
<span class="label">Pyreon Zero — Dev Mode</span>
|
|
979
|
-
</div>
|
|
980
|
-
<div class="message">${escapeHtml(error.message || "Unknown error")}</div>
|
|
981
|
-
<pre class="stack">${formatStack(escapeHtml(error.stack || ""))}</pre>
|
|
982
|
-
<div class="hint">
|
|
983
|
-
This error occurred during server-side rendering. Check the terminal for
|
|
984
|
-
the full stack trace. This overlay is only shown in development.
|
|
985
|
-
</div>
|
|
986
|
-
</div>
|
|
987
|
-
</body>
|
|
988
|
-
</html>`;
|
|
989
|
-
}
|
|
990
|
-
function escapeHtml(str) {
|
|
991
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
992
|
-
}
|
|
993
|
-
function formatStack(stack) {
|
|
994
|
-
return stack.split("\n").map((line) => {
|
|
995
|
-
if (line.includes("at ")) {
|
|
996
|
-
const fileMatch = line.match(/\(([^)]+)\)/);
|
|
997
|
-
if (fileMatch) return line.replace(fileMatch[0], `(<span class="file">${fileMatch[1]}</span>)`);
|
|
998
|
-
}
|
|
999
|
-
return line;
|
|
1000
|
-
}).join("\n");
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
//#endregion
|
|
1004
|
-
//#region src/vite-plugin.ts
|
|
1005
|
-
/**
|
|
1006
|
-
* Scan node_modules/@pyreon/ to discover all installed Pyreon packages.
|
|
1007
|
-
* Returns package names to exclude from Vite's dep optimizer.
|
|
1008
|
-
*/
|
|
1009
|
-
function scanPyreonPackages(root) {
|
|
1010
|
-
const pyreonDir = join(root, "node_modules", "@pyreon");
|
|
1011
|
-
if (!existsSync(pyreonDir)) return [];
|
|
1012
|
-
try {
|
|
1013
|
-
return readdirSync(pyreonDir).filter((name) => !name.startsWith(".")).map((name) => `@pyreon/${name}`);
|
|
1014
|
-
} catch {
|
|
1015
|
-
return [];
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Resolve a package that isn't at the app's top-level `node_modules` but is
|
|
1020
|
-
* nested under another `@pyreon/*` package. Used to alias `@pyreon/runtime-server`
|
|
1021
|
-
* to the copy under `node_modules/@pyreon/zero/node_modules/@pyreon/runtime-server`
|
|
1022
|
-
* so `ssrLoadModule` works without requiring the app to declare it as a
|
|
1023
|
-
* direct dep.
|
|
1024
|
-
*/
|
|
1025
|
-
function resolveNestedPackage(root, name) {
|
|
1026
|
-
const direct = join(root, "node_modules", name);
|
|
1027
|
-
if (existsSync(direct)) return direct;
|
|
1028
|
-
const nested = join(root, "node_modules", "@pyreon", "zero", "node_modules", name);
|
|
1029
|
-
if (existsSync(nested)) return nested;
|
|
1030
|
-
}
|
|
1031
|
-
const VIRTUAL_ROUTES_ID = "virtual:zero/routes";
|
|
1032
|
-
const RESOLVED_VIRTUAL_ROUTES_ID = `\0${VIRTUAL_ROUTES_ID}`;
|
|
1033
|
-
const VIRTUAL_MIDDLEWARE_ID = "virtual:zero/route-middleware";
|
|
1034
|
-
const RESOLVED_VIRTUAL_MIDDLEWARE_ID = `\0${VIRTUAL_MIDDLEWARE_ID}`;
|
|
1035
|
-
const VIRTUAL_API_ROUTES_ID = "virtual:zero/api-routes";
|
|
1036
|
-
const RESOLVED_VIRTUAL_API_ROUTES_ID = `\0${VIRTUAL_API_ROUTES_ID}`;
|
|
1037
|
-
/**
|
|
1038
|
-
* Zero Vite plugin — adds file-based routing and zero-config conventions
|
|
1039
|
-
* on top of @pyreon/vite-plugin.
|
|
1040
|
-
*
|
|
1041
|
-
* @example
|
|
1042
|
-
* // vite.config.ts
|
|
1043
|
-
* import pyreon from "@pyreon/vite-plugin"
|
|
1044
|
-
* import zero from "@pyreon/zero"
|
|
1045
|
-
*
|
|
1046
|
-
* export default {
|
|
1047
|
-
* plugins: [pyreon(), zero()],
|
|
1048
|
-
* }
|
|
1049
|
-
*/
|
|
1050
|
-
function zeroPlugin(userConfig = {}) {
|
|
1051
|
-
const config = resolveConfig(userConfig);
|
|
1052
|
-
let routesDir;
|
|
1053
|
-
let root;
|
|
1054
|
-
return {
|
|
1055
|
-
name: "pyreon-zero",
|
|
1056
|
-
enforce: "pre",
|
|
1057
|
-
_zeroConfig: userConfig,
|
|
1058
|
-
configResolved(resolvedConfig) {
|
|
1059
|
-
root = resolvedConfig.root;
|
|
1060
|
-
routesDir = `${root}/src/routes`;
|
|
1061
|
-
},
|
|
1062
|
-
resolveId(id) {
|
|
1063
|
-
if (id === VIRTUAL_ROUTES_ID) return RESOLVED_VIRTUAL_ROUTES_ID;
|
|
1064
|
-
if (id === VIRTUAL_MIDDLEWARE_ID) return RESOLVED_VIRTUAL_MIDDLEWARE_ID;
|
|
1065
|
-
if (id === VIRTUAL_API_ROUTES_ID) return RESOLVED_VIRTUAL_API_ROUTES_ID;
|
|
1066
|
-
},
|
|
1067
|
-
async load(id) {
|
|
1068
|
-
if (id === RESOLVED_VIRTUAL_ROUTES_ID) try {
|
|
1069
|
-
return generateRouteModuleFromRoutes(await scanRouteFilesWithExports(routesDir, config.mode), routesDir, { staticImports: config.mode === "ssg" });
|
|
1070
|
-
} catch (_err) {
|
|
1071
|
-
return `export const routes = []`;
|
|
1072
|
-
}
|
|
1073
|
-
if (id === RESOLVED_VIRTUAL_MIDDLEWARE_ID) try {
|
|
1074
|
-
return generateMiddlewareModule(await scanRouteFiles(routesDir), routesDir);
|
|
1075
|
-
} catch (_err) {
|
|
1076
|
-
return `export const routeMiddleware = []`;
|
|
1077
|
-
}
|
|
1078
|
-
if (id === RESOLVED_VIRTUAL_API_ROUTES_ID) try {
|
|
1079
|
-
return generateApiRouteModule(await scanRouteFiles(routesDir), routesDir);
|
|
1080
|
-
} catch (_err) {
|
|
1081
|
-
return `export const apiRoutes = []`;
|
|
1082
|
-
}
|
|
1083
|
-
},
|
|
1084
|
-
configureServer(server) {
|
|
1085
|
-
if (config.mode === "ssr") server.middlewares.use((req, res, next) => {
|
|
1086
|
-
const accept = req.headers.accept ?? "";
|
|
1087
|
-
if (!accept.includes("text/html") && !accept.includes("*/*")) return next();
|
|
1088
|
-
const pathname = req.url?.split("?")[0] ?? "/";
|
|
1089
|
-
if (pathname.startsWith("/@") || pathname.startsWith("/__")) return next();
|
|
1090
|
-
if (/\.\w+$/.test(pathname)) return next();
|
|
1091
|
-
renderSsr(server, root, req.originalUrl ?? pathname, pathname).then((result) => {
|
|
1092
|
-
if (result === null) return next();
|
|
1093
|
-
res.statusCode = 200;
|
|
1094
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1095
|
-
res.setHeader("Content-Length", Buffer.byteLength(result));
|
|
1096
|
-
res.end(result);
|
|
1097
|
-
}, (err) => {
|
|
1098
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1099
|
-
server.ssrFixStacktrace(error);
|
|
1100
|
-
const html = renderErrorOverlay(error);
|
|
1101
|
-
res.statusCode = 500;
|
|
1102
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1103
|
-
res.setHeader("Content-Length", Buffer.byteLength(html));
|
|
1104
|
-
res.end(html);
|
|
1105
|
-
});
|
|
1106
|
-
});
|
|
1107
|
-
server.middlewares.use((req, res, next) => {
|
|
1108
|
-
const accept = req.headers.accept ?? "";
|
|
1109
|
-
if (!accept.includes("text/html") && !accept.includes("*/*")) return next();
|
|
1110
|
-
const pathname = req.url?.split("?")[0] ?? "/";
|
|
1111
|
-
if (pathname.startsWith("/@") || pathname.startsWith("/__")) return next();
|
|
1112
|
-
if (/\.\w+$/.test(pathname)) return next();
|
|
1113
|
-
handle404(server, routesDir, pathname, res).then((handled) => {
|
|
1114
|
-
if (!handled) next();
|
|
1115
|
-
}, (err) => {
|
|
1116
|
-
console.error("[Pyreon] Error in 404 handler:", err);
|
|
1117
|
-
next();
|
|
1118
|
-
});
|
|
1119
|
-
});
|
|
1120
|
-
server.middlewares.use((req, res, next) => {
|
|
1121
|
-
if (!(req.headers.accept ?? "").includes("text/html")) return next();
|
|
1122
|
-
const originalEnd = res.end.bind(res);
|
|
1123
|
-
let errored = false;
|
|
1124
|
-
const handleError = (err) => {
|
|
1125
|
-
if (errored) return;
|
|
1126
|
-
errored = true;
|
|
1127
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1128
|
-
server.ssrFixStacktrace(error);
|
|
1129
|
-
const html = renderErrorOverlay(error);
|
|
1130
|
-
res.statusCode = 500;
|
|
1131
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1132
|
-
res.setHeader("Content-Length", Buffer.byteLength(html));
|
|
1133
|
-
originalEnd(html);
|
|
1134
|
-
};
|
|
1135
|
-
res.on("error", handleError);
|
|
1136
|
-
try {
|
|
1137
|
-
const result = next();
|
|
1138
|
-
if (result && typeof result.catch === "function") result.catch(handleError);
|
|
1139
|
-
} catch (err) {
|
|
1140
|
-
handleError(err);
|
|
1141
|
-
}
|
|
1142
|
-
});
|
|
1143
|
-
server.watcher.add(`${routesDir}/**/*.{tsx,jsx,ts,js}`);
|
|
1144
|
-
server.watcher.on("all", (event, path) => {
|
|
1145
|
-
if (path.startsWith(routesDir) && (event === "add" || event === "unlink")) {
|
|
1146
|
-
for (const resolvedId of [
|
|
1147
|
-
RESOLVED_VIRTUAL_ROUTES_ID,
|
|
1148
|
-
RESOLVED_VIRTUAL_MIDDLEWARE_ID,
|
|
1149
|
-
RESOLVED_VIRTUAL_API_ROUTES_ID
|
|
1150
|
-
]) {
|
|
1151
|
-
const mod = server.moduleGraph.getModuleById(resolvedId);
|
|
1152
|
-
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
1153
|
-
}
|
|
1154
|
-
server.ws.send({ type: "full-reload" });
|
|
1155
|
-
}
|
|
1156
|
-
});
|
|
1157
|
-
},
|
|
1158
|
-
config(userConfig) {
|
|
1159
|
-
const root = userConfig.root ?? process.cwd();
|
|
1160
|
-
const pyreonExclude = scanPyreonPackages(root);
|
|
1161
|
-
const runtimeServerAlias = resolveNestedPackage(root, "@pyreon/runtime-server");
|
|
1162
|
-
return {
|
|
1163
|
-
resolve: {
|
|
1164
|
-
conditions: ["bun"],
|
|
1165
|
-
...runtimeServerAlias ? { alias: { "@pyreon/runtime-server": runtimeServerAlias } } : {}
|
|
1166
|
-
},
|
|
1167
|
-
ssr: { resolve: {
|
|
1168
|
-
conditions: ["bun"],
|
|
1169
|
-
...runtimeServerAlias ? { alias: { "@pyreon/runtime-server": runtimeServerAlias } } : {}
|
|
1170
|
-
} },
|
|
1171
|
-
optimizeDeps: { exclude: pyreonExclude },
|
|
1172
|
-
server: { port: config.port },
|
|
1173
|
-
define: {
|
|
1174
|
-
__ZERO_MODE__: JSON.stringify(config.mode),
|
|
1175
|
-
__ZERO_BASE__: JSON.stringify(config.base)
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Check if the requested path matches any route. If not, render a 404 page.
|
|
1183
|
-
* Returns true if the 404 was handled (response sent), false otherwise.
|
|
1184
|
-
*
|
|
1185
|
-
* In dev mode, the _404.tsx component cannot be SSR-rendered because
|
|
1186
|
-
* the compiler emits _tpl() calls that require `document`. Instead,
|
|
1187
|
-
* we return a static 404 page. The actual component rendering happens
|
|
1188
|
-
* on the client side when the SPA loads.
|
|
1189
|
-
*/
|
|
1190
|
-
async function handle404(server, _routesDir, pathname, res) {
|
|
1191
|
-
const routes = (await server.ssrLoadModule(VIRTUAL_ROUTES_ID)).routes;
|
|
1192
|
-
if (flattenRoutePatterns(routes).some((pattern) => matchPattern(pattern, pathname))) return false;
|
|
1193
|
-
const html = await render404Page(void 0);
|
|
1194
|
-
res.statusCode = 404;
|
|
1195
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1196
|
-
res.setHeader("Content-Length", Buffer.byteLength(html));
|
|
1197
|
-
res.end(html);
|
|
1198
|
-
return true;
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* Dev-mode SSR render pipeline. Returns the composed HTML string, or `null`
|
|
1202
|
-
* if the URL doesn't match any known route (caller falls through to the 404
|
|
1203
|
-
* middleware). Mirrors the production `createServer` flow:
|
|
1204
|
-
* 1. Load virtual:zero/routes + app.ts via Vite's ssrLoadModule
|
|
1205
|
-
* 2. Create a per-request router bound to the request URL
|
|
1206
|
-
* 3. Pre-run loaders for the matched route(s)
|
|
1207
|
-
* 4. Render app tree with head tag collection
|
|
1208
|
-
* 5. Serialize loader data into `window.__PYREON_LOADER_DATA__`
|
|
1209
|
-
* 6. Inject everything into the user's transformed index.html (so Vite
|
|
1210
|
-
* still gets a chance to inject its HMR client + JSX runtime prelude)
|
|
1211
|
-
*/
|
|
1212
|
-
async function renderSsr(server, root, originalUrl, pathname) {
|
|
1213
|
-
const routes = (await server.ssrLoadModule(VIRTUAL_ROUTES_ID)).routes;
|
|
1214
|
-
if (!flattenRoutePatterns(routes).some((pattern) => matchPattern(pattern, pathname))) return null;
|
|
1215
|
-
let template = await readFile(join(root, "index.html"), "utf-8");
|
|
1216
|
-
template = await server.transformIndexHtml(originalUrl, template);
|
|
1217
|
-
const [core, headPkg, headSsr, routerPkg, runtimeServer] = await Promise.all([
|
|
1218
|
-
server.ssrLoadModule("@pyreon/core"),
|
|
1219
|
-
server.ssrLoadModule("@pyreon/head"),
|
|
1220
|
-
server.ssrLoadModule("@pyreon/head/ssr"),
|
|
1221
|
-
server.ssrLoadModule("@pyreon/router"),
|
|
1222
|
-
server.ssrLoadModule("@pyreon/runtime-server")
|
|
1223
|
-
]);
|
|
1224
|
-
let userLayout;
|
|
1225
|
-
for (const ext of [
|
|
1226
|
-
"tsx",
|
|
1227
|
-
"ts",
|
|
1228
|
-
"jsx",
|
|
1229
|
-
"js"
|
|
1230
|
-
]) try {
|
|
1231
|
-
const layoutMod = await server.ssrLoadModule(`/src/routes/_layout.${ext}`);
|
|
1232
|
-
userLayout = layoutMod.layout ?? layoutMod.default;
|
|
1233
|
-
if (userLayout) break;
|
|
1234
|
-
} catch {}
|
|
1235
|
-
const { App, router: routerInst } = (await server.ssrLoadModule("@pyreon/zero/server")).createApp({
|
|
1236
|
-
routes,
|
|
1237
|
-
routerMode: "history",
|
|
1238
|
-
url: pathname,
|
|
1239
|
-
...userLayout ? { layout: userLayout } : {}
|
|
1240
|
-
});
|
|
1241
|
-
await routerInst.preload(pathname);
|
|
1242
|
-
return runtimeServer.runWithRequestContext(async () => {
|
|
1243
|
-
const app = core.h(App, null);
|
|
1244
|
-
const { html: appHtml, head } = await headSsr.renderWithHead(app);
|
|
1245
|
-
const loaderData = routerPkg.serializeLoaderData(routerInst);
|
|
1246
|
-
const loaderScript = loaderData && Object.keys(loaderData).length > 0 ? `<script>window.__PYREON_LOADER_DATA__=${JSON.stringify(loaderData).replace(/<\//g, "<\\/")}<\/script>` : "";
|
|
1247
|
-
return template.replace("<!--pyreon-head-->", head).replace("<!--pyreon-app-->", appHtml).replace("<!--pyreon-scripts-->", loaderScript);
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
/** Extract all URL patterns from a nested route tree. */
|
|
1251
|
-
function flattenRoutePatterns(routes, prefix = "") {
|
|
1252
|
-
const patterns = [];
|
|
1253
|
-
for (const route of routes) {
|
|
1254
|
-
if (!route.path) continue;
|
|
1255
|
-
const fullPath = route.path === "/" && prefix ? prefix : `${prefix}${route.path}`;
|
|
1256
|
-
patterns.push(fullPath);
|
|
1257
|
-
if (route.children) patterns.push(...flattenRoutePatterns(route.children, fullPath));
|
|
1258
|
-
}
|
|
1259
|
-
return patterns;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
580
|
//#endregion
|
|
1263
581
|
//#region src/favicon.ts
|
|
1264
582
|
let sharpWarned$1 = false;
|
|
@@ -1925,7 +1243,7 @@ function seoPlugin(config = {}) {
|
|
|
1925
1243
|
apply: "build",
|
|
1926
1244
|
async generateBundle(_, _bundle) {
|
|
1927
1245
|
if (config.sitemap) {
|
|
1928
|
-
const { scanRouteFiles } = await import("./fs-router-
|
|
1246
|
+
const { scanRouteFiles } = await import("./fs-router-ZebyutPa.js").then((n) => n.n);
|
|
1929
1247
|
const routesDir = `${process.cwd()}/src/routes`;
|
|
1930
1248
|
try {
|
|
1931
1249
|
const sitemap = generateSitemap(await scanRouteFiles(routesDir), config.sitemap);
|
|
@@ -1955,7 +1273,7 @@ function seoMiddleware(config = {}) {
|
|
|
1955
1273
|
return async (ctx) => {
|
|
1956
1274
|
if (ctx.url.pathname === "/robots.txt" && config.robots) return new Response(generateRobots(config.robots), { headers: { "Content-Type": "text/plain" } });
|
|
1957
1275
|
if (ctx.url.pathname === "/sitemap.xml" && config.sitemap) try {
|
|
1958
|
-
const { scanRouteFiles } = await import("./fs-router-
|
|
1276
|
+
const { scanRouteFiles } = await import("./fs-router-ZebyutPa.js").then((n) => n.n);
|
|
1959
1277
|
const sitemap = generateSitemap(await scanRouteFiles(`${process.cwd()}/src/routes`), config.sitemap);
|
|
1960
1278
|
return new Response(sitemap, { headers: { "Content-Type": "application/xml" } });
|
|
1961
1279
|
} catch {}
|