@pyreon/zero 0.16.0 → 0.19.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/server.js CHANGED
@@ -1,7 +1,7 @@
1
- import { _ as render404Page, a as detectLocaleFromHeader, c as vercelAdapter, d as netlifyAdapter, f as cloudflareAdapter, g as createServer, h as resolveConfig, i as createLocaleContext, l as staticAdapter, m as defineConfig, o as i18nRouting, p as bunAdapter, r as zeroPlugin, s as resolveAdapter, t as getZeroPluginConfig, u as nodeAdapter, v as createApp } from "./vite-plugin-xjWZwudX.js";
2
- import { i as generateRouteModule, o as parseFileRoutes, r as generateMiddlewareModule, s as scanRouteFiles, t as filePathToUrlPath } from "./fs-router-MewHc5SB.js";
3
- import { existsSync } from "node:fs";
4
- import { join, resolve } from "node:path";
1
+ import { _ as render404Page, a as detectLocaleFromHeader, c as vercelAdapter, d as netlifyAdapter, f as cloudflareAdapter, g as createServer, h as resolveConfig, i as createLocaleContext, l as staticAdapter, m as defineConfig, o as i18nRouting, p as bunAdapter, r as zeroPlugin, s as resolveAdapter, t as getZeroPluginConfig, u as nodeAdapter, v as createApp } from "./vite-plugin-8TXXFqdP.js";
2
+ import { i as generateRouteModule, o as parseFileRoutes, r as generateMiddlewareModule, s as scanRouteFiles, t as filePathToUrlPath } from "./fs-router-BVY4lTH_.js";
3
+ import { existsSync, readdirSync } from "node:fs";
4
+ import { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { readFile, rm, writeFile } from "node:fs/promises";
6
6
 
7
7
  //#region src/isr.ts
@@ -22,6 +22,10 @@ function createISRHandler(handler, config) {
22
22
  const cache = /* @__PURE__ */ new Map();
23
23
  const revalidating = /* @__PURE__ */ new Set();
24
24
  const revalidateMs = config.revalidate * 1e3;
25
+ const REVALIDATE_TIMEOUT_MS = Math.max(1, config.revalidateTimeoutMs ?? 3e4);
26
+ function isCacheable(res) {
27
+ return res.status >= 200 && res.status < 300 && !res.headers.has("set-cookie");
28
+ }
25
29
  const maxEntries = Math.max(1, config.maxEntries ?? 1e3);
26
30
  const deriveKey = typeof config.cacheKey === "function" ? (req, _url) => config.cacheKey(req) : (_req, url) => url.pathname;
27
31
  function set(key, entry) {
@@ -46,20 +50,23 @@ function createISRHandler(handler, config) {
46
50
  if (revalidating.has(key)) return;
47
51
  revalidating.add(key);
48
52
  try {
49
- const res = await handler(new Request(url.href, {
53
+ const req = new Request(url.href, {
50
54
  method: "GET",
51
55
  headers: originalReq.headers
52
- }));
53
- const html = await res.text();
54
- const headers = {};
55
- res.headers.forEach((v, k) => {
56
- headers[k] = v;
57
- });
58
- set(key, {
59
- html,
60
- headers,
61
- timestamp: Date.now()
62
56
  });
57
+ const res = await Promise.race([handler(req), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("[Pyreon ISR] revalidation timeout")), REVALIDATE_TIMEOUT_MS))]);
58
+ if (isCacheable(res)) {
59
+ const html = await res.text();
60
+ const headers = {};
61
+ res.headers.forEach((v, k) => {
62
+ headers[k] = v;
63
+ });
64
+ set(key, {
65
+ html,
66
+ headers,
67
+ timestamp: Date.now()
68
+ });
69
+ }
63
70
  } catch {} finally {
64
71
  revalidating.delete(key);
65
72
  }
@@ -88,6 +95,14 @@ function createISRHandler(handler, config) {
88
95
  res.headers.forEach((v, k) => {
89
96
  headers[k] = v;
90
97
  });
98
+ if (!isCacheable(res)) return new Response(html, {
99
+ status: res.status,
100
+ statusText: res.statusText,
101
+ headers: {
102
+ ...headers,
103
+ "x-isr-cache": "BYPASS"
104
+ }
105
+ });
91
106
  set(key, {
92
107
  html,
93
108
  headers,
@@ -817,6 +832,190 @@ async function addDevBadgeToPng(pngBuffer, size) {
817
832
  }
818
833
  }
819
834
 
835
+ //#endregion
836
+ //#region src/icons-plugin.ts
837
+ /** Set key → exported component name. `ui` → `UiIcon`, `brand-marks` → `BrandMarksIcon`. */
838
+ function componentNameFromSetKey(key) {
839
+ const safe = key.split(/[-_/\s]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("").replace(/[^A-Za-z0-9_$]/g, "");
840
+ return `${/^[A-Za-z_$]/.test(safe) ? safe : `Set${safe}`}Icon`;
841
+ }
842
+ /** Filename stem → registry key. `Check-Circle.svg` → `check-circle`. */
843
+ function iconNameFromFile(file) {
844
+ return basename(file, ".svg").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
845
+ }
846
+ /** Registry key → safe JS import binding. `check-circle` → `checkCircle`. */
847
+ function bindingFromName(name) {
848
+ const safe = name.replace(/[-/](.)/g, (_, c) => c.toUpperCase()).replace(/[^A-Za-z0-9_$]/g, "_");
849
+ return /^[A-Za-z_$]/.test(safe) ? safe : `_${safe}`;
850
+ }
851
+ /** List the `*.svg` filenames in `dir` (sorted, stable). Empty if missing. */
852
+ function scanIconDir(dir) {
853
+ if (!existsSync(dir)) return [];
854
+ return readdirSync(dir).filter((f) => f.toLowerCase().endsWith(".svg")).sort();
855
+ }
856
+ /**
857
+ * Render the generated `.tsx` source for a set of svg filenames. Pure —
858
+ * unit-tested directly; the plugin only adds fs + watch around it.
859
+ */
860
+ function generateIconSetSource(files, opts) {
861
+ const query = opts.mode === "image" ? "" : "?raw";
862
+ const seen = /* @__PURE__ */ new Map();
863
+ const entries = [];
864
+ for (const file of files) {
865
+ const key = iconNameFromFile(file);
866
+ let binding = bindingFromName(key);
867
+ while (seen.has(binding)) binding = `${binding}_`;
868
+ seen.set(binding, key);
869
+ entries.push({
870
+ key,
871
+ binding,
872
+ file
873
+ });
874
+ }
875
+ const header = [
876
+ "// AUTO-GENERATED by @pyreon/zero iconsPlugin — do not edit.",
877
+ `// Add / remove .svg files in ${opts.importDir} and this regenerates.`,
878
+ "/// <reference types=\"vite/client\" />",
879
+ "import { createNamedIcon } from '@pyreon/zero'"
880
+ ];
881
+ const imports = entries.map((e) => `import ${e.binding} from '${opts.importDir}/${e.file}${query}'`);
882
+ const registry = [
883
+ "const REGISTRY = {",
884
+ ...entries.map((e) => ` ${JSON.stringify(e.key)}: ${e.binding},`),
885
+ "} as const"
886
+ ];
887
+ const tail = [
888
+ "export type IconName = keyof typeof REGISTRY",
889
+ `export const Icon = createNamedIcon(REGISTRY${opts.mode === "image" ? ", { mode: 'image' }" : ""})`,
890
+ ""
891
+ ];
892
+ return [
893
+ ...header,
894
+ "",
895
+ ...imports,
896
+ "",
897
+ ...registry,
898
+ "",
899
+ ...tail
900
+ ].join("\n");
901
+ }
902
+ /**
903
+ * Render the generated `.tsx` for the NAMED MULTI-SET form. One file, one
904
+ * `createNamedIcon` import, one strictly-typed component PER set with
905
+ * namespaced types (`UiIcon`/`UiIconName`, `BrandIcon`/`BrandIconName`) so
906
+ * sets never clash. Bindings are per-set-prefixed so two sets sharing a
907
+ * glyph filename don't collide.
908
+ */
909
+ function generateNamedIconSetsSource(sets) {
910
+ const header = [
911
+ "// AUTO-GENERATED by @pyreon/zero iconsPlugin — do not edit.",
912
+ "// Add / remove .svg files in the configured set folders and this regenerates.",
913
+ "/// <reference types=\"vite/client\" />",
914
+ "import { createNamedIcon } from '@pyreon/zero'"
915
+ ];
916
+ const blocks = [];
917
+ for (const set of sets) {
918
+ const component = componentNameFromSetKey(set.key);
919
+ const typeName = `${component}Name`;
920
+ const registry = `${component}_REGISTRY`;
921
+ const query = set.mode === "image" ? "" : "?raw";
922
+ const seen = /* @__PURE__ */ new Set();
923
+ const entries = [];
924
+ for (const file of set.files) {
925
+ const k = iconNameFromFile(file);
926
+ let binding = `${bindingFromName(set.key)}_${bindingFromName(k)}`;
927
+ while (seen.has(binding)) binding = `${binding}_`;
928
+ seen.add(binding);
929
+ entries.push({
930
+ key: k,
931
+ binding,
932
+ file
933
+ });
934
+ }
935
+ const imports = entries.map((e) => `import ${e.binding} from '${set.importDir}/${e.file}${query}'`);
936
+ blocks.push([
937
+ `// ── set "${set.key}" → <${component} name="…" /> ──`,
938
+ ...imports,
939
+ `const ${registry} = {`,
940
+ ...entries.map((e) => ` ${JSON.stringify(e.key)}: ${e.binding},`),
941
+ "} as const",
942
+ `export type ${typeName} = keyof typeof ${registry}`,
943
+ `export const ${component} = createNamedIcon(${registry}${set.mode === "image" ? ", { mode: 'image' }" : ""})`
944
+ ].join("\n"));
945
+ }
946
+ return [
947
+ ...header,
948
+ "",
949
+ blocks.join("\n\n"),
950
+ ""
951
+ ].join("\n");
952
+ }
953
+ function resolveOut(cfg, root) {
954
+ if (cfg.out) return join(root, cfg.out);
955
+ if (cfg.dir) {
956
+ const dir = join(root, cfg.dir);
957
+ return join(dirname(dir), `${basename(dir)}.gen.tsx`);
958
+ }
959
+ return join(root, "src", "icons.gen.tsx");
960
+ }
961
+ /**
962
+ * Vite plugin: scan `dir` for `*.svg`, write a strictly-typed
963
+ * `icons.gen.tsx`, regenerate on add / unlink in dev.
964
+ */
965
+ function iconsPlugin(cfg) {
966
+ const hasDir = typeof cfg.dir === "string";
967
+ const hasSets = !!cfg.sets && Object.keys(cfg.sets).length > 0;
968
+ if (hasDir === hasSets) throw new Error("[Pyreon] iconsPlugin: provide EXACTLY ONE of `dir` (single set) or `sets` (named multi-set). " + (hasDir ? "Both were given." : "Neither was given (or `sets` is empty)."));
969
+ let root = process.cwd();
970
+ const mode = cfg.mode ?? "inline";
971
+ /** Relative `./…` import dir from the generated file to a scanned folder. */
972
+ function rel(out, scanned) {
973
+ const r = relative(dirname(out), scanned).split("\\").join("/");
974
+ return r.startsWith(".") ? r : `./${r}`;
975
+ }
976
+ async function regenerate() {
977
+ const out = resolveOut(cfg, root);
978
+ let source;
979
+ if (hasSets) source = generateNamedIconSetsSource(Object.entries(cfg.sets ?? {}).map(([key, sc]) => {
980
+ const scanned = join(root, sc.dir);
981
+ return {
982
+ key,
983
+ files: scanIconDir(scanned),
984
+ mode: sc.mode ?? "inline",
985
+ importDir: rel(out, scanned)
986
+ };
987
+ }));
988
+ else {
989
+ const scanned = join(root, cfg.dir);
990
+ source = generateIconSetSource(scanIconDir(scanned), {
991
+ mode,
992
+ importDir: rel(out, scanned)
993
+ });
994
+ }
995
+ if ((existsSync(out) ? await readFile(out, "utf8") : null) !== source) await writeFile(out, source, "utf8");
996
+ }
997
+ const watchDirs = () => hasSets ? Object.values(cfg.sets ?? {}).map((s) => join(root, s.dir)) : [join(root, cfg.dir)];
998
+ return {
999
+ name: "pyreon:zero-icons",
1000
+ async configResolved(resolved) {
1001
+ root = resolved.root;
1002
+ await regenerate();
1003
+ },
1004
+ async buildStart() {
1005
+ await regenerate();
1006
+ },
1007
+ configureServer(server) {
1008
+ const dirs = watchDirs();
1009
+ for (const d of dirs) server.watcher.add(d);
1010
+ const onChange = (file) => {
1011
+ if (file.toLowerCase().endsWith(".svg") && dirs.some((d) => file.startsWith(d))) regenerate();
1012
+ };
1013
+ server.watcher.on("add", onChange);
1014
+ server.watcher.on("unlink", onChange);
1015
+ }
1016
+ };
1017
+ }
1018
+
820
1019
  //#endregion
821
1020
  //#region src/seo.ts
822
1021
  /**
@@ -830,7 +1029,7 @@ async function addDevBadgeToPng(pngBuffer, size) {
830
1029
  */
831
1030
  function generateSitemap(routeFiles, config, i18n) {
832
1031
  const { origin, exclude = [], changefreq = "weekly", priority = .7 } = config;
833
- const clusters = clusterPathsByLocale([...routeFiles.filter((f) => {
1032
+ const paths = routeFiles.filter((f) => {
834
1033
  const name = f.split("/").pop()?.replace(/\.\w+$/, "");
835
1034
  return name !== "_layout" && name !== "_error" && name !== "_loading";
836
1035
  }).map((f) => {
@@ -839,11 +1038,16 @@ function generateSitemap(routeFiles, config, i18n) {
839
1038
  path = path.replace(/\([\w-]+\)\//g, "");
840
1039
  if (!path.startsWith("/")) path = `/${path}`;
841
1040
  return path;
842
- }).filter((p) => p !== null).filter((p) => !exclude.some((e) => p.startsWith(e))).map((p) => ({
843
- path: p,
844
- changefreq,
845
- priority
846
- })), ...config.additionalPaths ?? []], i18n);
1041
+ }).filter((p) => p !== null).filter((p) => !exclude.some((e) => p.startsWith(e)));
1042
+ const clusters = clusterPathsByLocale((() => {
1043
+ const byPath = /* @__PURE__ */ new Map();
1044
+ for (const e of [...paths.map((p) => ({
1045
+ path: p,
1046
+ changefreq,
1047
+ priority
1048
+ })), ...config.additionalPaths ?? []]) if (!byPath.has(e.path)) byPath.set(e.path, e);
1049
+ return [...byPath.values()];
1050
+ })(), i18n);
847
1051
  return `<?xml version="1.0" encoding="UTF-8"?>
848
1052
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${i18n != null && i18n.locales.length > 0 ? " xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"" : ""}>
849
1053
  ${clusters.map((cluster) => renderClusterEntry(cluster, origin, changefreq, priority, i18n)).join("\n")}
@@ -1049,7 +1253,7 @@ function seoPlugin(config = {}) {
1049
1253
  },
1050
1254
  async generateBundle(_, _bundle) {
1051
1255
  if (config.sitemap && !useSsgPaths) {
1052
- const { scanRouteFiles } = await import("./fs-router-MewHc5SB.js").then((n) => n.n);
1256
+ const { scanRouteFiles } = await import("./fs-router-BVY4lTH_.js").then((n) => n.n);
1053
1257
  const routesDir = `${process.cwd()}/src/routes`;
1054
1258
  try {
1055
1259
  const files = await scanRouteFiles(routesDir);
@@ -1073,7 +1277,7 @@ function seoPlugin(config = {}) {
1073
1277
  },
1074
1278
  async closeBundle() {
1075
1279
  if (!config.sitemap || !useSsgPaths) return;
1076
- const { scanRouteFiles } = await import("./fs-router-MewHc5SB.js").then((n) => n.n);
1280
+ const { scanRouteFiles } = await import("./fs-router-BVY4lTH_.js").then((n) => n.n);
1077
1281
  const routesDir = `${process.cwd()}/src/routes`;
1078
1282
  const manifestPath = join(distDir, "_pyreon-ssg-paths.json");
1079
1283
  try {
@@ -1111,7 +1315,7 @@ function seoMiddleware(config = {}) {
1111
1315
  return async (ctx) => {
1112
1316
  if (ctx.url.pathname === "/robots.txt" && config.robots) return new Response(generateRobots(config.robots), { headers: { "Content-Type": "text/plain" } });
1113
1317
  if (ctx.url.pathname === "/sitemap.xml" && config.sitemap) try {
1114
- const { scanRouteFiles } = await import("./fs-router-MewHc5SB.js").then((n) => n.n);
1318
+ const { scanRouteFiles } = await import("./fs-router-BVY4lTH_.js").then((n) => n.n);
1115
1319
  const sitemap = generateSitemap(await scanRouteFiles(`${process.cwd()}/src/routes`), config.sitemap);
1116
1320
  return new Response(sitemap, { headers: { "Content-Type": "application/xml" } });
1117
1321
  } catch {}
@@ -1756,5 +1960,5 @@ function capitalize(s) {
1756
1960
  }
1757
1961
 
1758
1962
  //#endregion
1759
- export { _resetVercelRevalidateHandlerCache, aiPlugin, bunAdapter, cloudflareAdapter, compose, createApp, createISRHandler, createLocaleContext, createServer, zeroPlugin as default, defineConfig, detectLocaleFromHeader, faviconLinks, faviconPlugin, filePathToUrlPath, generateLlmsFullTxt, generateLlmsTxt, generateMiddlewareModule, generateRobots, generateRouteModule, generateSitemap, getContext, getZeroPluginConfig, i18nRouting, inferJsonLd, jsonLd, netlifyAdapter, nodeAdapter, ogImagePath, ogImagePlugin, parseFileRoutes, render404Page, resolveAdapter, resolveConfig, scanRouteFiles, seoMiddleware, seoPlugin, staticAdapter, vercelAdapter, vercelRevalidateHandler };
1963
+ export { _resetVercelRevalidateHandlerCache, aiPlugin, bunAdapter, cloudflareAdapter, componentNameFromSetKey, compose, createApp, createISRHandler, createLocaleContext, createServer, zeroPlugin as default, defineConfig, detectLocaleFromHeader, faviconLinks, faviconPlugin, filePathToUrlPath, generateIconSetSource, generateLlmsFullTxt, generateLlmsTxt, generateMiddlewareModule, generateNamedIconSetsSource, generateRobots, generateRouteModule, generateSitemap, getContext, getZeroPluginConfig, i18nRouting, iconNameFromFile, iconsPlugin, inferJsonLd, jsonLd, netlifyAdapter, nodeAdapter, ogImagePath, ogImagePlugin, parseFileRoutes, render404Page, resolveAdapter, resolveConfig, scanIconDir, scanRouteFiles, seoMiddleware, seoPlugin, staticAdapter, vercelAdapter, vercelRevalidateHandler };
1760
1964
  //# sourceMappingURL=server.js.map
package/lib/testing.js CHANGED
@@ -7,17 +7,19 @@ function matchApiRoute(pattern, path) {
7
7
  const patternParts = pattern.split("/").filter(Boolean);
8
8
  const pathParts = path.split("/").filter(Boolean);
9
9
  const params = {};
10
+ const isUnsafeParam = (name) => name === "__proto__" || name === "constructor" || name === "prototype";
10
11
  for (let i = 0; i < patternParts.length; i++) {
11
12
  const pp = patternParts[i];
12
13
  if (!pp) continue;
13
14
  if (pp.endsWith("*")) {
14
15
  const paramName = pp.slice(1, -1);
15
- params[paramName] = pathParts.slice(i).join("/");
16
+ if (!isUnsafeParam(paramName)) params[paramName] = pathParts.slice(i).join("/");
16
17
  return params;
17
18
  }
18
19
  if (i >= pathParts.length) return null;
19
20
  if (pp.startsWith(":")) {
20
- params[pp.slice(1)] = pathParts[i];
21
+ const paramName = pp.slice(1);
22
+ if (!isUnsafeParam(paramName)) params[paramName] = pathParts[i];
21
23
  continue;
22
24
  }
23
25
  if (pp !== pathParts[i]) return null;
@@ -25,6 +25,15 @@ interface ISRConfig {
25
25
  * space (e.g. `/user/:id` where `:id` is free-form).
26
26
  */
27
27
  maxEntries?: number;
28
+ /**
29
+ * Max wall-time (ms) for a single background revalidation before it is
30
+ * abandoned. Without a bound, a handler that hangs leaves its key
31
+ * pinned in the in-flight set forever — every later request for that
32
+ * key short-circuits the de-dupe guard and the entry can never
33
+ * recover from stale. Default: `30000` (matches the Suspense
34
+ * streaming timeout).
35
+ */
36
+ revalidateTimeoutMs?: number;
28
37
  /**
29
38
  * Cache-key derivation function. The default keys cache entries by
30
39
  * `url.pathname` ONLY — query strings, cookies, and headers are
@@ -210,6 +219,38 @@ interface ZeroConfig {
210
219
  currentPath: string;
211
220
  elapsed: number;
212
221
  }) => void | Promise<void>;
222
+ /**
223
+ * Route-level code splitting in SSG mode. Default `true`.
224
+ *
225
+ * When `true` (default), each route file becomes its own dynamic-import
226
+ * chunk via `lazy(() => import("..."))` — only the route the user
227
+ * lands on plus its dependencies ship in the initial bundle, the
228
+ * rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
229
+ * has always had; brings parity to SSG.
230
+ *
231
+ * When `false`, every route is bundled statically into the main
232
+ * client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
233
+ * sites (2-5 pages) where the single-chunk-then-instant-nav trade
234
+ * is preferable — the chunk-fetch cost on navigation is gone, and
235
+ * the marginal bytes are negligible.
236
+ *
237
+ * Crossover point: ~5-8 routes. Below that, single-chunk is fine.
238
+ * Above that, lazy() shrinks the initial bundle by a meaningful
239
+ * amount (a 50-route docs site might drop from 200 KB to 80 KB on
240
+ * first paint).
241
+ *
242
+ * Underlying mechanism is the same 3-tier generator zero already
243
+ * uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
244
+ * component + inlined metadata when possible, lazy + lazy-thunked
245
+ * function exports when not, namespace-import fallback for cases
246
+ * the literal-extractor can't reach.
247
+ *
248
+ * @example
249
+ * ssg: {
250
+ * splitChunks: false, // bundle-everything for a 3-page marketing site
251
+ * }
252
+ */
253
+ splitChunks?: boolean;
213
254
  };
214
255
  /** ISR config — only used when mode is "isr". */
215
256
  isr?: ISRConfig;
@@ -17,8 +17,32 @@ declare const cdnProviders: {
17
17
  readonly vercel: () => ImageCdnProvider; /** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */
18
18
  readonly bunny: (pullZone: string) => ImageCdnProvider;
19
19
  };
20
- /** Placeholder generation strategy. */
21
- type PlaceholderStrategy = 'blur' | 'dominant-color' | 'none';
20
+ /**
21
+ * Placeholder generation strategy.
22
+ *
23
+ * - `'blur'` — tiny downscaled + blurred WebP data URI (a few hundred bytes).
24
+ * The richest preview; faithfully previews the image's content.
25
+ * - `'color'` — the image's dominant colour as a ~200-byte flat SVG data
26
+ * URI. Constant size regardless of source complexity (a blurred WebP
27
+ * grows with image content; this doesn't), zero decode, instant paint,
28
+ * zero layout shift. For real photos it's far smaller than `'blur'`; for
29
+ * trivial/solid sources `'blur'` can be the smaller of the two. Best when
30
+ * you want a clean solid backdrop rather than a blurry preview.
31
+ * - `'none'` — no placeholder (`placeholder: ''`). Skips all placeholder work.
32
+ *
33
+ * `'dominant-color'` is a deprecated alias of `'color'` — it was typed from
34
+ * the plugin's inception but never implemented (the build + dev paths always
35
+ * fell through to blur). It now resolves to `'color'`; prefer the shorter
36
+ * name in new code.
37
+ */
38
+ type PlaceholderStrategy = 'blur' | 'color' | 'dominant-color' | 'none';
39
+ /** Quality per output format (1-100), or a single number applied to all. */
40
+ type ImageQuality = number | Partial<Record<ImageFormat, number>>;
41
+ /**
42
+ * Normalize the public {@link PlaceholderStrategy} to an internal kind.
43
+ * @internal Exported for testing.
44
+ */
45
+ declare function normalizePlaceholder(s: PlaceholderStrategy): 'blur' | 'color' | 'none';
22
46
  /** SVG processing options for ?component imports. */
23
47
  interface SvgOptions {
24
48
  /** Replace fill/stroke with currentColor. Default: true */
@@ -33,11 +57,23 @@ interface ImagePluginConfig {
33
57
  widths?: number[];
34
58
  /** Output formats. Default: ["webp"] */
35
59
  formats?: ImageFormat[];
36
- /** Quality for lossy formats (1-100). Default: 80 */
37
- quality?: number;
38
- /** Blur placeholder size in px. Default: 16 */
60
+ /**
61
+ * Quality for lossy formats (1-100). Default: 80.
62
+ *
63
+ * Accepts a single number applied to every format, OR a per-format map so
64
+ * you can tune each codec independently — AVIF tolerates a much lower
65
+ * number than WebP/JPEG for the same perceived quality:
66
+ *
67
+ * ```ts
68
+ * imagePlugin({ formats: ['avif', 'webp'], quality: { avif: 55, webp: 75 } })
69
+ * ```
70
+ *
71
+ * Formats omitted from the map fall back to 80.
72
+ */
73
+ quality?: ImageQuality;
74
+ /** Blur placeholder size in px (only used by the `'blur'` strategy). Default: 16 */
39
75
  placeholderSize?: number;
40
- /** Placeholder strategy. Default: "blur" */
76
+ /** Placeholder strategy. Default: `"blur"`. See {@link PlaceholderStrategy}. */
41
77
  placeholder?: PlaceholderStrategy;
42
78
  /** File patterns to process. Default: /\.(jpe?g|png|webp|avif)$/i */
43
79
  include?: RegExp;
@@ -126,6 +162,28 @@ declare function parseWebPDimensions(buffer: Buffer): {
126
162
  width: number;
127
163
  height: number;
128
164
  };
165
+ /**
166
+ * Resolve the public {@link ImageQuality} config into a per-format lookup.
167
+ *
168
+ * - `undefined` → every format gets {@link DEFAULT_QUALITY}.
169
+ * - `number` → that number for every format (backward-compatible).
170
+ * - `Partial<Record<ImageFormat, number>>` → per-format; formats omitted
171
+ * from the map fall back to {@link DEFAULT_QUALITY}.
172
+ *
173
+ * @internal Exported for testing.
174
+ */
175
+ declare function resolveQuality(q: ImageQuality | undefined): (format: ImageFormat) => number;
176
+ /**
177
+ * Dispatch placeholder generation by strategy. Single source of truth used
178
+ * by every code path (CDN / dev / build) — pre-fix each path open-coded
179
+ * `generateBlurPlaceholder`, so `'none'` was honoured only in the CDN path
180
+ * and `'dominant-color'` (typed since the plugin's inception) was never
181
+ * implemented anywhere — the exact typed-but-unimplemented bug class the
182
+ * `audit-types` gate exists to catch.
183
+ *
184
+ * @internal Exported for testing.
185
+ */
186
+ declare function generatePlaceholder(input: string, strategy: 'blur' | 'color' | 'none', size: number): Promise<string>;
129
187
  //#endregion
130
- export { FormatSource, ImageCdnProvider, ImageFormat, ImagePluginConfig, PlaceholderStrategy, ProcessedImage, SvgOptions, cdnProviders, imagePlugin, parseJpegDimensions, parseWebPDimensions };
188
+ export { FormatSource, ImageCdnProvider, ImageFormat, ImagePluginConfig, ImageQuality, PlaceholderStrategy, ProcessedImage, SvgOptions, cdnProviders, generatePlaceholder, imagePlugin, normalizePlaceholder, parseJpegDimensions, parseWebPDimensions, resolveQuality };
131
189
  //# sourceMappingURL=image-plugin2.d.ts.map
@@ -1,9 +1,86 @@
1
1
  import * as _$_pyreon_core0 from "@pyreon/core";
2
- import { ComponentFn, Ref, VNodeChild } from "@pyreon/core";
2
+ import { ComponentFn, Ref, SvgAttributes, VNodeChild } from "@pyreon/core";
3
3
  import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
4
4
  import { LoaderContext, NavigationGuard } from "@pyreon/router";
5
5
  import { Middleware } from "@pyreon/server";
6
6
 
7
+ //#region src/icon.d.ts
8
+ /** An imported SVG component (`import X from './x.svg?component'`). */
9
+ type SvgComponent = (props: SvgAttributes) => VNodeChild;
10
+ /**
11
+ * Props for {@link Icon}. The standard `<svg>` attribute surface
12
+ * (`fill`, `class`, `style`, `aria-*`, `onClick`, …) — every one passed
13
+ * straight through and overriding the container-fill defaults — plus the
14
+ * two source props.
15
+ */
16
+ interface IconProps extends SvgAttributes {
17
+ /**
18
+ * An imported SVG component, e.g. `import X from './icon.svg?component'`.
19
+ * Rendered directly with no host wrapper. Recommended over `svg`.
20
+ */
21
+ as?: SvgComponent | undefined;
22
+ /**
23
+ * A full `<svg>…</svg>` markup string, e.g.
24
+ * `import x from './icon.svg?raw'`. Inlined inside a single `<span>` host.
25
+ */
26
+ svg?: string | undefined;
27
+ }
28
+ /**
29
+ * Render a loaded SVG — container-filling, theme-aware, props-transparent.
30
+ *
31
+ * @example
32
+ * import Check from './check.svg?component'
33
+ * <span style="width:2rem"><Icon as={Check} /></span>
34
+ *
35
+ * @example
36
+ * import check from './check.svg?raw'
37
+ * <span style="width:2rem"><Icon svg={check} /></span>
38
+ */
39
+ declare function Icon(props: IconProps): VNodeChild;
40
+ /**
41
+ * Build a reusable icon component from a loaded svg — a markup string OR an
42
+ * imported SVG component. The result is still just `<Icon>`, so it's
43
+ * container-sizable + theme-aware with every prop passed through.
44
+ *
45
+ * @example
46
+ * import check from './check.svg?raw'
47
+ * export const Check = createIcon(check)
48
+ *
49
+ * import StarSvg from './star.svg?component'
50
+ * export const Star = createIcon(StarSvg)
51
+ *
52
+ * // …sized + themed entirely by the consumer:
53
+ * <span style="width:48px"><Check class="text-green-600" /></span>
54
+ */
55
+ declare function createIcon(source: string | SvgComponent): (props: SvgAttributes) => VNodeChild;
56
+ /** How a named icon set renders each entry. */
57
+ type IconMode = 'inline' | 'image';
58
+ /** Props of a component built by {@link createNamedIcon}. */
59
+ type NamedIconProps<R extends Record<string, string>> = {
60
+ /** A name from the scanned set — strictly typed to the available files. */name: keyof R & string; /** `<img>` alt text (image mode). Defaults to `""` (decorative). */
61
+ alt?: string;
62
+ } & Omit<IconProps, 'as' | 'svg'>;
63
+ /**
64
+ * Build a strictly-typed `<Icon name="…" />` from a name→source registry.
65
+ *
66
+ * - `mode: 'inline'` (default) — `source` is raw `<svg>` markup; rendered via
67
+ * {@link Icon} so it's `currentColor`-themeable (system icons you recolor).
68
+ * - `mode: 'image'` — `source` is an asset URL; rendered as `<img>` with NO
69
+ * svg mutation, original colors preserved (colorful / brand icons).
70
+ *
71
+ * Either way it stays container-filling + props-transparent. Not called by
72
+ * hand normally — `iconsPlugin` emits the generated file that calls it.
73
+ *
74
+ * @example
75
+ * // icons.gen.tsx (auto-generated):
76
+ * export const Icon = createNamedIcon({ 'check-circle': '<svg…' })
77
+ * // app:
78
+ * <span style="width:2rem"><Icon name="check-circle" /></span>
79
+ */
80
+ declare function createNamedIcon<R extends Record<string, string>>(registry: R, options?: {
81
+ mode?: IconMode;
82
+ }): (props: NamedIconProps<R>) => VNodeChild;
83
+ //#endregion
7
84
  //#region src/image-plugin.d.ts
8
85
  /** Per-format source set for <picture> <source> elements. */
9
86
  interface FormatSource {
@@ -443,6 +520,15 @@ interface ISRConfig {
443
520
  * space (e.g. `/user/:id` where `:id` is free-form).
444
521
  */
445
522
  maxEntries?: number;
523
+ /**
524
+ * Max wall-time (ms) for a single background revalidation before it is
525
+ * abandoned. Without a bound, a handler that hangs leaves its key
526
+ * pinned in the in-flight set forever — every later request for that
527
+ * key short-circuits the de-dupe guard and the entry can never
528
+ * recover from stale. Default: `30000` (matches the Suspense
529
+ * streaming timeout).
530
+ */
531
+ revalidateTimeoutMs?: number;
446
532
  /**
447
533
  * Cache-key derivation function. The default keys cache entries by
448
534
  * `url.pathname` ONLY — query strings, cookies, and headers are
@@ -628,6 +714,38 @@ interface ZeroConfig {
628
714
  currentPath: string;
629
715
  elapsed: number;
630
716
  }) => void | Promise<void>;
717
+ /**
718
+ * Route-level code splitting in SSG mode. Default `true`.
719
+ *
720
+ * When `true` (default), each route file becomes its own dynamic-import
721
+ * chunk via `lazy(() => import("..."))` — only the route the user
722
+ * lands on plus its dependencies ship in the initial bundle, the
723
+ * rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
724
+ * has always had; brings parity to SSG.
725
+ *
726
+ * When `false`, every route is bundled statically into the main
727
+ * client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
728
+ * sites (2-5 pages) where the single-chunk-then-instant-nav trade
729
+ * is preferable — the chunk-fetch cost on navigation is gone, and
730
+ * the marginal bytes are negligible.
731
+ *
732
+ * Crossover point: ~5-8 routes. Below that, single-chunk is fine.
733
+ * Above that, lazy() shrinks the initial bundle by a meaningful
734
+ * amount (a 50-route docs site might drop from 200 KB to 80 KB on
735
+ * first paint).
736
+ *
737
+ * Underlying mechanism is the same 3-tier generator zero already
738
+ * uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
739
+ * component + inlined metadata when possible, lazy + lazy-thunked
740
+ * function exports when not, namespace-import fallback for cases
741
+ * the literal-extractor can't reach.
742
+ *
743
+ * @example
744
+ * ssg: {
745
+ * splitChunks: false, // bundle-everything for a 3-page marketing site
746
+ * }
747
+ */
748
+ splitChunks?: boolean;
631
749
  };
632
750
  /** ISR config — only used when mode is "isr". */
633
751
  isr?: ISRConfig;
@@ -1135,5 +1253,5 @@ declare function ogImagePlugin(..._: unknown[]): never;
1135
1253
  /** @deprecated Import from `@pyreon/zero/ai` instead */
1136
1254
  declare function aiPlugin(..._: unknown[]): never;
1137
1255
  //#endregion
1138
- export { type Adapter, type AdapterBuildOptions, type FileRoute, type I18nRoutingConfig, type ISRConfig, Image, type ImageProps, type ImageRenderProps, type ImageSource, Link, type LinkProps, type LinkRenderProps, type LoaderContext, type LocaleContext, Meta, type MetaProps, type RenderMode, type RouteMeta, type RouteMiddlewareEntry, type RouteModule, Script, type ScriptProps, type ScriptRenderProps, type ScriptStrategy, type Theme, ThemeToggle, type UseImageReturn, type UseLinkReturn, type UseScriptReturn, type ZeroConfig, aiPlugin, buildLocalePath, buildMetaTags, createImage, createLink, createScript, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useImage, useLink, useLocale, useScript, validateEnv };
1256
+ export { type Adapter, type AdapterBuildOptions, type FileRoute, type I18nRoutingConfig, type ISRConfig, Icon, type IconMode, type IconProps, Image, type ImageProps, type ImageRenderProps, type ImageSource, Link, type LinkProps, type LinkRenderProps, type LoaderContext, type LocaleContext, Meta, type MetaProps, type NamedIconProps, type RenderMode, type RouteMeta, type RouteMiddlewareEntry, type RouteModule, Script, type ScriptProps, type ScriptRenderProps, type ScriptStrategy, type SvgComponent, type Theme, ThemeToggle, type UseImageReturn, type UseLinkReturn, type UseScriptReturn, type ZeroConfig, aiPlugin, buildLocalePath, buildMetaTags, createIcon, createImage, createLink, createNamedIcon, createScript, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useImage, useLink, useLocale, useScript, validateEnv };
1139
1257
  //# sourceMappingURL=index2.d.ts.map