@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.
Files changed (52) hide show
  1. package/lib/{api-routes-DANluJic.js → api-routes-Ci0kVmM4.js} +2 -2
  2. package/lib/client.js +4 -1
  3. package/lib/env.js +6 -6
  4. package/lib/font.js +3 -3
  5. package/lib/{fs-router-ZebyutPa.js → fs-router-MewHc5SB.js} +25 -30
  6. package/lib/i18n-routing.js +112 -1
  7. package/lib/image.js +140 -58
  8. package/lib/index.js +252 -82
  9. package/lib/og-image.js +5 -5
  10. package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
  11. package/lib/script.js +114 -25
  12. package/lib/seo.js +186 -15
  13. package/lib/server.js +274 -564
  14. package/lib/types/config.d.ts +275 -3
  15. package/lib/types/env.d.ts +2 -2
  16. package/lib/types/i18n-routing.d.ts +193 -2
  17. package/lib/types/image.d.ts +105 -5
  18. package/lib/types/index.d.ts +634 -182
  19. package/lib/types/script.d.ts +78 -6
  20. package/lib/types/seo.d.ts +128 -4
  21. package/lib/types/server.d.ts +575 -72
  22. package/lib/vite-plugin-xjWZwudX.js +2454 -0
  23. package/package.json +11 -10
  24. package/src/adapters/bun.ts +20 -1
  25. package/src/adapters/cloudflare.ts +78 -1
  26. package/src/adapters/index.ts +25 -3
  27. package/src/adapters/netlify.ts +63 -1
  28. package/src/adapters/node.ts +25 -1
  29. package/src/adapters/static.ts +26 -1
  30. package/src/adapters/validate.ts +8 -1
  31. package/src/adapters/vercel.ts +76 -1
  32. package/src/adapters/warn-missing-env.ts +49 -0
  33. package/src/app.ts +14 -0
  34. package/src/client.ts +18 -0
  35. package/src/entry-server.ts +55 -5
  36. package/src/env.ts +7 -7
  37. package/src/font.ts +3 -3
  38. package/src/fs-router.ts +72 -3
  39. package/src/i18n-routing.ts +246 -12
  40. package/src/image.tsx +242 -91
  41. package/src/index.ts +4 -4
  42. package/src/isr.ts +24 -6
  43. package/src/manifest.ts +675 -0
  44. package/src/og-image.ts +5 -5
  45. package/src/script.tsx +159 -36
  46. package/src/seo.ts +346 -15
  47. package/src/server.ts +10 -2
  48. package/src/ssg-plugin.ts +1211 -54
  49. package/src/types.ts +301 -10
  50. package/src/vercel-revalidate-handler.ts +204 -0
  51. package/src/vite-plugin.ts +108 -30
  52. package/lib/vite-plugin-E4BHYvYW.js +0 -855
@@ -1,4 +1,4 @@
1
- import { l as __exportAll } from "./fs-router-ZebyutPa.js";
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-DANluJic.js.map
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(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,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-DANluJic.js").then((n) => n.t);
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, __exportAll as l, fs_router_exports as n, parseFileRoutes as o, generateMiddlewareModule as r, scanRouteFiles as s, filePathToUrlPath as t };
992
- //# sourceMappingURL=fs-router-ZebyutPa.js.map
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
@@ -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
- * Optimized image component with lazy loading, responsive images,
33
- * multi-format <picture> support, and blur-up placeholders.
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
- * @example
36
- * // With imagePlugin spread the import directly
37
- * import hero from "./hero.jpg?optimize"
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
- * // Manual usage
42
- * <Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
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 Image(props) {
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 imgEl = /* @__PURE__ */ jsx("img", {
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
- alt: props.alt,
79
- width: props.width,
80
- height: props.height,
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
- onLoad: () => loaded.set(true),
85
- style: () => [
86
- "display: block",
87
- "width: 100%",
88
- "height: 100%",
89
- `object-fit: ${fit}`,
90
- "transition: opacity 0.3s ease",
91
- props.placeholder && !loaded() ? "opacity: 0" : "opacity: 1"
92
- ].join("; ")
93
- });
94
- return /* @__PURE__ */ jsxs("div", {
95
- ref: containerRef,
96
- class: props.class,
97
- style: containerStyle,
98
- children: [props.placeholder && /* @__PURE__ */ jsx("img", {
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
- "position: absolute",
105
- "inset: 0",
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