@jasonshimmy/vite-plugin-cer-app 0.12.1 → 0.13.1

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 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.13.1] - 2026-03-22
5
+
6
+ - fix: optimize imports in Cloudflare and Netlify adapters to remove unused handler export (cc74ce3)
7
+
8
+ ## [v0.13.0] - 2026-03-22
9
+
10
+ - feat: implement streaming Web API Response support in Cloudflare and Netlify adapters (bfe00e3)
11
+
4
12
  ## [v0.12.1] - 2026-03-22
5
13
 
6
14
  - fix: add meta.hydrate support for route hydration strategies and implement related tests (0513743)
package/commits.txt CHANGED
@@ -1 +1 @@
1
- - fix: add meta.hydrate support for route hydration strategies and implement related tests (0513743)
1
+ - fix: optimize imports in Cloudflare and Netlify adapters to remove unused handler export (cc74ce3)
@@ -1 +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"}
1
+ {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../../src/cli/adapters/cloudflare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AA2LH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtE"}
@@ -36,6 +36,9 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, cpSyn
36
36
  * - Uses top-level await (supported in Cloudflare Workers ES modules).
37
37
  * - Mirrors the Netlify bridge's Web Request → Node.js mock → Response pattern;
38
38
  * Cloudflare Workers with nodejs_compat support node:stream + AsyncLocalStorage.
39
+ * - Responses are streamed via the Web Streams TransformStream API — chunks written
40
+ * by the SSR handler are forwarded to the client as they arrive rather than being
41
+ * buffered until the full page is rendered.
39
42
  * - Exports `{ fetch }` — the Cloudflare Pages _worker.js module format.
40
43
  */
41
44
  function generateWorkerBridge(clientHtml) {
@@ -51,7 +54,7 @@ import { Readable } from 'node:stream'
51
54
  // Must be set before the dynamic import below resolves.
52
55
  globalThis.__CER_CLIENT_TEMPLATE__ = \`${escaped}\`
53
56
 
54
- const { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
57
+ const { isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
55
58
 
56
59
  function matchApiPattern(pattern, urlPath) {
57
60
  const pp = pattern.split('/')
@@ -105,7 +108,9 @@ async function toNodeRequest(webReq) {
105
108
  }
106
109
 
107
110
  function createNodeResponse() {
108
- const chunks = []
111
+ const { readable, writable } = new TransformStream()
112
+ const writer = writable.getWriter()
113
+ const encoder = new TextEncoder()
109
114
  const headers = {}
110
115
  let _resolve
111
116
  let _ended = false
@@ -117,15 +122,14 @@ function createNodeResponse() {
117
122
  removeHeader(name) { delete headers[name.toLowerCase()] },
118
123
  write(chunk) {
119
124
  if (_ended) return
120
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
125
+ void writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk).catch(() => {})
121
126
  },
122
127
  end(chunk) {
123
128
  if (_ended) return
124
129
  _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 }))
130
+ if (chunk) void writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk).catch(() => {})
131
+ void writer.close().catch(() => {})
132
+ _resolve(new Response(readable, { status: res.statusCode, headers }))
129
133
  },
130
134
  json(data) {
131
135
  this.setHeader('Content-Type', 'application/json; charset=utf-8')
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../../src/cli/adapters/cloudflare.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../../src/cli/adapters/cloudflare.ts"],"names":[],"mappings":"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"}
@@ -17,7 +17,9 @@
17
17
  * - Writes `netlify.toml` that points to the correct publish directory and
18
18
  * adds a SPA fallback redirect (/* → /index.html).
19
19
  *
20
- * Netlify Functions v2 limitation: responses are buffered (no streaming).
20
+ * Responses are streamed via the Web Streams TransformStream API — chunks written
21
+ * by the SSR handler are forwarded to the client as they arrive rather than being
22
+ * buffered until the full page is rendered.
21
23
  * All other behaviour (cookies, headers, API routes, ISR) is fully supported.
22
24
  */
23
25
  export declare function runNetlifyAdapter(root: string): Promise<void>;
@@ -1 +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"}
1
+ {"version":3,"file":"netlify.d.ts","sourceRoot":"","sources":["../../../src/cli/adapters/netlify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA2KH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BnE"}
@@ -17,7 +17,9 @@
17
17
  * - Writes `netlify.toml` that points to the correct publish directory and
18
18
  * adds a SPA fallback redirect (/* → /index.html).
19
19
  *
20
- * Netlify Functions v2 limitation: responses are buffered (no streaming).
20
+ * Responses are streamed via the Web Streams TransformStream API — chunks written
21
+ * by the SSR handler are forwarded to the client as they arrive rather than being
22
+ * buffered until the full page is rendered.
21
23
  * All other behaviour (cookies, headers, API routes, ISR) is fully supported.
22
24
  */
23
25
  import { join } from 'pathe';
@@ -28,8 +30,11 @@ import { existsSync, mkdirSync, writeFileSync, copyFileSync, cpSync, readdirSync
28
30
  *
29
31
  * The function:
30
32
  * 1. Converts the incoming Web API Request to a mock Node.js IncomingMessage.
31
- * 2. Creates a mock ServerResponse that collects chunks and resolves a Promise
32
- * with a Web API Response once `end()` is called.
33
+ * 2. Creates a streaming mock ServerResponse backed by a TransformStream.
34
+ * Chunks written via res.write() are enqueued into the stream immediately;
35
+ * res.end() closes the stream and resolves the Promise with a streaming
36
+ * Web API Response. The platform receives the Response body as a ReadableStream
37
+ * so bytes flow to the client as they are written — no full-page buffering.
33
38
  * 3. Routes /api/* to the API handlers, everything else to the SSR handler.
34
39
  *
35
40
  * The server bundle is imported using a path relative to the function file
@@ -38,7 +43,7 @@ import { existsSync, mkdirSync, writeFileSync, copyFileSync, cpSync, readdirSync
38
43
  const NETLIFY_SSR_BRIDGE = `\
39
44
  // Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
40
45
  import { Readable } from 'node:stream'
41
- import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
46
+ import { isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
42
47
 
43
48
  function matchApiPattern(pattern, urlPath) {
44
49
  const pp = pattern.split('/')
@@ -92,7 +97,9 @@ async function toNodeRequest(webReq) {
92
97
  }
93
98
 
94
99
  function createNodeResponse() {
95
- const chunks = []
100
+ const { readable, writable } = new TransformStream()
101
+ const writer = writable.getWriter()
102
+ const encoder = new TextEncoder()
96
103
  const headers = {}
97
104
  let _resolve
98
105
  let _ended = false
@@ -104,15 +111,14 @@ function createNodeResponse() {
104
111
  removeHeader(name) { delete headers[name.toLowerCase()] },
105
112
  write(chunk) {
106
113
  if (_ended) return
107
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
114
+ void writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk).catch(() => {})
108
115
  },
109
116
  end(chunk) {
110
117
  if (_ended) return
111
118
  _ended = true
112
- if (chunk) {
113
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
114
- }
115
- _resolve(new Response(Buffer.concat(chunks), { status: res.statusCode, headers }))
119
+ if (chunk) void writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk).catch(() => {})
120
+ void writer.close().catch(() => {})
121
+ _resolve(new Response(readable, { status: res.statusCode, headers }))
116
122
  },
117
123
  // Helpers expected by API route handlers.
118
124
  json(data) {
@@ -1 +1 @@
1
- {"version":3,"file":"netlify.js","sourceRoot":"","sources":["../../../src/cli/adapters/netlify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"netlify.js","sourceRoot":"","sources":["../../../src/cli/adapters/netlify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;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;;;;;;;;;;;;;;GAcG;AACH,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwI1B,CAAA;AAED,iFAAiF;AAEjF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAY;IAClD,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,qCAAqC,CAAC,CAAA;IAClD,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAA;QACpD,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAA;QAC5C,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAA;IAC9C,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAA;IAC9C,CAAC;AACH,CAAC;AAED,iFAAiF;AAEjF,SAAS,SAAS,CAAC,IAAY,EAAE,OAAe;IAC9C,iCAAiC;IACjC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAA;IACpD,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,EAAE,kBAAkB,CAAC,CAAA;IAEhE,iEAAiE;IACjE,2EAA2E;IAC3E,uEAAuE;IACvE,8DAA8D;IAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAA;IACjD,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3B,MAAM,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACtD,CAAC;IACD,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,UAAU,CAAC,CAAA;IAEtD,+EAA+E;IAC/E,aAAa,CACX,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,EAC1B;;;;;;;;;;;;;CAaH,CACE,CAAA;AACH,CAAC;AAED,iFAAiF;AAEjF,SAAS,YAAY,CAAC,IAAY,EAAE,OAAe;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACzC,IAAI,UAAkB,CAAA;IAEtB,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1B,4EAA4E;QAC5E,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAA;QAC3C,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,MAAM,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACtD,CAAC;QACD,yEAAyE;QACzE,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE;YAC1B,SAAS,EAAE,IAAI;YACf,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CACd,GAAG,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,mBAAmB,CAAC;SAC1D,CAAC,CAAA;QACF,uDAAuD;QACvD,iBAAiB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;IAC1C,CAAC;SAAM,CAAC;QACN,4DAA4D;QAC5D,UAAU,GAAG,MAAM,CAAA;IACrB,CAAC;IAED,uCAAuC;IACvC,aAAa,CACX,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,EAC1B;;eAEW,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU;;;;;;;;;;;CAW1F,CACE,CAAA;AACH,CAAC;AAED,iFAAiF;AAEjF;;;;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"}
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "devDependencies": {
14
14
  "vite": "^8.0.1",
15
- "@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
15
+ "@jasonshimmy/vite-plugin-cer-app": "^0.13.0",
16
16
  "typescript": "^5.9.3"
17
17
  }
18
18
  }
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "vite": "^8.0.1",
16
- "@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
16
+ "@jasonshimmy/vite-plugin-cer-app": "^0.13.0",
17
17
  "typescript": "^5.9.3"
18
18
  }
19
19
  }
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "devDependencies": {
14
14
  "vite": "^8.0.1",
15
- "@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
15
+ "@jasonshimmy/vite-plugin-cer-app": "^0.13.0",
16
16
  "typescript": "^5.9.3"
17
17
  }
18
18
  }
package/docs/cli.md CHANGED
@@ -187,7 +187,7 @@ cer-app adapt --platform vercel --root ./packages/my-app
187
187
  - Copies content-hashed assets to `.netlify/publish/` (no `index.html` — HTML is served by the function).
188
188
  - Writes `netlify.toml` with the publish directory, `Cache-Control` headers for assets, and a catch-all redirect to the SSR function.
189
189
  - SPA/SSG builds: writes `netlify.toml` only (no function needed).
190
- - Note: Netlify Functions buffer the full responsestreaming is not supported.
190
+ - SSR responses are streamed via the Web Streams `TransformStream` API HTML chunks are forwarded to the client as they are written rather than waiting for the full page to render.
191
191
  - Deploy with `netlify deploy`.
192
192
 
193
193
  **Cloudflare behavior (`--platform cloudflare`):**
@@ -196,7 +196,7 @@ cer-app adapt --platform vercel --root ./packages/my-app
196
196
  - Requires the `nodejs_compat` compatibility flag (written automatically into `wrangler.toml`) for `AsyncLocalStorage` and `node:stream` support.
197
197
  - Copies content-hashed assets to `dist/` alongside the worker. Cloudflare Pages CDN serves matched static files first; all other requests fall through to `_worker.js`.
198
198
  - SPA/SSG builds: no worker generated — Cloudflare Pages serves `dist/` as a static site.
199
- - Note: responses are buffered (Cloudflare Functions limitation, same as Netlify).
199
+ - SSR responses are streamed via the Web Streams `TransformStream` API — HTML chunks are forwarded to the client as they are written rather than waiting for the full page to render.
200
200
  - Deploy with `wrangler pages deploy dist`.
201
201
 
202
202
  **Auto-run via `cer.config.ts`:**
@@ -67,6 +67,23 @@ The server renders HTML for each request. Uses Declarative Shadow DOM (DSD) to e
67
67
  7. `useHead()` calls are collected from the synchronous render and injected before `</head>`
68
68
  8. The rendered HTML is merged with the Vite client bundle shell and streamed as a chunked response
69
69
 
70
+ ### Streaming behavior by platform
71
+
72
+ The SSR server renders in two phases: a synchronous first chunk (the full page HTML up to `</body>`) and zero or more subsequent async chunks (component swap scripts and the DSD polyfill). All platforms stream both phases to the client.
73
+
74
+ | Platform | Streaming mechanism | Notes |
75
+ |---|---|---|
76
+ | `cer-app preview` | Node.js `res.write()` → native chunked HTTP | `Transfer-Encoding: chunked` set automatically |
77
+ | Vercel | Node.js `res.write()` → native chunked HTTP | Vercel injects real `req`/`res` into the handler |
78
+ | Netlify | `TransformStream` → `Response(readableStream)` | Web Streams API; Netlify Functions v2 |
79
+ | Cloudflare Pages | `TransformStream` → `Response(readableStream)` | Web Streams API; Cloudflare Workers |
80
+
81
+ **TTFB benefit:** The browser receives the first chunk (full page HTML, including all pre-rendered content) before async swap scripts are ready. Content is visible immediately — async scripts stream in afterward without blocking the initial paint.
82
+
83
+ **Error recovery:** If the SSR handler throws after writing the first chunk, the catch handler sets `res.statusCode = 500` and calls `res.end('Internal Server Error')`. On Netlify/Cloudflare the `Response` status (committed at `end()` time) will be 500, and the partial HTML will be followed by the error string. Use `meta.render: 'server'` with no `revalidate` on routes where partial responses are unacceptable — errors on those routes are caught before any output is written.
84
+
85
+ **Client disconnects:** If the client closes the connection during streaming (Netlify/Cloudflare), `writer.write()` and `writer.close()` rejections are silently swallowed — the server continues handling other requests normally.
86
+
70
87
  ### Build output
71
88
 
72
89
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * These tests exercise the full request/response pipeline through the generated
5
5
  * `dist/_worker.js`: Web API Request → server middleware → API route handler
6
- * / SSR handler → buffered Web API Response.
6
+ * / SSR handler → streaming Web API Response (TransformStream body).
7
7
  *
8
8
  * Prerequisites (run automatically via `npm run e2e:integration:cloudflare`):
9
9
  * 1. `cer-app build --mode ssr --root e2e/kitchen-sink`
@@ -46,8 +46,10 @@ describe.skipIf(!workerExists)('Cloudflare Pages worker — integration', () =>
46
46
  expect(res.headers.get('content-type')).toContain('text/html')
47
47
  })
48
48
 
49
- it('GET / response is a complete HTML document', async () => {
49
+ it('GET / response is a complete HTML document streamed via ReadableStream', async () => {
50
50
  const res = await call('/')
51
+ // Response body is a ReadableStream (streaming, not pre-buffered).
52
+ expect(res.body).toBeInstanceOf(ReadableStream)
51
53
  const body = await res.text()
52
54
  expect(body).toContain('<!DOCTYPE html')
53
55
  expect(body).toContain('</html>')
@@ -83,6 +83,14 @@ describe('runCloudflareAdapter — SSR mode', () => {
83
83
  expect(worker).toContain('isrHandler(nodeReq, res)')
84
84
  })
85
85
 
86
+ it('worker does not destructure unused handler export (only isrHandler is needed)', async () => {
87
+ await runCloudflareAdapter(root)
88
+ const worker = readText(root, 'dist/_worker.js')
89
+ // isrHandler wraps handler internally — the worker only needs isrHandler
90
+ expect(worker).not.toMatch(/\{\s*handler\s*,/)
91
+ expect(worker).not.toMatch(/,\s*handler\s*[,}]/)
92
+ })
93
+
86
94
  it('worker handles /api/* routing via matchApiPattern', async () => {
87
95
  await runCloudflareAdapter(root)
88
96
  const worker = readText(root, 'dist/_worker.js')
@@ -104,6 +112,43 @@ describe('runCloudflareAdapter — SSR mode', () => {
104
112
  expect(worker).toContain('if (_ended) return')
105
113
  })
106
114
 
115
+ it('worker returns a streaming Web API Response (TransformStream body)', async () => {
116
+ await runCloudflareAdapter(root)
117
+ const worker = readText(root, 'dist/_worker.js')
118
+ expect(worker).toContain('new Response(readable,')
119
+ expect(worker).toContain('TransformStream')
120
+ expect(worker).not.toContain('Buffer.concat')
121
+ })
122
+
123
+ it('streaming Response carries status code and accumulated response headers', async () => {
124
+ await runCloudflareAdapter(root)
125
+ const worker = readText(root, 'dist/_worker.js')
126
+ // end() resolves with new Response(readable, { status: res.statusCode, headers })
127
+ expect(worker).toContain('res.statusCode')
128
+ expect(worker).toContain('{ status: res.statusCode, headers }')
129
+ })
130
+
131
+ it('worker streams chunks via writer.write() instead of buffering', async () => {
132
+ await runCloudflareAdapter(root)
133
+ const worker = readText(root, 'dist/_worker.js')
134
+ expect(worker).toContain('writer.write(')
135
+ expect(worker).toContain('writer.close()')
136
+ })
137
+
138
+ it('worker uses TextEncoder to convert string chunks to Uint8Array', async () => {
139
+ await runCloudflareAdapter(root)
140
+ const worker = readText(root, 'dist/_worker.js')
141
+ expect(worker).toContain('encoder.encode(chunk)')
142
+ expect(worker).toContain('new TextEncoder()')
143
+ })
144
+
145
+ it('worker silently swallows writer rejections to prevent UnhandledPromiseRejection on client disconnect', async () => {
146
+ await runCloudflareAdapter(root)
147
+ const worker = readText(root, 'dist/_worker.js')
148
+ expect(worker).toContain('.catch(() => {})')
149
+ expect(worker).toContain('writer.close().catch(() => {})')
150
+ })
151
+
107
152
  it('worker wraps API handlers in runWithRequestContext for cookie/session access', async () => {
108
153
  await runCloudflareAdapter(root)
109
154
  const worker = readText(root, 'dist/_worker.js')
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * These tests exercise the full request/response pipeline through the generated
5
5
  * `netlify/functions/ssr.mjs` bridge: Web API Request → Node.js mock → SSR
6
- * handler → buffered Web API Response.
6
+ * handler → streaming Web API Response (TransformStream body).
7
7
  *
8
8
  * Prerequisites (run automatically via `npm run e2e:integration:netlify`):
9
9
  * 1. `cer-app build --mode ssr --root e2e/kitchen-sink` → dist/server/server.js
@@ -45,9 +45,10 @@ describe.skipIf(!bridgeExists)('Netlify SSR bridge — integration', () => {
45
45
 
46
46
  it('GET / response body is a complete HTML document', async () => {
47
47
  const res = await bridge(new Request('http://localhost/'))
48
+ // Response body is a ReadableStream (streaming, not pre-buffered).
49
+ expect(res.body).toBeInstanceOf(ReadableStream)
48
50
  const body = await res.text()
49
51
  expect(body).toContain('<!DOCTYPE html')
50
- // Response is buffered in full — not truncated mid-document.
51
52
  expect(body).toContain('</html>')
52
53
  })
53
54
 
@@ -58,6 +58,13 @@ describe('runNetlifyAdapter — SSR mode', () => {
58
58
  expect(bridge).toContain('isrHandler(nodeReq, res)')
59
59
  })
60
60
 
61
+ it('bridge does not import unused handler export (only isrHandler is needed)', async () => {
62
+ await runNetlifyAdapter(root)
63
+ const bridge = readText(root, 'netlify/functions/ssr.mjs')
64
+ // isrHandler wraps handler internally — the bridge only needs isrHandler
65
+ expect(bridge).not.toMatch(/import\s*\{[^}]*\bhandler\b[^}]*\}/)
66
+ })
67
+
61
68
  it('bridge exports a default async function', async () => {
62
69
  await runNetlifyAdapter(root)
63
70
  const bridge = readText(root, 'netlify/functions/ssr.mjs')
@@ -71,10 +78,41 @@ describe('runNetlifyAdapter — SSR mode', () => {
71
78
  expect(bridge).toContain("from 'node:stream'")
72
79
  })
73
80
 
74
- it('bridge returns a Web API Response', async () => {
81
+ it('bridge returns a streaming Web API Response (TransformStream body)', async () => {
82
+ await runNetlifyAdapter(root)
83
+ const bridge = readText(root, 'netlify/functions/ssr.mjs')
84
+ expect(bridge).toContain('new Response(readable,')
85
+ expect(bridge).toContain('TransformStream')
86
+ expect(bridge).not.toContain('Buffer.concat')
87
+ })
88
+
89
+ it('streaming Response carries status code and accumulated response headers', async () => {
90
+ await runNetlifyAdapter(root)
91
+ const bridge = readText(root, 'netlify/functions/ssr.mjs')
92
+ // end() resolves with new Response(readable, { status: res.statusCode, headers })
93
+ expect(bridge).toContain('res.statusCode')
94
+ expect(bridge).toContain('{ status: res.statusCode, headers }')
95
+ })
96
+
97
+ it('bridge streams chunks via writer.write() instead of buffering', async () => {
98
+ await runNetlifyAdapter(root)
99
+ const bridge = readText(root, 'netlify/functions/ssr.mjs')
100
+ expect(bridge).toContain('writer.write(')
101
+ expect(bridge).toContain('writer.close()')
102
+ })
103
+
104
+ it('bridge uses TextEncoder to convert string chunks to Uint8Array', async () => {
105
+ await runNetlifyAdapter(root)
106
+ const bridge = readText(root, 'netlify/functions/ssr.mjs')
107
+ expect(bridge).toContain('encoder.encode(chunk)')
108
+ expect(bridge).toContain('new TextEncoder()')
109
+ })
110
+
111
+ it('bridge silently swallows writer rejections to prevent UnhandledPromiseRejection on client disconnect', async () => {
75
112
  await runNetlifyAdapter(root)
76
113
  const bridge = readText(root, 'netlify/functions/ssr.mjs')
77
- expect(bridge).toContain('new Response(')
114
+ expect(bridge).toContain('.catch(() => {})')
115
+ expect(bridge).toContain('writer.close().catch(() => {})')
78
116
  })
79
117
 
80
118
  it('bridge handles /api/* routing', async () => {
@@ -107,6 +107,15 @@ describe('runVercelAdapter — SSR mode', () => {
107
107
  expect(launcher).toContain('await isrHandler(req, res)')
108
108
  })
109
109
 
110
+ it('launcher uses native Node.js streaming — passes real req/res directly, no TransformStream', async () => {
111
+ await runVercelAdapter(root)
112
+ const launcher = readText(root, '.vercel/output/functions/index.func/index.js')
113
+ // Vercel injects real Node.js req/res, so the launcher passes them straight through.
114
+ // No TransformStream mock is needed — Node.js handles chunked transfer natively.
115
+ expect(launcher).toContain('await isrHandler(req, res)')
116
+ expect(launcher).not.toContain('TransformStream')
117
+ })
118
+
110
119
  it('launcher routes /api/* requests to apiRoutes', async () => {
111
120
  await runVercelAdapter(root)
112
121
  const launcher = readText(root, '.vercel/output/functions/index.func/index.js')
@@ -48,6 +48,9 @@ import {
48
48
  * - Uses top-level await (supported in Cloudflare Workers ES modules).
49
49
  * - Mirrors the Netlify bridge's Web Request → Node.js mock → Response pattern;
50
50
  * Cloudflare Workers with nodejs_compat support node:stream + AsyncLocalStorage.
51
+ * - Responses are streamed via the Web Streams TransformStream API — chunks written
52
+ * by the SSR handler are forwarded to the client as they arrive rather than being
53
+ * buffered until the full page is rendered.
51
54
  * - Exports `{ fetch }` — the Cloudflare Pages _worker.js module format.
52
55
  */
53
56
  function generateWorkerBridge(clientHtml: string): string {
@@ -64,7 +67,7 @@ import { Readable } from 'node:stream'
64
67
  // Must be set before the dynamic import below resolves.
65
68
  globalThis.__CER_CLIENT_TEMPLATE__ = \`${escaped}\`
66
69
 
67
- const { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
70
+ const { isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
68
71
 
69
72
  function matchApiPattern(pattern, urlPath) {
70
73
  const pp = pattern.split('/')
@@ -118,7 +121,9 @@ async function toNodeRequest(webReq) {
118
121
  }
119
122
 
120
123
  function createNodeResponse() {
121
- const chunks = []
124
+ const { readable, writable } = new TransformStream()
125
+ const writer = writable.getWriter()
126
+ const encoder = new TextEncoder()
122
127
  const headers = {}
123
128
  let _resolve
124
129
  let _ended = false
@@ -130,15 +135,14 @@ function createNodeResponse() {
130
135
  removeHeader(name) { delete headers[name.toLowerCase()] },
131
136
  write(chunk) {
132
137
  if (_ended) return
133
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
138
+ void writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk).catch(() => {})
134
139
  },
135
140
  end(chunk) {
136
141
  if (_ended) return
137
142
  _ended = true
138
- if (chunk) {
139
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
140
- }
141
- _resolve(new Response(Buffer.concat(chunks), { status: res.statusCode, headers }))
143
+ if (chunk) void writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk).catch(() => {})
144
+ void writer.close().catch(() => {})
145
+ _resolve(new Response(readable, { status: res.statusCode, headers }))
142
146
  },
143
147
  json(data) {
144
148
  this.setHeader('Content-Type', 'application/json; charset=utf-8')
@@ -17,7 +17,9 @@
17
17
  * - Writes `netlify.toml` that points to the correct publish directory and
18
18
  * adds a SPA fallback redirect (/* → /index.html).
19
19
  *
20
- * Netlify Functions v2 limitation: responses are buffered (no streaming).
20
+ * Responses are streamed via the Web Streams TransformStream API — chunks written
21
+ * by the SSR handler are forwarded to the client as they arrive rather than being
22
+ * buffered until the full page is rendered.
21
23
  * All other behaviour (cookies, headers, API routes, ISR) is fully supported.
22
24
  */
23
25
 
@@ -40,8 +42,11 @@ import {
40
42
  *
41
43
  * The function:
42
44
  * 1. Converts the incoming Web API Request to a mock Node.js IncomingMessage.
43
- * 2. Creates a mock ServerResponse that collects chunks and resolves a Promise
44
- * with a Web API Response once `end()` is called.
45
+ * 2. Creates a streaming mock ServerResponse backed by a TransformStream.
46
+ * Chunks written via res.write() are enqueued into the stream immediately;
47
+ * res.end() closes the stream and resolves the Promise with a streaming
48
+ * Web API Response. The platform receives the Response body as a ReadableStream
49
+ * so bytes flow to the client as they are written — no full-page buffering.
45
50
  * 3. Routes /api/* to the API handlers, everything else to the SSR handler.
46
51
  *
47
52
  * The server bundle is imported using a path relative to the function file
@@ -50,7 +55,7 @@ import {
50
55
  const NETLIFY_SSR_BRIDGE = `\
51
56
  // Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
52
57
  import { Readable } from 'node:stream'
53
- import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
58
+ import { isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
54
59
 
55
60
  function matchApiPattern(pattern, urlPath) {
56
61
  const pp = pattern.split('/')
@@ -104,7 +109,9 @@ async function toNodeRequest(webReq) {
104
109
  }
105
110
 
106
111
  function createNodeResponse() {
107
- const chunks = []
112
+ const { readable, writable } = new TransformStream()
113
+ const writer = writable.getWriter()
114
+ const encoder = new TextEncoder()
108
115
  const headers = {}
109
116
  let _resolve
110
117
  let _ended = false
@@ -116,15 +123,14 @@ function createNodeResponse() {
116
123
  removeHeader(name) { delete headers[name.toLowerCase()] },
117
124
  write(chunk) {
118
125
  if (_ended) return
119
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
126
+ void writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk).catch(() => {})
120
127
  },
121
128
  end(chunk) {
122
129
  if (_ended) return
123
130
  _ended = true
124
- if (chunk) {
125
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : Buffer.from(chunk))
126
- }
127
- _resolve(new Response(Buffer.concat(chunks), { status: res.statusCode, headers }))
131
+ if (chunk) void writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk).catch(() => {})
132
+ void writer.close().catch(() => {})
133
+ _resolve(new Response(readable, { status: res.statusCode, headers }))
128
134
  },
129
135
  // Helpers expected by API route handlers.
130
136
  json(data) {
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "devDependencies": {
14
14
  "vite": "^8.0.1",
15
- "@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
15
+ "@jasonshimmy/vite-plugin-cer-app": "^0.13.0",
16
16
  "typescript": "^5.9.3"
17
17
  }
18
18
  }
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "vite": "^8.0.1",
16
- "@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
16
+ "@jasonshimmy/vite-plugin-cer-app": "^0.13.0",
17
17
  "typescript": "^5.9.3"
18
18
  }
19
19
  }
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "devDependencies": {
14
14
  "vite": "^8.0.1",
15
- "@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
15
+ "@jasonshimmy/vite-plugin-cer-app": "^0.13.0",
16
16
  "typescript": "^5.9.3"
17
17
  }
18
18
  }