@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.
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/types.ts DELETED
@@ -1,624 +0,0 @@
1
- import type { ComponentFn } from '@pyreon/core'
2
- import type { LoaderContext, NavigationGuard } from '@pyreon/router'
3
- import type { Middleware } from '@pyreon/server'
4
- import type { I18nRoutingConfig } from './i18n-routing'
5
-
6
- // Re-export router's `LoaderContext` so consumers importing it from
7
- // `@pyreon/zero` keep working. The previous duplicate `interface
8
- // LoaderContext` (with a `request: Request` field that was never
9
- // populated by the actually-constructed runtime ctx) was a
10
- // typed-but-unimplemented bug class — caught by `audit-types`. If
11
- // SSR loaders need access to the request, plumb it through the
12
- // router-level `LoaderContext` in a follow-up PR; do NOT add fields
13
- // here that the runtime doesn't populate.
14
- export type { LoaderContext }
15
-
16
- // ─── Route module conventions ────────────────────────────────────────────────
17
-
18
- /** What a route file (e.g. `src/routes/index.tsx`) can export. */
19
- export interface RouteModule {
20
- /** Default export is the page component. */
21
- default?: ComponentFn
22
- /** Layout wrapper — wraps this route and all children. */
23
- layout?: ComponentFn
24
- /** Loading component shown while lazy-loading or during Suspense. */
25
- loading?: ComponentFn
26
- /** Error component shown when the route errors. */
27
- error?: ComponentFn
28
- /** Server-side data loader. */
29
- loader?: (ctx: LoaderContext) => Promise<unknown>
30
- /** Per-route middleware. */
31
- middleware?: Middleware | Middleware[]
32
- /** Navigation guard — can redirect or block navigation. */
33
- guard?: NavigationGuard
34
- /** Route metadata. */
35
- meta?: RouteMeta
36
- /** Rendering mode override for this route. */
37
- renderMode?: RenderMode
38
- }
39
-
40
- /** Per-route metadata. */
41
- export interface RouteMeta {
42
- title?: string
43
- description?: string
44
- [key: string]: unknown
45
- }
46
-
47
- // ─── Rendering modes ─────────────────────────────────────────────────────────
48
-
49
- export type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr'
50
-
51
- export interface ISRConfig {
52
- /** Revalidation interval in seconds. */
53
- revalidate: number
54
- /**
55
- * Maximum number of distinct URL paths to keep in the in-memory cache.
56
- * Oldest-first LRU eviction once the cap is reached. Default: `1000`.
57
- * Set higher for SSG-heavy sites, lower for routes with unbounded URL
58
- * space (e.g. `/user/:id` where `:id` is free-form).
59
- */
60
- maxEntries?: number
61
- /**
62
- * Max wall-time (ms) for a single background revalidation before it is
63
- * abandoned. Without a bound, a handler that hangs leaves its key
64
- * pinned in the in-flight set forever — every later request for that
65
- * key short-circuits the de-dupe guard and the entry can never
66
- * recover from stale. Default: `30000` (matches the Suspense
67
- * streaming timeout).
68
- */
69
- revalidateTimeoutMs?: number
70
- /**
71
- * Cache-key derivation function. The default keys cache entries by
72
- * `url.pathname` ONLY — query strings, cookies, and headers are
73
- * stripped.
74
- *
75
- * **⚠️ Auth-gated incompatibility.** The default behavior is
76
- * unsafe for request-dependent loaders. A loader that reads
77
- * `request.headers.get('cookie')` to gate auth will render ONCE
78
- * with the first user's cookie, then serve that HTML to every
79
- * subsequent user. To use ISR with personalized / auth-gated
80
- * pages, supply a `cacheKey` that varies on the auth identifier
81
- * (session cookie, user-id header, etc.), OR don't use ISR for
82
- * such routes — use SSR instead.
83
- *
84
- * @example
85
- * // Vary cache by session cookie:
86
- * isr: {
87
- * revalidate: 60,
88
- * cacheKey: (req) => {
89
- * const url = new URL(req.url)
90
- * const session = req.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] ?? 'anon'
91
- * return `${url.pathname}::${session}`
92
- * },
93
- * }
94
- *
95
- * @example
96
- * // Vary by a query parameter:
97
- * isr: {
98
- * revalidate: 60,
99
- * cacheKey: (req) => {
100
- * const url = new URL(req.url)
101
- * return `${url.pathname}?sort=${url.searchParams.get('sort') ?? ''}`
102
- * },
103
- * }
104
- */
105
- cacheKey?: (req: Request) => string
106
- /**
107
- * Pluggable cache backing for multi-instance / horizontally-scaled
108
- * production. Default: in-memory `Map` per-process (capped by
109
- * `maxEntries`). Pass a Redis / Vercel KV / Cloudflare KV / Upstash
110
- * adapter (anything matching the `ISRStore` interface from
111
- * `@pyreon/zero/server`) for state shared across instances — a
112
- * revalidation in one pod is visible to all pods.
113
- *
114
- * The store interface accepts BOTH sync and async returns; the
115
- * handler `await`s the result either way, so an in-memory store
116
- * stays cheap (no Promise allocation per request) while a Redis
117
- * store can return its native promises directly.
118
- *
119
- * When set, `maxEntries` is ignored — the custom store owns its own
120
- * eviction / TTL policy.
121
- *
122
- * @example
123
- * // Redis adapter (uses `ioredis` or `@upstash/redis`):
124
- * const redis = new Redis(...)
125
- * const store: ISRStore = {
126
- * async get(key) {
127
- * const v = await redis.get(`isr:${key}`)
128
- * return v ? JSON.parse(v) : undefined
129
- * },
130
- * async set(key, entry) {
131
- * await redis.set(`isr:${key}`, JSON.stringify(entry), 'EX', 86400)
132
- * },
133
- * async delete(key) {
134
- * await redis.del(`isr:${key}`)
135
- * },
136
- * }
137
- *
138
- * isr: { revalidate: 60, store }
139
- */
140
- // The actual type lives in `./isr` to avoid `types.ts` pulling the
141
- // implementation file; we type it as `unknown` here and let consumers
142
- // pass an `ISRStore` directly — `createISRHandler`'s signature checks
143
- // the shape statically.
144
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
- store?: import('./isr').ISRStore<any>
146
- /**
147
- * Construct the `Request` used for background revalidation. Default:
148
- * the ORIGINAL user's request (headers, method, URL) — which means a
149
- * `cacheKey`-bearing entry triggered by user A is revalidated against
150
- * A's cookies / auth headers. For auth-gated `cacheKey` setups this
151
- * is risky: if A's session expires before the revalidation runs, the
152
- * new render may misbehave (auth-gate hits redirect path, or worse,
153
- * still emits A's personalized HTML because the server hasn't yet
154
- * invalidated the session token).
155
- *
156
- * Supply `revalidateRequest` to construct a request scoped to the
157
- * cache key — e.g. anonymous for anonymous entries, service-account
158
- * for shared entries. Returning `null` SKIPS revalidation entirely
159
- * for this entry (stale stays stale until the next live request).
160
- *
161
- * Compatible with `store`: the revalidate path still reads/writes
162
- * the configured store; this hook only controls what request the
163
- * re-render runs against.
164
- *
165
- * @example
166
- * isr: {
167
- * revalidate: 60,
168
- * cacheKey: (req) => {
169
- * const session = req.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] ?? 'anon'
170
- * return `${new URL(req.url).pathname}::${session}`
171
- * },
172
- * revalidateRequest: (req) => {
173
- * // Anonymous entries re-revalidate as anonymous (safe default).
174
- * // Authenticated entries skip revalidation — the user's next
175
- * // hit will re-render with their current cookies on cache miss.
176
- * const hasAuth = /session=(?!anon)/.test(req.headers.get('cookie') ?? '')
177
- * return hasAuth ? null : new Request(req.url, { method: 'GET' })
178
- * },
179
- * }
180
- */
181
- revalidateRequest?: (req: Request) => Request | null
182
- }
183
-
184
- // ─── Zero config ─────────────────────────────────────────────────────────────
185
-
186
- export interface ZeroConfig {
187
- /** Default rendering mode. Default: "ssr" */
188
- mode?: RenderMode
189
-
190
- /** Vite config overrides. */
191
- vite?: Record<string, unknown>
192
-
193
- /** SSR options. */
194
- ssr?: {
195
- /** Streaming mode. Default: "string" */
196
- mode?: 'string' | 'stream'
197
- }
198
-
199
- /** SSG options — only used when mode is "ssg". */
200
- ssg?: {
201
- /** Paths to prerender (or function returning paths). */
202
- paths?: string[] | (() => string[] | Promise<string[]>)
203
- /**
204
- * Auto-emit `dist/404.html` from the route tree's `_404.tsx` /
205
- * `_not-found.tsx` convention. fs-router already wires `_404.tsx` as
206
- * `notFoundComponent` on its parent layout route; the SSG plugin walks
207
- * the tree, picks up the first one, renders it through the same SSR
208
- * pipeline as regular paths (so styler CSS / @pyreon/head metadata land
209
- * correctly), and writes the result to `dist/404.html`. Static hosts
210
- * (Netlify, Cloudflare Pages, GitHub Pages, S3+CloudFront) serve this
211
- * file automatically for unmatched URLs. Default: `true`. Set to
212
- * `false` to opt out — the route tree is left alone.
213
- */
214
- emit404?: boolean
215
- /**
216
- * When a route loader throws `redirect('/target')` during SSG, write
217
- * a `dist/_redirects` file (Netlify / Cloudflare Pages convention)
218
- * AND a `dist/_redirects.json` (Vercel convention) listing every
219
- * redirected source path → target. Static hosts pick whichever
220
- * format their platform supports automatically. The redirected
221
- * path's HTML file is NOT emitted — the redirect is the response.
222
- *
223
- * Without this option, redirect-throwing loaders land in
224
- * `errors[]` and the path silently disappears from the build —
225
- * the user sees no output for `/old` AND no warning that the
226
- * loader ran a redirect. Default: `true`. Set to `false` to
227
- * restore the pre-PR-B behaviour (redirects treated as errors).
228
- */
229
- emitRedirects?: boolean
230
- /**
231
- * Additionally emit a static HTML file at the source path with a
232
- * `<meta http-equiv="refresh">` redirect — for adapters / hosts
233
- * that don't read `_redirects` (plain S3, GitHub Pages, simple
234
- * file servers). The meta-refresh fallback works on any HTTP
235
- * server that serves static files.
236
- *
237
- * - `'none'` (default): only `_redirects` / `_redirects.json` are
238
- * emitted; no per-redirect HTML file.
239
- * - `'meta-refresh'`: emit `dist/<source>/index.html` containing
240
- * `<meta http-equiv="refresh" content="0; url=<target>">` plus
241
- * a canonical link tag for SEO. Status code information is
242
- * lost (meta-refresh has no status equivalent), so 301/302/307/
243
- * 308 all collapse to "client-side refresh".
244
- */
245
- redirectsAsHtml?: 'none' | 'meta-refresh'
246
- /**
247
- * Callback invoked when a path's render throws (loader-throw that
248
- * isn't a `redirect()`, render exception, anything that lands in the
249
- * `errors[]` collection). Returns either:
250
- * - `string` → written as the path's HTML in place of the failed
251
- * render. Use this to emit a per-path fallback page (e.g. a generic
252
- * "this content is temporarily unavailable" template) so static
253
- * hosts have something to serve at that URL instead of 404'ing.
254
- * - `null` → skip; the path produces no HTML output. The error
255
- * stays in `errors[]` for the post-build summary.
256
- *
257
- * The callback runs ONCE per failed path. Async callbacks are
258
- * awaited. If the callback itself throws, the throw is captured as
259
- * a separate error entry and the path is skipped (no fallback HTML).
260
- * Default: `undefined` — failed paths just land in `errors[]`.
261
- *
262
- * @example
263
- * ssg: {
264
- * onPathError: async (path, error) => {
265
- * console.error(\`SSG render failed for \${path}:\`, error)
266
- * return \`<!DOCTYPE html><html><body><h1>Page unavailable</h1></body></html>\`
267
- * },
268
- * }
269
- */
270
- onPathError?: (
271
- path: string,
272
- error: unknown,
273
- ) => string | null | Promise<string | null>
274
- /**
275
- * When `'json'` (default), write `dist/_pyreon-ssg-errors.json` after
276
- * the render loop summarising every error encountered (path traversal,
277
- * timeout, render exception, getStaticPaths throw, fallback callback
278
- * throw). Each entry has `{ path, message, name, stack }`. The file
279
- * is ONLY written when `errors.length > 0` — successful builds don't
280
- * leak an empty manifest. Reading it lets CI gate on render failures
281
- * without parsing console output (e.g.
282
- * `cat dist/_pyreon-ssg-errors.json | jq '.errors | length' | grep -q 0`).
283
- *
284
- * Set to `'none'` to opt out entirely — errors stay in console-only,
285
- * matching pre-PR-G behaviour.
286
- */
287
- errorArtifact?: 'json' | 'none'
288
- /**
289
- * Maximum number of paths rendered in parallel during the SSG closeBundle
290
- * loop. Default: `4` — a sensible balance between speedup and the risk
291
- * of exhausting downstream resources (DB connection pools, fetch
292
- * rate-limits) inside loaders. Set to `1` to render fully sequentially
293
- * (the pre-PR-D behaviour). Set to a higher value for faster builds
294
- * on CI / multi-core hosts; the practical ceiling is the number of
295
- * loader-side concurrent connections your app's data layer tolerates.
296
- *
297
- * The render-error pipeline (`onPathError` callback, `errors[]`
298
- * collection, `_pyreon-ssg-errors.json` artifact) is unchanged —
299
- * concurrency only affects how many paths are in flight at once,
300
- * not how their successes / failures are recorded.
301
- *
302
- * @example
303
- * ssg: {
304
- * concurrency: 8, // Faster builds for static-content sites
305
- * }
306
- */
307
- concurrency?: number
308
- /**
309
- * Per-path progress callback. Invoked once per path AFTER its render
310
- * settles (success, redirect, OR failure) — never during in-flight
311
- * renders. Receives `{ completed, total, currentPath, elapsed }`
312
- * where:
313
- * - `completed` is the count of paths whose render has settled (1-indexed)
314
- * - `total` is the full path count from `resolvePaths()`
315
- * - `currentPath` is the path that just settled
316
- * - `elapsed` is wall-clock ms since the loop started
317
- *
318
- * Use cases: build-tool progress bars (Vite picks up stdout), CI
319
- * heartbeat lines on long builds (10k-path sites take minutes —
320
- * silent stretches look hung), build-time perf instrumentation.
321
- *
322
- * Async callbacks are awaited before the next path's progress fires,
323
- * so a slow callback can serialize progress reporting (it does NOT
324
- * gate the worker pool — paths keep rendering in parallel; only
325
- * the progress callbacks themselves are serialized). Throws are
326
- * captured into `errors[]` with the path suffix `(onProgress)` so
327
- * a buggy callback can't take down the build.
328
- *
329
- * @example
330
- * ssg: {
331
- * onProgress: ({ completed, total, currentPath, elapsed }) => {
332
- * console.log(`[${completed}/${total}] ${currentPath} (${elapsed}ms)`)
333
- * },
334
- * }
335
- */
336
- onProgress?: (info: {
337
- completed: number
338
- total: number
339
- currentPath: string
340
- elapsed: number
341
- }) => void | Promise<void>
342
- /**
343
- * Route-level code splitting in SSG mode. Default `true`.
344
- *
345
- * When `true` (default), each route file becomes its own dynamic-import
346
- * chunk via `lazy(() => import("..."))` — only the route the user
347
- * lands on plus its dependencies ship in the initial bundle, the
348
- * rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
349
- * has always had; brings parity to SSG.
350
- *
351
- * When `false`, every route is bundled statically into the main
352
- * client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
353
- * sites (2-5 pages) where the single-chunk-then-instant-nav trade
354
- * is preferable — the chunk-fetch cost on navigation is gone, and
355
- * the marginal bytes are negligible.
356
- *
357
- * Crossover point: ~5-8 routes. Below that, single-chunk is fine.
358
- * Above that, lazy() shrinks the initial bundle by a meaningful
359
- * amount (a 50-route docs site might drop from 200 KB to 80 KB on
360
- * first paint).
361
- *
362
- * Underlying mechanism is the same 3-tier generator zero already
363
- * uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
364
- * component + inlined metadata when possible, lazy + lazy-thunked
365
- * function exports when not, namespace-import fallback for cases
366
- * the literal-extractor can't reach.
367
- *
368
- * @example
369
- * ssg: {
370
- * splitChunks: false, // bundle-everything for a 3-page marketing site
371
- * }
372
- */
373
- splitChunks?: boolean
374
- }
375
-
376
- /** ISR config — only used when mode is "isr". */
377
- isr?: ISRConfig
378
-
379
- /**
380
- * Deploy adapter. Default: `"node"`.
381
- *
382
- * Accepts either a built-in adapter name (string) OR a constructed
383
- * `Adapter` instance (e.g. `vercelAdapter()`). The scaffolded templates
384
- * emit the instance form (`adapter: vercelAdapter()`) by convention.
385
- * `resolveAdapter` (see `adapters/index.ts`) accepts both shapes —
386
- * strings go through a switch lookup, instances pass through
387
- * unchanged.
388
- */
389
- adapter?: 'node' | 'bun' | 'static' | 'vercel' | 'cloudflare' | 'netlify' | Adapter
390
-
391
- /** Base URL path. Default: "/" */
392
- base?: string
393
-
394
- /**
395
- * i18n routing — locale-prefixed route variants generated at build time
396
- * (PR H of the SSG roadmap). When set, every `FileRoute` is fanned into
397
- * per-locale duplicates by `expandRoutesForLocales` from
398
- * `@pyreon/zero`. Independent from the `i18nRouting()` Vite plugin
399
- * (which only handles request-time locale detection); both can be used
400
- * together. See `expandRoutesForLocales` JSDoc for strategy semantics.
401
- */
402
- i18n?: I18nRoutingConfig
403
-
404
- /** App-level middleware applied to all routes. */
405
- middleware?: Middleware[]
406
-
407
- /** Server port for dev/preview. Default: 3000 */
408
- port?: number
409
- }
410
-
411
- // ─── File-system route ───────────────────────────────────────────────────────
412
-
413
- /**
414
- * Which optional metadata exports a route file declares.
415
- * Detected at scan time by parsing the file source. The code generator
416
- * uses this to skip emitting `import * as mod` for routes that only
417
- * export `default`, eliminating the dual-import collision with `lazy()`
418
- * and silencing `IMPORT_IS_UNDEFINED` warnings from Rolldown.
419
- */
420
- export interface RouteFileExports {
421
- /** Has `export const loader` or `export function loader` */
422
- hasLoader: boolean
423
- /** Has `export const guard` or `export function guard` */
424
- hasGuard: boolean
425
- /** Has `export const meta` */
426
- hasMeta: boolean
427
- /** Has `export const renderMode` */
428
- hasRenderMode: boolean
429
- /** Has `export const error` (custom per-route error component) */
430
- hasError: boolean
431
- /** Has `export const middleware` */
432
- hasMiddleware: boolean
433
- /**
434
- * Has `export const loaderKey` or `export function loaderKey`. When present,
435
- * the route generator wires it as the `loaderKey` field on the route record,
436
- * which controls cache identity for `_loaderCache`. Useful for auth-gate
437
- * loaders that should invalidate when the session cookie changes — read
438
- * `document.cookie` (CSR) or `ctx.request.headers.get('cookie')` (SSR) and
439
- * derive a key from session identity. Default cache key is `path + params`,
440
- * which doesn't see cookie changes.
441
- */
442
- hasLoaderKey: boolean
443
- /**
444
- * Has `export const gcTime` (number, in ms). When present, the route generator
445
- * inlines it on the route record. `gcTime: 0` disables caching entirely —
446
- * the loader runs on every navigation. Useful for auth-gate loaders that
447
- * must validate session on every navigation rather than serve stale data.
448
- */
449
- hasGcTime: boolean
450
- /**
451
- * Has `export function getStaticPaths` or `export const getStaticPaths`.
452
- * Used at SSG build time to enumerate concrete values for dynamic routes
453
- * (`/posts/[id].tsx` → `[/posts/1, /posts/2, …]`). The function returns
454
- * `Array<{ params: Record<string, string> }>`. Mirrors Astro's per-route
455
- * convention. Without it, dynamic routes are silently skipped during SSG
456
- * auto-detect — the user must hand-list every value in `ssg.paths`.
457
- */
458
- hasGetStaticPaths: boolean
459
- /**
460
- * Has `export const revalidate` (number, in seconds, or `false` for
461
- * never-revalidate). PR I — build-time ISR. The SSG plugin emits a
462
- * `dist/_pyreon-revalidate.json` manifest mapping `{ path: revalidate }`
463
- * which the deploy adapter (Vercel / Cloudflare / Netlify) consumes
464
- * to wire platform-specific ISR rebuild-on-stale. The route generator
465
- * does NOT inline `revalidate` onto the route record — it's a
466
- * build-time-only concern that never reaches the runtime router.
467
- */
468
- hasRevalidate: boolean
469
- /**
470
- * Raw text of the `export const meta = …` initializer, captured as a
471
- * literal expression. When present, the route generator inlines this
472
- * value directly into the generated routes module instead of importing
473
- * it from the route file — which means the route file can be lazy()'d
474
- * without forcing the entire dependency tree into the main bundle.
475
- *
476
- * Only set when the meta export is a top-level `export const meta = { … }`
477
- * literal that can be extracted via balanced-brace scanning. Anything
478
- * fancier (computed values, function calls, references to other
479
- * declarations) leaves this undefined and falls back to a static module
480
- * import.
481
- */
482
- metaLiteral?: string
483
- /**
484
- * Raw text of the `export const renderMode = …` initializer, captured
485
- * as a literal expression. Same inlining strategy as `metaLiteral`.
486
- */
487
- renderModeLiteral?: string
488
- /**
489
- * Raw text of the `export const revalidate = …` initializer (e.g.
490
- * `'60'`, `'false'`, `'3600'`). Captured at scan time so the SSG
491
- * plugin can read the value to emit the build-time ISR manifest
492
- * WITHOUT loading the route module — which is critical because the
493
- * manifest is emitted from the synthetic SSR build's outer plugin
494
- * context, where evaluating route modules would re-trigger the
495
- * recursive sub-build env-flag guard.
496
- *
497
- * Only set when the revalidate export is a top-level
498
- * `export const revalidate = <numeric|boolean literal>` that passes
499
- * `isPureLiteral`. Anything else (function calls, references to
500
- * other declarations) leaves this undefined and the manifest falls
501
- * back to omitting the entry.
502
- */
503
- revalidateLiteral?: string
504
- }
505
-
506
- /** Internal representation of a file-system route before conversion to RouteRecord. */
507
- export interface FileRoute {
508
- /** File path relative to routes dir (e.g. "users/[id].tsx") */
509
- filePath: string
510
- /** Parsed URL path pattern (e.g. "/users/:id") */
511
- urlPath: string
512
- /** Directory path for grouping (e.g. "users" or "" for root) */
513
- dirPath: string
514
- /** Route segment depth for nesting. */
515
- depth: number
516
- /** Whether this is a layout file. */
517
- isLayout: boolean
518
- /** Whether this is an error boundary file. */
519
- isError: boolean
520
- /** Whether this is a loading fallback file. */
521
- isLoading: boolean
522
- /** Whether this is a not-found (404) file. */
523
- isNotFound: boolean
524
- /** Whether this is a catch-all route. */
525
- isCatchAll: boolean
526
- /** Resolved rendering mode. */
527
- renderMode: RenderMode
528
- /**
529
- * Detected optional exports from the file source.
530
- * When undefined, the generator treats the file as having no metadata
531
- * exports and emits the optimal `lazy()` shape (one dynamic import,
532
- * no static metadata wiring). When provided, the generator emits a
533
- * single namespace import for files with metadata or `lazy()` for
534
- * files with only a default export.
535
- */
536
- exports?: RouteFileExports
537
- }
538
-
539
- // ─── Route middleware ────────────────────────────────────────────────────
540
-
541
- /** Entry mapping a URL pattern to its route-level middleware. */
542
- export interface RouteMiddlewareEntry {
543
- pattern: string
544
- middleware: Middleware | Middleware[]
545
- }
546
-
547
- // ─── Adapter ─────────────────────────────────────────────────────────────────
548
-
549
- export interface Adapter {
550
- name: string
551
- /** Build the production server/output for this adapter. */
552
- build(options: AdapterBuildOptions): Promise<void>
553
- /**
554
- * Revalidate a prerendered path on the deploy platform's ISR layer
555
- * (PR I — build-time ISR). Called by user code (webhook handlers,
556
- * cron jobs, CMS triggers, etc.) to trigger a rebuild-on-stale for
557
- * the named path. Optional — adapters without platform ISR support
558
- * (static, node, bun) implement a no-op. Returns `{ regenerated:
559
- * boolean }` so user code can branch on whether the platform actually
560
- * accepted the revalidation request.
561
- *
562
- * Distinct from runtime ISR (`mode: 'isr'`, on-demand LRU caching in
563
- * `@pyreon/zero/server`'s `createISRHandler`). Build-time ISR is
564
- * static prerender + platform-driven rebuild-on-stale; runtime ISR is
565
- * SSR-cached-with-TTL. They can coexist.
566
- *
567
- * Per-route `revalidate` metadata flows from `export const revalidate
568
- * = 60` in route files into a `dist/_pyreon-revalidate.json` manifest
569
- * the adapter reads at deploy time. Adapters use that manifest to
570
- * configure platform ISR (Vercel `output/config.json`, Cloudflare
571
- * Cache API rules, Netlify revalidation headers).
572
- */
573
- revalidate?(path: string): Promise<AdapterRevalidateResult>
574
- }
575
-
576
- /**
577
- * Result of `Adapter.revalidate(path)`. `regenerated: false` means the
578
- * adapter does not support platform ISR (no-op fallback) OR the
579
- * platform rejected the request. Adapters that throw on platform-API
580
- * failure should let it propagate so user code can handle the rejection.
581
- */
582
- export interface AdapterRevalidateResult {
583
- regenerated: boolean
584
- }
585
-
586
- /**
587
- * Inputs the build pipeline passes to an adapter's `build()` method.
588
- *
589
- * The `kind` field discriminates the two shapes. **SSR mode** (`'ssr'`)
590
- * carries `serverEntry` + `clientOutDir` so adapters can wrap the user's
591
- * server bundle as a serverless function. **SSG mode** (`'ssg'`) carries
592
- * only `outDir` (which IS the rendered dist/) — no serverEntry exists
593
- * because every page is already prerendered. SSG-mode adapters write
594
- * platform-specific routing config so the host knows the deploy is
595
- * fully-static (no function invocation per request).
596
- *
597
- * Pre-PR-J this was a single SSR-shaped struct; the SSG path had no way
598
- * to invoke `adapter.build()` because it couldn't supply `serverEntry`.
599
- * Adding `kind` (with TS-narrowing per branch) lets `ssgPlugin`
600
- * `closeBundle` call `adapter.build({ kind: 'ssg', outDir, config })`
601
- * cleanly, AND keeps the SSR-mode adapter implementations unchanged.
602
- */
603
- export type AdapterBuildOptions =
604
- | {
605
- kind: 'ssr'
606
- /** Path to the built server entry. */
607
- serverEntry: string
608
- /** Path to the client build output. */
609
- clientOutDir: string
610
- /** Final output directory. */
611
- outDir: string
612
- config: ZeroConfig
613
- }
614
- | {
615
- kind: 'ssg'
616
- /**
617
- * The rendered dist directory. For SSG, this directory IS the
618
- * publishable output — adapters write platform-specific routing
619
- * config alongside (e.g. `.vercel/output/config.json`,
620
- * `_routes.json`, `netlify.toml`) but generally don't move files.
621
- */
622
- outDir: string
623
- config: ZeroConfig
624
- }
@@ -1,36 +0,0 @@
1
- import { onMount, onUnmount } from '@pyreon/core'
2
-
3
- /**
4
- * Observes an element and calls `onIntersect` once it enters the viewport.
5
- * Automatically disconnects after the first intersection.
6
- *
7
- * @param getElement - Getter for the target element (may be undefined before mount).
8
- * @param onIntersect - Callback fired when the element becomes visible.
9
- * @param rootMargin - IntersectionObserver rootMargin. Default: "200px".
10
- */
11
- export function useIntersectionObserver(
12
- getElement: () => HTMLElement | undefined,
13
- onIntersect: () => void,
14
- rootMargin = '200px',
15
- ) {
16
- onMount(() => {
17
- const el = getElement()
18
- if (!el) return undefined
19
-
20
- const observer = new IntersectionObserver(
21
- (entries) => {
22
- for (const entry of entries) {
23
- if (entry.isIntersecting) {
24
- onIntersect()
25
- observer.disconnect()
26
- }
27
- }
28
- },
29
- { rootMargin },
30
- )
31
-
32
- observer.observe(el)
33
- onUnmount(() => observer.disconnect())
34
- return undefined
35
- })
36
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Clone a Response with modified headers.
3
- * Avoids repeating the `new Response(body, { status, statusText, headers })` pattern.
4
- */
5
- export function withHeaders(response: Response, modify: (headers: Headers) => void): Response {
6
- const headers = new Headers(response.headers)
7
- modify(headers)
8
- return new Response(response.body, {
9
- status: response.status,
10
- statusText: response.statusText,
11
- headers,
12
- })
13
- }