@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.
- package/CHANGELOG.md +8 -0
- package/ROADMAP.md +80 -16
- 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 +24 -0
- package/dist/cli/adapters/netlify.d.ts.map +1 -0
- package/dist/cli/adapters/netlify.js +291 -0
- package/dist/cli/adapters/netlify.js.map +1 -0
- package/dist/cli/adapters/vercel.d.ts +20 -0
- package/dist/cli/adapters/vercel.d.ts.map +1 -0
- package/dist/cli/adapters/vercel.js +256 -0
- package/dist/cli/adapters/vercel.js.map +1 -0
- package/dist/cli/commands/adapt.d.ts +3 -0
- package/dist/cli/commands/adapt.d.ts.map +1 -0
- package/dist/cli/commands/adapt.js +29 -0
- package/dist/cli/commands/adapt.js.map +1 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +23 -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/cli/index.js +2 -0
- package/dist/cli/index.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 +12 -0
- 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 +63 -0
- package/docs/composables.md +116 -0
- package/docs/getting-started.md +36 -0
- package/docs/middleware.md +13 -7
- 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 +7 -1
- package/package.json +3 -1
- package/src/__tests__/cli/adapters/cloudflare-worker.integration.test.ts +130 -0
- package/src/__tests__/cli/adapters/cloudflare.test.ts +240 -0
- package/src/__tests__/cli/adapters/netlify-bridge.integration.test.ts +138 -0
- package/src/__tests__/cli/adapters/netlify.test.ts +241 -0
- package/src/__tests__/cli/adapters/vercel-launcher.integration.test.ts +129 -0
- package/src/__tests__/cli/adapters/vercel.test.ts +247 -0
- 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 +320 -0
- package/src/cli/adapters/vercel.ts +311 -0
- package/src/cli/commands/adapt.ts +31 -0
- package/src/cli/commands/build.ts +23 -0
- package/src/cli/commands/preview.ts +64 -3
- package/src/cli/index.ts +2 -0
- 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 +12 -0
- 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
|
-
**
|
|
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
|
|
|
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
|
|
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
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAC5B,OAAO,EACL,UAAU,EACV,SAAS,EACT,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,WAAW,EACX,QAAQ,GACT,MAAM,SAAS,CAAA;AAEhB,iFAAiF;AAEjF;;;;;;;;;;;GAWG;AACH,SAAS,oBAAoB,CAAC,UAAkB;IAC9C,0DAA0D;IAC1D,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAE/F,OAAO;;;;;;;;yCAQgC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0I/C,CAAA;AACD,CAAC;AAED,iFAAiF;AAEjF,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,IAAY;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAClC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,yCAAyC,OAAO,8BAA8B,CAC/E,CAAA;IACH,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAA;IACtD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAA;IACtD,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;IAElE,IAAI,KAAK,EAAE,CAAC;QACV,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAC1B,CAAC;SAAM,CAAC;QACN,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAC7B,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAA;IACrD,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;QACzC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;QAC/B,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAA;IAC1D,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAA;IAC1D,CAAC;AACH,CAAC;AAED,iFAAiF;AAEjF,SAAS,SAAS,CAAC,IAAY,EAAE,OAAe;IAC9C,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAA;IACzD,MAAM,UAAU,GAAG,UAAU,CAAC,cAAc,CAAC;QAC3C,CAAC,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC;QACvC,CAAC,CAAC,EAAE,CAAA;IAEN,oDAAoD;IACpD,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,oBAAoB,CAAC,UAAU,CAAC,CAAC,CAAA;IAE5E,oEAAoE;IACpE,6EAA6E;IAC7E,sEAAsE;IACtE,uEAAuE;IACvE,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAA;IAEnD,4EAA4E;IAC5E,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AAChC,CAAC;AAED,iFAAiF;AAEjF,SAAS,YAAY,CAAC,IAAY,EAAE,OAAe;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAEzC,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1B,iEAAiE;QACjE,4EAA4E;QAC5E,iBAAiB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACvC,CAAC;IACD,+DAA+D;IAE/D,kBAAkB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AACjC,CAAC;AAED,iFAAiF;AAEjF,SAAS,kBAAkB,CAAC,IAAY,EAAE,OAAgB;IACxD,MAAM,KAAK,GAAG;QACZ,qEAAqE;QACrE,kBAAkB;QAClB,mCAAmC;KACpC,CAAA;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,KAAK,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAA;IACvD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACd,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IACtC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;IAC1B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAEd,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,SAAiB,EAAE,OAAe;IAC3D,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAM;IAClC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACvC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3C,IAAI,KAAK,KAAK,YAAY;YAAE,SAAQ;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QACjC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACxC,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -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"}
|