Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: use rolldown rebuild #7

Open
wants to merge 9 commits into
base: refactor-rolldown-hmr
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"esbuild": "^0.24.0",
"postcss": "^8.4.49",
"react-refresh": "^0.14.2",
"rolldown": "0.15.0",
"rolldown": "./rolldown-0.15.0-b.tgz",
"rollup": "^4.23.0"
},
"optionalDependencies": {
Expand Down
Binary file added packages/vite/rolldown-0.15.0-b.tgz
Binary file not shown.
150 changes: 67 additions & 83 deletions packages/vite/src/node/server/environments/rolldown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { createRequire } from 'node:module'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import MagicString from 'magic-string'
import * as rolldown from 'rolldown'
import type * as rolldown from 'rolldown'
import * as rolldownExperimental from 'rolldown/experimental'
import sirv from 'sirv'
import { createLogger } from '../../publicUtils'
import { DevEnvironment } from '../environment'
Expand All @@ -19,7 +20,8 @@ import type {
import { CLIENT_ENTRY, VITE_PACKAGE_DIR } from '../../constants'
import { injectEnvironmentToHooks } from '../../build'
import { cleanUrl } from '../../../shared/utils'
import { generatedAssetsMap } from '../../plugins/asset'
import { combineSourcemaps } from '../../utils'
import { genSourceMapUrl } from '../sourcemap'

const require = createRequire(import.meta.url)

Expand Down Expand Up @@ -135,7 +137,7 @@ export async function rolldownDevHandleHotUpdate(
//

class RolldownEnvironment extends DevEnvironment {
instance!: rolldown.RolldownBuild
instance!: Awaited<ReturnType<typeof rolldownExperimental.rebuild>>
result!: rolldown.RolldownOutput
outDir!: string
buildTimestamp = Date.now()
Expand Down Expand Up @@ -186,8 +188,6 @@ class RolldownEnvironment extends DevEnvironment {
return
}

await this.instance?.close()

if (this.config.build.emptyOutDir !== false) {
fs.rmSync(this.outDir, { recursive: true, force: true })
}
Expand Down Expand Up @@ -239,7 +239,6 @@ class RolldownEnvironment extends DevEnvironment {
// resolveNewUrlToAsset: true,
// },
}
this.instance = await rolldown.rolldown(this.inputOptions)

const format: rolldown.ModuleFormat =
this.name === 'client' || this.rolldownDevOptions.ssrModuleRunner
Expand All @@ -257,75 +256,52 @@ class RolldownEnvironment extends DevEnvironment {
? `import __nodeModule from "node:module"; const require = __nodeModule.createRequire(import.meta.url);`
: undefined,
}
// `generate` should work but we use `write` so it's easier to see output and debug
this.result = await this.instance.write(this.outputOptions)

// find changed assets
const changedAssets: string[] = []
for (const [id, { content }] of generatedAssetsMap.get(this) ?? []) {
if (content) {
const data = content.toString('utf8')
if (this.lastAssets[id] !== data) {
changedAssets.push(id)
}
this.lastAssets[id] = data
}
}
// detect change of content of assert url placeholder __VITE_ASSET__xxx
const changedAssetsRegex = new RegExp(
// eslint-disable-next-line
`__VITE_ASSET__(${changedAssets.join('|')})__`,
)

// extract hmr chunk
// cf. https://github.com/web-infra-dev/rspack/blob/5a967f7a10ec51171a304a1ce8d741bd09fa8ed5/crates/rspack_plugin_hmr/src/lib.rs#L60
const chunk = this.result.output[0]
this.newModules = {}
const modules: Record<string, string | null> = {}
for (const [id, mod] of Object.entries(chunk.modules)) {
const current = mod.code
const last = this.lastModules?.[id]
if (current?.match(changedAssetsRegex)) {
// TODO:
// need to replace __VITE_ASSET__xxx
// we should property run `renderChunk` to hmr chunk too
this.newModules[id] = current
}
if (current !== last) {
this.newModules[id] = current
}
modules[id] = current
}
this.lastModules = modules
this.fileModuleIds = new Set(chunk.moduleIds.map((id) => cleanUrl(id)))
this.instance = await rolldownExperimental.rebuild({
...this.inputOptions,
output: this.outputOptions,
})
this.result = await this.instance.build()
this.fileModuleIds = new Set(
this.result.output[0].moduleIds.map((id) => cleanUrl(id)),
)

this.buildTimestamp = Date.now()
console.timeEnd(`[rolldown:${this.name}:build]`)
}

async buildHmr(file: string) {
logger.info(`hmr '${file}'`, { timestamp: true })
await this.build()
const stableIds: string[] = []
let innerCode = ''
for (const [id, code] of Object.entries(this.newModules)) {
const stableId = path.relative(this.config.root, id)
stableIds.push(stableId)
innerCode += `\
rolldown_runtime.define(${JSON.stringify(stableId)},function(require, module, exports){
${code}
});
`
console.time(`[rolldown:${this.name}:rebuild]`)
const result = await this.instance.build()
this.fileModuleIds = new Set(
this.result.output[0].moduleIds.map((id) => cleanUrl(id)),
)
console.timeEnd(`[rolldown:${this.name}:rebuild]`)
const chunk = result.output.find(
(v) => v.type === 'chunk' && v.name === 'hmr-update',
)
const updatePath = path.join(this.outDir, 'hmr-update.js')
if (!chunk) {
return [updatePath, '']
}
const output = `\
self.rolldown_runtime.patch(${JSON.stringify(stableIds)}, function(){
${innerCode}
});
`
// dump for debugging
const updatePath = path.join(this.outDir, `hmr-update-${Date.now()}.js`)
fs.writeFileSync(updatePath, output)
return [updatePath, output]
assert(chunk.type === 'chunk')
const { output, stableIds } = patchIsolatedModuleChunk(chunk.code)
output.prepend(
`self.rolldown_runtime.patch(${JSON.stringify(stableIds)}, function(){\n`,
)
output.append('\n});')
let outputString = output.toString()
if (chunk.map) {
// collapse sourcemap
const map = combineSourcemaps(chunk.fileName, [
output.generateMap({ hires: 'boundary' }) as any,
chunk.map as any,
])
outputString = outputString.replace(/^\/\/# sourceMappingURL=.*/gm, '')
outputString += `\n//# sourceMappingURL=${genSourceMapUrl(map as any)}`
}
return [updatePath, outputString]
}

async handleUpdate(ctx: HmrContext): Promise<void> {
Expand Down Expand Up @@ -382,6 +358,27 @@ ${innerCode}
}
}

function patchIsolatedModuleChunk(code: string) {
// silly but we can do `render_app` on our own for now.
// extract isolated module between #region and #endregion then wrap by rolldown_runtime.define.
// https://github.com/rolldown/rolldown/blob/a29240168290e45b36fdc1a6d5c375281fb8dc3e/crates/rolldown/src/ecmascript/format/app.rs#L28-L55
const output = new MagicString(code)
const matches = code.matchAll(/^\/\/#region (.*)$/gm)
const stableIds: string[] = []
for (const match of matches) {
const stableId = match[1]!
stableIds.push(stableId)
const start = match.index!
const end = code.indexOf('//#endregion', match.index)
output.appendLeft(
start,
`rolldown_runtime.define(${JSON.stringify(stableId)},function(require, module, exports){\n\n`,
)
output.appendRight(end, `\n\n});\n`)
}
return { output, stableIds }
}

class RolldownModuleRunner {
// intercept globals
private context = {
Expand Down Expand Up @@ -455,24 +452,11 @@ function patchRuntimePlugin(environment: RolldownEnvironment): rolldown.Plugin {
},
},
renderChunk(code, chunk) {
// TODO: this magic string is heavy

// silly but we can do `render_app` on our own for now
// https://github.com/rolldown/rolldown/blob/a29240168290e45b36fdc1a6d5c375281fb8dc3e/crates/rolldown/src/ecmascript/format/app.rs#L28-L55
const output = new MagicString(code)

// extract isolated module between #region and #endregion
const matches = code.matchAll(/^\/\/#region (.*)$/gm)
for (const match of matches) {
const stableId = match[1]!
const start = match.index!
const end = code.indexOf('//#endregion', match.index)
output.appendLeft(
start,
`rolldown_runtime.define(${JSON.stringify(stableId)},function(require, module, exports){\n\n`,
)
output.appendRight(end, `\n\n});\n`)
if (!chunk.isEntry) {
return
}
// TODO: this magic string is heavy
const { output } = patchIsolatedModuleChunk(code)
assert(chunk.facadeModuleId)
const stableId = path.relative(
environment.config.root,
Expand Down
27 changes: 26 additions & 1 deletion playground/rolldown-dev-react/__tests__/basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from 'vitest'
import { editFile, isBuild, page, viteTestUrl } from '../../test-utils'
import { addFile, editFile, isBuild, page, viteTestUrl } from '../../test-utils'

test('basic', async () => {
await page.getByRole('button', { name: 'Count: 0' }).click()
Expand Down Expand Up @@ -66,4 +66,29 @@ test.runIf(!isBuild)('hmr css', async () => {
)
.toBe('rgb(0, 128, 0)')
await page.getByRole('button', { name: 'Count: 2' }).click()

editFile('./src/test-style-url.css', (s) => s.replace('orange', 'red'))
await expect
.poll(() =>
page
.locator('.test-style-url')
.evaluate((el) => getComputedStyle(el).color),
)
.toBe('rgb(255, 0, 0)')
await page.getByRole('button', { name: 'Count: 3' }).click()
})

test.runIf(!isBuild)('hmr new file', async () => {
await page.goto(viteTestUrl)
await page.getByRole('button', { name: 'Count: 0' }).click()

addFile('./src/new-file.ts', 'export default "[new-file:ok]"')
editFile(
'./src/app.tsx',
(s) =>
'import newFile from "./new-file";\n' +
s.replace('Count:', 'Count-{newFile}:'),
)

await page.getByRole('button', { name: 'Count-[new-file:ok]: 1' }).click()
})
1 change: 0 additions & 1 deletion playground/rolldown-dev-react/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import testAlias from 'test-alias'
import { throwError } from './error'
import './test-style.css'
import testStyleInline from './test-style-inline.css?inline'
// TODO: hmr for url assets?
import testStyleUrl from './test-style-url.css?url'

// TODO: isolating finalizer doesn't rewrite yet
Expand Down
11 changes: 6 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading