@jasonshimmy/vite-plugin-cer-app 0.9.0 → 0.11.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 (108) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/ROADMAP.md +80 -16
  3. package/commits.txt +1 -1
  4. package/dist/cli/adapters/cloudflare.d.ts +27 -0
  5. package/dist/cli/adapters/cloudflare.d.ts.map +1 -0
  6. package/dist/cli/adapters/cloudflare.js +282 -0
  7. package/dist/cli/adapters/cloudflare.js.map +1 -0
  8. package/dist/cli/adapters/netlify.d.ts +24 -0
  9. package/dist/cli/adapters/netlify.d.ts.map +1 -0
  10. package/dist/cli/adapters/netlify.js +291 -0
  11. package/dist/cli/adapters/netlify.js.map +1 -0
  12. package/dist/cli/adapters/vercel.d.ts +20 -0
  13. package/dist/cli/adapters/vercel.d.ts.map +1 -0
  14. package/dist/cli/adapters/vercel.js +256 -0
  15. package/dist/cli/adapters/vercel.js.map +1 -0
  16. package/dist/cli/commands/adapt.d.ts +3 -0
  17. package/dist/cli/commands/adapt.d.ts.map +1 -0
  18. package/dist/cli/commands/adapt.js +29 -0
  19. package/dist/cli/commands/adapt.js.map +1 -0
  20. package/dist/cli/commands/build.d.ts.map +1 -1
  21. package/dist/cli/commands/build.js +23 -0
  22. package/dist/cli/commands/build.js.map +1 -1
  23. package/dist/cli/commands/preview.d.ts.map +1 -1
  24. package/dist/cli/commands/preview.js +66 -2
  25. package/dist/cli/commands/preview.js.map +1 -1
  26. package/dist/cli/index.js +2 -0
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/plugin/dev-server.d.ts.map +1 -1
  29. package/dist/plugin/dev-server.js +23 -6
  30. package/dist/plugin/dev-server.js.map +1 -1
  31. package/dist/plugin/dts-generator.d.ts.map +1 -1
  32. package/dist/plugin/dts-generator.js +7 -1
  33. package/dist/plugin/dts-generator.js.map +1 -1
  34. package/dist/plugin/index.d.ts.map +1 -1
  35. package/dist/plugin/index.js +1 -0
  36. package/dist/plugin/index.js.map +1 -1
  37. package/dist/plugin/transforms/auto-import.d.ts +2 -0
  38. package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
  39. package/dist/plugin/transforms/auto-import.js +6 -3
  40. package/dist/plugin/transforms/auto-import.js.map +1 -1
  41. package/dist/runtime/composables/define-server-middleware.d.ts +16 -0
  42. package/dist/runtime/composables/define-server-middleware.d.ts.map +1 -0
  43. package/dist/runtime/composables/define-server-middleware.js +17 -0
  44. package/dist/runtime/composables/define-server-middleware.js.map +1 -0
  45. package/dist/runtime/composables/index.d.ts +3 -0
  46. package/dist/runtime/composables/index.d.ts.map +1 -1
  47. package/dist/runtime/composables/index.js +2 -0
  48. package/dist/runtime/composables/index.js.map +1 -1
  49. package/dist/runtime/composables/use-session.d.ts +59 -0
  50. package/dist/runtime/composables/use-session.d.ts.map +1 -0
  51. package/dist/runtime/composables/use-session.js +125 -0
  52. package/dist/runtime/composables/use-session.js.map +1 -0
  53. package/dist/runtime/entry-server-template.d.ts +1 -1
  54. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  55. package/dist/runtime/entry-server-template.js +52 -5
  56. package/dist/runtime/entry-server-template.js.map +1 -1
  57. package/dist/types/config.d.ts +12 -0
  58. package/dist/types/config.d.ts.map +1 -1
  59. package/dist/types/config.js.map +1 -1
  60. package/dist/types/middleware.d.ts +1 -1
  61. package/dist/types/middleware.d.ts.map +1 -1
  62. package/docs/cli.md +63 -0
  63. package/docs/composables.md +116 -0
  64. package/docs/getting-started.md +36 -0
  65. package/docs/middleware.md +13 -7
  66. package/docs/routing.md +27 -0
  67. package/docs/server-api.md +72 -0
  68. package/e2e/cypress/e2e/api.cy.ts +42 -0
  69. package/e2e/cypress/e2e/server-middleware.cy.ts +46 -0
  70. package/e2e/cypress/e2e/session.cy.ts +73 -0
  71. package/e2e/cypress/tsconfig.json +14 -0
  72. package/e2e/kitchen-sink/cer.config.ts +5 -0
  73. package/e2e/kitchen-sink/server/api/echo.ts +12 -0
  74. package/e2e/kitchen-sink/server/api/session.ts +23 -0
  75. package/e2e/kitchen-sink/server/middleware/01-headers.ts +6 -0
  76. package/e2e/scripts/clean.mjs +7 -1
  77. package/package.json +3 -1
  78. package/src/__tests__/cli/adapters/cloudflare-worker.integration.test.ts +130 -0
  79. package/src/__tests__/cli/adapters/cloudflare.test.ts +240 -0
  80. package/src/__tests__/cli/adapters/netlify-bridge.integration.test.ts +138 -0
  81. package/src/__tests__/cli/adapters/netlify.test.ts +241 -0
  82. package/src/__tests__/cli/adapters/vercel-launcher.integration.test.ts +129 -0
  83. package/src/__tests__/cli/adapters/vercel.test.ts +247 -0
  84. package/src/__tests__/cli/preview-hardening.test.ts +90 -0
  85. package/src/__tests__/plugin/cer-app-plugin.test.ts +1 -1
  86. package/src/__tests__/plugin/dev-server.test.ts +67 -0
  87. package/src/__tests__/plugin/dts-generator.test.ts +35 -0
  88. package/src/__tests__/plugin/entry-server-template.test.ts +10 -1
  89. package/src/__tests__/plugin/transforms/auto-import.test.ts +49 -0
  90. package/src/__tests__/runtime/use-cookie.test.ts +38 -0
  91. package/src/__tests__/runtime/use-session.test.ts +237 -0
  92. package/src/cli/adapters/cloudflare.ts +311 -0
  93. package/src/cli/adapters/netlify.ts +320 -0
  94. package/src/cli/adapters/vercel.ts +311 -0
  95. package/src/cli/commands/adapt.ts +31 -0
  96. package/src/cli/commands/build.ts +23 -0
  97. package/src/cli/commands/preview.ts +64 -3
  98. package/src/cli/index.ts +2 -0
  99. package/src/plugin/dev-server.ts +13 -6
  100. package/src/plugin/dts-generator.ts +7 -1
  101. package/src/plugin/index.ts +1 -0
  102. package/src/plugin/transforms/auto-import.ts +8 -3
  103. package/src/runtime/composables/define-server-middleware.ts +18 -0
  104. package/src/runtime/composables/index.ts +3 -0
  105. package/src/runtime/composables/use-session.ts +169 -0
  106. package/src/runtime/entry-server-template.ts +52 -5
  107. package/src/types/config.ts +12 -0
  108. 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.11.0] - 2026-03-22
