@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/zero",
3
- "version": "0.24.4",
3
+ "version": "0.24.6",
4
4
  "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
5
  "license": "MIT",
6
6
  "author": "Vit Bokisch",
@@ -13,7 +13,6 @@
13
13
  "lib",
14
14
  "!lib/**/*.map",
15
15
  "!lib/analysis",
16
- "src",
17
16
  "!src/tests",
18
17
  "LICENSE",
19
18
  "README.md"
@@ -25,142 +24,114 @@
25
24
  "types": "./lib/types/index.d.ts",
26
25
  "exports": {
27
26
  ".": {
28
- "bun": "./src/index.ts",
29
27
  "import": "./lib/index.js",
30
28
  "types": "./lib/types/index.d.ts"
31
29
  },
32
30
  "./server": {
33
- "bun": "./src/server.ts",
34
31
  "import": "./lib/server.js",
35
32
  "types": "./lib/types/server.d.ts"
36
33
  },
37
34
  "./client": {
38
- "bun": "./src/client.ts",
39
35
  "import": "./lib/client.js",
40
36
  "types": "./lib/types/client.d.ts"
41
37
  },
42
38
  "./config": {
43
- "bun": "./src/config.ts",
44
39
  "import": "./lib/config.js",
45
40
  "types": "./lib/types/config.d.ts"
46
41
  },
47
42
  "./image": {
48
- "bun": "./src/image.tsx",
49
43
  "import": "./lib/image.js",
50
44
  "types": "./lib/types/image.d.ts"
51
45
  },
52
46
  "./link": {
53
- "bun": "./src/link.tsx",
54
47
  "import": "./lib/link.js",
55
48
  "types": "./lib/types/link.d.ts"
56
49
  },
57
50
  "./script": {
58
- "bun": "./src/script.tsx",
59
51
  "import": "./lib/script.js",
60
52
  "types": "./lib/types/script.d.ts"
61
53
  },
62
54
  "./font": {
63
- "bun": "./src/font.ts",
64
55
  "import": "./lib/font.js",
65
56
  "types": "./lib/types/font.d.ts"
66
57
  },
67
58
  "./cache": {
68
- "bun": "./src/cache.ts",
69
59
  "import": "./lib/cache.js",
70
60
  "types": "./lib/types/cache.d.ts"
71
61
  },
72
62
  "./seo": {
73
- "bun": "./src/seo.ts",
74
63
  "import": "./lib/seo.js",
75
64
  "types": "./lib/types/seo.d.ts"
76
65
  },
77
66
  "./theme": {
78
- "bun": "./src/theme.tsx",
79
67
  "import": "./lib/theme.js",
80
68
  "types": "./lib/types/theme.d.ts"
81
69
  },
82
70
  "./image-plugin": {
83
- "bun": "./src/image-plugin.ts",
84
71
  "import": "./lib/image-plugin.js",
85
72
  "types": "./lib/types/image-plugin.d.ts"
86
73
  },
87
74
  "./image-types": {
88
- "bun": "./src/image-types.ts",
89
75
  "import": "./lib/image-types.js",
90
76
  "types": "./lib/types/image-types.d.ts"
91
77
  },
92
78
  "./actions": {
93
- "bun": "./src/actions.ts",
94
79
  "import": "./lib/actions.js",
95
80
  "types": "./lib/types/actions.d.ts"
96
81
  },
97
82
  "./api-routes": {
98
- "bun": "./src/api-routes.ts",
99
83
  "import": "./lib/api-routes.js",
100
84
  "types": "./lib/types/api-routes.d.ts"
101
85
  },
102
86
  "./cors": {
103
- "bun": "./src/cors.ts",
104
87
  "import": "./lib/cors.js",
105
88
  "types": "./lib/types/cors.d.ts"
106
89
  },
107
90
  "./rate-limit": {
108
- "bun": "./src/rate-limit.ts",
109
91
  "import": "./lib/rate-limit.js",
110
92
  "types": "./lib/types/rate-limit.d.ts"
111
93
  },
112
94
  "./compression": {
113
- "bun": "./src/compression.ts",
114
95
  "import": "./lib/compression.js",
115
96
  "types": "./lib/types/compression.d.ts"
116
97
  },
117
98
  "./testing": {
118
- "bun": "./src/testing.ts",
119
99
  "import": "./lib/testing.js",
120
100
  "types": "./lib/types/testing.d.ts"
121
101
  },
122
102
  "./meta": {
123
- "bun": "./src/meta.tsx",
124
103
  "import": "./lib/meta.js",
125
104
  "types": "./lib/types/meta.d.ts"
126
105
  },
127
106
  "./favicon": {
128
- "bun": "./src/favicon.ts",
129
107
  "import": "./lib/favicon.js",
130
108
  "types": "./lib/types/favicon.d.ts"
131
109
  },
132
110
  "./og-image": {
133
- "bun": "./src/og-image.ts",
134
111
  "import": "./lib/og-image.js",
135
112
  "types": "./lib/types/og-image.d.ts"
136
113
  },
137
114
  "./i18n-routing": {
138
- "bun": "./src/i18n-routing.ts",
139
115
  "import": "./lib/i18n-routing.js",
140
116
  "types": "./lib/types/i18n-routing.d.ts"
141
117
  },
142
118
  "./ai": {
143
- "bun": "./src/ai.ts",
144
119
  "import": "./lib/ai.js",
145
120
  "types": "./lib/types/ai.d.ts"
146
121
  },
147
122
  "./middleware": {
148
- "bun": "./src/middleware.ts",
149
123
  "import": "./lib/middleware.js",
150
124
  "types": "./lib/types/middleware.d.ts"
151
125
  },
152
126
  "./csp": {
153
- "bun": "./src/csp.ts",
154
127
  "import": "./lib/csp.js",
155
128
  "types": "./lib/types/csp.d.ts"
156
129
  },
157
130
  "./env": {
158
- "bun": "./src/env.ts",
159
131
  "import": "./lib/env.js",
160
132
  "types": "./lib/types/env.d.ts"
161
133
  },
162
134
  "./logger": {
163
- "bun": "./src/logger.ts",
164
135
  "import": "./lib/logger.js",
165
136
  "types": "./lib/types/logger.d.ts"
166
137
  }
@@ -173,15 +144,15 @@
173
144
  "lint": "oxlint ."
174
145
  },
