@pyreon/zero 0.12.3 → 0.12.5
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/README.md +15 -2
- package/lib/favicon.js +151 -6
- package/lib/favicon.js.map +1 -1
- package/lib/index.js +343 -4039
- package/lib/index.js.map +1 -1
- package/lib/meta.js +22 -48
- package/lib/meta.js.map +1 -1
- package/lib/server.js +1534 -0
- package/lib/server.js.map +1 -0
- package/lib/theme.js +5 -2
- package/lib/theme.js.map +1 -1
- package/lib/types/favicon.d.ts +17 -0
- package/lib/types/favicon.d.ts.map +1 -1
- package/lib/types/index.d.ts +189 -1540
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/meta.d.ts +11 -46
- package/lib/types/meta.d.ts.map +1 -1
- package/lib/types/server.d.ts +455 -0
- package/lib/types/server.d.ts.map +1 -0
- package/lib/types/theme.d.ts +1 -1
- package/lib/types/theme.d.ts.map +1 -1
- package/package.json +15 -10
- package/src/favicon.ts +163 -33
- package/src/index.ts +47 -182
- package/src/meta.tsx +37 -3
- package/src/server.ts +70 -0
- package/src/theme.tsx +10 -3
- package/lib/fs-router-Dil4IKZR.js +0 -290
- package/lib/fs-router-Dil4IKZR.js.map +0 -1
package/src/favicon.ts
CHANGED
|
@@ -71,6 +71,23 @@ export interface FaviconPluginConfig {
|
|
|
71
71
|
* ```
|
|
72
72
|
*/
|
|
73
73
|
locales?: Record<string, FaviconLocaleConfig>
|
|
74
|
+
/**
|
|
75
|
+
* Dev mode favicon — shown only during development to distinguish
|
|
76
|
+
* dev tabs from production. Can be:
|
|
77
|
+
* - A path to a separate icon file
|
|
78
|
+
* - `true` to auto-generate a dev badge (grayscale + "DEV" overlay)
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* faviconPlugin({
|
|
83
|
+
* source: "./icon.svg",
|
|
84
|
+
* devSource: "./icon-dev.svg", // custom dev icon
|
|
85
|
+
* // OR
|
|
86
|
+
* devSource: true, // auto-generate grayscale badge
|
|
87
|
+
* })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
devSource?: string | boolean
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
interface FaviconSize {
|
|
@@ -122,36 +139,59 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
122
139
|
// Dev server: serve generated favicons on-the-fly
|
|
123
140
|
configureServer(server) {
|
|
124
141
|
const sourcePath = join(root, config.source)
|
|
142
|
+
const darkPath = config.darkSource ? join(root, config.darkSource) : null
|
|
143
|
+
const devSourcePath = typeof config.devSource === 'string'
|
|
144
|
+
? join(root, config.devSource)
|
|
145
|
+
: null
|
|
146
|
+
const autoDevBadge = config.devSource === true
|
|
125
147
|
const devCache = new Map<string, Uint8Array>()
|
|
126
148
|
|
|
149
|
+
/** Resolve source path for a request — handles dark variants and dev badge. */
|
|
150
|
+
function resolveSourceForDev(baseName: string, defaultSource: string): string {
|
|
151
|
+
// Dark variant: favicon-dark-32x32.png → use darkSource
|
|
152
|
+
if (darkPath && baseName.includes('-dark-')) return darkPath
|
|
153
|
+
// Light variant: favicon-light-32x32.png → use source
|
|
154
|
+
if (baseName.includes('-light-')) return defaultSource
|
|
155
|
+
return defaultSource
|
|
156
|
+
}
|
|
157
|
+
|
|
127
158
|
server.middlewares.use(async (req, res, next) => {
|
|
128
159
|
const url = req.url ?? ''
|
|
129
160
|
|
|
130
|
-
// Resolve locale-specific source
|
|
161
|
+
// Resolve locale-specific source
|
|
131
162
|
const localeSource = resolveLocaleSource(url, config, root)
|
|
132
|
-
|
|
133
|
-
// Serve source as favicon.svg in dev
|
|
134
163
|
const svgUrl = localeSource ? localeSource.url : url
|
|
135
164
|
const svgPath = localeSource ? localeSource.sourcePath : sourcePath
|
|
136
165
|
const isSvgSource = localeSource ? localeSource.source.endsWith('.svg') : config.source.endsWith('.svg')
|
|
137
166
|
|
|
167
|
+
// Serve favicon.svg — in dev, add dev badge overlay if configured
|
|
138
168
|
if (svgUrl.endsWith('/favicon.svg') && isSvgSource) {
|
|
139
169
|
try {
|
|
140
|
-
|
|
170
|
+
let content = await readFile(svgPath, 'utf-8')
|
|
171
|
+
if (autoDevBadge) content = addDevBadgeToSvg(content)
|
|
172
|
+
else if (devSourcePath && existsSync(devSourcePath)) {
|
|
173
|
+
content = await readFile(devSourcePath, 'utf-8')
|
|
174
|
+
}
|
|
141
175
|
res.setHeader('Content-Type', 'image/svg+xml')
|
|
142
176
|
res.end(content)
|
|
143
177
|
return
|
|
144
178
|
} catch { /* fall through */ }
|
|
145
179
|
}
|
|
146
180
|
|
|
147
|
-
// Serve generated PNGs on-demand
|
|
181
|
+
// Serve generated PNGs on-demand — supports dark variants + dev badge
|
|
148
182
|
const baseName = svgUrl.split('/').pop() ?? ''
|
|
149
|
-
|
|
183
|
+
// Strip light-/dark- prefix for size matching
|
|
184
|
+
const cleanName = baseName.replace(/-?(light|dark)-/, '-')
|
|
185
|
+
const sizeMatch = SIZES.find((s) => s.name === cleanName || baseName === s.name)
|
|
150
186
|
if (sizeMatch) {
|
|
151
|
-
const
|
|
187
|
+
const resolvedSource = resolveSourceForDev(baseName, svgPath)
|
|
188
|
+
const cacheKey = `${resolvedSource}:${sizeMatch.size}:${autoDevBadge}`
|
|
152
189
|
let png = devCache.get(cacheKey)
|
|
153
190
|
if (!png) {
|
|
154
|
-
|
|
191
|
+
let result = await resizeToPng(resolvedSource, sizeMatch.size)
|
|
192
|
+
if (result && autoDevBadge) {
|
|
193
|
+
result = await addDevBadgeToPng(result, sizeMatch.size)
|
|
194
|
+
}
|
|
155
195
|
if (result) {
|
|
156
196
|
png = result
|
|
157
197
|
devCache.set(cacheKey, result)
|
|
@@ -210,12 +250,14 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
210
250
|
// Inject favicon <link> tags into HTML
|
|
211
251
|
transformIndexHtml() {
|
|
212
252
|
const isSvg = config.source.endsWith('.svg')
|
|
253
|
+
const hasDark = !!config.darkSource
|
|
213
254
|
const tags: Array<{
|
|
214
255
|
tag: string
|
|
215
256
|
attrs: Record<string, string>
|
|
216
257
|
injectTo: 'head'
|
|
217
258
|
}> = []
|
|
218
259
|
|
|
260
|
+
// SVG favicon (with prefers-color-scheme media query when dark variant exists)
|
|
219
261
|
if (isSvg) {
|
|
220
262
|
tags.push({
|
|
221
263
|
tag: 'link',
|
|
@@ -224,23 +266,28 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
224
266
|
})
|
|
225
267
|
}
|
|
226
268
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
tag: 'link',
|
|
235
|
-
attrs: { rel: 'icon', type: 'image/png', sizes: '
|
|
236
|
-
injectTo: 'head',
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
tag: 'link',
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
269
|
+
if (hasDark) {
|
|
270
|
+
// Dual-variant PNG/ICO favicons — light active, dark hidden via media="not all".
|
|
271
|
+
// The themeScript and initTheme() swap these based on the resolved theme.
|
|
272
|
+
const lightAttrs = { 'data-favicon-theme': 'light' }
|
|
273
|
+
const darkAttrs = { 'data-favicon-theme': 'dark', media: 'not all' }
|
|
274
|
+
|
|
275
|
+
tags.push(
|
|
276
|
+
{ tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-light-32x32.png', ...lightAttrs }, injectTo: 'head' },
|
|
277
|
+
{ tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-dark-32x32.png', ...darkAttrs }, injectTo: 'head' },
|
|
278
|
+
{ tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-light-16x16.png', ...lightAttrs }, injectTo: 'head' },
|
|
279
|
+
{ tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-dark-16x16.png', ...darkAttrs }, injectTo: 'head' },
|
|
280
|
+
{ tag: 'link', attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon-light.png', ...lightAttrs }, injectTo: 'head' },
|
|
281
|
+
{ tag: 'link', attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon-dark.png', ...darkAttrs }, injectTo: 'head' },
|
|
282
|
+
)
|
|
283
|
+
} else {
|
|
284
|
+
// Single-variant (no dark mode)
|
|
285
|
+
tags.push(
|
|
286
|
+
{ tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' }, injectTo: 'head' },
|
|
287
|
+
{ tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' }, injectTo: 'head' },
|
|
288
|
+
{ tag: 'link', attrs: { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }, injectTo: 'head' },
|
|
289
|
+
)
|
|
290
|
+
}
|
|
244
291
|
|
|
245
292
|
if (generateManifest) {
|
|
246
293
|
tags.push({
|
|
@@ -371,14 +418,43 @@ async function generateFaviconSet(
|
|
|
371
418
|
}
|
|
372
419
|
|
|
373
420
|
// Generate PNG sizes via sharp
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
421
|
+
if (darkSource) {
|
|
422
|
+
// Dual-variant: generate light + dark PNGs with prefixed names
|
|
423
|
+
const darkPath = join(rootDir, darkSource)
|
|
424
|
+
const darkExists = existsSync(darkPath)
|
|
425
|
+
|
|
426
|
+
for (const { size, name } of SIZES) {
|
|
427
|
+
// Light variant
|
|
428
|
+
const lightName = name.replace(/^(favicon-)/, '$1light-').replace(/^(apple-touch-icon)/, '$1-light').replace(/^(icon-)/, '$1light-')
|
|
429
|
+
const lightPng = await resizeToPng(sourcePath, size)
|
|
430
|
+
if (lightPng) {
|
|
431
|
+
this.emitFile({ type: 'asset', fileName: `${prefix}${lightName}`, source: lightPng })
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Dark variant
|
|
435
|
+
if (darkExists) {
|
|
436
|
+
const darkName = name.replace(/^(favicon-)/, '$1dark-').replace(/^(apple-touch-icon)/, '$1-dark').replace(/^(icon-)/, '$1dark-')
|
|
437
|
+
const darkPng = await resizeToPng(darkPath, size)
|
|
438
|
+
if (darkPng) {
|
|
439
|
+
this.emitFile({ type: 'asset', fileName: `${prefix}${darkName}`, source: darkPng })
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Also generate standard names (used by manifest + external references)
|
|
445
|
+
for (const { size, name } of SIZES) {
|
|
446
|
+
const pngBuffer = await resizeToPng(sourcePath, size)
|
|
447
|
+
if (pngBuffer) {
|
|
448
|
+
this.emitFile({ type: 'asset', fileName: `${prefix}${name}`, source: pngBuffer })
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
// Single-variant
|
|
453
|
+
for (const { size, name } of SIZES) {
|
|
454
|
+
const pngBuffer = await resizeToPng(sourcePath, size)
|
|
455
|
+
if (pngBuffer) {
|
|
456
|
+
this.emitFile({ type: 'asset', fileName: `${prefix}${name}`, source: pngBuffer })
|
|
457
|
+
}
|
|
382
458
|
}
|
|
383
459
|
}
|
|
384
460
|
|
|
@@ -519,3 +595,57 @@ export function createIcoFromPngs(entries: IcoEntry[]): Uint8Array {
|
|
|
519
595
|
|
|
520
596
|
return Buffer.concat([header, dirEntries, ...dataBuffers])
|
|
521
597
|
}
|
|
598
|
+
|
|
599
|
+
// ─── Dev badge helpers ──────────────────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Add a "DEV" badge overlay to an SVG string.
|
|
603
|
+
* Adds a small colored circle with "DEV" text in the bottom-right corner.
|
|
604
|
+
*/
|
|
605
|
+
function addDevBadgeToSvg(svg: string): string {
|
|
606
|
+
const viewBoxMatch = svg.match(/viewBox="([^"]*)"/)
|
|
607
|
+
const viewBox = viewBoxMatch?.[1] ?? '0 0 32 32'
|
|
608
|
+
const [, , w, h] = viewBox.split(' ').map(Number)
|
|
609
|
+
const size = Math.min(w ?? 32, h ?? 32)
|
|
610
|
+
const r = size * 0.28
|
|
611
|
+
const cx = (w ?? 32) - r
|
|
612
|
+
const cy = (h ?? 32) - r
|
|
613
|
+
const fontSize = r * 0.85
|
|
614
|
+
|
|
615
|
+
const badge = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="#ef4444" stroke="white" stroke-width="${size * 0.03}"/>` +
|
|
616
|
+
`<text x="${cx}" y="${cy}" font-size="${fontSize}" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="central" font-family="sans-serif">D</text>`
|
|
617
|
+
|
|
618
|
+
// Insert badge before closing </svg>
|
|
619
|
+
return svg.replace(/<\/svg>\s*$/, `${badge}</svg>`)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Add a "DEV" badge to a PNG buffer via sharp composite.
|
|
624
|
+
* Composites a red circle with "D" in the bottom-right corner.
|
|
625
|
+
*/
|
|
626
|
+
async function addDevBadgeToPng(pngBuffer: Uint8Array, size: number): Promise<Uint8Array> {
|
|
627
|
+
try {
|
|
628
|
+
const sharp = await import('sharp').then((m) => m.default ?? m)
|
|
629
|
+
const r = Math.round(size * 0.28)
|
|
630
|
+
const d = r * 2
|
|
631
|
+
const fontSize = Math.round(r * 0.85)
|
|
632
|
+
|
|
633
|
+
const badgeSvg = `<svg width="${d}" height="${d}" xmlns="http://www.w3.org/2000/svg">
|
|
634
|
+
<circle cx="${r}" cy="${r}" r="${r}" fill="#ef4444"/>
|
|
635
|
+
<text x="${r}" y="${r}" font-size="${fontSize}" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="central" font-family="sans-serif">D</text>
|
|
636
|
+
</svg>`
|
|
637
|
+
|
|
638
|
+
const badgePng = await sharp(Buffer.from(badgeSvg)).png().toBuffer()
|
|
639
|
+
|
|
640
|
+
return await (sharp(Buffer.from(pngBuffer)) as any)
|
|
641
|
+
.composite([{
|
|
642
|
+
input: badgePng,
|
|
643
|
+
gravity: 'southeast',
|
|
644
|
+
}])
|
|
645
|
+
.png()
|
|
646
|
+
.toBuffer()
|
|
647
|
+
} catch {
|
|
648
|
+
// sharp not available — return original
|
|
649
|
+
return pngBuffer
|
|
650
|
+
}
|
|
651
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,46 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
filePathToUrlPath,
|
|
17
|
-
generateMiddlewareModule,
|
|
18
|
-
generateRouteModule,
|
|
19
|
-
parseFileRoutes,
|
|
20
|
-
scanRouteFiles,
|
|
21
|
-
} from './fs-router'
|
|
22
|
-
|
|
23
|
-
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
export { defineConfig, resolveConfig } from "./config";
|
|
26
|
-
|
|
27
|
-
// ─── ISR ─────────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export { createISRHandler } from "./isr";
|
|
30
|
-
|
|
31
|
-
// ─── Adapters ────────────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
export {
|
|
34
|
-
bunAdapter,
|
|
35
|
-
cloudflareAdapter,
|
|
36
|
-
netlifyAdapter,
|
|
37
|
-
nodeAdapter,
|
|
38
|
-
resolveAdapter,
|
|
39
|
-
staticAdapter,
|
|
40
|
-
vercelAdapter,
|
|
41
|
-
} from "./adapters";
|
|
42
|
-
|
|
43
|
-
// ─── Components ─────────────────────────────────────────────────────────────
|
|
1
|
+
/**
|
|
2
|
+
* @pyreon/zero — client-safe exports.
|
|
3
|
+
*
|
|
4
|
+
* This entry contains only browser-safe components and hooks.
|
|
5
|
+
* No node:fs, node:path, or other server-only imports.
|
|
6
|
+
*
|
|
7
|
+
* For server/build-time features, use subpath imports:
|
|
8
|
+
* import { faviconPlugin } from "@pyreon/zero/favicon"
|
|
9
|
+
* import { createServer } from "@pyreon/zero/server"
|
|
10
|
+
* import { defineConfig } from "@pyreon/zero/config"
|
|
11
|
+
* import { validateEnv } from "@pyreon/zero/env"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ─── Components (browser-safe) ──────────────────────────────────────────────
|
|
44
15
|
|
|
45
16
|
export type { ImageProps, ImageSource } from "./image";
|
|
46
17
|
export { Image } from "./image";
|
|
@@ -48,41 +19,10 @@ export type { LinkProps, LinkRenderProps, UseLinkReturn } from "./link";
|
|
|
48
19
|
export { createLink, Link, prefetchRoute, useLink } from "./link";
|
|
49
20
|
export type { ScriptProps, ScriptStrategy } from "./script";
|
|
50
21
|
export { Script } from "./script";
|
|
22
|
+
export type { MetaProps } from "./meta";
|
|
23
|
+
export { buildMetaTags, Meta } from "./meta";
|
|
51
24
|
|
|
52
|
-
// ───
|
|
53
|
-
|
|
54
|
-
export { render404Page } from "./not-found";
|
|
55
|
-
|
|
56
|
-
// ─── Middleware ──────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
export type { CacheConfig, CacheRule } from "./cache";
|
|
59
|
-
export { cacheMiddleware, securityHeaders, varyEncoding } from "./cache";
|
|
60
|
-
export { compose, getContext } from "./middleware";
|
|
61
|
-
|
|
62
|
-
// ─── Font optimization ─────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
export type {
|
|
65
|
-
FallbackMetrics,
|
|
66
|
-
FontConfig,
|
|
67
|
-
FontDisplay,
|
|
68
|
-
GoogleFontInput,
|
|
69
|
-
GoogleFontStatic,
|
|
70
|
-
GoogleFontVariable,
|
|
71
|
-
LocalFont,
|
|
72
|
-
} from "./font";
|
|
73
|
-
export { fontPlugin, fontVariables } from "./font";
|
|
74
|
-
|
|
75
|
-
// ─── Image processing ──────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
export type {
|
|
78
|
-
FormatSource,
|
|
79
|
-
ImageFormat,
|
|
80
|
-
ImagePluginConfig,
|
|
81
|
-
ProcessedImage,
|
|
82
|
-
} from "./image-plugin";
|
|
83
|
-
export { imagePlugin } from "./image-plugin";
|
|
84
|
-
|
|
85
|
-
// ─── Theme ──────────────────────────────────────────────────────────────────
|
|
25
|
+
// ─── Theme (browser-safe) ───────────────────────────────────────────────────
|
|
86
26
|
|
|
87
27
|
export type { Theme } from "./theme";
|
|
88
28
|
export {
|
|
@@ -96,120 +36,45 @@ export {
|
|
|
96
36
|
toggleTheme,
|
|
97
37
|
} from "./theme";
|
|
98
38
|
|
|
99
|
-
// ───
|
|
100
|
-
|
|
101
|
-
export type {
|
|
102
|
-
ChangeFreq,
|
|
103
|
-
JsonLdType,
|
|
104
|
-
RobotsConfig,
|
|
105
|
-
RobotsRule,
|
|
106
|
-
SeoPluginConfig,
|
|
107
|
-
SitemapConfig,
|
|
108
|
-
SitemapEntry,
|
|
109
|
-
} from "./seo";
|
|
110
|
-
export {
|
|
111
|
-
generateRobots,
|
|
112
|
-
generateSitemap,
|
|
113
|
-
jsonLd,
|
|
114
|
-
seoMiddleware,
|
|
115
|
-
seoPlugin,
|
|
116
|
-
} from "./seo";
|
|
117
|
-
|
|
118
|
-
// ─── API routes ──────────────────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
export type {
|
|
121
|
-
ApiContext,
|
|
122
|
-
ApiHandler,
|
|
123
|
-
ApiRouteEntry,
|
|
124
|
-
ApiRouteModule,
|
|
125
|
-
HttpMethod,
|
|
126
|
-
} from "./api-routes";
|
|
127
|
-
export { createApiMiddleware, generateApiRouteModule } from "./api-routes";
|
|
128
|
-
|
|
129
|
-
// ─── CORS ────────────────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
export type { CorsConfig } from "./cors";
|
|
132
|
-
export { corsMiddleware } from "./cors";
|
|
133
|
-
|
|
134
|
-
// ─── Rate limiting ──────────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
export type { RateLimitConfig } from "./rate-limit";
|
|
137
|
-
export { rateLimitMiddleware } from "./rate-limit";
|
|
138
|
-
|
|
139
|
-
// ─── Compression ────────────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
export type { CompressionConfig } from "./compression";
|
|
142
|
-
export {
|
|
143
|
-
compressionMiddleware,
|
|
144
|
-
compressResponse,
|
|
145
|
-
isCompressible,
|
|
146
|
-
} from "./compression";
|
|
147
|
-
|
|
148
|
-
// ─── Actions ─────────────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
export type { Action, ActionContext, ActionHandler } from "./actions";
|
|
151
|
-
export { createActionMiddleware, defineAction } from "./actions";
|
|
152
|
-
|
|
153
|
-
// ─── Favicon ────────────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
export type { FaviconLocaleConfig, FaviconPluginConfig } from "./favicon";
|
|
156
|
-
export { faviconLinks, faviconPlugin } from "./favicon";
|
|
157
|
-
|
|
158
|
-
// ─── OG Image ───────────────────────────────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
export type {
|
|
161
|
-
OgImageLayer,
|
|
162
|
-
OgImagePluginConfig,
|
|
163
|
-
OgImageTemplate,
|
|
164
|
-
} from "./og-image";
|
|
165
|
-
export { ogImagePath, ogImagePlugin } from "./og-image";
|
|
166
|
-
|
|
167
|
-
// ─── Meta ───────────────────────────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
export type { MetaProps } from "./meta";
|
|
170
|
-
export { buildMetaTags, Meta } from "./meta";
|
|
171
|
-
|
|
172
|
-
// ─── I18n routing ───────────────────────────────────────────────────────────
|
|
39
|
+
// ─── I18n hooks (browser-safe) ──────────────────────────────────────────────
|
|
173
40
|
|
|
174
41
|
export type { I18nRoutingConfig, LocaleContext } from "./i18n-routing";
|
|
175
42
|
export {
|
|
176
43
|
buildLocalePath,
|
|
177
|
-
createLocaleContext,
|
|
178
|
-
detectLocaleFromHeader,
|
|
179
44
|
extractLocaleFromPath,
|
|
180
|
-
i18nRouting,
|
|
181
45
|
setLocale,
|
|
182
46
|
useLocale,
|
|
183
47
|
} from "./i18n-routing";
|
|
184
48
|
|
|
185
|
-
// ───
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
export {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
49
|
+
// ─── Server-only stubs ──────────────────────────────────────────────────────
|
|
50
|
+
// Throw clear error messages when developers accidentally import server-only
|
|
51
|
+
// APIs from the main entry. These are tree-shaken if not imported.
|
|
52
|
+
|
|
53
|
+
function serverOnly(name: string, subpath: string): never {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`[Pyreon] "${name}" is server-only and cannot be imported from "@pyreon/zero".\n` +
|
|
56
|
+
`Import from the subpath instead:\n\n` +
|
|
57
|
+
` import { ${name} } from "@pyreon/zero/${subpath}"\n`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
62
|
+
/** @deprecated Import from `@pyreon/zero/favicon` instead */
|
|
63
|
+
export function faviconPlugin(..._: unknown[]): never { return serverOnly('faviconPlugin', 'favicon') }
|
|
64
|
+
/** @deprecated Import from `@pyreon/zero/seo` instead */
|
|
65
|
+
export function seoPlugin(..._: unknown[]): never { return serverOnly('seoPlugin', 'seo') }
|
|
66
|
+
/** @deprecated Import from `@pyreon/zero/server` instead */
|
|
67
|
+
export function createServer(..._: unknown[]): never { return serverOnly('createServer', 'server') }
|
|
68
|
+
/** @deprecated Import from `@pyreon/zero/config` instead */
|
|
69
|
+
export function defineConfig(..._: unknown[]): never { return serverOnly('defineConfig', 'config') }
|
|
70
|
+
/** @deprecated Import from `@pyreon/zero/env` instead */
|
|
71
|
+
export function validateEnv(..._: unknown[]): never { return serverOnly('validateEnv', 'env') }
|
|
72
|
+
/** @deprecated Import from `@pyreon/zero/og-image` instead */
|
|
73
|
+
export function ogImagePlugin(..._: unknown[]): never { return serverOnly('ogImagePlugin', 'og-image') }
|
|
74
|
+
/** @deprecated Import from `@pyreon/zero/ai` instead */
|
|
75
|
+
export function aiPlugin(..._: unknown[]): never { return serverOnly('aiPlugin', 'ai') }
|
|
76
|
+
|
|
77
|
+
// ─── Types (no runtime, safe everywhere) ────────────────────────────────────
|
|
213
78
|
|
|
214
79
|
export type {
|
|
215
80
|
Adapter,
|
package/src/meta.tsx
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
import type { VNodeChild } from '@pyreon/core'
|
|
2
2
|
import type { UseHeadInput } from '@pyreon/head'
|
|
3
3
|
import { useHead } from '@pyreon/head'
|
|
4
|
-
import type { FaviconPluginConfig } from './favicon'
|
|
5
|
-
import { faviconLinks } from './favicon'
|
|
6
4
|
import type { I18nRoutingConfig } from './i18n-routing'
|
|
7
5
|
import { extractLocaleFromPath } from './i18n-routing'
|
|
8
|
-
|
|
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
|
+
}
|
|
9
43
|
|
|
10
44
|
// ─── Meta component ────────────────────────────────────────────────────────
|
|
11
45
|
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pyreon/zero/server — server-only exports.
|
|
3
|
+
*
|
|
4
|
+
* Import from this subpath for SSR, middleware, adapters, and build tools.
|
|
5
|
+
* These modules use node:fs, node:path, etc. and must NOT be imported
|
|
6
|
+
* in client-side code.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createServer, createApp } from "@pyreon/zero/server"
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ─── Server entry ───────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export type { CreateAppOptions } from "./app";
|
|
17
|
+
export { createApp } from "./app";
|
|
18
|
+
export type { CreateServerOptions } from "./entry-server";
|
|
19
|
+
export { createServer } from "./entry-server";
|
|
20
|
+
|
|
21
|
+
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export { defineConfig, resolveConfig } from "./config";
|
|
24
|
+
|
|
25
|
+
// ─── File-system routing ────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export type { GenerateRouteModuleOptions } from './fs-router'
|
|
28
|
+
export {
|
|
29
|
+
filePathToUrlPath,
|
|
30
|
+
generateMiddlewareModule,
|
|
31
|
+
generateRouteModule,
|
|
32
|
+
parseFileRoutes,
|
|
33
|
+
scanRouteFiles,
|
|
34
|
+
} from './fs-router'
|
|
35
|
+
|
|
36
|
+
// ─── ISR ────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export { createISRHandler } from "./isr";
|
|
39
|
+
|
|
40
|
+
// ─── Adapters ───────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
bunAdapter,
|
|
44
|
+
cloudflareAdapter,
|
|
45
|
+
netlifyAdapter,
|
|
46
|
+
nodeAdapter,
|
|
47
|
+
resolveAdapter,
|
|
48
|
+
staticAdapter,
|
|
49
|
+
vercelAdapter,
|
|
50
|
+
} from "./adapters";
|
|
51
|
+
|
|
52
|
+
// ─── 404 ────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export { render404Page } from "./not-found";
|
|
55
|
+
|
|
56
|
+
// ─── Middleware ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export { compose, getContext } from "./middleware";
|
|
59
|
+
|
|
60
|
+
// ─── Vite plugin ────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export { zeroPlugin as default } from "./vite-plugin";
|
|
63
|
+
|
|
64
|
+
// ─── I18n server-only ───────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
createLocaleContext,
|
|
68
|
+
detectLocaleFromHeader,
|
|
69
|
+
i18nRouting,
|
|
70
|
+
} from "./i18n-routing";
|