+
Bundling in progress
+
The page will automatically reload when ready.
+
+
+
+
+`
+}
diff --git a/packages/vite/src/node/server/middlewares/memoryFiles.ts b/packages/vite/src/node/server/middlewares/memoryFiles.ts
new file mode 100644
index 00000000000000..76d4bdabb60bea
--- /dev/null
+++ b/packages/vite/src/node/server/middlewares/memoryFiles.ts
@@ -0,0 +1,43 @@
+import type { Connect } from 'dep-types/connect'
+import * as mrmime from 'mrmime'
+import { cleanUrl } from '../../../shared/utils'
+import type { ViteDevServer } from '..'
+import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment'
+
+export function memoryFilesMiddleware(
+ server: ViteDevServer,
+): Connect.NextHandleFunction {
+ const memoryFiles =
+ server.environments.client instanceof FullBundleDevEnvironment
+ ? server.environments.client.memoryFiles
+ : undefined
+ if (!memoryFiles) {
+ throw new Error('memoryFilesMiddleware can only be used for fullBundleMode')
+ }
+ const headers = server.config.server.headers
+
+ return function viteMemoryFilesMiddleware(req, res, next) {
+ const cleanedUrl = cleanUrl(req.url!)
+ if (cleanedUrl.endsWith('.html')) {
+ return next()
+ }
+
+ const pathname = decodeURIComponent(cleanedUrl)
+ const filePath = pathname.slice(1) // remove first /
+
+ const file = memoryFiles.get(filePath)
+ if (file) {
+ const mime = mrmime.lookup(filePath)
+ if (mime) {
+ res.setHeader('Content-Type', mime)
+ }
+
+ for (const name in headers) {
+ res.setHeader(name, headers[name]!)
+ }
+
+ return res.end(file)
+ }
+ next()
+ }
+}
diff --git a/packages/vite/types/hmrPayload.d.ts b/packages/vite/types/hmrPayload.d.ts
index 0cbd649f7279da..8796a6b05cfd79 100644
--- a/packages/vite/types/hmrPayload.d.ts
+++ b/packages/vite/types/hmrPayload.d.ts
@@ -24,6 +24,12 @@ export interface UpdatePayload {
export interface Update {
type: 'js-update' | 'css-update'
+ /**
+ * URL of HMR patch chunk
+ *
+ * This only exists when full-bundle mode is enabled.
+ */
+ url?: string
path: string
acceptedPath: string
timestamp: number
diff --git a/playground/hmr-full-bundle-mode/__tests__/hmr-full-bundle-mode.spec.ts b/playground/hmr-full-bundle-mode/__tests__/hmr-full-bundle-mode.spec.ts
new file mode 100644
index 00000000000000..b2629d786153d6
--- /dev/null
+++ b/playground/hmr-full-bundle-mode/__tests__/hmr-full-bundle-mode.spec.ts
@@ -0,0 +1,123 @@
+import { setTimeout } from 'node:timers/promises'
+import { expect, test } from 'vitest'
+import { editFile, isBuild, page } from '~utils'
+
+if (isBuild) {
+ test('should render', async () => {
+ expect(await page.textContent('h1')).toContain('HMR Full Bundle Mode')
+ await expect.poll(() => page.textContent('.app')).toBe('hello')
+ await expect.poll(() => page.textContent('.hmr')).toBe('hello')
+ })
+} else {
+ // INITIAL -> BUNDLING -> BUNDLED
+ test('show bundling in progress', async () => {
+ const reloadPromise = page.waitForEvent('load')
+ await expect
+ .poll(() => page.textContent('body'))
+ .toContain('Bundling in progress')
+ await reloadPromise // page shown after reload
+ await expect.poll(() => page.textContent('h1')).toBe('HMR Full Bundle Mode')
+ await expect.poll(() => page.textContent('.app')).toBe('hello')
+ })
+
+ // BUNDLED -> GENERATE_HMR_PATCH -> BUNDLING -> BUNDLE_ERROR -> BUNDLING -> BUNDLED
+ test('handle bundle error', async () => {
+ editFile('main.js', (code) =>
+ code.replace("text('.app', 'hello')", "text('.app', 'hello'); text("),
+ )
+ await expect.poll(() => page.isVisible('vite-error-overlay')).toBe(true)
+ editFile('main.js', (code) =>
+ code.replace("text('.app', 'hello'); text(", "text('.app', 'hello')"),
+ )
+ await expect.poll(() => page.isVisible('vite-error-overlay')).toBe(false)
+ await expect.poll(() => page.textContent('.app')).toBe('hello')
+ })
+
+ // BUNDLED -> GENERATE_HMR_PATCH -> BUNDLING -> BUNDLED
+ test('update bundle', async () => {
+ editFile('main.js', (code) =>
+ code.replace("text('.app', 'hello')", "text('.app', 'hello1')"),
+ )
+ await expect.poll(() => page.textContent('.app')).toBe('hello1')
+
+ editFile('main.js', (code) =>
+ code.replace("text('.app', 'hello1')", "text('.app', 'hello')"),
+ )
+ await expect.poll(() => page.textContent('.app')).toBe('hello')
+ })
+
+ // BUNDLED -> GENERATE_HMR_PATCH -> BUNDLING -> BUNDLING -> BUNDLED
+ test('debounce bundle', async () => {
+ editFile('main.js', (code) =>
+ code.replace(
+ "text('.app', 'hello')",
+ "text('.app', 'hello1')\n" + '// @delay-transform',
+ ),
+ )
+ await setTimeout(100)
+ editFile('main.js', (code) =>
+ code.replace("text('.app', 'hello1')", "text('.app', 'hello2')"),
+ )
+ await expect.poll(() => page.textContent('.app')).toBe('hello2')
+
+ editFile('main.js', (code) =>
+ code.replace(
+ "text('.app', 'hello2')\n" + '// @delay-transform',
+ "text('.app', 'hello')",
+ ),
+ )
+ await expect.poll(() => page.textContent('.app')).toBe('hello')
+ })
+
+ // BUNDLED -> GENERATING_HMR_PATCH -> BUNDLED
+ test('handle generate hmr patch error', async () => {
+ await expect.poll(() => page.textContent('.hmr')).toBe('hello')
+ editFile('hmr.js', (code) =>
+ code.replace("const foo = 'hello'", "const foo = 'hello"),
+ )
+ await expect.poll(() => page.isVisible('vite-error-overlay')).toBe(true)
+
+ editFile('hmr.js', (code) =>
+ code.replace("const foo = 'hello", "const foo = 'hello'"),
+ )
+ await expect.poll(() => page.isVisible('vite-error-overlay')).toBe(false)
+ await expect.poll(() => page.textContent('.hmr')).toContain('hello')
+ })
+
+ // BUNDLED -> GENERATING_HMR_PATCH -> BUNDLED
+ test('generate hmr patch', async () => {
+ await expect.poll(() => page.textContent('.hmr')).toBe('hello')
+ editFile('hmr.js', (code) =>
+ code.replace("const foo = 'hello'", "const foo = 'hello1'"),
+ )
+ await expect.poll(() => page.textContent('.hmr')).toBe('hello1')
+
+ editFile('hmr.js', (code) =>
+ code.replace("const foo = 'hello1'", "const foo = 'hello'"),
+ )
+ await expect.poll(() => page.textContent('.hmr')).toContain('hello')
+ })
+
+ // BUNDLED -> GENERATING_HMR_PATCH -> GENERATING_HMR_PATCH -> BUNDLED
+ test('continuous generate hmr patch', async () => {
+ editFile('hmr.js', (code) =>
+ code.replace(
+ "const foo = 'hello'",
+ "const foo = 'hello1'\n" + '// @delay-transform',
+ ),
+ )
+ await setTimeout(100)
+ editFile('hmr.js', (code) =>
+ code.replace("const foo = 'hello1'", "const foo = 'hello2'"),
+ )
+ await expect.poll(() => page.textContent('.hmr')).toBe('hello2')
+
+ editFile('hmr.js', (code) =>
+ code.replace(
+ "const foo = 'hello2'\n" + '// @delay-transform',
+ "const foo = 'hello'",
+ ),
+ )
+ await expect.poll(() => page.textContent('.hmr')).toBe('hello')
+ })
+}
diff --git a/playground/hmr-full-bundle-mode/hmr.js b/playground/hmr-full-bundle-mode/hmr.js
new file mode 100644
index 00000000000000..9f01c0ef741ee6
--- /dev/null
+++ b/playground/hmr-full-bundle-mode/hmr.js
@@ -0,0 +1,13 @@
+export const foo = 'hello'
+
+text('.hmr', foo)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
+
+import.meta.hot?.accept((mod) => {
+ if (mod) {
+ text('.hmr', mod.foo)
+ }
+})
diff --git a/playground/hmr-full-bundle-mode/index.html b/playground/hmr-full-bundle-mode/index.html
new file mode 100644
index 00000000000000..8bb880b25ac710
--- /dev/null
+++ b/playground/hmr-full-bundle-mode/index.html
@@ -0,0 +1,6 @@
+