175
146
  "dependencies": {
176
- "@pyreon/core": "^0.24.4",
177
- "@pyreon/head": "^0.24.4",
178
- "@pyreon/meta": "^0.24.4",
179
- "@pyreon/reactivity": "^0.24.4",
180
- "@pyreon/router": "^0.24.4",
181
- "@pyreon/runtime-dom": "^0.24.4",
182
- "@pyreon/runtime-server": "^0.24.4",
183
- "@pyreon/server": "^0.24.4",
184
- "@pyreon/vite-plugin": "^0.24.4",
147
+ "@pyreon/core": "^0.24.6",
148
+ "@pyreon/head": "^0.24.6",
149
+ "@pyreon/meta": "^0.24.6",
150
+ "@pyreon/reactivity": "^0.24.6",
151
+ "@pyreon/router": "^0.24.6",
152
+ "@pyreon/runtime-dom": "^0.24.6",
153
+ "@pyreon/runtime-server": "^0.24.6",
154
+ "@pyreon/server": "^0.24.6",
155
+ "@pyreon/vite-plugin": "^0.24.6",
185
156
  "vite": "^8.0.0"
186
157
  },
187
158
  "devDependencies": {
package/src/actions.ts DELETED
@@ -1,196 +0,0 @@
1
- import type { MiddlewareContext } from '@pyreon/server'
2
-
3
- // ─── Types ───────────────────────────────────────────────────────────────────
4
-
5
- /** Context passed to server action handlers. */
6
- export interface ActionContext {
7
- /** The original request. */
8
- request: Request
9
- /** Parsed form data (for form submissions). */
10
- formData: FormData | null
11
- /** Parsed JSON body (for JSON submissions). */
12
- json: unknown
13
- /** Request headers. */
14
- headers: Headers
15
- }
16
-
17
- /** A server action handler function. */
18
- export type ActionHandler<T = unknown> = (ctx: ActionContext) => T | Promise<T>
19
-
20
- /** A registered action with its ID and handler. */
21
- interface RegisteredAction {
22
- id: string
23
- handler: ActionHandler
24
- }
25
-
26
- /** Client-side callable action returned by defineAction. */
27
- export interface Action<T = unknown> {
28
- /** Call the action with JSON data. */
29
- (data?: unknown): Promise<T>
30
- /** The action's unique ID. */
31
- actionId: string
32
- }
33
-
34
- // ─── Registry ────────────────────────────────────────────────────────────────
35
-
36
- /**
37
- * Module-level registry of every `defineAction()` call. Lookup is by the
38
- * `action_<uuid>` string the client sends in `POST /_zero/actions/<id>`.
39
- *
40
- * **HMR caveat (dev-only):** the registry uses fresh `crypto.randomUUID()`
41
- * per `defineAction()` invocation. When Vite hot-replaces a module that
42
- * calls `defineAction()`, the module re-runs and a NEW entry is inserted
43
- * — the OLD entry stays in the Map until the dev process exits. Each
44
- * entry holds `{ id, handler }` (~80 bytes). Bounded by the count of
45
- * distinct UUIDs minted in the session; a realistic dev session sees
46
- * <50 entries, so total dev-memory cost stays under ~5KB. Production
47
- * registers each module exactly once at startup — no leak. A
48
- * FinalizationRegistry-based purge is tracked as a follow-up; the
49
- * current cost is too small to justify the WeakRef/finalizer complexity.
50
- */
51
- const actionRegistry = new Map<string, RegisteredAction>()
52
-
53
- /**
54
- * Define a server action. Returns a callable function that:
55
- * - On the **client**: sends a POST request to `/_zero/actions/<id>`
56
- * - On the **server** (SSR): executes the handler directly (no fetch)
57
- *
58
- * @example
59
- * // In a route file or module:
60
- * export const createPost = defineAction(async (ctx) => {
61
- * const data = ctx.json as { title: string; body: string }
62
- * // ... save to database
63
- * return { success: true, id: 123 }
64
- * })
65
- *
66
- * // In a component:
67
- * const result = await createPost({ title: 'Hello', body: '...' })
68
- */
69
- export function defineAction<T = unknown>(handler: ActionHandler<T>): Action<T> {
70
- const id = `action_${crypto.randomUUID().slice(0, 8)}`
71
-
72
- actionRegistry.set(id, { id, handler: handler as ActionHandler })
73
-
74
- const callable = async (data?: unknown): Promise<T> => {
75
- // Server-side: execute handler directly (no network round-trip)
76
- if (typeof globalThis.window === 'undefined') {
77
- return handler({
78
- request: new Request(`http://localhost/_zero/actions/${id}`, {
79
- method: 'POST',
80
- headers: { 'Content-Type': 'application/json' },
81
- body: JSON.stringify(data ?? null),
82
- }),
83
- formData: null,
84
- json: data ?? null,
85
- headers: new Headers({ 'Content-Type': 'application/json' }),
86
- })
87
- }
88
-
89
- // Client-side: POST to the action endpoint
90
- const response = await fetch(`/_zero/actions/${id}`, {
91
- method: 'POST',
92
- headers: { 'Content-Type': 'application/json' },
93
- body: JSON.stringify(data ?? null),
94
- })
95
- if (!response.ok) {
96
- const body = await response.json().catch(() => ({}))
97
- throw new Error((body as { error?: string }).error ?? `Action failed: ${response.statusText}`)
98
- }
99
- return response.json()
100
- }
101
-
102
- callable.actionId = id
103
- return callable as Action<T>
104
- }
105
-
106
- /** Get all registered actions. Useful for testing. */
107
- export function getRegisteredActions(): Map<string, RegisteredAction> {
108
- return actionRegistry
109
- }
110
-
111
- /**
112
- * Reset the action registry. Useful for testing.
113
- * @internal
114
- */
115
- export function _resetActions(): void {
116
- actionRegistry.clear()
117
- }
118
-
119
- // ─── Server handler ──────────────────────────────────────────────────────────
120
-
121
- /**
122
- * Create a middleware that handles action requests at `/_zero/actions/*`.
123
- * Mount this before the SSR handler in the server entry.
124
- */
125
- export function createActionMiddleware(): (
126
- ctx: MiddlewareContext,
127
- ) => Response | undefined | Promise<Response | undefined> {
128
- return async (ctx: MiddlewareContext) => {
129
- if (!ctx.path.startsWith('/_zero/actions/')) return
130
-
131
- const actionId = ctx.path.slice('/_zero/actions/'.length)
132
- const action = actionRegistry.get(actionId)
133
-
134
- if (!action) {
135
- return Response.json({ error: 'Action not found' }, { status: 404 })
136
- }
137
-
138
- if (ctx.req.method !== 'POST') {
139
- return Response.json({ error: 'Method not allowed' }, { status: 405 })
140
- }
141
-
142
- return executeAction(action, ctx.req)
143
- }
144
- }
145
-
146
- async function executeAction(action: RegisteredAction, req: Request): Promise<Response> {
147
- // Parse the request payload separately so a malformed body returns
148
- // 400 (Bad Request) instead of being conflated with a runtime 500.
149
- // `req.json()` / `req.formData()` throw on syntactically invalid
150
- // payloads (truncated JSON, malformed multipart, invalid UTF-8, etc.)
151
- // — that's a client problem, not a server problem, and the HTTP
152
- // status code should reflect that.
153
- const contentType = req.headers.get('content-type') ?? ''
154
- let formData: FormData | null = null
155
- let json: unknown = null
156
- try {
157
- if (contentType.includes('application/json')) {
158
- json = await req.json()
159
- } else if (
160
- contentType.includes('multipart/form-data') ||
161
- contentType.includes('application/x-www-form-urlencoded')
162
- ) {
163
- formData = await req.formData()
164
- }
165
- } catch (err) {
166
- // Malformed request body — log for ops diagnostics but return 400
167
- // (not 500) so the client sees the right status code. Don't leak
168
- // the parser's internal error message; surface only the shape.
169
- console.error('[Pyreon Action] failed to parse request body:', err)
170
- return Response.json(
171
- { error: 'Invalid request body' },
172
- { status: 400 },
173
- )
174
- }
175
-
176
- // Execute the user-supplied action handler. Surface errors to server
177
- // logs via `console.error` — the cloud adapter audit (PR #755) found
178
- // this same swallow-error pattern hiding production crashes from
179
- // operators. Without it, a CMS-triggered action that crashed inside
180
- // the user's handler returned a generic 500 to the client AND
181
- // logged nothing on the server side, so the operator couldn't
182
- // diagnose the failure.
183
- try {
184
- const result = await action.handler({
185
- request: req,
186
- formData,
187
- json,
188
- headers: req.headers,
189
- })
190
- return Response.json(result ?? null)
191
- } catch (err) {
192
- console.error('[Pyreon Action] handler failed:', err)
193
- const message = err instanceof Error ? err.message : 'Internal server error'
194
- return Response.json({ error: message }, { status: 500 })
195
- }
196
- }
@@ -1,114 +0,0 @@
1
- import type { Adapter, AdapterBuildOptions, AdapterRevalidateResult } from '../types'
2
- import { validateBuildInputs } from './validate'
3
-
4
- /**
5
- * Bun adapter — generates a standalone Bun.serve() entry.
6
- *
7
- * **SSG mode (PR J)**: no-op. Bun adapter exists for serving the SSR
8
- * runtime; SSG output is already complete static HTML — serve it with
9
- * any static-file server (`bun preview` / `bunx serve` / nginx / Caddy).
10
- * Use `staticAdapter()` if you want explicit SSG semantics.
11
- */
12
- export function bunAdapter(): Adapter {
13
- return {
14
- name: 'bun',
15
- async build(options: AdapterBuildOptions) {
16
- if (options.kind === 'ssg') {
17
- // Bun runner has nothing to add for prerendered SSG dist.
18
- return
19
- }
20
- await validateBuildInputs(options)
21
- const { writeFile, cp, mkdir } = await import('node:fs/promises')
22
- const { join } = await import('node:path')
23
-
24
- const outDir = options.outDir
25
- await mkdir(outDir, { recursive: true })
26
-
27
- // Copy server and client builds
28
- await cp(options.clientOutDir, join(outDir, 'client'), {
29
- recursive: true,
30
- })
31
- await cp(join(options.serverEntry, '..'), join(outDir, 'server'), {
32
- recursive: true,
33
- })
34
-
35
- const port = options.config.port ?? 3000
36
- const serverEntry = `
37
- import { normalize } from "node:path"
38
-
39
- const handler = (await import("./server/entry-server.js")).default
40
- const clientDir = new URL("./client/", import.meta.url).pathname
41
-
42
- Bun.serve({
43
- port: ${port},
44
- async fetch(req) {
45
- const url = new URL(req.url)
46
-
47
- // Try static files first (GET only).
48
- //
49
- // Path safety: decode percent-encoding, normalize \`..\` segments,
50
- // then assert the resulting path doesn't escape the clientDir
51
- // prefix. The previous implementation used \`Bun.resolveSync\`,
52
- // which is MODULE resolution — it throws on any non-existent
53
- // path, so it crashed every SSR route (URLs without a matching
54
- // static file) with a 500 before the SSR handler ran.
55
- // \`node:path.normalize\` is pure-string path arithmetic and
56
- // doesn't touch the filesystem — safe for arbitrary input.
57
- if (req.method === "GET") {
58
- let decoded
59
- try {
60
- decoded = decodeURIComponent(url.pathname)
61
- } catch {
62
- // Malformed %-encoding → reject (don't fall through to SSR
63
- // with a corrupt URL).
64
- return new Response("Bad Request", { status: 400 })
65
- }
66
- // Reject null bytes outright — no legitimate use in a URL,
67
- // and they can confuse downstream filesystem code.
68
- if (decoded.includes("\\0")) {
69
- return new Response("Forbidden", { status: 403 })
70
- }
71
- const reqPath = decoded === "/" ? "/index.html" : decoded
72
- // Prepend clientDir then normalize. If the normalized result
73
- // no longer starts with clientDir, a \`..\` segment escaped —
74
- // reject. Using string-startsWith with clientDir (which ends
75
- // in "/") prevents the "/clientdir-evil/" sibling-prefix
76
- // bypass.
77
- const candidate = normalize(clientDir + reqPath)
78
- if (!candidate.startsWith(clientDir)) {
79
- return new Response("Forbidden", { status: 403 })
80
- }
81
- const file = Bun.file(candidate)
82
- if (await file.exists()) {
83
- return new Response(file, {
84
- headers: {
85
- "cache-control": candidate.endsWith(".js") || candidate.endsWith(".css")
86
- ? "public, max-age=31536000, immutable"
87
- : "public, max-age=3600",
88
- },
89
- })
90
- }
91
- }
92
-
93
- // Fall through to SSR handler
94
- return handler(req)
95
- },
96
- })
97
-
98
- console.log("\\n ⚡ Zero production server running on http://localhost:${port}\\n")
99
- `.trimStart()
100
-
101
- await writeFile(join(outDir, 'index.ts'), serverEntry)
102
- },
103
- async revalidate(_path: string): Promise<AdapterRevalidateResult> {
104
- // Self-hosted Bun has no platform-driven ISR — same shape as
105
- // nodeAdapter. See nodeAdapter.revalidate for full rationale.
106
- if (process.env.NODE_ENV !== 'production') {
107
- console.warn(
108
- '[Pyreon] bunAdapter.revalidate() is a no-op — self-hosted Bun has no platform-driven ISR. Use mode: "isr" for runtime LRU caching, or vercelAdapter / cloudflareAdapter / netlifyAdapter for platform-driven build-time ISR.',
109
- )
110
- }
111
- return { regenerated: false }
112
- },
113
- }
114
- }
@@ -1,166 +0,0 @@
1
- import type { Adapter, AdapterBuildOptions, AdapterRevalidateResult } from '../types'
2
- import { validateBuildInputs } from './validate'
3
- import { warnMissingEnv } from './warn-missing-env'
4
-
5
- /**
6
- * Cloudflare Pages adapter — generates output for Cloudflare Pages with Functions.
7
- *
8
- * Produces:
9
- * - Client assets in the output directory root (served as static)
10
- * - `_worker.js` — Cloudflare Pages Function for SSR
11
- *
12
- * Note: Cloudflare Pages Functions have a ~1MB module size limit.
13
- * For large apps, configure Vite's SSR build to bundle server code:
14
- * `ssr: { noExternal: true }` in vite.config.ts.
15
- *
16
- * Deploy with: `npx wrangler pages deploy ./dist`
17
- *
18
- * @example
19
- * ```ts
20
- * // zero.config.ts
21
- * import { defineConfig } from "@pyreon/zero"
22
- *
23
- * export default defineConfig({
24
- * adapter: "cloudflare",
25
- * })
26
- * ```
27
- */
28
- export function cloudflareAdapter(): Adapter {
29
- return {
30
- name: 'cloudflare',
31
- async build(options: AdapterBuildOptions) {
32
- if (options.kind === 'ssg') {
33
- // PR J — SSG branch. Emit Cloudflare Pages `_routes.json` with
34
- // `include: []` + `exclude: ['/*']` — i.e. "every URL is a
35
- // static asset, never invoke a Pages Function". Without this
36
- // file, Pages defaults to running the worker on every request,
37
- // which is wasteful for prerendered SSG output (and incurs
38
- // function-invocation costs on paid plans).
39
- //
40
- // Reference: https://developers.cloudflare.com/pages/functions/routing/
41
- // — `version: 1`, `include` lists URL globs that DO invoke the
42
- // function, `exclude` lists globs that bypass it. Setting
43
- // `include: []` makes the function unreachable; the result is
44
- // a pure-static deploy.
45
- //
46
- // Deploy with: `npx wrangler pages deploy ./dist`
47
- const { writeFile } = await import('node:fs/promises')
48
- const { join } = await import('node:path')
49
- const routesConfig = {
50
- version: 1,
51
- include: [] as string[],
52
- exclude: ['/*'],
53
- }
54
- await writeFile(
55
- join(options.outDir, '_routes.json'),
56
- JSON.stringify(routesConfig, null, 2),
57
- )
58
- return
59
- }
60
- await validateBuildInputs(options)
61
- const { writeFile, cp, mkdir } = await import('node:fs/promises')
62
- const { join } = await import('node:path')
63
-
64
- const outDir = options.outDir
65
- await mkdir(outDir, { recursive: true })
66
-
67
- // Copy client assets to root (Cloudflare serves static files from root)
68
- await cp(options.clientOutDir, outDir, { recursive: true })
69
-
70
- // Copy server build
71
- await cp(join(options.serverEntry, '..'), join(outDir, '_server'), {
72
- recursive: true,
73
- })
74
-
75
- // Generate Cloudflare Pages _worker.js (ES module format).
76
- //
77
- // Static assets are handled by Cloudflare Pages itself via the
78
- // asset binding (Cloudflare's CDN serves files from the dist
79
- // root before invoking the worker). The pre-fix harness had an
80
- // \`if (ext && ...) { /* comment */ }\` block here computing an
81
- // \`ext\` variable and checking a condition with an EMPTY body —
82
- // pure dead code that did nothing at runtime. Removed for
83
- // clarity.
84
- const workerEntry = `
85
- import handler from "./_server/entry-server.js"
86
-
87
- export default {
88
- async fetch(request, env, ctx) {
89
- try {
90
- return await handler(request)
91
- } catch (err) {
92
- // Surface the error to Cloudflare Tail logs so production
93
- // crashes give real diagnostic info — pre-fix the catch
94
- // swallowed \`err\` entirely and the operator saw only a
95
- // bare "Internal Server Error" with no stack, no message,
96
- // no path. Logging via \`console.error\` is the standard
97
- // Workers logging surface (lands in \`wrangler tail\` + the
98
- // Cloudflare dashboard log stream).
99
- console.error("[Pyreon SSR] handler failed:", err)
100
- return new Response("Internal Server Error", { status: 500 })
101
- }
102
- },
103
- }
104
- `.trimStart()
105
-
106
- await writeFile(join(outDir, '_worker.js'), workerEntry)
107
-
108
- // Cloudflare Pages config — _routes.json for routing
109
- const routesConfig = {
110
- version: 1,
111
- include: ['/*'],
112
- exclude: ['/assets/*', '/favicon.*', '/site.webmanifest', '/robots.txt', '/sitemap.xml'],
113
- }
114
-
115
- await writeFile(join(outDir, '_routes.json'), JSON.stringify(routesConfig, null, 2))
116
- },
117
- async revalidate(path: string): Promise<AdapterRevalidateResult> {
118
- // Cloudflare Pages ISR via Cache API delete + zone purge.
119
- // Reads `CLOUDFLARE_ZONE_ID` and `CLOUDFLARE_API_TOKEN` from env
120
- // (set in Workers / Pages dashboard → Variables). The zone
121
- // purge endpoint accepts a list of URLs and removes them from
122
- // every PoP's edge cache; the next visitor triggers a fresh
123
- // origin fetch which rebuilds the prerendered page.
124
- //
125
- // Reference: https://developers.cloudflare.com/api/operations/zone-purge
126
- const zoneId = process.env.CLOUDFLARE_ZONE_ID
127
- const apiToken = process.env.CLOUDFLARE_API_TOKEN
128
- const siteUrl = process.env.CLOUDFLARE_SITE_URL
129
- if (!zoneId || !apiToken || !siteUrl) {
130
- // M2.4 — warn even in production (dedupe per process). See vercel.ts
131
- // for the rationale.
132
- const missing: string[] = []
133
- if (!zoneId) missing.push('CLOUDFLARE_ZONE_ID')
134
- if (!apiToken) missing.push('CLOUDFLARE_API_TOKEN')
135
- if (!siteUrl) missing.push('CLOUDFLARE_SITE_URL')
136
- return warnMissingEnv(
137
- 'cloudflare',
138
- missing,
139
- 'Set them in Cloudflare Pages dashboard → Settings → Environment Variables. Note: Cloudflare imposes a 1000-purge-per-24h rate limit per zone — high-frequency revalidation will hit it.',
140
- )
141
- }
142
- const fullUrl = `${siteUrl.replace(/\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`
143
- try {
144
- const res = await fetch(
145
- `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
146
- {
147
- method: 'POST',
148
- headers: {
149
- 'Authorization': `Bearer ${apiToken}`,
150
- 'Content-Type': 'application/json',
151
- },
152
- body: JSON.stringify({ files: [fullUrl] }),
153
- },
154
- )
155
- return { regenerated: res.ok }
156
- } catch (err) {
157
- if (process.env.NODE_ENV !== 'production') {
158
- console.warn(
159
- `[Pyreon] cloudflareAdapter.revalidate(${path}) failed: ${err instanceof Error ? err.message : String(err)}`,
160
- )
161
- }
162
- return { regenerated: false }
163
- }
164
- },
165
- }
166
- }