From 843c623186661cb120f15676b8fef5a0604988aa Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Wed, 29 May 2024 16:04:41 +0800 Subject: [PATCH 01/16] feat: add ssr basic api --- e2e/cases/server/ssr/package.json | 11 ++++++ e2e/cases/server/ssr/rsbuild.config.ts | 26 ++++++++++++++ e2e/cases/server/ssr/src/App.css | 26 ++++++++++++++ e2e/cases/server/ssr/src/App.tsx | 12 +++++++ e2e/cases/server/ssr/src/env.d.ts | 1 + e2e/cases/server/ssr/src/index.server.tsx | 11 ++++++ e2e/cases/server/ssr/src/index.tsx | 10 ++++++ e2e/cases/server/ssr/template.html | 11 ++++++ packages/core/src/server/devServer.ts | 43 ++++++++++++++++++++++- pnpm-lock.yaml | 6 ++++ 10 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 e2e/cases/server/ssr/package.json create mode 100644 e2e/cases/server/ssr/rsbuild.config.ts create mode 100644 e2e/cases/server/ssr/src/App.css create mode 100644 e2e/cases/server/ssr/src/App.tsx create mode 100644 e2e/cases/server/ssr/src/env.d.ts create mode 100644 e2e/cases/server/ssr/src/index.server.tsx create mode 100644 e2e/cases/server/ssr/src/index.tsx create mode 100644 e2e/cases/server/ssr/template.html diff --git a/e2e/cases/server/ssr/package.json b/e2e/cases/server/ssr/package.json new file mode 100644 index 0000000000..fe4d9fd908 --- /dev/null +++ b/e2e/cases/server/ssr/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "name": "@e2e/custom-server", + "version": "1.0.0", + "scripts": { + "dev": "node ./server.mjs" + }, + "dependencies": { + "polka": "^0.5.2" + } +} diff --git a/e2e/cases/server/ssr/rsbuild.config.ts b/e2e/cases/server/ssr/rsbuild.config.ts new file mode 100644 index 0000000000..e5820ee13c --- /dev/null +++ b/e2e/cases/server/ssr/rsbuild.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; + +export default defineConfig({ + plugins: [pluginReact()], + source: { + entry({ target }) { + if (target === 'web') { + return { + index: './src/index', + }; + } + if (target === 'node') { + return { + index: './src/index.server', + }; + } + }, + }, + html: { + template: './template.html', + }, + output: { + targets: ['web', 'node'], + }, +}); diff --git a/e2e/cases/server/ssr/src/App.css b/e2e/cases/server/ssr/src/App.css new file mode 100644 index 0000000000..164c0a6aa5 --- /dev/null +++ b/e2e/cases/server/ssr/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/e2e/cases/server/ssr/src/App.tsx b/e2e/cases/server/ssr/src/App.tsx new file mode 100644 index 0000000000..dff1751231 --- /dev/null +++ b/e2e/cases/server/ssr/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/e2e/cases/server/ssr/src/env.d.ts b/e2e/cases/server/ssr/src/env.d.ts new file mode 100644 index 0000000000..b0ac762b09 --- /dev/null +++ b/e2e/cases/server/ssr/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/e2e/cases/server/ssr/src/index.server.tsx b/e2e/cases/server/ssr/src/index.server.tsx new file mode 100644 index 0000000000..ba0330624e --- /dev/null +++ b/e2e/cases/server/ssr/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/e2e/cases/server/ssr/src/index.tsx b/e2e/cases/server/ssr/src/index.tsx new file mode 100644 index 0000000000..2b875af736 --- /dev/null +++ b/e2e/cases/server/ssr/src/index.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')!); +root.render( + + + , +); diff --git a/e2e/cases/server/ssr/template.html b/e2e/cases/server/ssr/template.html new file mode 100644 index 0000000000..a8a5e608ff --- /dev/null +++ b/e2e/cases/server/ssr/template.html @@ -0,0 +1,11 @@ + + + + + + Rsbuild + React + TS + + +
+ + diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index 462d92c7d3..9659376843 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -2,11 +2,13 @@ import fs from 'node:fs'; import { type CreateDevMiddlewareReturns, type CreateDevServerOptions, + type MultiStats, type OutputFileSystem, ROOT_DIST_DIR, type RsbuildDevServer, type StartDevServerOptions, type StartServerResult, + type Stats, debug, getNodeEnv, getPublicPathFromCompiler, @@ -28,6 +30,7 @@ import { import { createHttpServer } from './httpServer'; import { notFoundMiddleware } from './middlewares'; import { onBeforeRestartServer } from './restart'; +import { type ServerUtils, getTransformedHtml, ssrLoadModule } from './ssr'; import { setupWatchFiles } from './watchFiles'; export async function createDevServer< @@ -143,13 +146,15 @@ export async function createDevServer< compileMiddlewareAPI, }); + const distPath = rsbuildConfig.output?.distPath?.root || ROOT_DIST_DIR; + const devMiddlewares = await getMiddlewares({ pwd: options.context.rootPath, compileMiddlewareAPI, dev: devConfig, server: serverConfig, output: { - distPath: rsbuildConfig.output?.distPath?.root || ROOT_DIST_DIR, + distPath, }, outputFileSystem, }); @@ -164,6 +169,34 @@ export async function createDevServer< } } + const serverUtils: ServerUtils = { + readFile: (fileName: string) => + new Promise((resolve, reject) => { + outputFileSystem.readFile(fileName, (err, data) => { + if (err) { + return reject(err); + } + + resolve(data!.toString()); + }); + }), + distPath, + getHTMLPaths: options.context.pluginAPI!.getHTMLPaths, + }; + + let _stats: Stats | MultiStats; + + const waitFirstCompileDone = new Promise((resolve) => { + options.context.hooks.onDevCompileDone.tap(({ stats, isFirstCompile }) => { + _stats = stats; + + if (!isFirstCompile) { + return; + } + resolve(); + }); + }); + const server = { port, middlewares, @@ -218,6 +251,14 @@ export async function createDevServer< routes, }); }, + ssrLoadModule: async (entryName: string) => { + await waitFirstCompileDone; + return ssrLoadModule(_stats, entryName, serverUtils); + }, + getTransformedHtml: async (entryName: string) => { + await waitFirstCompileDone; + return getTransformedHtml(entryName, serverUtils); + }, onHTTPUpgrade: devMiddlewares.onUpgrade, close: async () => { await options.context.hooks.onCloseDevServer.call(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72eb9eaf76..e52c1342fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -254,6 +254,12 @@ importers: specifier: ^0.5.2 version: 0.5.2 + e2e/cases/server/ssr: + dependencies: + polka: + specifier: ^0.5.2 + version: 0.5.2 + e2e/cases/source-build/app: dependencies: '@e2e/source-build-components': From f211d2c28704f5f9a4414e2c41d6d7c905beec18 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Mon, 3 Jun 2024 19:13:59 +0800 Subject: [PATCH 02/16] feat: add server bundle runner --- e2e/cases/server/ssr/index.test.ts | 30 +++++ e2e/cases/server/ssr/package.json | 3 +- e2e/cases/server/ssr/rsbuild.config.ts | 24 ++++ e2e/cases/server/ssr/scripts/index.mjs | 3 + e2e/cases/server/ssr/scripts/server.mjs | 69 ++++++++++ e2e/cases/server/ssr/src/index.server.tsx | 3 + e2e/cases/server/ssr/src/test.ts | 3 + e2e/package.json | 2 +- packages/core/src/server/devServer.ts | 26 ++-- packages/core/src/server/runner/basic.ts | 120 +++++++++++++++++ packages/core/src/server/runner/cjs.ts | 122 ++++++++++++++++++ packages/core/src/server/runner/esm.ts | 108 ++++++++++++++++ packages/core/src/server/runner/index.ts | 57 ++++++++ .../core/src/server/runner/legacy/asModule.js | 28 ++++ packages/core/src/server/runner/type.ts | 51 ++++++++ packages/core/src/server/ssr.ts | 80 ++++++++++++ 16 files changed, 715 insertions(+), 14 deletions(-) create mode 100644 e2e/cases/server/ssr/index.test.ts create mode 100644 e2e/cases/server/ssr/scripts/index.mjs create mode 100644 e2e/cases/server/ssr/scripts/server.mjs create mode 100644 e2e/cases/server/ssr/src/test.ts create mode 100644 packages/core/src/server/runner/basic.ts create mode 100644 packages/core/src/server/runner/cjs.ts create mode 100644 packages/core/src/server/runner/esm.ts create mode 100644 packages/core/src/server/runner/index.ts create mode 100644 packages/core/src/server/runner/legacy/asModule.js create mode 100644 packages/core/src/server/runner/type.ts create mode 100644 packages/core/src/server/ssr.ts diff --git a/e2e/cases/server/ssr/index.test.ts b/e2e/cases/server/ssr/index.test.ts new file mode 100644 index 0000000000..323c327b72 --- /dev/null +++ b/e2e/cases/server/ssr/index.test.ts @@ -0,0 +1,30 @@ +import { rspackOnlyTest } from '@e2e/helper'; +import { expect } from '@playwright/test'; +import { startDevServer } from './scripts/server.mjs'; + +rspackOnlyTest('server ssr', async ({ page }) => { + const { config, close } = await startDevServer(__dirname); + + const url1 = new URL(`http://localhost:${config.port}`); + + const res = await page.goto(url1.href); + + expect(await res?.text()).toMatch(/Rsbuild with React/); + + await close(); +}); + +rspackOnlyTest('server ssr with esm target', async ({ page }) => { + process.env.TEST_ESM_LIBRARY = '1'; + const { config, close } = await startDevServer(__dirname); + + const url1 = new URL(`http://localhost:${config.port}`); + + const res = await page.goto(url1.href); + + expect(await res?.text()).toMatch(/Rsbuild with React/); + + await close(); + + delete process.env.TEST_ESM_LIBRARY; +}); diff --git a/e2e/cases/server/ssr/package.json b/e2e/cases/server/ssr/package.json index fe4d9fd908..ac138e287b 100644 --- a/e2e/cases/server/ssr/package.json +++ b/e2e/cases/server/ssr/package.json @@ -3,7 +3,8 @@ "name": "@e2e/custom-server", "version": "1.0.0", "scripts": { - "dev": "node ./server.mjs" + "dev": "node ./scripts/index.mjs", + "dev:esm": "TEST_ESM_LIBRARY=true node --experimental-vm-modules ./scripts/index.mjs" }, "dependencies": { "polka": "^0.5.2" diff --git a/e2e/cases/server/ssr/rsbuild.config.ts b/e2e/cases/server/ssr/rsbuild.config.ts index e5820ee13c..629c6c3afa 100644 --- a/e2e/cases/server/ssr/rsbuild.config.ts +++ b/e2e/cases/server/ssr/rsbuild.config.ts @@ -23,4 +23,28 @@ export default defineConfig({ output: { targets: ['web', 'node'], }, + tools: { + rspack: (config, { isServer }) => { + if (process.env.TEST_ESM_LIBRARY && isServer) { + return { + ...config, + experiments: { + ...config.experiments, + outputModule: true, + }, + output: { + ...config.output, + filename: '[name].mjs', + chunkFilename: '[name].mjs', + chunkFormat: 'module', + chunkLoading: 'import', + library: { + type: 'module', + }, + }, + }; + } + return config; + }, + }, }); diff --git a/e2e/cases/server/ssr/scripts/index.mjs b/e2e/cases/server/ssr/scripts/index.mjs new file mode 100644 index 0000000000..35cdecbc3e --- /dev/null +++ b/e2e/cases/server/ssr/scripts/index.mjs @@ -0,0 +1,3 @@ +import { startDevServer } from './server.mjs'; + +startDevServer(process.cwd()); diff --git a/e2e/cases/server/ssr/scripts/server.mjs b/e2e/cases/server/ssr/scripts/server.mjs new file mode 100644 index 0000000000..f5399ef511 --- /dev/null +++ b/e2e/cases/server/ssr/scripts/server.mjs @@ -0,0 +1,69 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createRsbuild, logger } from '@rsbuild/core'; +import { loadConfig } from '@rsbuild/core'; +import polka from 'polka'; + +export const serverRender = (rsbuildServer) => async (_req, res, _next) => { + const indexModule = await rsbuildServer.ssrLoadModule('index'); + + const markup = indexModule.render(); + + const template = await rsbuildServer.getTransformedHtml('index'); + + const html = template.replace('', markup); + + res.writeHead(200, { + 'Content-Type': 'text/html', + }); + res.end(html); +}; + +export async function startDevServer(fixtures, overridsConfig) { + const { content } = await loadConfig({ + cwd: fixtures, + }); + + const rsbuild = await createRsbuild({ + cwd: fixtures, + rsbuildConfig: content, + }); + + const app = polka(); + + const rsbuildServer = await rsbuild.createDevServer(); + + const serverRenderMiddleware = serverRender(rsbuildServer); + + app.use('/', async (req, res, next) => { + if (req.method === 'GET' && req.url === '/') { + try { + await serverRenderMiddleware(req, res, next); + } catch (err) { + logger.error('ssr render error, downgrade to csr...\n', err); + next(); + } + } else { + next(); + } + }); + + app.use(rsbuildServer.middlewares); + + const { port } = rsbuildServer; + + const { server } = app.listen({ port }, async () => { + await rsbuildServer.afterListen(); + }); + + // subscribe the server's http upgrade event to handle WebSocket upgrade + server.on('upgrade', rsbuildServer.onHTTPUpgrade); + + return { + config: { port }, + close: async () => { + await rsbuildServer.close(); + server.close(); + }, + }; +} diff --git a/e2e/cases/server/ssr/src/index.server.tsx b/e2e/cases/server/ssr/src/index.server.tsx index ba0330624e..9c0b1b1aad 100644 --- a/e2e/cases/server/ssr/src/index.server.tsx +++ b/e2e/cases/server/ssr/src/index.server.tsx @@ -2,6 +2,9 @@ import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from './App'; +// test dynamic import +import('./test'); + export function render() { return ReactDOMServer.renderToString( diff --git a/e2e/cases/server/ssr/src/test.ts b/e2e/cases/server/ssr/src/test.ts new file mode 100644 index 0000000000..7ffafc6670 --- /dev/null +++ b/e2e/cases/server/ssr/src/test.ts @@ -0,0 +1,3 @@ +export const getA = () => { + return 'A'; +}; diff --git a/e2e/package.json b/e2e/package.json index 1dcc2b28b0..8fb981a55c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "scripts": { "test": "pnpm test:rspack && pnpm test:webpack", - "test:rspack": "playwright test", + "test:rspack": "NODE_OPTIONS=--experimental-vm-modules playwright test", "test:webpack": "cross-env PROVIDE_TYPE=webpack playwright test" }, "dependencies": { diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index 5e129f0757..9ec51a4b4b 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -1,9 +1,12 @@ import fs from 'node:fs'; +import { isAbsolute, join } from 'node:path'; import { type CreateDevServerOptions, + type MultiStats, ROOT_DIST_DIR, type Rspack, type StartDevServerOptions, + type Stats, debug, getNodeEnv, getPublicPathFromCompiler, @@ -194,9 +197,10 @@ export async function createDevServer< }); const distPath = config.output.distPath.root || ROOT_DIST_DIR; + const pwd = options.context.rootPath; const devMiddlewares = await getMiddlewares({ - pwd: options.context.rootPath, + pwd, compileMiddlewareAPI, dev: devConfig, server: serverConfig, @@ -218,17 +222,15 @@ export async function createDevServer< } const serverUtils: ServerUtils = { - readFile: (fileName: string) => - new Promise((resolve, reject) => { - outputFileSystem.readFile(fileName, (err, data) => { - if (err) { - return reject(err); - } - - resolve(data!.toString()); - }); - }), - distPath, + readFileSync: (fileName: string) => { + if ('readFileSync' in outputFileSystem) { + // bundle require needs a synchronous method, although readFileSync is not within the outputFileSystem type definition, but nodejs fs API implemented. + // @ts-expect-error + return outputFileSystem.readFileSync(fileName, 'utf-8'); + } + return fs.readFileSync(fileName, 'utf-8'); + }, + distPath: isAbsolute(distPath) ? distPath : join(pwd, distPath), getHTMLPaths: options.context.pluginAPI!.getHTMLPaths, }; diff --git a/packages/core/src/server/runner/basic.ts b/packages/core/src/server/runner/basic.ts new file mode 100644 index 0000000000..35f7d542d7 --- /dev/null +++ b/packages/core/src/server/runner/basic.ts @@ -0,0 +1,120 @@ +import path from 'node:path'; + +import type { CompilerOptions, Runner } from './type'; + +import type { + BasicGlobalContext, + BasicModuleScope, + BasicRunnerFile, + ModuleObject, + RunnerRequirer, +} from './type'; + +const isRelativePath = (p: string) => /^\.\.?\//.test(p); +const getSubPath = (p: string) => { + const lastSlash = p.lastIndexOf('/'); + let firstSlash = p.indexOf('/'); + + if (lastSlash !== -1 && firstSlash !== lastSlash) { + if (firstSlash !== -1) { + let next = p.indexOf('/', firstSlash + 1); + let dir = p.slice(firstSlash + 1, next); + + while (dir === '.') { + firstSlash = next; + next = p.indexOf('/', firstSlash + 1); + dir = p.slice(firstSlash + 1, next); + } + } + + return p.slice(firstSlash + 1, lastSlash + 1); + } + return ''; +}; + +export interface IBasicRunnerOptions { + name: string; + runInNewContext?: boolean; + readFileSync: (path: string) => string; + dist: string; + compilerOptions: CompilerOptions; +} + +export abstract class BasicRunner implements Runner { + protected globalContext: BasicGlobalContext | null = null; + protected baseModuleScope: BasicModuleScope | null = null; + protected requirers: Map = new Map(); + constructor(protected _options: IBasicRunnerOptions) {} + + run(file: string): Promise { + if (!this.globalContext) { + this.globalContext = this.createGlobalContext(); + } + this.baseModuleScope = this.createBaseModuleScope(); + this.createRunner(); + const res = this.getRequire()( + this._options.dist, + file.startsWith('./') ? file : `./${file}`, + ); + if (typeof res === 'object' && 'then' in res) { + return res as Promise; + } + return Promise.resolve(res); + } + + getRequire(): RunnerRequirer { + const entryRequire = this.requirers.get('entry')!; + return (currentDirectory, modulePath, context = {}) => { + const p = Array.isArray(modulePath) + ? modulePath + : modulePath.split('?')[0]!; + return entryRequire(currentDirectory, p, context); + }; + } + + protected abstract createGlobalContext(): BasicGlobalContext; + protected abstract createBaseModuleScope(): BasicModuleScope; + protected abstract createModuleScope( + requireFn: RunnerRequirer, + m: ModuleObject, + file: BasicRunnerFile, + ): BasicModuleScope; + + protected getFile( + modulePath: string[] | string, + currentDirectory: string, + ): BasicRunnerFile | null { + if (Array.isArray(modulePath)) { + return { + path: path.join(currentDirectory, '.array-require.js'), + content: `module.exports = (${modulePath + .map((arg) => { + return `require(${JSON.stringify(`./${arg}`)})`; + }) + .join(', ')});`, + subPath: '', + }; + } + if (isRelativePath(modulePath)) { + const p = path.join(currentDirectory, modulePath); + return { + path: p, + content: this._options.readFileSync(p), + subPath: getSubPath(modulePath), + }; + } + return null; + } + + protected preExecute(_code: string, _file: BasicRunnerFile) {} + protected postExecute(_m: Record, _file: BasicRunnerFile) {} + + protected createRunner() { + this.requirers.set( + 'entry', + (_currentDirectory, _modulePath, _context = {}) => { + throw new Error('Not implement'); + }, + ); + } +} diff --git a/packages/core/src/server/runner/cjs.ts b/packages/core/src/server/runner/cjs.ts new file mode 100644 index 0000000000..03dde9a0cd --- /dev/null +++ b/packages/core/src/server/runner/cjs.ts @@ -0,0 +1,122 @@ +import path from 'node:path'; +import vm from 'node:vm'; + +import { BasicRunner } from './basic'; +import type { + BasicGlobalContext, + BasicModuleScope, + BasicRunnerFile, + ModuleObject, + RunnerRequirer, +} from './type'; + +const define = (...args: unknown[]) => { + const factory = args.pop() as () => void; + factory(); +}; + +export class CommonJsRunner extends BasicRunner { + protected createGlobalContext(): BasicGlobalContext { + return { + console: console, + setTimeout: (( + cb: (...args: any[]) => void, + ms: number | undefined, + ...args: any + ) => { + const timeout = setTimeout(cb, ms, ...args); + timeout.unref(); + return timeout; + }) as typeof setTimeout, + clearTimeout: clearTimeout, + }; + } + + protected createBaseModuleScope(): BasicModuleScope { + const baseModuleScope: BasicModuleScope = { + console: this.globalContext!.console, + setTimeout: this.globalContext!.setTimeout, + clearTimeout: this.globalContext!.clearTimeout, + nsObj: (m: Record) => { + Object.defineProperty(m, Symbol.toStringTag, { + value: 'Module', + }); + return m; + }, + }; + return baseModuleScope; + } + + protected createModuleScope( + requireFn: RunnerRequirer, + m: ModuleObject, + file: BasicRunnerFile, + ): BasicModuleScope { + return { + ...this.baseModuleScope!, + require: requireFn.bind(null, path.dirname(file.path)), + module: m, + exports: m.exports, + __dirname: path.dirname(file.path), + __filename: file.path, + define, + }; + } + + protected createRunner() { + this.requirers.set('miss', this.createMissRequirer()); + this.requirers.set('entry', this.createCjsRequirer()); + } + + protected createMissRequirer(): RunnerRequirer { + return (_currentDirectory, modulePath, _context = {}) => { + const modulePathStr = modulePath as string; + return require( + modulePathStr.startsWith('node:') + ? modulePathStr.slice(5) + : modulePathStr, + ); + }; + } + + protected createCjsRequirer(): RunnerRequirer { + const requireCache = Object.create(null); + + return (currentDirectory, modulePath, context = {}) => { + const file = context.file || this.getFile(modulePath, currentDirectory); + if (!file) { + return this.requirers.get('miss')!(currentDirectory, modulePath); + } + + if (file.path in requireCache) { + return requireCache[file.path].exports; + } + + const m = { + exports: {}, + }; + requireCache[file.path] = m; + const currentModuleScope = this.createModuleScope( + this.getRequire(), + m, + file, + ); + + const args = Object.keys(currentModuleScope); + const argValues = args.map((arg) => currentModuleScope[arg]); + const code = `(function(${args.join(', ')}) { + ${file.content} + })`; + + this.preExecute(code, file); + const fn = this._options.runInNewContext + ? vm.runInNewContext(code, this.globalContext!, file.path) + : vm.runInThisContext(code, file.path); + + fn.call(m.exports, ...argValues); + + this.postExecute(m, file); + return m.exports; + }; + } +} diff --git a/packages/core/src/server/runner/esm.ts b/packages/core/src/server/runner/esm.ts new file mode 100644 index 0000000000..8d28ef5f76 --- /dev/null +++ b/packages/core/src/server/runner/esm.ts @@ -0,0 +1,108 @@ +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import vm, { type SourceTextModule } from 'node:vm'; + +import asModule from './legacy/asModule'; + +import { CommonJsRunner } from './cjs'; +import { EEsmMode, type RunnerRequirer } from './type'; + +export class EsmRunner extends CommonJsRunner { + protected createRunner() { + super.createRunner(); + this.requirers.set('cjs', this.getRequire()); + this.requirers.set('esm', this.createEsmRequirer()); + this.requirers.set('entry', (currentDirectory, modulePath, context) => { + const file = this.getFile(modulePath, currentDirectory); + if (!file) { + return this.requirers.get('miss')!(currentDirectory, modulePath); + } + + if ( + file.path.endsWith('.mjs') && + this._options.compilerOptions.experiments?.outputModule + ) { + return this.requirers.get('esm')!(currentDirectory, modulePath, { + ...context, + file, + }); + } + return this.requirers.get('cjs')!(currentDirectory, modulePath, { + ...context, + file, + }); + }); + } + + protected createEsmRequirer(): RunnerRequirer { + const esmContext = vm.createContext(this.baseModuleScope!, { + name: 'context for esm', + }); + const esmCache = new Map(); + const esmIdentifier = this._options.name; + return (currentDirectory, modulePath, context = {}) => { + if (!vm.SourceTextModule) { + throw new Error( + "Running esm bundle needs add Node.js option '--experimental-vm-modules'.", + ); + } + const _require = this.getRequire(); + const file = context.file || this.getFile(modulePath, currentDirectory); + if (!file) { + return this.requirers.get('miss')!(currentDirectory, modulePath); + } + + let esm = esmCache.get(file.path); + if (!esm) { + esm = new vm.SourceTextModule(file.content, { + identifier: `${esmIdentifier}-${file.path}`, + // no attribute + url: `${pathToFileURL(file.path).href}?${esmIdentifier}`, + context: esmContext, + initializeImportMeta: (meta: { url: string }, _: any) => { + meta.url = pathToFileURL(file!.path).href; + }, + importModuleDynamically: async ( + specifier: any, + module: { context: any }, + ) => { + const result = await _require(path.dirname(file!.path), specifier, { + esmMode: EEsmMode.Evaluated, + }); + return await asModule(result, module.context); + }, + } as any); + esmCache.set(file.path, esm); + } + if (context.esmMode === EEsmMode.Unlinked) return esm; + return (async () => { + await esm.link(async (specifier, referencingModule) => { + return await asModule( + await _require( + path.dirname( + referencingModule.identifier + ? referencingModule.identifier.slice(esmIdentifier.length + 1) + : fileURLToPath((referencingModule as any).url), + ), + specifier, + { + esmMode: EEsmMode.Unlinked, + }, + ), + referencingModule.context, + true, + ); + }); + if ((esm as any).instantiate) (esm as any).instantiate(); + await esm.evaluate(); + if (context.esmMode === EEsmMode.Evaluated) { + return esm; + } + const ns = esm.namespace as { + default: unknown; + }; + return ns.default && ns.default instanceof Promise ? ns.default : ns; + })(); + }; + } +} diff --git a/packages/core/src/server/runner/index.ts b/packages/core/src/server/runner/index.ts new file mode 100644 index 0000000000..09d0b51edf --- /dev/null +++ b/packages/core/src/server/runner/index.ts @@ -0,0 +1,57 @@ +import { EsmRunner } from './esm'; +/** + * The following code is modified based on @rspack/test-tools/runner + */ +import type { CompilerOptions, Runner, RunnerFactory } from './type'; + +export class BasicRunnerFactory implements RunnerFactory { + constructor(protected name: string) {} + + create( + compilerOptions: CompilerOptions, + dist: string, + readFileSync: (path: string) => string, + ): Runner { + const runner = this.createRunner(compilerOptions, dist, readFileSync); + return runner; + } + + protected createRunner( + compilerOptions: CompilerOptions, + dist: string, + readFileSync: (path: string) => string, + ): Runner { + const runnerOptions = { + name: this.name, + dist, + compilerOptions, + readFileSync, + }; + if ( + compilerOptions.target === 'web' || + compilerOptions.target === 'webworker' + ) { + throw new Error( + `not support run ${compilerOptions.target} resource in rsbuild server`, + ); + } + return new EsmRunner(runnerOptions); + } +} + +export const run = async ( + bundlePath: string, + outputPath: string, + compilerOptions: CompilerOptions, + readFileSync: (path: string) => string, +) => { + const runnerFactory = new BasicRunnerFactory(bundlePath); + const runner = runnerFactory.create( + compilerOptions, + outputPath, + readFileSync, + ); + const mod = runner.run(bundlePath); + + return mod; +}; diff --git a/packages/core/src/server/runner/legacy/asModule.js b/packages/core/src/server/runner/legacy/asModule.js new file mode 100644 index 0000000000..0408b6de6c --- /dev/null +++ b/packages/core/src/server/runner/legacy/asModule.js @@ -0,0 +1,28 @@ +const vm = require('node:vm'); + +const SYNTHETIC_MODULES_STORE = '__SYNTHETIC_MODULES_STORE'; + +module.exports = async (something, context, unlinked) => { + if (something instanceof vm.Module) { + return something; + } + context[SYNTHETIC_MODULES_STORE] = context[SYNTHETIC_MODULES_STORE] || []; + const i = context[SYNTHETIC_MODULES_STORE].length; + context[SYNTHETIC_MODULES_STORE].push(something); + const code = [...new Set(['default', ...Object.keys(something)])] + .map( + (name) => + `const _${name} = ${SYNTHETIC_MODULES_STORE}[${i}]${ + name === 'default' ? '' : `[${JSON.stringify(name)}]` + }; export { _${name} as ${name}};`, + ) + .join('\n'); + const m = new vm.SourceTextModule(code, { + context, + }); + if (unlinked) return m; + await m.link(() => {}); + if (m.instantiate) m.instantiate(); + await m.evaluate(); + return m; +}; diff --git a/packages/core/src/server/runner/type.ts b/packages/core/src/server/runner/type.ts new file mode 100644 index 0000000000..e64232c71e --- /dev/null +++ b/packages/core/src/server/runner/type.ts @@ -0,0 +1,51 @@ +import type { RspackOptionsNormalized } from '@rspack/core'; + +export type RunnerRequirer = ( + currentDirectory: string, + modulePath: string[] | string, + context?: { + file?: BasicRunnerFile; + esmMode?: EEsmMode; + }, +) => Record | Promise>; + +export type BasicRunnerFile = { + path: string; + content: string; + subPath: string; +}; + +export enum EEsmMode { + Unknown = 0, + Evaluated = 1, + Unlinked = 2, +} + +export interface BasicModuleScope { + console: Console; + [key: string]: any; +} + +export interface BasicGlobalContext { + console: Console; + setTimeout: typeof setTimeout; + clearTimeout: typeof clearTimeout; + [key: string]: any; +} + +export type ModuleObject = { exports: unknown }; + +export type CompilerOptions = RspackOptionsNormalized; + +export interface Runner { + run(file: string): Promise; + getRequire(): RunnerRequirer; +} + +export interface RunnerFactory { + create( + compilerOptions: CompilerOptions, + dist: string, + readFileSync: (fileName: string) => string, + ): Runner; +} diff --git a/packages/core/src/server/ssr.ts b/packages/core/src/server/ssr.ts new file mode 100644 index 0000000000..a6d2c6ca3e --- /dev/null +++ b/packages/core/src/server/ssr.ts @@ -0,0 +1,80 @@ +import { join } from 'node:path'; +import type { MultiStats, Stats } from '@rsbuild/shared'; +import { run } from './runner'; + +export type ServerUtils = { + readFileSync: (fileName: string) => string; + getHTMLPaths: () => Record; + distPath: string; +}; + +export const ssrLoadModule = async ( + stats: Stats | MultiStats, + entryName: string, + utils: ServerUtils, +) => { + // TODO:need a unique and accurate ssr environment identifier. + // get ssr bundle from server stats + const serverStats = + 'stats' in stats + ? stats.stats.find((s) => s.compilation.options.target === 'node')! + : stats; + + const { chunks, entrypoints, outputPath } = serverStats.toJson({ + all: false, + chunks: true, + entrypoints: true, + outputPath: true, + }); + + if (!entrypoints?.[entryName]) { + throw new Error(`can't find ssr entry(${entryName})`); + } + + const { chunks: entryChunks } = entrypoints[entryName]; + + const files = entryChunks.reduce((prev, curr) => { + const c = curr ? chunks?.find((c) => c.names.includes(curr)) : undefined; + + return c + ? prev.concat(c?.files.filter((file) => !file.endsWith('.css'))) + : prev; + }, []); + + if (files.length === 0) { + throw new Error(`can't get ssr bundle by entryName(${entryName})`); + } + + if (files.length > 1) { + throw new Error( + `only support load single ssr bundle, but got ${files.length}: ${files.join(',')}`, + ); + } + + const res = await run( + files[0], + outputPath!, + serverStats.compilation.options, + utils.readFileSync, + ); + + return res; +}; + +export const getTransformedHtml = async ( + entryName: string, + utils: ServerUtils, +) => { + const htmlPaths = utils.getHTMLPaths(); + const htmlPath = htmlPaths[entryName]; + + if (!htmlPath) { + throw new Error(`can't get html file by entryName(${entryName})`); + } + + const fileName = join(utils.distPath, htmlPath); + + const fileContent = utils.readFileSync(fileName); + + return fileContent; +}; From f684f3fe9d968ab70963ebd367a224dcf3144182 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Thu, 6 Jun 2024 15:34:04 +0800 Subject: [PATCH 03/16] feat(server): support load ssr module --- e2e/cases/server/ssr/index.test.ts | 19 +++++++++++++++++-- e2e/cases/server/ssr/rsbuild.config.ts | 22 ++++++++++++++++++++++ packages/core/src/server/devServer.ts | 13 +++++++++++++ packages/core/src/server/ssr.ts | 9 +++++++-- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/e2e/cases/server/ssr/index.test.ts b/e2e/cases/server/ssr/index.test.ts index 323c327b72..2bce86afe2 100644 --- a/e2e/cases/server/ssr/index.test.ts +++ b/e2e/cases/server/ssr/index.test.ts @@ -2,7 +2,7 @@ import { rspackOnlyTest } from '@e2e/helper'; import { expect } from '@playwright/test'; import { startDevServer } from './scripts/server.mjs'; -rspackOnlyTest('server ssr', async ({ page }) => { +rspackOnlyTest('support ssr', async ({ page }) => { const { config, close } = await startDevServer(__dirname); const url1 = new URL(`http://localhost:${config.port}`); @@ -14,7 +14,7 @@ rspackOnlyTest('server ssr', async ({ page }) => { await close(); }); -rspackOnlyTest('server ssr with esm target', async ({ page }) => { +rspackOnlyTest('support ssr with esm target', async ({ page }) => { process.env.TEST_ESM_LIBRARY = '1'; const { config, close } = await startDevServer(__dirname); @@ -28,3 +28,18 @@ rspackOnlyTest('server ssr with esm target', async ({ page }) => { delete process.env.TEST_ESM_LIBRARY; }); + +rspackOnlyTest('support ssr with split chunk', async ({ page }) => { + process.env.TEST_SPLIT_CHUNK = '1'; + const { config, close } = await startDevServer(__dirname); + + const url1 = new URL(`http://localhost:${config.port}`); + + const res = await page.goto(url1.href); + + expect(await res?.text()).toMatch(/Rsbuild with React/); + + await close(); + + delete process.env.TEST_SPLIT_CHUNK; +}); diff --git a/e2e/cases/server/ssr/rsbuild.config.ts b/e2e/cases/server/ssr/rsbuild.config.ts index 629c6c3afa..835be015e7 100644 --- a/e2e/cases/server/ssr/rsbuild.config.ts +++ b/e2e/cases/server/ssr/rsbuild.config.ts @@ -44,6 +44,28 @@ export default defineConfig({ }, }; } + + if (process.env.TEST_SPLIT_CHUNK && isServer) { + return { + ...config, + optimization: { + runtimeChunk: true, + splitChunks: { + chunks: 'all', + enforceSizeThreshold: 50000, + minSize: 0, + cacheGroups: { + 'lib-react': { + test: /[\\/]node_modules[\\/](react|react-dom|scheduler|react-refresh|@rspack[\\/]plugin-react-refresh)[\\/]/, + priority: 0, + name: 'lib-react', + reuseExistingChunk: true, + }, + }, + }, + }, + }; + } return config; }, }, diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index 586185bd34..e2ca30153e 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -77,6 +77,19 @@ export type RsbuildDevServer = { * Close the Rsbuild server. */ close: () => Promise; + + /** + * Load and execute SSR module in Server. + * + * @param entryName - relate to rsbuild source.entry + * @returns the return of server-entry. + */ + ssrLoadModule: (entryName: string) => Promise; + + /** + * Get the compiled HTML template. + */ + getTransformedHtml: (entryName: string) => Promise; }; export async function createDevServer< diff --git a/packages/core/src/server/ssr.ts b/packages/core/src/server/ssr.ts index a6d2c6ca3e..e6df0a0552 100644 --- a/packages/core/src/server/ssr.ts +++ b/packages/core/src/server/ssr.ts @@ -33,8 +33,11 @@ export const ssrLoadModule = async ( const { chunks: entryChunks } = entrypoints[entryName]; + // find main entryChunk from chunks const files = entryChunks.reduce((prev, curr) => { - const c = curr ? chunks?.find((c) => c.names.includes(curr)) : undefined; + const c = curr + ? chunks?.find((c) => c.names.includes(curr) && c.entry) + : undefined; return c ? prev.concat(c?.files.filter((file) => !file.endsWith('.css'))) @@ -45,9 +48,10 @@ export const ssrLoadModule = async ( throw new Error(`can't get ssr bundle by entryName(${entryName})`); } + // An entrypoint should have only one entryChunk, but there may be some boundary cases if (files.length > 1) { throw new Error( - `only support load single ssr bundle, but got ${files.length}: ${files.join(',')}`, + `only support load single ssr bundle entry, but got ${files.length}: ${files.join(',')}`, ); } @@ -65,6 +69,7 @@ export const getTransformedHtml = async ( entryName: string, utils: ServerUtils, ) => { + // TODO: should get html resource from stats? const htmlPaths = utils.getHTMLPaths(); const htmlPath = htmlPaths[entryName]; From 366fa7816ea54eb0348d97287534d2342cebbc53 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Thu, 6 Jun 2024 15:45:58 +0800 Subject: [PATCH 04/16] fix: update type --- packages/core/src/server/devServer.ts | 4 ++-- packages/core/src/server/ssr.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index e2ca30153e..2a19d30914 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -314,9 +314,9 @@ export async function createDevServer< routes, }); }, - ssrLoadModule: async (entryName: string) => { + ssrLoadModule: async (entryName: string) => { await waitFirstCompileDone; - return ssrLoadModule(_stats, entryName, serverUtils); + return ssrLoadModule(_stats, entryName, serverUtils); }, getTransformedHtml: async (entryName: string) => { await waitFirstCompileDone; diff --git a/packages/core/src/server/ssr.ts b/packages/core/src/server/ssr.ts index e6df0a0552..d92ac65875 100644 --- a/packages/core/src/server/ssr.ts +++ b/packages/core/src/server/ssr.ts @@ -8,11 +8,11 @@ export type ServerUtils = { distPath: string; }; -export const ssrLoadModule = async ( +export const ssrLoadModule = async ( stats: Stats | MultiStats, entryName: string, utils: ServerUtils, -) => { +): Promise => { // TODO:need a unique and accurate ssr environment identifier. // get ssr bundle from server stats const serverStats = @@ -62,7 +62,7 @@ export const ssrLoadModule = async ( utils.readFileSync, ); - return res; + return res as T; }; export const getTransformedHtml = async ( From d996ae9ee7e711b3ebc8f71682edc62847bbb238 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Thu, 6 Jun 2024 15:55:56 +0800 Subject: [PATCH 05/16] fix: add cross-env --- e2e/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/package.json b/e2e/package.json index d2f7b16f5d..a8edf48c44 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "scripts": { "test": "pnpm test:rspack && pnpm test:webpack", - "test:rspack": "NODE_OPTIONS=--experimental-vm-modules playwright test", + "test:rspack": "cross-env NODE_OPTIONS=--experimental-vm-modules playwright test", "test:webpack": "cross-env PROVIDE_TYPE=webpack playwright test" }, "dependencies": { From 370a270122bf414d966b04a5343ee598ba9a9688 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Thu, 6 Jun 2024 17:22:16 +0800 Subject: [PATCH 06/16] fix: debug windows test --- e2e/cases/server/ssr/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/cases/server/ssr/index.test.ts b/e2e/cases/server/ssr/index.test.ts index 2bce86afe2..908b9eb7da 100644 --- a/e2e/cases/server/ssr/index.test.ts +++ b/e2e/cases/server/ssr/index.test.ts @@ -2,7 +2,7 @@ import { rspackOnlyTest } from '@e2e/helper'; import { expect } from '@playwright/test'; import { startDevServer } from './scripts/server.mjs'; -rspackOnlyTest('support ssr', async ({ page }) => { +rspackOnlyTest.only('support ssr', async ({ page }) => { const { config, close } = await startDevServer(__dirname); const url1 = new URL(`http://localhost:${config.port}`); From e7c71268ab117d32e4f43cf3b72706b53444ade3 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Thu, 6 Jun 2024 17:45:28 +0800 Subject: [PATCH 07/16] fix: update test case --- e2e/cases/server/ssr/index.test.ts | 23 ++++++++++++++++------- e2e/cases/server/ssr/scripts/server.mjs | 8 +++++--- e2e/scripts/shared.ts | 2 +- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/e2e/cases/server/ssr/index.test.ts b/e2e/cases/server/ssr/index.test.ts index 908b9eb7da..badba37051 100644 --- a/e2e/cases/server/ssr/index.test.ts +++ b/e2e/cases/server/ssr/index.test.ts @@ -1,9 +1,12 @@ -import { rspackOnlyTest } from '@e2e/helper'; +import { rspackOnlyTest, updateConfigForTest } from '@e2e/helper'; import { expect } from '@playwright/test'; import { startDevServer } from './scripts/server.mjs'; -rspackOnlyTest.only('support ssr', async ({ page }) => { - const { config, close } = await startDevServer(__dirname); +rspackOnlyTest('support SSR', async ({ page }) => { + const { config, close } = await startDevServer( + __dirname, + await updateConfigForTest({}, __dirname), + ); const url1 = new URL(`http://localhost:${config.port}`); @@ -14,9 +17,12 @@ rspackOnlyTest.only('support ssr', async ({ page }) => { await close(); }); -rspackOnlyTest('support ssr with esm target', async ({ page }) => { +rspackOnlyTest('support SSR with esm target', async ({ page }) => { process.env.TEST_ESM_LIBRARY = '1'; - const { config, close } = await startDevServer(__dirname); + const { config, close } = await startDevServer( + __dirname, + await updateConfigForTest({}, __dirname), + ); const url1 = new URL(`http://localhost:${config.port}`); @@ -29,9 +35,12 @@ rspackOnlyTest('support ssr with esm target', async ({ page }) => { delete process.env.TEST_ESM_LIBRARY; }); -rspackOnlyTest('support ssr with split chunk', async ({ page }) => { +rspackOnlyTest('support SSR with split chunk', async ({ page }) => { process.env.TEST_SPLIT_CHUNK = '1'; - const { config, close } = await startDevServer(__dirname); + const { config, close } = await startDevServer( + __dirname, + await updateConfigForTest({}, __dirname), + ); const url1 = new URL(`http://localhost:${config.port}`); diff --git a/e2e/cases/server/ssr/scripts/server.mjs b/e2e/cases/server/ssr/scripts/server.mjs index f5399ef511..a4afd80ca0 100644 --- a/e2e/cases/server/ssr/scripts/server.mjs +++ b/e2e/cases/server/ssr/scripts/server.mjs @@ -20,9 +20,11 @@ export const serverRender = (rsbuildServer) => async (_req, res, _next) => { }; export async function startDevServer(fixtures, overridsConfig) { - const { content } = await loadConfig({ - cwd: fixtures, - }); + const { content } = overridsConfig + ? { content: overridsConfig } + : await loadConfig({ + cwd: fixtures, + }); const rsbuild = await createRsbuild({ cwd: fixtures, diff --git a/e2e/scripts/shared.ts b/e2e/scripts/shared.ts index 7bd36cee05..deff20a44f 100644 --- a/e2e/scripts/shared.ts +++ b/e2e/scripts/shared.ts @@ -99,7 +99,7 @@ export async function getRandomPort( } } -const updateConfigForTest = async ( +export const updateConfigForTest = async ( originalConfig: RsbuildConfig, cwd: string = process.cwd(), ) => { From b57bf6f48b6da23bed285018b0d479715702f990 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Tue, 11 Jun 2024 12:06:17 +0800 Subject: [PATCH 08/16] fix: should register onDevCompileDone hook before startCompile --- packages/core/src/server/devServer.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index 2a19d30914..315e2bed58 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -201,6 +201,20 @@ export async function createDevServer< }); } + let _stats: Stats | MultiStats; + + // should register onDevCompileDone hook before startCompile + const waitFirstCompileDone = new Promise((resolve) => { + options.context.hooks.onDevCompileDone.tap(({ stats, isFirstCompile }) => { + _stats = stats; + + if (!isFirstCompile) { + return; + } + resolve(); + }); + }); + const compileMiddlewareAPI = runCompile ? await startCompile() : undefined; const fileWatcher = await setupWatchFiles({ @@ -247,19 +261,6 @@ export async function createDevServer< getHTMLPaths: options.context.pluginAPI!.getHTMLPaths, }; - let _stats: Stats | MultiStats; - - const waitFirstCompileDone = new Promise((resolve) => { - options.context.hooks.onDevCompileDone.tap(({ stats, isFirstCompile }) => { - _stats = stats; - - if (!isFirstCompile) { - return; - } - resolve(); - }); - }); - const server = { port, middlewares, From 43f064875c2b0285d72178cab8006763bc8fd433 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Tue, 11 Jun 2024 12:48:20 +0800 Subject: [PATCH 09/16] fix: name conflict --- e2e/cases/server/ssr/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/cases/server/ssr/package.json b/e2e/cases/server/ssr/package.json index ac138e287b..a90236b5dd 100644 --- a/e2e/cases/server/ssr/package.json +++ b/e2e/cases/server/ssr/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "@e2e/custom-server", + "name": "@e2e/custom-server-ssr", "version": "1.0.0", "scripts": { "dev": "node ./scripts/index.mjs", From 8f49f806e211d73066b4c0a7d4e355ea8dab2148 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Tue, 11 Jun 2024 15:15:15 +0800 Subject: [PATCH 10/16] fix: set node env --- e2e/cases/server/ssr/scripts/server.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/cases/server/ssr/scripts/server.mjs b/e2e/cases/server/ssr/scripts/server.mjs index a4afd80ca0..661d6aebe4 100644 --- a/e2e/cases/server/ssr/scripts/server.mjs +++ b/e2e/cases/server/ssr/scripts/server.mjs @@ -20,6 +20,8 @@ export const serverRender = (rsbuildServer) => async (_req, res, _next) => { }; export async function startDevServer(fixtures, overridsConfig) { + process.env.NODE_ENV = 'development'; + const { content } = overridsConfig ? { content: overridsConfig } : await loadConfig({ From 583a5aa1cdf70a9d4729ed266f8ca45d08a5fc2e Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Fri, 28 Jun 2024 17:11:04 +0800 Subject: [PATCH 11/16] fix: use environment api --- e2e/cases/server/ssr/rsbuild.config.ts | 123 +++++++++--------- e2e/cases/server/ssr/scripts/server.mjs | 7 +- packages/core/src/server/devServer.ts | 86 ++++++------ .../src/server/{ssr.ts => environment.ts} | 18 ++- packages/core/src/server/runner/basic.ts | 9 +- packages/core/src/server/runner/cjs.ts | 2 +- packages/core/src/server/runner/esm.ts | 2 +- packages/core/src/server/runner/index.ts | 6 +- packages/shared/src/types/config/dev.ts | 13 ++ 9 files changed, 138 insertions(+), 128 deletions(-) rename packages/core/src/server/{ssr.ts => environment.ts} (83%) diff --git a/e2e/cases/server/ssr/rsbuild.config.ts b/e2e/cases/server/ssr/rsbuild.config.ts index 835be015e7..bb2b71d0e3 100644 --- a/e2e/cases/server/ssr/rsbuild.config.ts +++ b/e2e/cases/server/ssr/rsbuild.config.ts @@ -3,70 +3,75 @@ import { pluginReact } from '@rsbuild/plugin-react'; export default defineConfig({ plugins: [pluginReact()], - source: { - entry({ target }) { - if (target === 'web') { - return { + environments: { + web: { + output: { + target: 'web', + }, + source: { + entry: { index: './src/index', - }; - } - if (target === 'node') { - return { - index: './src/index.server', - }; - } + }, + }, }, - }, - html: { - template: './template.html', - }, - output: { - targets: ['web', 'node'], - }, - tools: { - rspack: (config, { isServer }) => { - if (process.env.TEST_ESM_LIBRARY && isServer) { - return { - ...config, - experiments: { - ...config.experiments, - outputModule: true, - }, - output: { - ...config.output, - filename: '[name].mjs', - chunkFilename: '[name].mjs', - chunkFormat: 'module', - chunkLoading: 'import', - library: { - type: 'module', - }, - }, - }; - } + ssr: { + output: { + target: 'node', + }, + source: { + entry: { + index: './src/index.server', + }, + }, + tools: { + rspack: (config) => { + if (process.env.TEST_ESM_LIBRARY) { + return { + ...config, + experiments: { + ...config.experiments, + outputModule: true, + }, + output: { + ...config.output, + filename: '[name].mjs', + chunkFilename: '[name].mjs', + chunkFormat: 'module', + chunkLoading: 'import', + library: { + type: 'module', + }, + }, + }; + } - if (process.env.TEST_SPLIT_CHUNK && isServer) { - return { - ...config, - optimization: { - runtimeChunk: true, - splitChunks: { - chunks: 'all', - enforceSizeThreshold: 50000, - minSize: 0, - cacheGroups: { - 'lib-react': { - test: /[\\/]node_modules[\\/](react|react-dom|scheduler|react-refresh|@rspack[\\/]plugin-react-refresh)[\\/]/, - priority: 0, - name: 'lib-react', - reuseExistingChunk: true, + if (process.env.TEST_SPLIT_CHUNK) { + return { + ...config, + optimization: { + runtimeChunk: true, + splitChunks: { + chunks: 'all', + enforceSizeThreshold: 50000, + minSize: 0, + cacheGroups: { + 'lib-react': { + test: /[\\/]node_modules[\\/](react|react-dom|scheduler|react-refresh|@rspack[\\/]plugin-react-refresh)[\\/]/, + priority: 0, + name: 'lib-react', + reuseExistingChunk: true, + }, + }, }, }, - }, - }, - }; - } - return config; + }; + } + return config; + }, + }, }, }, + html: { + template: './template.html', + }, }); diff --git a/e2e/cases/server/ssr/scripts/server.mjs b/e2e/cases/server/ssr/scripts/server.mjs index 661d6aebe4..6ee1405e06 100644 --- a/e2e/cases/server/ssr/scripts/server.mjs +++ b/e2e/cases/server/ssr/scripts/server.mjs @@ -1,15 +1,14 @@ -import fs from 'node:fs'; -import path from 'node:path'; import { createRsbuild, logger } from '@rsbuild/core'; import { loadConfig } from '@rsbuild/core'; import polka from 'polka'; export const serverRender = (rsbuildServer) => async (_req, res, _next) => { - const indexModule = await rsbuildServer.ssrLoadModule('index'); + const indexModule = await rsbuildServer.environments.ssr.loadBundle('index'); const markup = indexModule.render(); - const template = await rsbuildServer.getTransformedHtml('index'); + const template = + await rsbuildServer.environments.web.getTransformedHtml('index'); const html = template.replace('', markup); diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts index ad8c17b108..6fadae550e 100644 --- a/packages/core/src/server/devServer.ts +++ b/packages/core/src/server/devServer.ts @@ -1,5 +1,4 @@ import fs from 'node:fs'; -// import { isAbsolute, join } from 'node:path'; import type { CreateDevServerOptions, EnvironmentAPI, @@ -22,6 +21,7 @@ import type { NormalizedConfig, NormalizedDevConfig, } from '../types'; +import { getTransformedHtml, loadBundle } from './environment'; import { type RsbuildDevMiddlewareOptions, getMiddlewares, @@ -37,7 +37,6 @@ import { import { createHttpServer } from './httpServer'; import { notFoundMiddleware } from './middlewares'; import { onBeforeRestartServer } from './restart'; -import { type ServerUtils, getTransformedHtml, ssrLoadModule } from './ssr'; import { setupWatchFiles } from './watchFiles'; export type RsbuildDevServer = { @@ -85,19 +84,6 @@ export type RsbuildDevServer = { * Close the Rsbuild server. */ close: () => Promise; - - /** - * Load and execute SSR module in Server. - * - * @param entryName - relate to rsbuild source.entry - * @returns the return of server-entry. - */ - ssrLoadModule: (entryName: string) => Promise; - - /** - * Get the compiled HTML template. - */ - getTransformedHtml: (entryName: string) => Promise; }; const formatDevConfig = (config: NormalizedDevConfig, port: number) => { @@ -239,42 +225,48 @@ export async function createDevServer< const pwd = options.context.rootPath; - const serverUtils: ServerUtils = { - readFileSync: (fileName: string) => { - if ('readFileSync' in outputFileSystem) { - // bundle require needs a synchronous method, although readFileSync is not within the outputFileSystem type definition, but nodejs fs API implemented. - // @ts-expect-error - return outputFileSystem.readFileSync(fileName, 'utf-8'); - } - return fs.readFileSync(fileName, 'utf-8'); - }, - // distPath: isAbsolute(distPath) ? distPath : join(pwd, distPath), - getHTMLPaths: options.context.pluginAPI!.getHTMLPaths, + const readFileSync = (fileName: string) => { + if ('readFileSync' in outputFileSystem) { + // bundle require needs a synchronous method, although readFileSync is not within the outputFileSystem type definition, but nodejs fs API implemented. + // @ts-expect-error + return outputFileSystem.readFileSync(fileName, 'utf-8'); + } + return fs.readFileSync(fileName, 'utf-8'); }; const environmentAPI = Object.fromEntries( - Object.keys(options.context.environments).map((name, index) => { - return [ - name, - { - getStats: async () => { - if (!runCompile) { - throw new Error("can't get stats info when runCompile is false"); - } - await waitFirstCompileDone; - return lastStats[index]; - }, - loadModule: async (entryName: string) => { - await waitFirstCompileDone; - return loadModule(lastStats[index], entryName, serverUtils); - }, - getTransformedHtml: async (entryName: string) => { - await waitFirstCompileDone; - return getTransformedHtml(entryName, serverUtils); + Object.entries(options.context.environments).map( + ([name, environment], index) => { + return [ + name, + { + getStats: async () => { + if (!runCompile) { + throw new Error( + "can't get stats info when runCompile is false", + ); + } + await waitFirstCompileDone; + return lastStats[index]; + }, + loadBundle: async (entryName: string) => { + await waitFirstCompileDone; + return loadBundle(lastStats[index], entryName, { + readFileSync, + environment, + }); + }, + getTransformedHtml: async (entryName: string) => { + await waitFirstCompileDone; + return getTransformedHtml(entryName, { + readFileSync, + environment, + }); + }, }, - }, - ]; - }), + ]; + }, + ), ); const devMiddlewares = await getMiddlewares({ diff --git a/packages/core/src/server/ssr.ts b/packages/core/src/server/environment.ts similarity index 83% rename from packages/core/src/server/ssr.ts rename to packages/core/src/server/environment.ts index 749db7f0bf..b23ffae4dd 100644 --- a/packages/core/src/server/ssr.ts +++ b/packages/core/src/server/environment.ts @@ -4,11 +4,10 @@ import { run } from './runner'; export type ServerUtils = { readFileSync: (fileName: string) => string; - getHTMLPaths: () => Record; - distPath: string; + environment: EnvironmentContext; }; -export const loadModule = async ( +export const loadBundle = async ( stats: Stats, entryName: string, utils: ServerUtils, @@ -48,29 +47,28 @@ export const loadModule = async ( ); } - const res = await run( + const res = await run( files[0], outputPath!, stats.compilation.options, utils.readFileSync, ); - return res as T; + return res; }; export const getTransformedHtml = async ( entryName: string, - environment: EnvironmentContext, -) => { - // TODO: should get html resource from stats? - const htmlPaths = utils.getHTMLPaths(); + utils: ServerUtils, +): Promise => { + const { htmlPaths, distPath } = utils.environment; const htmlPath = htmlPaths[entryName]; if (!htmlPath) { throw new Error(`can't get html file by entryName(${entryName})`); } - const fileName = join(utils.distPath, htmlPath); + const fileName = join(distPath, htmlPath); const fileContent = utils.readFileSync(fileName); diff --git a/packages/core/src/server/runner/basic.ts b/packages/core/src/server/runner/basic.ts index 35f7d542d7..498f5be0d5 100644 --- a/packages/core/src/server/runner/basic.ts +++ b/packages/core/src/server/runner/basic.ts @@ -106,10 +106,13 @@ export abstract class BasicRunner implements Runner { return null; } - protected preExecute(_code: string, _file: BasicRunnerFile) {} - protected postExecute(_m: Record, _file: BasicRunnerFile) {} + protected preExecute(_code: string, _file: BasicRunnerFile): void {} + protected postExecute( + _m: Record, + _file: BasicRunnerFile, + ): void {} - protected createRunner() { + protected createRunner(): void { this.requirers.set( 'entry', (_currentDirectory, _modulePath, _context = {}) => { diff --git a/packages/core/src/server/runner/cjs.ts b/packages/core/src/server/runner/cjs.ts index 03dde9a0cd..bcb0abcfed 100644 --- a/packages/core/src/server/runner/cjs.ts +++ b/packages/core/src/server/runner/cjs.ts @@ -63,7 +63,7 @@ export class CommonJsRunner extends BasicRunner { }; } - protected createRunner() { + protected createRunner(): void { this.requirers.set('miss', this.createMissRequirer()); this.requirers.set('entry', this.createCjsRequirer()); } diff --git a/packages/core/src/server/runner/esm.ts b/packages/core/src/server/runner/esm.ts index b954f87b52..74bd18e5d9 100644 --- a/packages/core/src/server/runner/esm.ts +++ b/packages/core/src/server/runner/esm.ts @@ -8,7 +8,7 @@ import { CommonJsRunner } from './cjs'; import { EEsmMode, type RunnerRequirer } from './type'; export class EsmRunner extends CommonJsRunner { - protected createRunner() { + protected createRunner(): void { super.createRunner(); this.requirers.set('cjs', this.getRequire()); this.requirers.set('esm', this.createEsmRequirer()); diff --git a/packages/core/src/server/runner/index.ts b/packages/core/src/server/runner/index.ts index d4c6bc57dd..22791d5886 100644 --- a/packages/core/src/server/runner/index.ts +++ b/packages/core/src/server/runner/index.ts @@ -41,12 +41,12 @@ export class BasicRunnerFactory implements RunnerFactory { } } -export const run = async ( +export const run = async ( bundlePath: string, outputPath: string, compilerOptions: CompilerOptions, readFileSync: (path: string) => string, -) => { +): Promise => { const runnerFactory = new BasicRunnerFactory(bundlePath); const runner = runnerFactory.create( compilerOptions, @@ -55,5 +55,5 @@ export const run = async ( ); const mod = runner.run(bundlePath); - return mod; + return mod as T; }; diff --git a/packages/shared/src/types/config/dev.ts b/packages/shared/src/types/config/dev.ts index 4218b9f8da..e3db0c817a 100644 --- a/packages/shared/src/types/config/dev.ts +++ b/packages/shared/src/types/config/dev.ts @@ -18,6 +18,19 @@ export type RequestHandler = ( export type EnvironmentAPI = { [name: string]: { getStats: () => Promise; + + /** + * Load and execute stats bundle in Server. + * + * @param entryName - relate to rsbuild source.entry + * @returns the return of entry module. + */ + loadBundle: (entryName: string) => Promise; + + /** + * Get the compiled HTML template. + */ + getTransformedHtml: (entryName: string) => Promise; }; }; From 6aab4f6aadac293586790cc2b3275668eaf35532 Mon Sep 17 00:00:00 2001 From: gaoyuan <9aoyuao@gmail.com> Date: Mon, 1 Jul 2024 13:33:21 +0800 Subject: [PATCH 12/16] Update e2e/cases/server/ssr/scripts/server.mjs Co-authored-by: neverland --- e2e/cases/server/ssr/scripts/server.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/cases/server/ssr/scripts/server.mjs b/e2e/cases/server/ssr/scripts/server.mjs index 6ee1405e06..1de8069393 100644 --- a/e2e/cases/server/ssr/scripts/server.mjs +++ b/e2e/cases/server/ssr/scripts/server.mjs @@ -43,7 +43,7 @@ export async function startDevServer(fixtures, overridsConfig) { try { await serverRenderMiddleware(req, res, next); } catch (err) { - logger.error('ssr render error, downgrade to csr...\n', err); + logger.error('SSR render error, downgrade to CSR...\n', err); next(); } } else { From 6f23272135ab825305261214f5d8001162b526c0 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Mon, 1 Jul 2024 16:23:47 +0800 Subject: [PATCH 13/16] fix: update type and error log --- packages/core/src/server/environment.ts | 6 +++--- .../runner/{legacy/asModule.js => asModule.ts} | 11 ++++++++--- packages/core/src/server/runner/esm.ts | 13 ++++++------- packages/core/src/server/runner/type.ts | 4 ++-- packages/shared/src/types/config/dev.ts | 2 +- 5 files changed, 20 insertions(+), 16 deletions(-) rename packages/core/src/server/runner/{legacy/asModule.js => asModule.ts} (73%) diff --git a/packages/core/src/server/environment.ts b/packages/core/src/server/environment.ts index b23ffae4dd..aeb5db198f 100644 --- a/packages/core/src/server/environment.ts +++ b/packages/core/src/server/environment.ts @@ -20,7 +20,7 @@ export const loadBundle = async ( }); if (!entrypoints?.[entryName]) { - throw new Error(`can't find ssr entry(${entryName})`); + throw new Error(`can't find entry(${entryName})`); } const { chunks: entryChunks } = entrypoints[entryName]; @@ -37,13 +37,13 @@ export const loadBundle = async ( }, []); if (files.length === 0) { - throw new Error(`can't get ssr bundle by entryName(${entryName})`); + throw new Error(`can't get bundle by entryName(${entryName})`); } // An entrypoint should have only one entryChunk, but there may be some boundary cases if (files.length > 1) { throw new Error( - `only support load single ssr bundle entry, but got ${files.length}: ${files.join(',')}`, + `only support load single entryChunk, but got ${files.length}: ${files.join(',')}`, ); } diff --git a/packages/core/src/server/runner/legacy/asModule.js b/packages/core/src/server/runner/asModule.ts similarity index 73% rename from packages/core/src/server/runner/legacy/asModule.js rename to packages/core/src/server/runner/asModule.ts index 0408b6de6c..3061443f2f 100644 --- a/packages/core/src/server/runner/legacy/asModule.js +++ b/packages/core/src/server/runner/asModule.ts @@ -1,8 +1,12 @@ -const vm = require('node:vm'); +import vm from 'node:vm'; const SYNTHETIC_MODULES_STORE = '__SYNTHETIC_MODULES_STORE'; -module.exports = async (something, context, unlinked) => { +export const asModule = async ( + something: Record, + context: Record, + unlinked?: boolean, +): Promise => { if (something instanceof vm.Module) { return something; } @@ -21,7 +25,8 @@ module.exports = async (something, context, unlinked) => { context, }); if (unlinked) return m; - await m.link(() => {}); + await m.link((() => {}) as () => vm.Module); + // @ts-expect-error copy from webpack if (m.instantiate) m.instantiate(); await m.evaluate(); return m; diff --git a/packages/core/src/server/runner/esm.ts b/packages/core/src/server/runner/esm.ts index 74bd18e5d9..16092d7137 100644 --- a/packages/core/src/server/runner/esm.ts +++ b/packages/core/src/server/runner/esm.ts @@ -1,11 +1,10 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import vm, { type SourceTextModule } from 'node:vm'; -// @ts-expect-error -import asModule from './legacy/asModule'; +import { asModule } from './asModule'; import { CommonJsRunner } from './cjs'; -import { EEsmMode, type RunnerRequirer } from './type'; +import { EsmMode, type RunnerRequirer } from './type'; export class EsmRunner extends CommonJsRunner { protected createRunner(): void { @@ -67,14 +66,14 @@ export class EsmRunner extends CommonJsRunner { module: { context: any }, ) => { const result = await _require(path.dirname(file!.path), specifier, { - esmMode: EEsmMode.Evaluated, + esmMode: EsmMode.Evaluated, }); return await asModule(result, module.context); }, } as any); esmCache.set(file.path, esm); } - if (context.esmMode === EEsmMode.Unlinked) return esm; + if (context.esmMode === EsmMode.Unlinked) return esm; return (async () => { await esm.link(async (specifier, referencingModule) => { return await asModule( @@ -86,7 +85,7 @@ export class EsmRunner extends CommonJsRunner { ), specifier, { - esmMode: EEsmMode.Unlinked, + esmMode: EsmMode.Unlinked, }, ), referencingModule.context, @@ -95,7 +94,7 @@ export class EsmRunner extends CommonJsRunner { }); if ((esm as any).instantiate) (esm as any).instantiate(); await esm.evaluate(); - if (context.esmMode === EEsmMode.Evaluated) { + if (context.esmMode === EsmMode.Evaluated) { return esm; } const ns = esm.namespace as { diff --git a/packages/core/src/server/runner/type.ts b/packages/core/src/server/runner/type.ts index e64232c71e..7c439994c0 100644 --- a/packages/core/src/server/runner/type.ts +++ b/packages/core/src/server/runner/type.ts @@ -5,7 +5,7 @@ export type RunnerRequirer = ( modulePath: string[] | string, context?: { file?: BasicRunnerFile; - esmMode?: EEsmMode; + esmMode?: EsmMode; }, ) => Record | Promise>; @@ -15,7 +15,7 @@ export type BasicRunnerFile = { subPath: string; }; -export enum EEsmMode { +export enum EsmMode { Unknown = 0, Evaluated = 1, Unlinked = 2, diff --git a/packages/shared/src/types/config/dev.ts b/packages/shared/src/types/config/dev.ts index e3db0c817a..8ccfcbedac 100644 --- a/packages/shared/src/types/config/dev.ts +++ b/packages/shared/src/types/config/dev.ts @@ -25,7 +25,7 @@ export type EnvironmentAPI = { * @param entryName - relate to rsbuild source.entry * @returns the return of entry module. */ - loadBundle: (entryName: string) => Promise; + loadBundle: (entryName: string) => Promise; /** * Get the compiled HTML template. From 3652021be6478714efba579bf444a7bd23cd9be3 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Mon, 1 Jul 2024 16:57:09 +0800 Subject: [PATCH 14/16] fix: simply ssr test case --- e2e/cases/server/ssr/index.test.ts | 41 +++++++------- e2e/cases/server/ssr/package.json | 4 +- e2e/cases/server/ssr/rsbuild.config.ts | 47 +++++++++++++++- e2e/cases/server/ssr/scripts/index.mjs | 3 -- e2e/cases/server/ssr/scripts/server.mjs | 72 ------------------------- e2e/scripts/shared.ts | 2 +- packages/core/src/index.ts | 2 + 7 files changed, 72 insertions(+), 99 deletions(-) delete mode 100644 e2e/cases/server/ssr/scripts/index.mjs delete mode 100644 e2e/cases/server/ssr/scripts/server.mjs diff --git a/e2e/cases/server/ssr/index.test.ts b/e2e/cases/server/ssr/index.test.ts index badba37051..ed9363aff6 100644 --- a/e2e/cases/server/ssr/index.test.ts +++ b/e2e/cases/server/ssr/index.test.ts @@ -1,54 +1,55 @@ -import { rspackOnlyTest, updateConfigForTest } from '@e2e/helper'; +import { dev, rspackOnlyTest } from '@e2e/helper'; import { expect } from '@playwright/test'; -import { startDevServer } from './scripts/server.mjs'; rspackOnlyTest('support SSR', async ({ page }) => { - const { config, close } = await startDevServer( - __dirname, - await updateConfigForTest({}, __dirname), - ); + const rsbuild = await dev({ + cwd: __dirname, + rsbuildConfig: {}, + }); - const url1 = new URL(`http://localhost:${config.port}`); + const url1 = new URL(`http://localhost:${rsbuild.port}`); const res = await page.goto(url1.href); expect(await res?.text()).toMatch(/Rsbuild with React/); - await close(); + await rsbuild.close(); }); rspackOnlyTest('support SSR with esm target', async ({ page }) => { process.env.TEST_ESM_LIBRARY = '1'; - const { config, close } = await startDevServer( - __dirname, - await updateConfigForTest({}, __dirname), - ); - const url1 = new URL(`http://localhost:${config.port}`); + const rsbuild = await dev({ + cwd: __dirname, + rsbuildConfig: {}, + }); + + const url1 = new URL(`http://localhost:${rsbuild.port}`); const res = await page.goto(url1.href); expect(await res?.text()).toMatch(/Rsbuild with React/); - await close(); + await rsbuild.close(); delete process.env.TEST_ESM_LIBRARY; }); rspackOnlyTest('support SSR with split chunk', async ({ page }) => { process.env.TEST_SPLIT_CHUNK = '1'; - const { config, close } = await startDevServer( - __dirname, - await updateConfigForTest({}, __dirname), - ); - const url1 = new URL(`http://localhost:${config.port}`); + const rsbuild = await dev({ + cwd: __dirname, + rsbuildConfig: {}, + }); + + const url1 = new URL(`http://localhost:${rsbuild.port}`); const res = await page.goto(url1.href); expect(await res?.text()).toMatch(/Rsbuild with React/); - await close(); + await rsbuild.close(); delete process.env.TEST_SPLIT_CHUNK; }); diff --git a/e2e/cases/server/ssr/package.json b/e2e/cases/server/ssr/package.json index a90236b5dd..96c6cd2285 100644 --- a/e2e/cases/server/ssr/package.json +++ b/e2e/cases/server/ssr/package.json @@ -3,8 +3,8 @@ "name": "@e2e/custom-server-ssr", "version": "1.0.0", "scripts": { - "dev": "node ./scripts/index.mjs", - "dev:esm": "TEST_ESM_LIBRARY=true node --experimental-vm-modules ./scripts/index.mjs" + "dev": "npx rsbuild dev", + "dev:esm": "TEST_ESM_LIBRARY=true npx rsbuild dev" }, "dependencies": { "polka": "^0.5.2" diff --git a/e2e/cases/server/ssr/rsbuild.config.ts b/e2e/cases/server/ssr/rsbuild.config.ts index bb2b71d0e3..f898c18a01 100644 --- a/e2e/cases/server/ssr/rsbuild.config.ts +++ b/e2e/cases/server/ssr/rsbuild.config.ts @@ -1,8 +1,53 @@ -import { defineConfig } from '@rsbuild/core'; +import { + type RequestHandler, + type ServerAPIs, + defineConfig, + logger, +} from '@rsbuild/core'; import { pluginReact } from '@rsbuild/plugin-react'; +export const serverRender = + (serverAPI: ServerAPIs): RequestHandler => + async (_req, res, _next) => { + const indexModule = await serverAPI.environments.ssr.loadBundle<{ + render: () => string; + }>('index'); + + const markup = indexModule.render(); + + const template = + await serverAPI.environments.web.getTransformedHtml('index'); + + const html = template.replace('', markup); + + res.writeHead(200, { + 'Content-Type': 'text/html', + }); + res.end(html); + }; + export default defineConfig({ plugins: [pluginReact()], + dev: { + setupMiddlewares: [ + ({ unshift }, serverAPI) => { + const serverRenderMiddleware = serverRender(serverAPI); + + unshift(async (req, res, next) => { + if (req.method === 'GET' && req.url === '/') { + try { + await serverRenderMiddleware(req, res, next); + } catch (err) { + logger.error('SSR render error, downgrade to CSR...\n', err); + next(); + } + } else { + next(); + } + }); + }, + ], + }, environments: { web: { output: { diff --git a/e2e/cases/server/ssr/scripts/index.mjs b/e2e/cases/server/ssr/scripts/index.mjs deleted file mode 100644 index 35cdecbc3e..0000000000 --- a/e2e/cases/server/ssr/scripts/index.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { startDevServer } from './server.mjs'; - -startDevServer(process.cwd()); diff --git a/e2e/cases/server/ssr/scripts/server.mjs b/e2e/cases/server/ssr/scripts/server.mjs deleted file mode 100644 index 1de8069393..0000000000 --- a/e2e/cases/server/ssr/scripts/server.mjs +++ /dev/null @@ -1,72 +0,0 @@ -import { createRsbuild, logger } from '@rsbuild/core'; -import { loadConfig } from '@rsbuild/core'; -import polka from 'polka'; - -export const serverRender = (rsbuildServer) => async (_req, res, _next) => { - const indexModule = await rsbuildServer.environments.ssr.loadBundle('index'); - - const markup = indexModule.render(); - - const template = - await rsbuildServer.environments.web.getTransformedHtml('index'); - - const html = template.replace('', markup); - - res.writeHead(200, { - 'Content-Type': 'text/html', - }); - res.end(html); -}; - -export async function startDevServer(fixtures, overridsConfig) { - process.env.NODE_ENV = 'development'; - - const { content } = overridsConfig - ? { content: overridsConfig } - : await loadConfig({ - cwd: fixtures, - }); - - const rsbuild = await createRsbuild({ - cwd: fixtures, - rsbuildConfig: content, - }); - - const app = polka(); - - const rsbuildServer = await rsbuild.createDevServer(); - - const serverRenderMiddleware = serverRender(rsbuildServer); - - app.use('/', async (req, res, next) => { - if (req.method === 'GET' && req.url === '/') { - try { - await serverRenderMiddleware(req, res, next); - } catch (err) { - logger.error('SSR render error, downgrade to CSR...\n', err); - next(); - } - } else { - next(); - } - }); - - app.use(rsbuildServer.middlewares); - - const { port } = rsbuildServer; - - const { server } = app.listen({ port }, async () => { - await rsbuildServer.afterListen(); - }); - - // subscribe the server's http upgrade event to handle WebSocket upgrade - server.on('upgrade', rsbuildServer.onHTTPUpgrade); - - return { - config: { port }, - close: async () => { - await rsbuildServer.close(); - server.close(); - }, - }; -} diff --git a/e2e/scripts/shared.ts b/e2e/scripts/shared.ts index 081100ddd5..af628ffa37 100644 --- a/e2e/scripts/shared.ts +++ b/e2e/scripts/shared.ts @@ -99,7 +99,7 @@ export async function getRandomPort( } } -export const updateConfigForTest = async ( +const updateConfigForTest = async ( originalConfig: RsbuildConfig, cwd: string = process.cwd(), ) => { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 12173aba57..a697b8e13b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -120,6 +120,8 @@ export type { ModifyRsbuildConfigFn, TransformFn, TransformHandler, + ServerAPIs, + RequestHandler, } from '@rsbuild/shared'; export { From ce3c23a1e7ffc3fa9f249df8007ff1e07fb2c413 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Mon, 1 Jul 2024 17:20:47 +0800 Subject: [PATCH 15/16] fix: remove useless deps --- e2e/cases/server/ssr/package.json | 3 --- pnpm-lock.yaml | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/e2e/cases/server/ssr/package.json b/e2e/cases/server/ssr/package.json index 96c6cd2285..ecb7c000bb 100644 --- a/e2e/cases/server/ssr/package.json +++ b/e2e/cases/server/ssr/package.json @@ -5,8 +5,5 @@ "scripts": { "dev": "npx rsbuild dev", "dev:esm": "TEST_ESM_LIBRARY=true npx rsbuild dev" - }, - "dependencies": { - "polka": "^0.5.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7abdb70a6f..4b912c84fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -295,11 +295,7 @@ importers: specifier: ^0.5.2 version: 0.5.2 - e2e/cases/server/ssr: - dependencies: - polka: - specifier: ^0.5.2 - version: 0.5.2 + e2e/cases/server/ssr: {} e2e/cases/source-build/app: dependencies: From 2e50b270701fb9ce24393293441e04e792fb0cea Mon Sep 17 00:00:00 2001 From: neverland Date: Mon, 1 Jul 2024 17:38:27 +0800 Subject: [PATCH 16/16] Update packages/core/src/server/environment.ts --- packages/core/src/server/environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/server/environment.ts b/packages/core/src/server/environment.ts index aeb5db198f..7b36f9ecd2 100644 --- a/packages/core/src/server/environment.ts +++ b/packages/core/src/server/environment.ts @@ -43,7 +43,7 @@ export const loadBundle = async ( // An entrypoint should have only one entryChunk, but there may be some boundary cases if (files.length > 1) { throw new Error( - `only support load single entryChunk, but got ${files.length}: ${files.join(',')}`, + `only support load single entry chunk, but got ${files.length}: ${files.join(',')}`, ); }