@jasonshimmy/vite-plugin-cer-app 0.10.0 → 0.12.0

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 (103) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/IMPLEMENTATION_PLAN.md +4 -0
  3. package/ROADMAP.md +63 -11
  4. package/commits.txt +1 -1
  5. package/dist/cli/adapters/cloudflare.d.ts +27 -0
  6. package/dist/cli/adapters/cloudflare.d.ts.map +1 -0
  7. package/dist/cli/adapters/cloudflare.js +282 -0
  8. package/dist/cli/adapters/cloudflare.js.map +1 -0
  9. package/dist/cli/adapters/netlify.d.ts.map +1 -1
  10. package/dist/cli/adapters/netlify.js +34 -9
  11. package/dist/cli/adapters/netlify.js.map +1 -1
  12. package/dist/cli/adapters/vercel.d.ts.map +1 -1
  13. package/dist/cli/adapters/vercel.js +43 -4
  14. package/dist/cli/adapters/vercel.js.map +1 -1
  15. package/dist/cli/commands/adapt.d.ts.map +1 -1
  16. package/dist/cli/commands/adapt.js +6 -2
  17. package/dist/cli/commands/adapt.js.map +1 -1
  18. package/dist/cli/commands/build.d.ts.map +1 -1
  19. package/dist/cli/commands/build.js +4 -0
  20. package/dist/cli/commands/build.js.map +1 -1
  21. package/dist/cli/commands/preview.d.ts.map +1 -1
  22. package/dist/cli/commands/preview.js +66 -2
  23. package/dist/cli/commands/preview.js.map +1 -1
  24. package/dist/plugin/dev-server.d.ts.map +1 -1
  25. package/dist/plugin/dev-server.js +23 -6
  26. package/dist/plugin/dev-server.js.map +1 -1
  27. package/dist/plugin/dts-generator.d.ts.map +1 -1
  28. package/dist/plugin/dts-generator.js +7 -1
  29. package/dist/plugin/dts-generator.js.map +1 -1
  30. package/dist/plugin/index.d.ts.map +1 -1
  31. package/dist/plugin/index.js +1 -0
  32. package/dist/plugin/index.js.map +1 -1
  33. package/dist/plugin/transforms/auto-import.d.ts +2 -0
  34. package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
  35. package/dist/plugin/transforms/auto-import.js +6 -3
  36. package/dist/plugin/transforms/auto-import.js.map +1 -1
  37. package/dist/runtime/composables/define-server-middleware.d.ts +16 -0
  38. package/dist/runtime/composables/define-server-middleware.d.ts.map +1 -0
  39. package/dist/runtime/composables/define-server-middleware.js +17 -0
  40. package/dist/runtime/composables/define-server-middleware.js.map +1 -0
  41. package/dist/runtime/composables/index.d.ts +3 -0
  42. package/dist/runtime/composables/index.d.ts.map +1 -1
  43. package/dist/runtime/composables/index.js +2 -0
  44. package/dist/runtime/composables/index.js.map +1 -1
  45. package/dist/runtime/composables/use-session.d.ts +59 -0
  46. package/dist/runtime/composables/use-session.d.ts.map +1 -0
  47. package/dist/runtime/composables/use-session.js +125 -0
  48. package/dist/runtime/composables/use-session.js.map +1 -0
  49. package/dist/runtime/entry-server-template.d.ts +1 -1
  50. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  51. package/dist/runtime/entry-server-template.js +52 -5
  52. package/dist/runtime/entry-server-template.js.map +1 -1
  53. package/dist/types/config.d.ts +4 -3
  54. package/dist/types/config.d.ts.map +1 -1
  55. package/dist/types/config.js.map +1 -1
  56. package/dist/types/middleware.d.ts +1 -1
  57. package/dist/types/middleware.d.ts.map +1 -1
  58. package/docs/cli.md +13 -2
  59. package/docs/composables.md +116 -0
  60. package/docs/getting-started.md +36 -0
  61. package/docs/middleware.md +13 -7
  62. package/docs/rendering-modes.md +15 -5
  63. package/docs/routing.md +27 -0
  64. package/docs/server-api.md +72 -0
  65. package/e2e/cypress/e2e/api.cy.ts +42 -0
  66. package/e2e/cypress/e2e/server-middleware.cy.ts +46 -0
  67. package/e2e/cypress/e2e/session.cy.ts +73 -0
  68. package/e2e/cypress/tsconfig.json +14 -0
  69. package/e2e/kitchen-sink/cer.config.ts +5 -0
  70. package/e2e/kitchen-sink/server/api/echo.ts +12 -0
  71. package/e2e/kitchen-sink/server/api/session.ts +23 -0
  72. package/e2e/kitchen-sink/server/middleware/01-headers.ts +6 -0
  73. package/e2e/scripts/clean.mjs +2 -0
  74. package/package.json +1 -1
  75. package/src/__tests__/cli/adapters/cloudflare-worker.integration.test.ts +130 -0
  76. package/src/__tests__/cli/adapters/cloudflare.test.ts +247 -0
  77. package/src/__tests__/cli/adapters/netlify.test.ts +25 -2
  78. package/src/__tests__/cli/adapters/vercel-launcher.integration.test.ts +129 -0
  79. package/src/__tests__/cli/adapters/vercel.test.ts +22 -1
  80. package/src/__tests__/cli/preview-hardening.test.ts +90 -0
  81. package/src/__tests__/plugin/cer-app-plugin.test.ts +1 -1
  82. package/src/__tests__/plugin/dev-server.test.ts +67 -0
  83. package/src/__tests__/plugin/dts-generator.test.ts +35 -0
  84. package/src/__tests__/plugin/entry-server-template.test.ts +10 -1
  85. package/src/__tests__/plugin/transforms/auto-import.test.ts +49 -0
  86. package/src/__tests__/runtime/use-cookie.test.ts +38 -0
  87. package/src/__tests__/runtime/use-session.test.ts +237 -0
  88. package/src/cli/adapters/cloudflare.ts +311 -0
  89. package/src/cli/adapters/netlify.ts +34 -9
  90. package/src/cli/adapters/vercel.ts +43 -4
  91. package/src/cli/commands/adapt.ts +6 -2
  92. package/src/cli/commands/build.ts +4 -0
  93. package/src/cli/commands/preview.ts +64 -3
  94. package/src/plugin/dev-server.ts +13 -6
  95. package/src/plugin/dts-generator.ts +7 -1
  96. package/src/plugin/index.ts +1 -0
  97. package/src/plugin/transforms/auto-import.ts +8 -3
  98. package/src/runtime/composables/define-server-middleware.ts +18 -0
  99. package/src/runtime/composables/index.ts +3 -0
  100. package/src/runtime/composables/use-session.ts +169 -0
  101. package/src/runtime/entry-server-template.ts +52 -5
  102. package/src/types/config.ts +4 -3
  103. package/src/types/middleware.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
