@pyreon/zero 0.24.5 → 0.24.6

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 (54) hide show
  1. package/package.json +10 -39
  2. package/src/actions.ts +0 -196
  3. package/src/adapters/bun.ts +0 -114
  4. package/src/adapters/cloudflare.ts +0 -166
  5. package/src/adapters/index.ts +0 -61
  6. package/src/adapters/netlify.ts +0 -154
  7. package/src/adapters/node.ts +0 -163
  8. package/src/adapters/static.ts +0 -42
  9. package/src/adapters/validate.ts +0 -23
  10. package/src/adapters/vercel.ts +0 -182
  11. package/src/adapters/warn-missing-env.ts +0 -49
  12. package/src/ai.ts +0 -623
  13. package/src/api-routes.ts +0 -219
  14. package/src/app.ts +0 -92
  15. package/src/cache.ts +0 -136
  16. package/src/client.ts +0 -143
  17. package/src/compression.ts +0 -116
  18. package/src/config.ts +0 -35
  19. package/src/cors.ts +0 -94
  20. package/src/csp.ts +0 -226
  21. package/src/entry-server.ts +0 -224
  22. package/src/env.ts +0 -344
  23. package/src/error-overlay.ts +0 -118
  24. package/src/favicon.ts +0 -841
  25. package/src/font.ts +0 -511
  26. package/src/fs-router.ts +0 -1519
  27. package/src/i18n-routing.ts +0 -533
  28. package/src/icon.tsx +0 -182
  29. package/src/icons-plugin.ts +0 -296
  30. package/src/image-plugin.ts +0 -751
  31. package/src/image-types.ts +0 -60
  32. package/src/image.tsx +0 -340
  33. package/src/index.ts +0 -92
  34. package/src/isr.ts +0 -394
  35. package/src/link.tsx +0 -304
  36. package/src/logger.ts +0 -144
  37. package/src/manifest.ts +0 -787
  38. package/src/meta.tsx +0 -354
  39. package/src/middleware.ts +0 -65
  40. package/src/not-found.ts +0 -44
  41. package/src/og-image.ts +0 -378
  42. package/src/rate-limit.ts +0 -140
  43. package/src/script.tsx +0 -260
  44. package/src/seo.ts +0 -617
  45. package/src/server.ts +0 -89
  46. package/src/sharp.d.ts +0 -22
  47. package/src/ssg-plugin.ts +0 -1582
  48. package/src/testing.ts +0 -146
  49. package/src/theme.tsx +0 -257
  50. package/src/types.ts +0 -624
  51. package/src/utils/use-intersection-observer.ts +0 -36
  52. package/src/utils/with-headers.ts +0 -13
  53. package/src/vercel-revalidate-handler.ts +0 -204
  54. package/src/vite-plugin.ts +0 -848
