@pyreon/zero 0.1.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/lib/cache.js +80 -0
  4. package/lib/cache.js.map +1 -0
  5. package/lib/client.js +58 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/config.js +35 -0
  8. package/lib/config.js.map +1 -0
  9. package/lib/font.js +251 -0
  10. package/lib/font.js.map +1 -0
  11. package/lib/fs-router-BkbIWqek.js +30 -0
  12. package/lib/fs-router-BkbIWqek.js.map +1 -0
  13. package/lib/fs-router-jfd1QGLB.js +261 -0
  14. package/lib/fs-router-jfd1QGLB.js.map +1 -0
  15. package/lib/image-plugin.js +289 -0
  16. package/lib/image-plugin.js.map +1 -0
  17. package/lib/image.js +113 -0
  18. package/lib/image.js.map +1 -0
  19. package/lib/index.js +1665 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/link.js +186 -0
  22. package/lib/link.js.map +1 -0
  23. package/lib/script.js +102 -0
  24. package/lib/script.js.map +1 -0
  25. package/lib/seo.js +136 -0
  26. package/lib/seo.js.map +1 -0
  27. package/lib/theme.js +165 -0
  28. package/lib/theme.js.map +1 -0
  29. package/lib/types/adapters/bun.d.ts +6 -0
  30. package/lib/types/adapters/bun.d.ts.map +1 -0
  31. package/lib/types/adapters/index.d.ts +10 -0
  32. package/lib/types/adapters/index.d.ts.map +1 -0
  33. package/lib/types/adapters/node.d.ts +6 -0
  34. package/lib/types/adapters/node.d.ts.map +1 -0
  35. package/lib/types/adapters/static.d.ts +7 -0
  36. package/lib/types/adapters/static.d.ts.map +1 -0
  37. package/lib/types/app.d.ts +24 -0
  38. package/lib/types/app.d.ts.map +1 -0
  39. package/lib/types/cache.d.ts +54 -0
  40. package/lib/types/cache.d.ts.map +1 -0
  41. package/lib/types/client.d.ts +19 -0
  42. package/lib/types/client.d.ts.map +1 -0
  43. package/lib/types/config.d.ts +18 -0
  44. package/lib/types/config.d.ts.map +1 -0
  45. package/lib/types/entry-server.d.ts +26 -0
  46. package/lib/types/entry-server.d.ts.map +1 -0
  47. package/lib/types/font.d.ts +119 -0
  48. package/lib/types/font.d.ts.map +1 -0
  49. package/lib/types/fs-router.d.ts +33 -0
  50. package/lib/types/fs-router.d.ts.map +1 -0
  51. package/lib/types/image-plugin.d.ts +79 -0
  52. package/lib/types/image-plugin.d.ts.map +1 -0
  53. package/lib/types/image.d.ts +50 -0
  54. package/lib/types/image.d.ts.map +1 -0
  55. package/lib/types/index.d.ts +27 -0
  56. package/lib/types/index.d.ts.map +1 -0
  57. package/lib/types/isr.d.ts +9 -0
  58. package/lib/types/isr.d.ts.map +1 -0
  59. package/lib/types/link.d.ts +116 -0
  60. package/lib/types/link.d.ts.map +1 -0
  61. package/lib/types/script.d.ts +34 -0
  62. package/lib/types/script.d.ts.map +1 -0
  63. package/lib/types/seo.d.ts +88 -0
  64. package/lib/types/seo.d.ts.map +1 -0
  65. package/lib/types/theme.d.ts +38 -0
  66. package/lib/types/theme.d.ts.map +1 -0
  67. package/lib/types/types.d.ts +104 -0
  68. package/lib/types/types.d.ts.map +1 -0
  69. package/lib/types/utils/use-intersection-observer.d.ts +10 -0
  70. package/lib/types/utils/use-intersection-observer.d.ts.map +1 -0
  71. package/lib/types/utils/with-headers.d.ts +6 -0
  72. package/lib/types/utils/with-headers.d.ts.map +1 -0
  73. package/lib/types/vite-plugin.d.ts +17 -0
  74. package/lib/types/vite-plugin.d.ts.map +1 -0
  75. package/package.json +100 -0
  76. package/src/adapters/bun.ts +65 -0
  77. package/src/adapters/index.ts +29 -0
  78. package/src/adapters/node.ts +113 -0
  79. package/src/adapters/static.ts +17 -0
  80. package/src/app.ts +62 -0
  81. package/src/cache.ts +149 -0
  82. package/src/client.ts +43 -0
  83. package/src/config.ts +36 -0
  84. package/src/entry-server.ts +51 -0
  85. package/src/font.ts +461 -0
  86. package/src/fs-router.ts +380 -0
  87. package/src/image-plugin.ts +452 -0
  88. package/src/image.tsx +167 -0
  89. package/src/index.ts +119 -0
  90. package/src/isr.ts +95 -0
  91. package/src/link.tsx +266 -0
  92. package/src/script.tsx +133 -0
  93. package/src/seo.ts +281 -0
  94. package/src/sharp.d.ts +20 -0
  95. package/src/theme.tsx +162 -0
  96. package/src/types.ts +130 -0
  97. package/src/utils/use-intersection-observer.ts +36 -0
  98. package/src/utils/with-headers.ts +16 -0
  99. package/src/vite-plugin.ts +92 -0
