@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.
- package/CHANGELOG.md +8 -0
- package/IMPLEMENTATION_PLAN.md +4 -0
- package/ROADMAP.md +63 -11
- package/commits.txt +1 -1
- package/dist/cli/adapters/cloudflare.d.ts +27 -0
- package/dist/cli/adapters/cloudflare.d.ts.map +1 -0
- package/dist/cli/adapters/cloudflare.js +282 -0
- package/dist/cli/adapters/cloudflare.js.map +1 -0
- package/dist/cli/adapters/netlify.d.ts.map +1 -1
- package/dist/cli/adapters/netlify.js +34 -9
- package/dist/cli/adapters/netlify.js.map +1 -1
- package/dist/cli/adapters/vercel.d.ts.map +1 -1
- package/dist/cli/adapters/vercel.js +43 -4
- package/dist/cli/adapters/vercel.js.map +1 -1
- package/dist/cli/commands/adapt.d.ts.map +1 -1
- package/dist/cli/commands/adapt.js +6 -2
- package/dist/cli/commands/adapt.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +4 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +66 -2
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/plugin/dev-server.d.ts.map +1 -1
- package/dist/plugin/dev-server.js +23 -6
- package/dist/plugin/dev-server.js.map +1 -1
- package/dist/plugin/dts-generator.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +7 -1
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -0
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/transforms/auto-import.d.ts +2 -0
- package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
- package/dist/plugin/transforms/auto-import.js +6 -3
- package/dist/plugin/transforms/auto-import.js.map +1 -1
- package/dist/runtime/composables/define-server-middleware.d.ts +16 -0
- package/dist/runtime/composables/define-server-middleware.d.ts.map +1 -0
- package/dist/runtime/composables/define-server-middleware.js +17 -0
- package/dist/runtime/composables/define-server-middleware.js.map +1 -0
- package/dist/runtime/composables/index.d.ts +3 -0
- package/dist/runtime/composables/index.d.ts.map +1 -1
- package/dist/runtime/composables/index.js +2 -0
- package/dist/runtime/composables/index.js.map +1 -1
- package/dist/runtime/composables/use-session.d.ts +59 -0
- package/dist/runtime/composables/use-session.d.ts.map +1 -0
- package/dist/runtime/composables/use-session.js +125 -0
- package/dist/runtime/composables/use-session.js.map +1 -0
- package/dist/runtime/entry-server-template.d.ts +1 -1
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +52 -5
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/dist/types/config.d.ts +4 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/middleware.d.ts +1 -1
- package/dist/types/middleware.d.ts.map +1 -1
- package/docs/cli.md +13 -2
- package/docs/composables.md +116 -0
- package/docs/getting-started.md +36 -0
- package/docs/middleware.md +13 -7
- package/docs/rendering-modes.md +15 -5
- package/docs/routing.md +27 -0
- package/docs/server-api.md +72 -0
- package/e2e/cypress/e2e/api.cy.ts +42 -0
- package/e2e/cypress/e2e/server-middleware.cy.ts +46 -0
- package/e2e/cypress/e2e/session.cy.ts +73 -0
- package/e2e/cypress/tsconfig.json +14 -0
- package/e2e/kitchen-sink/cer.config.ts +5 -0
- package/e2e/kitchen-sink/server/api/echo.ts +12 -0
- package/e2e/kitchen-sink/server/api/session.ts +23 -0
- package/e2e/kitchen-sink/server/middleware/01-headers.ts +6 -0
- package/e2e/scripts/clean.mjs +2 -0
- package/package.json +1 -1
- package/src/__tests__/cli/adapters/cloudflare-worker.integration.test.ts +130 -0
- package/src/__tests__/cli/adapters/cloudflare.test.ts +247 -0
- package/src/__tests__/cli/adapters/netlify.test.ts +25 -2
- package/src/__tests__/cli/adapters/vercel-launcher.integration.test.ts +129 -0
- package/src/__tests__/cli/adapters/vercel.test.ts +22 -1
- package/src/__tests__/cli/preview-hardening.test.ts +90 -0
- package/src/__tests__/plugin/cer-app-plugin.test.ts +1 -1
- package/src/__tests__/plugin/dev-server.test.ts +67 -0
- package/src/__tests__/plugin/dts-generator.test.ts +35 -0
- package/src/__tests__/plugin/entry-server-template.test.ts +10 -1
- package/src/__tests__/plugin/transforms/auto-import.test.ts +49 -0
- package/src/__tests__/runtime/use-cookie.test.ts +38 -0
- package/src/__tests__/runtime/use-session.test.ts +237 -0
- package/src/cli/adapters/cloudflare.ts +311 -0
- package/src/cli/adapters/netlify.ts +34 -9
- package/src/cli/adapters/vercel.ts +43 -4
- package/src/cli/commands/adapt.ts +6 -2
- package/src/cli/commands/build.ts +4 -0
- package/src/cli/commands/preview.ts +64 -3
- package/src/plugin/dev-server.ts +13 -6
- package/src/plugin/dts-generator.ts +7 -1
- package/src/plugin/index.ts +1 -0
- package/src/plugin/transforms/auto-import.ts +8 -3
- package/src/runtime/composables/define-server-middleware.ts +18 -0
- package/src/runtime/composables/index.ts +3 -0
- package/src/runtime/composables/use-session.ts +169 -0
- package/src/runtime/entry-server-template.ts +52 -5
- package/src/types/config.ts +4 -3
- 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)
|
package/IMPLEMENTATION_PLAN.md
CHANGED
|
@@ -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
|
-
**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
-
|
|
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
|
-
**
|
|
221
|
-
|
|
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:
|
|
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;
|
|
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
|
-
//
|
|
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
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAC5B,OAAO,EACL,UAAU,EACV,SAAS,EACT,aAAa,EACb,YAAY,EACZ,MAAM,EACN,WAAW,EACX,QAAQ,EACR,MAAM,GACP,MAAM,SAAS,CAAA;AAEhB,gFAAgF;AAEhF;;;;;;;;;;;GAWG;AACH,MAAM,kBAAkB,GAAG
|
|
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;
|
|
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
|
|
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 ───────────────────────────────────────────────────────────────
|