package/src/meta.tsx DELETED
@@ -1,354 +0,0 @@
1
- import type { VNodeChild } from '@pyreon/core'
2
- import type { UseHeadInput } from '@pyreon/head'
3
- import { useHead } from '@pyreon/head'
4
- import type { I18nRoutingConfig } from './i18n-routing'
5
- import { extractLocaleFromPath } from './i18n-routing'
6
-
7
- // ─── Inline helpers (no node:fs dependency) ─────────────────────────────────
8
- // These are inlined to avoid importing from favicon.ts/og-image.ts which
9
- // pull in node:fs at the top level — making Meta unsafe for client bundles.
10
-
11
- /** Favicon plugin config shape (type-only). */
12
- interface FaviconPluginConfig {
13
- source: string
14
- themeColor?: string
15
- manifest?: boolean
16
- locales?: Record<string, { source: string; darkSource?: string }>
17
- [key: string]: unknown
18
- }
19
-
20
- function faviconLinks(
21
- locale: string | undefined,
22
- config: FaviconPluginConfig,
23
- ): Array<{ rel: string; type?: string; sizes?: string; href: string }> {
24
- const hasLocaleOverride = locale && config.locales?.[locale]
25
- const prefix = hasLocaleOverride ? `/${locale}` : ''
26
- const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')
27
- const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []
28
- if (isSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })
29
- links.push(
30
- { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },
31
- { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },
32
- { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },
33
- )
34
- if (config.manifest !== false) links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })
35
- return links
36
- }
37
-
38
- function ogImagePath(templateName: string, locale?: string, outDir = 'og', format: 'png' | 'jpeg' = 'png'): string {
39
- const ext = format === 'jpeg' ? 'jpg' : 'png'
40
- const suffix = locale ? `-${locale}` : ''
41
- return `/${outDir}/${templateName}${suffix}.${ext}`
42
- }
43
-
44
- // ─── Meta component ────────────────────────────────────────────────────────
45
-
46
- export interface MetaProps {
47
- /** Page title. Accepts reactive accessor `() => string`. */
48
- title?: string | (() => string)
49
- /** Page description. Accepts reactive accessor. */
50
- description?: string | (() => string)
51
- /** Canonical URL. Also sets og:url. */
52
- canonical?: string
53
- /** Open Graph image URL. Also sets twitter:image. */
54
- image?: string
55
- /** Image alt text for accessibility. */
56
- imageAlt?: string
57
- /** Image width in pixels (og:image:width). Helps crawlers layout before loading. */
58
- imageWidth?: number
59
- /** Image height in pixels (og:image:height). */
60
- imageHeight?: number
61
- /** Open Graph type. Default: "website" */
62
- type?: 'website' | 'article' | 'product' | 'profile'
63
- /** Site name for og:site_name. */
64
- siteName?: string
65
- /** Twitter card type. Default: "summary_large_image" */
66
- twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'
67
- /** Twitter @handle. */
68
- twitterSite?: string
69
- /** Twitter creator @handle. */
70
- twitterCreator?: string
71
- /** Locale. Default: "en_US" */
72
- locale?: string
73
- /** Alternate locales for hreflang. */
74
- alternateLocales?: Array<{ locale: string; url: string }>
75
- /** Robots directives. Default: "index, follow" */
76
- robots?: string
77
- /** Convenience: set `true` to emit `noindex, nofollow`. Overrides `robots`. */
78
- noIndex?: boolean
79
- /** Published time (ISO 8601) for article type. */
80
- publishedTime?: string
81
- /** Modified time (ISO 8601) for article type. */
82
- modifiedTime?: string
83
- /** Article author. */
84
- author?: string
85
- /** Article tags. */
86
- tags?: string[]
87
- /** JSON-LD structured data object. */
88
- jsonLd?: Record<string, unknown>
89
- /** Additional custom meta tags. */
90
- extra?: Array<{ name?: string; property?: string; content: string }>
91
- /**
92
- * Open Graph video URL. Also sets og:video:type if the URL ends with
93
- * a known extension (.mp4, .webm).
94
- */
95
- video?: string
96
- /** Video width in pixels. */
97
- videoWidth?: number
98
- /** Video height in pixels. */
99
- videoHeight?: number
100
- /**
101
- * Open Graph audio URL.
102
- */
103
- audio?: string
104
- /**
105
- * I18n routing config — when provided, auto-generates hreflang alternate
106
- * links for all locales based on the current path.
107
- * Also sets og:locale and og:locale:alternate.
108
- */
109
- i18n?: I18nRoutingConfig
110
- /** Base URL for building absolute hreflang URLs. e.g. "https://example.com" */
111
- origin?: string
112
- /**
113
- * Favicon plugin config — when provided, injects locale-aware favicon
114
- * `<link>` tags into `<head>`. Uses the current locale to select
115
- * the correct favicon set.
116
- */
117
- favicon?: FaviconPluginConfig
118
- /**
119
- * OG image template name — auto-resolves to the correct locale-specific
120
- * OG image path generated by `ogImagePlugin`.
121
- * Sets both `og:image` and `twitter:image` unless `image` is also provided.
122
- */
123
- ogTemplate?: string
124
- /** Output directory for OG images. Default: "og" */
125
- ogImageDir?: string
126
- /** OG image format. Default: "png" */
127
- ogImageFormat?: 'png' | 'jpeg'
128
- children?: VNodeChild
129
- }
130
-
131
- const resolveStr = (v: string | (() => string) | undefined): string | undefined =>
132
- typeof v === 'function' ? v() : v
133
-
134
- /**
135
- * Declarative meta component for SSR-compatible page metadata.
136
- *
137
- * Supports reactive title/description — when passed as `() => string` accessors,
138
- * they are forwarded to `useHead()` as a reactive getter so updates propagate
139
- * automatically via signal tracking.
140
- *
141
- * @example
142
- * ```tsx
143
- * <Meta title="My Page" description="..." image="/og.jpg" canonical="https://..." />
144
- * ```
145
- *
146
- * @example Reactive title
147
- * ```tsx
148
- * const count = signal(0)
149
- * <Meta title={() => `${count()} items`} />
150
- * ```
151
- */
152
- export function Meta(props: MetaProps): VNodeChild {
153
- const hasReactiveTitle = typeof props.title === 'function'
154
- const hasReactiveDescription = typeof props.description === 'function'
155
-
156
- // If title or description are reactive accessors, pass a getter to useHead
157
- // so it re-evaluates when the signals change.
158
- if (hasReactiveTitle || hasReactiveDescription) {
159
- useHead((): UseHeadInput => {
160
- const title = resolveStr(props.title)
161
- const description = resolveStr(props.description)
162
- const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]
163
- const tags = buildMetaTags(resolved)
164
- const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }
165
- if (title) input.title = title
166
- return input
167
- })
168
- } else {
169
- const title = resolveStr(props.title)
170
- const description = resolveStr(props.description)
171
- const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]
172
- const tags = buildMetaTags(resolved)
173
- const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }
174
- if (title) input.title = title
175
- useHead(input)
176
- }
177
-
178
- return props.children ?? null
179
- }
180
-
181
- interface MetaTagEntry {
182
- name?: string
183
- property?: string
184
- content: string
185
- [key: string]: string | undefined
186
- }
187
-
188
- interface LinkTagEntry {
189
- rel: string
190
- href?: string
191
- hreflang?: string
192
- type?: string
193
- sizes?: string
194
- [key: string]: string | undefined
195
- }
196
-
197
- interface ScriptTagEntry {
198
- type: string
199
- children: string
200
- }
201
-
202
- interface MetaTags {
203
- meta: MetaTagEntry[]
204
- link: LinkTagEntry[]
205
- script: ScriptTagEntry[]
206
- }
207
-
208
- export function buildMetaTags(
209
- props: Omit<MetaProps, 'title' | 'description' | 'children'> & {
210
- title?: string
211
- description?: string
212
- },
213
- ): MetaTags {
214
- const meta: MetaTagEntry[] = []
215
- const link: LinkTagEntry[] = []
216
- const script: ScriptTagEntry[] = []
217
-
218
- const {
219
- title, description, canonical, imageAlt, imageWidth, imageHeight,
220
- type = 'website', siteName,
221
- twitterCard = 'summary_large_image', twitterSite, twitterCreator,
222
- locale = 'en_US', alternateLocales,
223
- publishedTime, modifiedTime, author, tags, jsonLd, extra,
224
- video, videoWidth, videoHeight, audio,
225
- favicon, ogTemplate, ogImageDir, ogImageFormat,
226
- } = props
227
-
228
- // noIndex convenience overrides robots
229
- const robots = props.noIndex ? 'noindex, nofollow' : (props.robots ?? 'index, follow')
230
-
231
- // Resolve image: explicit `image` prop takes precedence over `ogTemplate`
232
- const image = props.image ?? (
233
- ogTemplate
234
- ? ogImagePath(ogTemplate, locale !== 'en_US' ? locale : undefined, ogImageDir, ogImageFormat)
235
- : undefined
236
- )
237
-
238
- // Auto-resolve image dimensions for OG template images
239
- const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : undefined)
240
- const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : undefined)
241
-
242
- if (description) meta.push({ name: 'description', content: description })
243
- if (robots) meta.push({ name: 'robots', content: robots })
244
- if (author) meta.push({ name: 'author', content: author })
245
-
246
- if (title) meta.push({ property: 'og:title', content: title })
247
- if (description) meta.push({ property: 'og:description', content: description })
248
- if (canonical) meta.push({ property: 'og:url', content: canonical })
249
- if (image) meta.push({ property: 'og:image', content: image })
250
- if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })
251
- if (resolvedImageWidth) meta.push({ property: 'og:image:width', content: String(resolvedImageWidth) })
252
- if (resolvedImageHeight) meta.push({ property: 'og:image:height', content: String(resolvedImageHeight) })
253
- meta.push({ property: 'og:type', content: type })
254
- if (siteName) meta.push({ property: 'og:site_name', content: siteName })
255
- meta.push({ property: 'og:locale', content: locale })
256
-
257
- // Video
258
- if (video) {
259
- meta.push({ property: 'og:video', content: video })
260
- if (videoWidth) meta.push({ property: 'og:video:width', content: String(videoWidth) })
261
- if (videoHeight) meta.push({ property: 'og:video:height', content: String(videoHeight) })
262
- // Auto-detect video type from extension
263
- if (video.endsWith('.mp4')) meta.push({ property: 'og:video:type', content: 'video/mp4' })
264
- else if (video.endsWith('.webm')) meta.push({ property: 'og:video:type', content: 'video/webm' })
265
- }
266
-
267
- // Audio
268
- if (audio) {
269
- meta.push({ property: 'og:audio', content: audio })
270
- }
271
-
272
- if (type === 'article') {
273
- if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })
274
- if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })
275
- if (author) meta.push({ property: 'article:author', content: author })
276
- if (tags) for (const tag of tags) meta.push({ property: 'article:tag', content: tag })
277
- }
278
-
279
- meta.push({ name: 'twitter:card', content: twitterCard })
280
- if (title) meta.push({ name: 'twitter:title', content: title })
281
- if (description) meta.push({ name: 'twitter:description', content: description })
282
- if (image) meta.push({ name: 'twitter:image', content: image })
283
- if (imageAlt) meta.push({ name: 'twitter:image:alt', content: imageAlt })
284
- if (twitterSite) meta.push({ name: 'twitter:site', content: twitterSite })
285
- if (twitterCreator) meta.push({ name: 'twitter:creator', content: twitterCreator })
286
-
287
- if (canonical) link.push({ rel: 'canonical', href: canonical })
288
- if (alternateLocales) {
289
- for (const alt of alternateLocales) {
290
- link.push({ rel: 'alternate', hreflang: alt.locale, href: alt.url })
291
- }
292
- }
293
-
294
- if (jsonLd) {
295
- script.push({
296
- type: 'application/ld+json',
297
- children: JSON.stringify({ '@context': 'https://schema.org', ...jsonLd }),
298
- })
299
- }
300
-
301
- if (extra) for (const tag of extra) meta.push(tag)
302
-
303
- // I18n: auto-generate hreflang alternates from i18nRouting config
304
- if (props.i18n) {
305
- const i18nConfig = props.i18n
306
- const origin = props.origin ?? ''
307
- const currentPath = canonical?.replace(origin, '') ?? '/'
308
- const { pathWithoutLocale } = extractLocaleFromPath(
309
- currentPath,
310
- i18nConfig.locales,
311
- i18nConfig.defaultLocale,
312
- )
313
- const strategy = i18nConfig.strategy ?? 'prefix-except-default'
314
-
315
- for (const loc of i18nConfig.locales) {
316
- const localizedPath =
317
- strategy === 'prefix-except-default' && loc === i18nConfig.defaultLocale
318
- ? pathWithoutLocale
319
- : `/${loc}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`
320
-
321
- link.push({
322
- rel: 'alternate',
323
- hreflang: loc,
324
- href: `${origin}${localizedPath}`,
325
- })
326
-
327
- // og:locale:alternate for non-current locales
328
- if (loc !== locale) {
329
- meta.push({ property: 'og:locale:alternate', content: loc })
330
- }
331
- }
332
-
333
- // x-default hreflang pointing to default locale
334
- link.push({
335
- rel: 'alternate',
336
- hreflang: 'x-default',
337
- href: `${origin}${pathWithoutLocale}`,
338
- })
339
- }
340
-
341
- // Favicon: inject locale-aware favicon links
342
- if (favicon) {
343
- const faviconLocale = locale !== 'en_US' ? locale : undefined
344
- for (const fl of faviconLinks(faviconLocale, favicon)) {
345
- link.push(fl as LinkTagEntry)
346
- }
347
- // Theme color meta from favicon config
348
- if (favicon.themeColor) {
349
- meta.push({ name: 'theme-color', content: favicon.themeColor })
350
- }
351
- }
352
-
353
- return { meta, link, script }
354
- }
package/src/middleware.ts DELETED
@@ -1,65 +0,0 @@
1
- import type { Middleware, MiddlewareContext } from '@pyreon/server'
2
-
3
- // ─── Middleware composition ─────────────────────────────────────────────────
4
- //
5
- // Chains multiple middleware functions into a single middleware.
6
- // Each middleware runs in order. If any returns a Response, the chain
7
- // short-circuits and that Response is returned. If all return void,
8
- // the composed middleware returns void (continues to rendering).
9
-
10
- /**
11
- * Compose multiple middleware into a single middleware function.
12
- * Middleware runs sequentially — if any returns a Response, the chain stops.
13
- *
14
- * @example
15
- * import { compose } from "@pyreon/zero/middleware"
16
- * import { corsMiddleware } from "@pyreon/zero/cors"
17
- * import { rateLimitMiddleware } from "@pyreon/zero/rate-limit"
18
- *
19
- * const combined = compose(
20
- * corsMiddleware({ origin: "*" }),
21
- * rateLimitMiddleware({ max: 100 }),
22
- * cacheMiddleware(),
23
- * )
24
- */
25
- export function compose(...middlewares: Middleware[]): Middleware {
26
- return async (ctx: MiddlewareContext) => {
27
- for (const mw of middlewares) {
28
- const result = await mw(ctx)
29
- if (result instanceof Response) return result
30
- }
31
- }
32
- }
33
-
34
- // ─── Shared request context ─────────────────────────────────────────────────
35
- //
36
- // Lightweight context bag attached to MiddlewareContext.locals so middleware
37
- // can communicate without coupling. Uses a namespaced key to avoid collisions
38
- // with user-defined locals.
39
-
40
- const ZERO_CTX_KEY = '__zeroCtx'
41
-
42
- /**
43
- * Get the shared Zero context from a middleware context.
44
- * Creates one if it doesn't exist. Middleware can use this to
45
- * pass data to downstream middleware without polluting `ctx.locals`.
46
- *
47
- * @example
48
- * const authMiddleware: Middleware = (ctx) => {
49
- * const zctx = getContext(ctx)
50
- * zctx.userId = "user_123"
51
- * }
52
- *
53
- * const loggingMiddleware: Middleware = (ctx) => {
54
- * const zctx = getContext(ctx)
55
- * console.log("User:", zctx.userId)
56
- * }
57
- */
58
- export function getContext(ctx: MiddlewareContext): Record<string, unknown> {
59
- let zctx = ctx.locals[ZERO_CTX_KEY] as Record<string, unknown> | undefined
60
- if (!zctx) {
61
- zctx = {}
62
- ctx.locals[ZERO_CTX_KEY] = zctx
63
- }
64
- return zctx
65
- }
package/src/not-found.ts DELETED
@@ -1,44 +0,0 @@
1
- import type { ComponentFn } from "@pyreon/core";
2
- import { h } from "@pyreon/core";
3
- import { renderToString } from "@pyreon/runtime-server";
4
-
5
- // ─── 404 Not Found rendering ────────────────────────────────────────────────
6
- //
7
- // Shared utility for rendering 404 pages in both dev (vite-plugin) and
8
- // production (entry-server). Renders the notFoundComponent into HTML
9
- // and wraps it in a minimal document if no template is provided.
10
-
11
- const DEFAULT_404_BODY =
12
- "<h1>404 — Not Found</h1><p>The page you requested does not exist.</p>";
13
-
14
- /**
15
- * Render a 404 component to a full HTML string.
16
- * If no component is provided, returns a default 404 page.
17
- */
18
- export async function render404Page(
19
- component: ComponentFn | undefined,
20
- template?: string,
21
- ): Promise<string> {
22
- let body: string;
23
- if (component) {
24
- body = await renderToString(h(component, null));
25
- } else {
26
- body = DEFAULT_404_BODY;
27
- }
28
-
29
- if (template?.includes("<!--pyreon-app-->")) {
30
- return template.replace("<!--pyreon-app-->", body);
31
- }
32
-
33
- return `<!DOCTYPE html>
34
- <html lang="en">
35
- <head>
36
- <meta charset="UTF-8">
37
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
38
- <title>404 — Not Found</title>
39
- </head>
40
- <body>
41
- ${body}
42
- </body>
43
- </html>`;
44
- }