@pyreon/zero 0.12.2 → 0.12.4

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 (145) hide show
  1. package/lib/actions.js +97 -0
  2. package/lib/actions.js.map +1 -0
  3. package/lib/ai.js +503 -0
  4. package/lib/ai.js.map +1 -0
  5. package/lib/api-routes.js +137 -0
  6. package/lib/api-routes.js.map +1 -0
  7. package/lib/compression.js +80 -0
  8. package/lib/compression.js.map +1 -0
  9. package/lib/cors.js +57 -0
  10. package/lib/cors.js.map +1 -0
  11. package/lib/csp.js +119 -0
  12. package/lib/csp.js.map +1 -0
  13. package/lib/env.js +217 -0
  14. package/lib/env.js.map +1 -0
  15. package/lib/favicon.js +424 -0
  16. package/lib/favicon.js.map +1 -0
  17. package/lib/i18n-routing.js +167 -0
  18. package/lib/i18n-routing.js.map +1 -0
  19. package/lib/index.js +340 -4015
  20. package/lib/index.js.map +1 -1
  21. package/lib/link.js +5 -0
  22. package/lib/link.js.map +1 -1
  23. package/lib/logger.js +78 -0
  24. package/lib/logger.js.map +1 -0
  25. package/lib/meta.js +310 -0
  26. package/lib/meta.js.map +1 -0
  27. package/lib/middleware.js +53 -0
  28. package/lib/middleware.js.map +1 -0
  29. package/lib/og-image.js +233 -0
  30. package/lib/og-image.js.map +1 -0
  31. package/lib/rate-limit.js +76 -0
  32. package/lib/rate-limit.js.map +1 -0
  33. package/lib/server.js +1534 -0
  34. package/lib/server.js.map +1 -0
  35. package/lib/testing.js +179 -0
  36. package/lib/testing.js.map +1 -0
  37. package/lib/theme.js +11 -2
  38. package/lib/theme.js.map +1 -1
  39. package/lib/types/actions.d.ts +27 -24
  40. package/lib/types/actions.d.ts.map +1 -1
  41. package/lib/types/ai.d.ts +76 -95
  42. package/lib/types/ai.d.ts.map +1 -1
  43. package/lib/types/api-routes.d.ts +37 -33
  44. package/lib/types/api-routes.d.ts.map +1 -1
  45. package/lib/types/cache.d.ts +26 -22
  46. package/lib/types/cache.d.ts.map +1 -1
  47. package/lib/types/client.d.ts +13 -9
  48. package/lib/types/client.d.ts.map +1 -1
  49. package/lib/types/compression.d.ts +14 -10
  50. package/lib/types/compression.d.ts.map +1 -1
  51. package/lib/types/config.d.ts +39 -4
  52. package/lib/types/config.d.ts.map +1 -1
  53. package/lib/types/cors.d.ts +20 -16
  54. package/lib/types/cors.d.ts.map +1 -1
  55. package/lib/types/csp.d.ts +42 -61
  56. package/lib/types/csp.d.ts.map +1 -1
  57. package/lib/types/env.d.ts +26 -26
  58. package/lib/types/env.d.ts.map +1 -1
  59. package/lib/types/favicon.d.ts +58 -54
  60. package/lib/types/favicon.d.ts.map +1 -1
  61. package/lib/types/font.d.ts +68 -65
  62. package/lib/types/font.d.ts.map +1 -1
  63. package/lib/types/i18n-routing.d.ts +43 -37
  64. package/lib/types/i18n-routing.d.ts.map +1 -1
  65. package/lib/types/image-plugin.d.ts +49 -45
  66. package/lib/types/image-plugin.d.ts.map +1 -1
  67. package/lib/types/image.d.ts +47 -36
  68. package/lib/types/image.d.ts.map +1 -1
  69. package/lib/types/index.d.ts +594 -56
  70. package/lib/types/index.d.ts.map +1 -1
  71. package/lib/types/link.d.ts +61 -56
  72. package/lib/types/link.d.ts.map +1 -1
  73. package/lib/types/logger.d.ts +37 -48
  74. package/lib/types/logger.d.ts.map +1 -1
  75. package/lib/types/meta.d.ts +145 -105
  76. package/lib/types/meta.d.ts.map +1 -1
  77. package/lib/types/middleware.d.ts +8 -4
  78. package/lib/types/middleware.d.ts.map +1 -1
  79. package/lib/types/og-image.d.ts +63 -59
  80. package/lib/types/og-image.d.ts.map +1 -1
  81. package/lib/types/rate-limit.d.ts +20 -16
  82. package/lib/types/rate-limit.d.ts.map +1 -1
  83. package/lib/types/script.d.ts +23 -19
  84. package/lib/types/script.d.ts.map +1 -1
  85. package/lib/types/seo.d.ts +47 -43
  86. package/lib/types/seo.d.ts.map +1 -1
  87. package/lib/types/server.d.ts +455 -0
  88. package/lib/types/server.d.ts.map +1 -0
  89. package/lib/types/testing.d.ts +64 -27
  90. package/lib/types/testing.d.ts.map +1 -1
  91. package/lib/types/theme.d.ts +22 -12
  92. package/lib/types/theme.d.ts.map +1 -1
  93. package/package.json +17 -12
  94. package/src/actions.ts +1 -3
  95. package/src/adapters/bun.ts +2 -0
  96. package/src/adapters/cloudflare.ts +2 -0
  97. package/src/adapters/netlify.ts +2 -0
  98. package/src/adapters/node.ts +2 -0
  99. package/src/adapters/validate.ts +16 -0
  100. package/src/adapters/vercel.ts +2 -0
  101. package/src/compression.ts +19 -3
  102. package/src/entry-server.ts +28 -5
  103. package/src/index.ts +20 -182
  104. package/src/link.tsx +6 -0
  105. package/src/meta.tsx +78 -16
  106. package/src/rate-limit.ts +11 -9
  107. package/src/server.ts +70 -0
  108. package/src/theme.tsx +12 -1
  109. package/src/vite-plugin.ts +5 -1
  110. package/lib/fs-router-Dil4IKZR.js +0 -290
  111. package/lib/fs-router-Dil4IKZR.js.map +0 -1
  112. package/lib/types/adapters/bun.d.ts +0 -6
  113. package/lib/types/adapters/bun.d.ts.map +0 -1
  114. package/lib/types/adapters/cloudflare.d.ts +0 -26
  115. package/lib/types/adapters/cloudflare.d.ts.map +0 -1
  116. package/lib/types/adapters/index.d.ts +0 -13
  117. package/lib/types/adapters/index.d.ts.map +0 -1
  118. package/lib/types/adapters/netlify.d.ts +0 -21
  119. package/lib/types/adapters/netlify.d.ts.map +0 -1
  120. package/lib/types/adapters/node.d.ts +0 -6
  121. package/lib/types/adapters/node.d.ts.map +0 -1
  122. package/lib/types/adapters/static.d.ts +0 -7
  123. package/lib/types/adapters/static.d.ts.map +0 -1
  124. package/lib/types/adapters/vercel.d.ts +0 -21
  125. package/lib/types/adapters/vercel.d.ts.map +0 -1
  126. package/lib/types/app.d.ts +0 -24
  127. package/lib/types/app.d.ts.map +0 -1
  128. package/lib/types/entry-server.d.ts +0 -37
  129. package/lib/types/entry-server.d.ts.map +0 -1
  130. package/lib/types/error-overlay.d.ts +0 -6
  131. package/lib/types/error-overlay.d.ts.map +0 -1
  132. package/lib/types/fs-router.d.ts +0 -47
  133. package/lib/types/fs-router.d.ts.map +0 -1
  134. package/lib/types/isr.d.ts +0 -9
  135. package/lib/types/isr.d.ts.map +0 -1
  136. package/lib/types/not-found.d.ts +0 -7
  137. package/lib/types/not-found.d.ts.map +0 -1
  138. package/lib/types/types.d.ts +0 -111
  139. package/lib/types/types.d.ts.map +0 -1
  140. package/lib/types/utils/use-intersection-observer.d.ts +0 -10
  141. package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
  142. package/lib/types/utils/with-headers.d.ts +0 -6
  143. package/lib/types/utils/with-headers.d.ts.map +0 -1
  144. package/lib/types/vite-plugin.d.ts +0 -17
  145. package/lib/types/vite-plugin.d.ts.map +0 -1
