diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35b94997..60a498af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -597,6 +597,37 @@ importers: specifier: ^5.3.0 version: 5.4.5 + rsbuild/ssr-express-with-manifest: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@rsbuild/core': + specifier: 1.1.0 + version: 1.1.0 + '@rsbuild/plugin-react': + specifier: 1.0.6 + version: 1.0.6(@rsbuild/core@1.1.0) + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/react': + specifier: ^18.3.12 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.1 + express: + specifier: ^4.19.2 + version: 4.19.2 + typescript: + specifier: ^5.3.0 + version: 5.4.5 + rsbuild/styled-components: dependencies: react: @@ -6114,6 +6145,11 @@ packages: engines: {node: '>=16.7.0'} hasBin: true + '@rsbuild/core@1.1.0': + resolution: {integrity: sha512-SyQlJjWgR1VwLt4nuiY0g6L9INv2koH232TeDZuopvNgbRytskD3kJ8bbGWBBXsQjZjtqBEh5ishqf8CIfF8dQ==} + engines: {node: '>=16.7.0'} + hasBin: true + '@rsbuild/monorepo-utils@0.5.1': resolution: {integrity: sha512-WcdTU0TCCpw1UPn0cIzID3O+/L51G+oWVN3f5kZg4OBKVtpFNptcoDhkG5T9lWCO40r/yyETt0rEbrXdMOzEpg==} deprecated: deprecated @@ -6206,6 +6242,11 @@ packages: peerDependencies: '@rsbuild/core': ^1.0.1-beta.16 + '@rsbuild/plugin-react@1.0.6': + resolution: {integrity: sha512-k2VS7nvNm74DlVQROK+w+Ua1j60n3qSnVFva8zjmj6uakLCxxp85aRwfEHzaVP/YdDLffweypROuQPYvTZ57ew==} + peerDependencies: + '@rsbuild/core': 1.x + '@rsbuild/plugin-react@1.0.7': resolution: {integrity: sha512-t7T/GqDwodusTAnxGpqVRnQ/G+HYh98zk71qIg19WkjVJJGv57AC1Ppx0/6zzbZAbxU60bfK8TeEEXjhXCdSxA==} peerDependencies: @@ -9511,6 +9552,9 @@ packages: core-js@3.38.1: resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==} + core-js@3.39.0: + resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -20384,7 +20428,7 @@ snapshots: chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.4.5) jest-validate: 29.7.0 - jiti: 1.21.0 + jiti: 1.21.6 lodash.get: 4.4.2 transitivePeerDependencies: - typescript @@ -21634,6 +21678,15 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + '@rsbuild/core@1.1.0': + dependencies: + '@rspack/core': 1.1.1(@swc/helpers@0.5.13) + '@rspack/lite-tapable': 1.0.1 + '@swc/helpers': 0.5.13 + core-js: 3.39.0 + optionalDependencies: + fsevents: 2.3.3 + '@rsbuild/monorepo-utils@0.5.1(@swc/helpers@0.5.3)': dependencies: '@rsbuild/shared': 0.5.1(@swc/helpers@0.5.3) @@ -21814,6 +21867,12 @@ snapshots: '@rspack/plugin-react-refresh': 1.0.0(react-refresh@0.14.2) react-refresh: 0.14.2 + '@rsbuild/plugin-react@1.0.6(@rsbuild/core@1.1.0)': + dependencies: + '@rsbuild/core': 1.1.0 + '@rspack/plugin-react-refresh': 1.0.0(react-refresh@0.14.2) + react-refresh: 0.14.2 + '@rsbuild/plugin-react@1.0.7(@rsbuild/core@1.0.19)': dependencies: '@rsbuild/core': 1.0.19 @@ -27132,6 +27191,8 @@ snapshots: core-js@3.38.1: {} + core-js@3.39.0: {} + core-util-is@1.0.3: {} cors@2.8.5: diff --git a/rsbuild/ssr-express-with-manifest/.gitignore b/rsbuild/ssr-express-with-manifest/.gitignore new file mode 100644 index 00000000..38d7344c --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/.gitignore @@ -0,0 +1,13 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea diff --git a/rsbuild/ssr-express-with-manifest/README.md b/rsbuild/ssr-express-with-manifest/README.md new file mode 100644 index 00000000..37b1dd33 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/README.md @@ -0,0 +1,29 @@ +# Rsbuild Project + +## Setup + +Install the dependencies: + +```bash +pnpm install +``` + +## Get Started + +Start the dev server: + +```bash +pnpm dev +``` + +Build the app for production: + +```bash +pnpm build +``` + +Preview the production build locally: + +```bash +pnpm preview +``` diff --git a/rsbuild/ssr-express-with-manifest/package.json b/rsbuild/ssr-express-with-manifest/package.json new file mode 100644 index 00000000..38dd3070 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/package.json @@ -0,0 +1,23 @@ +{ + "name": "rsbuild-ssr-express-with-manifest", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "rsbuild build", + "dev": "node ./server.mjs", + "preview": "node ./prod-server.mjs" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@rsbuild/core": "1.1.0", + "@rsbuild/plugin-react": "1.0.6", + "@types/express": "^4.17.21", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "express": "^4.19.2", + "typescript": "^5.3.0" + } +} diff --git a/rsbuild/ssr-express-with-manifest/prod-server.mjs b/rsbuild/ssr-express-with-manifest/prod-server.mjs new file mode 100644 index 00000000..dab16f36 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/prod-server.mjs @@ -0,0 +1,50 @@ +import express from "express"; +import { createRequire } from "node:module"; +import fs from "node:fs"; +import path from "node:path"; + +const require = createRequire(import.meta.url); + +const templateHtml = fs.readFileSync('./template.html', 'utf-8'); + +const serverRender = (_req, res) => { + const remotesPath = path.join(process.cwd(), `./dist/server/index.js`); + + const importedApp = require(remotesPath); + + const markup = importedApp.render(); + + const { entries } = JSON.parse(fs.readFileSync('./dist/manifest.json', 'utf-8')); + + const { js, css } = entries['index'].initial; + + const scriptTags = js.map(file => ``).join('\n'); + const styleTags = css.map(file => ``).join('\n'); + + const html = templateHtml.replace("", markup).replace('', `${scriptTags}\n${styleTags}`); + + res.status(200).set({ "Content-Type": "text/html" }).send(html); +}; + +const port = process.env.PORT || 3000; + +export async function preview() { + const app = express(); + + app.get("/", (req, res, next) => { + try { + serverRender(req, res, next); + } catch (err) { + console.error("SSR render error, downgrade to CSR...\n", err); + next(); + } + }); + + app.use(express.static("dist")); + + app.listen(port, () => { + console.log(`Server started at http://localhost:${port}`); + }); +} + +preview(process.cwd()); diff --git a/rsbuild/ssr-express-with-manifest/rsbuild.config.mjs b/rsbuild/ssr-express-with-manifest/rsbuild.config.mjs new file mode 100644 index 00000000..e25f5352 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/rsbuild.config.mjs @@ -0,0 +1,40 @@ +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; + +export default defineConfig({ + plugins: [pluginReact()], + dev: { + writeToDisk: true + }, + environments: { + web: { + output: { + target: "web", + }, + source: { + entry: { + index: "./src/index", + }, + }, + output: { + manifest: true, + }, + }, + ssr: { + output: { + target: "node", + distPath: { + root: "dist/server", + }, + }, + source: { + entry: { + index: "./src/index.server", + }, + }, + }, + }, + tools: { + htmlPlugin: false + } +}); diff --git a/rsbuild/ssr-express-with-manifest/server.mjs b/rsbuild/ssr-express-with-manifest/server.mjs new file mode 100644 index 00000000..be9f79db --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/server.mjs @@ -0,0 +1,69 @@ +import express from "express"; +import fs from 'node:fs'; +import { createRsbuild, loadConfig, logger } from "@rsbuild/core"; + +const templateHtml = fs.readFileSync('./template.html', 'utf-8'); + +const serverRender = (serverAPI) => async (_req, res) => { + const indexModule = await serverAPI.environments.ssr.loadBundle("index"); + + const markup = indexModule.render(); + + const { entries } = JSON.parse(fs.readFileSync('./dist/manifest.json', 'utf-8')); + + const { js, css } = entries['index'].initial; + + const scriptTags = js.map(file => ``).join('\n'); + const styleTags = css.map(file => ``).join('\n'); + + const html = templateHtml.replace("", markup).replace('', `${scriptTags}\n${styleTags}`); + + res.writeHead(200, { + "Content-Type": "text/html", + }); + res.end(html); +}; + +export async function startDevServer() { + const { content } = await loadConfig({}); + + // Init Rsbuild + const rsbuild = await createRsbuild({ + rsbuildConfig: content, + }); + + const app = express(); + + // Create Rsbuild DevServer instance + const rsbuildServer = await rsbuild.createDevServer(); + + const serverRenderMiddleware = serverRender(rsbuildServer); + + app.get("/", async (req, res, next) => { + try { + await serverRenderMiddleware(req, res, next); + } catch (err) { + logger.error("SSR render error, downgrade to CSR...\n", err); + next(); + } + }); + + // Apply Rsbuild’s built-in middlewares + app.use(rsbuildServer.middlewares); + + const httpServer = app.listen(rsbuildServer.port, () => { + // Notify Rsbuild that the custom server has started + rsbuildServer.afterListen(); + }); + + rsbuildServer.connectWebSocket({ server: httpServer }); + + return { + close: async () => { + await rsbuildServer.close(); + httpServer.close(); + }, + }; +} + +startDevServer(); diff --git a/rsbuild/ssr-express-with-manifest/src/App.css b/rsbuild/ssr-express-with-manifest/src/App.css new file mode 100644 index 00000000..164c0a6a --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/src/App.css @@ -0,0 +1,26 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} + +.content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; +} + +.content h1 { + font-size: 3.6rem; + font-weight: 700; +} + +.content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; +} diff --git a/rsbuild/ssr-express-with-manifest/src/App.tsx b/rsbuild/ssr-express-with-manifest/src/App.tsx new file mode 100644 index 00000000..dff17512 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/src/App.tsx @@ -0,0 +1,12 @@ +import './App.css'; + +const App = () => { + return ( +
+