+ ## [v0.12.0] - 2026-03-22
5
+
6
+ - feat: implement ISR support with isrHandler for SSR fallback in Cloudflare, Netlify, and Vercel adapters (ddcc4d4)
7
+
8
+ ## [v0.11.0] - 2026-03-22
9
+
10
+ - feat: add Cloudflare Pages adapter for SSR and SSG support (2734c26)
11
+
4
12
  ## [v0.10.0] - 2026-03-22
5
13
 
6
14
  - feat: add adapters for Vercel and Netlify deployment platforms (7112bf6)
@@ -287,6 +287,10 @@ Requires a `revalidate` option per route (via `meta.ssg.revalidate: 60`).
287
287
 
288
288
  **Files:**
289
289
  - `src/cli/commands/preview.ts` — ISR cache layer
290
+ - `src/runtime/isr-handler.ts` — `createIsrHandler` factory (shared by all platforms)
291
+ - `src/cli/adapters/vercel.ts` — uses `isrHandler` for SSR fallback
292
+ - `src/cli/adapters/netlify.ts` — uses `isrHandler` for SSR fallback
293
+ - `src/cli/adapters/cloudflare.ts` — uses `isrHandler` for SSR fallback
290
294
  - `src/plugin/virtual/routes.ts` — include `meta.ssg.revalidate` in route
291
295
  - `src/types/page.ts` — add `revalidate` to `PageSsgConfig`
292
296
 
package/ROADMAP.md CHANGED
@@ -98,6 +98,47 @@ but lacks basic production safeguards.
98
98
 
99
99
  ## Phase 9 — Auth Primitives
100
100
 
