Skip to content

Commit 68a603d

Browse files
committed
wip: ssr module runner
1 parent 0c92ed2 commit 68a603d

File tree

4 files changed

+150
-22
lines changed

4 files changed

+150
-22
lines changed

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

+132-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,19 @@ 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(result[1].toString())
271+
}
256272
console.timeEnd(`[rolldown:${this.name}:hmr]`)
257-
ctx.server.ws.send('rolldown:hmr', result)
258273
} else {
259274
await this.build()
260275
if (this.name === 'client') {
@@ -263,40 +278,136 @@ class RolldownEnvironment extends DevEnvironment {
263278
}
264279
}
265280

281+
runner!: RolldownModuleRunner
282+
283+
getRunner() {
284+
if (!this.runner) {
285+
const output = this.result.output[0]
286+
const filepath = path.join(this.outDir, output.fileName)
287+
this.runner = new RolldownModuleRunner()
288+
const code = fs.readFileSync(filepath, 'utf-8')
289+
this.runner.evaluate(code)
290+
}
291+
return this.runner
292+
}
293+
266294
async import(input: string): Promise<unknown> {
267-
const output = this.result.output.find((o) => o.name === input)
268-
assert(output, `invalid import input '${input}'`)
295+
if (this.outputOptions.format === 'app') {
296+
return this.getRunner().import(input)
297+
}
298+
// input is no use
299+
const output = this.result.output[0]
269300
const filepath = path.join(this.outDir, output.fileName)
270301
return import(`${pathToFileURL(filepath)}?t=${this.buildTimestamp}`)
271302
}
272303
}
273304

274-
function patchRuntimePlugin(
275-
rolldownDevOptions: RolldownDevOptions,
276-
): rolldown.Plugin {
305+
class RolldownModuleRunner {
306+
// intercept globals
307+
private context = {
308+
rolldown_runtime: {} as any,
309+
__rolldown_hot: {
310+
send: () => {},
311+
},
312+
// TODO: external require doesn't work in app format.
313+
// TODO: also it should be aware of importer for non static require/import.
314+
_require: require,
315+
}
316+
317+
// TODO: support resolution?
318+
async import(id: string): Promise<unknown> {
319+
const mod = this.context.rolldown_runtime.moduleCache[id]
320+
assert(mod, `Module not found '${id}'`)
321+
return mod.exports
322+
}
323+
324+
evaluate(code: string) {
325+
const context = {
326+
self: this.context,
327+
...this.context,
328+
}
329+
// TODO: sourcemap not working?
330+
// extract sourcemap
331+
const sourcemap = code.match(/^\/\/# sourceMappingURL=.*/m)?.[0] ?? ''
332+
if (sourcemap) {
333+
code = code.replace(sourcemap, '')
334+
}
335+
code = `\
336+
'use strict';(${Object.keys(context).join(',')})=>{{${code}
337+
// TODO: need to re-expose runtime utilities for now
338+
self.__toCommonJS = __toCommonJS;
339+
self.__export = __export;
340+
self.__toESM = __toESM;
341+
}}
342+
//# sourceMappingSource=rolldown-module-runner
343+
${sourcemap}
344+
`
345+
const fn = (0, eval)(code)
346+
try {
347+
fn(...Object.values(context))
348+
} catch (e) {
349+
console.error('[RolldownModuleRunner:ERROR]', e)
350+
throw e
351+
}
352+
}
353+
}
354+
355+
function patchRuntimePlugin(environment: RolldownEnvironment): rolldown.Plugin {
277356
return {
278357
name: 'vite:rolldown-patch-runtime',
358+
// TODO: external require doesn't work in app format.
359+
// rewrite `require -> _require` and provide _require from module runner.
360+
// for now just rewrite known ones in "react-dom/server".
361+
transform: {
362+
filter: {
363+
code: {
364+
include: [/require\(['"](stream|util)['"]\)/],
365+
},
366+
},
367+
handler(code) {
368+
if (!environment.rolldownDevOptions.ssrModuleRunner) {
369+
return
370+
}
371+
return code.replace(
372+
/require(\(['"](stream|util)['"]\))/g,
373+
'_require($1)',
374+
)
375+
},
376+
},
279377
renderChunk(code) {
280378
// patch rolldown_runtime to workaround a few things
281379
// TODO: is there a robust way to inject code specifically to entry or runtime?
282380
if (code.includes('//#region rolldown:runtime')) {
283-
// TODO: is this magic string heavy?
381+
// TODO: this magic string is heavy
284382
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
288383
output
384+
// replace hard-coded WebSocket setup with custom client
385+
.replace(
386+
/const socket =.*?\n\};/s,
387+
environment.name === 'client' ? getRolldownClientCode() : '',
388+
)
389+
// fix rolldown_runtime.patch
390+
.replace(
391+
'this.executeModuleStack.length > 1',
392+
'this.executeModuleStack.length > 0',
393+
)
289394
.replace('parents: [parent],', 'parents: parent ? [parent] : [],')
395+
.replace(
396+
'if (module.parents.indexOf(parent) === -1) {',
397+
'if (parent && module.parents.indexOf(parent) === -1) {',
398+
)
290399
.replace(
291400
'for (var i = 0; i < module.parents.length; i++) {',
292401
`
293-
if (module.parents.length === 0) {
402+
boundaries.push(moduleId);
403+
invalidModuleIds.push(moduleId);
404+
if (module.parents.filter(Boolean).length === 0) {
294405
__rolldown_hot.send("rolldown:hmr-deadend", { moduleId });
295406
break;
296407
}
297408
for (var i = 0; i < module.parents.length; i++) {`,
298409
)
299-
if (rolldownDevOptions.reactRefresh) {
410+
if (environment.rolldownDevOptions.reactRefresh) {
300411
output.prepend(getReactRefreshRuntimeCode())
301412
}
302413
return {

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

+5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
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+
// TODO: source map and stacktrace
11+
throwError()
12+
}
813
const ssrHtml = ReactDOMServer.renderToString(<App />)
914
res.setHeader('content-type', 'text/html')
1015
// 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: true,
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)