From 046ffe37ca5de1591bfa68652d7a04e36da46c13 Mon Sep 17 00:00:00 2001 From: pavanscales Date: Tue, 26 May 2026 23:11:45 +0530 Subject: [PATCH] fix: resolve async .some() bug in /resume endpoint; harden server error handling; cleanup stale test scripts; update README --- .gitignore | 3 +- README.md | 115 ++++++++++++++++++++++++++++++++++-- scripts/patch-react-dom.mjs | 21 ++++--- server.js | 70 ++++++++++++++++------ test-prerender.mjs | 56 ------------------ test-prerender2.mjs | 39 ------------ test-prerender3.mjs | 58 ------------------ test-prerender4.mjs | 54 ----------------- test-prerender5.mjs | 49 --------------- test-prerender6.mjs | 43 -------------- test-prerender7.mjs | 50 ---------------- test-prerender8.mjs | 59 ------------------ test-prerender9.mjs | 47 --------------- test-resume.mjs | 54 ----------------- 14 files changed, 178 insertions(+), 540 deletions(-) delete mode 100644 test-prerender.mjs delete mode 100644 test-prerender2.mjs delete mode 100644 test-prerender3.mjs delete mode 100644 test-prerender4.mjs delete mode 100644 test-prerender5.mjs delete mode 100644 test-prerender6.mjs delete mode 100644 test-prerender7.mjs delete mode 100644 test-prerender8.mjs delete mode 100644 test-prerender9.mjs delete mode 100644 test-resume.mjs diff --git a/.gitignore b/.gitignore index 83e0198..3bc61fa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ node_modules/ dist/ .rsc_cache/ nul -_test_*.mjs *.log +test-prerender*.mjs +test-resume.mjs diff --git a/README.md b/README.md index cd5d7b4..b582013 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,120 @@ [![CI](https://github.com/renderffx/ppr-scratch/actions/workflows/ci.yml/badge.svg)](https://github.com/renderffx/ppr-scratch/actions/workflows/ci.yml) -Next.js-style Partial Prerendering engine using React 19 Canary. +Next.js-style Partial Prerendering engine using React 19 Canary (`prerenderToNodeStream` / `resumeToPipeableStream`). -``` +## Quick Start + +```bash +npm install npm run build npm start +# http://localhost:3000 +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `npm run build` | Full pipeline: prereqs → clean → bundle → RSC flight → prerender | +| `npm start` | Start Express server on port 3000 | +| `npm run test` | Artifact contract tests | +| `npm run test:unit` | Unit tests (phase, dynamic-apis, flight-cache) | +| `npm run test:e2e` | End-to-end prerender→resume pipeline test | +| `npm run test:all` | All tests | +| `npm run dev:loop` | Build + all tests | +| `npm run clean` | Remove `dist/` | +| `npm run clean:cache` | Remove `.rsc_cache/` | +| `npm run prewarm` | Prewarm RSC cache entries | + +## API + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/` | Serves static prerendered shell HTML | +| GET | `/rsc-payload` | Raw RSC Flight v1 binary payload | +| GET | `/cache/:name` | Cached RSC entry by component name | +| POST | `/resume` | Resume postponed boundaries (JSON) | +| POST | `/resume/stream` | Resume boundaries as chunked HTML | +| GET | `/api/status` | Build status and artifact health | +| GET | `/api/manifest` | Raw build manifest | + +Environment: `PORT` (default 3000), `PPR_RESUME_TIMEOUT` (default 10000ms). + +## Architecture + +### Build Pipeline + ``` +src/App.js ──esbuild──▶ dist/App.bundle.js + │ +build-rsc.js │ + └── react-server-dom-webpack ──▶ dist/rsc-payload.bin + +build.js + ├── setPhase('prerender') + ├── prewarm-cache → .rsc_cache/*.bin + ├── prerenderToNodeStream(App) + │ └── dynamic components throw NEVER promise + │ └── Suspense boundaries → postponed state + │ └── onShellReady fires → abort(request) via patch + │ └── resolves with {postponed, prelude} + ├── prelude → dist/shell.html ( markers) + ├── postponed JSON → dist/postponed.json + ├── RSC payload embedded in shell + └── manifest → dist/manifest.json +``` + +### Request Pipeline + +``` +GET / → shell.html (instant, no JS required) +POST /resume → resumeToPipeableStream(App, postponed) + → pipes resumed content + $RC() swap scripts +POST /resume/stream → same, chunked HTML transfer +``` + +## React Patch + +React's `prerenderToNodeStream` waits for all content (blocks on never-resolving promises). The `postinstall` script patches `onShellReady` from `void 0` to a function that calls `abort(request)`, which marks pending Suspense boundaries as real postponed state. + +Targets: `node_modules/react-dom/cjs/react-dom-server.node.{production,development}.js` + +Run manually: `node scripts/patch-react-dom.mjs` + +## Project Structure + +``` +├── src/ +│ ├── App.js React app with static + dynamic boundaries +│ ├── phase.js Prerender/request phase management +│ ├── dynamic-apis.js Cookies, headers, searchParams (suspend during prerender) +│ ├── ErrorBoundary.js Catch errors per boundary +│ ├── flight-cache.js RSC Flight cache (memory + disk, TTL 5min) +│ └── cache-registry.js Component definitions for cache prewarming +├── scripts/ +│ ├── patch-react-dom.mjs React abort-on-shellReady patch +│ └── check-prereqs.mjs Node/npm/patch validation +├── tests/ +│ ├── ppr.test.js Artifact contract tests +│ ├── unit.test.js Unit tests (phase, dynamic-apis, flight-cache) +│ └── ppr-e2e.test.js Prerender→resume end-to-end +├── server.js Express server +├── build.js PPR build orchestrator +├── build-rsc.js RSC Flight payload builder +└── prewarm-cache.mjs Cache prewarming +``` + +## Output Artifacts + +| File | Contents | +|------|----------| +| `dist/shell.html` | Static HTML with `` markers + embedded RSC payload | +| `dist/postponed.json` | React postponed state (resumableState, replayNodes) | +| `dist/rsc-payload.bin` | RSC Flight v1 binary protocol payload | +| `dist/manifest.json` | Build metadata (boundaries, cache entries, timestamps) | +| `.rsc_cache/*.bin` | Per-component RSC Flight cache entries | ---- +## License -**Author:** [@infinterenders](https://x.com/infinterenders) · [renderffx](https://github.com/renderffx) +ISC — © 2026 renderffx diff --git a/scripts/patch-react-dom.mjs b/scripts/patch-react-dom.mjs index 4b065f4..a2ae3f4 100644 --- a/scripts/patch-react-dom.mjs +++ b/scripts/patch-react-dom.mjs @@ -9,9 +9,8 @@ const FILES = [ resolve(__dirname, '../node_modules/react-dom/cjs/react-dom-server.node.development.js'), ]; -// Pattern: onShellReady is void 0 (needs patching) -const SEARCH = 'resolve(readable);\n },\n void 0,\n void 0,\n reject'; -const REPLACE = 'resolve(readable);\n },\n function () {\n abort(request);\n },\n void 0,\n reject'; +const RE = /(resolve\(readable\);\s*\n\s*\},)\s*\n\s*void\s+0\s*,\s*\n(\s*void\s+0\s*,\s*\n\s*reject)/; +const REPLACE = '$1\n function () {\n abort(request);\n },\n$2'; let patched = 0; @@ -23,16 +22,24 @@ for (const filePath of FILES) { continue; } - if (content.includes(REPLACE)) continue; + if (content.includes('abort(request)')) { + console.log(` [OK] already patched: ${filePath}`); + continue; + } - if (!content.includes(SEARCH)) continue; + if (!RE.test(content)) { + console.warn(` [WARN] pattern not found in: ${filePath}`); + continue; + } - content = content.replace(SEARCH, REPLACE); + content = content.replace(RE, REPLACE); writeFileSync(filePath, content, 'utf-8'); patched++; - console.log(` patched: ${filePath}`); + console.log(` [PATCH] applied: ${filePath}`); } if (patched > 0) { console.log(`\nReact DOM patch complete: ${patched} file(s) patched`); +} else { + console.log('\nNo files needed patching.'); } diff --git a/server.js b/server.js index ae7695e..c25fdc0 100644 --- a/server.js +++ b/server.js @@ -8,12 +8,20 @@ import { getCachedBuffer } from './src/flight-cache.js'; const app = express(); const PORT = process.env.PORT || 3000; -const RESUME_TIMEOUT_MS = parseInt(process.env.PPR_RESUME_TIMEOUT || '10000', 10); +const RESUME_TIMEOUT_MS = Math.max(1000, parseInt(process.env.PPR_RESUME_TIMEOUT || '10000', 10)); -app.use(express.json()); +app.disable('x-powered-by'); +app.use(express.json({ limit: '1mb' })); app.use(express.static('./public', { index: false })); const CACHED_NAMES = ['CookieBasedGreeting', 'HeaderBasedContent', 'AsyncDataWidget', 'AuthBasedSection']; +const VALID_CACHE_NAMES = new Set(CACHED_NAMES); + +function escapeHtml(s) { + return String(s).replace(/[&<>"']/g, function (m) { + return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m]; + }); +} function withTimeout(promise, ms, label) { return Promise.race([ @@ -51,16 +59,32 @@ app.get('/rsc-payload', (req, res) => { app.get('/cache/:name', async (req, res) => { const { name } = req.params; - const buf = await getCachedBuffer(name, {}); - if (!buf) { - return res.status(404).json({ error: `No cache entry for ${name}` }); + if (!VALID_CACHE_NAMES.has(name)) { + return res.status(400).json({ error: `Invalid cache name. Valid: ${CACHED_NAMES.join(', ')}` }); + } + try { + const buf = await getCachedBuffer(name, {}); + if (!buf) { + return res.status(404).json({ error: `No cache entry for ${name}` }); + } + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('X-Cache-Entry', name); + res.setHeader('Content-Length', buf.length); + res.send(buf); + } catch (err) { + res.status(500).json({ error: `Cache read failed: ${err.message}` }); } - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('X-Cache-Entry', name); - res.setHeader('Content-Length', buf.length); - res.send(buf); }); +function readPostponed(path) { + const raw = readFileSync(path, 'utf-8'); + const parsed = JSON.parse(raw); + if (!parsed.resumableState || !Array.isArray(parsed.replayNodes)) { + throw new Error('Invalid postponed state: missing resumableState or replayNodes'); + } + return parsed; +} + app.post('/resume', async (req, res) => { const postponedPath = './dist/postponed.json'; if (!existsSync(postponedPath)) { @@ -68,7 +92,8 @@ app.post('/resume', async (req, res) => { } try { - const hasCachedContent = CACHED_NAMES.some(n => getCachedBuffer(n, {})); + const cacheChecks = await Promise.all(CACHED_NAMES.map(n => getCachedBuffer(n, {}))); + const hasCachedContent = cacheChecks.some(Boolean); if (hasCachedContent) { const resumedHtml = []; @@ -77,9 +102,9 @@ app.post('/resume', async (req, res) => { if (buf) { resumedHtml.push( `
` + - `

${name} served from RSC cache

` + + `

${escapeHtml(name)} served from RSC cache

` + `

Cached RSC Flight payload: ${buf.length} bytes

` + - `
Cache key: ${name}:{}
` + + `
Cache key: ${escapeHtml(name)}:{}
` + `
` ); } @@ -93,7 +118,7 @@ app.post('/resume', async (req, res) => { }); } - const postponed = JSON.parse(readFileSync(postponedPath, 'utf-8')); + const postponed = readPostponed(postponedPath); const chunks = []; const html = await withTimeout(new Promise((resolve, reject) => { @@ -112,9 +137,6 @@ app.post('/resume', async (req, res) => { onShellError(err) { reject(err); }, - onAllReady() { - // All content has been resumed — ensure flush - }, }); }), RESUME_TIMEOUT_MS, 'Resume'); @@ -157,7 +179,7 @@ app.post('/resume/stream', async (req, res) => { if (buf) { res.write( `
` + - `

${name} (Streamed from RSC cache)

` + + `

${escapeHtml(name)} (Streamed from RSC cache)

` + `

RSC payload: ${buf.length} bytes

` + `

Served at: ${new Date().toISOString()}

` + `
` @@ -166,7 +188,7 @@ app.post('/resume/stream', async (req, res) => { } } } else { - const postponed = JSON.parse(readFileSync(postponedPath, 'utf-8')); + const postponed = readPostponed(postponedPath); const streamable = resumeToPipeableStream(createElement(App), postponed, { onShellReady() { streamable.pipe(res); @@ -181,7 +203,7 @@ app.post('/resume/stream', async (req, res) => { res.write(''); res.end(); } catch (err) { - res.status(500).json({ error: err.message }); + try { res.status(500).json({ error: err.message }); } catch {} } }); @@ -222,6 +244,16 @@ app.get('/api/manifest', (req, res) => { res.json(JSON.parse(readFileSync('./dist/manifest.json', 'utf-8'))); }); +app.use((req, res) => { + res.status(404).json({ error: `Not found: ${req.method} ${req.path}` }); +}); + +app.use((err, req, res, _next) => { + console.error(`[ERROR] ${req.method} ${req.path}:`, err); + if (res.headersSent) return; + res.status(500).json({ error: 'Internal server error' }); +}); + app.listen(PORT, () => { console.log(`PPR server running on http://localhost:${PORT}`); console.log(` GET / - Static shell`); diff --git a/test-prerender.mjs b/test-prerender.mjs deleted file mode 100644 index 5a9d5a3..0000000 --- a/test-prerender.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; -import { Writable } from 'node:stream'; - -const NEVER = new Promise(() => {}); - -function DynamicComponent({ name }) { - throw NEVER; -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(DynamicComponent, { name: 'CookieWidget' }) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function main() { - const result = prerenderToNodeStream(createElement(App)); - console.log('prerender result keys:', Object.keys(result)); - - const chunks = []; - const writable = new Writable({ - write(chunk, _, cb) { - chunks.push(Buffer.from(chunk)); - cb(); - }, - }); - - result.pipe(writable); - - await new Promise((resolve, reject) => { - writable.on('finish', resolve); - writable.on('error', reject); - }); - - const html = Buffer.concat(chunks).toString('utf-8'); - console.log('HTML:', html); - console.log('HTML length:', html.length); - console.log('Has $? marker:', html.includes('')); - console.log('Has postponed:', result.postponed !== null && result.postponed !== undefined); - - if (result.postponed) { - console.log('Postponed type:', typeof result.postponed); - console.log('Postponed is buffer:', Buffer.isBuffer(result.postponed)); - console.log('Postponed keys:', Object.keys(result.postponed)); - console.log('Postponed value:', result.postponed); - } -} - -main().catch(console.error); diff --git a/test-prerender2.mjs b/test-prerender2.mjs deleted file mode 100644 index fbd2ca6..0000000 --- a/test-prerender2.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; - -const NEVER = new Promise(() => {}); - -function DynamicComponent({ name }) { - throw NEVER; -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(DynamicComponent, { name: 'CookieWidget' }) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function main() { - const result = prerenderToNodeStream(createElement(App)); - console.log('typeof result:', typeof result); - console.log('result:', result); - console.log('Object.keys(result):', Object.keys(result)); - console.log('result prototype:', Object.getPrototypeOf(result)); - - // Maybe it's a promise? - if (result && typeof result.then === 'function') { - console.log('It is a promise/thenable!'); - const resolved = await result; - console.log('resolved:', resolved); - console.log('resolved keys:', Object.keys(resolved)); - console.log('resolved prototype:', Object.getPrototypeOf(resolved)); - } -} - -main().catch(console.error); diff --git a/test-prerender3.mjs b/test-prerender3.mjs deleted file mode 100644 index c0902a2..0000000 --- a/test-prerender3.mjs +++ /dev/null @@ -1,58 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; - -const NEVER = new Promise(() => {}); - -function DynamicComponent({ name }) { - throw NEVER; -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(DynamicComponent, { name: 'CookieWidget' }) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function main() { - const resolved = await prerenderToNodeStream(createElement(App)); - console.log('type:', typeof resolved); - console.log('keys:', Object.keys(resolved)); - console.log('has pipe:', typeof resolved.pipe); - console.log('has abort:', typeof resolved.abort); - console.log('has postponed:', 'postponed' in resolved); - console.log('postponed:', resolved.postponed); - console.log('postponed type:', typeof resolved.postponed); - if (resolved.postponed) { - console.log('postponed keys:', Object.keys(resolved.postponed)); - } - - const chunks = []; - const { Writable } = await import('node:stream'); - const writable = new Writable({ - write(chunk, _, cb) { - chunks.push(Buffer.from(chunk)); - cb(); - }, - }); - - resolved.pipe(writable); - - await new Promise((resolve, reject) => { - writable.on('finish', resolve); - writable.on('error', reject); - }); - - const html = Buffer.concat(chunks).toString('utf-8'); - console.log('HTML length:', html.length); - console.log('HTML:', html); - console.log('Has $? marker:', html.includes('')); - console.log('Has $? marker:', html.includes('')); -} - -main().catch(console.error); diff --git a/test-prerender4.mjs b/test-prerender4.mjs deleted file mode 100644 index 2b72680..0000000 --- a/test-prerender4.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; -import { Writable } from 'node:stream'; - -// Simple component that doesn't throw -function StaticContent() { - return createElement('div', null, 'I am static'); -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(StaticContent, null) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function test() { - console.log('Starting prerender...'); - const result = await prerenderToNodeStream(createElement(App)); - console.log('Resolved!'); - console.log('Keys:', Object.keys(result)); - console.log('Has postponed:', 'postponed' in result, 'value:', result.postponed); - - const chunks = []; - const writable = new Writable({ - write(chunk, _, cb) { - chunks.push(Buffer.from(chunk)); - cb(); - }, - }); - - result.pipe(writable); - - await new Promise((resolve, reject) => { - writable.on('finish', resolve); - writable.on('error', reject); - }); - - const html = Buffer.concat(chunks).toString('utf-8'); - console.log('HTML length:', html.length); - console.log('HTML sample:', html.slice(0, 500)); - console.log('Has $? marker:', html.includes('')); - console.log('Has $? marker:', html.includes('')); -} - -test().catch(e => { - console.error('Error:', e.message); - console.error('Stack:', e.stack); -}); diff --git a/test-prerender5.mjs b/test-prerender5.mjs deleted file mode 100644 index 52b090b..0000000 --- a/test-prerender5.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; -import { Writable, Readable } from 'node:stream'; - -// Simple component that doesn't throw -function StaticContent() { - return createElement('div', null, 'I am static'); -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(StaticContent, null) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function test() { - console.log('Starting prerender...'); - const result = await prerenderToNodeStream(createElement(App)); - console.log('Keys:', Object.keys(result)); - console.log('postponed:', result.postponed); - console.log('prelude type:', typeof result.prelude); - console.log('prelude is ReadableStream:', result.prelude instanceof ReadableStream); - - // prelude might be a ReadableStream (Web API) - const reader = result.prelude.getReader(); - const chunks = []; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - } - - const decoder = new TextDecoder(); - const html = chunks.map(c => decoder.decode(c, { stream: true })).join(''); - console.log('HTML length:', html.length); - console.log('HTML sample:', html.slice(0, 1000)); - console.log('Has $? marker:', html.includes('')); -} - -test().catch(e => { - console.error('Error:', e.message); - console.error('Stack:', e.stack); -}); diff --git a/test-prerender6.mjs b/test-prerender6.mjs deleted file mode 100644 index bcf7faf..0000000 --- a/test-prerender6.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; -import { Writable } from 'node:stream'; - -function StaticContent() { - return createElement('div', null, 'I am static'); -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(StaticContent, null) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function test() { - console.log('Starting prerender...'); - const result = await prerenderToNodeStream(createElement(App)); - console.log('Keys:', Object.keys(result)); - console.log('postponed:', result.postponed); - console.log('prelude type:', typeof result.prelude); - console.log('prelude constructor:', result.prelude?.constructor?.name); - console.log('prelude prototype:', Object.getPrototypeOf(result.prelude)?.constructor?.name); - - // Check if it's iterable/async iterable - console.log('prelude[Symbol.asyncIterator]:', typeof result.prelude?.[Symbol.asyncIterator]); - console.log('prelude[Symbol.iterator]:', typeof result.prelude?.[Symbol.iterator]); - console.log('prelude.pipe:', typeof result.prelude?.pipe); - console.log('prelude.on:', typeof result.prelude?.on); - - // Try to get the prelude toString - console.log('prelude toString:', String(result.prelude)); -} - -test().catch(e => { - console.error('Error:', e.message); - console.error('Stack:', e.stack); -}); diff --git a/test-prerender7.mjs b/test-prerender7.mjs deleted file mode 100644 index 8b2bd4e..0000000 --- a/test-prerender7.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; -import { Writable } from 'node:stream'; - -function StaticContent() { - return createElement('div', null, 'I am static'); -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(StaticContent, null) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function test() { - console.log('Starting prerender...'); - const result = await prerenderToNodeStream(createElement(App)); - console.log('postponed:', result.postponed); - - const chunks = []; - const writable = new Writable({ - write(chunk, _, cb) { - chunks.push(Buffer.from(chunk)); - cb(); - }, - }); - - result.prelude.pipe(writable); - - await new Promise((resolve, reject) => { - writable.on('finish', resolve); - writable.on('error', reject); - }); - - const html = Buffer.concat(chunks).toString('utf-8'); - console.log('HTML length:', html.length); - console.log('HTML sample:', html.slice(0, 1000)); - console.log('Has $? marker:', html.includes('')); -} - -test().catch(e => { - console.error('Error:', e.message); - console.error('Stack:', e.stack); -}); diff --git a/test-prerender8.mjs b/test-prerender8.mjs deleted file mode 100644 index cd1cd9e..0000000 --- a/test-prerender8.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; -import { Writable } from 'node:stream'; - -const NEVER = new Promise(() => {}); - -function DynamicComponent({ name }) { - throw NEVER; -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(DynamicComponent, { name: 'CookieWidget' }) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function test() { - console.log('Starting prerender with async boundary...'); - const result = await prerenderToNodeStream(createElement(App)); - console.log('postponed:', result.postponed); - console.log('postponed type:', typeof result.postponed); - if (result.postponed) { - console.log('postponed keys:', Object.keys(result.postponed)); - console.log('postponed value:', String(result.postponed).slice(0, 200)); - console.log('is Buffer:', Buffer.isBuffer(result.postponed)); - } - - const chunks = []; - const writable = new Writable({ - write(chunk, _, cb) { - chunks.push(Buffer.from(chunk)); - cb(); - }, - }); - - result.prelude.pipe(writable); - - await new Promise((resolve, reject) => { - writable.on('finish', resolve); - writable.on('error', reject); - }); - - const html = Buffer.concat(chunks).toString('utf-8'); - console.log('HTML length:', html.length); - console.log('HTML:', html); - console.log('Has $? marker:', html.includes('')); - console.log('Has $ marker:', html.includes('')); -} - -test().catch(e => { - console.error('Error:', e.message); - console.error('Stack:', e.stack); -}); diff --git a/test-prerender9.mjs b/test-prerender9.mjs deleted file mode 100644 index e925358..0000000 --- a/test-prerender9.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import { prerenderToNodeStream } from 'react-dom/static'; -import { createElement, Suspense } from 'react'; -import { Writable } from 'node:stream'; - -const NEVER = new Promise(() => {}); - -function DynamicComponent({ name }) { - throw NEVER; -} - -// Use options with onShellReady/onAllReady callbacks -const options = { - onShellReady() { - console.log('onShellReady fired!'); - }, - onShellError(err) { - console.log('onShellError:', err); - }, - onAllReady() { - console.log('onAllReady fired!'); - }, -}; - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(DynamicComponent, { name: 'CookieWidget' }) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function test() { - console.log('Starting prerender with options...'); - try { - const result = await prerenderToNodeStream(createElement(App), options); - console.log('Resolved!'); - console.log('postponed:', result.postponed); - } catch (e) { - console.error('Error:', e.message); - } -} - -test(); diff --git a/test-resume.mjs b/test-resume.mjs deleted file mode 100644 index 6e0cfb1..0000000 --- a/test-resume.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { resumeToPipeableStream } from 'react-dom/server'; -import { createElement, Suspense } from 'react'; -import { Writable } from 'node:stream'; - -function DynamicContent() { - return createElement('div', null, 'Hello from resumed content!'); -} - -function App() { - return createElement('html', null, - createElement('body', null, - createElement('h1', null, 'Static Shell'), - createElement(Suspense, { fallback: createElement('div', null, 'Loading...') }, - createElement(DynamicContent, null) - ), - createElement('footer', null, 'Static Footer') - ) - ); -} - -async function test() { - console.log('Testing resumeToPipeableStream...'); - - // Try with null/undefined postponed state - try { - const result = resumeToPipeableStream(createElement(App), null); - console.log('Resume result type:', typeof result); - console.log('Resume result keys:', Object.keys(result)); - console.log('has pipe:', typeof result.pipe); - - const chunks = []; - const writable = new Writable({ - write(chunk, _, cb) { - chunks.push(Buffer.from(chunk)); - cb(); - }, - }); - - result.pipe(writable); - - await new Promise((resolve, reject) => { - writable.on('finish', resolve); - writable.on('error', reject); - }); - - const html = Buffer.concat(chunks).toString('utf-8'); - console.log('HTML length:', html.length); - console.log('HTML:', html); - } catch (e) { - console.log('Error with null:', e.message); - } -} - -test().catch(console.error);