@pyreon/zero 0.14.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-Ci0kVmM4.js +146 -0
- package/lib/client.js +7 -2
- package/lib/csp.js +19 -9
- package/lib/env.js +6 -6
- package/lib/font.js +3 -3
- package/lib/{fs-router-CQ7Zxeca.js → fs-router-MewHc5SB.js} +56 -24
- package/lib/i18n-routing.js +112 -1
- package/lib/image-plugin.js +4 -0
- package/lib/image.js +141 -108
- package/lib/index.js +253 -132
- package/lib/link.js +1 -49
- package/lib/og-image.js +5 -5
- package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
- package/lib/script.js +115 -74
- package/lib/seo.js +186 -15
- package/lib/server.js +275 -1247
- package/lib/theme.js +1 -50
- package/lib/types/config.d.ts +275 -3
- package/lib/types/env.d.ts +2 -2
- package/lib/types/i18n-routing.d.ts +197 -6
- package/lib/types/image.d.ts +105 -5
- package/lib/types/index.d.ts +640 -178
- package/lib/types/link.d.ts +3 -3
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +603 -77
- package/lib/types/theme.d.ts +2 -2
- package/lib/vite-plugin-xjWZwudX.js +2454 -0
- package/package.json +16 -13
- 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 +35 -1
- package/src/client.ts +18 -0
- package/src/csp.ts +28 -12
- package/src/entry-server.ts +55 -5
- package/src/env.ts +7 -7
- package/src/font.ts +3 -3
- package/src/fs-router.ts +123 -4
- 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 +1523 -0
- package/src/types.ts +329 -19
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +326 -68
- 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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-CjeV3_4I.js";
|
|
2
|
+
|
|
3
|
+
//#region src/api-routes.ts
|
|
4
|
+
var api_routes_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
apiFilePathToPattern: () => apiFilePathToPattern,
|
|
6
|
+
createApiMiddleware: () => createApiMiddleware,
|
|
7
|
+
generateApiRouteModule: () => generateApiRouteModule,
|
|
8
|
+
isApiRoute: () => isApiRoute,
|
|
9
|
+
matchApiRoute: () => matchApiRoute
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Match a URL path against an API route pattern.
|
|
13
|
+
* Returns extracted params or null if no match.
|
|
14
|
+
*/
|
|
15
|
+
function matchApiRoute(pattern, path) {
|
|
16
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
17
|
+
const pathParts = path.split("/").filter(Boolean);
|
|
18
|
+
const params = {};
|
|
19
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
20
|
+
const pp = patternParts[i];
|
|
21
|
+
if (!pp) continue;
|
|
22
|
+
if (pp.endsWith("*")) {
|
|
23
|
+
const paramName = pp.slice(1, -1);
|
|
24
|
+
params[paramName] = pathParts.slice(i).join("/");
|
|
25
|
+
return params;
|
|
26
|
+
}
|
|
27
|
+
if (i >= pathParts.length) return null;
|
|
28
|
+
if (pp.startsWith(":")) {
|
|
29
|
+
params[pp.slice(1)] = pathParts[i];
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (pp !== pathParts[i]) return null;
|
|
33
|
+
}
|
|
34
|
+
return patternParts.length === pathParts.length ? params : null;
|
|
35
|
+
}
|
|
36
|
+
const HTTP_METHODS = [
|
|
37
|
+
"GET",
|
|
38
|
+
"POST",
|
|
39
|
+
"PUT",
|
|
40
|
+
"PATCH",
|
|
41
|
+
"DELETE",
|
|
42
|
+
"HEAD",
|
|
43
|
+
"OPTIONS"
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Create a middleware that dispatches API route requests.
|
|
47
|
+
* API routes are matched by URL pattern and HTTP method.
|
|
48
|
+
*/
|
|
49
|
+
function createApiMiddleware(routes) {
|
|
50
|
+
return async (ctx) => {
|
|
51
|
+
for (const route of routes) {
|
|
52
|
+
const params = matchApiRoute(route.pattern, ctx.path);
|
|
53
|
+
if (!params) continue;
|
|
54
|
+
const method = ctx.req.method.toUpperCase();
|
|
55
|
+
const handler = route.module[method];
|
|
56
|
+
if (!handler) {
|
|
57
|
+
const allowed = HTTP_METHODS.filter((m) => route.module[m]).join(", ");
|
|
58
|
+
return new Response(null, {
|
|
59
|
+
status: 405,
|
|
60
|
+
headers: {
|
|
61
|
+
Allow: allowed,
|
|
62
|
+
"Content-Type": "application/json"
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return handler({
|
|
67
|
+
request: ctx.req,
|
|
68
|
+
url: ctx.url,
|
|
69
|
+
path: ctx.path,
|
|
70
|
+
params,
|
|
71
|
+
headers: ctx.req.headers
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Detect whether a route file is an API route.
|
|
78
|
+
* API routes are `.ts` or `.js` files inside an `api/` directory.
|
|
79
|
+
*/
|
|
80
|
+
function isApiRoute(filePath) {
|
|
81
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
82
|
+
return normalized.startsWith("api/") && (normalized.endsWith(".ts") || normalized.endsWith(".js")) && !normalized.endsWith(".tsx") && !normalized.endsWith(".jsx");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Convert an API route file path to a URL pattern.
|
|
86
|
+
*
|
|
87
|
+
* Examples:
|
|
88
|
+
* "api/posts.ts" → "/api/posts"
|
|
89
|
+
* "api/posts/index.ts" → "/api/posts"
|
|
90
|
+
* "api/posts/[id].ts" → "/api/posts/:id"
|
|
91
|
+
* "api/[...path].ts" → "/api/:path*"
|
|
92
|
+
*/
|
|
93
|
+
function apiFilePathToPattern(filePath) {
|
|
94
|
+
let route = filePath;
|
|
95
|
+
for (const ext of [".ts", ".js"]) if (route.endsWith(ext)) {
|
|
96
|
+
route = route.slice(0, -ext.length);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
const segments = route.split("/");
|
|
100
|
+
const urlSegments = [];
|
|
101
|
+
for (const seg of segments) {
|
|
102
|
+
if (seg === "index") continue;
|
|
103
|
+
const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/);
|
|
104
|
+
if (catchAll) {
|
|
105
|
+
urlSegments.push(`:${catchAll[1]}*`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const dynamic = seg.match(/^\[(\w+)\]$/);
|
|
109
|
+
if (dynamic) {
|
|
110
|
+
urlSegments.push(`:${dynamic[1]}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
urlSegments.push(seg);
|
|
114
|
+
}
|
|
115
|
+
return `/${urlSegments.join("/")}`;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generate a virtual module that exports API route entries.
|
|
119
|
+
* Each entry maps a URL pattern to a module with HTTP method handlers.
|
|
120
|
+
*/
|
|
121
|
+
function generateApiRouteModule(files, routesDir) {
|
|
122
|
+
const apiFiles = files.filter(isApiRoute);
|
|
123
|
+
if (apiFiles.length === 0) return "export const apiRoutes = []\n";
|
|
124
|
+
const imports = [];
|
|
125
|
+
const entries = [];
|
|
126
|
+
for (let i = 0; i < apiFiles.length; i++) {
|
|
127
|
+
const name = `_api${i}`;
|
|
128
|
+
const file = apiFiles[i];
|
|
129
|
+
if (!file) continue;
|
|
130
|
+
const fullPath = `${routesDir}/${file}`;
|
|
131
|
+
const pattern = apiFilePathToPattern(file);
|
|
132
|
+
imports.push(`import * as ${name} from "${fullPath}"`);
|
|
133
|
+
entries.push(` { pattern: ${JSON.stringify(pattern)}, module: ${name} }`);
|
|
134
|
+
}
|
|
135
|
+
return [
|
|
136
|
+
...imports,
|
|
137
|
+
"",
|
|
138
|
+
"export const apiRoutes = [",
|
|
139
|
+
entries.join(",\n"),
|
|
140
|
+
"]"
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
export { matchApiRoute as i, createApiMiddleware as n, generateApiRouteModule as r, api_routes_exports as t };
|
|
146
|
+
//# sourceMappingURL=api-routes-Ci0kVmM4.js.map
|
package/lib/client.js
CHANGED
|
@@ -14,9 +14,12 @@ function createApp(options) {
|
|
|
14
14
|
routes: options.routes,
|
|
15
15
|
mode: options.routerMode ?? "history",
|
|
16
16
|
...options.url ? { url: options.url } : {},
|
|
17
|
+
...options.base && options.base !== "/" ? { base: options.base } : {},
|
|
17
18
|
scrollBehavior: "top"
|
|
18
19
|
});
|
|
19
|
-
const
|
|
20
|
+
const hasLayoutInRoutes = options.layout !== void 0 && options.routes.some((r) => r.component === options.layout);
|
|
21
|
+
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.");
|
|
22
|
+
const Layout = hasLayoutInRoutes ? DefaultLayout : options.layout ?? DefaultLayout;
|
|
20
23
|
function App() {
|
|
21
24
|
return h(HeadProvider, null, h(RouterProvider, { router }, h(Layout, null, h(RouterView, null))));
|
|
22
25
|
}
|
|
@@ -70,10 +73,12 @@ function startClient(options) {
|
|
|
70
73
|
if (typeof document === "undefined") throw new Error("[Pyreon] startClient() can only be called in the browser.");
|
|
71
74
|
const container = document.getElementById("app");
|
|
72
75
|
if (!container) throw new Error("[Pyreon] Missing #app container element");
|
|
76
|
+
const base = typeof __ZERO_BASE__ !== "undefined" && __ZERO_BASE__ !== "/" ? __ZERO_BASE__ : void 0;
|
|
73
77
|
const { App, router } = createApp({
|
|
74
78
|
routes: options.routes,
|
|
75
79
|
routerMode: "history",
|
|
76
|
-
...options.layout ? { layout: options.layout } : {}
|
|
80
|
+
...options.layout ? { layout: options.layout } : {},
|
|
81
|
+
...base ? { base } : {}
|
|
77
82
|
});
|
|
78
83
|
const ssrLoaderData = window.__PYREON_LOADER_DATA__;
|
|
79
84
|
const hasSSRLoaderData = ssrLoaderData !== void 0 && typeof ssrLoaderData === "object" && ssrLoaderData !== null;
|
package/lib/csp.js
CHANGED
|
@@ -63,17 +63,27 @@ function buildCspHeader(directives, nonce) {
|
|
|
63
63
|
return parts.join("; ");
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* Generate a random nonce string (base64, 16 bytes).
|
|
66
|
+
* Generate a cryptographically-random nonce string (base64, 16 bytes).
|
|
67
|
+
*
|
|
68
|
+
* Throws when `crypto.getRandomValues` is unavailable. CSP nonces protect
|
|
69
|
+
* against XSS by gating inline script execution; a predictable nonce
|
|
70
|
+
* (`Math.random` ~31 bits of entropy) bypasses CSP entirely. Silent
|
|
71
|
+
* degradation here was a security anti-pattern — we surface the
|
|
72
|
+
* misconfiguration loudly instead.
|
|
73
|
+
*
|
|
74
|
+
* Realistic deployments always have `crypto.getRandomValues`: Node 18+,
|
|
75
|
+
* Bun, Deno, browsers, edge workers (Cloudflare/Vercel/Netlify), and
|
|
76
|
+
* vitest/happy-dom all expose it via `globalThis.crypto`. If you hit
|
|
77
|
+
* this throw, your environment is unusual — fix the env, don't downgrade
|
|
78
|
+
* the security primitive.
|
|
67
79
|
*/
|
|
68
80
|
function generateNonce() {
|
|
69
|
-
if (typeof crypto
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
81
|
+
if (typeof crypto === "undefined" || !crypto.getRandomValues) throw new Error("[Pyreon] CSP nonce generation requires `crypto.getRandomValues` (Web Crypto API). No secure RNG is available in this environment. CSP nonces must be cryptographically random — falling back to `Math.random` would silently weaken XSS protection. Ensure Node 18+, Bun, Deno, an edge runtime, or a browser environment.");
|
|
82
|
+
const bytes = new Uint8Array(16);
|
|
83
|
+
crypto.getRandomValues(bytes);
|
|
84
|
+
let binary = "";
|
|
85
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
86
|
+
return typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
|
|
77
87
|
}
|
|
78
88
|
/**
|
|
79
89
|
* CSP middleware — sets Content-Security-Policy header.
|
package/lib/env.js
CHANGED
|
@@ -148,11 +148,11 @@ function toValidator(value) {
|
|
|
148
148
|
* })
|
|
149
149
|
* ```
|
|
150
150
|
*/
|
|
151
|
-
function validateEnv(
|
|
151
|
+
function validateEnv(envSchema, source) {
|
|
152
152
|
const env = source ?? (typeof process !== "undefined" ? process.env : {});
|
|
153
153
|
const result = {};
|
|
154
154
|
const errors = [];
|
|
155
|
-
for (const [key, entry] of Object.entries(
|
|
155
|
+
for (const [key, entry] of Object.entries(envSchema)) {
|
|
156
156
|
const validator = toValidator(entry);
|
|
157
157
|
try {
|
|
158
158
|
result[key] = validator.parse(env[key], key);
|
|
@@ -167,17 +167,17 @@ function validateEnv(schema, source) {
|
|
|
167
167
|
}
|
|
168
168
|
return result;
|
|
169
169
|
}
|
|
170
|
-
function publicEnv(
|
|
170
|
+
function publicEnv(envSchema) {
|
|
171
171
|
const prefix = "ZERO_PUBLIC_";
|
|
172
172
|
const env = typeof process !== "undefined" ? process.env : {};
|
|
173
|
-
if (!
|
|
173
|
+
if (!envSchema) {
|
|
174
174
|
const result = {};
|
|
175
175
|
for (const [key, value] of Object.entries(env)) if (key.startsWith(prefix) && value !== void 0) result[key.slice(12)] = value;
|
|
176
176
|
return result;
|
|
177
177
|
}
|
|
178
178
|
const prefixedSource = {};
|
|
179
|
-
for (const key of Object.keys(
|
|
180
|
-
return validateEnv(
|
|
179
|
+
for (const key of Object.keys(envSchema)) prefixedSource[key] = env[`${prefix}${key}`];
|
|
180
|
+
return validateEnv(envSchema, prefixedSource);
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
183
|
* Create an env validator from a custom parse function.
|
package/lib/font.js
CHANGED
|
@@ -46,10 +46,10 @@ function parseGoogleFamily(input) {
|
|
|
46
46
|
const entries = afterAt.split(";").filter(Boolean);
|
|
47
47
|
const weights = /* @__PURE__ */ new Set();
|
|
48
48
|
for (const entry of entries) if (entry.includes(",")) {
|
|
49
|
-
const
|
|
50
|
-
const weight = Number(
|
|
49
|
+
const tuple = entry.split(",");
|
|
50
|
+
const weight = Number(tuple[tuple.length - 1]);
|
|
51
51
|
if (weight > 0) weights.add(weight);
|
|
52
|
-
if (
|
|
52
|
+
if (tuple[0] === "1") italic = true;
|
|
53
53
|
} else if (entry.includes("..")) {} else {
|
|
54
54
|
const weight = Number(entry);
|
|
55
55
|
if (weight > 0) weights.add(weight);
|
|
@@ -1,23 +1,7 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-CjeV3_4I.js";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
|
|
4
|
-
//#region \0rolldown/runtime.js
|
|
5
|
-
var __defProp = Object.defineProperty;
|
|
6
|
-
var __exportAll = (all, no_symbols) => {
|
|
7
|
-
let target = {};
|
|
8
|
-
for (var name in all) {
|
|
9
|
-
__defProp(target, name, {
|
|
10
|
-
get: all[name],
|
|
11
|
-
enumerable: true
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
if (!no_symbols) {
|
|
15
|
-
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
-
}
|
|
17
|
-
return target;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
5
|
//#region src/fs-router.ts
|
|
22
6
|
var fs_router_exports = /* @__PURE__ */ __exportAll({
|
|
23
7
|
detectRouteExports: () => detectRouteExports,
|
|
@@ -44,7 +28,11 @@ const ROUTE_EXPORT_NAMES = [
|
|
|
44
28
|
"meta",
|
|
45
29
|
"renderMode",
|
|
46
30
|
"error",
|
|
47
|
-
"middleware"
|
|
31
|
+
"middleware",
|
|
32
|
+
"loaderKey",
|
|
33
|
+
"gcTime",
|
|
34
|
+
"getStaticPaths",
|
|
35
|
+
"revalidate"
|
|
48
36
|
];
|
|
49
37
|
/**
|
|
50
38
|
* Detect which optional metadata exports a route file source declares.
|
|
@@ -74,10 +62,13 @@ function detectRouteExports(source) {
|
|
|
74
62
|
} else for (const name of tok.names) if (ROUTE_EXPORT_NAMES.includes(name)) found.add(name);
|
|
75
63
|
const rawMeta = found.has("meta") ? extractLiteralExport(source, "meta") : void 0;
|
|
76
64
|
const rawRenderMode = found.has("renderMode") ? extractLiteralExport(source, "renderMode") : void 0;
|
|
65
|
+
const rawRevalidate = found.has("revalidate") ? extractLiteralExport(source, "revalidate") : void 0;
|
|
77
66
|
const cleanMeta = rawMeta !== void 0 ? stripTypeAssertions(rawMeta) : void 0;
|
|
78
67
|
const cleanRenderMode = rawRenderMode !== void 0 ? stripTypeAssertions(rawRenderMode) : void 0;
|
|
68
|
+
const cleanRevalidate = rawRevalidate !== void 0 ? stripTypeAssertions(rawRevalidate) : void 0;
|
|
79
69
|
const metaLiteral = cleanMeta !== void 0 && isPureLiteral(cleanMeta) ? cleanMeta : void 0;
|
|
80
70
|
const renderModeLiteral = cleanRenderMode !== void 0 && isPureLiteral(cleanRenderMode) ? cleanRenderMode : void 0;
|
|
71
|
+
const revalidateLiteral = cleanRevalidate !== void 0 && isPureLiteral(cleanRevalidate) ? cleanRevalidate : void 0;
|
|
81
72
|
return {
|
|
82
73
|
hasLoader: found.has("loader"),
|
|
83
74
|
hasGuard: found.has("guard"),
|
|
@@ -85,8 +76,13 @@ function detectRouteExports(source) {
|
|
|
85
76
|
hasRenderMode: found.has("renderMode"),
|
|
86
77
|
hasError: found.has("error"),
|
|
87
78
|
hasMiddleware: found.has("middleware"),
|
|
79
|
+
hasLoaderKey: found.has("loaderKey"),
|
|
80
|
+
hasGcTime: found.has("gcTime"),
|
|
81
|
+
hasGetStaticPaths: found.has("getStaticPaths"),
|
|
82
|
+
hasRevalidate: found.has("revalidate"),
|
|
88
83
|
...metaLiteral !== void 0 ? { metaLiteral } : {},
|
|
89
|
-
...renderModeLiteral !== void 0 ? { renderModeLiteral } : {}
|
|
84
|
+
...renderModeLiteral !== void 0 ? { renderModeLiteral } : {},
|
|
85
|
+
...revalidateLiteral !== void 0 ? { revalidateLiteral } : {}
|
|
90
86
|
};
|
|
91
87
|
}
|
|
92
88
|
/**
|
|
@@ -568,7 +564,11 @@ const EMPTY_EXPORTS = {
|
|
|
568
564
|
hasMeta: false,
|
|
569
565
|
hasRenderMode: false,
|
|
570
566
|
hasError: false,
|
|
571
|
-
hasMiddleware: false
|
|
567
|
+
hasMiddleware: false,
|
|
568
|
+
hasLoaderKey: false,
|
|
569
|
+
hasGcTime: false,
|
|
570
|
+
hasGetStaticPaths: false,
|
|
571
|
+
hasRevalidate: false
|
|
572
572
|
};
|
|
573
573
|
/**
|
|
574
574
|
* True if a route file declares ANY metadata export.
|
|
@@ -576,7 +576,7 @@ const EMPTY_EXPORTS = {
|
|
|
576
576
|
* `import * as mod` (for metadata access) instead of lazy().
|
|
577
577
|
*/
|
|
578
578
|
function hasAnyMetaExport(exports) {
|
|
579
|
-
return exports.hasLoader || exports.hasGuard || exports.hasMeta || exports.hasRenderMode || exports.hasError || exports.hasMiddleware;
|
|
579
|
+
return exports.hasLoader || exports.hasGuard || exports.hasMeta || exports.hasRenderMode || exports.hasError || exports.hasMiddleware || exports.hasLoaderKey || exports.hasGcTime || exports.hasGetStaticPaths;
|
|
580
580
|
}
|
|
581
581
|
/**
|
|
582
582
|
* Parse a set of file paths (relative to routes dir) into FileRoute objects.
|
|
@@ -776,6 +776,9 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
776
776
|
props.push(`${indent} component: ${mod}.default`);
|
|
777
777
|
if (exp.hasLoader) props.push(`${indent} loader: ${mod}.loader`);
|
|
778
778
|
if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`);
|
|
779
|
+
if (exp.hasLoaderKey) props.push(`${indent} loaderKey: ${mod}.loaderKey`);
|
|
780
|
+
if (exp.hasGcTime) props.push(`${indent} gcTime: ${mod}.gcTime`);
|
|
781
|
+
if (exp.hasGetStaticPaths) props.push(`${indent} getStaticPaths: ${mod}.getStaticPaths`);
|
|
779
782
|
if (exp.hasMeta || exp.hasRenderMode) {
|
|
780
783
|
const metaParts = [];
|
|
781
784
|
if (exp.hasMeta) metaParts.push(`...${mod}.meta`);
|
|
@@ -793,7 +796,7 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
793
796
|
}
|
|
794
797
|
else {
|
|
795
798
|
const inlineableMeta = (!exp.hasMeta || exp.metaLiteral !== void 0) && (!exp.hasRenderMode || exp.renderModeLiteral !== void 0);
|
|
796
|
-
const needsFunctionExports = exp.hasLoader || exp.hasGuard || exp.hasError;
|
|
799
|
+
const needsFunctionExports = exp.hasLoader || exp.hasGuard || exp.hasError || exp.hasGetStaticPaths;
|
|
797
800
|
if (hasMeta && inlineableMeta && !needsFunctionExports) {
|
|
798
801
|
const comp = nextLazy(page.filePath, loadingName, errorName);
|
|
799
802
|
props.push(`${indent} component: ${comp}`);
|
|
@@ -805,6 +808,18 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
805
808
|
props.push(`${indent} component: ${comp}`);
|
|
806
809
|
if (exp.hasLoader) props.push(`${indent} loader: (ctx) => import("${fullPath}").then((m) => m.loader(ctx))`);
|
|
807
810
|
if (exp.hasGuard) props.push(`${indent} beforeEnter: (to, from) => import("${fullPath}").then((m) => m.guard(to, from))`);
|
|
811
|
+
if (exp.hasLoaderKey) {
|
|
812
|
+
const mod = nextModuleImport(page.filePath);
|
|
813
|
+
props.push(`${indent} loaderKey: ${mod}.loaderKey`);
|
|
814
|
+
}
|
|
815
|
+
if (exp.hasGcTime) {
|
|
816
|
+
const mod = nextModuleImport(page.filePath);
|
|
817
|
+
props.push(`${indent} gcTime: ${mod}.gcTime`);
|
|
818
|
+
}
|
|
819
|
+
if (exp.hasGetStaticPaths) {
|
|
820
|
+
const mod = nextModuleImport(page.filePath);
|
|
821
|
+
props.push(`${indent} getStaticPaths: ${mod}.getStaticPaths`);
|
|
822
|
+
}
|
|
808
823
|
emitInlineMeta(exp, props, indent);
|
|
809
824
|
if (errorName) {
|
|
810
825
|
const errorRef = exp.hasError ? `lazy(() => import("${fullPath}").then((m) => ({ default: m.error })))` : errorName;
|
|
@@ -816,6 +831,9 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
816
831
|
props.push(`${indent} component: ${mod}.default`);
|
|
817
832
|
if (exp.hasLoader) props.push(`${indent} loader: ${mod}.loader`);
|
|
818
833
|
if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`);
|
|
834
|
+
if (exp.hasLoaderKey) props.push(`${indent} loaderKey: ${mod}.loaderKey`);
|
|
835
|
+
if (exp.hasGcTime) props.push(`${indent} gcTime: ${mod}.gcTime`);
|
|
836
|
+
if (exp.hasGetStaticPaths) props.push(`${indent} getStaticPaths: ${mod}.getStaticPaths`);
|
|
819
837
|
if (exp.hasMeta || exp.hasRenderMode) {
|
|
820
838
|
const metaParts = [];
|
|
821
839
|
if (exp.hasMeta) metaParts.push(`...${mod}.meta`);
|
|
@@ -849,6 +867,8 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
849
867
|
if (layoutMod !== void 0) {
|
|
850
868
|
if (exp.hasLoader) props.push(`${indent}loader: ${layoutMod}.loader`);
|
|
851
869
|
if (exp.hasGuard) props.push(`${indent}beforeEnter: ${layoutMod}.guard`);
|
|
870
|
+
if (exp.hasLoaderKey) props.push(`${indent}loaderKey: ${layoutMod}.loaderKey`);
|
|
871
|
+
if (exp.hasGcTime) props.push(`${indent}gcTime: ${layoutMod}.gcTime`);
|
|
852
872
|
if (exp.hasMeta || exp.hasRenderMode) {
|
|
853
873
|
const metaParts = [];
|
|
854
874
|
if (exp.hasMeta) metaParts.push(`...${layoutMod}.meta`);
|
|
@@ -885,6 +905,12 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
885
905
|
/**
|
|
886
906
|
* Generate a virtual module that maps URL patterns to their middleware exports.
|
|
887
907
|
* Used by the server entry to dispatch per-route middleware.
|
|
908
|
+
*
|
|
909
|
+
* Detects whether each route file actually exports `middleware` (via
|
|
910
|
+
* `detectRouteExports` source scanning) and only emits an import for files
|
|
911
|
+
* that do. The `lazy()` import path tolerates missing exports, but the SSG
|
|
912
|
+
* static-import path fails Rolldown's missing-export check at build time —
|
|
913
|
+
* skipping no-middleware files keeps both paths working.
|
|
888
914
|
*/
|
|
889
915
|
function generateMiddlewareModule(files, routesDir) {
|
|
890
916
|
const routes = parseFileRoutes(files);
|
|
@@ -893,6 +919,11 @@ function generateMiddlewareModule(files, routesDir) {
|
|
|
893
919
|
let counter = 0;
|
|
894
920
|
for (const route of routes) {
|
|
895
921
|
if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue;
|
|
922
|
+
let hasMw = false;
|
|
923
|
+
try {
|
|
924
|
+
hasMw = detectRouteExports(readFileSync(`${routesDir}/${route.filePath}`, "utf-8")).hasMiddleware;
|
|
925
|
+
} catch {}
|
|
926
|
+
if (!hasMw) continue;
|
|
896
927
|
const name = `_mw${counter++}`;
|
|
897
928
|
const fullPath = `${routesDir}/${route.filePath}`;
|
|
898
929
|
imports.push(`import { middleware as ${name} } from "${fullPath}"`);
|
|
@@ -937,7 +968,8 @@ async function scanRouteFiles(routesDir) {
|
|
|
937
968
|
*/
|
|
938
969
|
async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
939
970
|
const { readFile } = await import("node:fs/promises");
|
|
940
|
-
const
|
|
971
|
+
const { isApiRoute } = await import("./api-routes-Ci0kVmM4.js").then((n) => n.t);
|
|
972
|
+
const files = (await scanRouteFiles(routesDir)).filter((f) => !isApiRoute(f));
|
|
941
973
|
const exportsMap = /* @__PURE__ */ new Map();
|
|
942
974
|
await Promise.all(files.map(async (filePath) => {
|
|
943
975
|
try {
|
|
@@ -952,4 +984,4 @@ async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
|
952
984
|
|
|
953
985
|
//#endregion
|
|
954
986
|
export { generateRouteModuleFromRoutes as a, scanRouteFilesWithExports as c, generateRouteModule as i, fs_router_exports as n, parseFileRoutes as o, generateMiddlewareModule as r, scanRouteFiles as s, filePathToUrlPath as t };
|
|
955
|
-
//# sourceMappingURL=fs-router-
|
|
987
|
+
//# sourceMappingURL=fs-router-MewHc5SB.js.map
|
package/lib/i18n-routing.js
CHANGED
|
@@ -42,6 +42,117 @@ function buildLocalePath(path, locale, defaultLocale, strategy) {
|
|
|
42
42
|
return `/${locale}${clean}`;
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
+
* Fan a `FileRoute[]` into per-locale duplicates so the file-system router
|
|
46
|
+
* knows about every localized URL pattern at build time. PR H — was the
|
|
47
|
+
* missing half of the i18n story before this PR (the `i18nRouting()` Vite
|
|
48
|
+
* plugin only handled request-time locale detection; routes themselves
|
|
49
|
+
* were never duplicated, so static-host SSG outputs and SSR matching had
|
|
50
|
+
* no `/de/about` / `/cs/about` records to render against).
|
|
51
|
+
*
|
|
52
|
+
* Strategy semantics:
|
|
53
|
+
*
|
|
54
|
+
* - **`prefix-except-default`** (default): the default locale's routes
|
|
55
|
+
* keep their original `urlPath` unchanged (`/about` stays `/about`); all
|
|
56
|
+
* non-default locales get a prefix (`/de/about`, `/cs/about`). Best for
|
|
57
|
+
* SEO-on-default-locale apps — search engines see canonical URLs at
|
|
58
|
+
* `/about` while non-default speakers get explicit prefixes.
|
|
59
|
+
*
|
|
60
|
+
* - **`prefix`**: every locale gets its own prefix, including the default
|
|
61
|
+
* (`/en/about`, `/de/about`, `/cs/about`). Root `/` becomes `/en` /
|
|
62
|
+
* `/de` / `/cs`. Better when no locale is "primary" — every URL
|
|
63
|
+
* self-identifies its locale.
|
|
64
|
+
*
|
|
65
|
+
* Layouts, error boundaries, loading components, and 404 pages duplicate
|
|
66
|
+
* along with their pages — same source file (same `filePath`), new
|
|
67
|
+
* locale-prefixed `urlPath` / `dirPath` / `depth`. The route tree built
|
|
68
|
+
* from the expanded array therefore has one fully-formed subtree per
|
|
69
|
+
* locale, so layout matching, dynamic params (`[id]` → `:id`), and
|
|
70
|
+
* catch-all routes (`[...slug]` → `:slug*`) all compose naturally with
|
|
71
|
+
* the locale prefix — no special cases.
|
|
72
|
+
*
|
|
73
|
+
* `getStaticPaths` composition (for SSG): each duplicate route inherits
|
|
74
|
+
* the same `exports.getStaticPaths`. The SSG plugin's `expandUrlPattern`
|
|
75
|
+
* step then expands `/blog/[slug]` × `[en, de]` × `getStaticPaths()
|
|
76
|
+
* → ['a', 'b']` into `/blog/a`, `/blog/b`, `/de/blog/a`, `/de/blog/b`
|
|
77
|
+
* (or all six prefixed forms under `'prefix'` strategy). Cardinality
|
|
78
|
+
* compounds, which is by design — `ssg.concurrency` (PR D) limits
|
|
79
|
+
* in-flight renders independent of route count.
|
|
80
|
+
*
|
|
81
|
+
* No-op when `config.locales` is empty or contains only the default
|
|
82
|
+
* locale (prefix-except-default strategy with no other locales) — returns
|
|
83
|
+
* the input array unchanged. Always return a fresh array on duplication
|
|
84
|
+
* so callers don't accidentally mutate cached input.
|
|
85
|
+
*
|
|
86
|
+
* Reference: the helper is called from `vite-plugin.ts`'s virtual route
|
|
87
|
+
* module load AND `ssg-plugin.ts`'s pre-render path expansion. Tested in
|
|
88
|
+
* isolation — duplication is a pure transform on FileRoute[] with no
|
|
89
|
+
* filesystem or network side effects.
|
|
90
|
+
*/
|
|
91
|
+
function expandRoutesForLocales(routes, config) {
|
|
92
|
+
const strategy = config.strategy ?? "prefix-except-default";
|
|
93
|
+
const { locales, defaultLocale } = config;
|
|
94
|
+
if (locales.length === 0) return routes;
|
|
95
|
+
for (const locale of locales) validateLocale(locale);
|
|
96
|
+
validateLocale(defaultLocale);
|
|
97
|
+
if (strategy === "prefix-except-default" && locales.length === 1 && locales[0] === defaultLocale) return routes;
|
|
98
|
+
const expanded = [];
|
|
99
|
+
for (const route of routes) for (const locale of locales) {
|
|
100
|
+
if (strategy === "prefix-except-default" && locale === defaultLocale) {
|
|
101
|
+
expanded.push(route);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (strategy === "prefix-except-default" && route.isLayout && route.urlPath === "/") continue;
|
|
105
|
+
const newUrlPath = prefixUrlPath(route.urlPath, locale);
|
|
106
|
+
const newDirPath = route.dirPath === "" ? locale : `${locale}/${route.dirPath}`;
|
|
107
|
+
const newDepth = newUrlPath === "/" ? 0 : newUrlPath.split("/").filter(Boolean).length;
|
|
108
|
+
expanded.push({
|
|
109
|
+
...route,
|
|
110
|
+
urlPath: newUrlPath,
|
|
111
|
+
dirPath: newDirPath,
|
|
112
|
+
depth: newDepth
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return expanded;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Prepend `/locale` to a URL pattern. Handles three shapes:
|
|
119
|
+
* `/` → `/de`
|
|
120
|
+
* `/about` → `/de/about`
|
|
121
|
+
* `/users/:id` / `/blog/:slug*` → `/de/users/:id` / `/de/blog/:slug*`
|
|
122
|
+
*
|
|
123
|
+
* Internal helper to `expandRoutesForLocales`; not exported because the
|
|
124
|
+
* public surface for path-building is `buildLocalePath` (which strips
|
|
125
|
+
* existing locale prefixes — different semantics).
|
|
126
|
+
*/
|
|
127
|
+
function prefixUrlPath(urlPath, locale) {
|
|
128
|
+
if (urlPath === "/") return `/${locale}`;
|
|
129
|
+
return `/${locale}${urlPath}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validate a locale string (PR L2).
|
|
133
|
+
*
|
|
134
|
+
* The locale drives both URL pattern emission AND filesystem writes
|
|
135
|
+
* (see `expandRoutesForLocales` for full rationale). Reject input that
|
|
136
|
+
* would either:
|
|
137
|
+
* - break path-traversal boundaries (`..`, `/`, `\`)
|
|
138
|
+
* - produce invalid URL segments (whitespace, NUL)
|
|
139
|
+
* - create hidden-file artifacts (`.` leading)
|
|
140
|
+
* - silently kill the app (empty string)
|
|
141
|
+
*
|
|
142
|
+
* Throws with an actionable `[Pyreon]` error message. Called per-locale
|
|
143
|
+
* by `expandRoutesForLocales` after the empty-locales no-op guard.
|
|
144
|
+
*
|
|
145
|
+
* @internal — exported for unit testing.
|
|
146
|
+
*/
|
|
147
|
+
function validateLocale(locale) {
|
|
148
|
+
if (typeof locale !== "string" || locale === "") throw new Error(`[Pyreon] Invalid i18n locale: ${JSON.stringify(locale)}. Locales must be non-empty strings (e.g. "en", "de", "en-US").`);
|
|
149
|
+
if (locale.trim() !== locale) throw new Error(`[Pyreon] Invalid i18n locale: ${JSON.stringify(locale)}. Leading or trailing whitespace not allowed.`);
|
|
150
|
+
if (locale.includes("/") || locale.includes("\\")) throw new Error(`[Pyreon] Invalid i18n locale: ${JSON.stringify(locale)}. Path separators ("/", "\\\\") not allowed — they would break URL emission and could write outside the dist directory.`);
|
|
151
|
+
if (locale === ".." || locale === ".") throw new Error(`[Pyreon] Invalid i18n locale: ${JSON.stringify(locale)}. Path-traversal segments not allowed.`);
|
|
152
|
+
if (locale.startsWith(".")) throw new Error(`[Pyreon] Invalid i18n locale: ${JSON.stringify(locale)}. Leading dot not allowed — it would create a hidden-file directory (\`dist/.${locale.slice(1)}/\`) invisible to most file listings.`);
|
|
153
|
+
if (locale.includes("\0")) throw new Error(`[Pyreon] Invalid i18n locale: ${JSON.stringify(locale)}. NUL characters not allowed.`);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
45
156
|
* Create a LocaleContext for use in components and loaders.
|
|
46
157
|
*/
|
|
47
158
|
function createLocaleContext(locale, path, config) {
|
|
@@ -163,5 +274,5 @@ function setLocale(locale, config) {
|
|
|
163
274
|
}
|
|
164
275
|
|
|
165
276
|
//#endregion
|
|
166
|
-
export { LocaleCtx, buildLocalePath, createLocaleContext, detectLocaleFromHeader, extractLocaleFromPath, i18nRouting, localeSignal, setLocale, useLocale };
|
|
277
|
+
export { LocaleCtx, buildLocalePath, createLocaleContext, detectLocaleFromHeader, expandRoutesForLocales, extractLocaleFromPath, i18nRouting, localeSignal, setLocale, useLocale, validateLocale };
|
|
167
278
|
//# sourceMappingURL=i18n-routing.js.map
|
package/lib/image-plugin.js
CHANGED
|
@@ -11,9 +11,13 @@ function warnSharpMissing() {
|
|
|
11
11
|
}
|
|
12
12
|
/** Built-in CDN providers. */
|
|
13
13
|
const cdnProviders = {
|
|
14
|
+
/** Cloudinary: `https://res.cloudinary.com/{cloud}/image/upload/...` */
|
|
14
15
|
cloudinary: (cloudName) => (src, { width, quality, format }) => `https://res.cloudinary.com/${cloudName}/image/upload/w_${width},q_${quality},f_${format}/${src}`,
|
|
16
|
+
/** Imgix: `https://{domain}.imgix.net/...?w=...&q=...&fm=...` */
|
|
15
17
|
imgix: (domain) => (src, { width, quality, format }) => `https://${domain}.imgix.net/${src}?w=${width}&q=${quality}&fm=${format}&auto=format`,
|
|
18
|
+
/** Vercel Image Optimization: `/_next/image?url=...&w=...&q=...` */
|
|
16
19
|
vercel: () => (src, { width, quality }) => `/_vercel/image?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`,
|
|
20
|
+
/** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */
|
|
17
21
|
bunny: (pullZone) => (src, { width, quality }) => `https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`
|
|
18
22
|
};
|
|
19
23
|
const IMAGE_EXT_RE = /\.(jpe?g|png|webp|avif)$/i;
|