@pyreon/zero 0.12.1 → 0.12.2

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 (42) hide show
  1. package/lib/index.js +1476 -82
  2. package/lib/index.js.map +1 -1
  3. package/lib/types/adapters/cloudflare.d.ts +26 -0
  4. package/lib/types/adapters/cloudflare.d.ts.map +1 -0
  5. package/lib/types/adapters/index.d.ts +3 -0
  6. package/lib/types/adapters/index.d.ts.map +1 -1
  7. package/lib/types/adapters/netlify.d.ts +21 -0
  8. package/lib/types/adapters/netlify.d.ts.map +1 -0
  9. package/lib/types/adapters/vercel.d.ts +21 -0
  10. package/lib/types/adapters/vercel.d.ts.map +1 -0
  11. package/lib/types/ai.d.ts +182 -0
  12. package/lib/types/ai.d.ts.map +1 -0
  13. package/lib/types/csp.d.ts +107 -0
  14. package/lib/types/csp.d.ts.map +1 -0
  15. package/lib/types/env.d.ts +118 -0
  16. package/lib/types/env.d.ts.map +1 -0
  17. package/lib/types/favicon.d.ts +42 -0
  18. package/lib/types/favicon.d.ts.map +1 -1
  19. package/lib/types/index.d.ts +13 -3
  20. package/lib/types/index.d.ts.map +1 -1
  21. package/lib/types/logger.d.ts +68 -0
  22. package/lib/types/logger.d.ts.map +1 -0
  23. package/lib/types/meta.d.ts +36 -0
  24. package/lib/types/meta.d.ts.map +1 -1
  25. package/lib/types/og-image.d.ts +107 -0
  26. package/lib/types/og-image.d.ts.map +1 -0
  27. package/lib/types/types.d.ts +1 -1
  28. package/lib/types/types.d.ts.map +1 -1
  29. package/package.json +35 -10
  30. package/src/adapters/cloudflare.ts +82 -0
  31. package/src/adapters/index.ts +13 -1
  32. package/src/adapters/netlify.ts +84 -0
  33. package/src/adapters/vercel.ts +84 -0
  34. package/src/ai.ts +623 -0
  35. package/src/csp.ts +207 -0
  36. package/src/env.ts +344 -0
  37. package/src/favicon.ts +221 -80
  38. package/src/index.ts +41 -2
  39. package/src/logger.ts +144 -0
  40. package/src/meta.tsx +84 -2
  41. package/src/og-image.ts +378 -0
  42. package/src/types.ts +1 -1
package/src/meta.tsx CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { VNodeChild } from '@pyreon/core'
2
2
  import { useHead } from '@pyreon/head'
3
+ import type { FaviconPluginConfig } from './favicon'
4
+ import { faviconLinks } from './favicon'
3
5
  import type { I18nRoutingConfig } from './i18n-routing'
4
6
  import { extractLocaleFromPath } from './i18n-routing'
7
+ import { ogImagePath } from './og-image'
5
8
 
6
9
  // ─── Meta component ────────────────────────────────────────────────────────
7
10
 