5
+
6
+ - feat: add Cloudflare Pages adapter for SSR and SSG support (2734c26)
7
+
8
+ ## [v0.10.0] - 2026-03-22
9
+
10
+ - feat: add adapters for Vercel and Netlify deployment platforms (7112bf6)
11
+
4
12
  ## [v0.9.0] - 2026-03-21
5
13
 
6
14
  - feat: add useCookie and useSeoMeta composables with tests (6536902)
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,26 +244,36 @@ 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
 
225
- ### 10.2 Vercel adapter 🔜
276
+ ### 10.2 Vercel adapter
226
277
 
227
278
  **Problem:** Vercel expects functions in `.vercel/output/functions/` with a
228
279
  specific manifest format.
@@ -233,11 +284,23 @@ Static assets are moved to `.vercel/output/static/`.
233
284
 
234
285
  **Complexity:** Medium. Mostly file system manipulation.
235
286
 
287
+ **Files:**
288
+ - `src/cli/adapters/vercel.ts` — Vercel Build Output API v3 adapter
289
+ - `src/cli/commands/adapt.ts` — `cer-app adapt` command
290
+ - `src/types/config.ts` — `adapter` field on `CerAppConfig`
291
+ - `src/cli/commands/build.ts` — auto-runs adapter post-build
292
+
236
293
  ---
