Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8cb872e
fix(dev): run closeBundle after buildEnd failure (#23165)
teamleaderleo Aug 21, 2026
6162968
fix(hmr): handle `import.meta.hot.invalidate` in virtual module (#23171)
sapphi-red Aug 21, 2026
67a6807
refactor: remove HmrUrl concept (#23172)
sapphi-red Aug 21, 2026
a500dee
feat(cli): support naming the CPU profile via --profile [name] (#23042)
shulaoda Aug 21, 2026
b78e2f1
feat: support subpath imports in dynamic import statements (#23185)
Rich-Harris Aug 21, 2026
8156684
feat(css): minify style tag (#23183)
sapphi-red Aug 21, 2026
924997a
feat(worker): remove worker chunk if it's detected that it's not refe…
sapphi-red Aug 21, 2026
e17d2d5
feat: add closeServer and closePreviewServer hooks (#23110)
jamesopstad Aug 21, 2026
5164b6a
test(hmr): skip virtual module `import.meta.hot.invalidate` test in b…
sapphi-red Aug 21, 2026
1b5cfe3
feat: accept Rolldown watch options in `server.watch` (#23133)
shulaoda Aug 21, 2026
517b97f
feat: searched params attached to workers are now preserved (#22280)
jurerotar Aug 21, 2026
a6c08e1
refactor: exclude postfix from `__VITE_ASSET__` (#22886)
sapphi-red Aug 21, 2026
4366ac4
feat: use `import.meta.ROLLDOWN_FILE_URL_*` for assets in JS (#22888)
sapphi-red Aug 21, 2026
e38f29e
feat: use `import.meta.ROLLDOWN_FILE_URL_*` for other plugins (#22894)
sapphi-red Aug 21, 2026
ce69ea6
docs: plugin asset emitting (#22898)
sapphi-red Aug 21, 2026
0291408
test: add `renderBuiltUrl` change changes hash (#23118)
sapphi-red Aug 21, 2026
92bd2a7
refactor: use `urlId` of `import.meta.ROLLDOWN_FILE_URL` in wasm plug…
sapphi-red Aug 21, 2026
7519607
test: add asset path replacement in `typeof` test (#23157)
sapphi-red Aug 21, 2026
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: 2 additions & 0 deletions docs/config/server-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ export default defineConfig({

File system watcher options to pass on to [chokidar](https://github.com/paulmillr/chokidar/tree/3.6.0#api).

When bundled-dev mode is enabled, [Rolldown watch options](https://rolldown.rs/reference/InputOptions.watch) (for example, `usePolling`, `pollInterval`, `useDebounce`, `debounceDuration`, `include`, `exclude`) are also accepted. The chokidar-only options are still used by the chokidar watcher, which keeps watching files outside the module graph, such as config file dependencies and env files.

The Vite server watcher watches the `root` and skips the `.git/`, `node_modules/`, `test-results/`, and Vite's `cacheDir` and `build.outDir` directories by default. When updating a watched file, Vite will apply HMR and update the page only if needed.

If set to `null`, no files will be watched. [`server.watcher`](/guide/api-javascript.html#vitedevserver) will provide a compatible event emitter, but calling `add` or `unwatch` will have no effect.
Expand Down
79 changes: 79 additions & 0 deletions docs/guide/api-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,58 @@ Vite plugins can also provide hooks that serve Vite-specific purposes. These hoo
})
```

### `closeServer`

- **Type:** `(context: { reason: 'restart' | 'close' }) => void | Promise<void>`
- **Kind:** `async`, `parallel`
- **Scope:** [Global](/guide/api-environment-plugins#per-environment-hooks-and-global-hooks)

Called when the dev server is restarted or closed, after the server has been torn down. Typically used to dispose resources created in [`configureServer`](/guide/api-plugin.html#configureserver).

The `context.reason` distinguishes the two cases:
- `'restart'`: the server is restarting (e.g. a config file change or a call to `server.restart()`).
- `'close'`: the server is shutting down (e.g. the `q` shortcut, or a call to `server.close()`).

```js
const myPlugin = () => {
let resource
return {
name: 'close-server',
configureServer(server) {
resource = createResource()
},
async closeServer({ reason }) {
if (reason === 'close') {
await resource.dispose()
}
},
}
}
```

### `closePreviewServer`

- **Type:** `() => void | Promise<void>`
- **Kind:** `async`, `parallel`
- **Scope:** [Global](/guide/api-environment-plugins#per-environment-hooks-and-global-hooks)

Same as [`closeServer`](/guide/api-plugin.html#closeserver) but for the preview server. The preview server never restarts, so there is no `reason`.

```js
const myPlugin = () => {
let resource
return {
name: 'close-preview-server',
configurePreviewServer(server) {
resource = createResource()
},
async closePreviewServer() {
await resource.dispose()
},
}
}
```

### `transformIndexHtml`

- **Type:** `IndexHtmlTransformHook | { order?: 'pre' | 'post', handler: IndexHtmlTransformHook }`
Expand Down Expand Up @@ -544,6 +596,33 @@ function outputMetadataPlugin(): Plugin {
}
```

## Referencing Emitted Assets

To emit an asset from a plugin, call [`this.emitFile({ type: 'asset', ... })`](https://rolldown.rs/reference/Interface.PluginContext#in-depth-type-asset). It returns a `referenceId` that you can use to generate the asset's URL, since its final file name isn't known until the bundle is generated.

### In JavaScript

Use `import.meta.ROLLDOWN_FILE_URL_<referenceId>`:

```js
const referenceId = this.emitFile({
type: 'asset',
name: 'icon.png',
source: fileContent,
})

// it's a JavaScript expression, so append any query or hash with string concatenation
return `export default import.meta.ROLLDOWN_FILE_URL_${referenceId} + '#frag'`
```

### In CSS or HTML

`import.meta.ROLLDOWN_FILE_URL_<referenceId>` only works in JavaScript expression position. In CSS or HTML, use the `__VITE_ASSET__<referenceId>__` token instead, appending any query or hash right after it:

```css
background: url(__VITE_ASSET__<referenceId>__#frag);
```

## Plugin Ordering

A Vite plugin can additionally specify an `enforce` property (similar to webpack loaders) to adjust its application order. The value of `enforce` can be either `"pre"` or `"post"`. The resolved plugins will be in the following order:
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ vite [root]
| `-l, --logLevel <level>` | info \| warn \| error \| silent (`string`) |
| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) |
| `--configLoader <loader>` | Use `bundle` to bundle the config with Rolldown, or `runner` (experimental) to process it on the fly, or `native` (experimental) to load using the native runtime (default: `bundle`) |
| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) |
| `--profile [name]` | Start built-in Node.js inspector and write the profile to `<name>.cpuprofile` (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) (`boolean \| string`) |
| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) |
| `-f, --filter <filter>` | Filter debug logs (`string`) |
| `-m, --mode <mode>` | Set env mode (`string`) |
Expand Down Expand Up @@ -67,7 +67,7 @@ vite build [root]
| `-l, --logLevel <level>` | info \| warn \| error \| silent (`string`) |
| `--clearScreen` | Allow/disable clear screen when logging (`boolean`) |
| `--configLoader <loader>` | Use `bundle` to bundle the config with Rolldown, or `runner` (experimental) to process it on the fly, or `native` (experimental) to load using the native runtime (default: `bundle`) |
| `--profile` | Start built-in Node.js inspector (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) |
| `--profile [name]` | Start built-in Node.js inspector and write the profile to `<name>.cpuprofile` (check [Performance bottlenecks](/guide/troubleshooting#performance-bottlenecks)) (`boolean \| string`) |
| `-d, --debug [feat]` | Show debug logs (`string \| boolean`) |
| `-f, --filter <filter>` | Filter debug logs (`string`) |
| `-m, --mode <mode>` | Set env mode (`string`) |
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ Note that variables only represent file names one level deep. If `file` is `'foo

Also note that the dynamic import must match the following rules to be bundled:

- Imports must start with `./` or `../`: ``import(`./dir/${foo}.js`)`` is valid, but ``import(`${foo}.js`)`` is not.
- Imports must start with `./` or `../` or `#`: ``import(`./dir/${foo}.js`)`` is valid, but ``import(`${foo}.js`)`` is not.
- Imports must end with a file extension: ``import(`./dir/${foo}.js`)`` is valid, but ``import(`./dir/${foo}`)`` is not.
- Imports to the own directory must specify a file name pattern: ``import(`./prefix-${foo}.js`)`` is valid, but ``import(`./${foo}.js`)`` is not.

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ vite build --profile
Once your application is opened in the browser, just await finish loading it and then go back to the terminal and press `p` key (will stop the Node.js inspector) then press `q` key to stop the dev server.
:::

Node.js inspector will generate `vite-profile-0.cpuprofile` in the root folder, go to https://www.speedscope.app/, and upload the CPU profile using the `BROWSE` button to inspect the result.
Node.js inspector will generate `vite-profile-0.cpuprofile` in the root folder. You can pass `--profile <name>` (or `--profile=<name>`) to write `<name>.cpuprofile` instead. Go to https://www.speedscope.app/, and upload the CPU profile using the `BROWSE` button to inspect the result.

You can install [vite-plugin-inspect](https://github.com/antfu/vite-plugin-inspect), which lets you inspect the intermediate state of Vite plugins and can also help you to identify which plugins or middlewares are the bottleneck in your applications. The plugin can be used in both dev and build modes. Check the readme file for more details.

Expand Down
21 changes: 16 additions & 5 deletions packages/vite/bin/vite.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg))
const filterIndex = process.argv.findIndex((arg) =>
/^(?:-f|--filter)$/.test(arg),
)
const profileIndex = process.argv.indexOf('--profile')
const profileIndex = process.argv.findIndex(
(arg) => arg === '--profile' || arg.startsWith('--profile='),
)

if (debugIndex > 0) {
let value = process.argv[debugIndex + 1]
Expand Down Expand Up @@ -63,10 +65,19 @@ function start() {
}

if (profileIndex > 0) {
process.argv.splice(profileIndex, 1)
const next = process.argv[profileIndex]
if (next && next[0] !== '-') {
process.argv.splice(profileIndex, 1)
const [profileArg] = process.argv.splice(profileIndex, 1)
// `--profile [name]` writes the profile to `<name>.cpuprofile`. The value is
// optional and consumed like cac does for other `[optional]` value flags.
let profileName = profileArg.slice('--profile='.length)
if (!profileName) {
const next = process.argv[profileIndex]
if (next && next[0] !== '-') {
process.argv.splice(profileIndex, 1)
profileName = next
}
}
if (profileName) {
global.__vite_profile_name = profileName
}
const inspector = await import('node:inspector').then((r) => r.default)
const session = (global.__vite_profile_session = new inspector.Session())
Expand Down
8 changes: 7 additions & 1 deletion packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ErrorPayload, HotPayload } from '#types/hmrPayload'
import type { ViteHotContext } from '#types/hot'
import { HMRClient, HMRContext } from '../shared/hmr'
import { wrapId } from '../shared/utils'
import {
createWebSocketModuleRunnerTransport,
normalizeModuleRunnerTransport,
Expand Down Expand Up @@ -139,6 +140,10 @@ const debounceReload = (time: number) => {
}
export const pageReload = debounceReload(20)

function wrapIdIfNeeded(id: string): string {
return id[0] === '.' || id[0] === '/' ? id : wrapId(id)
}

const hmrClient = new HMRClient(
{
error: (err) => console.error('[vite]', err),
Expand All @@ -152,10 +157,11 @@ const hmrClient = new HMRClient(
isWithinCircularImport,
}) {
const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
const browserPath = wrapIdIfNeeded(acceptedPathWithoutQuery)
const importPromise = import(
/* @vite-ignore */
base +
acceptedPathWithoutQuery.slice(1) +
browserPath.slice(1) +
`?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
query ? `&${query}` : ''
}`
Expand Down
5 changes: 1 addition & 4 deletions packages/vite/src/module-runner/hmrHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { HotPayload } from '#types/hmrPayload'
import { slash, unwrapId } from '../shared/utils'
import { slash } from '../shared/utils'
import { ERR_OUTDATED_OPTIMIZED_DEP } from '../shared/constants'
import { createHMRHandler } from '../shared/hmrHandler'
import type { ModuleRunner } from './runner'
Expand All @@ -19,9 +19,6 @@ export function createHMRHandlerForRunner(
await Promise.all(
payload.updates.map(async (update): Promise<void> => {
if (update.type === 'js-update') {
// runner always caches modules by their full path without /@id/ prefix
update.acceptedPath = unwrapId(update.acceptedPath)
update.path = unwrapId(update.path)
return hmrClient.queueUpdate(update)
}

Expand Down
66 changes: 66 additions & 0 deletions packages/vite/src/node/__tests__/build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,34 @@ describe('build', () => {
assertOutputHashContentChange(result[0], result[1])
})

test('file hash should change when renderBuiltUrl changes', async () => {
const createRenderBuiltUrl = (base: string) => (filename: string) =>
`${base}/${filename}`
const renderBuiltUrlA = createRenderBuiltUrl('/cdn-a')
const renderBuiltUrlB = createRenderBuiltUrl('/cdn-b')

expect(renderBuiltUrlA.toString()).toBe(renderBuiltUrlB.toString())

const result = await Promise.all([
buildProjectWithRenderBuiltUrl(renderBuiltUrlA),
buildProjectWithRenderBuiltUrl(renderBuiltUrlB),
])

expect(getOutputHashChanges(result[0], result[1])).toMatchInlineSnapshot(`
{
"changed": [
"index",
],
"unchanged": [
"_subentry",
"asset.txt",
"undefined",
],
}
`)
assertOutputHashContentChange(result[0], result[1])
})

test('top-level input is used as the default build entry', async () => {
const result = (await build({
root: resolve(dirname, 'packages/build-project'),
Expand Down Expand Up @@ -1512,6 +1540,44 @@ test('copies public directory after building same environment with write false f
).resolves.toBe('<svg></svg>')
})

async function buildProjectWithRenderBuiltUrl(
renderBuiltUrl: (filename: string) => string,
) {
return (await build({
root: resolve(dirname, 'packages/build-project'),
logLevel: 'silent',
build: {
write: false,
assetsInlineLimit: 0,
},
experimental: {
renderBuiltUrl,
},
plugins: [
{
name: 'test',
resolveId(id) {
if (id === 'entry.js' || id === 'subentry.js') {
return '\0' + id
}
},
load(id) {
if (id === '\0entry.js') {
return `
import assetUrl from '/asset.txt?url'
console.log(assetUrl)
window.addEventListener('click', () => { import('subentry.js') })
`
}
if (id === '\0subentry.js') {
return `export default 'subentry'`
}
},
},
],
})) as RolldownOutput
}

/**
* for each chunks in output1, if there's a chunk in output2 with the same fileName,
* ensure that the chunk code is the same. if not, the chunk hash should have changed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
asset
Loading
Loading