@pyreon/zero 0.24.4 → 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.
- package/package.json +10 -39
- package/src/actions.ts +0 -196
- package/src/adapters/bun.ts +0 -114
- package/src/adapters/cloudflare.ts +0 -166
- package/src/adapters/index.ts +0 -61
- package/src/adapters/netlify.ts +0 -154
- package/src/adapters/node.ts +0 -163
- package/src/adapters/static.ts +0 -42
- package/src/adapters/validate.ts +0 -23
- package/src/adapters/vercel.ts +0 -182
- package/src/adapters/warn-missing-env.ts +0 -49
- package/src/ai.ts +0 -623
- package/src/api-routes.ts +0 -219
- package/src/app.ts +0 -92
- package/src/cache.ts +0 -136
- package/src/client.ts +0 -143
- package/src/compression.ts +0 -116
- package/src/config.ts +0 -35
- package/src/cors.ts +0 -94
- package/src/csp.ts +0 -226
- package/src/entry-server.ts +0 -224
- package/src/env.ts +0 -344
- package/src/error-overlay.ts +0 -118
- package/src/favicon.ts +0 -841
- package/src/font.ts +0 -511
- package/src/fs-router.ts +0 -1519
- package/src/i18n-routing.ts +0 -533
- package/src/icon.tsx +0 -182
- package/src/icons-plugin.ts +0 -296
- package/src/image-plugin.ts +0 -751
- package/src/image-types.ts +0 -60
- package/src/image.tsx +0 -340
- package/src/index.ts +0 -92
- package/src/isr.ts +0 -394
- package/src/link.tsx +0 -304
- package/src/logger.ts +0 -144
- package/src/manifest.ts +0 -787
- package/src/meta.tsx +0 -354
- package/src/middleware.ts +0 -65
- package/src/not-found.ts +0 -44
- package/src/og-image.ts +0 -378
- package/src/rate-limit.ts +0 -140
- package/src/script.tsx +0 -260
- package/src/seo.ts +0 -617
- package/src/server.ts +0 -89
- package/src/sharp.d.ts +0 -22
- package/src/ssg-plugin.ts +0 -1582
- package/src/testing.ts +0 -146
- package/src/theme.tsx +0 -257
- package/src/types.ts +0 -624
- package/src/utils/use-intersection-observer.ts +0 -36
- package/src/utils/with-headers.ts +0 -13
- package/src/vercel-revalidate-handler.ts +0 -204
- 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
|
-
}
|