@@ -16,6 +19,10 @@ export interface MetaProps {
16
19
  image?: string
17
20
  /** Image alt text for accessibility. */
18
21
  imageAlt?: string
22
+ /** Image width in pixels (og:image:width). Helps crawlers layout before loading. */
23
+ imageWidth?: number
24
+ /** Image height in pixels (og:image:height). */
25
+ imageHeight?: number
19
26
  /** Open Graph type. Default: "website" */
20
27
  type?: 'website' | 'article' | 'product' | 'profile'
21
28
  /** Site name for og:site_name. */
@@ -32,6 +39,8 @@ export interface MetaProps {
32
39
  alternateLocales?: Array<{ locale: string; url: string }>
33
40
  /** Robots directives. Default: "index, follow" */
34
41
  robots?: string
42
+ /** Convenience: set `true` to emit `noindex, nofollow`. Overrides `robots`. */
43
+ noIndex?: boolean
35
44
  /** Published time (ISO 8601) for article type. */
36
45
  publishedTime?: string
37
46
  /** Modified time (ISO 8601) for article type. */
@@ -44,6 +53,19 @@ export interface MetaProps {
44
53
  jsonLd?: Record<string, unknown>
45
54
  /** Additional custom meta tags. */
46
55
  extra?: Array<{ name?: string; property?: string; content: string }>
56
+ /**
57
+ * Open Graph video URL. Also sets og:video:type if the URL ends with
58
+ * a known extension (.mp4, .webm).
59
+ */
60
+ video?: string
61
+ /** Video width in pixels. */
62
+ videoWidth?: number
63
+ /** Video height in pixels. */
64
+ videoHeight?: number
65
+ /**
66
+ * Open Graph audio URL.
67
+ */
68
+ audio?: string
47
69
  /**
48
70
  * I18n routing config — when provided, auto-generates hreflang alternate
49
71
  * links for all locales based on the current path.
@@ -52,6 +74,22 @@ export interface MetaProps {
52
74
  i18n?: I18nRoutingConfig
53
75
  /** Base URL for building absolute hreflang URLs. e.g. "https://example.com" */
54
76
  origin?: string
77
+ /**
78
+ * Favicon plugin config — when provided, injects locale-aware favicon
79
+ * `<link>` tags into `<head>`. Uses the current locale to select
80
+ * the correct favicon set.
81
+ */
82
+ favicon?: FaviconPluginConfig
83
+ /**
84
+ * OG image template name — auto-resolves to the correct locale-specific
85
+ * OG image path generated by `ogImagePlugin`.
86
+ * Sets both `og:image` and `twitter:image` unless `image` is also provided.
87
+ */
88
+ ogTemplate?: string
89
+ /** Output directory for OG images. Default: "og" */
90
+ ogImageDir?: string
91
+ /** OG image format. Default: "png" */
92
+ ogImageFormat?: 'png' | 'jpeg'
55
93
  children?: VNodeChild
56
94
  }
57
95
 
@@ -116,14 +154,29 @@ export function buildMetaTags(
116
154
  const script: Array<{ type: string; children: string }> = []
117
155
 
118
156
  const {
119
- title, description, canonical, image, imageAlt,
157
+ title, description, canonical, imageAlt, imageWidth, imageHeight,
120
158
  type = 'website', siteName,
121
159
  twitterCard = 'summary_large_image', twitterSite, twitterCreator,
122
160
  locale = 'en_US', alternateLocales,
123
- robots = 'index, follow',
124
161
  publishedTime, modifiedTime, author, tags, jsonLd, extra,
162
+ video, videoWidth, videoHeight, audio,
163
+ favicon, ogTemplate, ogImageDir, ogImageFormat,
125
164
  } = props
126
165
 
166
+ // noIndex convenience overrides robots
167
+ const robots = props.noIndex ? 'noindex, nofollow' : (props.robots ?? 'index, follow')
168
+
169
+ // Resolve image: explicit `image` prop takes precedence over `ogTemplate`
170
+ const image = props.image ?? (
171
+ ogTemplate
172
+ ? ogImagePath(ogTemplate, locale !== 'en_US' ? locale : undefined, ogImageDir, ogImageFormat)
173
+ : undefined
174
+ )
175
+
176
+ // Auto-resolve image dimensions for OG template images
177
+ const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : undefined)
178
+ const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : undefined)
179
+
127
180
  if (description) meta.push({ name: 'description', content: description })
128
181
  if (robots) meta.push({ name: 'robots', content: robots })
129
182
  if (author) meta.push({ name: 'author', content: author })
@@ -133,10 +186,27 @@ export function buildMetaTags(
133
186
  if (canonical) meta.push({ property: 'og:url', content: canonical })
134
187
  if (image) meta.push({ property: 'og:image', content: image })
135
188
  if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })
189
+ if (resolvedImageWidth) meta.push({ property: 'og:image:width', content: String(resolvedImageWidth) })
190
+ if (resolvedImageHeight) meta.push({ property: 'og:image:height', content: String(resolvedImageHeight) })
136
191
  meta.push({ property: 'og:type', content: type })
137
192
  if (siteName) meta.push({ property: 'og:site_name', content: siteName })
