@pyreon/zero 0.18.0 → 0.20.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/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
@@ -1,6 +1,22 @@
1
1
  import { Plugin } from "vite";
2
2
 
3
3
  //#region src/favicon.d.ts
4
+ /**
5
+ * Stable content hash (FNV-1a, 32-bit) of the favicon source file(s),
6
+ * rendered as a `?v=<hex>` cache-bust query for the injected `<head>`
7
+ * links. Browsers cache favicons extremely aggressively (often per-
8
+ * session / effectively forever), so with a stable URL a changed icon
9
+ * is never re-fetched by returning visitors. Same source bytes →
10
+ * identical query (no needless cache churn); changed bytes → new query
11
+ * → browser re-downloads. Falls back to `''` (no query, prior
12
+ * behaviour) if a source can't be read — never break the build over a
13
+ * cache-bust nicety. NOTE: this versions everything referenced via
14
+ * `<link>` (svg/png/apple-touch/manifest). The bare `/favicon.ico`
15
+ * convention request (browsers fetch it with no link tag) and the
16
+ * `site.webmanifest`'s internal icon entries keep stable URLs — those
17
+ * rely on host cache headers / are re-resolved on PWA (re)install.
18
+ */
19
+ declare function faviconVersionQuery(paths: string[]): string;
4
20
  interface FaviconLocaleConfig {
5
21
  /** Locale-specific source icon (SVG or PNG). */
6
22
  source: string;
@@ -102,5 +118,5 @@ interface IcoEntry {
102
118
  /** @internal Exported for testing */
103
119
  declare function createIcoFromPngs(entries: IcoEntry[]): Uint8Array;
104
120
  //#endregion
105
- export { FaviconLocaleConfig, FaviconPluginConfig, IcoEntry, createIcoFromPngs, faviconLinks, faviconPlugin };
121
+ export { FaviconLocaleConfig, FaviconPluginConfig, IcoEntry, createIcoFromPngs, faviconLinks, faviconPlugin, faviconVersionQuery };
106
122
  //# sourceMappingURL=favicon2.d.ts.map
@@ -1,5 +1,3 @@
1
- import * as _$_pyreon_core0 from "@pyreon/core";
2
- import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
3
1
  import { Plugin } from "vite";
4
2
  //#region src/types.d.ts
5
3
  type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr';
@@ -265,9 +263,9 @@ declare function createLocaleContext(locale: string, path: string, config: I18nR
265
263
  */
266
264
  declare function i18nRouting(config: I18nRoutingConfig): Plugin;
267
265
  /** @internal Context for the current locale. */
268
- declare const LocaleCtx: _$_pyreon_core0.Context<string>;
266
+ declare const LocaleCtx: import("@pyreon/core").Context<string>;
269
267
  /** Current locale signal — set by the server middleware or client-side detection. */
270
- declare const localeSignal: _$_pyreon_reactivity0.Signal<string>;
268
+ declare const localeSignal: import("@pyreon/reactivity").Signal<string>;
271
269
  /**
272
270
  * Read the current locale reactively.
273
271
  *
@@ -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,84 @@
1
- import * as _$_pyreon_core0 from "@pyreon/core";
2
- import { ComponentFn, Ref, VNodeChild } from "@pyreon/core";
3
- import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
1
+ import { ComponentFn, Ref, SvgAttributes, VNodeChild } from "@pyreon/core";
4
2
  import { LoaderContext, NavigationGuard } from "@pyreon/router";
5
3
  import { Middleware } from "@pyreon/server";
6
4
 
5
+ //#region src/icon.d.ts
6
+ /** An imported SVG component (`import X from './x.svg?component'`). */
7
+ type SvgComponent = (props: SvgAttributes) => VNodeChild;
8
+ /**
9
+ * Props for {@link Icon}. The standard `<svg>` attribute surface
10
+ * (`fill`, `class`, `style`, `aria-*`, `onClick`, …) — every one passed
11
+ * straight through and overriding the container-fill defaults — plus the
12
+ * two source props.
13
+ */
14
+ interface IconProps extends SvgAttributes {
15
+ /**
16
+ * An imported SVG component, e.g. `import X from './icon.svg?component'`.
17
+ * Rendered directly with no host wrapper. Recommended over `svg`.
18
+ */
19
+ as?: SvgComponent | undefined;
20
+ /**
21
+ * A full `<svg>…</svg>` markup string, e.g.
22
+ * `import x from './icon.svg?raw'`. Inlined inside a single `<span>` host.
23
+ */
24
+ svg?: string | undefined;
25
+ }
26
+ /**
27
+ * Render a loaded SVG — container-filling, theme-aware, props-transparent.
28
+ *
29
+ * @example
30
+ * import Check from './check.svg?component'
31
+ * <span style="width:2rem"><Icon as={Check} /></span>
32
+ *
33
+ * @example
34
+ * import check from './check.svg?raw'
35
+ * <span style="width:2rem"><Icon svg={check} /></span>
36
+ */
37
+ declare function Icon(props: IconProps): VNodeChild;
38
+ /**
39
+ * Build a reusable icon component from a loaded svg — a markup string OR an
40
+ * imported SVG component. The result is still just `<Icon>`, so it's
41
+ * container-sizable + theme-aware with every prop passed through.
42
+ *
43
+ * @example
44
+ * import check from './check.svg?raw'
45
+ * export const Check = createIcon(check)
46
+ *
47
+ * import StarSvg from './star.svg?component'
48
+ * export const Star = createIcon(StarSvg)
49
+ *
50
+ * // …sized + themed entirely by the consumer:
51
+ * <span style="width:48px"><Check class="text-green-600" /></span>
52
+ */
53
+ declare function createIcon(source: string | SvgComponent): (props: SvgAttributes) => VNodeChild;
54
+ /** How a named icon set renders each entry. */
55
+ type IconMode = 'inline' | 'image';
56
+ /** Props of a component built by {@link createNamedIcon}. */
57
+ type NamedIconProps<R extends Record<string, string>> = {
58
+ /** A name from the scanned set — strictly typed to the available files. */name: keyof R & string; /** `<img>` alt text (image mode). Defaults to `""` (decorative). */
59
+ alt?: string;
60
+ } & Omit<IconProps, 'as' | 'svg'>;
61
+ /**
62
+ * Build a strictly-typed `<Icon name="…" />` from a name→source registry.
63
+ *
64
+ * - `mode: 'inline'` (default) — `source` is raw `<svg>` markup; rendered via
65
+ * {@link Icon} so it's `currentColor`-themeable (system icons you recolor).
66
+ * - `mode: 'image'` — `source` is an asset URL; rendered as `<img>` with NO
67
+ * svg mutation, original colors preserved (colorful / brand icons).
68
+ *
69
+ * Either way it stays container-filling + props-transparent. Not called by
70
+ * hand normally — `iconsPlugin` emits the generated file that calls it.
71
+ *
72
+ * @example
73
+ * // icons.gen.tsx (auto-generated):
74
+ * export const Icon = createNamedIcon({ 'check-circle': '<svg…' })
75
+ * // app:
76
+ * <span style="width:2rem"><Icon name="check-circle" /></span>
77
+ */
78
+ declare function createNamedIcon<R extends Record<string, string>>(registry: R, options?: {
79
+ mode?: IconMode;
80
+ }): (props: NamedIconProps<R>) => VNodeChild;
81
+ //#endregion
7
82
  //#region src/image-plugin.d.ts
8
83
  /** Per-format source set for <picture> <source> elements. */
9
84
  interface FormatSource {
@@ -195,7 +270,7 @@ interface LinkProps {
195
270
  /** Props passed to a custom component via createLink. */
196
271
  interface LinkRenderProps {
197
272
  href: string;
198
- ref: _$_pyreon_core0.Ref<HTMLAnchorElement>;
273
+ ref: import('@pyreon/core').Ref<HTMLAnchorElement>;
199
274
  onClick: (e: MouseEvent) => void;
200
275
  onMouseEnter: () => void;
201
276
  onTouchStart: () => void;
@@ -212,7 +287,7 @@ interface LinkRenderProps {
212
287
  /** Return type of useLink. */
213
288
  interface UseLinkReturn {
214
289
  /** Ref object — attach to the root element for viewport-based prefetch. */
215
- ref: _$_pyreon_core0.Ref<HTMLAnchorElement>;
290
+ ref: import('@pyreon/core').Ref<HTMLAnchorElement>;
216
291
  /** Click handler — performs client-side navigation. */
217
292
  handleClick: (e: MouseEvent) => void;
218
293
  /** Mouse enter handler — triggers hover prefetch. */
@@ -443,6 +518,15 @@ interface ISRConfig {
443
518
  * space (e.g. `/user/:id` where `:id` is free-form).
444
519
  */
445
520
  maxEntries?: number;
521
+ /**
522
+ * Max wall-time (ms) for a single background revalidation before it is
523
+ * abandoned. Without a bound, a handler that hangs leaves its key
524
+ * pinned in the in-flight set forever — every later request for that
525
+ * key short-circuits the de-dupe guard and the entry can never
526
+ * recover from stale. Default: `30000` (matches the Suspense
527
+ * streaming timeout).
528
+ */
529
+ revalidateTimeoutMs?: number;
446
530
  /**
447
531
  * Cache-key derivation function. The default keys cache entries by
448
532
  * `url.pathname` ONLY — query strings, cookies, and headers are
@@ -1105,7 +1189,7 @@ declare function buildMetaTags(props: Omit<MetaProps, 'title' | 'description' |
1105
1189
  //#region src/theme.d.ts
1106
1190
  type Theme = 'light' | 'dark' | 'system';
1107
1191
  /** Reactive theme signal. */
1108
- declare const theme: _$_pyreon_reactivity0.Signal<Theme>;
1192
+ declare const theme: import("@pyreon/reactivity").Signal<Theme>;
1109
1193
  /**
1110
1194
  * Set the default theme for SSR (when `matchMedia` is unavailable).
1111
1195
  * Call once at server startup before rendering.
@@ -1167,5 +1251,5 @@ declare function ogImagePlugin(..._: unknown[]): never;
1167
1251
  /** @deprecated Import from `@pyreon/zero/ai` instead */
1168
1252
  declare function aiPlugin(..._: unknown[]): never;
1169
1253
  //#endregion
1170
- 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 };
1254
+ 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 };
1171
1255
  //# sourceMappingURL=index2.d.ts.map
@@ -1,5 +1,3 @@
1
- import * as _$_pyreon_core0 from "@pyreon/core";
2
-
3
1
  //#region src/link.d.ts
4
2
  interface LinkProps {
5
3
  /** Target URL path. */
@@ -26,7 +24,7 @@ interface LinkProps {
26
24
  /** Props passed to a custom component via createLink. */
27
25
  interface LinkRenderProps {
28
26
  href: string;
29
- ref: _$_pyreon_core0.Ref<HTMLAnchorElement>;
27
+ ref: import('@pyreon/core').Ref<HTMLAnchorElement>;
30
28
  onClick: (e: MouseEvent) => void;
31
29
  onMouseEnter: () => void;
32
30
  onTouchStart: () => void;
@@ -43,7 +41,7 @@ interface LinkRenderProps {
43
41
  /** Return type of useLink. */
44
42
  interface UseLinkReturn {
45
43
  /** Ref object — attach to the root element for viewport-based prefetch. */
46
- ref: _$_pyreon_core0.Ref<HTMLAnchorElement>;
44
+ ref: import('@pyreon/core').Ref<HTMLAnchorElement>;
47
45
  /** Click handler — performs client-side navigation. */
48
46
  handleClick: (e: MouseEvent) => void;
49
47
  /** Mouse enter handler — triggers hover prefetch. */
@@ -1,6 +1,4 @@
1
- import * as _$_pyreon_core0 from "@pyreon/core";
2
1
  import { ComponentFn } from "@pyreon/core";
3
- import * as _$_pyreon_router0 from "@pyreon/router";
4
2
  import { RouteRecord } from "@pyreon/router";
5
3
  import { Middleware, MiddlewareContext } from "@pyreon/server";
6
4
  import { Plugin } from "vite";
@@ -36,8 +34,8 @@ interface CreateAppOptions {
36
34
  * Used internally by entry-server and entry-client.
37
35
  */
38
36
  declare function createApp(options: CreateAppOptions): {
39
- App: () => _$_pyreon_core0.VNode;
40
- router: _$_pyreon_router0.Router<string>;
37
+ App: () => import("@pyreon/core").VNode;
38
+ router: import("@pyreon/router").Router<string>;
41
39
  };
42
40
  //#endregion
43
41
  //#region src/api-routes.d.ts
@@ -146,6 +144,15 @@ interface ISRConfig {
146
144
  * space (e.g. `/user/:id` where `:id` is free-form).
147
145
  */
148
146
  maxEntries?: number;
147
+ /**
148
+ * Max wall-time (ms) for a single background revalidation before it is
149
+ * abandoned. Without a bound, a handler that hangs leaves its key
150
+ * pinned in the in-flight set forever — every later request for that
151
+ * key short-circuits the de-dupe guard and the entry can never
152
+ * recover from stale. Default: `30000` (matches the Suspense
153
+ * streaming timeout).
154
+ */
155
+ revalidateTimeoutMs?: number;
149
156
  /**
150
157
  * Cache-key derivation function. The default keys cache entries by
151
158
  * `url.pathname` ONLY — query strings, cookies, and headers are
@@ -1106,6 +1113,83 @@ declare function faviconLinks(locale: string | undefined, config: FaviconPluginC
1106
1113
  href: string;
1107
1114
  }>;
1108
1115
  //#endregion
1116
+ //#region src/icon.d.ts
1117
+ /** How a named icon set renders each entry. */
1118
+ type IconMode = 'inline' | 'image';
1119
+ //#endregion
1120
+ //#region src/icons-plugin.d.ts
1121
+ /** One named set in the multi-set form. */
1122
+ interface IconSetConfig {
1123
+ /** Folder of `*.svg` files to scan for this set. */
1124
+ dir: string;
1125
+ /**
1126
+ * `'inline'` (default — system icons, `currentColor`-themeable) or
1127
+ * `'image'` (colorful / brand icons, rendered `<img>`, no mutation).
1128
+ */
1129
+ mode?: IconMode;
1130
+ }
1131
+ interface IconsPluginConfig {
1132
+ /**
1133
+ * Single-set form: a folder of `*.svg` files → one `<Icon name="…" />`
1134
+ * with a single `IconName` union. Mutually exclusive with `sets`.
1135
+ */
1136
+ dir?: string;
1137
+ /**
1138
+ * Named multi-set form: `{ ui: { dir }, brand: { dir, mode } }` → one
1139
+ * generated file exporting a strictly-typed component PER set with
1140
+ * NAMESPACED types so they never clash:
1141
+ * `ui` → `<UiIcon name="…" />` + `type UiIconName`
1142
+ * `brand` → `<BrandIcon name="…" />` + `type BrandIconName`
1143
+ * Mutually exclusive with `dir`.
1144
+ */
1145
+ sets?: Record<string, IconSetConfig>;
1146
+ /**
1147
+ * Where to write the generated `.tsx`. Single-set default: `icons.gen.tsx`
1148
+ * next to `dir` (e.g. `src/icons` → `src/icons.gen.tsx`). Multi-set
1149
+ * default: `src/icons.gen.tsx` under the project root. Recommend
1150
+ * gitignoring it — it's a build artifact.
1151
+ */
1152
+ out?: string;
1153
+ /** Single-set form only — render mode (`'inline'` default | `'image'`). */
1154
+ mode?: IconMode;
1155
+ }
1156
+ /** Set key → exported component name. `ui` → `UiIcon`, `brand-marks` → `BrandMarksIcon`. */
1157
+ declare function componentNameFromSetKey(key: string): string;
1158
+ /** Filename stem → registry key. `Check-Circle.svg` → `check-circle`. */
1159
+ declare function iconNameFromFile(file: string): string;
1160
+ /** List the `*.svg` filenames in `dir` (sorted, stable). Empty if missing. */
1161
+ declare function scanIconDir(dir: string): string[];
1162
+ /**
1163
+ * Render the generated `.tsx` source for a set of svg filenames. Pure —
1164
+ * unit-tested directly; the plugin only adds fs + watch around it.
1165
+ */
1166
+ declare function generateIconSetSource(files: string[], opts: {
1167
+ mode: IconMode;
1168
+ importDir: string;
1169
+ }): string;
1170
+ /** One resolved set for the multi-set generator. */
1171
+ interface NamedSetInput {
1172
+ /** Set key (`ui`) — becomes `<UiIcon>` + `type UiIconName`. */
1173
+ key: string;
1174
+ files: string[];
1175
+ mode: IconMode;
1176
+ /** Relative import dir from the generated file to this set's folder. */
1177
+ importDir: string;
1178
+ }
1179
+ /**
1180
+ * Render the generated `.tsx` for the NAMED MULTI-SET form. One file, one
1181
+ * `createNamedIcon` import, one strictly-typed component PER set with
1182
+ * namespaced types (`UiIcon`/`UiIconName`, `BrandIcon`/`BrandIconName`) so
1183
+ * sets never clash. Bindings are per-set-prefixed so two sets sharing a
1184
+ * glyph filename don't collide.
1185
+ */
1186
+ declare function generateNamedIconSetsSource(sets: NamedSetInput[]): string;
1187
+ /**
1188
+ * Vite plugin: scan `dir` for `*.svg`, write a strictly-typed
1189
+ * `icons.gen.tsx`, regenerate on add / unlink in dev.
1190
+ */
1191
+ declare function iconsPlugin(cfg: IconsPluginConfig): Plugin;
1192
+ //#endregion
1109
1193
  //#region src/seo.d.ts
1110
1194
  interface SitemapConfig {
1111
1195
  /** Base URL of the site (required). e.g. "https://example.com" */
@@ -1499,5 +1583,5 @@ declare function inferJsonLd(options: InferJsonLdOptions): Record<string, unknow
1499
1583
  */
1500
1584
  declare function aiPlugin(config: AiPluginConfig): Plugin;
1501
1585
  //#endregion
1502
- export { type AiPluginConfig, type CreateAppOptions, type CreateServerOptions, type FaviconLocaleConfig, type FaviconPluginConfig, type GenerateRouteModuleOptions, type GetStaticPaths, type InferJsonLdOptions, type OgImageLayer, type OgImagePluginConfig, type OgImageTemplate, type RobotsConfig, type SeoPluginConfig, type SitemapConfig, type VercelRevalidateHandlerOptions, _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 };
1586
+ export { type AiPluginConfig, type CreateAppOptions, type CreateServerOptions, type FaviconLocaleConfig, type FaviconPluginConfig, type GenerateRouteModuleOptions, type GetStaticPaths, type IconSetConfig, type IconsPluginConfig, type InferJsonLdOptions, type NamedSetInput, type OgImageLayer, type OgImagePluginConfig, type OgImageTemplate, type RobotsConfig, type SeoPluginConfig, type SitemapConfig, type VercelRevalidateHandlerOptions, _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 };
1503
1587
  //# sourceMappingURL=server2.d.ts.map
@@ -1,10 +1,9 @@
1
- import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
2
1
  import { VNodeChild } from "@pyreon/core";
3
2
 
4
3
  //#region src/theme.d.ts
5
4
  type Theme = 'light' | 'dark' | 'system';
6
5
  /** Reactive theme signal. */
7
- declare const theme: _$_pyreon_reactivity0.Signal<Theme>;
6
+ declare const theme: import("@pyreon/reactivity").Signal<Theme>;
8
7
  /**
9
8
  * Set the default theme for SSR (when `matchMedia` is unavailable).
10
9
  * Call once at server startup before rendering.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
@@ -168,15 +168,15 @@
168
168
  "lint": "oxlint ."
169
169
  },
170
170
  "dependencies": {
171
- "@pyreon/core": "^0.18.0",
172
- "@pyreon/head": "^0.18.0",
173
- "@pyreon/meta": "^0.18.0",
174
- "@pyreon/reactivity": "^0.18.0",
175
- "@pyreon/router": "^0.18.0",
176
- "@pyreon/runtime-dom": "^0.18.0",
177
- "@pyreon/runtime-server": "^0.18.0",
178
- "@pyreon/server": "^0.18.0",
179
- "@pyreon/vite-plugin": "^0.18.0",
171
+ "@pyreon/core": "^0.20.0",
172
+ "@pyreon/head": "^0.20.0",
173
+ "@pyreon/meta": "^0.20.0",
174
+ "@pyreon/reactivity": "^0.20.0",
175
+ "@pyreon/router": "^0.20.0",
176
+ "@pyreon/runtime-dom": "^0.20.0",
177
+ "@pyreon/runtime-server": "^0.20.0",
178
+ "@pyreon/server": "^0.20.0",
179
+ "@pyreon/vite-plugin": "^0.20.0",
180
180
  "vite": "^8.0.0"
181
181
  },
182
182
  "devDependencies": {
package/src/api-routes.ts CHANGED
@@ -52,6 +52,15 @@ export function matchApiRoute(pattern: string, path: string): Record<string, str
52
52
  const pathParts = path.split('/').filter(Boolean)
53
53
  const params: Record<string, string> = {}
54
54
 
55
+ // A param NAME comes from the route pattern (file-based route like
56
+ // `[__proto__].ts`) — developer-controlled, so this is defense-in-depth
57
+ // rather than an attacker vector, but assigning `params['__proto__'] =
58
+ // …` still mutates the result object's prototype instead of setting a
59
+ // param. Skip the dangerous names (consistent with reconcile / i18n
60
+ // deepMerge guards) so a stray route name can't pollute.
61
+ const isUnsafeParam = (name: string): boolean =>
62
+ name === '__proto__' || name === 'constructor' || name === 'prototype'
63
+
55
64
  for (let i = 0; i < patternParts.length; i++) {
56
65
  const pp = patternParts[i]
57
66
  if (!pp) continue
@@ -59,7 +68,7 @@ export function matchApiRoute(pattern: string, path: string): Record<string, str
59
68
  // Catch-all: :param*
60
69
  if (pp.endsWith('*')) {
61
70
  const paramName = pp.slice(1, -1)
62
- params[paramName] = pathParts.slice(i).join('/')
71
+ if (!isUnsafeParam(paramName)) params[paramName] = pathParts.slice(i).join('/')
63
72
  return params
64
73
  }
65
74
 
@@ -68,7 +77,8 @@ export function matchApiRoute(pattern: string, path: string): Record<string, str
68
77
 
69
78
  // Dynamic segment: :param
70
79
  if (pp.startsWith(':')) {
71
- params[pp.slice(1)] = pathParts[i]!
80
+ const paramName = pp.slice(1)
81
+ if (!isUnsafeParam(paramName)) params[paramName] = pathParts[i]!
72
82
  continue
73
83
  }
74
84