Skip to content

Commit 2a0948c

Browse files
committed
wip: ssr module runner
1 parent 7440a62 commit 2a0948c

File tree

4 files changed

+154
-22
lines changed

4 files changed

+154
-22
lines changed

packages/vite/src/node/server/environments/rolldown.ts

+137-21
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export function rolldownDevHandleConfig(
6363
createEnvironment: RolldownEnvironment.createFactory({
6464
hmr: config.experimental?.rolldownDev?.hmr,
6565
reactRefresh: config.experimental?.rolldownDev?.reactRefresh,
66+
ssrModuleRunner: false,
6667
}),
6768
},
6869
build: {
@@ -81,6 +82,7 @@ export function rolldownDevHandleConfig(
8182
createEnvironment: RolldownEnvironment.createFactory({
8283
hmr: false,
8384
reactRefresh: false,
85+
ssrModuleRunner: config.experimental?.rolldownDev?.ssrModuleRunner,
8486
}),
8587
},
8688
},
@@ -134,6 +136,8 @@ class RolldownEnvironment extends DevEnvironment {
134136
result!: rolldown.RolldownOutput
135137
outDir!: string
136138
buildTimestamp = Date.now()
139+
inputOptions!: rolldown.InputOptions
140+
outputOptions!: rolldown.OutputOptions
137141

138142
static createFactory(
139143
rolldownDevOptioins: RolldownDevOptions,
@@ -200,7 +204,7 @@ class RolldownEnvironment extends DevEnvironment {
200204
plugins = plugins.map((p) => injectEnvironmentToHooks(this as any, p))
201205

202206
console.time(`[rolldown:${this.name}:build]`)
203-
const inputOptions: rolldown.InputOptions = {
207+
this.inputOptions = {
204208
dev: this.rolldownDevOptions.hmr,
205209
input: this.config.build.rollupOptions.input,
206210
cwd: this.config.root,
@@ -212,30 +216,34 @@ class RolldownEnvironment extends DevEnvironment {
212216
},
213217
plugins: [
214218
...plugins,
215-
patchRuntimePlugin(this.rolldownDevOptions),
219+
patchRuntimePlugin(this),
216220
patchCssPlugin(),
217221
reactRefreshPlugin(),
218222
],
219223
moduleTypes: {
220224
'.css': 'js',
221225
},
222226
}
223-
this.instance = await rolldown.rolldown(inputOptions)
227+
this.instance = await rolldown.rolldown(this.inputOptions)
224228

225-
// `generate` should work but we use `write` so it's easier to see output and debug
226-
const outputOptions: rolldown.OutputOptions = {
229+
const format: rolldown.ModuleFormat =
230+
this.name === 'client' || this.rolldownDevOptions.ssrModuleRunner
231+
? 'app'
232+
: 'esm'
233+
this.outputOptions = {
227234
dir: this.outDir,
228-
format: this.rolldownDevOptions.hmr ? 'app' : 'esm',
235+
format,
229236
// TODO: hmr_rebuild returns source map file when `sourcemap: true`
230237
sourcemap: 'inline',
231238
// TODO: https://github.com/rolldown/rolldown/issues/2041
232239
// handle `require("stream")` in `react-dom/server`
233240
banner:
234-
this.name === 'ssr'
241+
this.name === 'ssr' && format === 'esm'
235242
? `import __nodeModule from "node:module"; const require = __nodeModule.createRequire(import.meta.url);`
236243
: undefined,
237244
}
238-
this.result = await this.instance.write(outputOptions)
245+
// `generate` should work but we use `write` so it's easier to see output and debug
246+
this.result = await this.instance.write(this.outputOptions)
239247

240248
this.buildTimestamp = Date.now()
241249
console.timeEnd(`[rolldown:${this.name}:build]`)
@@ -249,12 +257,22 @@ class RolldownEnvironment extends DevEnvironment {
249257
if (!output.moduleIds.includes(ctx.file)) {
250258
return
251259
}
252-
if (this.rolldownDevOptions.hmr) {
260+
if (
261+
this.rolldownDevOptions.hmr ||
262+
this.rolldownDevOptions.ssrModuleRunner
263+
) {
253264
logger.info(`hmr '${ctx.file}'`, { timestamp: true })
254265
console.time(`[rolldown:${this.name}:hmr]`)
255266
const result = await this.instance.experimental_hmr_rebuild([ctx.file])
267+
if (this.name === 'client') {
268+
ctx.server.ws.send('rolldown:hmr', result)
269+
} else {
270+
this.getRunner().evaluate(
271+
result[1].toString(),
272+
path.join(this.outDir, result[0]),
273+
)
274+
}
256275
console.timeEnd(`[rolldown:${this.name}:hmr]`)
257-
ctx.server.ws.send('rolldown:hmr', result)
258276
} else {
259277
await this.build()
260278
if (this.name === 'client') {
@@ -263,40 +281,138 @@ class RolldownEnvironment extends DevEnvironment {
263281
}
264282
}
265283

284+
runner!: RolldownModuleRunner
285+
286+
getRunner() {
287+
if (!this.runner) {
288+
const output = this.result.output[0]
289+
const filepath = path.join(this.outDir, output.fileName)
290+
this.runner = new RolldownModuleRunner()
291+
const code = fs.readFileSync(filepath, 'utf-8')
292+
this.runner.evaluate(code, filepath)
293+
}
294+
return this.runner
295+
}
296+
266297
async import(input: string): Promise<unknown> {
267-
const output = this.result.output.find((o) => o.name === input)
268-
assert(output, `invalid import input '${input}'`)
298+
if (this.outputOptions.format === 'app') {
299+
return this.getRunner().import(input)
300+
}
301+
// input is no use
302+
const output = this.result.output[0]
269303
const filepath = path.join(this.outDir, output.fileName)
304+
// TODO: source map not applied when adding `?t=...`?
305+
// return import(`${pathToFileURL(filepath)}`)
270306
return import(`${pathToFileURL(filepath)}?t=${this.buildTimestamp}`)
271307
}
272308
}
273309

274-
function patchRuntimePlugin(
275-
rolldownDevOptions: RolldownDevOptions,
276-
): rolldown.Plugin {
310+
class RolldownModuleRunner {
311+
// intercept globals
312+
private context = {
313+
rolldown_runtime: {} as any,
314+
__rolldown_hot: {
315+
send: () => {},
316+
},
317+
// TODO: external require doesn't work in app format.
318+
// TODO: also it should be aware of importer for non static require/import.
319+
_require: require,
320+
}
321+
322+
// TODO: support resolution?
323+
async import(id: string): Promise<unknown> {
324+
const mod = this.context.rolldown_runtime.moduleCache[id]
325+
assert(mod, `Module not found '${id}'`)
326+
return mod.exports
327+
}
328+
329+
evaluate(code: string, sourceURL: string) {
330+
const context = {
331+
self: this.context,
332+
...this.context,
333+
}
334+
// extract sourcemap and move to the bottom
335+
const sourcemap = code.match(/^\/\/# sourceMappingURL=.*/m)?.[0] ?? ''
336+
if (sourcemap) {
337+
code = code.replace(sourcemap, '')
338+
}
339+
code = `\
340+
'use strict';(${Object.keys(context).join(',')})=>{{${code}
341+
// TODO: need to re-expose runtime utilities for now
342+
self.__toCommonJS = __toCommonJS;
343+
self.__export = __export;
344+
self.__toESM = __toESM;
345+
}}
346+
//# sourceURL=${sourceURL}
347+
//# sourceMappingSource=rolldown-module-runner
348+
${sourcemap}
349+
`
350+
const fn = (0, eval)(code)
351+
try {
352+
fn(...Object.values(context))
353+
} catch (e) {
354+
console.error('[RolldownModuleRunner:ERROR]', e)
355+
throw e
356+
}
357+
}
358+
}
359+
360+
function patchRuntimePlugin(environment: RolldownEnvironment): rolldown.Plugin {
277361
return {
278362
name: 'vite:rolldown-patch-runtime',
363+
// TODO: external require doesn't work in app format.
364+
// rewrite `require -> _require` and provide _require from module runner.
365+
// for now just rewrite known ones in "react-dom/server".
366+
transform: {
367+
filter: {
368+
code: {
369+
include: [/require\(['"](stream|util)['"]\)/],
370+
},
371+
},
372+
handler(code) {
373+
if (!environment.rolldownDevOptions.ssrModuleRunner) {
374+
return
375+
}
376+
return code.replace(
377+
/require(\(['"](stream|util)['"]\))/g,
378+
'_require($1)',
379+
)
380+
},
381+
},
279382
renderChunk(code) {
280383
// patch rolldown_runtime to workaround a few things
281384
// TODO: is there a robust way to inject code specifically to entry or runtime?
282385
if (code.includes('//#region rolldown:runtime')) {
283-
// TODO: is this magic string heavy?
386+
// TODO: this magic string is heavy
284387
const output = new MagicString(code)
285-
// replace hard-coded WebSocket setup with custom client
286-
output.replace(/const socket =.*?\n\};/s, getRolldownClientCode())
287-
// trigger full rebuild on non-accepting entry invalidation
288388
output
389+
// replace hard-coded WebSocket setup with custom client
390+
.replace(
391+
/const socket =.*?\n\};/s,
392+
environment.name === 'client' ? getRolldownClientCode() : '',
393+
)
394+
// fix rolldown_runtime.patch
395+
.replace(
396+
'this.executeModuleStack.length > 1',
397+
'this.executeModuleStack.length > 0',
398+
)
289399
.replace('parents: [parent],', 'parents: parent ? [parent] : [],')
400+
.replace(
401+
'if (module.parents.indexOf(parent) === -1) {',
402+
'if (parent && module.parents.indexOf(parent) === -1) {',
403+
)
290404
.replace(
291405
'for (var i = 0; i < module.parents.length; i++) {',
292406
`
293-
if (module.parents.length === 0) {
407+
boundaries.push(moduleId);
408+
invalidModuleIds.push(moduleId);
409+
if (module.parents.filter(Boolean).length === 0) {
294410
__rolldown_hot.send("rolldown:hmr-deadend", { moduleId });
295411
break;
296412
}
297413
for (var i = 0; i < module.parents.length; i++) {`,
298414
)
299-
if (rolldownDevOptions.reactRefresh) {
415+
if (environment.rolldownDevOptions.reactRefresh) {
300416
output.prepend(getReactRefreshRuntimeCode())
301417
}
302418
return {

playground/rolldown-dev-ssr/src/entry-server.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import ReactDOMServer from 'react-dom/server'
22
import type { Connect } from 'vite'
33
import { App } from './app'
4+
import { throwError } from './error'
45

56
const handler: Connect.SimpleHandleFunction = (req, res) => {
67
const url = new URL(req.url ?? '/', 'https://vite.dev')
78
console.log(`[SSR] ${req.method} ${url.pathname}`)
9+
if (url.pathname === '/crash-ssr') {
10+
throwError()
11+
}
812
const ssrHtml = ReactDOMServer.renderToString(<App />)
913
res.setHeader('content-type', 'text/html')
1014
// TODO: transformIndexHtml?
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//
2+
// random new lines
3+
//
4+
export function throwError(): never {
5+
//
6+
// and more
7+
//
8+
throw new Error('boom')
9+
}

playground/rolldown-dev-ssr/vite.config.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default defineConfig({
3030
rolldownDev: {
3131
hmr: true,
3232
reactRefresh: true,
33+
ssrModuleRunner: !process.env['NO_MODULE_RUNNER'],
3334
},
3435
},
3536
plugins: [
@@ -39,7 +40,9 @@ export default defineConfig({
3940
return () => {
4041
server.middlewares.use(async (req, res, next) => {
4142
try {
42-
const mod = await (server.environments.ssr as any).import('index')
43+
const mod = await (server.environments.ssr as any).import(
44+
'src/entry-server.tsx',
45+
)
4346
await mod.default(req, res)
4447
} catch (e) {
4548
next(e)

0 commit comments

Comments
 (0)