Skip to content
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
10 changes: 6 additions & 4 deletions packages/vite/src/node/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ interface ScriptAssetsUrl {
}

const htmlProxyRE =
/[?&]html-proxy=?(?:&inline-css)?(?:&style-attr)?&index=(\d+)\.(?:js|css)$/
/[?&]html-proxy=?(?:&inline-css)?(?:&style-attr)?&index=(\d+)(?:&h=[a-z0-9]+)?\.(?:js|css)$/
const isHtmlProxyRE = /[?&]html-proxy\b/

const inlineCSSRE = /__VITE_INLINE_CSS__([a-z\d]{8}_\d+)__/g
Expand Down Expand Up @@ -592,7 +592,9 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
addToHTMLProxyCache(config, filePath, inlineModuleIndex, {
code: contents,
})
js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"`
// include a content hash in the proxy id so that editing the
// inline script is detected as a change by the bundler
js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}&h=${getHash(contents)}.js"`

@h-a-n-a h-a-n-a Aug 18, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the right fix for that is, instead of adding a hash here, on the Rolldown side. A changed file should invalidate every module whose id contains the same file. For example, in vue, foo.vue?vue&type=script and foo.vue?vue&type=template should both has file dependencies of foo.vue. A change to foo.vue will invalidate these modules with queries all together. This is the same counterpart as webpack's fileDependencies and Vite unbundled-dev's fileToModulesMap.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed — the &h= hash is a workaround, not a root fix. Do you plan to fix this scenario on the rolldown side, or is it already supported/fixed there?

@h-a-n-a h-a-n-a Aug 18, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks fine for a workaround 👍. I haven't fixed this. Let me handle this in Rolldown since it might be also related to #22956.

@h-a-n-a h-a-n-a Aug 19, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldRemove = true
}

Expand Down Expand Up @@ -713,7 +715,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
const filePath = id.replace(normalizePath(config.root), '')
addToHTMLProxyCache(config, filePath, inlineModuleIndex, { code })
// will transform with css plugin and cache result with css-post plugin
js += `\nimport "${id}?html-proxy&inline-css&style-attr&index=${inlineModuleIndex}.css"`
js += `\nimport "${id}?html-proxy&inline-css&style-attr&index=${inlineModuleIndex}&h=${getHash(code)}.css"`
const hash = getHash(cleanUrl(id))
// will transform in `applyHtmlTransforms`
overwriteAttrValue(
Expand All @@ -732,7 +734,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
addToHTMLProxyCache(config, filePath, inlineModuleIndex, {
code: styleNode.value,
})
js += `\nimport "${id}?html-proxy&inline-css&index=${inlineModuleIndex}.css"`
js += `\nimport "${id}?html-proxy&inline-css&index=${inlineModuleIndex}&h=${getHash(styleNode.value)}.css"`
const hash = getHash(cleanUrl(id))
// will transform in `applyHtmlTransforms`
s.update(
Expand Down
15 changes: 15 additions & 0 deletions packages/vite/src/node/server/bundledDev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import getEtag from 'etag'
import { ChunkMetadataMap, resolveRolldownOptions } from '../build'
import { BUNDLED_DEV_CLIENT_FILENAME } from '../constants'
import { getHmrImplementation } from '../plugins/clientInjections'
import { isHTMLRequest } from '../plugins/html'
import { createDebugger, formatAndTruncateFileList } from '../utils'
import type { DevEnvironment } from './environment'
import { type NormalizedHotChannelClient, debugHmr, getShortName } from './hmr'
Expand Down Expand Up @@ -195,6 +196,20 @@ export class BundledDev {
if (changedFiles.length === 0) {
return
}
// Edits to an HTML entry may leave its transformed module code
// unchanged (classic inline scripts and markup stay in the html
// output; only module scripts/styles turn into imports), in which
// case rolldown reports Noop updates and nothing would happen.
// Always rebuild and reload on html changes, like the unbundled
// dev server does.
if (changedFiles.some((file) => isHTMLRequest(file))) {
debug?.(`TRIGGER: html entry changed, forcing rebuild`)
this.devEngine.ensureLatestBuildOutput().then(
() => this.debouncedFullReload(),
() => {},
)
return
}
if (updates.every((update) => update.update.type === 'Noop')) {
debug?.(`ignored file change for ${changedFiles.join(', ')}`)
return
Expand Down
46 changes: 46 additions & 0 deletions playground/html/__tests__/html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,52 @@ test('invalidate inline proxy module on reload', async () => {
expect(await page.textContent('.test')).toContain('ok')
})

test.runIf(isServe)(
'editing inline script in html reloads with updated content',
async () => {
await page.goto(viteTestUrl + '/a á.html')
try {
await untilBrowserLogAfter(
() =>
editFile('a á.html', (code) =>
code.replace('special character', 'special character edited'),
),
'special character edited',
)
} finally {
editFile('a á.html', (code) =>
code.replace('special character edited', 'special character'),
)
}
},
)

// TODO: enable for bundledDev once rolldown includes
// https://github.com/rolldown/rolldown/pull/10637 — classic inline scripts stay
// in the html output, and rolldown <1.2.4 drops re-emitted assets with changed
// content from rebuild output, so bundledDev would serve the stale html.
test.skipIf(isBundled)(
'editing classic inline script in html reloads with updated content',
async () => {
await page.goto(`${viteTestUrl}/inline-classic-script.html`)
expect(await page.textContent('.classic-script-content')).toContain(
'classic before',
)
editFile('inline-classic-script.html', (code) =>
code.replace('classic before', 'classic after edit'),
)
try {
await expect
.poll(() => page.textContent('.classic-script-content'))
.toContain('classic after edit')
} finally {
editFile('inline-classic-script.html', (code) =>
code.replace('classic after edit', 'classic before'),
)
}
},
)

test.runIf(isServe)(
'malformed URLs in src attributes should show errors',
async () => {
Expand Down
5 changes: 5 additions & 0 deletions playground/html/inline-classic-script.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="classic-script-content">placeholder</div>
<script>
document.querySelector('.classic-script-content').textContent =
'classic before'
</script>
1 change: 1 addition & 0 deletions playground/html/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const input = {
serveBothFolder: resolve(dirname, 'serve/both/index.html'),
write: resolve(dirname, 'write.html'),
'transform-inline-js': resolve(dirname, 'transform-inline-js.html'),
'inline-classic-script': resolve(dirname, 'inline-classic-script.html'),
malformedUrl: resolve(dirname, 'malformed-url.html'),
// resolved from `process.cwd()` by Rolldown (resolved from `root` by vite's resolver first)
relativeInput: relative(
Expand Down
Loading