refactor: remove unused components and files; update List component to use client-side rendering
- Deleted List.tsx, action.tsx, react.svg, client.tsx, entry.browser.tsx, entry.rsc.tsx, entry.ssr.tsx, error-boundary.tsx, request.tsx, index.css, and root.tsx as they were no longer needed. - Updated List component in pages/a/index.tsx to use useEffect for client-side behavior. - Removed pages/b/index.tsx as it was redundant. - Added new browser-entry.tsx and entry.tsx for client-side and server-side rendering respectively. - Introduced versioned component in pages/v/a/index.tsx to demonstrate async data fetching. - Updated tsconfig.json to allow unused local variables.x
This commit is contained in:
17
index.html
17
index.html
@@ -1,12 +1,17 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>RSC App</title>
|
<title>RSC App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
<body>
|
||||||
<script type="module" src="/src/entry-client.tsx"></script>
|
<div id="root">
|
||||||
</body>
|
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/browser-entry.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
12
package.json
12
package.json
@@ -5,20 +5,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "bun run --host src/entry.tsx"
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/cssinjs": "^2.1.2",
|
||||||
|
"antd": "^6.3.5",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5"
|
"react-dom": "^19.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "latest",
|
"rsc-html-stream": "^0.0.7"
|
||||||
"@vitejs/plugin-rsc": "latest",
|
|
||||||
"rsc-html-stream": "^0.0.7",
|
|
||||||
"vite": "^8.0.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1326
pnpm-lock.yaml
generated
1326
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
125
server.js
125
server.js
@@ -1,125 +0,0 @@
|
|||||||
// Production server for RSC
|
|
||||||
// Run with: node server.js
|
|
||||||
|
|
||||||
import { createServer } from 'node:http'
|
|
||||||
import { readFile } from 'node:fs/promises'
|
|
||||||
import { join, extname } from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
|
|
||||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
||||||
const PORT = process.env.PORT || 3000
|
|
||||||
|
|
||||||
// Import RSC handler - it exports { fetch: handler }
|
|
||||||
const rscHandler = (await import('./dist/rsc/index.js')).default.fetch
|
|
||||||
|
|
||||||
// MIME types
|
|
||||||
const mimeTypes = {
|
|
||||||
'.html': 'text/html',
|
|
||||||
'.js': 'application/javascript',
|
|
||||||
'.css': 'text/css',
|
|
||||||
'.json': 'application/json',
|
|
||||||
'.png': 'image/png',
|
|
||||||
'.jpg': 'image/jpeg',
|
|
||||||
'.svg': 'image/svg+xml',
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = createServer(async (req, res) => {
|
|
||||||
const url = new URL(req.url, `http://localhost:${PORT}`)
|
|
||||||
|
|
||||||
// Serve static client assets first (these don't go through RSC handler)
|
|
||||||
if (url.pathname.startsWith('/dist/client') ||
|
|
||||||
url.pathname.startsWith('/assets') ||
|
|
||||||
url.pathname === '/vite.svg') {
|
|
||||||
// Map paths to dist/client/*
|
|
||||||
let distPath
|
|
||||||
if (url.pathname.startsWith('/assets')) {
|
|
||||||
distPath = join(__dirname, '/dist/client', url.pathname)
|
|
||||||
} else if (url.pathname === '/vite.svg') {
|
|
||||||
distPath = join(__dirname, '/dist/client/vite.svg')
|
|
||||||
} else {
|
|
||||||
distPath = join(__dirname, url.pathname)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = await readFile(distPath)
|
|
||||||
const ext = extname(distPath)
|
|
||||||
const contentType = mimeTypes[ext] || 'application/octet-stream'
|
|
||||||
res.setHeader('Content-Type', contentType)
|
|
||||||
res.end(content)
|
|
||||||
} catch (err) {
|
|
||||||
res.statusCode = 404
|
|
||||||
res.end('Not Found')
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// All other routes use RSC handler (handles SSR, RSC, and dynamic pages)
|
|
||||||
try {
|
|
||||||
// Read the body first (for POST requests)
|
|
||||||
const bodyContent = req.method !== 'GET'
|
|
||||||
? await new Promise((resolve, reject) => {
|
|
||||||
const chunks = []
|
|
||||||
req.on('data', chunk => chunks.push(chunk))
|
|
||||||
req.on('end', () => resolve(Buffer.concat(chunks)))
|
|
||||||
req.on('error', reject)
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
// Clone the headers (req.headers is an object, not Map)
|
|
||||||
const headers = new Headers()
|
|
||||||
for (const key of Object.keys(req.headers)) {
|
|
||||||
const value = req.headers[key]
|
|
||||||
if (key.toLowerCase() !== 'content-length') {
|
|
||||||
headers.set(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create request with absolute URL (RSC handler requires full URL)
|
|
||||||
const rscRequest = new Request(`http://localhost:${PORT}${url.pathname}${url.search}`, {
|
|
||||||
method: req.method,
|
|
||||||
headers,
|
|
||||||
body: bodyContent,
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await rscHandler(rscRequest)
|
|
||||||
res.statusCode = response.status
|
|
||||||
|
|
||||||
// Forward headers
|
|
||||||
for (const [key, value] of response.headers.entries()) {
|
|
||||||
if (key !== 'transfer-encoding' && key !== 'content-length') {
|
|
||||||
res.setHeader(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For RSC stream, pipe directly
|
|
||||||
if (response.headers.get('content-type')?.includes('x-component')) {
|
|
||||||
response.body.pipe(res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For HTML, inject correct client assets base path
|
|
||||||
const html = await response.text()
|
|
||||||
const modifiedHtml = html.replace(
|
|
||||||
/import\("\/@id\/__x00__/g,
|
|
||||||
'import("/dist/client/@id/__x00__'
|
|
||||||
).replace(
|
|
||||||
/href="\/src\//g,
|
|
||||||
'href="/dist/client/src/'
|
|
||||||
).replace(
|
|
||||||
/src="\/src\//g,
|
|
||||||
'src="/dist/client/src/'
|
|
||||||
)
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/html')
|
|
||||||
res.end(modifiedHtml)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('RSC handler error:', err)
|
|
||||||
res.statusCode = 500
|
|
||||||
res.end('Internal Server Error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
|
||||||
console.log(`RSC Server running at http://localhost:${PORT}/`)
|
|
||||||
console.log(`Press Ctrl+C to stop`)
|
|
||||||
})
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
export default async function List() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>List</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
'use server'
|
|
||||||
|
|
||||||
let serverCounter = 0
|
|
||||||
|
|
||||||
export async function getServerCounter() {
|
|
||||||
return serverCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateServerCounter(change: number) {
|
|
||||||
serverCounter += change
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
14
src/browser-entry.tsx
Normal file
14
src/browser-entry.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// entry.tsx - 客户端入口
|
||||||
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
||||||
|
import A from './pages/a/index';
|
||||||
|
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
createRoot(root!).render(<A />);
|
||||||
|
|
||||||
|
// hydrateRoot(document.getElementById('root')!, <A />);
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Hydrating with new content...');
|
||||||
|
hydrateRoot(root!, <A />);
|
||||||
|
}, 30000)
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export function ClientCounter() {
|
|
||||||
const [count, setCount] = React.useState(0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
|
||||||
Client Counter: {count}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
15
src/entry.tsx
Normal file
15
src/entry.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import A from './pages/a/index';
|
||||||
|
import http from 'http';
|
||||||
|
|
||||||
|
const html = renderToString(<A />, {
|
||||||
|
identifierPrefix: 'test-',
|
||||||
|
});
|
||||||
|
console.log(html);
|
||||||
|
|
||||||
|
http.createServer((req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(html);
|
||||||
|
}).listen(3000, () => {
|
||||||
|
console.log('Server is running on http://localhost:3000');
|
||||||
|
});
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import {
|
|
||||||
createFromReadableStream,
|
|
||||||
createFromFetch,
|
|
||||||
setServerCallback,
|
|
||||||
createTemporaryReferenceSet,
|
|
||||||
encodeReply,
|
|
||||||
} from '@vitejs/plugin-rsc/browser'
|
|
||||||
import React from 'react'
|
|
||||||
import { createRoot, hydrateRoot } from 'react-dom/client'
|
|
||||||
import { rscStream } from 'rsc-html-stream/client'
|
|
||||||
import type { RscPayload } from './entry.rsc'
|
|
||||||
import { GlobalErrorBoundary } from './error-boundary'
|
|
||||||
import { createRscRenderRequest } from './request'
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// stash `setPayload` function to trigger re-rendering
|
|
||||||
// from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr)
|
|
||||||
let setPayload: (v: RscPayload) => void
|
|
||||||
|
|
||||||
// deserialize RSC stream back to React VDOM for CSR
|
|
||||||
const initialPayload = await createFromReadableStream<RscPayload>(
|
|
||||||
// initial RSC stream is injected in SSR stream as <script>...FLIGHT_DATA...</script>
|
|
||||||
rscStream,
|
|
||||||
)
|
|
||||||
|
|
||||||
// browser root component to (re-)render RSC payload as state
|
|
||||||
function BrowserRoot() {
|
|
||||||
const [payload, setPayload_] = React.useState(initialPayload)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setPayload = (v) => React.startTransition(() => setPayload_(v))
|
|
||||||
}, [setPayload_])
|
|
||||||
|
|
||||||
// re-fetch/render on client side navigation
|
|
||||||
React.useEffect(() => {
|
|
||||||
return listenNavigation(() => fetchRscPayload())
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return payload.root
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-fetch RSC and trigger re-rendering
|
|
||||||
async function fetchRscPayload() {
|
|
||||||
const renderRequest = createRscRenderRequest(window.location.href)
|
|
||||||
const payload = await createFromFetch<RscPayload>(fetch(renderRequest))
|
|
||||||
setPayload(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// register a handler which will be internally called by React
|
|
||||||
// on server function request after hydration.
|
|
||||||
setServerCallback(async (id, args) => {
|
|
||||||
const temporaryReferences = createTemporaryReferenceSet()
|
|
||||||
const renderRequest = createRscRenderRequest(window.location.href, {
|
|
||||||
id,
|
|
||||||
body: await encodeReply(args, { temporaryReferences }),
|
|
||||||
})
|
|
||||||
const payload = await createFromFetch<RscPayload>(fetch(renderRequest), {
|
|
||||||
temporaryReferences,
|
|
||||||
})
|
|
||||||
setPayload(payload)
|
|
||||||
const { ok, data } = payload.returnValue!
|
|
||||||
if (!ok) throw data
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
|
|
||||||
// hydration
|
|
||||||
const browserRoot = (
|
|
||||||
<React.StrictMode>
|
|
||||||
<GlobalErrorBoundary>
|
|
||||||
<BrowserRoot />
|
|
||||||
</GlobalErrorBoundary>
|
|
||||||
</React.StrictMode>
|
|
||||||
)
|
|
||||||
if ('__NO_HYDRATE' in globalThis) {
|
|
||||||
createRoot(document).render(browserRoot)
|
|
||||||
} else {
|
|
||||||
hydrateRoot(document, browserRoot, {
|
|
||||||
formState: initialPayload.formState,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// implement server HMR by triggering re-fetch/render of RSC upon server code change
|
|
||||||
if (import.meta.hot) {
|
|
||||||
import.meta.hot.on('rsc:update', () => {
|
|
||||||
fetchRscPayload()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a little helper to setup events interception for client side navigation
|
|
||||||
function listenNavigation(onNavigation: () => void) {
|
|
||||||
window.addEventListener('popstate', onNavigation)
|
|
||||||
|
|
||||||
const oldPushState = window.history.pushState
|
|
||||||
window.history.pushState = function (...args) {
|
|
||||||
const res = oldPushState.apply(this, args)
|
|
||||||
onNavigation()
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldReplaceState = window.history.replaceState
|
|
||||||
window.history.replaceState = function (...args) {
|
|
||||||
const res = oldReplaceState.apply(this, args)
|
|
||||||
onNavigation()
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClick(e: MouseEvent) {
|
|
||||||
let link = (e.target as Element).closest('a')
|
|
||||||
if (
|
|
||||||
link &&
|
|
||||||
link instanceof HTMLAnchorElement &&
|
|
||||||
link.href &&
|
|
||||||
(!link.target || link.target === '_self') &&
|
|
||||||
link.origin === location.origin &&
|
|
||||||
!link.hasAttribute('download') &&
|
|
||||||
e.button === 0 && // left clicks only
|
|
||||||
!e.metaKey && // open in new tab (mac)
|
|
||||||
!e.ctrlKey && // open in new tab (windows)
|
|
||||||
!e.altKey && // download
|
|
||||||
!e.shiftKey &&
|
|
||||||
!e.defaultPrevented
|
|
||||||
) {
|
|
||||||
e.preventDefault()
|
|
||||||
history.pushState(null, '', link.href)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('click', onClick)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('click', onClick)
|
|
||||||
window.removeEventListener('popstate', onNavigation)
|
|
||||||
window.history.pushState = oldPushState
|
|
||||||
window.history.replaceState = oldReplaceState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import {
|
|
||||||
renderToReadableStream,
|
|
||||||
createTemporaryReferenceSet,
|
|
||||||
decodeReply,
|
|
||||||
loadServerAction,
|
|
||||||
decodeAction,
|
|
||||||
decodeFormState,
|
|
||||||
} from '@vitejs/plugin-rsc/rsc'
|
|
||||||
import type { ReactFormState } from 'react-dom/client'
|
|
||||||
import { Root } from '../root.tsx'
|
|
||||||
import { parseRenderRequest } from './request.tsx'
|
|
||||||
import type { ComponentType } from 'react'
|
|
||||||
|
|
||||||
// The schema of payload which is serialized into RSC stream on rsc environment
|
|
||||||
// and deserialized on ssr/client environments.
|
|
||||||
export type RscPayload = {
|
|
||||||
// this demo renders/serializes/deserizlies entire root html element
|
|
||||||
// but this mechanism can be changed to render/fetch different parts of components
|
|
||||||
// based on your own route conventions.
|
|
||||||
root: React.ReactNode
|
|
||||||
// server action return value of non-progressive enhancement case
|
|
||||||
returnValue?: { ok: boolean; data: unknown }
|
|
||||||
// server action form state (e.g. useActionState) of progressive enhancement case
|
|
||||||
formState?: ReactFormState
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic page module loading - all pages are lazy loaded on demand
|
|
||||||
const pageModules = import.meta.glob('../pages/**/*.tsx', { eager: false })
|
|
||||||
|
|
||||||
// Find page component by pathname - dynamically resolves paths
|
|
||||||
async function findPage(pathname: string): Promise<ComponentType<any> | null> {
|
|
||||||
const segments = pathname.split('/').filter(Boolean)
|
|
||||||
|
|
||||||
// Build candidate paths to try
|
|
||||||
const candidates: string[] = []
|
|
||||||
|
|
||||||
if (segments.length === 0) {
|
|
||||||
// Root path: try index.tsx
|
|
||||||
candidates.push('../pages/index.tsx')
|
|
||||||
} else {
|
|
||||||
// Try original case path: /a -> pages/a/index.tsx, /blog -> pages/blog/index.tsx
|
|
||||||
const originalPath = `../pages/${segments.join('/')}/index.tsx`
|
|
||||||
candidates.push(originalPath)
|
|
||||||
|
|
||||||
// Try capitalized file: /pageA -> pages/PageA.tsx
|
|
||||||
const capitalizedPath = `../pages/${segments.map(s =>
|
|
||||||
s.charAt(0).toUpperCase() + s.slice(1)
|
|
||||||
).join('/')}.tsx`
|
|
||||||
candidates.push(capitalizedPath)
|
|
||||||
|
|
||||||
// Try capitalized index route: /a -> pages/A/index.tsx
|
|
||||||
const capitalizedIndexPath = `${capitalizedPath.replace('.tsx', '')}/index.tsx`
|
|
||||||
candidates.push(capitalizedIndexPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find first matching module
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
const loader = pageModules[candidate]
|
|
||||||
if (loader) {
|
|
||||||
const mod = await loader() as { default: ComponentType<any> }
|
|
||||||
return mod.default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// the plugin by default assumes `rsc` entry having default export of request handler.
|
|
||||||
// however, how server entries are executed can be customized by registering own server handler.
|
|
||||||
export default { fetch: handler }
|
|
||||||
|
|
||||||
async function handler(request: Request): Promise<Response> {
|
|
||||||
// differentiate RSC, SSR, action, etc.
|
|
||||||
const renderRequest = parseRenderRequest(request)
|
|
||||||
request = renderRequest.request
|
|
||||||
|
|
||||||
// handle server function request
|
|
||||||
let returnValue: RscPayload['returnValue'] | undefined
|
|
||||||
let formState: ReactFormState | undefined
|
|
||||||
let temporaryReferences: unknown | undefined
|
|
||||||
let actionStatus: number | undefined
|
|
||||||
if (renderRequest.isAction === true) {
|
|
||||||
if (renderRequest.actionId) {
|
|
||||||
// action is called via `ReactClient.setServerCallback`.
|
|
||||||
const contentType = request.headers.get('content-type')
|
|
||||||
const body = contentType?.startsWith('multipart/form-data')
|
|
||||||
? await request.formData()
|
|
||||||
: await request.text()
|
|
||||||
temporaryReferences = createTemporaryReferenceSet()
|
|
||||||
const args = await decodeReply(body, { temporaryReferences })
|
|
||||||
const action = await loadServerAction(renderRequest.actionId)
|
|
||||||
try {
|
|
||||||
const data = await action.apply(null, args)
|
|
||||||
returnValue = { ok: true, data }
|
|
||||||
} catch (e) {
|
|
||||||
returnValue = { ok: false, data: e }
|
|
||||||
actionStatus = 500
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// otherwise server function is called via `<form action={...}>`
|
|
||||||
// before hydration (e.g. when javascript is disabled).
|
|
||||||
// aka progressive enhancement.
|
|
||||||
const formData = await request.formData()
|
|
||||||
const decodedAction = await decodeAction(formData)
|
|
||||||
try {
|
|
||||||
const result = await decodedAction()
|
|
||||||
formState = await decodeFormState(result, formData)
|
|
||||||
} catch (e) {
|
|
||||||
// there's no single general obvious way to surface this error,
|
|
||||||
// so explicitly return classic 500 response.
|
|
||||||
return new Response('Internal Server Error: server action failed', {
|
|
||||||
status: 500,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic page resolution based on URL pathname
|
|
||||||
const pathname = renderRequest.url.pathname
|
|
||||||
const Page = await findPage(pathname)
|
|
||||||
|
|
||||||
// If no page found and it's not the root path, return 404
|
|
||||||
if (!Page && pathname !== '/') {
|
|
||||||
return new Response('Not Found: ' + pathname, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// serialization from React VDOM tree to RSC stream.
|
|
||||||
// we render RSC stream after handling server function request
|
|
||||||
// so that new render reflects updated state from server function call
|
|
||||||
// to achieve single round trip to mutate and fetch from server.
|
|
||||||
const rscPayload: RscPayload = {
|
|
||||||
root: <Root url={renderRequest.url} page={Page ? <Page /> : undefined} />,
|
|
||||||
formState,
|
|
||||||
returnValue,
|
|
||||||
}
|
|
||||||
const rscOptions = { temporaryReferences }
|
|
||||||
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions)
|
|
||||||
|
|
||||||
// Respond RSC stream without HTML rendering as decided by `RenderRequest`
|
|
||||||
if (renderRequest.isRsc) {
|
|
||||||
return new Response(rscStream, {
|
|
||||||
status: actionStatus,
|
|
||||||
headers: {
|
|
||||||
'content-type': 'text/x-component;charset=utf-8',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delegate to SSR environment for html rendering.
|
|
||||||
// The plugin provides `loadModule` helper to allow loading SSR environment entry module
|
|
||||||
// in RSC environment. however this can be customized by implementing own runtime communication
|
|
||||||
// e.g. `@cloudflare/vite-plugin`'s service binding.
|
|
||||||
const ssrEntryModule = await import.meta.viteRsc.loadModule<
|
|
||||||
typeof import('./entry.ssr.tsx')
|
|
||||||
>('ssr', 'index')
|
|
||||||
const ssrResult = await ssrEntryModule.renderHTML(rscStream, {
|
|
||||||
formState,
|
|
||||||
// allow quick simulation of javascript disabled browser
|
|
||||||
debugNojs: renderRequest.url.searchParams.has('__nojs'),
|
|
||||||
})
|
|
||||||
|
|
||||||
// respond html
|
|
||||||
return new Response(ssrResult.stream, {
|
|
||||||
status: ssrResult.status,
|
|
||||||
headers: {
|
|
||||||
'Content-type': 'text/html',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.hot) {
|
|
||||||
import.meta.hot.accept()
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
||||||
import React from 'react'
|
|
||||||
import type { ReactFormState } from 'react-dom/client'
|
|
||||||
import { renderToReadableStream } from 'react-dom/server.edge'
|
|
||||||
import { injectRSCPayload } from 'rsc-html-stream/server'
|
|
||||||
import type { RscPayload } from './entry.rsc'
|
|
||||||
|
|
||||||
export async function renderHTML(
|
|
||||||
rscStream: ReadableStream<Uint8Array>,
|
|
||||||
options: {
|
|
||||||
formState?: ReactFormState
|
|
||||||
nonce?: string
|
|
||||||
debugNojs?: boolean
|
|
||||||
},
|
|
||||||
): Promise<{ stream: ReadableStream<Uint8Array>; status?: number }> {
|
|
||||||
// duplicate one RSC stream into two.
|
|
||||||
// - one for SSR (ReactClient.createFromReadableStream below)
|
|
||||||
// - another for browser hydration payload by injecting <script>...FLIGHT_DATA...</script>.
|
|
||||||
const [rscStream1, rscStream2] = rscStream.tee()
|
|
||||||
|
|
||||||
// deserialize RSC stream back to React VDOM
|
|
||||||
let payload: Promise<RscPayload> | undefined
|
|
||||||
function SsrRoot() {
|
|
||||||
// deserialization needs to be kicked off inside ReactDOMServer context
|
|
||||||
// for ReactDomServer preinit/preloading to work
|
|
||||||
payload ??= createFromReadableStream<RscPayload>(rscStream1)
|
|
||||||
return React.use(payload).root
|
|
||||||
}
|
|
||||||
|
|
||||||
// render html (traditional SSR)
|
|
||||||
const bootstrapScriptContent =
|
|
||||||
await import.meta.viteRsc.loadBootstrapScriptContent('index')
|
|
||||||
let htmlStream: ReadableStream<Uint8Array>
|
|
||||||
let status: number | undefined
|
|
||||||
try {
|
|
||||||
htmlStream = await renderToReadableStream(<SsrRoot />, {
|
|
||||||
bootstrapScriptContent: options?.debugNojs
|
|
||||||
? undefined
|
|
||||||
: bootstrapScriptContent,
|
|
||||||
nonce: options?.nonce,
|
|
||||||
formState: options?.formState,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
// fallback to render an empty shell and run pure CSR on browser,
|
|
||||||
// which can replay server component error and trigger error boundary.
|
|
||||||
status = 500
|
|
||||||
htmlStream = await renderToReadableStream(
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<noscript>Internal Server Error: SSR failed</noscript>
|
|
||||||
</body>
|
|
||||||
</html>,
|
|
||||||
{
|
|
||||||
bootstrapScriptContent:
|
|
||||||
`self.__NO_HYDRATE=1;` +
|
|
||||||
(options?.debugNojs ? '' : bootstrapScriptContent),
|
|
||||||
nonce: options?.nonce,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseStream: ReadableStream<Uint8Array> = htmlStream
|
|
||||||
if (!options?.debugNojs) {
|
|
||||||
// initial RSC stream is injected in HTML stream as <script>...FLIGHT_DATA...</script>
|
|
||||||
// using utility made by devongovett https://github.com/devongovett/rsc-html-stream
|
|
||||||
responseStream = responseStream.pipeThrough(
|
|
||||||
injectRSCPayload(rscStream2, {
|
|
||||||
nonce: options?.nonce,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stream: responseStream, status }
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
// Minimal ErrorBoundary example to handle errors globally on browser
|
|
||||||
export function GlobalErrorBoundary(props: { children?: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<ErrorBoundary errorComponent={DefaultGlobalErrorPage}>
|
|
||||||
{props.children}
|
|
||||||
</ErrorBoundary>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/vercel/next.js/blob/33f8428f7066bf8b2ec61f025427ceb2a54c4bdf/packages/next/src/client/components/error-boundary.tsx
|
|
||||||
// https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
|
||||||
class ErrorBoundary extends React.Component<{
|
|
||||||
children?: React.ReactNode
|
|
||||||
errorComponent: React.FC<{
|
|
||||||
error: Error
|
|
||||||
reset: () => void
|
|
||||||
}>
|
|
||||||
}> {
|
|
||||||
state: { error?: Error } = {}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error) {
|
|
||||||
return { error }
|
|
||||||
}
|
|
||||||
|
|
||||||
reset = () => {
|
|
||||||
this.setState({ error: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const error = this.state.error
|
|
||||||
if (error) {
|
|
||||||
return <this.props.errorComponent error={error} reset={this.reset} />
|
|
||||||
}
|
|
||||||
return this.props.children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/build/webpack/loaders/next-app-loader.ts#L73
|
|
||||||
// https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/client/components/error-boundary.tsx#L145
|
|
||||||
function DefaultGlobalErrorPage(props: { error: Error; reset: () => void }) {
|
|
||||||
return (
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Unexpected Error</title>
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
style={{
|
|
||||||
height: '100vh',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
placeContent: 'center',
|
|
||||||
placeItems: 'center',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 400,
|
|
||||||
lineHeight: '24px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>Caught an unexpected error</p>
|
|
||||||
<pre>
|
|
||||||
Error:{' '}
|
|
||||||
{import.meta.env.DEV && 'message' in props.error
|
|
||||||
? props.error.message
|
|
||||||
: '(Unknown)'}
|
|
||||||
</pre>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
React.startTransition(() => {
|
|
||||||
props.reset()
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// Framework conventions (arbitrary choices for this demo):
|
|
||||||
// - Use `_.rsc` URL suffix to differentiate RSC requests from SSR requests
|
|
||||||
// - Use `x-rsc-action` header to pass server action ID
|
|
||||||
const URL_POSTFIX = '_.rsc'
|
|
||||||
const HEADER_ACTION_ID = 'x-rsc-action'
|
|
||||||
|
|
||||||
// Parsed request information used to route between RSC/SSR rendering and action handling.
|
|
||||||
// Created by parseRenderRequest() from incoming HTTP requests.
|
|
||||||
type RenderRequest = {
|
|
||||||
isRsc: boolean // true if request should return RSC payload (via _.rsc suffix)
|
|
||||||
isAction: boolean // true if this is a server action call (POST request)
|
|
||||||
actionId?: string // server action ID from x-rsc-action header
|
|
||||||
request: Request // normalized Request with _.rsc suffix removed from URL
|
|
||||||
url: URL // normalized URL with _.rsc suffix removed
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRscRenderRequest(
|
|
||||||
urlString: string,
|
|
||||||
action?: { id: string; body: BodyInit },
|
|
||||||
): Request {
|
|
||||||
const url = new URL(urlString)
|
|
||||||
url.pathname += URL_POSTFIX
|
|
||||||
const headers = new Headers()
|
|
||||||
if (action) {
|
|
||||||
headers.set(HEADER_ACTION_ID, action.id)
|
|
||||||
}
|
|
||||||
return new Request(url.toString(), {
|
|
||||||
method: action ? 'POST' : 'GET',
|
|
||||||
headers,
|
|
||||||
body: action?.body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseRenderRequest(request: Request): RenderRequest {
|
|
||||||
const url = new URL(request.url)
|
|
||||||
const isAction = request.method === 'POST'
|
|
||||||
if (url.pathname.endsWith(URL_POSTFIX)) {
|
|
||||||
url.pathname = url.pathname.slice(0, -URL_POSTFIX.length)
|
|
||||||
const actionId = request.headers.get(HEADER_ACTION_ID) || undefined
|
|
||||||
if (request.method === 'POST' && !actionId) {
|
|
||||||
throw new Error('Missing action id header for RSC action request')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isRsc: true,
|
|
||||||
isAction,
|
|
||||||
actionId,
|
|
||||||
request: new Request(url, request),
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
isRsc: false,
|
|
||||||
isAction,
|
|
||||||
request,
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
112
src/index.css
112
src/index.css
@@ -1,112 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
'use server';
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export default async function List() {
|
export default function List() {
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('useEffect in List');
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>List A</h1>
|
<h1>List 2</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
export default async function List() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>List B</h1>
|
|
||||||
<div>123</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
18
src/pages/v/a/index.tsx
Normal file
18
src/pages/v/a/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
'use server';
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const getVersion = async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
return '1.0.0';
|
||||||
|
}
|
||||||
|
export default async function List() {
|
||||||
|
const v = await getVersion();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>List - Version {v}</h1>
|
||||||
|
<div style={{
|
||||||
|
width: 200
|
||||||
|
}}>Primary Button</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
src/root.tsx
85
src/root.tsx
@@ -1,85 +0,0 @@
|
|||||||
import './index.css' // css import is automatically injected in exported server components
|
|
||||||
import viteLogo from '/vite.svg'
|
|
||||||
import { getServerCounter, updateServerCounter } from './action.tsx'
|
|
||||||
import reactLogo from './assets/react.svg'
|
|
||||||
import { ClientCounter } from './client.tsx'
|
|
||||||
import List from './List'
|
|
||||||
|
|
||||||
export function Root(props: { url: URL; page?: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charSet="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vite + RSC</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<App {...props} />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function App(props: { url: URL; page?: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div id="root">
|
|
||||||
<div>
|
|
||||||
<a href="https://vite.dev" target="_blank">
|
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://react.dev/reference/rsc/server-components"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<h1>Vite + RSC</h1>
|
|
||||||
{/* Dynamic page content */}
|
|
||||||
{props.page || <DefaultHome />}
|
|
||||||
<ul className="read-the-docs">
|
|
||||||
<li>
|
|
||||||
Edit <code>src/client.tsx</code> to test client HMR.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Edit <code>src/root.tsx</code> to test server HMR.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Visit{' '}
|
|
||||||
<a href="./_.rsc" target="_blank">
|
|
||||||
<code>_.rsc</code>
|
|
||||||
</a>{' '}
|
|
||||||
to view RSC stream payload.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Visit{' '}
|
|
||||||
<a href="?__nojs" target="_blank">
|
|
||||||
<code>?__nojs</code>
|
|
||||||
</a>{' '}
|
|
||||||
to test server action without js enabled.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default home page content when no dynamic page is provided
|
|
||||||
function DefaultHome() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="card">
|
|
||||||
<ClientCounter />
|
|
||||||
</div>
|
|
||||||
<div className="card">
|
|
||||||
<form action={updateServerCounter.bind(null, 1)}>
|
|
||||||
<button>Server Counter: {getServerCounter()}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div className="card">Request URL: Home</div>
|
|
||||||
<div className="card">
|
|
||||||
<List />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
29
src/ssr.tsx
Normal file
29
src/ssr.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { renderToReadableStream } from 'react-dom/server';
|
||||||
|
import VA from './pages/v/a';
|
||||||
|
import { createRoot, hydrateRoot } from 'react-dom/client'
|
||||||
|
export async function renderToString(comp: React.ReactElement): Promise<string> {
|
||||||
|
const response = await renderToReadableStream(
|
||||||
|
<>{comp}</>,
|
||||||
|
{
|
||||||
|
bootstrapScripts: []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const reader = response.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let html = '';
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
html += decoder.decode(value);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(await renderToString(<VA />));
|
||||||
|
|
||||||
|
// const root = createRoot(document.getElementById('root')!);
|
||||||
|
// root.render(<VA />);
|
||||||
|
// hydrateRoot(document.getElementById('root')!, <VA />);
|
||||||
|
//
|
||||||
|
// console.log(await renderToString(<VA />));
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
@@ -10,8 +10,13 @@
|
|||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": [
|
||||||
"types": ["vite/client", "@vitejs/plugin-rsc/types"],
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
],
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import react from '@vitejs/plugin-react'
|
|
||||||
import rsc from '@vitejs/plugin-rsc'
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
rsc({
|
|
||||||
// `entries` option is only a shorthand for specifying each `rollupOptions.input` below
|
|
||||||
// > entries: { rsc, ssr, client },
|
|
||||||
//
|
|
||||||
// by default, the plugin setup request handler based on `default export` of `rsc` environment `rollupOptions.input.index`.
|
|
||||||
// This can be disabled when setting up own server handler e.g. `@cloudflare/vite-plugin`.
|
|
||||||
// > serverHandler: false
|
|
||||||
}),
|
|
||||||
|
|
||||||
// use any of react plugins https://github.com/vitejs/vite-plugin-react
|
|
||||||
// to enable client component HMR
|
|
||||||
react(),
|
|
||||||
|
|
||||||
// use https://github.com/antfu-collective/vite-plugin-inspect
|
|
||||||
// to understand internal transforms required for RSC.
|
|
||||||
// import("vite-plugin-inspect").then(m => m.default()),
|
|
||||||
],
|
|
||||||
|
|
||||||
// specify entry point for each environment.
|
|
||||||
// (currently the plugin assumes `rollupOptions.input.index` for some features.)
|
|
||||||
environments: {
|
|
||||||
// `rsc` environment loads modules with `react-server` condition.
|
|
||||||
// this environment is responsible for:
|
|
||||||
// - RSC stream serialization (React VDOM -> RSC stream)
|
|
||||||
// - server functions handling
|
|
||||||
rsc: {
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
input: {
|
|
||||||
index: './src/framework/entry.rsc.tsx',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// `ssr` environment loads modules without `react-server` condition.
|
|
||||||
// this environment is responsible for:
|
|
||||||
// - RSC stream deserialization (RSC stream -> React VDOM)
|
|
||||||
// - traditional SSR (React VDOM -> HTML string/stream)
|
|
||||||
ssr: {
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
input: {
|
|
||||||
index: './src/framework/entry.ssr.tsx',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// client environment is used for hydration and client-side rendering
|
|
||||||
// this environment is responsible for:
|
|
||||||
// - RSC stream deserialization (RSC stream -> React VDOM)
|
|
||||||
// - traditional CSR (React VDOM -> Browser DOM tree mount/hydration)
|
|
||||||
// - refetch and re-render RSC
|
|
||||||
// - calling server functions
|
|
||||||
client: {
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
input: {
|
|
||||||
index: './src/framework/entry.browser.tsx',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user