@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.
Files changed (114) hide show
  1. package/lib/api-routes-Ci0kVmM4.js +146 -0
  2. package/lib/client.js +7 -2
  3. package/lib/csp.js +19 -9
  4. package/lib/env.js +6 -6
  5. package/lib/font.js +3 -3
  6. package/lib/{fs-router-CQ7Zxeca.js → fs-router-MewHc5SB.js} +56 -24
  7. package/lib/i18n-routing.js +112 -1
  8. package/lib/image-plugin.js +4 -0
  9. package/lib/image.js +141 -108
  10. package/lib/index.js +253 -132
  11. package/lib/link.js +1 -49
  12. package/lib/og-image.js +5 -5
  13. package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
  14. package/lib/script.js +115 -74
  15. package/lib/seo.js +186 -15
  16. package/lib/server.js +275 -1247
  17. package/lib/theme.js +1 -50
  18. package/lib/types/config.d.ts +275 -3
  19. package/lib/types/env.d.ts +2 -2
  20. package/lib/types/i18n-routing.d.ts +197 -6
  21. package/lib/types/image.d.ts +105 -5
  22. package/lib/types/index.d.ts +640 -178
  23. package/lib/types/link.d.ts +3 -3
  24. package/lib/types/script.d.ts +78 -6
  25. package/lib/types/seo.d.ts +128 -4
  26. package/lib/types/server.d.ts +603 -77
  27. package/lib/types/theme.d.ts +2 -2
  28. package/lib/vite-plugin-xjWZwudX.js +2454 -0
  29. package/package.json +16 -13
  30. package/src/adapters/bun.ts +20 -1
  31. package/src/adapters/cloudflare.ts +78 -1
  32. package/src/adapters/index.ts +25 -3
  33. package/src/adapters/netlify.ts +63 -1
  34. package/src/adapters/node.ts +25 -1
  35. package/src/adapters/static.ts +26 -1
  36. package/src/adapters/validate.ts +8 -1
  37. package/src/adapters/vercel.ts +76 -1
  38. package/src/adapters/warn-missing-env.ts +49 -0
  39. package/src/app.ts +35 -1
  40. package/src/client.ts +18 -0
  41. package/src/csp.ts +28 -12
  42. package/src/entry-server.ts +55 -5
  43. package/src/env.ts +7 -7
  44. package/src/font.ts +3 -3
  45. package/src/fs-router.ts +123 -4
  46. package/src/i18n-routing.ts +246 -12
  47. package/src/image.tsx +242 -91
  48. package/src/index.ts +4 -4
  49. package/src/isr.ts +24 -6
  50. package/src/manifest.ts +675 -0
  51. package/src/og-image.ts +5 -5
  52. package/src/script.tsx +159 -36
  53. package/src/seo.ts +346 -15
  54. package/src/server.ts +10 -2
  55. package/src/ssg-plugin.ts +1523 -0
  56. package/src/types.ts +329 -19
  57. package/src/vercel-revalidate-handler.ts +204 -0
  58. package/src/vite-plugin.ts +326 -68
  59. package/lib/actions.js.map +0 -1
  60. package/lib/ai.js.map +0 -1
  61. package/lib/api-routes.js.map +0 -1
  62. package/lib/cache.js.map +0 -1
  63. package/lib/client.js.map +0 -1
  64. package/lib/compression.js.map +0 -1
  65. package/lib/config.js.map +0 -1
  66. package/lib/cors.js.map +0 -1
  67. package/lib/csp.js.map +0 -1
  68. package/lib/env.js.map +0 -1
  69. package/lib/favicon.js.map +0 -1
  70. package/lib/font.js.map +0 -1
  71. package/lib/fs-router-3xzp-4Wj.js.map +0 -1
  72. package/lib/fs-router-CQ7Zxeca.js.map +0 -1
  73. package/lib/i18n-routing.js.map +0 -1
  74. package/lib/image-plugin.js.map +0 -1
  75. package/lib/image.js.map +0 -1
  76. package/lib/index.js.map +0 -1
  77. package/lib/link.js.map +0 -1
  78. package/lib/logger.js.map +0 -1
  79. package/lib/meta.js.map +0 -1
  80. package/lib/middleware.js.map +0 -1
  81. package/lib/og-image.js.map +0 -1
  82. package/lib/rate-limit.js.map +0 -1
  83. package/lib/script.js.map +0 -1
  84. package/lib/seo.js.map +0 -1
  85. package/lib/server.js.map +0 -1
  86. package/lib/testing.js.map +0 -1
  87. package/lib/theme.js.map +0 -1
  88. package/lib/types/actions.d.ts.map +0 -1
  89. package/lib/types/ai.d.ts.map +0 -1
  90. package/lib/types/api-routes.d.ts.map +0 -1
  91. package/lib/types/cache.d.ts.map +0 -1
  92. package/lib/types/client.d.ts.map +0 -1
  93. package/lib/types/compression.d.ts.map +0 -1
  94. package/lib/types/config.d.ts.map +0 -1
  95. package/lib/types/cors.d.ts.map +0 -1
  96. package/lib/types/csp.d.ts.map +0 -1
  97. package/lib/types/env.d.ts.map +0 -1
  98. package/lib/types/favicon.d.ts.map +0 -1
  99. package/lib/types/font.d.ts.map +0 -1
  100. package/lib/types/i18n-routing.d.ts.map +0 -1
  101. package/lib/types/image-plugin.d.ts.map +0 -1
  102. package/lib/types/image.d.ts.map +0 -1
  103. package/lib/types/index.d.ts.map +0 -1
  104. package/lib/types/link.d.ts.map +0 -1
  105. package/lib/types/logger.d.ts.map +0 -1
  106. package/lib/types/meta.d.ts.map +0 -1
  107. package/lib/types/middleware.d.ts.map +0 -1
  108. package/lib/types/og-image.d.ts.map +0 -1
  109. package/lib/types/rate-limit.d.ts.map +0 -1
  110. package/lib/types/script.d.ts.map +0 -1
  111. package/lib/types/seo.d.ts.map +0 -1
  112. package/lib/types/server.d.ts.map +0 -1
  113. package/lib/types/testing.d.ts.map +0 -1
  114. 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 Layout = options.layout ?? DefaultLayout;
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 !== "undefined" && crypto.getRandomValues) {
70
- const bytes = new Uint8Array(16);
71
- crypto.getRandomValues(bytes);
72
- let binary = "";
73
- for (const byte of bytes) binary += String.fromCharCode(byte);
74
- return typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
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(schema, source) {
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(schema)) {
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(schema) {
170
+ function publicEnv(envSchema) {
171
171
  const prefix = "ZERO_PUBLIC_";
172
172
  const env = typeof process !== "undefined" ? process.env : {};
173
- if (!schema) {
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(schema)) prefixedSource[key] = env[`${prefix}${key}`];
180
- return validateEnv(schema, prefixedSource);
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 parts = entry.split(",");
50
- const weight = Number(parts[parts.length - 1]);
49
+ const tuple = entry.split(",");
50
+ const weight = Number(tuple[tuple.length - 1]);
51
51
  if (weight > 0) weights.add(weight);
52
- if (parts[0] === "1") italic = true;
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 files = await scanRouteFiles(routesDir);
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-CQ7Zxeca.js.map
987
+ //# sourceMappingURL=fs-router-MewHc5SB.js.map
@@ -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
@@ -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;