package/lib/index.js ADDED
@@ -0,0 +1,1665 @@
1
+ import { a as scanRouteFiles, i as parseFileRoutes, r as generateRouteModule, t as filePathToUrlPath } from "./fs-router-jfd1QGLB.js";
2
+ import { Fragment, createRef, h, onMount, onUnmount } from "@pyreon/core";
3
+ import { HeadProvider } from "@pyreon/head";
4
+ import { RouterProvider, RouterView, createRouter, useRouter } from "@pyreon/router";
5
+ import { createHandler } from "@pyreon/server";
6
+ import { effect, signal } from "@pyreon/reactivity";
7
+ import { jsx, jsxs } from "@pyreon/core/jsx-runtime";
8
+ import { existsSync } from "node:fs";
9
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
10
+ import { basename, extname, join } from "node:path";
11
+
12
+ //#region src/app.ts
13
+ /**
14
+ * Create a full Zero app — assembles router, head provider, and root layout.
15
+ *
16
+ * Used internally by entry-server and entry-client.
17
+ */
18
+ function createApp(options) {
19
+ const router = createRouter({
20
+ routes: options.routes,
21
+ mode: options.routerMode ?? "history",
22
+ url: options.url,
23
+ scrollBehavior: "top"
24
+ });
25
+ const Layout = options.layout ?? DefaultLayout;
26
+ function App() {
27
+ return h(HeadProvider, null, h(RouterProvider, { router }, h(Layout, null, h(RouterView, null))));
28
+ }
29
+ return {
30
+ App,
31
+ router
32
+ };
33
+ }
34
+ function DefaultLayout(props) {
35
+ return h(Fragment, null, ...Array.isArray(props.children) ? props.children : [props.children]);
36
+ }
37
+
38
+ //#endregion
39
+ //#region src/entry-server.ts
40
+ /**
41
+ * Create the SSR request handler for production.
42
+ *
43
+ * @example
44
+ * import { routes } from "virtual:zero/routes"
45
+ * import { createServer } from "@pyreon/zero"
46
+ *
47
+ * export default createServer({ routes })
48
+ */
49
+ function createServer(options) {
50
+ const config = options.config ?? {};
51
+ const allMiddleware = [...config.middleware ?? [], ...options.middleware ?? []];
52
+ const { App } = createApp({
53
+ routes: options.routes,
54
+ routerMode: "history"
55
+ });
56
+ return createHandler({
57
+ App,
58
+ routes: options.routes,
59
+ middleware: allMiddleware,
60
+ mode: config.ssr?.mode ?? "string",
61
+ template: options.template,
62
+ clientEntry: options.clientEntry
63
+ });
64
+ }
65
+
66
+ //#endregion
67
+ //#region src/config.ts
68
+ /**
69
+ * Define a Zero configuration.
70
+ * Used in `zero.config.ts` at the project root.
71
+ *
72
+ * @example
73
+ * import { defineConfig } from "@pyreon/zero/config"
74
+ *
75
+ * export default defineConfig({
76
+ * mode: "ssr",
77
+ * ssr: { mode: "stream" },
78
+ * port: 3000,
79
+ * })
80
+ */
81
+ function defineConfig(config) {
82
+ return config;
83
+ }
84
+ /** Merge user config with defaults. */
85
+ function resolveConfig(userConfig = {}) {
86
+ return {
87
+ mode: "ssr",
88
+ base: "/",
89
+ port: 3e3,
90
+ adapter: "node",
91
+ ...userConfig,
92
+ ssr: {
93
+ mode: "string",
94
+ ...userConfig.ssr
95
+ }
96
+ };
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/vite-plugin.ts
101
+ const VIRTUAL_ROUTES_ID = "virtual:zero/routes";
102
+ const RESOLVED_VIRTUAL_ROUTES_ID = `\0${VIRTUAL_ROUTES_ID}`;
103
+ /**
104
+ * Zero Vite plugin — adds file-based routing and zero-config conventions
105
+ * on top of @pyreon/vite-plugin.
106
+ *
107
+ * @example
108
+ * // vite.config.ts
109
+ * import pyreon from "@pyreon/vite-plugin"
110
+ * import zero from "@pyreon/zero"
111
+ *
112
+ * export default {
113
+ * plugins: [pyreon(), zero()],
114
+ * }
115
+ */
116
+ function zeroPlugin(userConfig = {}) {
117
+ const config = resolveConfig(userConfig);
118
+ let routesDir;
119
+ let root;
120
+ return {
121
+ name: "pyreon-zero",
122
+ enforce: "pre",
123
+ _zeroConfig: userConfig,
124
+ configResolved(resolvedConfig) {
125
+ root = resolvedConfig.root;
126
+ routesDir = `${root}/src/routes`;
127
+ },
128
+ resolveId(id) {
129
+ if (id === VIRTUAL_ROUTES_ID) return RESOLVED_VIRTUAL_ROUTES_ID;
130
+ },
131
+ async load(id) {
132
+ if (id === RESOLVED_VIRTUAL_ROUTES_ID) try {
133
+ return generateRouteModule(await scanRouteFiles(routesDir), routesDir);
134
+ } catch (_err) {
135
+ return `export const routes = []`;
136
+ }
137
+ },
138
+ configureServer(server) {
139
+ server.watcher.add(`${routesDir}/**/*.{tsx,jsx,ts,js}`);
140
+ server.watcher.on("all", (event, path) => {
141
+ if (path.startsWith(routesDir) && (event === "add" || event === "unlink")) {
142
+ const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ROUTES_ID);
143
+ if (mod) {
144
+ server.moduleGraph.invalidateModule(mod);
145
+ server.ws.send({ type: "full-reload" });
146
+ }
147
+ }
148
+ });
149
+ },
150
+ config() {
151
+ return {
152
+ resolve: { conditions: ["bun"] },
153
+ server: { port: config.port },
154
+ define: {
155
+ __ZERO_MODE__: JSON.stringify(config.mode),
156
+ __ZERO_BASE__: JSON.stringify(config.base)
157
+ }
158
+ };
159
+ }
160
+ };
161
+ }
162
+
163
+ //#endregion
164
+ //#region src/isr.ts
165
+ /**
166
+ * In-memory ISR cache with stale-while-revalidate semantics.
167
+ *
168
+ * Wraps an SSR handler and caches responses per URL path.
169
+ * Serves stale content immediately while revalidating in the background.
170
+ */
171
+ function createISRHandler(handler, config) {
172
+ const cache = /* @__PURE__ */ new Map();
173
+ const revalidating = /* @__PURE__ */ new Set();
174
+ const revalidateMs = config.revalidate * 1e3;
175
+ async function revalidate(url) {
176
+ const key = url.pathname;
177
+ if (revalidating.has(key)) return;
178
+ revalidating.add(key);
179
+ try {
180
+ const res = await handler(new Request(url.href, { method: "GET" }));
181
+ const html = await res.text();
182
+ const headers = {};
183
+ res.headers.forEach((v, k) => {
184
+ headers[k] = v;
185
+ });
186
+ cache.set(key, {
187
+ html,
188
+ headers,
189
+ timestamp: Date.now()
190
+ });
191
+ } catch {} finally {
192
+ revalidating.delete(key);
193
+ }
194
+ }
195
+ return async (req) => {
196
+ if (req.method !== "GET") return handler(req);
197
+ const url = new URL(req.url);
198
+ const key = url.pathname;
199
+ const entry = cache.get(key);
200
+ if (entry) {
201
+ const age = Date.now() - entry.timestamp;
202
+ if (age > revalidateMs) revalidate(url);
203
+ return new Response(entry.html, {
204
+ status: 200,
205
+ headers: {
206
+ ...entry.headers,
207
+ "content-type": "text/html; charset=utf-8",
208
+ "x-isr-cache": age > revalidateMs ? "STALE" : "HIT",
209
+ "x-isr-age": String(Math.round(age / 1e3))
210
+ }
211
+ });
212
+ }
213
+ const res = await handler(req);
214
+ const html = await res.text();
215
+ const headers = {};
216
+ res.headers.forEach((v, k) => {
217
+ headers[k] = v;
218
+ });
219
+ cache.set(key, {
220
+ html,
221
+ headers,
222
+ timestamp: Date.now()
223
+ });
224
+ return new Response(html, {
225
+ status: 200,
226
+ headers: {
227
+ ...headers,
228
+ "content-type": "text/html; charset=utf-8",
229
+ "x-isr-cache": "MISS"
230
+ }
231
+ });
232
+ };
233
+ }
234
+
235
+ //#endregion
236
+ //#region src/adapters/bun.ts
237
+ /**
238
+ * Bun adapter — generates a standalone Bun.serve() entry.
239
+ */
240
+ function bunAdapter() {
241
+ return {
242
+ name: "bun",
243
+ async build(options) {
244
+ const { writeFile, cp, mkdir } = await import("node:fs/promises");
245
+ const { join } = await import("node:path");
246
+ const outDir = options.outDir;
247
+ await mkdir(outDir, { recursive: true });
248
+ await cp(options.clientOutDir, join(outDir, "client"), { recursive: true });
249
+ await cp(join(options.serverEntry, ".."), join(outDir, "server"), { recursive: true });
250
+ const port = options.config.port ?? 3e3;
251
+ const serverEntry = `
252
+ const handler = (await import("./server/entry-server.js")).default
253
+ const clientDir = new URL("./client/", import.meta.url).pathname
254
+
255
+ Bun.serve({
256
+ port: ${port},
257
+ async fetch(req) {
258
+ const url = new URL(req.url)
259
+
260
+ // Try static files first
261
+ if (req.method === "GET") {
262
+ const filePath = clientDir + (url.pathname === "/" ? "index.html" : url.pathname)
263
+ // Prevent path traversal — ensure resolved path stays within clientDir
264
+ const resolved = Bun.resolveSync(filePath, ".")
265
+ if (!resolved.startsWith(Bun.resolveSync(clientDir, "."))) {
266
+ return new Response("Forbidden", { status: 403 })
267
+ }
268
+ const file = Bun.file(filePath)
269
+ if (await file.exists()) {
270
+ return new Response(file, {
271
+ headers: {
272
+ "cache-control": filePath.endsWith(".js") || filePath.endsWith(".css")
273
+ ? "public, max-age=31536000, immutable"
274
+ : "public, max-age=3600",
275
+ },
276
+ })
277
+ }
278
+ }
279
+
280
+ // Fall through to SSR handler
281
+ return handler(req)
282
+ },
283
+ })
284
+
285
+ console.log("\\n ⚡ Zero production server running on http://localhost:${port}\\n")
286
+ `.trimStart();
287
+ await writeFile(join(outDir, "index.ts"), serverEntry);
288
+ }
289
+ };
290
+ }
291
+
292
+ //#endregion
293
+ //#region src/adapters/node.ts
294
+ /**
295
+ * Node.js adapter — generates a standalone server entry using node:http.
296
+ */
297
+ function nodeAdapter() {
298
+ return {
299
+ name: "node",
300
+ async build(options) {
301
+ const { writeFile, cp, mkdir } = await import("node:fs/promises");
302
+ const { join } = await import("node:path");
303
+ const outDir = options.outDir;
304
+ await mkdir(outDir, { recursive: true });
305
+ await cp(options.clientOutDir, join(outDir, "client"), { recursive: true });
306
+ await cp(join(options.serverEntry, ".."), join(outDir, "server"), { recursive: true });
307
+ const port = options.config.port ?? 3e3;
308
+ const serverEntry = `
309
+ import { createServer } from "node:http"
310
+ import { readFile } from "node:fs/promises"
311
+ import { join, extname } from "node:path"
312
+ import { fileURLToPath } from "node:url"
313
+
314
+ const __dirname = fileURLToPath(new URL(".", import.meta.url))
315
+ const handler = (await import("./server/entry-server.js")).default
316
+ const clientDir = join(__dirname, "client")
317
+
318
+ const MIME_TYPES = {
319
+ ".html": "text/html",
320
+ ".js": "application/javascript",
321
+ ".css": "text/css",
322
+ ".json": "application/json",
323
+ ".png": "image/png",
324
+ ".jpg": "image/jpeg",
325
+ ".svg": "image/svg+xml",
326
+ ".woff2": "font/woff2",
327
+ ".woff": "font/woff",
328
+ ".ico": "image/x-icon",
329
+ }
330
+
331
+ const server = createServer(async (req, res) => {
332
+ const url = new URL(req.url ?? "/", "http://localhost")
333
+
334
+ // Try to serve static files first
335
+ if (req.method === "GET") {
336
+ try {
337
+ const filePath = join(clientDir, url.pathname === "/" ? "index.html" : url.pathname)
338
+ // Prevent path traversal — ensure resolved path stays within clientDir
339
+ const { resolve } = await import("node:path")
340
+ const resolved = resolve(filePath)
341
+ if (!resolved.startsWith(resolve(clientDir))) {
342
+ res.writeHead(403)
343
+ res.end("Forbidden")
344
+ return
345
+ }
346
+ const ext = extname(filePath)
347
+ if (ext && ext !== ".html") {
348
+ const data = await readFile(filePath)
349
+ const mime = MIME_TYPES[ext] || "application/octet-stream"
350
+ res.writeHead(200, {
351
+ "content-type": mime,
352
+ "cache-control": ext === ".js" || ext === ".css"
353
+ ? "public, max-age=31536000, immutable"
354
+ : "public, max-age=3600",
355
+ })
356
+ res.end(data)
357
+ return
358
+ }
359
+ } catch {}
360
+ }
361
+
362
+ // Fall through to SSR handler
363
+ const headers = {}
364
+ for (const [key, value] of Object.entries(req.headers)) {
365
+ if (value) headers[key] = Array.isArray(value) ? value.join(", ") : value
366
+ }
367
+
368
+ const request = new Request(url.href, {
369
+ method: req.method,
370
+ headers,
371
+ })
372
+
373
+ const response = await handler(request)
374
+ const body = await response.text()
375
+
376
+ const responseHeaders = {}
377
+ response.headers.forEach((v, k) => { responseHeaders[k] = v })
378
+
379
+ res.writeHead(response.status, responseHeaders)
380
+ res.end(body)
381
+ })
382
+
383
+ server.listen(${port}, () => {
384
+ console.log("\\n ⚡ Zero production server running on http://localhost:${port}\\n")
385
+ })
386
+ `.trimStart();
387
+ await writeFile(join(outDir, "index.js"), serverEntry);
388
+ await writeFile(join(outDir, "package.json"), JSON.stringify({ type: "module" }, null, 2));
389
+ }
390
+ };
391
+ }
392
+
393
+ //#endregion
394
+ //#region src/adapters/static.ts
395
+ /**
396
+ * Static adapter — just copies the client build output.
397
+ * Used with SSG mode where all pages are pre-rendered at build time.
398
+ */
399
+ function staticAdapter() {
400
+ return {
401
+ name: "static",
402
+ async build(options) {
403
+ const { cp, mkdir } = await import("node:fs/promises");
404
+ await mkdir(options.outDir, { recursive: true });
405
+ await cp(options.clientOutDir, options.outDir, { recursive: true });
406
+ }
407
+ };
408
+ }
409
+
410
+ //#endregion
411
+ //#region src/adapters/index.ts
412
+ /**
413
+ * Resolve the adapter from config.
414
+ * Returns a built-in adapter or throws if unknown.
415
+ */
416
+ function resolveAdapter(config) {
417
+ const name = config.adapter ?? "node";
418
+ switch (name) {
419
+ case "node": return nodeAdapter();
420
+ case "bun": return bunAdapter();
421
+ case "static": return staticAdapter();
422
+ default: throw new Error(`[zero] Unknown adapter: "${name}". Use "node", "bun", or "static".`);
423
+ }
424
+ }
425
+
426
+ //#endregion
427
+ //#region src/utils/use-intersection-observer.ts
428
+ /**
429
+ * Observes an element and calls `onIntersect` once it enters the viewport.
430
+ * Automatically disconnects after the first intersection.
431
+ *
432
+ * @param getElement - Getter for the target element (may be undefined before mount).
433
+ * @param onIntersect - Callback fired when the element becomes visible.
434
+ * @param rootMargin - IntersectionObserver rootMargin. Default: "200px".
435
+ */
436
+ function useIntersectionObserver(getElement, onIntersect, rootMargin = "200px") {
437
+ onMount(() => {
438
+ const el = getElement();
439
+ if (!el) return void 0;
440
+ const observer = new IntersectionObserver((entries) => {
441
+ for (const entry of entries) if (entry.isIntersecting) {
442
+ onIntersect();
443
+ observer.disconnect();
444
+ }
445
+ }, { rootMargin });
446
+ observer.observe(el);
447
+ onUnmount(() => observer.disconnect());
448
+ });
449
+ }
450
+
451
+ //#endregion
452
+ //#region src/image.tsx
453
+ /**
454
+ * Optimized image component with lazy loading, responsive images,
455
+ * multi-format <picture> support, and blur-up placeholders.
456
+ *
457
+ * @example
458
+ * // With imagePlugin — spread the import directly
459
+ * import hero from "./hero.jpg?optimize"
460
+ * <Image {...hero} alt="Hero" priority />
461
+ *
462
+ * @example
463
+ * // Manual usage
464
+ * <Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
465
+ */
466
+ function Image(props) {
467
+ const isEager = props.priority || props.loading === "eager";
468
+ const loaded = signal(isEager);
469
+ const inView = signal(isEager);
470
+ const containerRef = createRef();
471
+ const resolvedSrcset = typeof props.srcset === "string" ? props.srcset : props.srcset?.map((s) => `${s.src} ${s.width}w`).join(", ");
472
+ const sizes = props.sizes ?? "100vw";
473
+ const fit = props.fit ?? "cover";
474
+ const hasFormats = props.formats && props.formats.length > 0;
475
+ const aspectRatio = `${props.width} / ${props.height}`;
476
+ if (!isEager) useIntersectionObserver(() => containerRef.current ?? void 0, () => inView.set(true));
477
+ const containerStyle = [
478
+ "position: relative",
479
+ "overflow: hidden",
480
+ `aspect-ratio: ${aspectRatio}`,
481
+ `max-width: ${props.width}px`,
482
+ "width: 100%",
483
+ props.style
484
+ ].filter(Boolean).join("; ");
485
+ const imgEl = /* @__PURE__ */ jsx("img", {
486
+ src: () => inView() ? props.src : "",
487
+ srcset: () => !hasFormats && inView() && resolvedSrcset ? resolvedSrcset : "",
488
+ sizes: resolvedSrcset ? sizes : void 0,
489
+ alt: props.alt,
490
+ width: props.width,
491
+ height: props.height,
492
+ loading: isEager ? "eager" : "lazy",
493
+ decoding: props.decoding ?? "async",
494
+ fetchpriority: props.priority ? "high" : void 0,
495
+ onload: () => loaded.set(true),
496
+ style: () => [
497
+ "display: block",
498
+ "width: 100%",
499
+ "height: 100%",
500
+ `object-fit: ${fit}`,
501
+ "transition: opacity 0.3s ease",
502
+ props.placeholder && !loaded() ? "opacity: 0" : "opacity: 1"
503
+ ].join("; ")
504
+ });
505
+ return /* @__PURE__ */ jsxs("div", {
506
+ ref: containerRef,
507
+ class: props.class,
508
+ style: containerStyle,
509
+ children: [props.placeholder && /* @__PURE__ */ jsx("img", {
510
+ src: props.placeholder,
511
+ alt: "",
512
+ "aria-hidden": "true",
513
+ loading: "eager",
514
+ style: () => [
515
+ "position: absolute",
516
+ "inset: 0",
517
+ "width: 100%",
518
+ "height: 100%",
519
+ "object-fit: cover",
520
+ "filter: blur(20px)",
521
+ "transform: scale(1.1)",
522
+ "transition: opacity 0.4s ease",
523
+ loaded() ? "opacity: 0; pointer-events: none" : "opacity: 1"
524
+ ].join("; ")
525
+ }), hasFormats ? /* @__PURE__ */ jsxs("picture", { children: [props.formats?.map((fmt) => /* @__PURE__ */ jsx("source", {
526
+ type: fmt.type,
527
+ srcset: () => inView() ? fmt.srcset : void 0,
528
+ sizes
529
+ })), imgEl] }) : imgEl]
530
+ });
531
+ }
532
+
533
+ //#endregion
534
+ //#region src/link.tsx
535
+ const prefetched = /* @__PURE__ */ new Set();
536
+ function doPrefetch(href) {
537
+ if (prefetched.has(href)) return;
538
+ prefetched.add(href);
539
+ const docLink = document.createElement("link");
540
+ docLink.rel = "prefetch";
541
+ docLink.href = href;
542
+ docLink.as = "document";
543
+ document.head.appendChild(docLink);
544
+ try {
545
+ const chunkHint = document.createElement("link");
546
+ chunkHint.rel = "modulepreload";
547
+ chunkHint.href = href;
548
+ document.head.appendChild(chunkHint);
549
+ } catch {}
550
+ }
551
+ /**
552
+ * Composable that provides all link behavior — navigation, prefetching,
553
+ * active state, and viewport observation.
554
+ *
555
+ * Use this for full control when `createLink` is too opinionated.
556
+ *
557
+ * @example
558
+ * function MyLink(props: LinkProps) {
559
+ * const link = useLink(props)
560
+ * return (
561
+ * <button ref={link.ref} class={link.classes()} onclick={link.handleClick}>
562
+ * {props.children}
563
+ * </button>
564
+ * )
565
+ * }
566
+ */
567
+ function useLink(props) {
568
+ const router = useRouter();
569
+ const elementRef = createRef();
570
+ const strategy = props.prefetch ?? "hover";
571
+ function handleClick(e) {
572
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || props.external) return;
573
+ e.preventDefault();
574
+ router.push(props.href);
575
+ }
576
+ function handleMouseEnter() {
577
+ if (strategy === "hover") doPrefetch(props.href);
578
+ }
579
+ function handleTouchStart() {
580
+ if (strategy === "hover" || strategy === "viewport") doPrefetch(props.href);
581
+ }
582
+ if (strategy === "viewport") useIntersectionObserver(() => elementRef.current ?? void 0, () => doPrefetch(props.href));
583
+ const isActive = () => {
584
+ const currentPath = router.currentRoute()?.path;
585
+ if (!currentPath || !props.href) return false;
586
+ if (props.href === "/") return currentPath === "/";
587
+ return currentPath.startsWith(props.href);
588
+ };
589
+ const isExactActive = () => {
590
+ const currentPath = router.currentRoute()?.path;
591
+ if (!currentPath) return false;
592
+ return currentPath === props.href;
593
+ };
594
+ const classes = () => {
595
+ const cls = [];
596
+ if (props.class) cls.push(props.class);
597
+ if (props.activeClass && isActive()) cls.push(props.activeClass);
598
+ if (props.exactActiveClass && isExactActive()) cls.push(props.exactActiveClass);
599
+ return cls.join(" ");
600
+ };
601
+ return {
602
+ ref: elementRef,
603
+ handleClick,
604
+ handleMouseEnter,
605
+ handleTouchStart,
606
+ isActive,
607
+ isExactActive,
608
+ classes
609
+ };
610
+ }
611
+ /**
612
+ * Higher-order component that wraps any component with link behavior.
613
+ *
614
+ * The wrapped component receives {@link LinkRenderProps} with all handlers,
615
+ * active state, and accessibility attributes pre-wired.
616
+ *
617
+ * @example
618
+ * // Custom button link
619
+ * const ButtonLink = createLink((props) => (
620
+ * <button
621
+ * ref={props.ref}
622
+ * class={props.class}
623
+ * onclick={props.onClick}
624
+ * onmouseenter={props.onMouseEnter}
625
+ * >
626
+ * {props.children}
627
+ * </button>
628
+ * ))
629
+ *
630
+ * // Custom styled component
631
+ * const CardLink = createLink((props) => (
632
+ * <div
633
+ * ref={props.ref}
634
+ * class={`card ${props.isActive() ? "card--active" : ""}`}
635
+ * onclick={props.onClick}
636
+ * onmouseenter={props.onMouseEnter}
637
+ * >
638
+ * {props.children}
639
+ * </div>
640
+ * ))
641
+ *
642
+ * // Usage
643
+ * <ButtonLink href="/about">About</ButtonLink>
644
+ * <CardLink href="/posts" prefetch="viewport">Posts</CardLink>
645
+ */
646
+ function createLink(Component) {
647
+ return function WrappedLink(props) {
648
+ const link = useLink(props);
649
+ return /* @__PURE__ */ jsx(Component, {
650
+ href: props.href,
651
+ ref: link.ref,
652
+ onClick: link.handleClick,
653
+ onMouseEnter: link.handleMouseEnter,
654
+ onTouchStart: link.handleTouchStart,
655
+ isActive: link.isActive,
656
+ isExactActive: link.isExactActive,
657
+ class: link.classes,
658
+ style: props.style,
659
+ target: props.external ? "_blank" : void 0,
660
+ rel: props.external ? "noopener noreferrer" : void 0,
661
+ "aria-label": props["aria-label"],
662
+ children: props.children
663
+ });
664
+ };
665
+ }
666
+ /**
667
+ * Default navigation link built on an `<a>` tag.
668
+ *
669
+ * @example
670
+ * <Link href="/about" prefetch="viewport">About</Link>
671
+ * <Link href="/posts" activeClass="nav-active">Posts</Link>
672
+ */
673
+ const Link = createLink((props) => /* @__PURE__ */ jsx("a", {
674
+ ref: props.ref,
675
+ href: props.href,
676
+ class: props.class,
677
+ style: props.style,
678
+ target: props.target,
679
+ rel: props.rel,
680
+ "aria-label": props["aria-label"],
681
+ "aria-current": props.isExactActive() ? "page" : void 0,
682
+ onclick: props.onClick,
683
+ onmouseenter: props.onMouseEnter,
684
+ ontouchstart: props.onTouchStart,
685
+ children: props.children
686
+ }));
687
+
688
+ //#endregion
689
+ //#region src/script.tsx
690
+ /**
691
+ * Optimized script loading component.
692
+ *
693
+ * @example
694
+ * // Load analytics after page is interactive
695
+ * <Script src="https://analytics.example.com/script.js" strategy="onIdle" />
696
+ *
697
+ * // Load chat widget when user scrolls
698
+ * <Script src="/chat-widget.js" strategy="onViewport" />
699
+ *
700
+ * // Inline script with deferred execution
701
+ * <Script strategy="afterHydration">
702
+ * {`console.log("App hydrated!")`}
703
+ * <\/Script>
704
+ */
705
+ function Script(props) {
706
+ function loadScript() {
707
+ if (props.id && document.getElementById(props.id)) return;
708
+ const script = document.createElement("script");
709
+ if (props.src) script.src = props.src;
710
+ if (props.id) script.id = props.id;
711
+ script.async = props.async !== false;
712
+ if (props.onLoad) script.onload = props.onLoad;
713
+ if (props.onError) script.onerror = () => props.onError?.(/* @__PURE__ */ new Error(`Failed to load: ${props.src}`));
714
+ if (props.children && !props.src) script.textContent = props.children;
715
+ document.head.appendChild(script);
716
+ }
717
+ onMount(() => {
718
+ switch (props.strategy ?? "afterHydration") {
719
+ case "beforeHydration": break;
720
+ case "afterHydration":
721
+ loadScript();
722
+ break;
723
+ case "onIdle":
724
+ if ("requestIdleCallback" in window) requestIdleCallback(() => loadScript(), { timeout: 5e3 });
725
+ else setTimeout(loadScript, 200);
726
+ break;
727
+ case "onInteraction": {
728
+ const events = [
729
+ "click",
730
+ "scroll",
731
+ "keydown",
732
+ "touchstart"
733
+ ];
734
+ function handler() {
735
+ for (const e of events) document.removeEventListener(e, handler);
736
+ loadScript();
737
+ }
738
+ for (const e of events) document.addEventListener(e, handler, {
739
+ once: true,
740
+ passive: true
741
+ });
742
+ onUnmount(() => {
743
+ for (const e of events) document.removeEventListener(e, handler);
744
+ });
745
+ break;
746
+ }
747
+ case "onViewport": break;
748
+ }
749
+ });
750
+ const sentinelRef = createRef();
751
+ const strategy = props.strategy ?? "afterHydration";
752
+ if (strategy === "onViewport") useIntersectionObserver(() => sentinelRef.current ?? void 0, () => loadScript());
753
+ if (strategy === "onViewport") return /* @__PURE__ */ jsx("div", {
754
+ ref: sentinelRef,
755
+ style: "width:0;height:0;overflow:hidden"
756
+ });
757
+ return null;
758
+ }
759
+
760
+ //#endregion
761
+ //#region src/cache.ts
762
+ const HASHED_ASSET = /\.[a-f0-9]{8,}\.\w+$/;
763
+ const STATIC_EXT = /\.(png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|mp4|webm|ogg|mp3|wav)$/i;
764
+ const SCRIPT_EXT = /\.(js|css|mjs)$/i;
765
+ /** @internal Exported for testing */
766
+ function matchGlob(pattern, path) {
767
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
768
+ return new RegExp(`^${regex}$`).test(path);
769
+ }
770
+ function resolveControl(path, immutableDuration, staticDuration, pageDuration, swr) {
771
+ if (HASHED_ASSET.test(path)) return `public, max-age=${immutableDuration}, immutable`;
772
+ if (SCRIPT_EXT.test(path)) return `public, max-age=3600, stale-while-revalidate=${swr}`;
773
+ if (STATIC_EXT.test(path)) return `public, max-age=${staticDuration}, stale-while-revalidate=${swr}`;
774
+ if (pageDuration > 0) return `public, max-age=${pageDuration}, stale-while-revalidate=${swr}`;
775
+ return "no-cache";
776
+ }
777
+ /**
778
+ * Cache control middleware for Zero.
779
+ * Sets Cache-Control headers on the response based on asset type.
780
+ *
781
+ * @example
782
+ * import { cacheMiddleware } from "@pyreon/zero/cache"
783
+ *
784
+ * export default createHandler({
785
+ * routes,
786
+ * middleware: [
787
+ * cacheMiddleware({
788
+ * pages: 60,
789
+ * staleWhileRevalidate: 300,
790
+ * rules: [
791
+ * { match: "/api/*", control: "no-store" },
792
+ * ],
793
+ * }),
794
+ * ],
795
+ * })
796
+ */
797
+ function cacheMiddleware(config = {}) {
798
+ const immutableDuration = config.immutable ?? 31536e3;
799
+ const staticDuration = config.static ?? 86400;
800
+ const pageDuration = config.pages ?? 0;
801
+ const swr = config.staleWhileRevalidate ?? 60;
802
+ const rules = config.rules ?? [];
803
+ return (ctx) => {
804
+ const path = ctx.url.pathname;
805
+ for (const rule of rules) if (matchGlob(rule.match, path)) {
806
+ ctx.headers.set("Cache-Control", rule.control);
807
+ return;
808
+ }
809
+ const control = resolveControl(path, immutableDuration, staticDuration, pageDuration, swr);
810
+ ctx.headers.set("Cache-Control", control);
811
+ };
812
+ }
813
+ /**
814
+ * Security headers middleware.
815
+ * Adds common security headers to all responses.
816
+ */
817
+ function securityHeaders() {
818
+ return (ctx) => {
819
+ ctx.headers.set("X-Content-Type-Options", "nosniff");
820
+ ctx.headers.set("X-Frame-Options", "DENY");
821
+ ctx.headers.set("X-XSS-Protection", "1; mode=block");
822
+ ctx.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
823
+ ctx.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
824
+ };
825
+ }
826
+ /**
827
+ * Compression detection middleware.
828
+ * Sets Vary: Accept-Encoding header so caches can serve compressed variants.
829
+ * Actual compression is handled by the runtime (Bun/Node) or reverse proxy.
830
+ */
831
+ function varyEncoding() {
832
+ return (ctx) => {
833
+ const existing = ctx.headers.get("Vary");
834
+ if (!existing?.includes("Accept-Encoding")) ctx.headers.set("Vary", existing ? `${existing}, Accept-Encoding` : "Accept-Encoding");
835
+ };
836
+ }
837
+
838
+ //#endregion
839
+ //#region src/font.ts
840
+ /**
841
+ * Normalize a GoogleFontInput (string or object) into a ResolvedFont.
842
+ */
843
+ function resolveGoogleFont(input) {
844
+ if (typeof input === "string") return parseGoogleFamily(input);
845
+ if (input.variable) return {
846
+ family: input.family,
847
+ italic: input.italic ?? false,
848
+ variable: true,
849
+ weightRange: input.weightRange
850
+ };
851
+ return {
852
+ family: input.family,
853
+ italic: input.italic ?? false,
854
+ variable: false,
855
+ weights: input.weights
856
+ };
857
+ }
858
+ /**
859
+ * Parse Google Fonts family string shorthand.
860
+ *
861
+ * Static weights: "Inter:wght@400;500;700"
862
+ * Variable range: "Inter:wght@100..900"
863
+ * Variable with italic: "Inter:ital,wght@100..900"
864
+ */
865
+ function parseGoogleFamily(input) {
866
+ const [familyPart, spec] = input.split(":");
867
+ const family = familyPart?.trim();
868
+ let italic = false;
869
+ if (spec) {
870
+ italic = spec.includes("ital");
871
+ const rangeMatch = spec.match(/wght@(\d+)\.\.(\d+)/);
872
+ if (rangeMatch) return {
873
+ family,
874
+ italic,
875
+ variable: true,
876
+ weightRange: [Number(rangeMatch[1]), Number(rangeMatch[2])]
877
+ };
878
+ const weightMatch = spec.match(/wght@([\d;]+)/);
879
+ if (weightMatch) return {
880
+ family,
881
+ italic,
882
+ variable: false,
883
+ weights: weightMatch[1]?.split(";").map(Number)
884
+ };
885
+ }
886
+ return {
887
+ family,
888
+ italic,
889
+ variable: false,
890
+ weights: [400]
891
+ };
892
+ }
893
+ /**
894
+ * Generate a Google Fonts CSS URL.
895
+ */
896
+ function googleFontsUrl(families, display = "swap") {
897
+ return `https://fonts.googleapis.com/css2?${families.map((f) => {
898
+ const axes = f.italic ? "ital,wght" : "wght";
899
+ const name = f.family.replace(/ /g, "+");
900
+ if (f.variable) {
901
+ const range = `${f.weightRange[0]}..${f.weightRange[1]}`;
902
+ return `family=${name}:${axes}@${f.italic ? `0,${range};1,${range}` : range}`;
903
+ }
904
+ return `family=${name}:${axes}@${f.weights.map((w) => f.italic ? `0,${w};1,${w}` : String(w)).join(";")}`;
905
+ }).join("&")}&display=${display}`;
906
+ }
907
+ /**
908
+ * Generate @font-face CSS for local fonts.
909
+ */
910
+ function localFontFaces(fonts, display) {
911
+ return fonts.map((f) => `@font-face {
912
+ font-family: "${f.family}";
913
+ src: url("${f.src}");
914
+ font-weight: ${f.weight ?? "400"};
915
+ font-style: ${f.style ?? "normal"};
916
+ font-display: ${f.display ?? display};
917
+ }`).join("\n\n");
918
+ }
919
+ /**
920
+ * Generate size-adjusted fallback @font-face declarations to reduce CLS.
921
+ */
922
+ function fallbackFontFaces(fallbacks) {
923
+ return Object.entries(fallbacks).map(([family, metrics]) => {
924
+ const overrides = [];
925
+ if (metrics.sizeAdjust != null) overrides.push(` size-adjust: ${metrics.sizeAdjust * 100}%;`);
926
+ if (metrics.ascentOverride != null) overrides.push(` ascent-override: ${metrics.ascentOverride}%;`);
927
+ if (metrics.descentOverride != null) overrides.push(` descent-override: ${metrics.descentOverride}%;`);
928
+ if (metrics.lineGapOverride != null) overrides.push(` line-gap-override: ${metrics.lineGapOverride}%;`);
929
+ return `@font-face {
930
+ font-family: "${family} Fallback";
931
+ src: local("${metrics.fallback}");
932
+ ${overrides.join("\n")}
933
+ }`;
934
+ }).join("\n\n");
935
+ }
936
+ /**
937
+ * Generate preload link tags for critical font files.
938
+ */
939
+ function preloadTags(fonts) {
940
+ return fonts.map((f) => {
941
+ const ext = f.src.split(".").pop();
942
+ const type = ext === "woff2" ? "font/woff2" : ext === "woff" ? "font/woff" : ext === "ttf" ? "font/ttf" : "font/otf";
943
+ return `<link rel="preload" href="${f.src}" as="font" type="${type}" crossorigin>`;
944
+ }).join("\n");
945
+ }
946
+ /**
947
+ * Download Google Fonts CSS with woff2 user agent.
948
+ */
949
+ async function downloadGoogleFontsCSS(url) {
950
+ const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } });
951
+ if (!response.ok) throw new Error(`Failed to fetch Google Fonts CSS: ${response.status}`);
952
+ return response.text();
953
+ }
954
+ /**
955
+ * Download a font file.
956
+ */
957
+ async function downloadFontFile(url) {
958
+ const response = await fetch(url);
959
+ if (!response.ok) throw new Error(`Failed to download font: ${url}`);
960
+ const arrayBuffer = await response.arrayBuffer();
961
+ return Buffer.from(arrayBuffer);
962
+ }
963
+ /**
964
+ * Extract font file URLs from Google Fonts CSS.
965
+ */
966
+ function extractFontUrls(css) {
967
+ const urls = [];
968
+ for (const match of css.matchAll(/url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/g)) if (match[1]) urls.push(match[1]);
969
+ return urls;
970
+ }
971
+ /**
972
+ * Self-host Google Fonts: download CSS + font files, rewrite URLs to local paths.
973
+ */
974
+ async function selfHostFonts(cssUrl, fontsSubDir) {
975
+ const css = await downloadGoogleFontsCSS(cssUrl);
976
+ const fontUrls = extractFontUrls(css);
977
+ const fontFiles = [];
978
+ let rewrittenCss = css;
979
+ for (const url of fontUrls) {
980
+ const fileName = url.split("/").at(-1)?.split("?")[0] ?? "font";
981
+ const content = await downloadFontFile(url);
982
+ fontFiles.push({
983
+ name: fileName,
984
+ content
985
+ });
986
+ rewrittenCss = rewrittenCss.replace(url, `/${fontsSubDir}/${fileName}`);
987
+ }
988
+ return {
989
+ css: rewrittenCss,
990
+ fontFiles
991
+ };
992
+ }
993
+ /**
994
+ * Zero font optimization Vite plugin.
995
+ *
996
+ * Dev mode: injects Google Fonts CDN link for fast startup.
997
+ * Build mode: downloads and self-hosts fonts for maximum performance + privacy.
998
+ *
999
+ * @example
1000
+ * import { fontPlugin } from "@pyreon/zero/font"
1001
+ *
1002
+ * export default {
1003
+ * plugins: [
1004
+ * pyreon(),
1005
+ * zero(),
1006
+ * fontPlugin({
1007
+ * google: ["Inter:wght@400;500;600;700", "JetBrains Mono:wght@400"],
1008
+ * fallbacks: {
1009
+ * "Inter": { fallback: "Arial", sizeAdjust: 1.07, ascentOverride: 90 },
1010
+ * },
1011
+ * }),
1012
+ * ],
1013
+ * }
1014
+ */
1015
+ function fontPlugin(config = {}) {
1016
+ const display = config.display ?? "swap";
1017
+ const shouldPreload = config.preload !== false;
1018
+ const shouldSelfHost = config.selfHost !== false;
1019
+ const googleFamilies = (config.google ?? []).map(resolveGoogleFont);
1020
+ let isBuild = false;
1021
+ let selfHostedCSS = "";
1022
+ let selfHostedFontFiles = [];
1023
+ return {
1024
+ name: "pyreon-zero-fonts",
1025
+ configResolved(resolvedConfig) {
1026
+ isBuild = resolvedConfig.command === "build";
1027
+ },
1028
+ async buildStart() {
1029
+ if (isBuild && shouldSelfHost && googleFamilies.length > 0) {
1030
+ const cssUrl = googleFontsUrl(googleFamilies, display);
1031
+ try {
1032
+ const result = await selfHostFonts(cssUrl, "assets/fonts");
1033
+ selfHostedCSS = result.css;
1034
+ selfHostedFontFiles = result.fontFiles;
1035
+ } catch {}
1036
+ }
1037
+ },
1038
+ generateBundle() {
1039
+ for (const file of selfHostedFontFiles) this.emitFile({
1040
+ type: "asset",
1041
+ fileName: `assets/fonts/${file.name}`,
1042
+ source: file.content
1043
+ });
1044
+ },
1045
+ transformIndexHtml(html) {
1046
+ const tags = [];
1047
+ collectGoogleFontTags(tags, {
1048
+ isBuild,
1049
+ selfHostedCSS,
1050
+ selfHostedFontFiles,
1051
+ shouldPreload,
1052
+ googleFamilies,
1053
+ display
1054
+ });
1055
+ collectLocalFontTags(tags, config, shouldPreload, display);
1056
+ if (tags.length === 0) return html;
1057
+ return html.replace("</head>", `${tags.join("\n")}\n</head>`);
1058
+ }
1059
+ };
1060
+ }
1061
+ function collectGoogleFontTags(tags, opts) {
1062
+ if (opts.isBuild && opts.selfHostedCSS) {
1063
+ tags.push(`<style>${opts.selfHostedCSS}</style>`);
1064
+ if (opts.shouldPreload) for (const file of opts.selfHostedFontFiles.slice(0, opts.googleFamilies.length)) {
1065
+ const type = file.name.split(".").pop() === "woff2" ? "font/woff2" : "font/woff";
1066
+ tags.push(`<link rel="preload" href="/assets/fonts/${file.name}" as="font" type="${type}" crossorigin>`);
1067
+ }
1068
+ } else if (opts.googleFamilies.length > 0) {
1069
+ const cssUrl = googleFontsUrl(opts.googleFamilies, opts.display);
1070
+ tags.push(`<link rel="preconnect" href="https://fonts.googleapis.com">`);
1071
+ tags.push(`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`);
1072
+ tags.push(`<link rel="stylesheet" href="${cssUrl}">`);
1073
+ }
1074
+ }
1075
+ function collectLocalFontTags(tags, config, shouldPreload, display) {
1076
+ if (shouldPreload && config.local?.length) tags.push(preloadTags(config.local));
1077
+ if (config.local?.length) tags.push(`<style>${localFontFaces(config.local, display)}</style>`);
1078
+ if (config.fallbacks && Object.keys(config.fallbacks).length > 0) tags.push(`<style>${fallbackFontFaces(config.fallbacks)}</style>`);
1079
+ }
1080
+ /**
1081
+ * Generate CSS variables for font families.
1082
+ */
1083
+ function fontVariables(families) {
1084
+ return `:root {\n${Object.entries(families).map(([key, value]) => ` --font-${key}: ${value};`).join("\n")}\n}`;
1085
+ }
1086
+
1087
+ //#endregion
1088
+ //#region src/image-plugin.ts
1089
+ let sharpWarned = false;
1090
+ function warnSharpMissing() {
1091
+ if (sharpWarned) return;
1092
+ sharpWarned = true;
1093
+ console.warn("\n[zero:image] sharp not installed — images will not be optimized. Install for full support: bun add -D sharp\n");
1094
+ }
1095
+ const IMAGE_EXT_RE = /\.(jpe?g|png|webp|avif)$/i;
1096
+ /**
1097
+ * Zero image processing Vite plugin.
1098
+ *
1099
+ * Transforms image imports with query params into optimized responsive images:
1100
+ *
1101
+ * @example
1102
+ * // vite.config.ts
1103
+ * import { imagePlugin } from "@pyreon/zero/image-plugin"
1104
+ *
1105
+ * export default {
1106
+ * plugins: [
1107
+ * pyreon(),
1108
+ * zero(),
1109
+ * imagePlugin({ widths: [480, 960, 1440], quality: 85 }),
1110
+ * ],
1111
+ * }
1112
+ *
1113
+ * @example
1114
+ * // In a component — import with ?optimize
1115
+ * import hero from "./images/hero.jpg?optimize"
1116
+ * // hero = { src, srcset, width, height, placeholder }
1117
+ *
1118
+ * <Image {...hero} alt="Hero" priority />
1119
+ */
1120
+ function imagePlugin(config = {}) {
1121
+ const defaultWidths = config.widths ?? [
1122
+ 640,
1123
+ 1024,
1124
+ 1920
1125
+ ];
1126
+ const defaultFormats = config.formats ?? ["webp"];
1127
+ const quality = config.quality ?? 80;
1128
+ const placeholderSize = config.placeholderSize ?? 16;
1129
+ const outSubDir = config.outDir ?? "assets/img";
1130
+ const include = config.include ?? IMAGE_EXT_RE;
1131
+ let root = "";
1132
+ let outDir = "";
1133
+ let isBuild = false;
1134
+ return {
1135
+ name: "pyreon-zero-images",
1136
+ enforce: "pre",
1137
+ configResolved(resolvedConfig) {
1138
+ root = resolvedConfig.root;
1139
+ outDir = resolvedConfig.build.outDir;
1140
+ isBuild = resolvedConfig.command === "build";
1141
+ },
1142
+ async resolveId(id) {
1143
+ if (id.includes("?optimize") && include.test(id.split("?")[0])) return `\0virtual:zero-image:${id}`;
1144
+ return null;
1145
+ },
1146
+ async load(id) {
1147
+ if (!id.startsWith("\0virtual:zero-image:")) return null;
1148
+ const rawPath = id.replace("\0virtual:zero-image:", "").split("?")[0] ?? id;
1149
+ const absPath = rawPath.startsWith("/") ? join(root, "public", rawPath) : rawPath;
1150
+ if (!isBuild) {
1151
+ const result = await loadDevImage(absPath, rawPath, placeholderSize);
1152
+ return `export default ${JSON.stringify(result)}`;
1153
+ }
1154
+ const processed = await processImage(absPath, {
1155
+ widths: defaultWidths,
1156
+ formats: defaultFormats,
1157
+ quality,
1158
+ placeholderSize,
1159
+ outSubDir,
1160
+ outDir: join(root, outDir)
1161
+ });
1162
+ await emitProcessedSources(processed, outSubDir, this);
1163
+ rebuildFormatSrcsets(processed, absPath);
1164
+ return `export default ${JSON.stringify(processed)}`;
1165
+ }
1166
+ };
1167
+ }
1168
+ async function loadDevImage(absPath, rawPath, placeholderSize) {
1169
+ const metadata = await getImageMetadata(absPath);
1170
+ const publicPath = rawPath.startsWith("/") ? rawPath : `/@fs/${absPath}`;
1171
+ return {
1172
+ src: publicPath,
1173
+ srcset: "",
1174
+ width: metadata.width,
1175
+ height: metadata.height,
1176
+ placeholder: await generateBlurPlaceholder(absPath, placeholderSize),
1177
+ formats: [],
1178
+ sources: [{
1179
+ src: publicPath,
1180
+ width: metadata.width,
1181
+ format: "original"
1182
+ }]
1183
+ };
1184
+ }
1185
+ async function emitProcessedSources(processed, outSubDir, ctx) {
1186
+ for (const source of processed.sources) {
1187
+ const fileName = join(outSubDir, basename(source.src));
1188
+ const content = await readFile(source.src);
1189
+ ctx.emitFile({
1190
+ type: "asset",
1191
+ fileName,
1192
+ source: content
1193
+ });
1194
+ source.src = `/${fileName}`;
1195
+ }
1196
+ }
1197
+ function rebuildFormatSrcsets(processed, fallbackPath) {
1198
+ const formatGroups = /* @__PURE__ */ new Map();
1199
+ for (const s of processed.sources) {
1200
+ let group = formatGroups.get(s.format);
1201
+ if (!group) {
1202
+ group = [];
1203
+ formatGroups.set(s.format, group);
1204
+ }
1205
+ group.push(`${s.src} ${s.width}w`);
1206
+ }
1207
+ processed.formats = [...formatGroups.entries()].map(([fmt, entries]) => ({
1208
+ type: `image/${fmt}`,
1209
+ srcset: entries.join(", ")
1210
+ }));
1211
+ processed.srcset = processed.formats.at(-1)?.srcset ?? "";
1212
+ processed.src = processed.sources.at(-1)?.src ?? fallbackPath;
1213
+ }
1214
+ async function processImage(absPath, opts) {
1215
+ const metadata = await getImageMetadata(absPath);
1216
+ const name = basename(absPath, extname(absPath));
1217
+ const sources = [];
1218
+ const processedDir = join(opts.outDir, opts.outSubDir);
1219
+ if (!existsSync(processedDir)) await mkdir(processedDir, { recursive: true });
1220
+ for (const format of opts.formats) for (const targetWidth of opts.widths) {
1221
+ const width = Math.min(targetWidth, metadata.width);
1222
+ const outPath = join(processedDir, `${name}-${width}.${format}`);
1223
+ await resizeImage(absPath, outPath, width, format, opts.quality);
1224
+ sources.push({
1225
+ src: outPath,
1226
+ width,
1227
+ format
1228
+ });
1229
+ }
1230
+ const formatGroups = /* @__PURE__ */ new Map();
1231
+ for (const s of sources) {
1232
+ let group = formatGroups.get(s.format);
1233
+ if (!group) {
1234
+ group = [];
1235
+ formatGroups.set(s.format, group);
1236
+ }
1237
+ group.push({
1238
+ src: s.src,
1239
+ width: s.width
1240
+ });
1241
+ }
1242
+ const formats = [...formatGroups.entries()].map(([fmt, group]) => ({
1243
+ type: `image/${fmt === "jpeg" ? "jpeg" : fmt}`,
1244
+ srcset: group.map((s) => `${s.src} ${s.width}w`).join(", ")
1245
+ }));
1246
+ const fallbackFormat = formats[formats.length - 1];
1247
+ const fallbackSources = formatGroups.get([...formatGroups.keys()].pop());
1248
+ const placeholder = await generateBlurPlaceholder(absPath, opts.placeholderSize);
1249
+ return {
1250
+ src: fallbackSources[fallbackSources.length - 1]?.src ?? absPath,
1251
+ srcset: fallbackFormat?.srcset ?? "",
1252
+ width: metadata.width,
1253
+ height: metadata.height,
1254
+ placeholder,
1255
+ formats,
1256
+ sources
1257
+ };
1258
+ }
1259
+ /**
1260
+ * Read basic image metadata.
1261
+ * Uses minimal binary header parsing — no external dependencies.
1262
+ */
1263
+ async function getImageMetadata(absPath) {
1264
+ const buffer = await readFile(absPath);
1265
+ const ext = extname(absPath).toLowerCase();
1266
+ if (ext === ".png") return {
1267
+ width: buffer.readUInt32BE(16),
1268
+ height: buffer.readUInt32BE(20),
1269
+ format: "png"
1270
+ };
1271
+ if (ext === ".jpg" || ext === ".jpeg") return {
1272
+ ...parseJpegDimensions(buffer),
1273
+ format: "jpeg"
1274
+ };
1275
+ if (ext === ".webp") return {
1276
+ ...parseWebPDimensions(buffer),
1277
+ format: "webp"
1278
+ };
1279
+ return {
1280
+ width: 0,
1281
+ height: 0,
1282
+ format: ext.slice(1)
1283
+ };
1284
+ }
1285
+ /** @internal Exported for testing */
1286
+ function parseJpegDimensions(buffer) {
1287
+ let offset = 2;
1288
+ while (offset < buffer.length) {
1289
+ if (buffer[offset] !== 255) break;
1290
+ const marker = buffer[offset + 1];
1291
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
1292
+ const height = buffer.readUInt16BE(offset + 5);
1293
+ return {
1294
+ width: buffer.readUInt16BE(offset + 7),
1295
+ height
1296
+ };
1297
+ }
1298
+ const length = buffer.readUInt16BE(offset + 2);
1299
+ offset += 2 + length;
1300
+ }
1301
+ return {
1302
+ width: 0,
1303
+ height: 0
1304
+ };
1305
+ }
1306
+ /** @internal Exported for testing */
1307
+ function parseWebPDimensions(buffer) {
1308
+ const chunk = buffer.toString("ascii", 12, 16);
1309
+ if (chunk === "VP8 ") return {
1310
+ width: buffer.readUInt16LE(26) & 16383,
1311
+ height: buffer.readUInt16LE(28) & 16383
1312
+ };
1313
+ if (chunk === "VP8L") {
1314
+ const bits = buffer.readUInt32LE(21);
1315
+ return {
1316
+ width: (bits & 16383) + 1,
1317
+ height: (bits >> 14 & 16383) + 1
1318
+ };
1319
+ }
1320
+ if (chunk === "VP8X") return {
1321
+ width: 1 + ((buffer[24] | buffer[25] << 8 | buffer[26] << 16) & 16777215),
1322
+ height: 1 + ((buffer[27] | buffer[28] << 8 | buffer[29] << 16) & 16777215)
1323
+ };
1324
+ return {
1325
+ width: 0,
1326
+ height: 0
1327
+ };
1328
+ }
1329
+ /**
1330
+ * Resize an image using native platform capabilities.
1331
+ * Uses sharp if available, falls back to canvas API.
1332
+ */
1333
+ async function resizeImage(input, output, width, format, quality) {
1334
+ try {
1335
+ let pipeline = (await import("sharp").then((m) => m.default ?? m))(input).resize(width);
1336
+ switch (format) {
1337
+ case "webp":
1338
+ pipeline = pipeline.webp({ quality });
1339
+ break;
1340
+ case "avif":
1341
+ pipeline = pipeline.avif({ quality });
1342
+ break;
1343
+ case "jpeg":
1344
+ pipeline = pipeline.jpeg({
1345
+ quality,
1346
+ mozjpeg: true
1347
+ });
1348
+ break;
1349
+ case "png":
1350
+ pipeline = pipeline.png({ compressionLevel: 9 });
1351
+ break;
1352
+ }
1353
+ await pipeline.toFile(output);
1354
+ } catch {
1355
+ warnSharpMissing();
1356
+ await writeFile(output, await readFile(input));
1357
+ }
1358
+ }
1359
+ /**
1360
+ * Generate a tiny blur placeholder as a base64 data URI.
1361
+ */
1362
+ async function generateBlurPlaceholder(input, size) {
1363
+ try {
1364
+ return `data:image/webp;base64,${(await (await import("sharp").then((m) => m.default ?? m))(input).resize(size, size, { fit: "inside" }).blur(2).webp({ quality: 20 }).toBuffer()).toString("base64")}`;
1365
+ } catch {
1366
+ return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E";
1367
+ }
1368
+ }
1369
+
1370
+ //#endregion
1371
+ //#region src/theme.tsx
1372
+ const STORAGE_KEY = "zero-theme";
1373
+ /** Reactive theme signal. */
1374
+ const theme = signal("system");
1375
+ /** Computed resolved theme (what's actually applied). */
1376
+ function resolvedTheme() {
1377
+ const t = theme();
1378
+ if (t === "system") {
1379
+ if (typeof window === "undefined") return "dark";
1380
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1381
+ }
1382
+ return t;
1383
+ }
1384
+ /** Toggle between light and dark. */
1385
+ function toggleTheme() {
1386
+ setTheme(resolvedTheme() === "dark" ? "light" : "dark");
1387
+ }
1388
+ /** Set theme explicitly. */
1389
+ function setTheme(t) {
1390
+ theme.set(t);
1391
+ if (typeof document !== "undefined") {
1392
+ document.documentElement.dataset.theme = resolvedTheme();
1393
+ try {
1394
+ localStorage.setItem(STORAGE_KEY, t);
1395
+ } catch {}
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Initialize the theme system. Call once in your app entry or layout.
1400
+ * Reads from localStorage, listens for system preference changes.
1401
+ */
1402
+ function initTheme() {
1403
+ onMount(() => {
1404
+ try {
1405
+ const stored = localStorage.getItem(STORAGE_KEY);
1406
+ if (stored === "light" || stored === "dark" || stored === "system") theme.set(stored);
1407
+ } catch {}
1408
+ document.documentElement.dataset.theme = resolvedTheme();
1409
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
1410
+ function onChange() {
1411
+ if (theme() === "system") document.documentElement.dataset.theme = resolvedTheme();
1412
+ }
1413
+ mq.addEventListener("change", onChange);
1414
+ onUnmount(() => mq.removeEventListener("change", onChange));
1415
+ const dispose = effect(() => {
1416
+ document.documentElement.dataset.theme = resolvedTheme();
1417
+ });
1418
+ if (dispose) onUnmount(() => dispose.dispose());
1419
+ });
1420
+ }
1421
+ /**
1422
+ * Theme toggle button component.
1423
+ *
1424
+ * @example
1425
+ * import { ThemeToggle } from "@pyreon/zero/theme"
1426
+ * <ThemeToggle />
1427
+ */
1428
+ function ThemeToggle(props) {
1429
+ initTheme();
1430
+ return /* @__PURE__ */ jsx("button", {
1431
+ class: props.class,
1432
+ style: props.style,
1433
+ onclick: toggleTheme,
1434
+ "aria-label": "Toggle theme",
1435
+ title: "Toggle theme",
1436
+ type: "button",
1437
+ children: () => resolvedTheme() === "dark" ? /* @__PURE__ */ jsxs("svg", {
1438
+ width: "18",
1439
+ height: "18",
1440
+ viewBox: "0 0 24 24",
1441
+ fill: "none",
1442
+ stroke: "currentColor",
1443
+ "stroke-width": "2",
1444
+ "stroke-linecap": "round",
1445
+ "stroke-linejoin": "round",
1446
+ "aria-hidden": "true",
1447
+ children: [
1448
+ /* @__PURE__ */ jsx("circle", {
1449
+ cx: "12",
1450
+ cy: "12",
1451
+ r: "5"
1452
+ }),
1453
+ /* @__PURE__ */ jsx("line", {
1454
+ x1: "12",
1455
+ y1: "1",
1456
+ x2: "12",
1457
+ y2: "3"
1458
+ }),
1459
+ /* @__PURE__ */ jsx("line", {
1460
+ x1: "12",
1461
+ y1: "21",
1462
+ x2: "12",
1463
+ y2: "23"
1464
+ }),
1465
+ /* @__PURE__ */ jsx("line", {
1466
+ x1: "4.22",
1467
+ y1: "4.22",
1468
+ x2: "5.64",
1469
+ y2: "5.64"
1470
+ }),
1471
+ /* @__PURE__ */ jsx("line", {
1472
+ x1: "18.36",
1473
+ y1: "18.36",
1474
+ x2: "19.78",
1475
+ y2: "19.78"
1476
+ }),
1477
+ /* @__PURE__ */ jsx("line", {
1478
+ x1: "1",
1479
+ y1: "12",
1480
+ x2: "3",
1481
+ y2: "12"
1482
+ }),
1483
+ /* @__PURE__ */ jsx("line", {
1484
+ x1: "21",
1485
+ y1: "12",
1486
+ x2: "23",
1487
+ y2: "12"
1488
+ }),
1489
+ /* @__PURE__ */ jsx("line", {
1490
+ x1: "4.22",
1491
+ y1: "19.78",
1492
+ x2: "5.64",
1493
+ y2: "18.36"
1494
+ }),
1495
+ /* @__PURE__ */ jsx("line", {
1496
+ x1: "18.36",
1497
+ y1: "5.64",
1498
+ x2: "19.78",
1499
+ y2: "4.22"
1500
+ })
1501
+ ]
1502
+ }) : /* @__PURE__ */ jsx("svg", {
1503
+ width: "18",
1504
+ height: "18",
1505
+ viewBox: "0 0 24 24",
1506
+ fill: "none",
1507
+ stroke: "currentColor",
1508
+ "stroke-width": "2",
1509
+ "stroke-linecap": "round",
1510
+ "stroke-linejoin": "round",
1511
+ "aria-hidden": "true",
1512
+ children: /* @__PURE__ */ jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })
1513
+ })
1514
+ });
1515
+ }
1516
+ /**
1517
+ * Inline script to prevent flash of wrong theme.
1518
+ * Include this in your index.html <head> BEFORE any stylesheets.
1519
+ *
1520
+ * @example
1521
+ * // index.html
1522
+ * <head>
1523
+ * <script>{themeScript}<\/script>
1524
+ * ...
1525
+ * </head>
1526
+ */
1527
+ const themeScript = `(function(){try{var t=localStorage.getItem("${STORAGE_KEY}");var r=t==="light"?"light":t==="dark"?"dark":window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.documentElement.dataset.theme=r}catch(e){}})()`;
1528
+
1529
+ //#endregion
1530
+ //#region src/seo.ts
1531
+ /**
1532
+ * Generate a sitemap.xml string from route file paths.
1533
+ */
1534
+ function generateSitemap(routeFiles, config) {
1535
+ const { origin, exclude = [], changefreq = "weekly", priority = .7 } = config;
1536
+ return `<?xml version="1.0" encoding="UTF-8"?>
1537
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1538
+ ${[...routeFiles.filter((f) => {
1539
+ const name = f.split("/").pop()?.replace(/\.\w+$/, "");
1540
+ return name !== "_layout" && name !== "_error" && name !== "_loading";
1541
+ }).map((f) => {
1542
+ let path = f.replace(/\.\w+$/, "").replace(/\/index$/, "/").replace(/^index$/, "/");
1543
+ if (path.includes("[")) return null;
1544
+ path = path.replace(/\([\w-]+\)\//g, "");
1545
+ if (!path.startsWith("/")) path = `/${path}`;
1546
+ return path;
1547
+ }).filter((p) => p !== null).filter((p) => !exclude.some((e) => p.startsWith(e))).map((p) => ({
1548
+ path: p,
1549
+ changefreq,
1550
+ priority
1551
+ })), ...config.additionalPaths ?? []].map((entry) => {
1552
+ return ` <url>
1553
+ <loc>${escapeXml(`${origin}${entry.path === "/" ? "" : entry.path}`)}</loc>
1554
+ <changefreq>${entry.changefreq ?? changefreq}</changefreq>
1555
+ <priority>${entry.priority ?? priority}</priority>${entry.lastmod ? `\n <lastmod>${entry.lastmod}</lastmod>` : ""}
1556
+ </url>`;
1557
+ }).join("\n")}
1558
+ </urlset>`;
1559
+ }
1560
+ function escapeXml(str) {
1561
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1562
+ }
1563
+ /**
1564
+ * Generate a robots.txt string.
1565
+ */
1566
+ function generateRobots(config = {}) {
1567
+ const { rules = [{
1568
+ userAgent: "*",
1569
+ allow: ["/"]
1570
+ }], sitemap, host } = config;
1571
+ const lines = [];
1572
+ for (const rule of rules) {
1573
+ lines.push(`User-agent: ${rule.userAgent}`);
1574
+ if (rule.allow) for (const path of rule.allow) lines.push(`Allow: ${path}`);
1575
+ if (rule.disallow) for (const path of rule.disallow) lines.push(`Disallow: ${path}`);
1576
+ if (rule.crawlDelay) lines.push(`Crawl-delay: ${rule.crawlDelay}`);
1577
+ lines.push("");
1578
+ }
1579
+ if (sitemap) lines.push(`Sitemap: ${sitemap}`);
1580
+ if (host) lines.push(`Host: ${host}`);
1581
+ return lines.join("\n");
1582
+ }
1583
+ /**
1584
+ * Generate a JSON-LD script tag string for structured data.
1585
+ *
1586
+ * @example
1587
+ * useHead({
1588
+ * script: [jsonLd({
1589
+ * "@type": "WebSite",
1590
+ * name: "My Site",
1591
+ * url: "https://example.com",
1592
+ * })],
1593
+ * })
1594
+ */
1595
+ function jsonLd(data) {
1596
+ const ld = {
1597
+ "@context": "https://schema.org",
1598
+ ...data
1599
+ };
1600
+ return `<script type="application/ld+json">${JSON.stringify(ld)}<\/script>`;
1601
+ }
1602
+ /**
1603
+ * Zero SEO Vite plugin.
1604
+ * Generates sitemap.xml and robots.txt at build time.
1605
+ *
1606
+ * @example
1607
+ * import { seoPlugin } from "@pyreon/zero/seo"
1608
+ *
1609
+ * export default {
1610
+ * plugins: [
1611
+ * pyreon(),
1612
+ * zero(),
1613
+ * seoPlugin({
1614
+ * sitemap: { origin: "https://example.com" },
1615
+ * robots: { sitemap: "https://example.com/sitemap.xml" },
1616
+ * }),
1617
+ * ],
1618
+ * }
1619
+ */
1620
+ function seoPlugin(config = {}) {
1621
+ return {
1622
+ name: "pyreon-zero-seo",
1623
+ apply: "build",
1624
+ async generateBundle(_, _bundle) {
1625
+ if (config.sitemap) {
1626
+ const { scanRouteFiles } = await import("./fs-router-jfd1QGLB.js").then((n) => n.n);
1627
+ const routesDir = `${process.cwd()}/src/routes`;
1628
+ try {
1629
+ const sitemap = generateSitemap(await scanRouteFiles(routesDir), config.sitemap);
1630
+ this.emitFile({
1631
+ type: "asset",
1632
+ fileName: "sitemap.xml",
1633
+ source: sitemap
1634
+ });
1635
+ } catch {}
1636
+ }
1637
+ if (config.robots) {
1638
+ const robots = generateRobots(config.robots);
1639
+ this.emitFile({
1640
+ type: "asset",
1641
+ fileName: "robots.txt",
1642
+ source: robots
1643
+ });
1644
+ }
1645
+ }
1646
+ };
1647
+ }
1648
+ /**
1649
+ * SEO middleware for dev server.
1650
+ * Serves sitemap.xml and robots.txt dynamically during development.
1651
+ */
1652
+ function seoMiddleware(config = {}) {
1653
+ return async (ctx) => {
1654
+ if (ctx.url.pathname === "/robots.txt" && config.robots) return new Response(generateRobots(config.robots), { headers: { "Content-Type": "text/plain" } });
1655
+ if (ctx.url.pathname === "/sitemap.xml" && config.sitemap) try {
1656
+ const { scanRouteFiles } = await import("./fs-router-jfd1QGLB.js").then((n) => n.n);
1657
+ const sitemap = generateSitemap(await scanRouteFiles(`${process.cwd()}/src/routes`), config.sitemap);
1658
+ return new Response(sitemap, { headers: { "Content-Type": "application/xml" } });
1659
+ } catch {}
1660
+ };
1661
+ }
1662
+
1663
+ //#endregion
1664
+ export { Image, Link, Script, ThemeToggle, bunAdapter, cacheMiddleware, createApp, createISRHandler, createLink, createServer, zeroPlugin as default, defineConfig, filePathToUrlPath, fontPlugin, fontVariables, generateRobots, generateRouteModule, generateSitemap, imagePlugin, initTheme, jsonLd, nodeAdapter, parseFileRoutes, resolveAdapter, resolveConfig, resolvedTheme, scanRouteFiles, securityHeaders, seoMiddleware, seoPlugin, setTheme, staticAdapter, theme, themeScript, toggleTheme, useLink, varyEncoding };
1665
+ //# sourceMappingURL=index.js.map