Rsbuild with React

+

Start building amazing things with Rsbuild.

+
+ ); +}; + +export default App; diff --git a/rsbuild/ssr-express-with-manifest/src/env.d.ts b/rsbuild/ssr-express-with-manifest/src/env.d.ts new file mode 100644 index 00000000..b0ac762b --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/rsbuild/ssr-express-with-manifest/src/index.server.tsx b/rsbuild/ssr-express-with-manifest/src/index.server.tsx new file mode 100644 index 00000000..ba033062 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/src/index.server.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import App from './App'; + +export function render() { + return ReactDOMServer.renderToString( + + + , + ); +} diff --git a/rsbuild/ssr-express-with-manifest/src/index.tsx b/rsbuild/ssr-express-with-manifest/src/index.tsx new file mode 100644 index 00000000..c8b18224 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/src/index.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.hydrateRoot( + document.getElementById('root')!, + + + , +); diff --git a/rsbuild/ssr-express-with-manifest/template.html b/rsbuild/ssr-express-with-manifest/template.html new file mode 100644 index 00000000..8ead1f37 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/template.html @@ -0,0 +1,12 @@ + + + + + + Rsbuild + Express + SSR + + + +
+ + diff --git a/rsbuild/ssr-express-with-manifest/tsconfig.json b/rsbuild/ssr-express-with-manifest/tsconfig.json new file mode 100644 index 00000000..c1a7cb53 --- /dev/null +++ b/rsbuild/ssr-express-with-manifest/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["DOM", "ES2020"], + "module": "ESNext", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "moduleResolution": "bundler" + }, + "include": ["src"] +} diff --git a/rsbuild/ssr-express/server.mjs b/rsbuild/ssr-express/server.mjs index 4e847e6c..39d42c27 100644 --- a/rsbuild/ssr-express/server.mjs +++ b/rsbuild/ssr-express/server.mjs @@ -1,5 +1,5 @@ import express from "express"; -import { createRsbuild, loadConfig } from "@rsbuild/core"; +import { createRsbuild, loadConfig, logger } from "@rsbuild/core"; const serverRender = (serverAPI) => async (_req, res) => { const indexModule = await serverAPI.environments.ssr.loadBundle("index");