138
193
  meta.push({ property: 'og:locale', content: locale })
139
194
 
195
+ // Video
196
+ if (video) {
197
+ meta.push({ property: 'og:video', content: video })
198
+ if (videoWidth) meta.push({ property: 'og:video:width', content: String(videoWidth) })
199
+ if (videoHeight) meta.push({ property: 'og:video:height', content: String(videoHeight) })
200
+ // Auto-detect video type from extension
201
+ if (video.endsWith('.mp4')) meta.push({ property: 'og:video:type', content: 'video/mp4' })
202
+ else if (video.endsWith('.webm')) meta.push({ property: 'og:video:type', content: 'video/webm' })
203
+ }
204
+
205
+ // Audio
206
+ if (audio) {
207
+ meta.push({ property: 'og:audio', content: audio })
208
+ }
209
+
140
210
  if (type === 'article') {
141
211
  if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })
142
212
  if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })
@@ -206,5 +276,17 @@ export function buildMetaTags(
206
276
  })
207
277
  }
208
278
 
279
+ // Favicon: inject locale-aware favicon links
280
+ if (favicon) {
281
+ const faviconLocale = locale !== 'en_US' ? locale : undefined
282
+ for (const fl of faviconLinks(faviconLocale, favicon)) {
283
+ link.push(fl as Record<string, string>)
284
+ }
285
+ // Theme color meta from favicon config
286
+ if (favicon.themeColor) {
287
+ meta.push({ name: 'theme-color', content: favicon.themeColor })
288
+ }
289
+ }
290
+
209
291
  return { meta, link, script }
210
292
  }