101
+ ### 9.0 Server middleware + `useSession()` ✅
102
+
103
+ **Problem:** No way to intercept every SSR/API request before routing (auth,
104
+ CORS, logging). No isomorphic session management.
105
+
106
+ **Design:**
107
+
108
+ *Convention:* Files in `server/middleware/` export a default
109
+ `defineServerMiddleware()` function. They run in alphabetical order on every
110
+ request before API routes and before SSR rendering.
111
+
112
+ ```ts
113
+ // server/middleware/auth.ts
114
+ export default defineServerMiddleware(async (req, res, next) => {
115
+ const session = useSession<{ userId: string }>()
116
+ const data = await session.get()
117
+ if (!data?.userId) { res.statusCode = 401; res.end('Unauthorized'); return }
118
+ ;(req as any).user = data
119
+ next()
120
+ })
121
+ ```
122
+
123
+ `useSession<T>(options?)` — HMAC-SHA-256 signed cookie session:
124
+ - Signing key: `runtimeConfig.private.sessionSecret` (resolved from
125
+ `SESSION_SECRET` env var at server startup).
126
+ - `get()` → verifies signature, returns typed data or `null`.
127
+ - `set(data)` → signs and writes `httpOnly` cookie.
128
+ - `clear()` → sets `maxAge = 0`.
129
+ - Web Crypto API (`crypto.subtle`) — works in Node.js ≥ 18 and Cloudflare Workers.
130
+
131
+ **Files:**
132
+ - `src/plugin/virtual/server-middleware.ts` — generator (existed, now wired)
133
+ - `src/runtime/entry-server-template.ts` — `runServerMiddleware()` exported
134
+ - `src/cli/adapters/netlify.ts` / `vercel.ts` / `cloudflare.ts` — call it before routing
135
+ - `src/runtime/composables/define-server-middleware.ts` — new composable
136
+ - `src/runtime/composables/use-session.ts` — new composable
137
+ - `src/runtime/composables/index.ts` — re-exports
138
+ - `src/plugin/dts-generator.ts` — globals + `virtual:cer-server-middleware` decl
139
+
140
+ ---
141
+
101
142
  ### 9.1 Client-side route middleware (navigation guards) ✅
102
143
 
103
144
  **Problem:** There is no way to intercept client-side navigations to redirect
@@ -203,22 +244,32 @@ response during SSR and `document.cookie` on the client.
203
244
 
204
245
  ## Phase 10 — Platform Adapters
205
246
 
206
- ### 10.1 Cloudflare Workers adapter 🔜
247
+ ### 10.1 Cloudflare Workers adapter
207
248
 
208
249
  **Problem:** The server bundle uses Node.js streams (`AsyncLocalStorage`,
209
250
  `createReadStream`, `IncomingMessage`). Cloudflare Workers run a Web
210
251
  platform environment without these.
211
252
 
212
- **Design:**
213
- - Replace `renderToStreamWithJITCSSDSD` with a Web Streams equivalent in
214
- `entry-server-template.ts` when the `cloudflare` adapter is selected.
215
- - Swap `AsyncLocalStorage` with `AsyncContext` (TC39 proposal, available in
216
- Workers) or a request-scoped `Map`.
217
- - The adapter wraps the handler as a `fetch(Request): Response` function.
218
- - Build output: a single `worker.js` compatible with `wrangler deploy`.
253
+ **Solution:** Cloudflare Pages Advanced Mode (`_worker.js`) with
254
+ `nodejs_compat` compatibility flag. Key techniques:
255
+ - `nodejs_compat` provides `AsyncLocalStorage`, `node:stream` (`Readable.from`)
256
+ and `node:buffer` everything the SSR bundle needs.
257
+ - `dist/client/index.html` is read at adapter-run time and inlined as a string
258
+ constant in `_worker.js` via `globalThis.__CER_CLIENT_TEMPLATE__`, set using
259
+ top-level `await` before the server bundle is dynamically imported. This
260
+ bypasses the `node:fs` call in the bundle's module init without changing the
261
+ server entry template beyond adding a graceful try-catch fallback.
262
+ - The worker bridge mirrors the Netlify bridge (Web Request → Node.js mock →
263
+ Response) and runs `runServerMiddleware` + API routing before SSR.
264
+ - Responses are buffered (Cloudflare Functions limitation, same as Netlify).
219
265
 
220
- **Complexity:** High. Requires a new server entry template and build pipeline
221
- changes.
266
+ **Files:**
267
+ - `src/cli/adapters/cloudflare.ts` — Cloudflare Pages adapter
268
+ - `src/runtime/entry-server-template.ts` — `globalThis.__CER_CLIENT_TEMPLATE__`
269
+ fallback + try-catch around `readFileSync`
270
+ - `src/cli/commands/adapt.ts` — `cer-app adapt --platform cloudflare`
271
+ - `src/types/config.ts` — `'cloudflare'` added to `adapter` union
272
+ - `src/cli/commands/build.ts` — auto-runs adapter post-build
222
273
 
223
274
  ---
224
275
 
@@ -280,9 +331,10 @@ canonical URL. No new infrastructure needed — all forwarded to `<head>`.
280
331
  | 8.1 | Path traversal fix in preview server | 🔴 Critical | ✅ |
281
332
  | 8.2 | `runtimeConfig.private` (server-only secrets) | 🔴 Critical | ✅ |
282
333
  | 8.3 | Preview server hardening (headers, timeouts, graceful shutdown) | 🟡 High | ✅ |
334
+ | 9.0 | Server middleware + `useSession()` | 🟡 High | ✅ |
283
335
  | 9.1 | Client-side route middleware (navigation guards) | 🟡 High | ✅ |