@@ -0,0 +1,233 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ //#region src/og-image.ts
5
+ /**
6
+ * OG Image generation plugin.
7
+ *
8
+ * Generates Open Graph images at build time from templates with
9
+ * text overlays. Supports locale-specific text for i18n apps.
10
+ * Uses sharp for image processing (same optional dep as favicon/image plugins).
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // vite.config.ts
15
+ * import { ogImagePlugin } from "@pyreon/zero/og-image"
16
+ *
17
+ * export default {
18
+ * plugins: [
19
+ * ogImagePlugin({
20
+ * locales: ["en", "de", "cs"],
21
+ * templates: [{
22
+ * name: "default",
23
+ * background: "./src/assets/og-bg.jpg",
24
+ * layers: [{
25
+ * text: { en: "Build faster", de: "Schneller bauen", cs: "Stavte rychleji" },
26
+ * y: "40%",
27
+ * fontSize: 72,
28
+ * }],
29
+ * }],
30
+ * }),
31
+ * ],
32
+ * }
33
+ * ```
34
+ */
35
+ let sharpWarned = false;
36
+ function warnSharpMissing() {
37
+ if (sharpWarned) return;
38
+ sharpWarned = true;
39
+ console.warn("\n[zero:og-image] sharp not installed — OG images will not be generated. Install for full support: bun add -D sharp\n");
40
+ }
41
+ function resolvePosition(value, dimension, fallback = "50%") {
42
+ if (value === void 0) value = fallback;
43
+ if (typeof value === "number") return value;
44
+ if (value.endsWith("%")) return Math.round(Number.parseFloat(value) / 100 * dimension);
45
+ return Number.parseInt(value, 10) || 0;
46
+ }
47
+ function resolveLayerText(layer, locale) {
48
+ if (typeof layer.text === "string") return layer.text;
49
+ if (typeof layer.text === "function") return layer.text(locale);
50
+ return layer.text[locale] ?? layer.text[Object.keys(layer.text)[0] ?? ""] ?? "";
51
+ }
52
+ function escapeXml(str) {
53
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
54
+ }
55
+ /**
56
+ * Build an SVG overlay with text layers.
57
+ * @internal Exported for testing.
58
+ */
59
+ function buildTextOverlaySvg(layers, width, height, locale) {
60
+ return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">${layers.map((layer) => {
61
+ const text = resolveLayerText(layer, locale);
62
+ const x = resolvePosition(layer.x, width, "50%");
63
+ const y = resolvePosition(layer.y, height, "50%");
64
+ const fontSize = layer.fontSize ?? 64;
65
+ const fontFamily = layer.fontFamily ?? "sans-serif";
66
+ const fontWeight = layer.fontWeight ?? "bold";
67
+ const color = layer.color ?? "#ffffff";
68
+ const anchor = layer.textAnchor ?? "middle";
69
+ const maxWidth = layer.maxWidth ?? Math.round(width * .8);
70
+ const words = text.split(" ");
71
+ const lines = [];
72
+ let currentLine = "";
73
+ const estimateWidth = (s) => {
74
+ let width = 0;
75
+ for (let i = 0; i < s.length; i++) {
76
+ const code = s.charCodeAt(i);
77
+ if (code >= 12288 && code <= 40959) width += fontSize * 1;
78
+ else if (code <= 126 && "iljft!|:;.,'".includes(s[i])) width += fontSize * .35;
79
+ else width += fontSize * .55;
80
+ }
81
+ return width;
82
+ };
83
+ for (const word of words) {
84
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
85
+ if (estimateWidth(testLine) > maxWidth && currentLine) {
86
+ lines.push(currentLine);
87
+ currentLine = word;
88
+ } else currentLine = testLine;
89
+ }
90
+ if (currentLine) lines.push(currentLine);
91
+ const tspans = lines.map((line, i) => {
92
+ return `<tspan x="${x}" dy="${i === 0 ? "0" : `${fontSize * 1.2}`}">${escapeXml(line)}</tspan>`;
93
+ }).join("");
94
+ return `<text x="${x}" y="${y}" font-size="${fontSize}" font-family="${escapeXml(fontFamily)}" font-weight="${fontWeight}" fill="${color}" text-anchor="${anchor}" dominant-baseline="middle">${tspans}</text>`;
95
+ }).join("")}</svg>`;
96
+ }
97
+ /**
98
+ * Render an OG image from a template for a specific locale.
99
+ * @internal Exported for testing.
100
+ */
101
+ async function renderOgImage(template, locale, rootDir) {
102
+ try {
103
+ const sharp = await import("sharp").then((m) => m.default ?? m);
104
+ const width = template.width ?? 1200;
105
+ const height = template.height ?? 630;
106
+ let pipeline;
107
+ if (typeof template.background === "string") pipeline = sharp(join(rootDir, template.background)).resize(width, height, { fit: "cover" });
108
+ else pipeline = sharp({ create: {
109
+ width,
110
+ height,
111
+ channels: 4,
112
+ background: template.background.color
113
+ } });
114
+ if (template.layers && template.layers.length > 0) {
115
+ const svgOverlay = buildTextOverlaySvg(template.layers, width, height, locale);
116
+ pipeline = pipeline.composite([{
117
+ input: Buffer.from(svgOverlay),
118
+ top: 0,
119
+ left: 0
120
+ }]);
121
+ }
122
+ if (template.format === "jpeg") return await pipeline.jpeg({ quality: template.quality ?? 90 }).toBuffer();
123
+ return await pipeline.png().toBuffer();
124
+ } catch {
125
+ warnSharpMissing();
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Compute the OG image path for a template and locale.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * ogImagePath("default", "de") // → "/og/default-de.png"
135
+ * ogImagePath("default") // → "/og/default.png"
136
+ * ogImagePath("hero", "en", "images") // → "/images/hero-en.png"
137
+ * ```
138
+ */
139
+ function ogImagePath(templateName, locale, outDir = "og", format = "png") {
140
+ const ext = format === "jpeg" ? "jpg" : "png";
141
+ return `/${outDir}/${templateName}${locale ? `-${locale}` : ""}.${ext}`;
142
+ }
143
+ /**
144
+ * OG image generation Vite plugin.
145
+ *
146
+ * Generates Open Graph images at build time. In dev, generates on-demand.
147
+ * Requires `sharp` as an optional dependency.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * // vite.config.ts
152
+ * import { ogImagePlugin } from "@pyreon/zero/og-image"
153
+ *
154
+ * export default {
155
+ * plugins: [
156
+ * ogImagePlugin({
157
+ * locales: ["en", "de"],
158
+ * templates: [{
159
+ * name: "default",
160
+ * background: { color: "#0066ff" },
161
+ * layers: [{ text: { en: "Hello", de: "Hallo" }, fontSize: 72 }],
162
+ * }],
163
+ * }),
164
+ * ],
165
+ * }
166
+ * ```
167
+ */
168
+ function ogImagePlugin(config) {
169
+ const outDir = config.outDir ?? "og";
170
+ let root = "";
171
+ let isBuild = false;
172
+ return {
173
+ name: "pyreon-zero-og-image",
174
+ enforce: "pre",
175
+ configResolved(resolvedConfig) {
176
+ root = resolvedConfig.root;
177
+ isBuild = resolvedConfig.command === "build";
178
+ },
179
+ configureServer(server) {
180
+ const devCache = /* @__PURE__ */ new Map();
181
+ server.middlewares.use(async (req, res, next) => {
182
+ const url = req.url ?? "";
183
+ if (!url.startsWith(`/${outDir}/`)) return next();
184
+ const match = url.slice(outDir.length + 2).match(/^(.+?)(?:-([a-z]{2,5}))?\.(png|jpe?g)$/);
185
+ if (!match) return next();
186
+ const [, templateName, locale, ext] = match;
187
+ const template = config.templates.find((t) => t.name === templateName);
188
+ if (!template) return next();
189
+ const resolvedLocale = locale ?? config.locales?.[0] ?? "en";
190
+ const cacheKey = `${templateName}:${resolvedLocale}`;
191
+ let buffer = devCache.get(cacheKey);
192
+ if (!buffer) {
193
+ const result = await renderOgImage(template, resolvedLocale, root);
194
+ if (!result) return next();
195
+ buffer = result;
196
+ devCache.set(cacheKey, result);
197
+ }
198
+ const contentType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
199
+ res.setHeader("Content-Type", contentType);
200
+ res.setHeader("Cache-Control", "no-cache");
201
+ res.end(Buffer.from(buffer));
202
+ });
203
+ },
204
+ async generateBundle() {
205
+ if (!isBuild) return;
206
+ for (const template of config.templates) {
207
+ const locales = config.locales ?? [void 0];
208
+ const ext = (template.format ?? "png") === "jpeg" ? "jpg" : "png";
209
+ for (const locale of locales) {
210
+ if (typeof template.background === "string") {
211
+ const bgPath = join(root, template.background);
212
+ if (!existsSync(bgPath)) {
213
+ console.warn(`[zero:og-image] Background not found: ${bgPath}`);
214
+ continue;
215
+ }
216
+ }
217
+ const buffer = await renderOgImage(template, locale ?? "en", root);
218
+ if (!buffer) continue;
219
+ const suffix = locale ? `-${locale}` : "";
220
+ this.emitFile({
221
+ type: "asset",
222
+ fileName: `${outDir}/${template.name}${suffix}.${ext}`,
223
+ source: buffer
224
+ });
225
+ }
226
+ }
227
+ }
228
+ };
229
+ }
230
+
231
+ //#endregion
232
+ export { buildTextOverlaySvg, ogImagePath, ogImagePlugin, renderOgImage };
233
+ //# sourceMappingURL=og-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-image.js","names":[],"sources":["../src/og-image.ts"],"sourcesContent":["/**\n * OG Image generation plugin.\n *\n * Generates Open Graph images at build time from templates with\n * text overlays. Supports locale-specific text for i18n apps.\n * Uses sharp for image processing (same optional dep as favicon/image plugins).\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { ogImagePlugin } from \"@pyreon/zero/og-image\"\n *\n * export default {\n * plugins: [\n * ogImagePlugin({\n * locales: [\"en\", \"de\", \"cs\"],\n * templates: [{\n * name: \"default\",\n * background: \"./src/assets/og-bg.jpg\",\n * layers: [{\n * text: { en: \"Build faster\", de: \"Schneller bauen\", cs: \"Stavte rychleji\" },\n * y: \"40%\",\n * fontSize: 72,\n * }],\n * }],\n * }),\n * ],\n * }\n * ```\n */\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[zero:og-image] sharp not installed — OG images will not be generated. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface OgImageLayer {\n /**\n * Text content. Can be:\n * - A string (same for all locales)\n * - A record mapping locale → text\n * - A function receiving locale and returning text\n */\n text: string | Record<string, string> | ((locale: string) => string)\n /** X position — number (px) or string with % (e.g. \"50%\"). Default: \"50%\" */\n x?: number | string\n /** Y position — number (px) or string with % (e.g. \"40%\"). Default: \"50%\" */\n y?: number | string\n /** Font size in px. Default: 64 */\n fontSize?: number\n /** Font family. Default: \"sans-serif\" */\n fontFamily?: string\n /** Font weight. Default: \"bold\" */\n fontWeight?: string\n /** Text color. Default: \"#ffffff\" */\n color?: string\n /** Text anchor (alignment). Default: \"middle\" */\n textAnchor?: 'start' | 'middle' | 'end'\n /** Max width in px before wrapping. Default: 80% of image width. */\n maxWidth?: number\n}\n\nexport interface OgImageTemplate {\n /** Template name — used for output file naming. */\n name: string\n /**\n * Background: path to an image file, or a solid color config.\n *\n * @example \"./src/assets/og-bg.jpg\"\n * @example { color: \"#0066ff\", width: 1200, height: 630 }\n */\n background: string | { color: string; width?: number; height?: number }\n /** Output width. Default: 1200 */\n width?: number\n /** Output height. Default: 630 */\n height?: number\n /** Output format. Default: \"png\" */\n format?: 'png' | 'jpeg'\n /** JPEG quality (1-100). Default: 90 */\n quality?: number\n /** Text layers to overlay on the background. */\n layers?: OgImageLayer[]\n}\n\nexport interface OgImagePluginConfig {\n /** Templates to generate. */\n templates: OgImageTemplate[]\n /** Locales to generate for. When omitted, generates a single image per template. */\n locales?: string[]\n /** Output directory prefix. Default: \"og\" */\n outDir?: string\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction resolvePosition(value: number | string | undefined, dimension: number, fallback = '50%'): number {\n if (value === undefined) value = fallback\n if (typeof value === 'number') return value\n if (value.endsWith('%')) return Math.round((Number.parseFloat(value) / 100) * dimension)\n return Number.parseInt(value, 10) || 0\n}\n\nfunction resolveLayerText(layer: OgImageLayer, locale: string): string {\n if (typeof layer.text === 'string') return layer.text\n if (typeof layer.text === 'function') return layer.text(locale)\n return layer.text[locale] ?? layer.text[Object.keys(layer.text)[0] ?? ''] ?? ''\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;')\n}\n\n/**\n * Build an SVG overlay with text layers.\n * @internal Exported for testing.\n */\nexport function buildTextOverlaySvg(\n layers: OgImageLayer[],\n width: number,\n height: number,\n locale: string,\n): string {\n const textElements = layers.map((layer) => {\n const text = resolveLayerText(layer, locale)\n const x = resolvePosition(layer.x, width, '50%')\n const y = resolvePosition(layer.y, height, '50%')\n const fontSize = layer.fontSize ?? 64\n const fontFamily = layer.fontFamily ?? 'sans-serif'\n const fontWeight = layer.fontWeight ?? 'bold'\n const color = layer.color ?? '#ffffff'\n const anchor = layer.textAnchor ?? 'middle'\n const maxWidth = layer.maxWidth ?? Math.round(width * 0.8)\n\n // Word wrapping via tspan elements.\n // Width estimation: Latin chars ~0.55em, CJK chars ~1.0em, narrow chars ~0.35em.\n const words = text.split(' ')\n const lines: string[] = []\n let currentLine = ''\n\n const estimateWidth = (s: string): number => {\n let width = 0\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i)\n if (code >= 0x3000 && code <= 0x9FFF) {\n // CJK characters — full width\n width += fontSize * 1.0\n } else if (code <= 0x7E && 'iljft!|:;.,\\''.includes(s[i]!)) {\n // Narrow Latin characters\n width += fontSize * 0.35\n } else {\n // Regular Latin characters\n width += fontSize * 0.55\n }\n }\n return width\n }\n\n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word\n if (estimateWidth(testLine) > maxWidth && currentLine) {\n lines.push(currentLine)\n currentLine = word\n } else {\n currentLine = testLine\n }\n }\n if (currentLine) lines.push(currentLine)\n\n const tspans = lines\n .map((line, i) => {\n const dy = i === 0 ? '0' : `${fontSize * 1.2}`\n return `<tspan x=\"${x}\" dy=\"${dy}\">${escapeXml(line)}</tspan>`\n })\n .join('')\n\n return `<text x=\"${x}\" y=\"${y}\" font-size=\"${fontSize}\" font-family=\"${escapeXml(fontFamily)}\" font-weight=\"${fontWeight}\" fill=\"${color}\" text-anchor=\"${anchor}\" dominant-baseline=\"middle\">${tspans}</text>`\n })\n\n return `<svg width=\"${width}\" height=\"${height}\" xmlns=\"http://www.w3.org/2000/svg\">${textElements.join('')}</svg>`\n}\n\n/**\n * Render an OG image from a template for a specific locale.\n * @internal Exported for testing.\n */\nexport async function renderOgImage(\n template: OgImageTemplate,\n locale: string,\n rootDir: string,\n): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const width = template.width ?? 1200\n const height = template.height ?? 630\n\n let pipeline: any\n if (typeof template.background === 'string') {\n const bgPath = join(rootDir, template.background)\n pipeline = sharp(bgPath).resize(width, height, { fit: 'cover' })\n } else {\n pipeline = (sharp as any)({\n create: {\n width,\n height,\n channels: 4,\n background: template.background.color,\n },\n })\n }\n\n // Overlay text layers if any\n if (template.layers && template.layers.length > 0) {\n const svgOverlay = buildTextOverlaySvg(template.layers, width, height, locale)\n pipeline = pipeline.composite([{\n input: Buffer.from(svgOverlay),\n top: 0,\n left: 0,\n }])\n }\n\n if (template.format === 'jpeg') {\n return await pipeline.jpeg({ quality: template.quality ?? 90 }).toBuffer()\n }\n return await pipeline.png().toBuffer()\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\n// ─── Path utility ───────────────────────────────────────────────────────────\n\n/**\n * Compute the OG image path for a template and locale.\n *\n * @example\n * ```ts\n * ogImagePath(\"default\", \"de\") // → \"/og/default-de.png\"\n * ogImagePath(\"default\") // → \"/og/default.png\"\n * ogImagePath(\"hero\", \"en\", \"images\") // → \"/images/hero-en.png\"\n * ```\n */\nexport function ogImagePath(\n templateName: string,\n locale?: string,\n outDir = 'og',\n format: 'png' | 'jpeg' = 'png',\n): string {\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n const suffix = locale ? `-${locale}` : ''\n return `/${outDir}/${templateName}${suffix}.${ext}`\n}\n\n// ─── Vite plugin ────────────────────────────────────────────────────────────\n\n/**\n * OG image generation Vite plugin.\n *\n * Generates Open Graph images at build time. In dev, generates on-demand.\n * Requires `sharp` as an optional dependency.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { ogImagePlugin } from \"@pyreon/zero/og-image\"\n *\n * export default {\n * plugins: [\n * ogImagePlugin({\n * locales: [\"en\", \"de\"],\n * templates: [{\n * name: \"default\",\n * background: { color: \"#0066ff\" },\n * layers: [{ text: { en: \"Hello\", de: \"Hallo\" }, fontSize: 72 }],\n * }],\n * }),\n * ],\n * }\n * ```\n */\nexport function ogImagePlugin(config: OgImagePluginConfig): Plugin {\n const outDir = config.outDir ?? 'og'\n let root = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-og-image',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n isBuild = resolvedConfig.command === 'build'\n },\n\n // Dev: generate on-demand\n configureServer(server) {\n const devCache = new Map<string, Uint8Array>()\n\n server.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n if (!url.startsWith(`/${outDir}/`)) return next()\n\n // Parse: /og/default-en.png → template=default, locale=en\n const fileName = url.slice(outDir.length + 2) // strip /{outDir}/\n const match = fileName.match(/^(.+?)(?:-([a-z]{2,5}))?\\.(png|jpe?g)$/)\n if (!match) return next()\n\n const [, templateName, locale, ext] = match\n const template = config.templates.find((t) => t.name === templateName)\n if (!template) return next()\n\n const resolvedLocale = locale ?? config.locales?.[0] ?? 'en'\n const cacheKey = `${templateName}:${resolvedLocale}`\n\n let buffer = devCache.get(cacheKey)\n if (!buffer) {\n const result = await renderOgImage(template, resolvedLocale, root)\n if (!result) return next()\n buffer = result\n devCache.set(cacheKey, result)\n }\n\n const contentType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png'\n res.setHeader('Content-Type', contentType)\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(buffer))\n })\n },\n\n // Build: generate all variants\n async generateBundle() {\n if (!isBuild) return\n\n for (const template of config.templates) {\n const locales = config.locales ?? [undefined]\n const format = template.format ?? 'png'\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n\n for (const locale of locales) {\n // Validate background exists if it's a file path\n if (typeof template.background === 'string') {\n const bgPath = join(root, template.background)\n if (!existsSync(bgPath)) {\n // oxlint-disable-next-line no-console\n console.warn(`[zero:og-image] Background not found: ${bgPath}`)\n continue\n }\n }\n\n const buffer = await renderOgImage(template, locale ?? 'en', root)\n if (!buffer) continue\n\n const suffix = locale ? `-${locale}` : ''\n this.emitFile({\n type: 'asset',\n fileName: `${outDir}/${template.name}${suffix}.${ext}`,\n source: buffer,\n })\n }\n }\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ //#region src/rate-limit.ts
2
+ /**
3
+ * Rate limiting middleware — limits requests per client within a time window.
4
+ * Uses an in-memory store (suitable for single-instance deployments).
5
+ *
6
+ * @example
7
+ * import { rateLimitMiddleware } from "@pyreon/zero/rate-limit"
8
+ *
9
+ * // 100 requests per minute (default)
10
+ * rateLimitMiddleware()
11
+ *
12
+ * // Strict API rate limiting
13
+ * rateLimitMiddleware({
14
+ * max: 20,
15
+ * window: 60,
16
+ * include: ["/api/*"],
17
+ * })
18
+ */
19
+ function rateLimitMiddleware(config = {}) {
20
+ const { max = 100, window: windowSec = 60, keyFn = defaultKeyFn, onLimit, include, exclude } = config;
21
+ const windowMs = windowSec * 1e3;
22
+ const store = /* @__PURE__ */ new Map();
23
+ const MAX_STORE_SIZE = 1e4;
24
+ let lastCleanup = Date.now();
25
+ function cleanupIfNeeded(now) {
26
+ if (store.size < MAX_STORE_SIZE / 2 && now - lastCleanup < windowMs) return;
27
+ lastCleanup = now;
28
+ for (const [key, entry] of store) if (entry.resetAt <= now) store.delete(key);
29
+ }
30
+ return (ctx) => {
31
+ if (include && !include.some((p) => matchSimpleGlob(p, ctx.path))) return;
32
+ if (exclude?.some((p) => matchSimpleGlob(p, ctx.path))) return;
33
+ const key = keyFn(ctx);
34
+ const now = Date.now();
35
+ cleanupIfNeeded(now);
36
+ let entry = store.get(key);
37
+ if (!entry || entry.resetAt <= now) {
38
+ entry = {
39
+ count: 0,
40
+ resetAt: now + windowMs
41
+ };
42
+ store.set(key, entry);
43
+ }
44
+ entry.count++;
45
+ const remaining = Math.max(0, max - entry.count);
46
+ const resetSeconds = Math.ceil((entry.resetAt - now) / 1e3);
47
+ ctx.headers.set("X-RateLimit-Limit", String(max));
48
+ ctx.headers.set("X-RateLimit-Remaining", String(remaining));
49
+ ctx.headers.set("X-RateLimit-Reset", String(resetSeconds));
50
+ if (entry.count > max) {
51
+ if (onLimit) return onLimit(ctx);
52
+ return new Response(JSON.stringify({ error: "Too many requests" }), {
53
+ status: 429,
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ "Retry-After": String(resetSeconds),
57
+ "X-RateLimit-Limit": String(max),
58
+ "X-RateLimit-Remaining": "0",
59
+ "X-RateLimit-Reset": String(resetSeconds)
60
+ }
61
+ });
62
+ }
63
+ };
64
+ }
65
+ function defaultKeyFn(ctx) {
66
+ return ctx.req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? ctx.req.headers.get("x-real-ip") ?? "unknown";
67
+ }
68
+ /** Simple glob matching for path patterns. Supports trailing `*`. */
69
+ function matchSimpleGlob(pattern, path) {
70
+ if (pattern.endsWith("/*")) return path.startsWith(pattern.slice(0, -1));
71
+ return pattern === path;
72
+ }
73
+
74
+ //#endregion
75
+ export { rateLimitMiddleware };
76
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","names":[],"sources":["../src/rate-limit.ts"],"sourcesContent":["import type { Middleware, MiddlewareContext } from '@pyreon/server'\n\n// ─── Rate limiting middleware ───────────────────────────────────────────────\n\nexport interface RateLimitConfig {\n /** Maximum requests per window. Default: `100` */\n max?: number\n /** Time window in seconds. Default: `60` */\n window?: number\n /** Function to extract the client identifier. Default: IP from headers. */\n keyFn?: (ctx: MiddlewareContext) => string\n /** Custom response when rate limited. */\n onLimit?: (ctx: MiddlewareContext) => Response\n /** URL patterns to rate limit (glob-style). Default: all paths. */\n include?: string[]\n /** URL patterns to exclude from rate limiting. */\n exclude?: string[]\n}\n\ninterface RateLimitEntry {\n count: number\n resetAt: number\n}\n\n/**\n * Rate limiting middleware — limits requests per client within a time window.\n * Uses an in-memory store (suitable for single-instance deployments).\n *\n * @example\n * import { rateLimitMiddleware } from \"@pyreon/zero/rate-limit\"\n *\n * // 100 requests per minute (default)\n * rateLimitMiddleware()\n *\n * // Strict API rate limiting\n * rateLimitMiddleware({\n * max: 20,\n * window: 60,\n * include: [\"/api/*\"],\n * })\n */\nexport function rateLimitMiddleware(config: RateLimitConfig = {}): Middleware {\n const {\n max = 100,\n window: windowSec = 60,\n keyFn = defaultKeyFn,\n onLimit,\n include,\n exclude,\n } = config\n\n const windowMs = windowSec * 1000\n const store = new Map<string, RateLimitEntry>()\n const MAX_STORE_SIZE = 10000\n let lastCleanup = Date.now()\n\n // Inline cleanup — runs during request processing, no setInterval needed.\n // Evicts expired entries when store exceeds half capacity or on window boundary.\n function cleanupIfNeeded(now: number) {\n if (store.size < MAX_STORE_SIZE / 2 && now - lastCleanup < windowMs) return\n lastCleanup = now\n for (const [key, entry] of store) {\n if (entry.resetAt <= now) store.delete(key)\n }\n }\n\n return (ctx: MiddlewareContext) => {\n // Check include/exclude patterns\n if (include && !include.some((p) => matchSimpleGlob(p, ctx.path))) return\n if (exclude?.some((p) => matchSimpleGlob(p, ctx.path))) return\n\n const key = keyFn(ctx)\n const now = Date.now()\n\n cleanupIfNeeded(now)\n\n let entry = store.get(key)\n\n if (!entry || entry.resetAt <= now) {\n entry = { count: 0, resetAt: now + windowMs }\n store.set(key, entry)\n }\n\n entry.count++\n const remaining = Math.max(0, max - entry.count)\n const resetSeconds = Math.ceil((entry.resetAt - now) / 1000)\n\n // Set rate limit headers on all responses\n ctx.headers.set('X-RateLimit-Limit', String(max))\n ctx.headers.set('X-RateLimit-Remaining', String(remaining))\n ctx.headers.set('X-RateLimit-Reset', String(resetSeconds))\n\n if (entry.count > max) {\n if (onLimit) return onLimit(ctx)\n\n return new Response(JSON.stringify({ error: 'Too many requests' }), {\n status: 429,\n headers: {\n 'Content-Type': 'application/json',\n 'Retry-After': String(resetSeconds),\n 'X-RateLimit-Limit': String(max),\n 'X-RateLimit-Remaining': '0',\n 'X-RateLimit-Reset': String(resetSeconds),\n },\n })\n }\n }\n}\n\nfunction defaultKeyFn(ctx: MiddlewareContext): string {\n return (\n ctx.req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n ctx.req.headers.get('x-real-ip') ??\n 'unknown'\n )\n}\n\n/** Simple glob matching for path patterns. Supports trailing `*`. */\nfunction matchSimpleGlob(pattern: string, path: string): boolean {\n if (pattern.endsWith('/*')) {\n return path.startsWith(pattern.slice(0, -1))\n }\n return pattern === path\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAyCA,SAAgB,oBAAoB,SAA0B,EAAE,EAAc;CAC5E,MAAM,EACJ,MAAM,KACN,QAAQ,YAAY,IACpB,QAAQ,cACR,SACA,SACA,YACE;CAEJ,MAAM,WAAW,YAAY;CAC7B,MAAM,wBAAQ,IAAI,KAA6B;CAC/C,MAAM,iBAAiB;CACvB,IAAI,cAAc,KAAK,KAAK;CAI5B,SAAS,gBAAgB,KAAa;AACpC,MAAI,MAAM,OAAO,iBAAiB,KAAK,MAAM,cAAc,SAAU;AACrE,gBAAc;AACd,OAAK,MAAM,CAAC,KAAK,UAAU,MACzB,KAAI,MAAM,WAAW,IAAK,OAAM,OAAO,IAAI;;AAI/C,SAAQ,QAA2B;AAEjC,MAAI,WAAW,CAAC,QAAQ,MAAM,MAAM,gBAAgB,GAAG,IAAI,KAAK,CAAC,CAAE;AACnE,MAAI,SAAS,MAAM,MAAM,gBAAgB,GAAG,IAAI,KAAK,CAAC,CAAE;EAExD,MAAM,MAAM,MAAM,IAAI;EACtB,MAAM,MAAM,KAAK,KAAK;AAEtB,kBAAgB,IAAI;EAEpB,IAAI,QAAQ,MAAM,IAAI,IAAI;AAE1B,MAAI,CAAC,SAAS,MAAM,WAAW,KAAK;AAClC,WAAQ;IAAE,OAAO;IAAG,SAAS,MAAM;IAAU;AAC7C,SAAM,IAAI,KAAK,MAAM;;AAGvB,QAAM;EACN,MAAM,YAAY,KAAK,IAAI,GAAG,MAAM,MAAM,MAAM;EAChD,MAAM,eAAe,KAAK,MAAM,MAAM,UAAU,OAAO,IAAK;AAG5D,MAAI,QAAQ,IAAI,qBAAqB,OAAO,IAAI,CAAC;AACjD,MAAI,QAAQ,IAAI,yBAAyB,OAAO,UAAU,CAAC;AAC3D,MAAI,QAAQ,IAAI,qBAAqB,OAAO,aAAa,CAAC;AAE1D,MAAI,MAAM,QAAQ,KAAK;AACrB,OAAI,QAAS,QAAO,QAAQ,IAAI;AAEhC,UAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,qBAAqB,CAAC,EAAE;IAClE,QAAQ;IACR,SAAS;KACP,gBAAgB;KAChB,eAAe,OAAO,aAAa;KACnC,qBAAqB,OAAO,IAAI;KAChC,yBAAyB;KACzB,qBAAqB,OAAO,aAAa;KAC1C;IACF,CAAC;;;;AAKR,SAAS,aAAa,KAAgC;AACpD,QACE,IAAI,IAAI,QAAQ,IAAI,kBAAkB,EAAE,MAAM,IAAI,CAAC,IAAI,MAAM,IAC7D,IAAI,IAAI,QAAQ,IAAI,YAAY,IAChC;;;AAKJ,SAAS,gBAAgB,SAAiB,MAAuB;AAC/D,KAAI,QAAQ,SAAS,KAAK,CACxB,QAAO,KAAK,WAAW,QAAQ,MAAM,GAAG,GAAG,CAAC;AAE9C,QAAO,YAAY"}