diff --git a/package.json b/package.json index 6f927b6..ce8f994 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@ant-design/cssinjs": "^2.1.2", "antd": "^6.3.5", + "fast-glob": "^3.3.3", "react": "^19.2.5", "react-dom": "^19.2.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02c96bc..55e94f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: antd: specifier: ^6.3.5 version: 6.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 react: specifier: ^19.2.5 version: 19.2.5 @@ -84,6 +87,18 @@ packages: '@emotion/unitless@0.7.5': resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@rc-component/async-validator@5.1.0': resolution: {integrity: sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==} engines: {node: '>=14.x'} @@ -387,6 +402,10 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + bun-types@1.3.12: resolution: {integrity: sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA==} @@ -403,12 +422,54 @@ packages: dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-mobile@5.0.0: resolution: {integrity: sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + json2mq@0.2.0: resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-dom@19.2.5: resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: @@ -421,9 +482,16 @@ packages: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rsc-html-stream@0.0.7: resolution: {integrity: sha512-v9+fuY7usTgvXdNl8JmfXCvSsQbq2YMd60kOeeMIqCJFZ69fViuIxztHei7v5mlMMa2h3SqS+v44Gu9i9xANZA==} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -440,6 +508,10 @@ packages: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} @@ -497,6 +569,18 @@ snapshots: '@emotion/unitless@0.7.5': {} + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@rc-component/async-validator@5.1.0': dependencies: '@babel/runtime': 7.29.2 @@ -916,6 +1000,10 @@ snapshots: - luxon - moment + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + bun-types@1.3.12: dependencies: '@types/node': 25.6.0 @@ -928,12 +1016,51 @@ snapshots: dayjs@1.11.20: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-mobile@5.0.0: {} + is-number@7.0.0: {} + json2mq@0.2.0: dependencies: string-convert: 0.2.1 + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + picomatch@2.3.2: {} + + queue-microtask@1.2.3: {} + react-dom@19.2.5(react@19.2.5): dependencies: react: 19.2.5 @@ -943,8 +1070,14 @@ snapshots: react@19.2.5: {} + reusify@1.1.0: {} + rsc-html-stream@0.0.7: {} + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + scheduler@0.27.0: {} scroll-into-view-if-needed@3.1.0: @@ -957,4 +1090,8 @@ snapshots: throttle-debounce@5.0.2: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + undici-types@7.19.2: {} diff --git a/src/browser-entry.tsx b/src/browser-entry.tsx index ef0f42f..c0509ed 100644 --- a/src/browser-entry.tsx +++ b/src/browser-entry.tsx @@ -1,4 +1,4 @@ -import { hydrateRoot } from 'react-dom/client'; +import { hydrateRoot, createRoot } from 'react-dom/client'; import App from './pages/a/ClientApp'; // import AServer from './pages/a/server/index.tsx'; @@ -14,5 +14,11 @@ declare global { if (typeof document !== 'undefined') { // const data = window.__SERVER_DATA__ ?? { version: '' }; - hydrateRoot(document.getElementById('root')!, ); + // hydrateRoot(document.getElementById('root')!, ); + + // hydrateRoot(document.getElementById('root')!, ); + // console.log('Hydration complete'); + + const root = createRoot(document.getElementById('root')!); + root.render(); } \ No newline at end of file diff --git a/src/entry.tsx b/src/entry.tsx index 5ae2c99..209cdd3 100644 --- a/src/entry.tsx +++ b/src/entry.tsx @@ -4,7 +4,7 @@ import A from './pages/a/List'; Bun.serve({ port: 3000, async fetch(req) { - const stream = await renderToReadableStream(, { + const stream = await renderToReadableStream(, { bootstrapScripts: [], }); diff --git a/src/pages/a/ClientApp.tsx b/src/pages/a/ClientApp.tsx index a718f0e..6b5930e 100644 --- a/src/pages/a/ClientApp.tsx +++ b/src/pages/a/ClientApp.tsx @@ -1,19 +1,12 @@ 'use client'; import List from './List.tsx'; -// Client Component - 用于 hydration,结构需要和 ServerApp 一致 -declare global { - interface Window { - __SERVER_DATA__?: { version: string }; - } -} - export default function ClientApp() { - const version = typeof window !== 'undefined' && window.__SERVER_DATA__?.version - ? window.__SERVER_DATA__.version - : 'loading'; - - return ; + const data = typeof window !== 'undefined' && window.__SERVER_DATA__ ? window.__SERVER_DATA__ : { version: 'loading' }; + return ; } // 'use client'; diff --git a/src/pages/a/List.tsx b/src/pages/a/List.tsx index 647599a..9369df0 100644 --- a/src/pages/a/List.tsx +++ b/src/pages/a/List.tsx @@ -1,15 +1,22 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; -export default function List({ version }: { version: string }) { +const Item = () => { + return
Item
+} +export default function List({ version, data = {} }: { version: string, data: any }) { useEffect(() => { console.log('useEffect in List123'); + console.log('Received data in List:', data); }, []); - //
Timestamp: {timestamp ? new Date(timestamp).toLocaleTimeString('en-US') : 'N/A'}
+ const tm =
Timestamp: {data.timestamp ? new Date(data.timestamp).toLocaleTimeString('en-US') : 'N/A'}
return (
-
sdf3
-

List - Version {version}

+
sdf32323232
+ {tm} + {version === '2.0.0' && } + {/*

List - Version {version}

*/} + {/*

List - Version2 {version + 1}

*/}
Primary Button
diff --git a/src/pages/a/main.tsx b/src/pages/a/main.tsx index ea94803..4acbcef 100644 --- a/src/pages/a/main.tsx +++ b/src/pages/a/main.tsx @@ -8,7 +8,8 @@ type ServerData = { async function fetchServerData() { await new Promise(resolve => setTimeout(resolve, 1000)); return { - version: '3.0.0', + // version: '3.0.0' + Math.floor(Math.random() * 100), + version: '2.0.0', timestamp: Date.now(), innerHtml: '
Inner HTML Content
' }; @@ -27,6 +28,6 @@ export const getData = fetchServerData; // 导出数据获取函数,供 server // Server Component - 通过 props 接收数据,不再自己 fetch export default async function ServerApp(props: { data: ServerData }) { return <> - + } diff --git a/src/server-node.tsx b/src/server-node.tsx index 8ad1a71..694dbc5 100644 --- a/src/server-node.tsx +++ b/src/server-node.tsx @@ -3,7 +3,7 @@ import Main, { createServerDataScript, getData } from './pages/a/main.tsx'; import http from 'http'; import fs from 'fs'; import path from 'path'; - +import fg from 'fast-glob' const PORT = 3000; const distDir = path.join(process.cwd(), 'dist'); const indexHtmlPath = path.join(process.cwd(), 'dist/index.html'); @@ -19,10 +19,24 @@ const mimeTypes: Record = { '.ico': 'image/x-icon', }; +const indexList = fg.globSync('index-*.js', { + cwd: process.cwd() + '/dist', +}) +const indexPath = indexList.map(file => { + const basename = path.basename(file, '.js'); + const [_, hash] = basename.split('-'); + console.log('Found index file:', `http://localhost:${PORT}/${basename}`); + return { file, hash, basename }; +}); -async function ssrRender(res: http.ServerResponse, mode: 'server' | 'client' = 'client') { +async function ssrRender(res: http.ServerResponse, opts: { + mode: 'server' | 'client', + urlPath: string, + filename?: string +}) { let template: string; + const mode = opts.mode || 'client'; try { template = fs.readFileSync(indexHtmlPath, 'utf-8'); } catch { @@ -30,7 +44,7 @@ async function ssrRender(res: http.ServerResponse, mode: 'server' | 'client' = ' res.end('dist/index.html not found'); return; } - if (mode === 'client' ) { + if (mode === 'client') { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(template); return; @@ -38,19 +52,21 @@ async function ssrRender(res: http.ServerResponse, mode: 'server' | 'client' = ' try { const serverData = await getData() + const filename = opts.filename; + const bootstrapScripts = [filename] + const scriptTag = bootstrapScripts.map(src => ``).join(''); const stream = await renderToReadableStream(
, { - bootstrapScripts: [] }); let renderedHtml = ''; for await (const chunk of stream) { renderedHtml += new TextDecoder().decode(chunk); } - - // console.log('Rendered HTML:', renderedHtml); + console.log('Rendered HTML:', renderedHtml); const html = template .replace('
', `
${renderedHtml}
`) - .replace('', `${createServerDataScript(serverData)}`); + .replace('', `${createServerDataScript(serverData)}`) + .replace('', `${scriptTag}`); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); } catch (err) { @@ -64,12 +80,12 @@ http.createServer((req, res) => { const urlPath = req.url?.split('?')[0] || '/'; if (urlPath === '/') { - ssrRender(res, 'client'); + ssrRender(res, { mode: 'client', urlPath: '' }); return; } - - if (urlPath === '/a') { - ssrRender(res, 'server'); + const find = indexPath.find((item) => urlPath === `/${item.basename}`); + if (find) { + ssrRender(res, { mode: 'server', urlPath, filename: './' + find.file }); return; } @@ -85,7 +101,7 @@ http.createServer((req, res) => { } catch { } - ssrRender(res); + // ssrRender(res, { mode: 'client', urlPath: '/a' }); }).listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); \ No newline at end of file