284
336
  | 9.2 | `useCookie()` composable | 🟡 High | ✅ |
285
- | 10.1 | Cloudflare Workers adapter | 🟢 Medium | 🔜 |
337
+ | 10.1 | Cloudflare Workers adapter | 🟢 Medium | |
286
338
  | 10.2 | Vercel adapter | 🟢 Medium | ✅ |
287
339
  | 10.3 | Netlify adapter | 🟢 Medium | ✅ |
288
340
  | 11.1 | DevTools overlay | 🟢 Medium | ❌ |
package/commits.txt CHANGED
@@ -1 +1 @@
1
- - feat: add adapters for Vercel and Netlify deployment platforms (7112bf6)
1
+ - feat: implement ISR support with isrHandler for SSR fallback in Cloudflare, Netlify, and Vercel adapters (ddcc4d4)
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Cloudflare Pages adapter.
3
+ *
4
+ * Transforms the cer-app `dist/` output into the files needed to deploy on
5
+ * Cloudflare Pages with an edge-side SSR worker.
6
+ *
7
+ * SSR mode:
8
+ * - Reads dist/client/index.html and inlines it as a string constant in the
9
+ * generated `_worker.js` (Cloudflare Pages Advanced Mode convention).
10
+ * The worker sets globalThis.__CER_CLIENT_TEMPLATE__ before dynamically
11
+ * importing the server bundle, bypassing the node:fs call in server.js.
12
+ * - Copies content-hashed assets and other public files to dist/ alongside
13
+ * _worker.js — Cloudflare Pages serves them from the CDN before the worker.
14
+ * - Writes `wrangler.toml` with nodejs_compat for AsyncLocalStorage + streams.
15
+ *
16
+ * SPA/SSG mode:
17
+ * - Copies static files to dist/ (already in correct layout after build).
18
+ * - Writes wrangler.toml pointing at dist/ with no function.
19
+ *
20
+ * Cloudflare Pages Advanced Mode (_worker.js) reference:
21
+ * https://developers.cloudflare.com/pages/functions/advanced-mode/
22
+ *
23
+ * nodejs_compat compatibility flag reference:
24
+ * https://developers.cloudflare.com/workers/runtime-apis/nodejs/
25
+ */
26
+ export declare function runCloudflareAdapter(root: string): Promise<void>;
27
+ //# sourceMappingURL=cloudflare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../../src/cli/adapters/cloudflare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAuLH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtE"}
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Cloudflare Pages adapter.
3
+ *
4
+ * Transforms the cer-app `dist/` output into the files needed to deploy on
5
+ * Cloudflare Pages with an edge-side SSR worker.
6
+ *
7
+ * SSR mode:
8
+ * - Reads dist/client/index.html and inlines it as a string constant in the
9
+ * generated `_worker.js` (Cloudflare Pages Advanced Mode convention).
10
+ * The worker sets globalThis.__CER_CLIENT_TEMPLATE__ before dynamically
11
+ * importing the server bundle, bypassing the node:fs call in server.js.
12
+ * - Copies content-hashed assets and other public files to dist/ alongside
13
+ * _worker.js — Cloudflare Pages serves them from the CDN before the worker.
14
+ * - Writes `wrangler.toml` with nodejs_compat for AsyncLocalStorage + streams.
15
+ *
16
+ * SPA/SSG mode:
17
+ * - Copies static files to dist/ (already in correct layout after build).
18
+ * - Writes wrangler.toml pointing at dist/ with no function.
19
+ *
20
+ * Cloudflare Pages Advanced Mode (_worker.js) reference:
21
+ * https://developers.cloudflare.com/pages/functions/advanced-mode/
22
+ *
23
+ * nodejs_compat compatibility flag reference:
24
+ * https://developers.cloudflare.com/workers/runtime-apis/nodejs/
25
+ */
26
+ import { join } from 'pathe';
27
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, cpSync, readdirSync, statSync, } from 'node:fs';
28
+ // ─── Worker bridge template ───────────────────────────────────────────────────
29
+ /**
30
+ * Generates the _worker.js content with the client HTML inlined.
31
+ *
32
+ * Key design points:
33
+ * - Sets globalThis.__CER_CLIENT_TEMPLATE__ BEFORE dynamically importing the
34
+ * server bundle so the bundle's module-init code sees the value instead of
35
+ * trying readFileSync (which throws in Workers without node:fs).
36
+ * - Uses top-level await (supported in Cloudflare Workers ES modules).
37
+ * - Mirrors the Netlify bridge's Web Request → Node.js mock → Response pattern;
38
+ * Cloudflare Workers with nodejs_compat support node:stream + AsyncLocalStorage.
39
+ * - Exports `{ fetch }` — the Cloudflare Pages _worker.js module format.
40
+ */
41
+ function generateWorkerBridge(clientHtml) {
42
+ // Escape the HTML for embedding in a JS template literal.
43
+ const escaped = clientHtml.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
44
+ return `\
45
+ // Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
46
+ // Cloudflare Pages _worker.js — Advanced Mode SSR bridge.
47
+ // Requires compatibility_flags = ["nodejs_compat"] in wrangler.toml.
48
+ import { Readable } from 'node:stream'
49
+
50
+ // Inline the client HTML template so the server bundle does not need node:fs.
51
+ // Must be set before the dynamic import below resolves.
52
+ globalThis.__CER_CLIENT_TEMPLATE__ = \`${escaped}\`
53
+
54
+ const { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
55
+
56
+ function matchApiPattern(pattern, urlPath) {
57
+ const pp = pattern.split('/')
58
+ const up = urlPath.split('/')
59
+ if (pp.length !== up.length) return null
60
+ const params = {}
61
+ for (let i = 0; i < pp.length; i++) {
62
+ if (pp[i].startsWith(':')) params[pp[i].slice(1)] = decodeURIComponent(up[i])
63
+ else if (pp[i] !== up[i]) return null
64
+ }
65
+ return params
66
+ }
67
+
68
+ function parseQuery(search) {
69
+ if (!search) return {}
70
+ const qs = search.startsWith('?') ? search.slice(1) : search
71
+ const result = {}
72
+ for (const part of qs.split('&')) {
73
+ if (!part) continue
74
+ const eqIdx = part.indexOf('=')
75
+ if (eqIdx === -1) result[decodeURIComponent(part)] = ''
76
+ else result[decodeURIComponent(part.slice(0, eqIdx))] = decodeURIComponent(part.slice(eqIdx + 1))
77
+ }
78
+ return result
79
+ }
80
+
81
+ async function toNodeRequest(webReq) {
82
+ const url = new URL(webReq.url)
83
+ const hasBody = webReq.body !== null
84
+ && webReq.method !== 'GET'
85
+ && webReq.method !== 'HEAD'
86
+ const body = hasBody ? Buffer.from(await webReq.arrayBuffer()) : null
87
+ const req = Object.assign(
88
+ body ? Readable.from([body]) : Readable.from([]),
89
+ {
90
+ url: url.pathname + url.search,
91
+ method: webReq.method,
92
+ headers: Object.fromEntries(webReq.headers.entries()),
93
+ },
94
+ )
95
+ req.query = parseQuery(url.search)
96
+ if (body !== null) {
97
+ const ct = webReq.headers.get('content-type') ?? ''
98
+ if (ct.includes('application/json')) {
99
+ try { req.body = JSON.parse(body.toString('utf-8')) } catch { req.body = undefined }
100
+ } else {
101
+ req.body = body
102
+ }
103
+ }
104
+ return req
105
+ }
106
+
107
+ function createNodeResponse() {
108
+ const chunks = []
109
+ const headers = {}
110
+ let _resolve
111
+ let _ended = false
112
+ const promise = new Promise((res) => { _resolve = res })
113
+ const res = {
114
+ statusCode: 200,
115
+ setHeader(name, value) { headers[name.toLowerCase()] = String(value) },
116
+ getHeader(name) { return headers[name.toLowerCase()] },
117
+ removeHeader(name) { delete headers[name.toLowerCase()] },
118
+ write(chunk) {
119
+ if (_ended) return
120
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
121
+ },
122
+ end(chunk) {
123
+ if (_ended) return
124
+ _ended = true
125
+ if (chunk) {
126
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
127
+ }
128
+ _resolve(new Response(Buffer.concat(chunks), { status: res.statusCode, headers }))
129
+ },
130
+ json(data) {
131
+ this.setHeader('Content-Type', 'application/json; charset=utf-8')
132
+ this.end(JSON.stringify(data))
133
+ },
134
+ status(code) { this.statusCode = code; return this },
135
+ get writableEnded() { return _ended },
136
+ }
137
+ return { res, promise }
138
+ }
139
+
140
+ async function handleRequest(webReq) {
141
+ const url = new URL(webReq.url)
142
+ const urlPath = url.pathname
143
+ const method = webReq.method ?? 'GET'
144
+
145
+ const nodeReq = await toNodeRequest(webReq)
146
+ const { res, promise } = createNodeResponse()
147
+
148
+ // Run server middleware chain (auth, logging, CORS, etc.).
149
+ if (!(await runServerMiddleware(nodeReq, res))) return promise
150
+
151
+ // Route /api/* requests to the API handlers exported by the server bundle.
152
+ if (urlPath.startsWith('/api/')) {
153
+ for (const route of (apiRoutes ?? [])) {
154
+ const params = matchApiPattern(route.path, urlPath)
155
+ if (params !== null) {
156
+ nodeReq.params = params
157
+ const fn = route.handlers[method.toLowerCase()]
158
+ ?? route.handlers[method.toUpperCase()]
159
+ ?? route.handlers['default']
160
+ if (typeof fn === 'function') {
161
+ // Wrap in request context so useCookie / useSession work in API handlers.
162
+ runWithRequestContext(nodeReq, res, () => Promise.resolve(fn(nodeReq, res))).catch(() => {
163
+ if (!res.writableEnded) {
164
+ res.statusCode = 500
165
+ res.end(JSON.stringify({ error: 'Internal Server Error' }))
166
+ }
167
+ })
168
+ return promise
169
+ }
170
+ }
171
+ }
172
+ return new Response('Not Found', { status: 404 })
173
+ }
174
+
175
+ // All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
176
+ isrHandler(nodeReq, res).catch(() => {
177
+ if (!res.writableEnded) {
178
+ res.statusCode = 500
179
+ res.end('Internal Server Error')
180
+ }
181
+ })
182
+ return promise
183
+ }
184
+
185
+ export default {
186
+ async fetch(request, _env, _ctx) {
187
+ return handleRequest(request)
188
+ },
189
+ }
190
+ `;
191
+ }
192
+ // ─── Public API ───────────────────────────────────────────────────────────────
193
+ export async function runCloudflareAdapter(root) {
194
+ const distDir = join(root, 'dist');
195
+ if (!existsSync(distDir)) {
196
+ throw new Error(`[cer-app] No dist/ directory found at ${distDir}. Run 'cer-app build' first.`);
197
+ }
198
+ const serverBundle = join(distDir, 'server/server.js');
199
+ const ssgManifest = join(distDir, 'ssg-manifest.json');
200
+ const isSSR = existsSync(serverBundle) && !existsSync(ssgManifest);
201
+ if (isSSR) {
202
+ _buildSSR(root, distDir);
203
+ }
204
+ else {
205
+ _buildStatic(root, distDir);
206
+ }
207
+ console.log('[cer-app] Cloudflare adapter complete.');
208
+ if (isSSR) {
209
+ console.log(' Worker: dist/_worker.js');
210
+ console.log(' Static: dist/');
211
+ console.log(' Deploy with: wrangler pages deploy dist');
212
+ }
213
+ else {
214
+ console.log(' Deploy with: wrangler pages deploy dist');
215
+ }
216
+ }
217
+ // ─── SSR output ───────────────────────────────────────────────────────────────
218
+ function _buildSSR(root, distDir) {
219
+ const clientHtmlPath = join(distDir, 'client/index.html');
220
+ const clientHtml = existsSync(clientHtmlPath)
221
+ ? readFileSync(clientHtmlPath, 'utf-8')
222
+ : '';
223
+ // Write the worker bridge with inlined client HTML.
224
+ writeFileSync(join(distDir, '_worker.js'), generateWorkerBridge(clientHtml));
225
+ // Copy assets from dist/client/ into dist/ (at the same URL paths).
226
+ // Cloudflare Pages CDN serves files in the deploy directory as static first;
227
+ // requests that don't match a static file fall through to _worker.js.
228
+ // We deliberately skip index.html so HTML requests hit the SSR worker.
229
+ _copyClientAssets(join(distDir, 'client'), distDir);
230
+ // wrangler.toml: nodejs_compat for AsyncLocalStorage + node:stream support.
231
+ _writeWranglerToml(root, true);
232
+ }
233
+ // ─── Static (SPA / SSG) output ────────────────────────────────────────────────
234
+ function _buildStatic(root, distDir) {
235
+ const clientDir = join(distDir, 'client');
236
+ if (existsSync(clientDir)) {
237
+ // SSG: pre-rendered HTML lives in dist/; assets in dist/client/.
238
+ // Move assets alongside the HTML so Cloudflare Pages serves them correctly.
239
+ _copyClientAssets(clientDir, distDir);
240
+ }
241
+ // SPA: everything is already in dist/ — nothing to reorganise.
242
+ _writeWranglerToml(root, false);
243
+ }
244
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
245
+ function _writeWranglerToml(root, ssrMode) {
246
+ const lines = [
247
+ '# Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.',
248
+ 'name = "cer-app"',
249
+ 'compatibility_date = "2024-09-23"',
250
+ ];
251
+ if (ssrMode) {
252
+ lines.push('compatibility_flags = ["nodejs_compat"]');
253
+ }
254
+ lines.push('');
255
+ lines.push('[pages_build_output_dir]');
256
+ lines.push('dir = "dist"');
257
+ lines.push('');
258
+ writeFileSync(join(root, 'wrangler.toml'), lines.join('\n'));
259
+ }
260
+ /**
261
+ * Copies Vite-hashed assets and other public files (favicon, robots.txt, …)
262
+ * from the client dist directory into `destDir`.
263
+ * Skips index.html — served by the SSR worker or SSG pre-rendering.
264
+ */
265
+ function _copyClientAssets(clientDir, destDir) {
266
+ if (!existsSync(clientDir))
267
+ return;
268
+ mkdirSync(destDir, { recursive: true });
269
+ for (const entry of readdirSync(clientDir)) {
270
+ if (entry === 'index.html')
271
+ continue;
272
+ const src = join(clientDir, entry);
273
+ const dest = join(destDir, entry);
274
+ if (statSync(src).isDirectory()) {
275
+ cpSync(src, dest, { recursive: true });
276
+ }
277
+ else {
278
+ copyFileSync(src, dest);
279
+ }
280
+ }
281
+ }
282
+ //# sourceMappingURL=cloudflare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../../src/cli/adapters/cloudflare.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"netlify.d.ts","sourceRoot":"","sources":["../../../src/cli/adapters/netlify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA8IH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BnE"}
1
+ {"version":3,"file":"netlify.d.ts","sourceRoot":"","sources":["../../../src/cli/adapters/netlify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAuKH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BnE"}
@@ -38,7 +38,7 @@ import { existsSync, mkdirSync, writeFileSync, copyFileSync, cpSync, readdirSync
38
38
  const NETLIFY_SSR_BRIDGE = `\
39
39
  // Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
40
40
  import { Readable } from 'node:stream'
41
- import { handler, apiRoutes } from '../../dist/server/server.js'
41
+ import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
42
42
 
43
43
  function matchApiPattern(pattern, urlPath) {
44
44
  const pp = pattern.split('/')
@@ -52,6 +52,19 @@ function matchApiPattern(pattern, urlPath) {
52
52
  return params
53
53
  }
54
54
 
55
+ function parseQuery(search) {
56
+ if (!search) return {}
57
+ const qs = search.startsWith('?') ? search.slice(1) : search
58
+ const result = {}
59
+ for (const part of qs.split('&')) {
60
+ if (!part) continue
61
+ const eqIdx = part.indexOf('=')
62
+ if (eqIdx === -1) result[decodeURIComponent(part)] = ''
63
+ else result[decodeURIComponent(part.slice(0, eqIdx))] = decodeURIComponent(part.slice(eqIdx + 1))
64
+ }
65
+ return result
66
+ }
67
+
55
68
  async function toNodeRequest(webReq) {
56
69
  const url = new URL(webReq.url)
57
70
  const hasBody = webReq.body !== null
@@ -66,6 +79,15 @@ async function toNodeRequest(webReq) {
66
79
  headers: Object.fromEntries(webReq.headers.entries()),
67
80
  },
68
81
  )
82
+ req.query = parseQuery(url.search)
83
+ if (body !== null) {
84
+ const ct = webReq.headers.get('content-type') ?? ''
85
+ if (ct.includes('application/json')) {
86
+ try { req.body = JSON.parse(body.toString('utf-8')) } catch { req.body = undefined }
87
+ } else {
88
+ req.body = body
89
+ }
90
+ }
69
91
  return req
70
92
  }
71
93
 
@@ -108,20 +130,25 @@ export default async (webReq) => {
108
130
  const urlPath = url.pathname
109
131
  const method = webReq.method ?? 'GET'
110
132
 
133
+ // Convert to Node.js-style req/res upfront so server middleware can run.
134
+ const nodeReq = await toNodeRequest(webReq)
135
+ const { res, promise } = createNodeResponse()
136
+
137
+ // Run server middleware chain (auth, logging, CORS, etc.).
138
+ if (!(await runServerMiddleware(nodeReq, res))) return promise
139
+
111
140
  // Route /api/* requests to the API handlers exported by the server bundle.
112
141
  if (urlPath.startsWith('/api/')) {
113
142
  for (const route of (apiRoutes ?? [])) {
114
143
  const params = matchApiPattern(route.path, urlPath)
115
144
  if (params !== null) {
116
- const nodeReq = await toNodeRequest(webReq)
117
145
  nodeReq.params = params
118
- const { res, promise } = createNodeResponse()
119
146
  const fn = route.handlers[method.toLowerCase()]
120
147
  ?? route.handlers[method.toUpperCase()]
121
148
  ?? route.handlers['default']
122
149
  if (typeof fn === 'function') {
123
- // Use Promise.resolve() so the catch works for both sync and async handlers.
124
- Promise.resolve(fn(nodeReq, res)).catch(() => {
150
+ // Wrap in request context so useCookie / useSession work in API handlers.
151
+ runWithRequestContext(nodeReq, res, () => Promise.resolve(fn(nodeReq, res))).catch(() => {
125
152
  if (!res.writableEnded) {
126
153
  res.statusCode = 500
127
154
  res.end(JSON.stringify({ error: 'Internal Server Error' }))
@@ -134,10 +161,8 @@ export default async (webReq) => {
134
161
  return new Response('Not Found', { status: 404 })
135
162
  }
136
163
 
137
- // All other requests: SSR.
138
- const nodeReq = await toNodeRequest(webReq)
139
- const { res, promise } = createNodeResponse()
140
- handler(nodeReq, res).catch(() => {
164
+ // All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
165
+ isrHandler(nodeReq, res).catch(() => {
141
166
  if (!res.writableEnded) {
142
167
  res.statusCode = 500
143
168
  res.end('Internal Server Error')
@@ -1 +1 @@
1
- {"version":3,"file":"netlify.js","sourceRoot":"","sources":["../../../src/cli/adapters/netlify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"netlify.js","sourceRoot":"","sources":["../../../src/cli/adapters/netlify.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/cli/adapters/vercel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAqFH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BlE"}
1
+ {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../../src/cli/adapters/vercel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA4HH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BlE"}
@@ -30,7 +30,7 @@ import { existsSync, mkdirSync, writeFileSync, copyFileSync, cpSync, readdirSync
30
30
  */
31
31
  const VERCEL_LAUNCHER = `\
32
32
  // Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
33
- import { handler, apiRoutes } from './server/server.js'
33
+ import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from './server/server.js'
34
34
 
35
35
  function matchApiPattern(pattern, urlPath) {
36
36
  const pp = pattern.split('/')
@@ -44,7 +44,44 @@ function matchApiPattern(pattern, urlPath) {
44
44
  return params
45
45
  }
46
46
 
47
+ function parseQuery(url) {
48
+ const qIndex = url.indexOf('?')
49
+ if (qIndex === -1) return {}
50
+ const qs = url.slice(qIndex + 1)
51
+ const result = {}
52
+ for (const part of qs.split('&')) {
53
+ if (!part) continue
54
+ const eqIdx = part.indexOf('=')
55
+ if (eqIdx === -1) result[decodeURIComponent(part)] = ''
56
+ else result[decodeURIComponent(part.slice(0, eqIdx))] = decodeURIComponent(part.slice(eqIdx + 1))
57
+ }
58
+ return result
59
+ }
60
+
61
+ async function readBody(req) {
62
+ return new Promise((resolve, reject) => {
63
+ const chunks = []
64
+ req.on('data', chunk => chunks.push(Buffer.from(chunk)))
65
+ req.on('end', () => resolve(Buffer.concat(chunks)))
66
+ req.on('error', reject)
67
+ })
68
+ }
69
+
70
+ async function parseBody(req) {
71
+ const contentType = req.headers['content-type'] ?? ''
72
+ const method = req.method?.toUpperCase() ?? 'GET'
73
+ if (!['POST', 'PUT', 'PATCH'].includes(method)) return undefined
74
+ const buf = await readBody(req)
75
+ if (contentType.includes('application/json')) {
76
+ try { return JSON.parse(buf.toString('utf-8')) } catch { return undefined }
77
+ }
78
+ return buf
79
+ }
80
+
47
81
  export default async function cerAppHandler(req, res) {
82
+ // Run server middleware chain (auth, logging, CORS, etc.).
83
+ if (!(await runServerMiddleware(req, res))) return
84
+
48
85
  const urlPath = (req.url ?? '/').split('?')[0]
49
86
  const method = req.method ?? 'GET'
50
87
 
@@ -54,6 +91,8 @@ export default async function cerAppHandler(req, res) {
54
91
  const params = matchApiPattern(route.path, urlPath)
55
92
  if (params !== null) {
56
93
  req.params = params
94
+ req.query = parseQuery(req.url ?? '/')
95
+ req.body = await parseBody(req)
57
96
  res.json = function (data) {
58
97
  this.setHeader('Content-Type', 'application/json; charset=utf-8')
59
98
  this.end(JSON.stringify(data))
@@ -64,7 +103,7 @@ export default async function cerAppHandler(req, res) {
64
103
  ?? route.handlers['default']
65
104
  if (typeof fn === 'function') {
66
105
  try {
67
- await fn(req, res)
106
+ await runWithRequestContext(req, res, () => Promise.resolve(fn(req, res)))
68
107
  } catch {
69
108
  if (!res.writableEnded) {
70
109
  res.statusCode = 500
@@ -81,8 +120,8 @@ export default async function cerAppHandler(req, res) {
81
120
  return
82
121
  }
83
122
 
84
- // All other requests: SSR.
85
- await handler(req, res)
123
+ // All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
124
+ await isrHandler(req, res)
86
125
  }
87
126
  `;
88
127
  // ─── Public API ───────────────────────────────────────────────────────────────