@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/ai.ts DELETED
@@ -1,623 +0,0 @@
1
- /**
2
- * AI integration utilities for Zero.
3
- *
4
- * - llms.txt auto-generation from routes and API routes
5
- * - JSON-LD auto-inference from route meta + Meta props
6
- * - AI plugin manifest (/.well-known/ai-plugin.json) from API routes
7
- *
8
- * @example
9
- * ```ts
10
- * // vite.config.ts
11
- * import { aiPlugin } from "@pyreon/zero/ai"
12
- *
13
- * export default {
14
- * plugins: [
15
- * aiPlugin({
16
- * name: "My App",
17
- * origin: "https://example.com",
18
- * description: "A modern web application",
19
- * }),
20
- * ],
21
- * }
22
- * ```
23
- */
24
- import type { Plugin } from 'vite'
25
- import { parseFileRoutes } from './fs-router'
26
-
27
- // ─── Types ──────────────────────────────────────────────────────────────────
28
-
29
- export interface AiPluginConfig {
30
- /** App/API name. */
31
- name: string
32
- /** App description for AI agents. */
33
- description: string
34
- /** Base URL. e.g. "https://example.com" */
35
- origin: string
36
- /** Contact email (required by OpenAI plugin spec). */
37
- contactEmail?: string
38
- /** Legal info URL. */
39
- legalUrl?: string
40
- /** Logo URL for the plugin. */
41
- logoUrl?: string
42
- /** Routes directory relative to project root. Default: "src/routes" */
43
- routesDir?: string
44
- /** API routes directory relative to project root. Default: "src/api" */
45
- apiDir?: string
46
- /**
47
- * API route descriptions — map of pattern to description.
48
- * Used for llms.txt and ai-plugin.json.
49
- *
50
- * @example
51
- * ```ts
52
- * apiDescriptions: {
53
- * "GET /api/posts": "List all blog posts, supports ?page=N&limit=N",
54
- * "GET /api/posts/:id": "Get a single post by ID",
55
- * "POST /api/posts": "Create a new post (requires auth)",
56
- * }
57
- * ```
58
- */
59
- apiDescriptions?: Record<string, string>
60
- /**
61
- * Page descriptions — map of URL path to description.
62
- * Used for llms.txt. Falls back to route meta.title/description.
63
- */
64
- pageDescriptions?: Record<string, string>
65
- /**
66
- * Additional content to append to llms.txt.
67
- * Useful for authentication instructions, rate limits, etc.
68
- */
69
- llmsExtra?: string
70
- }
71
-
72
- // ─── llms.txt generation ────────────────────────────────────────────────────
73
-
74
- /**
75
- * Generate llms.txt content from route files and config.
76
- *
77
- * Format follows the llms.txt proposal:
78
- * ```
79
- * # {name}
80
- * > {description}
81
- *
82
- * ## Pages
83
- * - [/about](/about): About page
84
- *
85
- * ## API
86
- * - GET /api/posts: List posts
87
- * ```
88
- *
89
- * @internal Exported for testing.
90
- */
91
- export function generateLlmsTxt(
92
- routeFiles: string[],
93
- apiFiles: string[],
94
- config: AiPluginConfig,
95
- ): string {
96
- const lines: string[] = []
97
-
98
- // Header
99
- lines.push(`# ${config.name}`)
100
- lines.push(`> ${config.description}`)
101
- lines.push('')
102
-
103
- // Pages section
104
- const routes = parseFileRoutes(routeFiles)
105
- const pages = routes.filter(
106
- (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound
107
- && !r.isCatchAll && !r.urlPath.includes(':'),
108
- )
109
-
110
- if (pages.length > 0) {
111
- lines.push('## Pages')
112
- lines.push('')
113
- for (const page of pages) {
114
- const desc = config.pageDescriptions?.[page.urlPath]
115
- const url = `${config.origin}${page.urlPath === '/' ? '' : page.urlPath}`
116
- if (desc) {
117
- lines.push(`- [${page.urlPath}](${url}): ${desc}`)
118
- } else {
119
- lines.push(`- [${page.urlPath}](${url})`)
120
- }
121
- }
122
- lines.push('')
123
- }
124
-
125
- // Dynamic routes (documented separately — AI needs to know about params)
126
- const dynamicRoutes = routes.filter(
127
- (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound
128
- && (r.urlPath.includes(':') || r.isCatchAll),
129
- )
130
- if (dynamicRoutes.length > 0) {
131
- lines.push('## Dynamic Pages')
132
- lines.push('')
133
- for (const route of dynamicRoutes) {
134
- const desc = config.pageDescriptions?.[route.urlPath]
135
- if (desc) {
136
- lines.push(`- ${route.urlPath}: ${desc}`)
137
- } else {
138
- lines.push(`- ${route.urlPath}`)
139
- }
140
- }
141
- lines.push('')
142
- }
143
-
144
- // API section
145
- const apiPatterns = parseApiFiles(apiFiles)
146
- if (apiPatterns.length > 0 || config.apiDescriptions) {
147
- lines.push('## API Endpoints')
148
- lines.push('')
149
-
150
- // From apiDescriptions (most detailed — user-provided)
151
- if (config.apiDescriptions) {
152
- for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {
153
- lines.push(`- ${endpoint}: ${desc}`)
154
- }
155
- }
156
-
157
- // From auto-discovered API files (only those not already described)
158
- const describedPatterns = new Set(
159
- Object.keys(config.apiDescriptions ?? {}).map((k) => k.replace(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+/, '')),
160
- )
161
- for (const pattern of apiPatterns) {
162
- if (!describedPatterns.has(pattern)) {
163
- lines.push(`- ${pattern}`)
164
- }
165
- }
166
- lines.push('')
167
- }
168
-
169
- // Extra content
170
- if (config.llmsExtra) {
171
- lines.push(config.llmsExtra)
172
- lines.push('')
173
- }
174
-
175
- return lines.join('\n')
176
- }
177
-
178
- /**
179
- * Generate llms-full.txt — expanded version with more detail.
180
- * Includes all route metadata and API descriptions.
181
- *
182
- * @internal Exported for testing.
183
- */
184
- export function generateLlmsFullTxt(
185
- routeFiles: string[],
186
- apiFiles: string[],
187
- config: AiPluginConfig,
188
- ): string {
189
- const lines: string[] = []
190
-
191
- lines.push(`# ${config.name} — Full Reference`)
192
- lines.push(`> ${config.description}`)
193
- lines.push('')
194
- lines.push(`Base URL: ${config.origin}`)
195
- lines.push('')
196
-
197
- // All pages with details
198
- const routes = parseFileRoutes(routeFiles)
199
- const pages = routes.filter(
200
- (r) => !r.isLayout && !r.isError && !r.isLoading && !r.isNotFound,
201
- )
202
-
203
- if (pages.length > 0) {
204
- lines.push('## All Routes')
205
- lines.push('')
206
- for (const page of pages) {
207
- const desc = config.pageDescriptions?.[page.urlPath] ?? ''
208
- const dynamic = page.urlPath.includes(':') ? ' (dynamic)' : ''
209
- const catchAll = page.isCatchAll ? ' (catch-all)' : ''
210
- lines.push(`### ${page.urlPath}${dynamic}${catchAll}`)
211
- if (desc) lines.push(desc)
212
- lines.push(`- File: ${page.filePath}`)
213
- lines.push(`- Render mode: ${page.renderMode}`)
214
- lines.push('')
215
- }
216
- }
217
-
218
- // API endpoints with full detail
219
- if (config.apiDescriptions) {
220
- lines.push('## API Reference')
221
- lines.push('')
222
- for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {
223
- lines.push(`### ${endpoint}`)
224
- lines.push(desc)
225
- lines.push('')
226
- }
227
- }
228
-
229
- if (config.llmsExtra) {
230
- lines.push('## Additional Information')
231
- lines.push('')
232
- lines.push(config.llmsExtra)
233
- lines.push('')
234
- }
235
-
236
- return lines.join('\n')
237
- }
238
-
239
- // ─── JSON-LD auto-inference ─────────────────────────────────────────────────
240
-
241
- export interface InferJsonLdOptions {
242
- /** Page URL. */
243
- url: string
244
- /** Page title. */
245
- title?: string
246
- /** Page description. */
247
- description?: string
248
- /** Page image. */
249
- image?: string
250
- /** Site name. */
251
- siteName?: string
252
- /** Page type hint. */
253
- type?: 'website' | 'article' | 'product' | 'profile'
254
- /** Article metadata. */
255
- publishedTime?: string
256
- /** Article author. */
257
- author?: string
258
- /** Article tags. */
259
- tags?: string[]
260
- /** Breadcrumb path segments. */
261
- breadcrumbs?: Array<{ name: string; url: string }>
262
- }
263
-
264
- /**
265
- * Auto-infer JSON-LD structured data from page metadata.
266
- *
267
- * Returns an array of JSON-LD objects (multiple schemas can apply to one page).
268
- * For example, an article page gets both `Article` and `BreadcrumbList`.
269
- *
270
- * @example
271
- * ```tsx
272
- * const schemas = inferJsonLd({
273
- * url: "https://example.com/blog/my-post",
274
- * title: "My Post",
275
- * description: "A great article",
276
- * type: "article",
277
- * author: "Vit Bokisch",
278
- * publishedTime: "2026-03-31",
279
- * })
280
- * // → [Article schema, BreadcrumbList schema]
281
- * ```
282
- */
283
- export function inferJsonLd(options: InferJsonLdOptions): Record<string, unknown>[] {
284
- const schemas: Record<string, unknown>[] = []
285
-
286
- // Base: WebPage or Article
287
- if (options.type === 'article') {
288
- const article: Record<string, unknown> = {
289
- '@context': 'https://schema.org',
290
- '@type': 'Article',
291
- headline: options.title,
292
- url: options.url,
293
- }
294
- if (options.description) article.description = options.description
295
- if (options.image) article.image = options.image
296
- if (options.publishedTime) article.datePublished = options.publishedTime
297
- if (options.author) {
298
- article.author = { '@type': 'Person', name: options.author }
299
- }
300
- if (options.tags && options.tags.length > 0) {
301
- article.keywords = options.tags.join(', ')
302
- }
303
- if (options.siteName) {
304
- article.publisher = { '@type': 'Organization', name: options.siteName }
305
- }
306
- schemas.push(article)
307
- } else if (options.type === 'product') {
308
- const product: Record<string, unknown> = {
309
- '@context': 'https://schema.org',
310
- '@type': 'Product',
311
- name: options.title,
312
- url: options.url,
313
- }
314
- if (options.description) product.description = options.description
315
- if (options.image) product.image = options.image
316
- schemas.push(product)
317
- } else {
318
- const webpage: Record<string, unknown> = {
319
- '@context': 'https://schema.org',
320
- '@type': 'WebPage',
321
- name: options.title,
322
- url: options.url,
323
- }
324
- if (options.description) webpage.description = options.description
325
- if (options.image) webpage.thumbnailUrl = options.image
326
- schemas.push(webpage)
327
- }
328
-
329
- // BreadcrumbList from URL path or explicit breadcrumbs
330
- if (options.breadcrumbs && options.breadcrumbs.length > 0) {
331
- schemas.push({
332
- '@context': 'https://schema.org',
333
- '@type': 'BreadcrumbList',
334
- itemListElement: options.breadcrumbs.map((bc, i) => ({
335
- '@type': 'ListItem',
336
- position: i + 1,
337
- name: bc.name,
338
- item: bc.url,
339
- })),
340
- })
341
- } else {
342
- // Auto-generate breadcrumbs from URL path
343
- const urlObj = safeParseUrl(options.url)
344
- if (urlObj) {
345
- const segments = urlObj.pathname.split('/').filter(Boolean)
346
- if (segments.length > 0) {
347
- const items = [
348
- { '@type': 'ListItem', position: 1, name: 'Home', item: urlObj.origin },
349
- ]
350
- let path = ''
351
- for (let i = 0; i < segments.length; i++) {
352
- path += `/${segments[i]}`
353
- items.push({
354
- '@type': 'ListItem',
355
- position: i + 2,
356
- name: capitalize(segments[i]!.replace(/-/g, ' ')),
357
- item: `${urlObj.origin}${path}`,
358
- })
359
- }
360
- schemas.push({
361
- '@context': 'https://schema.org',
362
- '@type': 'BreadcrumbList',
363
- itemListElement: items,
364
- })
365
- }
366
- }
367
- }
368
-
369
- return schemas
370
- }
371
-
372
- // ─── AI plugin manifest ─────────────────────────────────────────────────────
373
-
374
- /**
375
- * Generate an OpenAI-compatible AI plugin manifest.
376
- *
377
- * Follows the /.well-known/ai-plugin.json spec.
378
- *
379
- * @internal Exported for testing.
380
- */
381
- export function generateAiPluginManifest(config: AiPluginConfig): Record<string, unknown> {
382
- return {
383
- schema_version: 'v1',
384
- name_for_human: config.name,
385
- name_for_model: config.name.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, ''),
386
- description_for_human: config.description,
387
- description_for_model: config.description,
388
- auth: { type: 'none' },
389
- api: {
390
- type: 'openapi',
391
- url: `${config.origin}/.well-known/openapi.yaml`,
392
- },
393
- logo_url: config.logoUrl ?? `${config.origin}/favicon.svg`,
394
- contact_email: config.contactEmail ?? '',
395
- legal_info_url: config.legalUrl ?? `${config.origin}/legal`,
396
- }
397
- }
398
-
399
- /**
400
- * Generate a minimal OpenAPI 3.0 spec from API route descriptions.
401
- *
402
- * @internal Exported for testing.
403
- */
404
- export function generateOpenApiSpec(
405
- apiFiles: string[],
406
- config: AiPluginConfig,
407
- ): Record<string, unknown> {
408
- const paths: Record<string, Record<string, unknown>> = {}
409
-
410
- // From user-provided descriptions
411
- if (config.apiDescriptions) {
412
- for (const [endpoint, desc] of Object.entries(config.apiDescriptions)) {
413
- const match = endpoint.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(.+)$/)
414
- if (match) {
415
- const method = match[1]!.toLowerCase()
416
- const path = match[2]!
417
- // Convert :param to {param} for OpenAPI
418
- const openApiPath = path.replace(/:(\w+)/g, '{$1}')
419
- if (!paths[openApiPath]) paths[openApiPath] = {}
420
- paths[openApiPath][method] = {
421
- summary: desc,
422
- responses: { '200': { description: 'Success' } },
423
- }
424
- }
425
- }
426
- }
427
-
428
- // Auto-discovered API files (fill in gaps)
429
- for (const pattern of parseApiFiles(apiFiles)) {
430
- const openApiPath = pattern.replace(/:(\w+)/g, '{$1}')
431
- if (!paths[openApiPath]) {
432
- paths[openApiPath] = {
433
- get: {
434
- summary: `${openApiPath} endpoint`,
435
- responses: { '200': { description: 'Success' } },
436
- },
437
- }
438
- }
439
- }
440
-
441
- return {
442
- openapi: '3.0.0',
443
- info: {
444
- title: config.name,
445
- description: config.description,
446
- version: '1.0.0',
447
- },
448
- servers: [{ url: config.origin }],
449
- paths,
450
- }
451
- }
452
-
453
- // ─── Vite plugin ────────────────────────────────────────────────────────────
454
-
455
- /**
456
- * AI integration Vite plugin.
457
- *
458
- * Generates at build time:
459
- * - `/llms.txt` — concise site summary for AI agents
460
- * - `/llms-full.txt` — detailed reference for AI agents
461
- * - `/.well-known/ai-plugin.json` — OpenAI plugin manifest
462
- * - `/.well-known/openapi.yaml` — minimal OpenAPI spec from API routes
463
- *
464
- * In dev, serves these files via middleware.
465
- *
466
- * @example
467
- * ```ts
468
- * import { aiPlugin } from "@pyreon/zero/ai"
469
- *
470
- * export default {
471
- * plugins: [
472
- * aiPlugin({
473
- * name: "My App",
474
- * origin: "https://example.com",
475
- * description: "A modern web application",
476
- * apiDescriptions: {
477
- * "GET /api/posts": "List blog posts",
478
- * "GET /api/posts/:id": "Get post by ID",
479
- * },
480
- * }),
481
- * ],
482
- * }
483
- * ```
484
- */
485
- export function aiPlugin(config: AiPluginConfig): Plugin {
486
- let root = ''
487
- let isBuild = false
488
- let routeFiles: string[] = []
489
- let apiFiles: string[] = []
490
-
491
- return {
492
- name: 'pyreon-zero-ai',
493
- enforce: 'post',
494
-
495
- configResolved(resolvedConfig) {
496
- root = resolvedConfig.root
497
- isBuild = resolvedConfig.command === 'build'
498
- },
499
-
500
- async buildStart() {
501
- // Scan for route and API files
502
- try {
503
- const { join } = await import('node:path')
504
-
505
- const routesDir = join(root, config.routesDir ?? 'src/routes')
506
- const apiDir = join(root, config.apiDir ?? 'src/api')
507
-
508
- routeFiles = await scanDir(routesDir, routesDir)
509
- apiFiles = await scanDir(apiDir, apiDir)
510
- } catch {
511
- // Directories may not exist
512
- }
513
- },
514
-
515
- configureServer(server) {
516
- server.middlewares.use(async (req, res, next) => {
517
- const url = req.url ?? ''
518
-
519
- if (url === '/llms.txt') {
520
- res.setHeader('Content-Type', 'text/plain; charset=utf-8')
521
- res.end(generateLlmsTxt(routeFiles, apiFiles, config))
522
- return
523
- }
524
-
525
- if (url === '/llms-full.txt') {
526
- res.setHeader('Content-Type', 'text/plain; charset=utf-8')
527
- res.end(generateLlmsFullTxt(routeFiles, apiFiles, config))
528
- return
529
- }
530
-
531
- if (url === '/.well-known/ai-plugin.json') {
532
- res.setHeader('Content-Type', 'application/json')
533
- res.end(JSON.stringify(generateAiPluginManifest(config), null, 2))
534
- return
535
- }
536
-
537
- if (url === '/.well-known/openapi.yaml' || url === '/.well-known/openapi.json') {
538
- res.setHeader('Content-Type', 'application/json')
539
- res.end(JSON.stringify(generateOpenApiSpec(apiFiles, config), null, 2))
540
- return
541
- }
542
-
543
- next()
544
- })
545
- },
546
-
547
- async generateBundle() {
548
- if (!isBuild) return
549
-
550
- this.emitFile({
551
- type: 'asset',
552
- fileName: 'llms.txt',
553
- source: generateLlmsTxt(routeFiles, apiFiles, config),
554
- })
555
-
556
- this.emitFile({
557
- type: 'asset',
558
- fileName: 'llms-full.txt',
559
- source: generateLlmsFullTxt(routeFiles, apiFiles, config),
560
- })
561
-
562
- this.emitFile({
563
- type: 'asset',
564
- fileName: '.well-known/ai-plugin.json',
565
- source: JSON.stringify(generateAiPluginManifest(config), null, 2),
566
- })
567
-
568
- this.emitFile({
569
- type: 'asset',
570
- fileName: '.well-known/openapi.json',
571
- source: JSON.stringify(generateOpenApiSpec(apiFiles, config), null, 2),
572
- })
573
- },
574
- }
575
- }
576
-
577
- // ─── Helpers ────────────────────────────────────────────────────────────────
578
-
579
- function parseApiFiles(files: string[]): string[] {
580
- return files
581
- .filter((f) => f.endsWith('.ts') || f.endsWith('.js'))
582
- .map((f) => {
583
- let path = f.replace(/\.\w+$/, '').replace(/\/index$/, '')
584
- if (!path.startsWith('/')) path = `/${path}`
585
- // Convert [param] to :param
586
- path = path.replace(/\[\.\.\.(\w+)\]/g, ':$1*').replace(/\[(\w+)\]/g, ':$1')
587
- return `/api${path === '/' ? '' : path}`
588
- })
589
- }
590
-
591
- async function scanDir(dir: string, base: string): Promise<string[]> {
592
- const { readdir, stat } = await import('node:fs/promises')
593
- const { join, relative } = await import('node:path')
594
-
595
- try {
596
- const entries = await readdir(dir)
597
- const files: string[] = []
598
- for (const entry of entries) {
599
- const full = join(dir, entry)
600
- const s = await stat(full)
601
- if (s.isDirectory()) {
602
- files.push(...(await scanDir(full, base)))
603
- } else {
604
- files.push(relative(base, full))
605
- }
606
- }
607
- return files
608
- } catch {
609
- return []
610
- }
611
- }
612
-
613
- function safeParseUrl(url: string): URL | null {
614
- try {
615
- return new URL(url)
616
- } catch {
617
- return null
618
- }
619
- }
620
-
621
- function capitalize(s: string): string {
622
- return s.charAt(0).toUpperCase() + s.slice(1)
623
- }