@@ -0,0 +1,378 @@
1
+ /**
2
+ * OG Image generation plugin.
3
+ *
4
+ * Generates Open Graph images at build time from templates with
5
+ * text overlays. Supports locale-specific text for i18n apps.
6
+ * Uses sharp for image processing (same optional dep as favicon/image plugins).
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // vite.config.ts
11
+ * import { ogImagePlugin } from "@pyreon/zero/og-image"
12
+ *
13
+ * export default {
14
+ * plugins: [
15
+ * ogImagePlugin({
16
+ * locales: ["en", "de", "cs"],
17
+ * templates: [{
18
+ * name: "default",
19
+ * background: "./src/assets/og-bg.jpg",
20
+ * layers: [{
21
+ * text: { en: "Build faster", de: "Schneller bauen", cs: "Stavte rychleji" },
22
+ * y: "40%",
23
+ * fontSize: 72,
24
+ * }],
25
+ * }],
26
+ * }),
27
+ * ],
28
+ * }
29
+ * ```
30
+ */
31
+ import { existsSync } from 'node:fs'
32
+ import { join } from 'node:path'
33
+ import type { Plugin } from 'vite'
34
+
35
+ let sharpWarned = false
36
+ function warnSharpMissing() {
37
+ if (sharpWarned) return
38
+ sharpWarned = true
39
+ // oxlint-disable-next-line no-console
40
+ console.warn(
41
+ '\n[zero:og-image] sharp not installed — OG images will not be generated. Install for full support: bun add -D sharp\n',
42
+ )
43
+ }
44
+
45
+ // ─── Types ──────────────────────────────────────────────────────────────────
46
+
47
+ export interface OgImageLayer {
48
+ /**
49
+ * Text content. Can be:
50
+ * - A string (same for all locales)
51
+ * - A record mapping locale → text
52
+ * - A function receiving locale and returning text
53
+ */
54
+ text: string | Record<string, string> | ((locale: string) => string)
55
+ /** X position — number (px) or string with % (e.g. "50%"). Default: "50%" */
56
+ x?: number | string
57
+ /** Y position — number (px) or string with % (e.g. "40%"). Default: "50%" */
58
+ y?: number | string
59
+ /** Font size in px. Default: 64 */
60
+ fontSize?: number
61
+ /** Font family. Default: "sans-serif" */
62
+ fontFamily?: string
63
+ /** Font weight. Default: "bold" */
64
+ fontWeight?: string
65
+ /** Text color. Default: "#ffffff" */
66
+ color?: string
67
+ /** Text anchor (alignment). Default: "middle" */
68
+ textAnchor?: 'start' | 'middle' | 'end'
69
+ /** Max width in px before wrapping. Default: 80% of image width. */
70
+ maxWidth?: number
71
+ }
72
+
73
+ export interface OgImageTemplate {
74
+ /** Template name — used for output file naming. */
75
+ name: string
76
+ /**
77
+ * Background: path to an image file, or a solid color config.
78
+ *
79
+ * @example "./src/assets/og-bg.jpg"
80
+ * @example { color: "#0066ff", width: 1200, height: 630 }
81
+ */
82
+ background: string | { color: string; width?: number; height?: number }
83
+ /** Output width. Default: 1200 */
84
+ width?: number
85
+ /** Output height. Default: 630 */
86
+ height?: number
87
+ /** Output format. Default: "png" */
88
+ format?: 'png' | 'jpeg'
89
+ /** JPEG quality (1-100). Default: 90 */
90
+ quality?: number
91
+ /** Text layers to overlay on the background. */
92
+ layers?: OgImageLayer[]
93
+ }
94
+
95
+ export interface OgImagePluginConfig {
96
+ /** Templates to generate. */
97
+ templates: OgImageTemplate[]
98
+ /** Locales to generate for. When omitted, generates a single image per template. */
99
+ locales?: string[]
100
+ /** Output directory prefix. Default: "og" */
101
+ outDir?: string
102
+ }
103
+
104
+ // ─── Helpers ────────────────────────────────────────────────────────────────
105
+
106
+ function resolvePosition(value: number | string | undefined, dimension: number, fallback = '50%'): number {
107
+ if (value === undefined) value = fallback
108
+ if (typeof value === 'number') return value
109
+ if (value.endsWith('%')) return Math.round((Number.parseFloat(value) / 100) * dimension)
110
+ return Number.parseInt(value, 10) || 0
111
+ }
112
+
113
+ function resolveLayerText(layer: OgImageLayer, locale: string): string {
114
+ if (typeof layer.text === 'string') return layer.text
115
+ if (typeof layer.text === 'function') return layer.text(locale)
116
+ return layer.text[locale] ?? layer.text[Object.keys(layer.text)[0] ?? ''] ?? ''
117
+ }
118
+
119
+ function escapeXml(str: string): string {
120
+ return str
121
+ .replace(/&/g, '&amp;')
122
+ .replace(/</g, '&lt;')
123
+ .replace(/>/g, '&gt;')
124
+ .replace(/"/g, '&quot;')
125
+ .replace(/'/g, '&apos;')
126
+ }
127
+
128
+ /**
129
+ * Build an SVG overlay with text layers.
130
+ * @internal Exported for testing.
131
+ */
132
+ export function buildTextOverlaySvg(
133
+ layers: OgImageLayer[],
134
+ width: number,
135
+ height: number,
136
+ locale: string,
137
+ ): string {
138
+ const textElements = layers.map((layer) => {
139
+ const text = resolveLayerText(layer, locale)
140
+ const x = resolvePosition(layer.x, width, '50%')
141
+ const y = resolvePosition(layer.y, height, '50%')
142
+ const fontSize = layer.fontSize ?? 64
143
+ const fontFamily = layer.fontFamily ?? 'sans-serif'
144
+ const fontWeight = layer.fontWeight ?? 'bold'
145
+ const color = layer.color ?? '#ffffff'
146
+ const anchor = layer.textAnchor ?? 'middle'
147
+ const maxWidth = layer.maxWidth ?? Math.round(width * 0.8)
148
+
149
+ // Word wrapping via tspan elements.
150
+ // Width estimation: Latin chars ~0.55em, CJK chars ~1.0em, narrow chars ~0.35em.
151
+ const words = text.split(' ')
152
+ const lines: string[] = []
153
+ let currentLine = ''
154
+
155
+ const estimateWidth = (s: string): number => {
156
+ let width = 0
157
+ for (let i = 0; i < s.length; i++) {
158
+ const code = s.charCodeAt(i)
159
+ if (code >= 0x3000 && code <= 0x9FFF) {
160
+ // CJK characters — full width
161
+ width += fontSize * 1.0
162
+ } else if (code <= 0x7E && 'iljft!|:;.,\''.includes(s[i]!)) {
163
+ // Narrow Latin characters
164
+ width += fontSize * 0.35
165
+ } else {
166
+ // Regular Latin characters
167
+ width += fontSize * 0.55
168
+ }
169
+ }
170
+ return width
171
+ }
172
+
173
+ for (const word of words) {
174
+ const testLine = currentLine ? `${currentLine} ${word}` : word
175
+ if (estimateWidth(testLine) > maxWidth && currentLine) {
176
+ lines.push(currentLine)
177
+ currentLine = word
178
+ } else {
179
+ currentLine = testLine
180
+ }
181
+ }
182
+ if (currentLine) lines.push(currentLine)
183
+
184
+ const tspans = lines
185
+ .map((line, i) => {
186
+ const dy = i === 0 ? '0' : `${fontSize * 1.2}`
187
+ return `<tspan x="${x}" dy="${dy}">${escapeXml(line)}</tspan>`
188
+ })
189
+ .join('')
190
+
191
+ 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>`
192
+ })
193
+
194
+ return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">${textElements.join('')}</svg>`
195
+ }
196
+
197
+ /**
198
+ * Render an OG image from a template for a specific locale.
199
+ * @internal Exported for testing.
200
+ */
201
+ export async function renderOgImage(
202
+ template: OgImageTemplate,
203
+ locale: string,
204
+ rootDir: string,
205
+ ): Promise<Uint8Array | null> {
206
+ try {
207
+ const sharp = await import('sharp').then((m) => m.default ?? m)
208
+ const width = template.width ?? 1200
209
+ const height = template.height ?? 630
210
+
211
+ let pipeline: any
212
+ if (typeof template.background === 'string') {
213
+ const bgPath = join(rootDir, template.background)
214
+ pipeline = sharp(bgPath).resize(width, height, { fit: 'cover' })
215
+ } else {
216
+ pipeline = (sharp as any)({
217
+ create: {
218
+ width,
219
+ height,
220
+ channels: 4,
221
+ background: template.background.color,
222
+ },
223
+ })
224
+ }
225
+
226
+ // Overlay text layers if any
227
+ if (template.layers && template.layers.length > 0) {
228
+ const svgOverlay = buildTextOverlaySvg(template.layers, width, height, locale)
229
+ pipeline = pipeline.composite([{
230
+ input: Buffer.from(svgOverlay),
231
+ top: 0,
232
+ left: 0,
233
+ }])
234
+ }
235
+
236
+ if (template.format === 'jpeg') {
237
+ return await pipeline.jpeg({ quality: template.quality ?? 90 }).toBuffer()
238
+ }
239
+ return await pipeline.png().toBuffer()
240
+ } catch {
241
+ warnSharpMissing()
242
+ return null
243
+ }
244
+ }
245
+
246
+ // ─── Path utility ───────────────────────────────────────────────────────────
247
+
248
+ /**
249
+ * Compute the OG image path for a template and locale.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * ogImagePath("default", "de") // → "/og/default-de.png"
254
+ * ogImagePath("default") // → "/og/default.png"
255
+ * ogImagePath("hero", "en", "images") // → "/images/hero-en.png"
256
+ * ```
257
+ */
258
+ export function ogImagePath(
259
+ templateName: string,
260
+ locale?: string,
261
+ outDir = 'og',
262
+ format: 'png' | 'jpeg' = 'png',
263
+ ): string {
264
+ const ext = format === 'jpeg' ? 'jpg' : 'png'
265
+ const suffix = locale ? `-${locale}` : ''
266
+ return `/${outDir}/${templateName}${suffix}.${ext}`
267
+ }
268
+
269
+ // ─── Vite plugin ────────────────────────────────────────────────────────────
270
+
271
+ /**
272
+ * OG image generation Vite plugin.
273
+ *
274
+ * Generates Open Graph images at build time. In dev, generates on-demand.
275
+ * Requires `sharp` as an optional dependency.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * // vite.config.ts
280
+ * import { ogImagePlugin } from "@pyreon/zero/og-image"
281
+ *
282
+ * export default {
283
+ * plugins: [
284
+ * ogImagePlugin({
285
+ * locales: ["en", "de"],
286
+ * templates: [{
287
+ * name: "default",
288
+ * background: { color: "#0066ff" },
289
+ * layers: [{ text: { en: "Hello", de: "Hallo" }, fontSize: 72 }],
290
+ * }],
291
+ * }),
292
+ * ],
293
+ * }
294
+ * ```
295
+ */
296
+ export function ogImagePlugin(config: OgImagePluginConfig): Plugin {
297
+ const outDir = config.outDir ?? 'og'
298
+ let root = ''
299
+ let isBuild = false
300
+
301
+ return {
302
+ name: 'pyreon-zero-og-image',
303
+ enforce: 'pre',
304
+
305
+ configResolved(resolvedConfig) {
306
+ root = resolvedConfig.root
307
+ isBuild = resolvedConfig.command === 'build'
308
+ },
309
+
310
+ // Dev: generate on-demand
311
+ configureServer(server) {
312
+ const devCache = new Map<string, Uint8Array>()
313
+
314
+ server.middlewares.use(async (req, res, next) => {
315
+ const url = req.url ?? ''
316
+ if (!url.startsWith(`/${outDir}/`)) return next()
317
+
318
+ // Parse: /og/default-en.png → template=default, locale=en
319
+ const fileName = url.slice(outDir.length + 2) // strip /{outDir}/
320
+ const match = fileName.match(/^(.+?)(?:-([a-z]{2,5}))?\.(png|jpe?g)$/)
321
+ if (!match) return next()
322
+
323
+ const [, templateName, locale, ext] = match
324
+ const template = config.templates.find((t) => t.name === templateName)
325
+ if (!template) return next()
326
+
327
+ const resolvedLocale = locale ?? config.locales?.[0] ?? 'en'
328
+ const cacheKey = `${templateName}:${resolvedLocale}`
329
+
330
+ let buffer = devCache.get(cacheKey)
331
+ if (!buffer) {
332
+ const result = await renderOgImage(template, resolvedLocale, root)
333
+ if (!result) return next()
334
+ buffer = result
335
+ devCache.set(cacheKey, result)
336
+ }
337
+
338
+ const contentType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png'
339
+ res.setHeader('Content-Type', contentType)
340
+ res.setHeader('Cache-Control', 'no-cache')
341
+ res.end(Buffer.from(buffer))
342
+ })
343
+ },
344
+
345
+ // Build: generate all variants
346
+ async generateBundle() {
347
+ if (!isBuild) return
348
+
349
+ for (const template of config.templates) {
350
+ const locales = config.locales ?? [undefined]
351
+ const format = template.format ?? 'png'
352
+ const ext = format === 'jpeg' ? 'jpg' : 'png'
353
+
354
+ for (const locale of locales) {
355
+ // Validate background exists if it's a file path
356
+ if (typeof template.background === 'string') {
357
+ const bgPath = join(root, template.background)
358
+ if (!existsSync(bgPath)) {
359
+ // oxlint-disable-next-line no-console
360
+ console.warn(`[zero:og-image] Background not found: ${bgPath}`)
361
+ continue
362
+ }
363
+ }
364
+
365
+ const buffer = await renderOgImage(template, locale ?? 'en', root)
366
+ if (!buffer) continue
367
+
368
+ const suffix = locale ? `-${locale}` : ''
369
+ this.emitFile({
370
+ type: 'asset',
371
+ fileName: `${outDir}/${template.name}${suffix}.${ext}`,
372
+ source: buffer,
373
+ })
374
+ }
375
+ }
376
+ },
377
+ }
378
+ }
package/src/types.ts CHANGED
@@ -75,7 +75,7 @@ export interface ZeroConfig {
75
75
  isr?: ISRConfig
76
76
 
77
77
  /** Deploy adapter. Default: "node" */
78
- adapter?: 'node' | 'bun' | 'static'
78
+ adapter?: 'node' | 'bun' | 'static' | 'vercel' | 'cloudflare' | 'netlify'
79
79
 
80
80
  /** Base URL path. Default: "/" */
81
81
  base?: string