237
294
 
238
- ### 10.3 Netlify adapter 🔜
295
+ ### 10.3 Netlify adapter
239
296
 
240
- Similar to Vercel but targets Netlify Functions / Edge Functions format.
297
+ Similar to Vercel but targets Netlify Functions v2 format. Writes a bridge
298
+ function (`netlify/functions/ssr.mjs`) that converts between the Web
299
+ `Request`/`Response` API and the Node.js-style handler. Responses are
300
+ buffered (no streaming — Netlify Functions limitation).
301
+
302
+ **Files:**
303
+ - `src/cli/adapters/netlify.ts` — Netlify adapter
241
304
 
242
305
  ---
243
306
 
@@ -268,11 +331,12 @@ canonical URL. No new infrastructure needed — all forwarded to `<head>`.
268
331
  | 8.1 | Path traversal fix in preview server | 🔴 Critical | ✅ |
269
332
  | 8.2 | `runtimeConfig.private` (server-only secrets) | 🔴 Critical | ✅ |
270
333
  | 8.3 | Preview server hardening (headers, timeouts, graceful shutdown) | 🟡 High | ✅ |
334
+ | 9.0 | Server middleware + `useSession()` | 🟡 High | ✅ |
271
335
  | 9.1 | Client-side route middleware (navigation guards) | 🟡 High | ✅ |
272
336
  | 9.2 | `useCookie()` composable | 🟡 High | ✅ |
273
- | 10.1 | Cloudflare Workers adapter | 🟢 Medium | 🔜 |
274
- | 10.2 | Vercel adapter | 🟢 Medium | 🔜 |
275
- | 10.3 | Netlify adapter | 🟢 Medium | 🔜 |
337
+ | 10.1 | Cloudflare Workers adapter | 🟢 Medium | |
338
+ | 10.2 | Vercel adapter | 🟢 Medium | |
339
+ | 10.3 | Netlify adapter | 🟢 Medium | |
276
340
  | 11.1 | DevTools overlay | 🟢 Medium | ❌ |
277
341
  | 11.2 | i18n | 🟢 Medium | 🔜 |
278
342
  | 11.3 | `useSeoMeta()` | 🟢 Medium | ✅ |
package/commits.txt CHANGED
@@ -1 +1 @@
1
- - feat: add useCookie and useSeoMeta composables with tests (6536902)
1
+ - feat: add Cloudflare Pages adapter for SSR and SSG support (2734c26)
@@ -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, 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.
176
+ handler(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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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Netlify adapter.
3
+ *
4
+ * Transforms the cer-app `dist/` output into the files needed to deploy on
5
+ * Netlify.
6
+ *
7
+ * SSR mode:
8
+ * - Writes a `netlify/functions/ssr.mjs` bridge that converts Netlify
9
+ * Functions v2 Web API (Request/Response) to the Node.js-style handler
10
+ * used by the cer-app server bundle. Handles /api/* routing inline.
11
+ * - Creates `.netlify/publish/` with content-hashed assets only (no HTML),
12
+ * so Netlify's CDN serves static files and everything else falls through
13
+ * to the SSR function.
14
+ * - Writes `netlify.toml` with the publish directory and catch-all redirect.
15
+ *
16
+ * SPA/SSG mode:
17
+ * - Writes `netlify.toml` that points to the correct publish directory and
18
+ * adds a SPA fallback redirect (/* → /index.html).
19
+ *
20
+ * Netlify Functions v2 limitation: responses are buffered (no streaming).
21
+ * All other behaviour (cookies, headers, API routes, ISR) is fully supported.
22
+ */
23
+ export declare function runNetlifyAdapter(root: string): Promise<void>;
24
+ //# sourceMappingURL=netlify.d.ts.map
@@ -0,0 +1 @@
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"}