@pyreon/zero 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/{api-routes-DANluJic.js → api-routes-Ci0kVmM4.js} +2 -2
- package/lib/client.js +4 -1
- package/lib/env.js +6 -6
- package/lib/font.js +3 -3
- package/lib/{fs-router-ZebyutPa.js → fs-router-MewHc5SB.js} +25 -30
- package/lib/i18n-routing.js +112 -1
- package/lib/image.js +140 -58
- package/lib/index.js +252 -82
- package/lib/og-image.js +5 -5
- package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
- package/lib/script.js +114 -25
- package/lib/seo.js +186 -15
- package/lib/server.js +274 -564
- package/lib/types/config.d.ts +275 -3
- package/lib/types/env.d.ts +2 -2
- package/lib/types/i18n-routing.d.ts +193 -2
- package/lib/types/image.d.ts +105 -5
- package/lib/types/index.d.ts +634 -182
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +575 -72
- package/lib/vite-plugin-xjWZwudX.js +2454 -0
- package/package.json +11 -10
- package/src/adapters/bun.ts +20 -1
- package/src/adapters/cloudflare.ts +78 -1
- package/src/adapters/index.ts +25 -3
- package/src/adapters/netlify.ts +63 -1
- package/src/adapters/node.ts +25 -1
- package/src/adapters/static.ts +26 -1
- package/src/adapters/validate.ts +8 -1
- package/src/adapters/vercel.ts +76 -1
- package/src/adapters/warn-missing-env.ts +49 -0
- package/src/app.ts +14 -0
- package/src/client.ts +18 -0
- package/src/entry-server.ts +55 -5
- package/src/env.ts +7 -7
- package/src/font.ts +3 -3
- package/src/fs-router.ts +72 -3
- package/src/i18n-routing.ts +246 -12
- package/src/image.tsx +242 -91
- package/src/index.ts +4 -4
- package/src/isr.ts +24 -6
- package/src/manifest.ts +675 -0
- package/src/og-image.ts +5 -5
- package/src/script.tsx +159 -36
- package/src/seo.ts +346 -15
- package/src/server.ts +10 -2
- package/src/ssg-plugin.ts +1211 -54
- package/src/types.ts +301 -10
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +108 -30
- package/lib/vite-plugin-E4BHYvYW.js +0 -855
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-CjeV3_4I.js";
|
|
2
2
|
|
|
3
3
|
//#region src/api-routes.ts
|
|
4
4
|
var api_routes_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -143,4 +143,4 @@ function generateApiRouteModule(files, routesDir) {
|
|
|
143
143
|
|
|
144
144
|
//#endregion
|
|
145
145
|
export { matchApiRoute as i, createApiMiddleware as n, generateApiRouteModule as r, api_routes_exports as t };
|
|
146
|
-
//# sourceMappingURL=api-routes-
|
|
146
|
+
//# sourceMappingURL=api-routes-Ci0kVmM4.js.map
|
package/lib/client.js
CHANGED
|
@@ -14,6 +14,7 @@ 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
20
|
const hasLayoutInRoutes = options.layout !== void 0 && options.routes.some((r) => r.component === options.layout);
|
|
@@ -72,10 +73,12 @@ function startClient(options) {
|
|
|
72
73
|
if (typeof document === "undefined") throw new Error("[Pyreon] startClient() can only be called in the browser.");
|
|
73
74
|
const container = document.getElementById("app");
|
|
74
75
|
if (!container) throw new Error("[Pyreon] Missing #app container element");
|
|
76
|
+
const base = typeof __ZERO_BASE__ !== "undefined" && __ZERO_BASE__ !== "/" ? __ZERO_BASE__ : void 0;
|
|
75
77
|
const { App, router } = createApp({
|
|
76
78
|
routes: options.routes,
|
|
77
79
|
routerMode: "history",
|
|
78
|
-
...options.layout ? { layout: options.layout } : {}
|
|
80
|
+
...options.layout ? { layout: options.layout } : {},
|
|
81
|
+
...base ? { base } : {}
|
|
79
82
|
});
|
|
80
83
|
const ssrLoaderData = window.__PYREON_LOADER_DATA__;
|
|
81
84
|
const hasSSRLoaderData = ssrLoaderData !== void 0 && typeof ssrLoaderData === "object" && ssrLoaderData !== null;
|
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,27 +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
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) {
|
|
20
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
21
|
-
throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
//#endregion
|
|
25
5
|
//#region src/fs-router.ts
|
|
26
6
|
var fs_router_exports = /* @__PURE__ */ __exportAll({
|
|
27
7
|
detectRouteExports: () => detectRouteExports,
|
|
@@ -50,7 +30,9 @@ const ROUTE_EXPORT_NAMES = [
|
|
|
50
30
|
"error",
|
|
51
31
|
"middleware",
|
|
52
32
|
"loaderKey",
|
|
53
|
-
"gcTime"
|
|
33
|
+
"gcTime",
|
|
34
|
+
"getStaticPaths",
|
|
35
|
+
"revalidate"
|
|
54
36
|
];
|
|
55
37
|
/**
|
|
56
38
|
* Detect which optional metadata exports a route file source declares.
|
|
@@ -80,10 +62,13 @@ function detectRouteExports(source) {
|
|
|
80
62
|
} else for (const name of tok.names) if (ROUTE_EXPORT_NAMES.includes(name)) found.add(name);
|
|
81
63
|
const rawMeta = found.has("meta") ? extractLiteralExport(source, "meta") : void 0;
|
|
82
64
|
const rawRenderMode = found.has("renderMode") ? extractLiteralExport(source, "renderMode") : void 0;
|
|
65
|
+
const rawRevalidate = found.has("revalidate") ? extractLiteralExport(source, "revalidate") : void 0;
|
|
83
66
|
const cleanMeta = rawMeta !== void 0 ? stripTypeAssertions(rawMeta) : void 0;
|
|
84
67
|
const cleanRenderMode = rawRenderMode !== void 0 ? stripTypeAssertions(rawRenderMode) : void 0;
|
|
68
|
+
const cleanRevalidate = rawRevalidate !== void 0 ? stripTypeAssertions(rawRevalidate) : void 0;
|
|
85
69
|
const metaLiteral = cleanMeta !== void 0 && isPureLiteral(cleanMeta) ? cleanMeta : void 0;
|
|
86
70
|
const renderModeLiteral = cleanRenderMode !== void 0 && isPureLiteral(cleanRenderMode) ? cleanRenderMode : void 0;
|
|
71
|
+
const revalidateLiteral = cleanRevalidate !== void 0 && isPureLiteral(cleanRevalidate) ? cleanRevalidate : void 0;
|
|
87
72
|
return {
|
|
88
73
|
hasLoader: found.has("loader"),
|
|
89
74
|
hasGuard: found.has("guard"),
|
|
@@ -93,8 +78,11 @@ function detectRouteExports(source) {
|
|
|
93
78
|
hasMiddleware: found.has("middleware"),
|
|
94
79
|
hasLoaderKey: found.has("loaderKey"),
|
|
95
80
|
hasGcTime: found.has("gcTime"),
|
|
81
|
+
hasGetStaticPaths: found.has("getStaticPaths"),
|
|
82
|
+
hasRevalidate: found.has("revalidate"),
|
|
96
83
|
...metaLiteral !== void 0 ? { metaLiteral } : {},
|
|
97
|
-
...renderModeLiteral !== void 0 ? { renderModeLiteral } : {}
|
|
84
|
+
...renderModeLiteral !== void 0 ? { renderModeLiteral } : {},
|
|
85
|
+
...revalidateLiteral !== void 0 ? { revalidateLiteral } : {}
|
|
98
86
|
};
|
|
99
87
|
}
|
|
100
88
|
/**
|
|
@@ -578,7 +566,9 @@ const EMPTY_EXPORTS = {
|
|
|
578
566
|
hasError: false,
|
|
579
567
|
hasMiddleware: false,
|
|
580
568
|
hasLoaderKey: false,
|
|
581
|
-
hasGcTime: false
|
|
569
|
+
hasGcTime: false,
|
|
570
|
+
hasGetStaticPaths: false,
|
|
571
|
+
hasRevalidate: false
|
|
582
572
|
};
|
|
583
573
|
/**
|
|
584
574
|
* True if a route file declares ANY metadata export.
|
|
@@ -586,7 +576,7 @@ const EMPTY_EXPORTS = {
|
|
|
586
576
|
* `import * as mod` (for metadata access) instead of lazy().
|
|
587
577
|
*/
|
|
588
578
|
function hasAnyMetaExport(exports) {
|
|
589
|
-
return exports.hasLoader || exports.hasGuard || exports.hasMeta || exports.hasRenderMode || exports.hasError || exports.hasMiddleware || exports.hasLoaderKey || exports.hasGcTime;
|
|
579
|
+
return exports.hasLoader || exports.hasGuard || exports.hasMeta || exports.hasRenderMode || exports.hasError || exports.hasMiddleware || exports.hasLoaderKey || exports.hasGcTime || exports.hasGetStaticPaths;
|
|
590
580
|
}
|
|
591
581
|
/**
|
|
592
582
|
* Parse a set of file paths (relative to routes dir) into FileRoute objects.
|
|
@@ -788,6 +778,7 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
788
778
|
if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`);
|
|
789
779
|
if (exp.hasLoaderKey) props.push(`${indent} loaderKey: ${mod}.loaderKey`);
|
|
790
780
|
if (exp.hasGcTime) props.push(`${indent} gcTime: ${mod}.gcTime`);
|
|
781
|
+
if (exp.hasGetStaticPaths) props.push(`${indent} getStaticPaths: ${mod}.getStaticPaths`);
|
|
791
782
|
if (exp.hasMeta || exp.hasRenderMode) {
|
|
792
783
|
const metaParts = [];
|
|
793
784
|
if (exp.hasMeta) metaParts.push(`...${mod}.meta`);
|
|
@@ -805,7 +796,7 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
805
796
|
}
|
|
806
797
|
else {
|
|
807
798
|
const inlineableMeta = (!exp.hasMeta || exp.metaLiteral !== void 0) && (!exp.hasRenderMode || exp.renderModeLiteral !== void 0);
|
|
808
|
-
const needsFunctionExports = exp.hasLoader || exp.hasGuard || exp.hasError;
|
|
799
|
+
const needsFunctionExports = exp.hasLoader || exp.hasGuard || exp.hasError || exp.hasGetStaticPaths;
|
|
809
800
|
if (hasMeta && inlineableMeta && !needsFunctionExports) {
|
|
810
801
|
const comp = nextLazy(page.filePath, loadingName, errorName);
|
|
811
802
|
props.push(`${indent} component: ${comp}`);
|
|
@@ -825,6 +816,10 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
825
816
|
const mod = nextModuleImport(page.filePath);
|
|
826
817
|
props.push(`${indent} gcTime: ${mod}.gcTime`);
|
|
827
818
|
}
|
|
819
|
+
if (exp.hasGetStaticPaths) {
|
|
820
|
+
const mod = nextModuleImport(page.filePath);
|
|
821
|
+
props.push(`${indent} getStaticPaths: ${mod}.getStaticPaths`);
|
|
822
|
+
}
|
|
828
823
|
emitInlineMeta(exp, props, indent);
|
|
829
824
|
if (errorName) {
|
|
830
825
|
const errorRef = exp.hasError ? `lazy(() => import("${fullPath}").then((m) => ({ default: m.error })))` : errorName;
|
|
@@ -838,6 +833,7 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
838
833
|
if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`);
|
|
839
834
|
if (exp.hasLoaderKey) props.push(`${indent} loaderKey: ${mod}.loaderKey`);
|
|
840
835
|
if (exp.hasGcTime) props.push(`${indent} gcTime: ${mod}.gcTime`);
|
|
836
|
+
if (exp.hasGetStaticPaths) props.push(`${indent} getStaticPaths: ${mod}.getStaticPaths`);
|
|
841
837
|
if (exp.hasMeta || exp.hasRenderMode) {
|
|
842
838
|
const metaParts = [];
|
|
843
839
|
if (exp.hasMeta) metaParts.push(`...${mod}.meta`);
|
|
@@ -917,7 +913,6 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
917
913
|
* skipping no-middleware files keeps both paths working.
|
|
918
914
|
*/
|
|
919
915
|
function generateMiddlewareModule(files, routesDir) {
|
|
920
|
-
const { readFileSync } = __require("node:fs");
|
|
921
916
|
const routes = parseFileRoutes(files);
|
|
922
917
|
const imports = [];
|
|
923
918
|
const entries = [];
|
|
@@ -973,7 +968,7 @@ async function scanRouteFiles(routesDir) {
|
|
|
973
968
|
*/
|
|
974
969
|
async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
975
970
|
const { readFile } = await import("node:fs/promises");
|
|
976
|
-
const { isApiRoute } = await import("./api-routes-
|
|
971
|
+
const { isApiRoute } = await import("./api-routes-Ci0kVmM4.js").then((n) => n.t);
|
|
977
972
|
const files = (await scanRouteFiles(routesDir)).filter((f) => !isApiRoute(f));
|
|
978
973
|
const exportsMap = /* @__PURE__ */ new Map();
|
|
979
974
|
await Promise.all(files.map(async (filePath) => {
|
|
@@ -988,5 +983,5 @@ async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
|
988
983
|
}
|
|
989
984
|
|
|
990
985
|
//#endregion
|
|
991
|
-
export { generateRouteModuleFromRoutes as a, scanRouteFilesWithExports as c, generateRouteModule as i,
|
|
992
|
-
//# sourceMappingURL=fs-router-
|
|
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 };
|
|
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.js
CHANGED
|
@@ -29,30 +29,34 @@ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px")
|
|
|
29
29
|
//#endregion
|
|
30
30
|
//#region src/image.tsx
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Composable that provides all image optimization behavior — lazy loading,
|
|
33
|
+
* srcset/sizes resolution, format selection, blur-placeholder state,
|
|
34
|
+
* load tracking.
|
|
34
35
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* <Image {...hero} alt="Hero" priority />
|
|
36
|
+
* Use this for full control when `createImage` is too opinionated about
|
|
37
|
+
* the surrounding markup (e.g. custom container layouts, non-`<div>`
|
|
38
|
+
* wrappers, additional overlay elements).
|
|
39
39
|
*
|
|
40
40
|
* @example
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* function MyImage(props: ImageProps) {
|
|
42
|
+
* const img = useImage(props)
|
|
43
|
+
* return (
|
|
44
|
+
* <figure ref={img.containerRef} style={img.containerStyle}>
|
|
45
|
+
* <img
|
|
46
|
+
* src={img.src}
|
|
47
|
+
* srcSet={img.srcSet}
|
|
48
|
+
* sizes={img.sizes}
|
|
49
|
+
* alt={props.alt}
|
|
50
|
+
* loading={img.loading}
|
|
51
|
+
* onLoad={img.handleLoad}
|
|
52
|
+
* style={img.imageStyle}
|
|
53
|
+
* />
|
|
54
|
+
* <figcaption>{props.alt}</figcaption>
|
|
55
|
+
* </figure>
|
|
56
|
+
* )
|
|
57
|
+
* }
|
|
43
58
|
*/
|
|
44
|
-
function
|
|
45
|
-
if (props.raw) return /* @__PURE__ */ jsx("img", {
|
|
46
|
-
src: props.src,
|
|
47
|
-
alt: props.alt,
|
|
48
|
-
width: props.width,
|
|
49
|
-
height: props.height,
|
|
50
|
-
class: props.class,
|
|
51
|
-
style: props.style,
|
|
52
|
-
decoding: props.decoding ?? "async",
|
|
53
|
-
loading: props.loading ?? "lazy",
|
|
54
|
-
fetchPriority: props.priority ? "high" : void 0
|
|
55
|
-
});
|
|
59
|
+
function useImage(props) {
|
|
56
60
|
const isEager = props.priority || props.loading === "eager";
|
|
57
61
|
const loaded = signal(isEager);
|
|
58
62
|
const inView = signal(isEager);
|
|
@@ -60,7 +64,7 @@ function Image(props) {
|
|
|
60
64
|
const resolvedSrcset = typeof props.srcset === "string" ? props.srcset : props.srcset?.map((s) => `${s.src} ${s.width}w`).join(", ");
|
|
61
65
|
const sizes = props.sizes ?? "100vw";
|
|
62
66
|
const fit = props.fit ?? "cover";
|
|
63
|
-
const hasFormats = props.formats && props.formats.length > 0;
|
|
67
|
+
const hasFormats = !!(props.formats && props.formats.length > 0);
|
|
64
68
|
const aspectRatio = `${props.width} / ${props.height}`;
|
|
65
69
|
if (!isEager) useIntersectionObserver(() => containerRef.current ?? void 0, () => inView.set(true));
|
|
66
70
|
const containerStyle = [
|
|
@@ -71,54 +75,132 @@ function Image(props) {
|
|
|
71
75
|
"width: 100%",
|
|
72
76
|
props.style
|
|
73
77
|
].filter(Boolean).join("; ");
|
|
74
|
-
const
|
|
78
|
+
const imageStyle = () => [
|
|
79
|
+
"display: block",
|
|
80
|
+
"width: 100%",
|
|
81
|
+
"height: 100%",
|
|
82
|
+
`object-fit: ${fit}`,
|
|
83
|
+
"transition: opacity 0.3s ease",
|
|
84
|
+
props.placeholder && !loaded() ? "opacity: 0" : "opacity: 1"
|
|
85
|
+
].join("; ");
|
|
86
|
+
const placeholderStyle = () => [
|
|
87
|
+
"position: absolute",
|
|
88
|
+
"inset: 0",
|
|
89
|
+
"width: 100%",
|
|
90
|
+
"height: 100%",
|
|
91
|
+
"object-fit: cover",
|
|
92
|
+
"filter: blur(20px)",
|
|
93
|
+
"transform: scale(1.1)",
|
|
94
|
+
"transition: opacity 0.4s ease",
|
|
95
|
+
loaded() ? "opacity: 0; pointer-events: none" : "opacity: 1"
|
|
96
|
+
].join("; ");
|
|
97
|
+
return {
|
|
98
|
+
containerRef,
|
|
99
|
+
inView,
|
|
100
|
+
loaded,
|
|
75
101
|
src: () => inView() ? props.src : "",
|
|
76
102
|
srcSet: () => !hasFormats && inView() && resolvedSrcset ? resolvedSrcset : "",
|
|
77
103
|
sizes: resolvedSrcset ? sizes : void 0,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
aspectRatio,
|
|
105
|
+
containerStyle,
|
|
106
|
+
imageStyle,
|
|
107
|
+
placeholderStyle,
|
|
81
108
|
loading: isEager ? "eager" : "lazy",
|
|
82
|
-
decoding: props.decoding ?? "async",
|
|
83
109
|
fetchPriority: props.priority ? "high" : void 0,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
handleLoad: () => loaded.set(true),
|
|
111
|
+
formats: props.formats,
|
|
112
|
+
hasFormats
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Higher-order component that wraps any component with image optimization.
|
|
117
|
+
*
|
|
118
|
+
* The wrapped component receives {@link ImageRenderProps} with the pre-rendered
|
|
119
|
+
* `image` JSX (bare `<img>` OR `<picture>` tree depending on formats), the
|
|
120
|
+
* pre-rendered `placeholder` JSX, and the container ref + styles. Consumers
|
|
121
|
+
* compose those pieces with whatever wrapper element / layout they want.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* // Custom figure-based image with caption
|
|
125
|
+
* const FigureImage = createImage((props) => (
|
|
126
|
+
* <figure ref={props.containerRef} class={props.class} style={props.containerStyle}>
|
|
127
|
+
* {props.placeholder}
|
|
128
|
+
* {props.image}
|
|
129
|
+
* <figcaption>Caption goes here</figcaption>
|
|
130
|
+
* </figure>
|
|
131
|
+
* ))
|
|
132
|
+
*
|
|
133
|
+
* // Usage — identical to default <Image>
|
|
134
|
+
* <FigureImage src="/hero.jpg" alt="Hero" width={1200} height={630} />
|
|
135
|
+
*/
|
|
136
|
+
function createImage(Component) {
|
|
137
|
+
return function WrappedImage(props) {
|
|
138
|
+
if (props.raw) return /* @__PURE__ */ jsx("img", {
|
|
139
|
+
src: props.src,
|
|
140
|
+
alt: props.alt,
|
|
141
|
+
width: props.width,
|
|
142
|
+
height: props.height,
|
|
143
|
+
class: props.class,
|
|
144
|
+
style: props.style,
|
|
145
|
+
decoding: props.decoding ?? "async",
|
|
146
|
+
loading: props.loading ?? "lazy",
|
|
147
|
+
fetchPriority: props.priority ? "high" : void 0
|
|
148
|
+
});
|
|
149
|
+
const img = useImage(props);
|
|
150
|
+
const imgEl = /* @__PURE__ */ jsx("img", {
|
|
151
|
+
src: img.src,
|
|
152
|
+
srcSet: img.srcSet,
|
|
153
|
+
sizes: img.sizes,
|
|
154
|
+
alt: props.alt,
|
|
155
|
+
width: props.width,
|
|
156
|
+
height: props.height,
|
|
157
|
+
loading: img.loading,
|
|
158
|
+
decoding: props.decoding ?? "async",
|
|
159
|
+
fetchPriority: img.fetchPriority,
|
|
160
|
+
onLoad: img.handleLoad,
|
|
161
|
+
style: img.imageStyle
|
|
162
|
+
});
|
|
163
|
+
const placeholderEl = props.placeholder ? /* @__PURE__ */ jsx("img", {
|
|
99
164
|
src: props.placeholder,
|
|
100
165
|
alt: "",
|
|
101
166
|
"aria-hidden": "true",
|
|
102
167
|
loading: "eager",
|
|
103
|
-
style:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"width: 100%",
|
|
107
|
-
"height: 100%",
|
|
108
|
-
"object-fit: cover",
|
|
109
|
-
"filter: blur(20px)",
|
|
110
|
-
"transform: scale(1.1)",
|
|
111
|
-
"transition: opacity 0.4s ease",
|
|
112
|
-
loaded() ? "opacity: 0; pointer-events: none" : "opacity: 1"
|
|
113
|
-
].join("; ")
|
|
114
|
-
}), hasFormats ? /* @__PURE__ */ jsxs("picture", { children: [props.formats?.map((fmt) => /* @__PURE__ */ jsx("source", {
|
|
168
|
+
style: img.placeholderStyle
|
|
169
|
+
}) : null;
|
|
170
|
+
const imageEl = img.hasFormats ? /* @__PURE__ */ jsxs("picture", { children: [img.formats?.map((fmt) => /* @__PURE__ */ jsx("source", {
|
|
115
171
|
type: fmt.type,
|
|
116
|
-
srcSet: () => inView() ? fmt.srcset ?? "" : "",
|
|
117
|
-
sizes
|
|
118
|
-
})), imgEl] }) : imgEl
|
|
119
|
-
|
|
172
|
+
srcSet: () => img.inView() ? fmt.srcset ?? "" : "",
|
|
173
|
+
sizes: img.sizes
|
|
174
|
+
})), imgEl] }) : imgEl;
|
|
175
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
176
|
+
containerRef: img.containerRef,
|
|
177
|
+
class: props.class,
|
|
178
|
+
containerStyle: img.containerStyle,
|
|
179
|
+
placeholder: placeholderEl,
|
|
180
|
+
image: imageEl
|
|
181
|
+
});
|
|
182
|
+
};
|
|
120
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Default optimized image component with lazy loading, responsive srcset,
|
|
186
|
+
* `<picture>` multi-format support, and blur-up placeholders.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* // With imagePlugin — spread the import directly
|
|
190
|
+
* import hero from "./hero.jpg?optimize"
|
|
191
|
+
* <Image {...hero} alt="Hero" priority />
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* // Manual usage
|
|
195
|
+
* <Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
|
|
196
|
+
*/
|
|
197
|
+
const Image = createImage((props) => /* @__PURE__ */ jsxs("div", {
|
|
198
|
+
ref: props.containerRef,
|
|
199
|
+
class: props.class,
|
|
200
|
+
style: props.containerStyle,
|
|
201
|
+
children: [props.placeholder, props.image]
|
|
202
|
+
}));
|
|
121
203
|
|
|
122
204
|
//#endregion
|
|
123
|
-
export { Image };
|
|
205
|
+
export { Image, createImage, useImage };
|
|
124
206
|
//# sourceMappingURL=image.js.map
|