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