From a71d5c8ac088553261bec93417587fd94d1800d2 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 27 May 2025 12:37:29 +0900 Subject: [PATCH 01/35] feat: add experimental.fullBundleMode --- packages/vite/src/node/config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index e010ea910827ad..b636a6970d23c4 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -551,6 +551,13 @@ export interface ExperimentalOptions { * @default 'v1' */ enableNativePlugin?: boolean | 'resolver' | 'v1' + /** + * Enable full bundle mode in dev. + * + * @experimental + * @default false + */ + fullBundleMode?: boolean } export interface LegacyOptions { @@ -754,6 +761,7 @@ export const configDefaults = Object.freeze({ renderBuiltUrl: undefined, hmrPartialAccept: false, enableNativePlugin: process.env._VITE_TEST_JS_PLUGIN ? false : 'v1', + fullBundleMode: false, }, future: { removePluginHookHandleHotUpdate: undefined, From c44e95b0008b9b4b657714ddaf0a4faa345e575e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 27 May 2025 12:40:57 +0900 Subject: [PATCH 02/35] feat: add `--fullBundleMode` flag for `vite dev` --- packages/vite/src/node/cli.ts | 168 +++++++++++++++++++--------------- 1 file changed, 92 insertions(+), 76 deletions(-) diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 86dcae3e1870e7..2461a639aed1e7 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -53,6 +53,10 @@ interface GlobalCLIOptions { w?: boolean } +interface ExperimentalDevOptions { + fullBundleMode?: boolean +} + interface BuilderCLIOptions { app?: boolean } @@ -195,93 +199,105 @@ cli '--force', `[boolean] force the optimizer to ignore the cache and re-bundle`, ) - .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => { - filterDuplicateOptions(options) - // output structure is preserved even after bundling so require() - // is ok here - const { createServer } = await import('./server') - try { - const server = await createServer({ - root, - base: options.base, - mode: options.mode, - configFile: options.config, - configLoader: options.configLoader, - logLevel: options.logLevel, - clearScreen: options.clearScreen, - server: cleanGlobalCLIOptions(options), - forceOptimizeDeps: options.force, - }) + .option('--fullBundleMode', `[boolean] use experimental full bundle mode`) + .action( + async ( + root: string, + options: ServerOptions & ExperimentalDevOptions & GlobalCLIOptions, + ) => { + filterDuplicateOptions(options) + // output structure is preserved even after bundling so require() + // is ok here + const { createServer } = await import('./server') + try { + const server = await createServer({ + root, + base: options.base, + mode: options.mode, + configFile: options.config, + configLoader: options.configLoader, + logLevel: options.logLevel, + clearScreen: options.clearScreen, + server: cleanGlobalCLIOptions(options), + forceOptimizeDeps: options.force, + experimental: { + fullBundleMode: options.fullBundleMode, + }, + }) - if (!server.httpServer) { - throw new Error('HTTP server not available') - } + if (!server.httpServer) { + throw new Error('HTTP server not available') + } - await server.listen() + await server.listen() - const info = server.config.logger.info + const info = server.config.logger.info - const modeString = - options.mode && options.mode !== 'development' - ? ` ${colors.bgGreen(` ${colors.bold(options.mode)} `)}` + const modeString = + options.mode && options.mode !== 'development' + ? ` ${colors.bgGreen(` ${colors.bold(options.mode)} `)}` + : '' + const viteStartTime = global.__vite_start_time ?? false + const startupDurationString = viteStartTime + ? colors.dim( + `ready in ${colors.reset( + colors.bold(Math.ceil(performance.now() - viteStartTime)), + )} ms`, + ) : '' - const viteStartTime = global.__vite_start_time ?? false - const startupDurationString = viteStartTime - ? colors.dim( - `ready in ${colors.reset( - colors.bold(Math.ceil(performance.now() - viteStartTime)), - )} ms`, - ) - : '' - const hasExistingLogs = - process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0 + const hasExistingLogs = + process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0 - info( - `\n ${colors.green( - `${colors.bold('ROLLDOWN-VITE')} v${VERSION}`, - )}${modeString} ${startupDurationString}\n`, - { - clear: !hasExistingLogs, - }, - ) + info( + `\n ${colors.green( + `${colors.bold('ROLLDOWN-VITE')} v${VERSION}`, + )}${modeString} ${startupDurationString}\n`, + { + clear: !hasExistingLogs, + }, + ) - server.printUrls() - const customShortcuts: CLIShortcut[] = [] - if (profileSession) { - customShortcuts.push({ - key: 'p', - description: 'start/stop the profiler', - async action(server) { - if (profileSession) { - await stopProfiler(server.config.logger.info) - } else { - const inspector = await import('node:inspector').then( - (r) => r.default, - ) - await new Promise((res) => { - profileSession = new inspector.Session() - profileSession.connect() - profileSession.post('Profiler.enable', () => { - profileSession!.post('Profiler.start', () => { - server.config.logger.info('Profiler started') - res() + server.printUrls() + const customShortcuts: CLIShortcut[] = [] + if (profileSession) { + customShortcuts.push({ + key: 'p', + description: 'start/stop the profiler', + async action(server) { + if (profileSession) { + await stopProfiler(server.config.logger.info) + } else { + const inspector = await import('node:inspector').then( + (r) => r.default, + ) + await new Promise((res) => { + profileSession = new inspector.Session() + profileSession.connect() + profileSession.post('Profiler.enable', () => { + profileSession!.post('Profiler.start', () => { + server.config.logger.info('Profiler started') + res() + }) }) }) - }) - } + } + }, + }) + } + server.bindCLIShortcuts({ print: true, customShortcuts }) + } catch (e) { + const logger = createLogger(options.logLevel) + logger.error( + colors.red(`error when starting dev server:\n${e.stack}`), + { + error: e, }, - }) + ) + stopProfiler(logger.info) + process.exit(1) } - server.bindCLIShortcuts({ print: true, customShortcuts }) - } catch (e) { - const logger = createLogger(options.logLevel) - logger.error(colors.red(`error when starting dev server:\n${e.stack}`), { - error: e, - }) - stopProfiler(logger.info) - process.exit(1) - } - }) + }, + ) // build cli From 8308b98a11c4847865128e1ae37d1639b6600de6 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 27 May 2025 13:57:28 +0900 Subject: [PATCH 03/35] feat: add `ResolvedConfig.isBundled` --- packages/vite/src/node/config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index b636a6970d23c4..7d60c169360afa 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -625,6 +625,8 @@ export interface ResolvedConfig cacheDir: string command: 'build' | 'serve' mode: string + /** `true` when build or full-bundle mode dev */ + isBundled: boolean isWorker: boolean // in nested worker bundle to find the main config /** @internal */ @@ -1788,6 +1790,7 @@ export async function resolveConfig( cacheDir, command, mode, + isBundled: config.experimental?.fullBundleMode || isBuild, isWorker: false, mainConfig: null, bundleChain: [], From 84cfb35264302c80dc3c98c27f75f1f35ff480fb Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:15:14 +0900 Subject: [PATCH 04/35] feat: disable minify by default in development --- .../vite/src/node/__tests__/build.spec.ts | 9 ++- packages/vite/src/node/build.ts | 3 +- packages/vite/src/node/config.ts | 64 ++++++++++--------- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index b5e366ec00f24b..26b6fc9dfde6b4 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url' import { stripVTControlCharacters } from 'node:util' import fsp from 'node:fs/promises' import colors from 'picocolors' -import { afterEach, describe, expect, test, vi } from 'vitest' +import { afterEach, describe, expect, onTestFinished, test, vi } from 'vitest' import type { LogLevel, OutputChunk, @@ -809,6 +809,13 @@ test('default sharedConfigBuild true on build api', async () => { test.for([true, false])( 'minify per environment (builder.sharedPlugins: %s)', async (sharedPlugins) => { + const _nodeEnv = process.env.NODE_ENV + // Overriding the NODE_ENV set by vitest + process.env.NODE_ENV = '' + onTestFinished(() => { + process.env.NODE_ENV = _nodeEnv + }) + const root = resolve(__dirname, 'fixtures/shared-plugins/minify') const builder = await createBuilder({ root, diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 004b848e54271b..c38546be4ca6e5 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -407,6 +407,7 @@ export function resolveBuildEnvironmentOptions( raw: BuildEnvironmentOptions, logger: Logger, consumer: 'client' | 'server' | undefined, + isProduction: boolean, ): ResolvedBuildEnvironmentOptions { const deprecatedPolyfillModulePreload = raw.polyfillModulePreload const { polyfillModulePreload, ...rest } = raw @@ -427,7 +428,7 @@ export function resolveBuildEnvironmentOptions( { ...buildEnvironmentOptionsDefaults, cssCodeSplit: !raw.lib, - minify: consumer === 'server' ? false : 'oxc', + minify: !isProduction || consumer === 'server' ? false : 'oxc', rollupOptions: {}, rolldownOptions: undefined, ssr: consumer === 'server', diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 7d60c169360afa..ec93b0235b165a 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -845,6 +845,7 @@ function resolveEnvironmentOptions( preserveSymlinks: boolean, forceOptimizeDeps: boolean | undefined, logger: Logger, + isProduction: boolean, environmentName: string, // Backward compatibility isSsrTargetWebworkerSet?: boolean, @@ -908,6 +909,7 @@ function resolveEnvironmentOptions( options.build ?? {}, logger, consumer, + isProduction, ), plugins: undefined!, // to be resolved later // will be set by `setOptimizeDepsPluginNames` later @@ -1495,6 +1497,36 @@ export async function resolveConfig( config.ssr?.target === 'webworker', ) + // load .env files + // Backward compatibility: set envDir to false when envFile is false + let envDir = config.envFile === false ? false : config.envDir + if (envDir !== false) { + envDir = config.envDir + ? normalizePath(path.resolve(resolvedRoot, config.envDir)) + : resolvedRoot + } + + const userEnv = loadEnv(mode, envDir, resolveEnvPrefix(config)) + + // Note it is possible for user to have a custom mode, e.g. `staging` where + // development-like behavior is expected. This is indicated by NODE_ENV=development + // loaded from `.staging.env` and set by us as VITE_USER_NODE_ENV + const userNodeEnv = process.env.VITE_USER_NODE_ENV + if (!isNodeEnvSet && userNodeEnv) { + if (userNodeEnv === 'development') { + process.env.NODE_ENV = 'development' + } else { + // NODE_ENV=production is not supported as it could break HMR in dev for frameworks like Vue + logger.warn( + `NODE_ENV=${userNodeEnv} is not supported in the .env file. ` + + `Only NODE_ENV=development is supported to create a development build of your project. ` + + `If you need to set process.env.NODE_ENV, you can set it in the Vite config instead.`, + ) + } + } + + const isProduction = process.env.NODE_ENV === 'production' + // Backward compatibility: merge config.environments.client.resolve back into config.resolve config.resolve ??= {} config.resolve.conditions = config.environments.client.resolve?.conditions @@ -1510,6 +1542,7 @@ export async function resolveConfig( resolvedDefaultResolve.preserveSymlinks, inlineConfig.forceOptimizeDeps, logger, + isProduction, environmentName, config.ssr?.target === 'webworker', config.server?.preTransformRequests, @@ -1534,6 +1567,7 @@ export async function resolveConfig( config.build ?? {}, logger, undefined, + isProduction, ) // Backward compatibility: merge config.environments.ssr back into config.ssr @@ -1554,36 +1588,6 @@ export async function resolveConfig( resolvedDefaultResolve.preserveSymlinks, ) - // load .env files - // Backward compatibility: set envDir to false when envFile is false - let envDir = config.envFile === false ? false : config.envDir - if (envDir !== false) { - envDir = config.envDir - ? normalizePath(path.resolve(resolvedRoot, config.envDir)) - : resolvedRoot - } - - const userEnv = loadEnv(mode, envDir, resolveEnvPrefix(config)) - - // Note it is possible for user to have a custom mode, e.g. `staging` where - // development-like behavior is expected. This is indicated by NODE_ENV=development - // loaded from `.staging.env` and set by us as VITE_USER_NODE_ENV - const userNodeEnv = process.env.VITE_USER_NODE_ENV - if (!isNodeEnvSet && userNodeEnv) { - if (userNodeEnv === 'development') { - process.env.NODE_ENV = 'development' - } else { - // NODE_ENV=production is not supported as it could break HMR in dev for frameworks like Vue - logger.warn( - `NODE_ENV=${userNodeEnv} is not supported in the .env file. ` + - `Only NODE_ENV=development is supported to create a development build of your project. ` + - `If you need to set process.env.NODE_ENV, you can set it in the Vite config instead.`, - ) - } - } - - const isProduction = process.env.NODE_ENV === 'production' - // resolve public base url const relativeBaseShortcut = config.base === '' || config.base === './' From 80547ba5f5cc6a9a92eb2d1fbabd4b1e5bf43550 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:16:22 +0900 Subject: [PATCH 05/35] feat: disable json minify by default in development --- packages/vite/src/node/plugins/index.ts | 2 +- packages/vite/src/node/plugins/json.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index a0cd0d2d80f5e2..6f863407850c32 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -105,7 +105,7 @@ export async function resolvePlugins( cssPlugin(config), esbuildBannerFooterCompatPlugin(config), config.oxc !== false ? oxcPlugin(config) : null, - jsonPlugin(config.json, isBuild, enableNativePluginV1), + jsonPlugin(config.json, config.isProduction, enableNativePluginV1), wasmHelperPlugin(config), webWorkerPlugin(config), assetPlugin(config), diff --git a/packages/vite/src/node/plugins/json.ts b/packages/vite/src/node/plugins/json.ts index a578f37b4189cc..918c6cde7f1474 100644 --- a/packages/vite/src/node/plugins/json.ts +++ b/packages/vite/src/node/plugins/json.ts @@ -40,11 +40,11 @@ export const isJSONRequest = (request: string): boolean => export function jsonPlugin( options: Required, - isBuild: boolean, + minify: boolean, enableNativePlugin: boolean, ): Plugin { if (enableNativePlugin) { - return nativeJsonPlugin({ ...options, minify: isBuild }) + return nativeJsonPlugin({ ...options, minify }) } return { @@ -101,7 +101,7 @@ export function jsonPlugin( ) { // during build, parse then double-stringify to remove all // unnecessary whitespaces to reduce bundle size. - if (isBuild) { + if (minify) { json = JSON.stringify(JSON.parse(json)) } From da7bf682e19c6334cc17e3e57ecafea7807b4389 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:40:17 +0900 Subject: [PATCH 06/35] Revert "feat: disable minify by default in development" This reverts commit 2af81b7c4aaead8a022a40d7368aaaf2c931129f. --- .../vite/src/node/__tests__/build.spec.ts | 9 +-- packages/vite/src/node/build.ts | 3 +- packages/vite/src/node/config.ts | 64 +++++++++---------- 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 26b6fc9dfde6b4..b5e366ec00f24b 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url' import { stripVTControlCharacters } from 'node:util' import fsp from 'node:fs/promises' import colors from 'picocolors' -import { afterEach, describe, expect, onTestFinished, test, vi } from 'vitest' +import { afterEach, describe, expect, test, vi } from 'vitest' import type { LogLevel, OutputChunk, @@ -809,13 +809,6 @@ test('default sharedConfigBuild true on build api', async () => { test.for([true, false])( 'minify per environment (builder.sharedPlugins: %s)', async (sharedPlugins) => { - const _nodeEnv = process.env.NODE_ENV - // Overriding the NODE_ENV set by vitest - process.env.NODE_ENV = '' - onTestFinished(() => { - process.env.NODE_ENV = _nodeEnv - }) - const root = resolve(__dirname, 'fixtures/shared-plugins/minify') const builder = await createBuilder({ root, diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index c38546be4ca6e5..004b848e54271b 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -407,7 +407,6 @@ export function resolveBuildEnvironmentOptions( raw: BuildEnvironmentOptions, logger: Logger, consumer: 'client' | 'server' | undefined, - isProduction: boolean, ): ResolvedBuildEnvironmentOptions { const deprecatedPolyfillModulePreload = raw.polyfillModulePreload const { polyfillModulePreload, ...rest } = raw @@ -428,7 +427,7 @@ export function resolveBuildEnvironmentOptions( { ...buildEnvironmentOptionsDefaults, cssCodeSplit: !raw.lib, - minify: !isProduction || consumer === 'server' ? false : 'oxc', + minify: consumer === 'server' ? false : 'oxc', rollupOptions: {}, rolldownOptions: undefined, ssr: consumer === 'server', diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index ec93b0235b165a..7d60c169360afa 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -845,7 +845,6 @@ function resolveEnvironmentOptions( preserveSymlinks: boolean, forceOptimizeDeps: boolean | undefined, logger: Logger, - isProduction: boolean, environmentName: string, // Backward compatibility isSsrTargetWebworkerSet?: boolean, @@ -909,7 +908,6 @@ function resolveEnvironmentOptions( options.build ?? {}, logger, consumer, - isProduction, ), plugins: undefined!, // to be resolved later // will be set by `setOptimizeDepsPluginNames` later @@ -1497,36 +1495,6 @@ export async function resolveConfig( config.ssr?.target === 'webworker', ) - // load .env files - // Backward compatibility: set envDir to false when envFile is false - let envDir = config.envFile === false ? false : config.envDir - if (envDir !== false) { - envDir = config.envDir - ? normalizePath(path.resolve(resolvedRoot, config.envDir)) - : resolvedRoot - } - - const userEnv = loadEnv(mode, envDir, resolveEnvPrefix(config)) - - // Note it is possible for user to have a custom mode, e.g. `staging` where - // development-like behavior is expected. This is indicated by NODE_ENV=development - // loaded from `.staging.env` and set by us as VITE_USER_NODE_ENV - const userNodeEnv = process.env.VITE_USER_NODE_ENV - if (!isNodeEnvSet && userNodeEnv) { - if (userNodeEnv === 'development') { - process.env.NODE_ENV = 'development' - } else { - // NODE_ENV=production is not supported as it could break HMR in dev for frameworks like Vue - logger.warn( - `NODE_ENV=${userNodeEnv} is not supported in the .env file. ` + - `Only NODE_ENV=development is supported to create a development build of your project. ` + - `If you need to set process.env.NODE_ENV, you can set it in the Vite config instead.`, - ) - } - } - - const isProduction = process.env.NODE_ENV === 'production' - // Backward compatibility: merge config.environments.client.resolve back into config.resolve config.resolve ??= {} config.resolve.conditions = config.environments.client.resolve?.conditions @@ -1542,7 +1510,6 @@ export async function resolveConfig( resolvedDefaultResolve.preserveSymlinks, inlineConfig.forceOptimizeDeps, logger, - isProduction, environmentName, config.ssr?.target === 'webworker', config.server?.preTransformRequests, @@ -1567,7 +1534,6 @@ export async function resolveConfig( config.build ?? {}, logger, undefined, - isProduction, ) // Backward compatibility: merge config.environments.ssr back into config.ssr @@ -1588,6 +1554,36 @@ export async function resolveConfig( resolvedDefaultResolve.preserveSymlinks, ) + // load .env files + // Backward compatibility: set envDir to false when envFile is false + let envDir = config.envFile === false ? false : config.envDir + if (envDir !== false) { + envDir = config.envDir + ? normalizePath(path.resolve(resolvedRoot, config.envDir)) + : resolvedRoot + } + + const userEnv = loadEnv(mode, envDir, resolveEnvPrefix(config)) + + // Note it is possible for user to have a custom mode, e.g. `staging` where + // development-like behavior is expected. This is indicated by NODE_ENV=development + // loaded from `.staging.env` and set by us as VITE_USER_NODE_ENV + const userNodeEnv = process.env.VITE_USER_NODE_ENV + if (!isNodeEnvSet && userNodeEnv) { + if (userNodeEnv === 'development') { + process.env.NODE_ENV = 'development' + } else { + // NODE_ENV=production is not supported as it could break HMR in dev for frameworks like Vue + logger.warn( + `NODE_ENV=${userNodeEnv} is not supported in the .env file. ` + + `Only NODE_ENV=development is supported to create a development build of your project. ` + + `If you need to set process.env.NODE_ENV, you can set it in the Vite config instead.`, + ) + } + } + + const isProduction = process.env.NODE_ENV === 'production' + // resolve public base url const relativeBaseShortcut = config.base === '' || config.base === './' From 3013f24a76b84f13ad2663c7e9f51cefc2dff3a6 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:40:33 +0900 Subject: [PATCH 07/35] Revert "feat: disable json minify by default in development" This reverts commit 81e38b82200b8faa442289d5cf35ba2c6a8a48d4. --- packages/vite/src/node/plugins/index.ts | 2 +- packages/vite/src/node/plugins/json.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 6f863407850c32..a0cd0d2d80f5e2 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -105,7 +105,7 @@ export async function resolvePlugins( cssPlugin(config), esbuildBannerFooterCompatPlugin(config), config.oxc !== false ? oxcPlugin(config) : null, - jsonPlugin(config.json, config.isProduction, enableNativePluginV1), + jsonPlugin(config.json, isBuild, enableNativePluginV1), wasmHelperPlugin(config), webWorkerPlugin(config), assetPlugin(config), diff --git a/packages/vite/src/node/plugins/json.ts b/packages/vite/src/node/plugins/json.ts index 918c6cde7f1474..a578f37b4189cc 100644 --- a/packages/vite/src/node/plugins/json.ts +++ b/packages/vite/src/node/plugins/json.ts @@ -40,11 +40,11 @@ export const isJSONRequest = (request: string): boolean => export function jsonPlugin( options: Required, - minify: boolean, + isBuild: boolean, enableNativePlugin: boolean, ): Plugin { if (enableNativePlugin) { - return nativeJsonPlugin({ ...options, minify }) + return nativeJsonPlugin({ ...options, minify: isBuild }) } return { @@ -101,7 +101,7 @@ export function jsonPlugin( ) { // during build, parse then double-stringify to remove all // unnecessary whitespaces to reduce bundle size. - if (minify) { + if (isBuild) { json = JSON.stringify(JSON.parse(json)) } From ba96ec6f32d6cde1fa2d092e15ee8f3d2fec1186 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:57:41 +0900 Subject: [PATCH 08/35] refactor: make `invalidateModule` function in DevEnvironment a method --- packages/vite/src/node/server/environment.ts | 65 ++++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 281cec3b5de1b8..4519847d30c42c 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -138,7 +138,7 @@ export class DevEnvironment extends BaseEnvironment { this.hot.on( 'vite:invalidate', async ({ path, message, firstInvalidatedBy }) => { - invalidateModule(this, { + this.invalidateModule({ path, message, firstInvalidatedBy, @@ -239,6 +239,36 @@ export class DevEnvironment extends BaseEnvironment { } } + private invalidateModule(m: { + path: string + message?: string + firstInvalidatedBy: string + }): void { + const mod = this.moduleGraph.urlToModuleMap.get(m.path) + if ( + mod && + mod.isSelfAccepting && + mod.lastHMRTimestamp > 0 && + !mod.lastHMRInvalidationReceived + ) { + mod.lastHMRInvalidationReceived = true + this.logger.info( + colors.yellow(`hmr invalidate `) + + colors.dim(m.path) + + (m.message ? ` ${m.message}` : ''), + { timestamp: true }, + ) + const file = getShortName(mod.file!, this.config.root) + updateModules( + this, + file, + [...mod.importers], + mod.lastHMRTimestamp, + m.firstInvalidatedBy, + ) + } + } + async close(): Promise { this._closing = true @@ -280,39 +310,6 @@ export class DevEnvironment extends BaseEnvironment { } } -function invalidateModule( - environment: DevEnvironment, - m: { - path: string - message?: string - firstInvalidatedBy: string - }, -) { - const mod = environment.moduleGraph.urlToModuleMap.get(m.path) - if ( - mod && - mod.isSelfAccepting && - mod.lastHMRTimestamp > 0 && - !mod.lastHMRInvalidationReceived - ) { - mod.lastHMRInvalidationReceived = true - environment.logger.info( - colors.yellow(`hmr invalidate `) + - colors.dim(m.path) + - (m.message ? ` ${m.message}` : ''), - { timestamp: true }, - ) - const file = getShortName(mod.file!, environment.config.root) - updateModules( - environment, - file, - [...mod.importers], - mod.lastHMRTimestamp, - m.firstInvalidatedBy, - ) - } -} - const callCrawlEndIfIdleAfterMs = 50 interface CrawlEndFinder { From 4897e080c472a4c7e94b90aff7e2726490faff05 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:51:50 +0900 Subject: [PATCH 09/35] feat: disable minify by default in full bundle mode --- packages/vite/src/node/build.ts | 3 ++- packages/vite/src/node/config.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 004b848e54271b..2e1d80ec5b6f9c 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -407,6 +407,7 @@ export function resolveBuildEnvironmentOptions( raw: BuildEnvironmentOptions, logger: Logger, consumer: 'client' | 'server' | undefined, + isFullBundledDev: boolean, ): ResolvedBuildEnvironmentOptions { const deprecatedPolyfillModulePreload = raw.polyfillModulePreload const { polyfillModulePreload, ...rest } = raw @@ -427,7 +428,7 @@ export function resolveBuildEnvironmentOptions( { ...buildEnvironmentOptionsDefaults, cssCodeSplit: !raw.lib, - minify: consumer === 'server' ? false : 'oxc', + minify: consumer === 'server' || isFullBundledDev ? false : 'oxc', rollupOptions: {}, rolldownOptions: undefined, ssr: consumer === 'server', diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 7d60c169360afa..ff314af959c8b4 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -846,6 +846,7 @@ function resolveEnvironmentOptions( forceOptimizeDeps: boolean | undefined, logger: Logger, environmentName: string, + isFullBundledDev: boolean, // Backward compatibility isSsrTargetWebworkerSet?: boolean, preTransformRequests?: boolean, @@ -908,6 +909,7 @@ function resolveEnvironmentOptions( options.build ?? {}, logger, consumer, + isFullBundledDev, ), plugins: undefined!, // to be resolved later // will be set by `setOptimizeDepsPluginNames` later @@ -1495,6 +1497,9 @@ export async function resolveConfig( config.ssr?.target === 'webworker', ) + const isFullBundledDev = + command === 'serve' && !!config.experimental?.fullBundleMode + // Backward compatibility: merge config.environments.client.resolve back into config.resolve config.resolve ??= {} config.resolve.conditions = config.environments.client.resolve?.conditions @@ -1511,6 +1516,7 @@ export async function resolveConfig( inlineConfig.forceOptimizeDeps, logger, environmentName, + isFullBundledDev, config.ssr?.target === 'webworker', config.server?.preTransformRequests, ) @@ -1534,6 +1540,7 @@ export async function resolveConfig( config.build ?? {}, logger, undefined, + isFullBundledDev, ) // Backward compatibility: merge config.environments.ssr back into config.ssr From e3111fe93d8a9d5e1e3bb08469be476bbe71e29e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:09:27 +0900 Subject: [PATCH 10/35] feat: disable buildImportAnalysisPlugin for full bundle mode --- packages/vite/src/node/build.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 2e1d80ec5b6f9c..629bb808a907c8 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -489,6 +489,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ pre: Plugin[] post: Plugin[] }> { + const isBuild = config.command === 'build' return { pre: [ completeSystemWrapPlugin(), @@ -505,7 +506,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ...(config.isWorker ? [webWorkerPostPlugin(config)] : []), ], post: [ - ...buildImportAnalysisPlugin(config), + ...(isBuild ? buildImportAnalysisPlugin(config) : []), ...(config.nativePluginEnabledLevel >= 1 ? [] : [ From 85ab705a8ae92d6834812b33b0926977679ae22a Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 27 May 2025 12:42:18 +0900 Subject: [PATCH 11/35] wip: full bundle dev env Co-Authored-By: underfin --- packages/vite/src/client/client.ts | 139 +++++++++++--- packages/vite/src/node/build.ts | 14 +- packages/vite/src/node/config.ts | 8 + .../vite/src/node/plugins/clientInjections.ts | 142 ++++++++------ packages/vite/src/node/plugins/css.ts | 9 +- packages/vite/src/node/plugins/define.ts | 6 +- .../src/node/plugins/dynamicImportVars.ts | 4 + .../vite/src/node/plugins/importMetaGlob.ts | 2 +- packages/vite/src/node/plugins/index.ts | 13 +- packages/vite/src/node/plugins/oxc.ts | 2 +- packages/vite/src/node/plugins/resolve.ts | 1 + packages/vite/src/node/plugins/wasm.ts | 2 +- packages/vite/src/node/plugins/worker.ts | 2 +- packages/vite/src/node/server/environment.ts | 26 +-- .../environments/fullBundleEnvironment.ts | 174 ++++++++++++++++++ packages/vite/src/node/server/hmr.ts | 8 + packages/vite/src/node/server/index.ts | 27 ++- .../vite/src/node/server/middlewares/error.ts | 1 + .../node/server/middlewares/htmlFallback.ts | 24 ++- .../src/node/server/middlewares/indexHtml.ts | 20 ++ .../node/server/middlewares/memoryFiles.ts | 39 ++++ packages/vite/types/hmrPayload.d.ts | 6 + 22 files changed, 539 insertions(+), 130 deletions(-) create mode 100644 packages/vite/src/node/server/environments/fullBundleEnvironment.ts create mode 100644 packages/vite/src/node/server/middlewares/memoryFiles.ts diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index ad57ce65371ecd..edc24da0d36179 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -20,6 +20,7 @@ declare const __HMR_BASE__: string declare const __HMR_TIMEOUT__: number declare const __HMR_ENABLE_OVERLAY__: boolean declare const __WS_TOKEN__: string +declare const __FULL_BUNDLE_MODE__: boolean console.debug('[vite] connecting...') @@ -37,6 +38,7 @@ const directSocketHost = __HMR_DIRECT_TARGET__ const base = __BASE__ || '/' const hmrTimeout = __HMR_TIMEOUT__ const wsToken = __WS_TOKEN__ +const isFullBundleMode = __FULL_BUNDLE_MODE__ const transport = normalizeModuleRunnerTransport( (() => { @@ -140,32 +142,53 @@ const hmrClient = new HMRClient( debug: (...msg) => console.debug('[vite]', ...msg), }, transport, - async function importUpdatedModule({ - acceptedPath, - timestamp, - explicitImportRequired, - isWithinCircularImport, - }) { - const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`) - const importPromise = import( - /* @vite-ignore */ - base + - acceptedPathWithoutQuery.slice(1) + - `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${ - query ? `&${query}` : '' - }` - ) - if (isWithinCircularImport) { - importPromise.catch(() => { - console.info( - `[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` + - `To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`, + isFullBundleMode + ? async function importUpdatedModule({ + url, + acceptedPath, + isWithinCircularImport, + }) { + const importPromise = import(base + url!).then(() => + // @ts-expect-error globalThis.__rolldown_runtime__ + globalThis.__rolldown_runtime__.loadExports(acceptedPath), ) - pageReload() - }) - } - return await importPromise - }, + if (isWithinCircularImport) { + importPromise.catch(() => { + console.info( + `[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` + + `To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`, + ) + pageReload() + }) + } + return await importPromise + } + : async function importUpdatedModule({ + acceptedPath, + timestamp, + explicitImportRequired, + isWithinCircularImport, + }) { + const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`) + const importPromise = import( + /* @vite-ignore */ + base + + acceptedPathWithoutQuery.slice(1) + + `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${ + query ? `&${query}` : '' + }` + ) + if (isWithinCircularImport) { + importPromise.catch(() => { + console.info( + `[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` + + `To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`, + ) + pageReload() + }) + } + return await importPromise + }, ) transport.connect!(createHMRHandler(handleMessage)) @@ -575,3 +598,69 @@ export function injectQuery(url: string, queryToInject: string): string { } export { ErrorOverlay } + +if (isFullBundleMode) { + class DevRuntime { + modules: Record = {} + + static getInstance() { + // @ts-expect-error __rolldown_runtime__ + let instance = globalThis.__rolldown_runtime__ + if (!instance) { + instance = new DevRuntime() + // @ts-expect-error __rolldown_runtime__ + globalThis.__rolldown_runtime__ = instance + } + return instance + } + + createModuleHotContext(moduleId: string) { + const ctx = createHotContext(moduleId) + // @ts-expect-error TODO: support CSS + ctx._internal = { + updateStyle, + removeStyle, + } + return ctx + } + + applyUpdates(_boundaries: string[]) { + // + } + + registerModule( + id: string, + module: { exports: Record unknown> }, + ) { + this.modules[id] = module + } + + loadExports(id: string) { + const module = this.modules[id] + if (module) { + return module.exports + } else { + console.warn(`Module ${id} not found`) + return {} + } + } + + // __esmMin + // @ts-expect-error need to add typing + createEsmInitializer = (fn, res) => () => (fn && (res = fn((fn = 0))), res) + // __commonJSMin + // @ts-expect-error need to add typing + createCjsInitializer = (cb, mod) => () => ( + mod || cb((mod = { exports: {} }).exports, mod), mod.exports + ) + // @ts-expect-error it is exits + __toESM = __toESM + // @ts-expect-error it is exits + __toCommonJS = __toCommonJS + // @ts-expect-error it is exits + __export = __export + } + + // @ts-expect-error __rolldown_runtime__ + globalThis.__rolldown_runtime__ ||= new DevRuntime() +} diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 629bb808a907c8..55b636e329a9ee 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -493,7 +493,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ return { pre: [ completeSystemWrapPlugin(), - ...(!config.isWorker ? [prepareOutDirPlugin()] : []), + ...(isBuild && !config.isWorker ? [prepareOutDirPlugin()] : []), perEnvironmentPlugin( 'vite:rollup-options-plugins', async (environment) => @@ -515,8 +515,8 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ? [buildEsbuildPlugin()] : []), ]), - terserPlugin(config), - ...(!config.isWorker + ...(isBuild ? [terserPlugin(config)] : []), + ...(isBuild && !config.isWorker ? [ manifestPlugin(config), ssrManifestPlugin(), @@ -557,10 +557,10 @@ function resolveConfigToBuild( ) } -function resolveRolldownOptions( +export function resolveRolldownOptions( environment: Environment, chunkMetadataMap: ChunkMetadataMap, -) { +): RolldownOptions { const { root, packageCache, build: options } = environment.config const libOptions = options.lib const { logger } = environment @@ -869,7 +869,7 @@ async function buildEnvironment( } } -function enhanceRollupError(e: RollupError) { +export function enhanceRollupError(e: RollupError): void { const stackOnly = extractStack(e) let msg = colors.red((e.plugin ? `[${e.plugin}] ` : '') + e.message) @@ -1035,7 +1035,7 @@ const dynamicImportWarningIgnoreList = [ `statically analyzed`, ] -function clearLine() { +export function clearLine(): void { const tty = process.stdout.isTTY && !process.env.CI if (tty) { process.stdout.clearLine(0) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index ff314af959c8b4..202f772e916b38 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -120,6 +120,7 @@ import { BasicMinimalPluginContext, basePluginContextMeta, } from './server/pluginContainer' +import { FullBundleDevEnvironment } from './server/environments/fullBundleEnvironment' const debug = createDebugger('vite:config', { depth: 10 }) const promisifiedRealpath = promisify(fs.realpath) @@ -233,6 +234,13 @@ function defaultCreateClientDevEnvironment( config: ResolvedConfig, context: CreateDevEnvironmentContext, ) { + if (config.experimental.fullBundleMode) { + return new FullBundleDevEnvironment(name, config, { + hot: true, + transport: context.ws, + }) + } + return new DevEnvironment(name, config, { hot: true, transport: context.ws, diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 3fa745f7d2f220..ee309eebdd6b25 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import fs from 'node:fs' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY, ENV_ENTRY } from '../constants' @@ -33,66 +34,7 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:client-inject', async buildStart() { - const resolvedServerHostname = (await resolveHostname(config.server.host)) - .name - const resolvedServerPort = config.server.port! - const devBase = config.base - - const serverHost = `${resolvedServerHostname}:${resolvedServerPort}${devBase}` - - let hmrConfig = config.server.hmr - hmrConfig = isObject(hmrConfig) ? hmrConfig : undefined - const host = hmrConfig?.host || null - const protocol = hmrConfig?.protocol || null - const timeout = hmrConfig?.timeout || 30000 - const overlay = hmrConfig?.overlay !== false - const isHmrServerSpecified = !!hmrConfig?.server - const hmrConfigName = path.basename(config.configFile || 'vite.config.js') - - // hmr.clientPort -> hmr.port - // -> (24678 if middleware mode and HMR server is not specified) -> new URL(import.meta.url).port - let port = hmrConfig?.clientPort || hmrConfig?.port || null - if (config.server.middlewareMode && !isHmrServerSpecified) { - port ||= 24678 - } - - let directTarget = hmrConfig?.host || resolvedServerHostname - directTarget += `:${hmrConfig?.port || resolvedServerPort}` - directTarget += devBase - - let hmrBase = devBase - if (hmrConfig?.path) { - hmrBase = path.posix.join(hmrBase, hmrConfig.path) - } - - const modeReplacement = escapeReplacement(config.mode) - const baseReplacement = escapeReplacement(devBase) - const serverHostReplacement = escapeReplacement(serverHost) - const hmrProtocolReplacement = escapeReplacement(protocol) - const hmrHostnameReplacement = escapeReplacement(host) - const hmrPortReplacement = escapeReplacement(port) - const hmrDirectTargetReplacement = escapeReplacement(directTarget) - const hmrBaseReplacement = escapeReplacement(hmrBase) - const hmrTimeoutReplacement = escapeReplacement(timeout) - const hmrEnableOverlayReplacement = escapeReplacement(overlay) - const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) - const wsTokenReplacement = escapeReplacement(config.webSocketToken) - - injectConfigValues = (code: string) => { - return code - .replace(`__MODE__`, modeReplacement) - .replace(/__BASE__/g, baseReplacement) - .replace(`__SERVER_HOST__`, serverHostReplacement) - .replace(`__HMR_PROTOCOL__`, hmrProtocolReplacement) - .replace(`__HMR_HOSTNAME__`, hmrHostnameReplacement) - .replace(`__HMR_PORT__`, hmrPortReplacement) - .replace(`__HMR_DIRECT_TARGET__`, hmrDirectTargetReplacement) - .replace(`__HMR_BASE__`, hmrBaseReplacement) - .replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement) - .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement) - .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement) - .replace(`__WS_TOKEN__`, wsTokenReplacement) - } + injectConfigValues = await createClientConfigValueReplacer(config) }, async transform(code, id) { const ssr = this.environment.config.consumer === 'server' @@ -120,3 +62,83 @@ function escapeReplacement(value: string | number | boolean | null) { const jsonValue = JSON.stringify(value) return () => jsonValue } + +async function createClientConfigValueReplacer( + config: ResolvedConfig, +): Promise<(code: string) => string> { + const resolvedServerHostname = (await resolveHostname(config.server.host)) + .name + const resolvedServerPort = config.server.port! + const devBase = config.base + + const serverHost = `${resolvedServerHostname}:${resolvedServerPort}${devBase}` + + let hmrConfig = config.server.hmr + hmrConfig = isObject(hmrConfig) ? hmrConfig : undefined + const host = hmrConfig?.host || null + const protocol = hmrConfig?.protocol || null + const timeout = hmrConfig?.timeout || 30000 + const overlay = hmrConfig?.overlay !== false + const isHmrServerSpecified = !!hmrConfig?.server + const hmrConfigName = path.basename(config.configFile || 'vite.config.js') + + // hmr.clientPort -> hmr.port + // -> (24678 if middleware mode and HMR server is not specified) -> new URL(import.meta.url).port + let port = hmrConfig?.clientPort || hmrConfig?.port || null + if (config.server.middlewareMode && !isHmrServerSpecified) { + port ||= 24678 + } + + let directTarget = hmrConfig?.host || resolvedServerHostname + directTarget += `:${hmrConfig?.port || resolvedServerPort}` + directTarget += devBase + + let hmrBase = devBase + if (hmrConfig?.path) { + hmrBase = path.posix.join(hmrBase, hmrConfig.path) + } + + const modeReplacement = escapeReplacement(config.mode) + const baseReplacement = escapeReplacement(devBase) + const serverHostReplacement = escapeReplacement(serverHost) + const hmrProtocolReplacement = escapeReplacement(protocol) + const hmrHostnameReplacement = escapeReplacement(host) + const hmrPortReplacement = escapeReplacement(port) + const hmrDirectTargetReplacement = escapeReplacement(directTarget) + const hmrBaseReplacement = escapeReplacement(hmrBase) + const hmrTimeoutReplacement = escapeReplacement(timeout) + const hmrEnableOverlayReplacement = escapeReplacement(overlay) + const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) + const wsTokenReplacement = escapeReplacement(config.webSocketToken) + const fullBundleModeReplacement = escapeReplacement( + config.experimental.fullBundleMode || false, + ) + + return (code) => + code + .replace(`__MODE__`, modeReplacement) + .replace(/__BASE__/g, baseReplacement) + .replace(`__SERVER_HOST__`, serverHostReplacement) + .replace(`__HMR_PROTOCOL__`, hmrProtocolReplacement) + .replace(`__HMR_HOSTNAME__`, hmrHostnameReplacement) + .replace(`__HMR_PORT__`, hmrPortReplacement) + .replace(`__HMR_DIRECT_TARGET__`, hmrDirectTargetReplacement) + .replace(`__HMR_BASE__`, hmrBaseReplacement) + .replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement) + .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement) + .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement) + .replace(`__WS_TOKEN__`, wsTokenReplacement) + .replaceAll(`__FULL_BUNDLE_MODE__`, fullBundleModeReplacement) +} + +export async function getHmrImplementation( + config: ResolvedConfig, +): Promise { + const content = fs.readFileSync(normalizedClientEntry, 'utf-8') + const replacer = await createClientConfigValueReplacer(config) + return ( + replacer(content) + // the rolldown runtime shouldn't be importer a module + .replace(/import\s*['"]@vite\/env['"]/, '') + ) +} diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 9f7af3ce3e84f7..ef4d72b48625a8 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -584,9 +584,12 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const cssContent = await getContentWithSourcemap(css) const code = [ - `import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify( - path.posix.join(config.base, CLIENT_PUBLIC_PATH), - )}`, + config.isBundled + ? // TODO: support CSS + `const { updateStyle: __vite__updateStyle, removeStyle: __vite__removeStyle } = import.meta.hot._internal` + : `import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify( + path.posix.join(config.base, CLIENT_PUBLIC_PATH), + )}`, `const __vite__id = ${JSON.stringify(id)}`, `const __vite__css = ${JSON.stringify(cssContent)}`, `__vite__updateStyle(__vite__id, __vite__css)`, diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 5cc527c060a391..001d472736a154 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -12,6 +12,7 @@ const importMetaEnvKeyReCache = new Map() const escapedDotRE = /(? = {} if (isBuild) { importMetaKeys['import.meta.hot'] = `undefined` + } + if (isBundled) { for (const key in config.env) { const val = JSON.stringify(config.env[key]) importMetaKeys[`import.meta.env.${key}`] = val @@ -134,7 +137,7 @@ export function definePlugin(config: ResolvedConfig): Plugin { transform: { async handler(code, id) { - if (this.environment.config.consumer === 'client' && !isBuild) { + if (this.environment.config.consumer === 'client' && !isBundled) { // for dev we inject actual global defines in the vite client to // avoid the transform cost. see the `clientInjection` and // `importAnalysis` plugin. @@ -217,6 +220,7 @@ export async function replaceDefine( }) if (result.errors.length > 0) { + // TODO: better error message throw new AggregateError(result.errors, 'oxc transform error') } diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index 3c233db53c4ec3..5cdcf5c80244d1 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -167,6 +167,10 @@ export async function transformDynamicImport( } export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { + if (config.experimental.enableNativePlugin === true && config.isBundled) { + return nativeDynamicImportVarsPlugin() + } + const resolve = createBackCompatIdResolver(config, { preferRelative: true, tryIndex: false, diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 6bbebfe93fac53..4d52f68ce85c40 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -42,7 +42,7 @@ interface ParsedGeneralImportGlobOptions extends GeneralImportGlobOptions { } export function importGlobPlugin(config: ResolvedConfig): Plugin { - if (config.command === 'build' && config.nativePluginEnabledLevel >= 1) { + if (config.isBundled && config.nativePluginEnabledLevel >= 1) { return nativeImportGlobPlugin({ root: config.root, restoreQueryExtension: config.experimental.importGlobRestoreExtension, diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index a0cd0d2d80f5e2..96afbc22eef9f2 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -41,8 +41,9 @@ export async function resolvePlugins( postPlugins: Plugin[], ): Promise { const isBuild = config.command === 'build' + const isBundled = config.isBundled const isWorker = config.isWorker - const buildPlugins = isBuild + const buildPlugins = isBundled ? await (await import('../build')).resolveBuildPlugins(config) : { pre: [], post: [] } const { modulePreload } = config.build @@ -50,10 +51,10 @@ export async function resolvePlugins( const enableNativePluginV1 = config.nativePluginEnabledLevel >= 1 return [ - !isBuild ? optimizedDepsPlugin() : null, + !isBundled ? optimizedDepsPlugin() : null, !isWorker ? watchPackageDataPlugin(config.packageCache) : null, - !isBuild ? preAliasPlugin(config) : null, - isBuild && + !isBundled ? preAliasPlugin(config) : null, + isBundled && enableNativePluginV1 && !config.resolve.alias.some((v) => v.customResolver) ? nativeAliasPlugin({ @@ -115,7 +116,7 @@ export async function resolvePlugins( wasmFallbackPlugin(config), definePlugin(config), cssPostPlugin(config), - isBuild && buildHtmlPlugin(config), + isBundled && buildHtmlPlugin(config), workerImportMetaUrlPlugin(config), assetImportMetaUrlPlugin(config), ...buildPlugins.pre, @@ -127,7 +128,7 @@ export async function resolvePlugins( ...buildPlugins.post, // internal server-only plugins are always applied after everything else - ...(isBuild + ...(isBundled ? [] : [ clientInjectionsPlugin(config), diff --git a/packages/vite/src/node/plugins/oxc.ts b/packages/vite/src/node/plugins/oxc.ts index 1633c602a11832..ae5059c570e1d8 100644 --- a/packages/vite/src/node/plugins/oxc.ts +++ b/packages/vite/src/node/plugins/oxc.ts @@ -281,7 +281,7 @@ function resolveTsconfigTarget(target: string | undefined): number | 'next' { } export function oxcPlugin(config: ResolvedConfig): Plugin { - if (config.command === 'build' && config.nativePluginEnabledLevel >= 1) { + if (config.isBundled && config.nativePluginEnabledLevel >= 1) { return perEnvironmentPlugin('native:transform', (environment) => { const { jsxInject, diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 82161b200a1c38..85193aae847433 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -237,6 +237,7 @@ export function oxcResolvePlugin( const depsOptimizerEnabled = resolveOptions.optimizeDeps && !resolveOptions.isBuild && + !partialEnv.config.experimental.fullBundleMode && !isDepOptimizationDisabled(partialEnv.config.optimizeDeps) const getDepsOptimizer = () => { const env = getEnv() diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index 1b3288b8df8f2c..51836a275fd322 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -54,7 +54,7 @@ const wasmHelper = async (opts = {}, url: string) => { const wasmHelperCode = wasmHelper.toString() export const wasmHelperPlugin = (config: ResolvedConfig): Plugin => { - if (config.command === 'build' && config.nativePluginEnabledLevel >= 1) { + if (config.isBundled && config.nativePluginEnabledLevel >= 1) { return nativeWasmHelperPlugin({ decodedBase: config.decodedBase, }) diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 390700ed31ca80..0eab6354a18ce8 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -234,7 +234,7 @@ export async function workerFileToUrl( } export function webWorkerPostPlugin(config: ResolvedConfig): Plugin { - if (config.command === 'build' && config.nativePluginEnabledLevel >= 1) { + if (config.isBundled && config.nativePluginEnabledLevel >= 1) { return perEnvironmentPlugin( 'native:web-worker-post-plugin', (environment) => { diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 4519847d30c42c..9cb16d27b2b2ca 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -44,6 +44,8 @@ export interface DevEnvironmentContext { inlineSourceMap?: boolean } depsOptimizer?: DepsOptimizer + /** @internal used for full bundle mode */ + disableDepsOptimizer?: boolean } export class DevEnvironment extends BaseEnvironment { @@ -146,17 +148,19 @@ export class DevEnvironment extends BaseEnvironment { }, ) - const { optimizeDeps } = this.config - if (context.depsOptimizer) { - this.depsOptimizer = context.depsOptimizer - } else if (isDepOptimizationDisabled(optimizeDeps)) { - this.depsOptimizer = undefined - } else { - this.depsOptimizer = ( - optimizeDeps.noDiscovery - ? createExplicitDepsOptimizer - : createDepsOptimizer - )(this) + if (!context.disableDepsOptimizer) { + const { optimizeDeps } = this.config + if (context.depsOptimizer) { + this.depsOptimizer = context.depsOptimizer + } else if (isDepOptimizationDisabled(optimizeDeps)) { + this.depsOptimizer = undefined + } else { + this.depsOptimizer = ( + optimizeDeps.noDiscovery + ? createExplicitDepsOptimizer + : createDepsOptimizer + )(this) + } } } diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts new file mode 100644 index 00000000000000..765a03dae574d3 --- /dev/null +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -0,0 +1,174 @@ +import type { RolldownBuild, RolldownOptions } from 'rolldown' +import type { Update } from 'types/hmrPayload' +import colors from 'picocolors' +import type { ChunkMetadata } from 'types/metadata' +import { + clearLine, + enhanceRollupError, + resolveRolldownOptions, +} from '../../build' +import { getHmrImplementation } from '../../plugins/clientInjections' +import { DevEnvironment, type DevEnvironmentContext } from '../environment' +import type { ResolvedConfig } from '../../config' +import type { ViteDevServer } from '../../server' +import { arraify, createDebugger } from '../../utils' +import { prepareError } from '../middlewares/error' + +const debug = createDebugger('vite:full-bundle-mode') + +export class FullBundleDevEnvironment extends DevEnvironment { + private rolldownOptions: RolldownOptions | undefined + private bundle: RolldownBuild | undefined + watchFiles = new Set() + memoryFiles = new Map() + + constructor( + name: string, + config: ResolvedConfig, + context: DevEnvironmentContext, + ) { + if (name !== 'client') { + throw new Error( + 'currently full bundle mode is only available for client environment', + ) + } + + super(name, config, { ...context, disableDepsOptimizer: true }) + } + + override async listen(server: ViteDevServer): Promise { + await super.listen(server) + + debug?.('setup bundle options') + const rollupOptions = await this.getRolldownOptions() + const { rolldown } = await import('rolldown') + this.rolldownOptions = rollupOptions + this.bundle = await rolldown(rollupOptions) + debug?.('bundle created') + + this.triggerGenerateInitialBundle(rollupOptions.output) + } + + async onFileChange( + _type: 'create' | 'update' | 'delete', + file: string, + server: ViteDevServer, + ): Promise { + // TODO: handle the case when the initial bundle is not generated yet + + debug?.(`file update detected ${file}, generating hmr patch`) + // NOTE: only single outputOptions is supported here + const hmrOutput = (await this.bundle!.generateHmrPatch([file]))! + + debug?.(`handle hmr output for ${file}`, { + ...hmrOutput, + code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, + }) + if (hmrOutput.fullReload) { + try { + await this.generateBundle(this.rolldownOptions!.output) + } catch (e) { + // TODO: support multiple errors + server.ws.send({ type: 'error', err: prepareError(e.errors[0]) }) + return + } + + server.ws.send({ type: 'full-reload' }) + const reason = hmrOutput.fullReloadReason + ? colors.dim(` (${hmrOutput.fullReloadReason})`) + : '' + this.logger.info( + colors.green(`page reload `) + colors.dim(file) + reason, + { + clear: !hmrOutput.firstInvalidatedBy, + timestamp: true, + }, + ) + return + } + + if (hmrOutput.code) { + this.memoryFiles.set(hmrOutput.filename, hmrOutput.code) + if (hmrOutput.sourcemapFilename && hmrOutput.sourcemap) { + this.memoryFiles.set(hmrOutput.sourcemapFilename, hmrOutput.sourcemap) + } + const updates: Update[] = hmrOutput.hmrBoundaries.map((boundary: any) => { + return { + type: 'js-update', + url: hmrOutput.filename, + path: boundary.boundary, + acceptedPath: boundary.acceptedVia, + firstInvalidatedBy: hmrOutput.firstInvalidatedBy, + timestamp: 0, + } + }) + server!.ws.send({ + type: 'update', + updates, + }) + this.logger.info( + colors.green(`hmr update `) + + colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), + { clear: !hmrOutput.firstInvalidatedBy, timestamp: true }, + ) + } + } + + override async close(): Promise { + await Promise.all([ + super.close(), + this.bundle?.close().finally(() => { + this.bundle = undefined + this.watchFiles.clear() + this.memoryFiles.clear() + }), + ]) + } + + private async getRolldownOptions() { + const chunkMetadataMap = new Map() + const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap) + rolldownOptions.experimental ??= {} + rolldownOptions.experimental.hmr = { + implement: await getHmrImplementation(this.getTopLevelConfig()), + } + + rolldownOptions.treeshake = false + + return rolldownOptions + } + + private async triggerGenerateInitialBundle( + outOpts: RolldownOptions['output'], + ) { + this.generateBundle(outOpts).then( + () => { + debug?.('initial bundle generated') + }, + (e) => { + enhanceRollupError(e) + clearLine() + this.logger.error(`${colors.red('✗')} Build failed` + e.stack) + // TODO: show error message on the browser + }, + ) + } + + // TODO: should debounce this + private async generateBundle(outOpts: RolldownOptions['output']) { + for (const outputOpts of arraify(outOpts)) { + const output = await this.bundle!.generate(outputOpts) + for (const outputFile of output.output) { + this.memoryFiles.set( + outputFile.fileName, + outputFile.type === 'chunk' ? outputFile.code : outputFile.source, + ) + } + } + + // TODO: should this be done for hmr patch file generation? + for (const file of await this.bundle!.watchFiles) { + this.watchFiles.add(file) + } + } +} diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index b30a6999394c25..f350b63695add3 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -30,6 +30,7 @@ import { BasicMinimalPluginContext, basePluginContextMeta, } from './pluginContainer' +import type { FullBundleDevEnvironment } from './environments/fullBundleEnvironment' import type { HttpServer } from '.' import { restartServerWithUrls } from '.' @@ -418,6 +419,13 @@ export async function handleHMRUpdate( return } + if (config.experimental.fullBundleMode) { + // TODO: support handleHotUpdate / hotUpdate + const environment = server.environments.client as FullBundleDevEnvironment + environment.onFileChange(type, file, server) + return + } + const timestamp = monotonicDateNow() const contextMeta = { type, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index ded3263bc8aca3..3a032295334e9e 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -101,6 +101,7 @@ import { searchForPackageRoot, searchForWorkspaceRoot } from './searchRoot' import type { DevEnvironment } from './environment' import { hostValidationMiddleware } from './middlewares/hostCheck' import { rejectInvalidRequestMiddleware } from './middlewares/rejectInvalidRequest' +import { memoryFilesMiddleware } from './middlewares/memoryFiles' const usedConfigs = new WeakSet() @@ -892,7 +893,9 @@ export async function _createServer( // Internal middlewares ------------------------------------------------------ - middlewares.use(cachedTransformMiddleware(server)) + if (!config.experimental.fullBundleMode) { + middlewares.use(cachedTransformMiddleware(server)) + } // proxy const { proxy } = serverConfig @@ -927,16 +930,26 @@ export async function _createServer( middlewares.use(servePublicMiddleware(server, publicFiles)) } - // main transform middleware - middlewares.use(transformMiddleware(server)) + if (config.experimental.fullBundleMode) { + middlewares.use(memoryFilesMiddleware(server)) + } else { + // main transform middleware + middlewares.use(transformMiddleware(server)) - // serve static files - middlewares.use(serveRawFsMiddleware(server)) - middlewares.use(serveStaticMiddleware(server)) + // serve static files + middlewares.use(serveRawFsMiddleware(server)) + middlewares.use(serveStaticMiddleware(server)) + } // html fallback if (config.appType === 'spa' || config.appType === 'mpa') { - middlewares.use(htmlFallbackMiddleware(root, config.appType === 'spa')) + middlewares.use( + htmlFallbackMiddleware( + root, + config.appType === 'spa', + server.environments.client, + ), + ) } // apply configureServer post hooks ------------------------------------------ diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index de1374d83c7f2f..ea4ac9a7f43137 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -71,6 +71,7 @@ export function errorMiddleware( if (allowNext) { next() } else { + // TODO: support error overlay res.statusCode = 500 res.end(` diff --git a/packages/vite/src/node/server/middlewares/htmlFallback.ts b/packages/vite/src/node/server/middlewares/htmlFallback.ts index b61b44bf061180..54239902de8ea7 100644 --- a/packages/vite/src/node/server/middlewares/htmlFallback.ts +++ b/packages/vite/src/node/server/middlewares/htmlFallback.ts @@ -3,13 +3,28 @@ import fs from 'node:fs' import type { Connect } from 'dep-types/connect' import { createDebugger } from '../../utils' import { cleanUrl } from '../../../shared/utils' +import type { DevEnvironment } from '../environment' +import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment' const debug = createDebugger('vite:html-fallback') export function htmlFallbackMiddleware( root: string, spaFallback: boolean, + clientEnvironment?: DevEnvironment, ): Connect.NextHandleFunction { + const memoryFiles = + clientEnvironment instanceof FullBundleDevEnvironment + ? clientEnvironment.memoryFiles + : undefined + + function checkFileExists(relativePath: string) { + return ( + memoryFiles?.has(relativePath) ?? + fs.existsSync(path.join(root, relativePath)) + ) + } + // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return function viteHtmlFallbackMiddleware(req, _res, next) { if ( @@ -34,8 +49,7 @@ export function htmlFallbackMiddleware( // .html files are not handled by serveStaticMiddleware // so we need to check if the file exists if (pathname.endsWith('.html')) { - const filePath = path.join(root, pathname) - if (fs.existsSync(filePath)) { + if (checkFileExists(pathname)) { debug?.(`Rewriting ${req.method} ${req.url} to ${url}`) req.url = url return next() @@ -43,8 +57,7 @@ export function htmlFallbackMiddleware( } // trailing slash should check for fallback index.html else if (pathname.endsWith('/')) { - const filePath = path.join(root, pathname, 'index.html') - if (fs.existsSync(filePath)) { + if (checkFileExists(path.join(pathname, 'index.html'))) { const newUrl = url + 'index.html' debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`) req.url = newUrl @@ -53,8 +66,7 @@ export function htmlFallbackMiddleware( } // non-trailing slash should check for fallback .html else { - const filePath = path.join(root, pathname + '.html') - if (fs.existsSync(filePath)) { + if (checkFileExists(pathname + '.html')) { const newUrl = url + '.html' debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`) req.url = newUrl diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index 7b85d8fb1ccceb..11cf734db28aa5 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -48,6 +48,7 @@ import { BasicMinimalPluginContext, basePluginContextMeta, } from '../pluginContainer' +import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment' interface AssetNode { start: number @@ -440,6 +441,10 @@ export function indexHtmlMiddleware( server: ViteDevServer | PreviewServer, ): Connect.NextHandleFunction { const isDev = isDevServer(server) + const memoryFiles = + isDev && server.environments.client instanceof FullBundleDevEnvironment + ? server.environments.client.memoryFiles + : undefined // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return async function viteIndexHtmlMiddleware(req, res, next) { @@ -450,6 +455,21 @@ export function indexHtmlMiddleware( const url = req.url && cleanUrl(req.url) // htmlFallbackMiddleware appends '.html' to URLs if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') { + if (memoryFiles) { + const cleanedUrl = cleanUrl(url).slice(1) // remove first / + const content = memoryFiles.get(cleanedUrl) + if (!content) { + return next() + } + + const html = + typeof content === 'string' ? content : Buffer.from(content.buffer) + const headers = isDev + ? server.config.server.headers + : server.config.preview.headers + return send(req, res, html, 'html', { headers }) + } + let filePath: string if (isDev && url.startsWith(FS_PREFIX)) { filePath = decodeURIComponent(fsPathFromId(url)) 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..5a28eb84c00302 --- /dev/null +++ b/packages/vite/src/node/server/middlewares/memoryFiles.ts @@ -0,0 +1,39 @@ +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!).slice(1) // remove first / + if (cleanedUrl.endsWith('.html')) { + return next() + } + const file = memoryFiles.get(cleanedUrl) + if (file) { + const mime = mrmime.lookup(cleanedUrl) + 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 From 06d4c1509f31353d670d3eaf11d1feaff15ca55c Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 5 Jun 2025 19:59:54 +0900 Subject: [PATCH 12/35] wip: revamp state handling --- .../environments/fullBundleEnvironment.ts | 290 +++++++++++++----- .../src/node/server/middlewares/indexHtml.ts | 53 +++- 2 files changed, 264 insertions(+), 79 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 765a03dae574d3..8ef06b9796ad1c 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -16,9 +16,14 @@ import { prepareError } from '../middlewares/error' const debug = createDebugger('vite:full-bundle-mode') +type HmrOutput = Exclude< + Awaited>, + undefined +> + export class FullBundleDevEnvironment extends DevEnvironment { - private rolldownOptions: RolldownOptions | undefined - private bundle: RolldownBuild | undefined + private state: BundleState = { type: 'initial' } + watchFiles = new Set() memoryFiles = new Map() @@ -39,14 +44,14 @@ export class FullBundleDevEnvironment extends DevEnvironment { override async listen(server: ViteDevServer): Promise { await super.listen(server) - debug?.('setup bundle options') + debug?.('INITIAL: setup bundle options') const rollupOptions = await this.getRolldownOptions() const { rolldown } = await import('rolldown') - this.rolldownOptions = rollupOptions - this.bundle = await rolldown(rollupOptions) - debug?.('bundle created') + const bundle = await rolldown(rollupOptions) + debug?.('INITIAL: bundle created') - this.triggerGenerateInitialBundle(rollupOptions.output) + debug?.('BUNDLING: trigger initial bundle') + this.triggerGenerateBundle({ options: rollupOptions, bundle }) } async onFileChange( @@ -54,33 +59,196 @@ export class FullBundleDevEnvironment extends DevEnvironment { file: string, server: ViteDevServer, ): Promise { - // TODO: handle the case when the initial bundle is not generated yet + if (this.state.type === 'initial') { + return + } + + if (this.state.type === 'bundling') { + debug?.( + `BUNDLING: file update detected ${file}, retriggering bundle generation`, + ) + this.state.abortController.abort() + this.triggerGenerateBundle(this.state) + return + } + if (this.state.type === 'bundle-error') { + debug?.( + `BUNDLE-ERROR: file update detected ${file}, retriggering bundle generation`, + ) + this.triggerGenerateBundle(this.state) + return + } - debug?.(`file update detected ${file}, generating hmr patch`) - // NOTE: only single outputOptions is supported here - const hmrOutput = (await this.bundle!.generateHmrPatch([file]))! + if ( + this.state.type === 'bundled' || + this.state.type === 'generating-hmr-patch' + ) { + if (this.state.type === 'bundled') { + debug?.(`BUNDLED: file update detected ${file}, generating HMR patch`) + } else if (this.state.type === 'generating-hmr-patch') { + debug?.( + `GENERATING-HMR-PATCH: file update detected ${file}, regenerating HMR patch`, + ) + } - debug?.(`handle hmr output for ${file}`, { - ...hmrOutput, - code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, - }) - if (hmrOutput.fullReload) { + this.state = { + type: 'generating-hmr-patch', + options: this.state.options, + bundle: this.state.bundle, + } + + let hmrOutput: HmrOutput try { - await this.generateBundle(this.rolldownOptions!.output) + // NOTE: only single outputOptions is supported here + hmrOutput = (await this.state.bundle.generateHmrPatch([file]))! } catch (e) { // TODO: support multiple errors server.ws.send({ type: 'error', err: prepareError(e.errors[0]) }) + + this.state = { + type: 'bundled', + options: this.state.options, + bundle: this.state.bundle, + } return } - server.ws.send({ type: 'full-reload' }) + debug?.(`handle hmr output for ${file}`, { + ...hmrOutput, + code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, + }) + + this.handleHmrOutput(file, hmrOutput, this.state) + return + } + this.state satisfies never // exhaustive check + } + + override async close(): Promise { + await Promise.all([ + super.close(), + (async () => { + if (this.state.type === 'initial') { + return + } + if (this.state.type === 'bundling') { + this.state.abortController.abort() + } + const bundle = this.state.bundle + this.state = { type: 'initial' } + + this.watchFiles.clear() + this.memoryFiles.clear() + await bundle.close() + })(), + ]) + } + + private async getRolldownOptions() { + const chunkMetadataMap = new Map() + const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap) + rolldownOptions.experimental ??= {} + rolldownOptions.experimental.hmr = { + implement: await getHmrImplementation(this.getTopLevelConfig()), + } + + rolldownOptions.treeshake = false + + return rolldownOptions + } + + private triggerGenerateBundle({ + options, + bundle, + }: BundleStateCommonProperties) { + const controller = new AbortController() + const promise = this.generateBundle( + options.output, + bundle, + controller.signal, + ) + this.state = { + type: 'bundling', + options, + bundle, + promise, + abortController: controller, + } + } + + private async generateBundle( + outOpts: RolldownOptions['output'], + bundle: RolldownBuild, + signal: AbortSignal, + ) { + try { + const newMemoryFiles = new Map() + for (const outputOpts of arraify(outOpts)) { + const output = await bundle.generate(outputOpts) + if (signal.aborted) return + + for (const outputFile of output.output) { + newMemoryFiles.set( + outputFile.fileName, + outputFile.type === 'chunk' ? outputFile.code : outputFile.source, + ) + } + } + + this.memoryFiles.clear() + for (const [file, code] of newMemoryFiles) { + this.memoryFiles.set(file, code) + } + + // TODO: should this be done for hmr patch file generation? + for (const file of await bundle.watchFiles) { + this.watchFiles.add(file) + } + if (signal.aborted) return + + if (this.state.type === 'initial') throw new Error('unreachable') + this.state = { + type: 'bundled', + bundle: this.state.bundle, + options: this.state.options, + } + debug?.('BUNDLED: bundle generated') + + this.hot.send({ type: 'full-reload' }) + this.logger.info(colors.green(`page reload`), { timestamp: true }) + } catch (e) { + enhanceRollupError(e) + clearLine() + this.logger.error(`${colors.red('✗')} Build failed` + e.stack) + + // TODO: support multiple errors + this.hot.send({ type: 'error', err: prepareError(e.errors[0]) }) + + if (this.state.type === 'initial') throw new Error('unreachable') + this.state = { + type: 'bundle-error', + bundle: this.state.bundle, + options: this.state.options, + } + debug?.('BUNDLED: bundle errored') + } + } + + private async handleHmrOutput( + file: string, + hmrOutput: HmrOutput, + { options, bundle }: BundleStateCommonProperties, + ) { + if (hmrOutput.fullReload) { + this.triggerGenerateBundle({ options, bundle }) + const reason = hmrOutput.fullReloadReason ? colors.dim(` (${hmrOutput.fullReloadReason})`) : '' this.logger.info( - colors.green(`page reload `) + colors.dim(file) + reason, + colors.green(`trigger page reload `) + colors.dim(file) + reason, { - clear: !hmrOutput.firstInvalidatedBy, + // clear: !hmrOutput.firstInvalidatedBy, timestamp: true, }, ) @@ -102,7 +270,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { timestamp: 0, } }) - server!.ws.send({ + this.hot.send({ type: 'update', updates, }) @@ -113,62 +281,30 @@ export class FullBundleDevEnvironment extends DevEnvironment { ) } } +} - override async close(): Promise { - await Promise.all([ - super.close(), - this.bundle?.close().finally(() => { - this.bundle = undefined - this.watchFiles.clear() - this.memoryFiles.clear() - }), - ]) - } - - private async getRolldownOptions() { - const chunkMetadataMap = new Map() - const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap) - rolldownOptions.experimental ??= {} - rolldownOptions.experimental.hmr = { - implement: await getHmrImplementation(this.getTopLevelConfig()), - } - - rolldownOptions.treeshake = false - - return rolldownOptions - } - - private async triggerGenerateInitialBundle( - outOpts: RolldownOptions['output'], - ) { - this.generateBundle(outOpts).then( - () => { - debug?.('initial bundle generated') - }, - (e) => { - enhanceRollupError(e) - clearLine() - this.logger.error(`${colors.red('✗')} Build failed` + e.stack) - // TODO: show error message on the browser - }, - ) - } - - // TODO: should debounce this - private async generateBundle(outOpts: RolldownOptions['output']) { - for (const outputOpts of arraify(outOpts)) { - const output = await this.bundle!.generate(outputOpts) - for (const outputFile of output.output) { - this.memoryFiles.set( - outputFile.fileName, - outputFile.type === 'chunk' ? outputFile.code : outputFile.source, - ) - } - } +// https://mermaid.live/edit#pako:eNqdUk1v4jAQ_SujuRSkFAUMJOSwalWuPXVPq0jIjYfEWmeMHKe0i_jvaxJoqcRuUX2x3se8mZFmh4VVhBmujd0WlXQefi5zhvAaH9Bg0H3DIdze_gDN2mtpev0IOuG5ZWU0l71yQkECcs66Dw-tOuLMd2QO3rU2BGEILumL1OudTVsU1DRnE6jz5upSWklMTvqQsKpqt9pIX1R90SXl0pbq__bTUIPADr9RxhY-V76v_q_S61bsM-7vdtBUckMZeHr1ERj5TCaDHLcVMRC_aGe5JvagGyiMbUhFoD1stTFQWvAWbo7XcZMj7HPGCGtytdQqnNru0CZHX1FNOR5ylXS_c8x5H3yy9fbpjQvMvGspQmfbssJsLU0TULtR0tNSy9LJ-p3dSP5lbX0qIaW9dY_9YXf3HWHpDr2PkcSK3INt2WM2XswnXQJmO3wNOJmOxCIdx0ksRDwX4zTCN8zS-SidTRbxNAlkIvYR_uk6xqNkFk9TMZ2JSSKSREz2fwERkhWq +type BundleState = + | BundleStateInitial + | BundleStateBundling + | BundleStateBundled + | BundleStateBundleError + | BundleStateGeneratingHmrPatch +type BundleStateInitial = { type: 'initial' } +type BundleStateBundling = { + type: 'bundling' + promise: Promise + abortController: AbortController +} & BundleStateCommonProperties +type BundleStateBundled = { type: 'bundled' } & BundleStateCommonProperties +type BundleStateBundleError = { + type: 'bundle-error' +} & BundleStateCommonProperties +type BundleStateGeneratingHmrPatch = { + type: 'generating-hmr-patch' +} & BundleStateCommonProperties - // TODO: should this be done for hmr patch file generation? - for (const file of await this.bundle!.watchFiles) { - this.watchFiles.add(file) - } - } +type BundleStateCommonProperties = { + options: RolldownOptions + bundle: RolldownBuild } diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index 11cf734db28aa5..d65b33de4b2cfe 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -49,6 +49,7 @@ import { basePluginContextMeta, } from '../pluginContainer' import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment' +import { getHmrImplementation } from '../../plugins/clientInjections' interface AssetNode { start: number @@ -457,10 +458,11 @@ export function indexHtmlMiddleware( if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') { if (memoryFiles) { const cleanedUrl = cleanUrl(url).slice(1) // remove first / - const content = memoryFiles.get(cleanedUrl) - if (!content) { + let content = memoryFiles.get(cleanedUrl) + if (!content && memoryFiles.size !== 0) { return next() } + content ??= await generateFallbackHtml(server as ViteDevServer) const html = typeof content === 'string' ? content : Buffer.from(content.buffer) @@ -510,3 +512,50 @@ function preTransformRequest( decodedUrl = unwrapId(stripBase(decodedUrl, decodedBase)) server.warmupRequest(decodedUrl) } + +async function generateFallbackHtml(server: ViteDevServer) { + const hmrRuntime = await getHmrImplementation(server.config) + return /* html */ ` + + + + ', '<\\/script>')} + + + + +
+

Bundling in progress

+

The page will automatically reload when ready.

+
+
+ + +` +} From 95fbccb36ac40be86316f70fb1e490e9fd391cd4 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:03:13 +0900 Subject: [PATCH 13/35] wip: full bundle dev env --- packages/vite/src/client/client.ts | 62 ++----- packages/vite/src/node/server/environment.ts | 2 +- .../environments/fullBundleEnvironment.ts | 154 ++++++++++++++++-- packages/vite/src/node/server/hmr.ts | 2 +- .../src/node/server/middlewares/indexHtml.ts | 17 +- 5 files changed, 161 insertions(+), 76 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index edc24da0d36179..5eb8dc03e3c269 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -1,3 +1,4 @@ +/// import type { ErrorPayload, HotPayload } from 'types/hmrPayload' import type { ViteHotContext } from 'types/hot' import { HMRClient, HMRContext } from '../shared/hmr' @@ -600,67 +601,26 @@ export function injectQuery(url: string, queryToInject: string): string { export { ErrorOverlay } if (isFullBundleMode) { - class DevRuntime { - modules: Record = {} - - static getInstance() { - // @ts-expect-error __rolldown_runtime__ - let instance = globalThis.__rolldown_runtime__ - if (!instance) { - instance = new DevRuntime() - // @ts-expect-error __rolldown_runtime__ - globalThis.__rolldown_runtime__ = instance - } - return instance - } - - createModuleHotContext(moduleId: string) { + class ViteDevRuntime extends DevRuntime { + override createModuleHotContext(moduleId: string) { const ctx = createHotContext(moduleId) // @ts-expect-error TODO: support CSS ctx._internal = { updateStyle, removeStyle, } + // @ts-expect-error TODO: support this function (used by plugin-react) + ctx.getExports = async () => + // @ts-expect-error __rolldown_runtime__ / ctx.ownerPath + __rolldown_runtime__.loadExports(ctx.ownerPath) return ctx } - applyUpdates(_boundaries: string[]) { - // - } - - registerModule( - id: string, - module: { exports: Record unknown> }, - ) { - this.modules[id] = module + override applyUpdates(_boundaries: string[]): void { + // TODO: how should this be handled? + // noop, handled in the HMR client } - - loadExports(id: string) { - const module = this.modules[id] - if (module) { - return module.exports - } else { - console.warn(`Module ${id} not found`) - return {} - } - } - - // __esmMin - // @ts-expect-error need to add typing - createEsmInitializer = (fn, res) => () => (fn && (res = fn((fn = 0))), res) - // __commonJSMin - // @ts-expect-error need to add typing - createCjsInitializer = (cb, mod) => () => ( - mod || cb((mod = { exports: {} }).exports, mod), mod.exports - ) - // @ts-expect-error it is exits - __toESM = __toESM - // @ts-expect-error it is exits - __toCommonJS = __toCommonJS - // @ts-expect-error it is exits - __export = __export } - // @ts-expect-error __rolldown_runtime__ - globalThis.__rolldown_runtime__ ||= new DevRuntime() + ;(globalThis as any).__rolldown_runtime__ ??= new ViteDevRuntime() } diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 9cb16d27b2b2ca..1177d76ab27ee1 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -243,7 +243,7 @@ export class DevEnvironment extends BaseEnvironment { } } - private invalidateModule(m: { + protected invalidateModule(m: { path: string message?: string firstInvalidatedBy: string diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 8ef06b9796ad1c..2d5ef0362513ad 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -1,3 +1,4 @@ +import path from 'node:path' import type { RolldownBuild, RolldownOptions } from 'rolldown' import type { Update } from 'types/hmrPayload' import colors from 'picocolors' @@ -11,7 +12,7 @@ import { getHmrImplementation } from '../../plugins/clientInjections' import { DevEnvironment, type DevEnvironmentContext } from '../environment' import type { ResolvedConfig } from '../../config' import type { ViteDevServer } from '../../server' -import { arraify, createDebugger } from '../../utils' +import { arraify, createDebugger, normalizePath } from '../../utils' import { prepareError } from '../middlewares/error' const debug = createDebugger('vite:full-bundle-mode') @@ -57,7 +58,6 @@ export class FullBundleDevEnvironment extends DevEnvironment { async onFileChange( _type: 'create' | 'update' | 'delete', file: string, - server: ViteDevServer, ): Promise { if (this.state.type === 'initial') { return @@ -67,15 +67,21 @@ export class FullBundleDevEnvironment extends DevEnvironment { debug?.( `BUNDLING: file update detected ${file}, retriggering bundle generation`, ) - this.state.abortController.abort() this.triggerGenerateBundle(this.state) return } if (this.state.type === 'bundle-error') { - debug?.( - `BUNDLE-ERROR: file update detected ${file}, retriggering bundle generation`, - ) - this.triggerGenerateBundle(this.state) + const files = await this.state.bundle.watchFiles + if (files.includes(file)) { + debug?.( + `BUNDLE-ERROR: file update detected ${file}, retriggering bundle generation`, + ) + this.triggerGenerateBundle(this.state) + } else { + debug?.( + `BUNDLE-ERROR: file update detected ${file}, but ignored as it is not a dependency`, + ) + } return } @@ -95,35 +101,111 @@ export class FullBundleDevEnvironment extends DevEnvironment { type: 'generating-hmr-patch', options: this.state.options, bundle: this.state.bundle, + patched: this.state.patched, } let hmrOutput: HmrOutput try { // NOTE: only single outputOptions is supported here - hmrOutput = (await this.state.bundle.generateHmrPatch([file]))! + hmrOutput = await this.state.bundle.generateHmrPatch([file]) } catch (e) { // TODO: support multiple errors - server.ws.send({ type: 'error', err: prepareError(e.errors[0]) }) + this.hot.send({ type: 'error', err: prepareError(e.errors[0]) }) this.state = { type: 'bundled', options: this.state.options, bundle: this.state.bundle, + patched: this.state.patched, } return } - debug?.(`handle hmr output for ${file}`, { - ...hmrOutput, - code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, - }) - this.handleHmrOutput(file, hmrOutput, this.state) return } this.state satisfies never // exhaustive check } + protected override invalidateModule(m: { + path: string + message?: string + firstInvalidatedBy: string + }): void { + ;(async () => { + if ( + this.state.type === 'initial' || + this.state.type === 'bundling' || + this.state.type === 'bundle-error' + ) { + debug?.( + `${this.state.type.toUpperCase()}: invalidate received, but ignored`, + ) + return + } + this.state.type satisfies 'bundled' | 'generating-hmr-patch' // exhaustive check + + debug?.( + `${this.state.type.toUpperCase()}: invalidate received, re-triggering HMR`, + ) + + // TODO: should this be a separate state? + this.state = { + type: 'generating-hmr-patch', + options: this.state.options, + bundle: this.state.bundle, + patched: this.state.patched, + } + + let hmrOutput: HmrOutput + try { + // NOTE: only single outputOptions is supported here + hmrOutput = await this.state.bundle.hmrInvalidate( + normalizePath(path.join(this.config.root, m.path)), + m.firstInvalidatedBy, + ) + } catch (e) { + // TODO: support multiple errors + this.hot.send({ type: 'error', err: prepareError(e.errors[0]) }) + + this.state = { + type: 'bundled', + options: this.state.options, + bundle: this.state.bundle, + patched: this.state.patched, + } + return + } + + if (hmrOutput.isSelfAccepting) { + this.logger.info( + colors.yellow(`hmr invalidate `) + + colors.dim(m.path) + + (m.message ? ` ${m.message}` : ''), + { timestamp: true }, + ) + } + + // TODO: need to check if this is enough + this.handleHmrOutput(m.path, hmrOutput, this.state) + })() + } + + triggerBundleRegenerationIfStale(): boolean { + if ( + (this.state.type === 'bundled' || + this.state.type === 'generating-hmr-patch') && + this.state.patched + ) { + this.triggerGenerateBundle(this.state) + debug?.( + `${this.state.type.toUpperCase()}: access to stale bundle, triggered bundle re-generation`, + ) + return true + } + return false + } + override async close(): Promise { await Promise.all([ super.close(), @@ -161,6 +243,10 @@ export class FullBundleDevEnvironment extends DevEnvironment { options, bundle, }: BundleStateCommonProperties) { + if (this.state.type === 'bundling') { + this.state.abortController.abort() + } + const controller = new AbortController() const promise = this.generateBundle( options.output, @@ -211,6 +297,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { type: 'bundled', bundle: this.state.bundle, options: this.state.options, + patched: false, } debug?.('BUNDLED: bundle generated') @@ -234,7 +321,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { } } - private async handleHmrOutput( + private handleHmrOutput( file: string, hmrOutput: HmrOutput, { options, bundle }: BundleStateCommonProperties, @@ -255,7 +342,16 @@ export class FullBundleDevEnvironment extends DevEnvironment { return } - if (hmrOutput.code) { + // TODO: handle `No corresponding module found for changed file path` + if ( + hmrOutput.code && + hmrOutput.code !== '__rolldown_runtime__.applyUpdates([]);' + ) { + debug?.(`handle hmr output for ${file}`, { + ...hmrOutput, + code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, + }) + this.memoryFiles.set(hmrOutput.filename, hmrOutput.code) if (hmrOutput.sourcemapFilename && hmrOutput.sourcemap) { this.memoryFiles.set(hmrOutput.sourcemapFilename, hmrOutput.sourcemap) @@ -279,7 +375,17 @@ export class FullBundleDevEnvironment extends DevEnvironment { colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), { clear: !hmrOutput.firstInvalidatedBy, timestamp: true }, ) + + this.state = { + type: 'bundled', + options, + bundle, + patched: true, + } + return } + + debug?.(`ignored file change for ${file}`) } } @@ -296,12 +402,26 @@ type BundleStateBundling = { promise: Promise abortController: AbortController } & BundleStateCommonProperties -type BundleStateBundled = { type: 'bundled' } & BundleStateCommonProperties +type BundleStateBundled = { + type: 'bundled' + /** + * Whether a hmr patch was generated. + * + * In other words, whether the bundle is stale. + */ + patched: boolean +} & BundleStateCommonProperties type BundleStateBundleError = { type: 'bundle-error' } & BundleStateCommonProperties type BundleStateGeneratingHmrPatch = { type: 'generating-hmr-patch' + /** + * Whether a hmr patch was generated. + * + * In other words, whether the bundle is stale. + */ + patched: boolean } & BundleStateCommonProperties type BundleStateCommonProperties = { diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index f350b63695add3..7d29f86f208968 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -422,7 +422,7 @@ export async function handleHMRUpdate( if (config.experimental.fullBundleMode) { // TODO: support handleHotUpdate / hotUpdate const environment = server.environments.client as FullBundleDevEnvironment - environment.onFileChange(type, file, server) + environment.onFileChange(type, file) return } diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index d65b33de4b2cfe..1901521ff1f5fb 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -442,9 +442,9 @@ export function indexHtmlMiddleware( server: ViteDevServer | PreviewServer, ): Connect.NextHandleFunction { const isDev = isDevServer(server) - const memoryFiles = + const fullBundleEnv = isDev && server.environments.client instanceof FullBundleDevEnvironment - ? server.environments.client.memoryFiles + ? server.environments.client : undefined // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` @@ -456,13 +456,18 @@ export function indexHtmlMiddleware( const url = req.url && cleanUrl(req.url) // htmlFallbackMiddleware appends '.html' to URLs if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') { - if (memoryFiles) { + if (fullBundleEnv) { const cleanedUrl = cleanUrl(url).slice(1) // remove first / - let content = memoryFiles.get(cleanedUrl) - if (!content && memoryFiles.size !== 0) { + let content = fullBundleEnv.memoryFiles.get(cleanedUrl) + if (!content && fullBundleEnv.memoryFiles.size !== 0) { return next() } - content ??= await generateFallbackHtml(server as ViteDevServer) + if ( + fullBundleEnv.triggerBundleRegenerationIfStale() || + content === undefined + ) { + content = await generateFallbackHtml(server as ViteDevServer) + } const html = typeof content === 'string' ? content : Buffer.from(content.buffer) From 73932655fcf15e9c923d5ef759fce32fa6441cbb Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 10 Jun 2025 20:22:08 +0900 Subject: [PATCH 14/35] test: add test for basic scenarios --- .../__tests__/hmr-full-bundle-mode.spec.ts | 123 ++++++++++++++++++ playground/hmr-full-bundle-mode/hmr.js | 13 ++ playground/hmr-full-bundle-mode/index.html | 6 + playground/hmr-full-bundle-mode/main.js | 7 + playground/hmr-full-bundle-mode/package.json | 12 ++ .../hmr-full-bundle-mode/vite.config.ts | 59 +++++++++ pnpm-lock.yaml | 2 + 7 files changed, 222 insertions(+) create mode 100644 playground/hmr-full-bundle-mode/__tests__/hmr-full-bundle-mode.spec.ts create mode 100644 playground/hmr-full-bundle-mode/hmr.js create mode 100644 playground/hmr-full-bundle-mode/index.html create mode 100644 playground/hmr-full-bundle-mode/main.js create mode 100644 playground/hmr-full-bundle-mode/package.json create mode 100644 playground/hmr-full-bundle-mode/vite.config.ts 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 @@ +

HMR Full Bundle Mode

+ +
+
+ + diff --git a/playground/hmr-full-bundle-mode/main.js b/playground/hmr-full-bundle-mode/main.js new file mode 100644 index 00000000000000..3a8003456f6a3e --- /dev/null +++ b/playground/hmr-full-bundle-mode/main.js @@ -0,0 +1,7 @@ +import './hmr.js' + +text('.app', 'hello') + +function text(el, text) { + document.querySelector(el).textContent = text +} diff --git a/playground/hmr-full-bundle-mode/package.json b/playground/hmr-full-bundle-mode/package.json new file mode 100644 index 00000000000000..dcd3f5e9ed014b --- /dev/null +++ b/playground/hmr-full-bundle-mode/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-hmr-full-bundle-mode", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + } +} diff --git a/playground/hmr-full-bundle-mode/vite.config.ts b/playground/hmr-full-bundle-mode/vite.config.ts new file mode 100644 index 00000000000000..2471a1eb5ead97 --- /dev/null +++ b/playground/hmr-full-bundle-mode/vite.config.ts @@ -0,0 +1,59 @@ +import { type Plugin, defineConfig } from 'vite' + +export default defineConfig({ + experimental: { + fullBundleMode: true, + }, + plugins: [waitBundleCompleteUntilAccess(), delayTransformComment()], +}) + +function waitBundleCompleteUntilAccess(): Plugin { + let resolvers: PromiseWithResolvers + + return { + name: 'wait-bundle-complete-until-access', + apply: 'serve', + configureServer(server) { + let accessCount = 0 + resolvers = promiseWithResolvers() + + server.middlewares.use((_req, _res, next) => { + accessCount++ + if (accessCount === 1) { + resolvers.resolve() + } + next() + }) + }, + async generateBundle() { + await resolvers.promise + await new Promise((resolve) => setTimeout(resolve, 300)) + }, + } +} + +function delayTransformComment(): Plugin { + return { + name: 'delay-transform-comment', + async transform(code) { + if (code.includes('// @delay-transform')) { + await new Promise((resolve) => setTimeout(resolve, 300)) + } + }, + } +} + +interface PromiseWithResolvers { + promise: Promise + resolve: (value: T | PromiseLike) => void + reject: (reason?: any) => void +} +function promiseWithResolvers(): PromiseWithResolvers { + let resolve: any + let reject: any + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + return { promise, resolve, reject } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 550cb6bdc0eb8f..49e1a1654ca11d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -825,6 +825,8 @@ importers: playground/hmr: {} + playground/hmr-full-bundle-mode: {} + playground/hmr-ssr: {} playground/html: {} From fe1db45511ae6679a71177ed8ebe1edf790bb136 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:17:30 +0900 Subject: [PATCH 15/35] wip: support assets --- packages/vite/src/node/plugins/asset.ts | 50 +++++++++++++------ .../environments/fullBundleEnvironment.ts | 14 ++++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index c4e8259bb2e414..c8abc3372ecf1f 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -237,20 +237,24 @@ export function assetPlugin(config: ResolvedConfig): Plugin { }, }, - renderChunk(code, chunk, opts) { - const s = renderAssetUrlInJS(this, chunk, opts, code) - - if (s) { - return { - code: s.toString(), - map: this.environment.config.build.sourcemap - ? s.generateMap({ hires: 'boundary' }) - : null, + ...(config.command === 'build' + ? { + renderChunk(code, chunk, opts) { + const s = renderAssetUrlInJS(this, chunk, opts, code) + + if (s) { + return { + code: s.toString(), + map: this.environment.config.build.sourcemap + ? s.generateMap({ hires: 'boundary' }) + : null, + } + } else { + return null + } + }, } - } else { - return null - } - }, + : {}), generateBundle(_, bundle) { // Remove empty entry point file @@ -308,7 +312,7 @@ export async function fileToUrl( id: string, ): Promise { const { environment } = pluginContext - if (environment.config.command === 'serve') { + if (!environment.config.isBundled) { return fileToDevUrl(environment, id) } else { return fileToBuiltUrl(pluginContext, id) @@ -457,7 +461,23 @@ async function fileToBuiltUrl( postfix = postfix.replace(noInlineRE, '').replace(/^&/, '?') } - url = `__VITE_ASSET__${referenceId}__${postfix ? `$_${postfix}__` : ``}` + if (environment.config.experimental.fullBundleMode) { + const outputFilename = pluginContext.getFileName(referenceId) + const outputUrl = toOutputFilePathInJS( + environment, + outputFilename, + 'asset', + 'assets/dummy.js', + 'js', + () => { + throw new Error('unreachable') + }, + ) + if (typeof outputUrl === 'object') throw new Error('not supported') + url = outputUrl + } else { + url = `__VITE_ASSET__${referenceId}__${postfix ? `$_${postfix}__` : ``}` + } } cache.set(id, url) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 2d5ef0362513ad..2c5258c8a48e36 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -236,6 +236,20 @@ export class FullBundleDevEnvironment extends DevEnvironment { rolldownOptions.treeshake = false + // set filenames to make output paths predictable so that `renderChunk` hook does not need to be used + if (Array.isArray(rolldownOptions.output)) { + for (const output of rolldownOptions.output) { + output.entryFileNames = 'assets/[name].js' + output.chunkFileNames = 'assets/[name]-[hash].js' + output.assetFileNames = 'assets/[name]-[hash][extname]' + } + } else { + rolldownOptions.output ??= {} + rolldownOptions.output.entryFileNames = 'assets/[name].js' + rolldownOptions.output.chunkFileNames = 'assets/[name]-[hash].js' + rolldownOptions.output.assetFileNames = 'assets/[name]-[hash][extname]' + } + return rolldownOptions } From a88c1bc357acfc476f4b46a707944b439191c1e2 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:55:29 +0900 Subject: [PATCH 16/35] wip: update for new rolldown --- .../environments/fullBundleEnvironment.ts | 80 +++++++++---------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 2c5258c8a48e36..64eb4a2a24f91e 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -104,7 +104,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { patched: this.state.patched, } - let hmrOutput: HmrOutput + let hmrOutput: HmrOutput | undefined try { // NOTE: only single outputOptions is supported here hmrOutput = await this.state.bundle.generateHmrPatch([file]) @@ -121,6 +121,11 @@ export class FullBundleDevEnvironment extends DevEnvironment { return } + if (!hmrOutput) { + debug?.(`ignored file change for ${file}`) + return + } + this.handleHmrOutput(file, hmrOutput, this.state) return } @@ -356,50 +361,41 @@ export class FullBundleDevEnvironment extends DevEnvironment { return } - // TODO: handle `No corresponding module found for changed file path` - if ( - hmrOutput.code && - hmrOutput.code !== '__rolldown_runtime__.applyUpdates([]);' - ) { - debug?.(`handle hmr output for ${file}`, { - ...hmrOutput, - code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, - }) - - this.memoryFiles.set(hmrOutput.filename, hmrOutput.code) - if (hmrOutput.sourcemapFilename && hmrOutput.sourcemap) { - this.memoryFiles.set(hmrOutput.sourcemapFilename, hmrOutput.sourcemap) - } - const updates: Update[] = hmrOutput.hmrBoundaries.map((boundary: any) => { - return { - type: 'js-update', - url: hmrOutput.filename, - path: boundary.boundary, - acceptedPath: boundary.acceptedVia, - firstInvalidatedBy: hmrOutput.firstInvalidatedBy, - timestamp: 0, - } - }) - this.hot.send({ - type: 'update', - updates, - }) - this.logger.info( - colors.green(`hmr update `) + - colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), - { clear: !hmrOutput.firstInvalidatedBy, timestamp: true }, - ) + debug?.(`handle hmr output for ${file}`, { + ...hmrOutput, + code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, + }) - this.state = { - type: 'bundled', - options, - bundle, - patched: true, - } - return + this.memoryFiles.set(hmrOutput.filename, hmrOutput.code) + if (hmrOutput.sourcemapFilename && hmrOutput.sourcemap) { + this.memoryFiles.set(hmrOutput.sourcemapFilename, hmrOutput.sourcemap) } + const updates: Update[] = hmrOutput.hmrBoundaries.map((boundary: any) => { + return { + type: 'js-update', + url: hmrOutput.filename, + path: boundary.boundary, + acceptedPath: boundary.acceptedVia, + firstInvalidatedBy: hmrOutput.firstInvalidatedBy, + timestamp: 0, + } + }) + this.hot.send({ + type: 'update', + updates, + }) + this.logger.info( + colors.green(`hmr update `) + + colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), + { clear: !hmrOutput.firstInvalidatedBy, timestamp: true }, + ) - debug?.(`ignored file change for ${file}`) + this.state = { + type: 'bundled', + options, + bundle, + patched: true, + } } } From 4f63f5eb031d04a693dc77c11f39230835e630ea Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:38:21 +0900 Subject: [PATCH 17/35] perf: skip warmup requests --- .../src/node/server/environments/fullBundleEnvironment.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 64eb4a2a24f91e..1891d17c7d75fc 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -42,8 +42,8 @@ export class FullBundleDevEnvironment extends DevEnvironment { super(name, config, { ...context, disableDepsOptimizer: true }) } - override async listen(server: ViteDevServer): Promise { - await super.listen(server) + override async listen(_server: ViteDevServer): Promise { + this.hot.listen() debug?.('INITIAL: setup bundle options') const rollupOptions = await this.getRolldownOptions() @@ -132,6 +132,10 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.state satisfies never // exhaustive check } + override async warmupRequest(_url: string): Promise { + // no-op + } + protected override invalidateModule(m: { path: string message?: string From 8f07c70560281112eb653c0f4fd591f17e78edca Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:38:41 +0900 Subject: [PATCH 18/35] perf: avoid buildStart hook call --- packages/vite/src/node/server/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 3a032295334e9e..c17cd7b5fffe39 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -979,10 +979,12 @@ export async function _createServer( if (initingServer) return initingServer initingServer = (async function () { - // For backward compatibility, we call buildStart for the client - // environment when initing the server. For other environments - // buildStart will be called when the first request is transformed - await environments.client.pluginContainer.buildStart() + if (!config.experimental.fullBundleMode) { + // For backward compatibility, we call buildStart for the client + // environment when initing the server. For other environments + // buildStart will be called when the first request is transformed + await environments.client.pluginContainer.buildStart() + } // ensure ws server started if (onListen || options.listen) { From ec219b87d7d0807cda64e9d21bbab33804c6b702 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 18 Jul 2025 17:58:09 +0900 Subject: [PATCH 19/35] wip: full bundle dev env --- .../node/server/environments/fullBundleEnvironment.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 1891d17c7d75fc..05f6b157d6ab7e 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -64,6 +64,8 @@ export class FullBundleDevEnvironment extends DevEnvironment { } if (this.state.type === 'bundling') { + // FIXME: we should retrigger only when we know that file is watched. + // but for the initial bundle we don't know that and need to trigger after the initial bundle debug?.( `BUNDLING: file update detected ${file}, retriggering bundle generation`, ) @@ -291,6 +293,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { signal: AbortSignal, ) { try { + const startTime = Date.now() const newMemoryFiles = new Map() for (const outputOpts of arraify(outOpts)) { const output = await bundle.generate(outputOpts) @@ -303,6 +306,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { ) } } + const generateTime = Date.now() this.memoryFiles.clear() for (const [file, code] of newMemoryFiles) { @@ -314,6 +318,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.watchFiles.add(file) } if (signal.aborted) return + const postGenerateTime = Date.now() if (this.state.type === 'initial') throw new Error('unreachable') this.state = { @@ -322,7 +327,9 @@ export class FullBundleDevEnvironment extends DevEnvironment { options: this.state.options, patched: false, } - debug?.('BUNDLED: bundle generated') + debug?.( + `BUNDLED: bundle generated in ${generateTime - startTime}ms + ${postGenerateTime - generateTime}ms`, + ) this.hot.send({ type: 'full-reload' }) this.logger.info(colors.green(`page reload`), { timestamp: true }) From 5debc64a770a90575ef357c7f268bb71ddd167ee Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:03:56 +0900 Subject: [PATCH 20/35] wip: update for new rolldown --- .../environments/fullBundleEnvironment.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 05f6b157d6ab7e..d89e2c0d6ca1bc 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -2,8 +2,8 @@ import path from 'node:path' import type { RolldownBuild, RolldownOptions } from 'rolldown' import type { Update } from 'types/hmrPayload' import colors from 'picocolors' -import type { ChunkMetadata } from 'types/metadata' import { + ChunkMetadataMap, clearLine, enhanceRollupError, resolveRolldownOptions, @@ -18,7 +18,7 @@ import { prepareError } from '../middlewares/error' const debug = createDebugger('vite:full-bundle-mode') type HmrOutput = Exclude< - Awaited>, + Awaited>, undefined > @@ -106,7 +106,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { patched: this.state.patched, } - let hmrOutput: HmrOutput | undefined + let hmrOutput: HmrOutput[] try { // NOTE: only single outputOptions is supported here hmrOutput = await this.state.bundle.generateHmrPatch([file]) @@ -123,12 +123,14 @@ export class FullBundleDevEnvironment extends DevEnvironment { return } - if (!hmrOutput) { + if (hmrOutput.every((output) => output.type === 'Noop')) { debug?.(`ignored file change for ${file}`) return } - this.handleHmrOutput(file, hmrOutput, this.state) + for (const output of hmrOutput) { + this.handleHmrOutput(file, output, this.state) + } return } this.state satisfies never // exhaustive check @@ -188,7 +190,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { return } - if (hmrOutput.isSelfAccepting) { + if (hmrOutput.type === 'Patch') { this.logger.info( colors.yellow(`hmr invalidate `) + colors.dim(m.path) + @@ -198,7 +200,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { } // TODO: need to check if this is enough - this.handleHmrOutput(m.path, hmrOutput, this.state) + this.handleHmrOutput(m.path, hmrOutput, this.state, m.firstInvalidatedBy) })() } @@ -238,7 +240,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { } private async getRolldownOptions() { - const chunkMetadataMap = new Map() + const chunkMetadataMap = new ChunkMetadataMap() const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap) rolldownOptions.experimental ??= {} rolldownOptions.experimental.hmr = { @@ -355,19 +357,19 @@ export class FullBundleDevEnvironment extends DevEnvironment { file: string, hmrOutput: HmrOutput, { options, bundle }: BundleStateCommonProperties, + firstInvalidatedBy?: string, ) { - if (hmrOutput.fullReload) { + if (hmrOutput.type === 'Noop') return + + if (hmrOutput.type === 'FullReload') { this.triggerGenerateBundle({ options, bundle }) - const reason = hmrOutput.fullReloadReason - ? colors.dim(` (${hmrOutput.fullReloadReason})`) + const reason = hmrOutput.reason + ? colors.dim(` (${hmrOutput.reason})`) : '' this.logger.info( colors.green(`trigger page reload `) + colors.dim(file) + reason, - { - // clear: !hmrOutput.firstInvalidatedBy, - timestamp: true, - }, + { clear: !firstInvalidatedBy, timestamp: true }, ) return } @@ -387,7 +389,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { url: hmrOutput.filename, path: boundary.boundary, acceptedPath: boundary.acceptedVia, - firstInvalidatedBy: hmrOutput.firstInvalidatedBy, + firstInvalidatedBy, timestamp: 0, } }) @@ -398,7 +400,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.logger.info( colors.green(`hmr update `) + colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), - { clear: !hmrOutput.firstInvalidatedBy, timestamp: true }, + { clear: !firstInvalidatedBy, timestamp: true }, ) this.state = { From 3b603499ba904e4fac8dcee7f9384bd61f725e34 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:59:46 +0900 Subject: [PATCH 21/35] wip: simplify --- .../vite/src/node/server/environments/fullBundleEnvironment.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index d89e2c0d6ca1bc..adb879a9a9183d 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -247,8 +247,6 @@ export class FullBundleDevEnvironment extends DevEnvironment { implement: await getHmrImplementation(this.getTopLevelConfig()), } - rolldownOptions.treeshake = false - // set filenames to make output paths predictable so that `renderChunk` hook does not need to be used if (Array.isArray(rolldownOptions.output)) { for (const output of rolldownOptions.output) { From fb8aa43b3b9bc56320f734faaef327d041daaca1 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:00:12 +0900 Subject: [PATCH 22/35] wip: skip optimizerResolvePlugin --- packages/vite/src/node/plugins/resolve.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 85193aae847433..faa746bdadb334 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -225,7 +225,7 @@ export function oxcResolvePlugin( overrideEnvConfig: (ResolvedConfig & ResolvedEnvironmentOptions) | undefined, ): Plugin[] { return [ - ...(!resolveOptions.isBuild + ...(resolveOptions.optimizeDeps && !resolveOptions.isBuild ? [optimizerResolvePlugin(resolveOptions)] : []), ...perEnvironmentOrWorkerPlugin( @@ -381,6 +381,12 @@ function optimizerResolvePlugin( return { name: 'vite:resolve-dev', + applyToEnvironment(environment) { + return ( + !environment.config.experimental.fullBundleMode && + !isDepOptimizationDisabled(environment.config.optimizeDeps) + ) + }, resolveId: { filter: { id: { From dce45559c9a6702c3191a57c206129d31558946d Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:00:29 +0900 Subject: [PATCH 23/35] wip: change flag to --full-bundle --- packages/vite/src/node/cli.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 2461a639aed1e7..44d7a8429eb17b 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -54,7 +54,7 @@ interface GlobalCLIOptions { } interface ExperimentalDevOptions { - fullBundleMode?: boolean + fullBundle?: boolean } interface BuilderCLIOptions { @@ -199,7 +199,7 @@ cli '--force', `[boolean] force the optimizer to ignore the cache and re-bundle`, ) - .option('--fullBundleMode', `[boolean] use experimental full bundle mode`) + .option('--fullBundle', `[boolean] use experimental full bundle mode`) .action( async ( root: string, @@ -221,7 +221,7 @@ cli server: cleanGlobalCLIOptions(options), forceOptimizeDeps: options.force, experimental: { - fullBundleMode: options.fullBundleMode, + fullBundleMode: options.fullBundle, }, }) From 9f6eccf3a95c8ce250aadd8f38b21374100dff74 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 1 Aug 2025 19:48:50 +0900 Subject: [PATCH 24/35] wip: fix dynamic import vars plugin --- packages/vite/src/node/plugins/dynamicImportVars.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index 5cdcf5c80244d1..f3a8d6689cfbce 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -167,17 +167,13 @@ export async function transformDynamicImport( } export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { - if (config.experimental.enableNativePlugin === true && config.isBundled) { - return nativeDynamicImportVarsPlugin() - } - const resolve = createBackCompatIdResolver(config, { preferRelative: true, tryIndex: false, extensions: [], }) - if (config.command === 'build' && config.nativePluginEnabledLevel >= 1) { + if (config.isBundled && config.nativePluginEnabledLevel >= 1) { return perEnvironmentPlugin('native:dynamic-import-vars', (environment) => { const { include, exclude } = environment.config.build.dynamicImportVarsOptions From 87a799d83c2fc99459575a6b98a130b96cc66b82 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:16:12 +0900 Subject: [PATCH 25/35] wip: fix define/modulePreloadPolyfill plugin --- packages/vite/src/node/plugins/define.ts | 2 +- packages/vite/src/node/plugins/modulePreloadPolyfill.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 001d472736a154..c40c531ccd07e4 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -118,7 +118,7 @@ export function definePlugin(config: ResolvedConfig): Plugin { return pattern } - if (isBuild && config.nativePluginEnabledLevel >= 1) { + if (isBundled && config.nativePluginEnabledLevel >= 1) { return { name: 'vite:define', options(option) { diff --git a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts index 8c1cc3b8c55be5..9fa3fa28c4e4e9 100644 --- a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts +++ b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts @@ -8,7 +8,7 @@ export const modulePreloadPolyfillId = 'vite/modulepreload-polyfill' const resolvedModulePreloadPolyfillId = '\0' + modulePreloadPolyfillId + '.js' export function modulePreloadPolyfillPlugin(config: ResolvedConfig): Plugin { - if (config.command === 'build' && config.nativePluginEnabledLevel >= 1) { + if (config.isBundled && config.nativePluginEnabledLevel >= 1) { return perEnvironmentPlugin( 'native:modulepreload-polyfill', (environment) => { From 5e881ae0c0da235c5cedf60222af0e98018869bc Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:45:31 +0900 Subject: [PATCH 26/35] perf: skip worker renderChunk in dev --- packages/vite/src/node/plugins/worker.ts | 101 ++++++++++++----------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 0eab6354a18ce8..65efb6c9fb9c57 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -463,53 +463,62 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { }, }, - renderChunk(code, chunk, outputOptions) { - let s: MagicString - const result = () => { - return ( - s && { - code: s.toString(), - map: this.environment.config.build.sourcemap - ? s.generateMap({ hires: 'boundary' }) - : null, - } - ) - } - workerAssetUrlRE.lastIndex = 0 - if (workerAssetUrlRE.test(code)) { - const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime( - outputOptions.format, - this.environment.config.isWorker, - ) - - let match: RegExpExecArray | null - s = new MagicString(code) - workerAssetUrlRE.lastIndex = 0 - - // Replace "__VITE_WORKER_ASSET__5aa0ddc0__" using relative paths - const workerMap = workerCache.get(config.mainConfig || config)! - const { fileNameHash } = workerMap - - while ((match = workerAssetUrlRE.exec(code))) { - const [full, hash] = match - const filename = fileNameHash.get(hash)! - const replacement = toOutputFilePathInJS( - this.environment, - filename, - 'asset', - chunk.fileName, - 'js', - toRelativeRuntime, - ) - const replacementString = - typeof replacement === 'string' - ? JSON.stringify(encodeURIPath(replacement)).slice(1, -1) - : `"+${replacement.runtime}+"` - s.update(match.index, match.index + full.length, replacementString) + ...(isBuild + ? { + renderChunk(code, chunk, outputOptions) { + let s: MagicString + const result = () => { + return ( + s && { + code: s.toString(), + map: this.environment.config.build.sourcemap + ? s.generateMap({ hires: 'boundary' }) + : null, + } + ) + } + workerAssetUrlRE.lastIndex = 0 + if (workerAssetUrlRE.test(code)) { + const toRelativeRuntime = + createToImportMetaURLBasedRelativeRuntime( + outputOptions.format, + this.environment.config.isWorker, + ) + + let match: RegExpExecArray | null + s = new MagicString(code) + workerAssetUrlRE.lastIndex = 0 + + // Replace "__VITE_WORKER_ASSET__5aa0ddc0__" using relative paths + const workerMap = workerCache.get(config.mainConfig || config)! + const { fileNameHash } = workerMap + + while ((match = workerAssetUrlRE.exec(code))) { + const [full, hash] = match + const filename = fileNameHash.get(hash)! + const replacement = toOutputFilePathInJS( + this.environment, + filename, + 'asset', + chunk.fileName, + 'js', + toRelativeRuntime, + ) + const replacementString = + typeof replacement === 'string' + ? JSON.stringify(encodeURIPath(replacement)).slice(1, -1) + : `"+${replacement.runtime}+"` + s.update( + match.index, + match.index + full.length, + replacementString, + ) + } + } + return result() + }, } - } - return result() - }, + : {}), generateBundle(opts, bundle) { // to avoid emitting duplicate assets for modern build and legacy build From 850ec9af18e412f240cd4f7b0ab294c231de4ae3 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:45:45 +0900 Subject: [PATCH 27/35] wip: add debug time --- .../src/node/server/environments/fullBundleEnvironment.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index adb879a9a9183d..dd09043620c1da 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -106,6 +106,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { patched: this.state.patched, } + const startTime = Date.now() let hmrOutput: HmrOutput[] try { // NOTE: only single outputOptions is supported here @@ -127,6 +128,10 @@ export class FullBundleDevEnvironment extends DevEnvironment { debug?.(`ignored file change for ${file}`) return } + const generateTime = Date.now() + debug?.( + `GENERATING-HMR-PATCH: patch generated in ${generateTime - startTime}ms`, + ) for (const output of hmrOutput) { this.handleHmrOutput(file, output, this.state) From bfa60c4462c6aaa0adc7d513300337a0d060e4e0 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:52:00 +0900 Subject: [PATCH 28/35] perf: copy files lazily --- .../environments/fullBundleEnvironment.ts | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index dd09043620c1da..8eb540daf749a6 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -22,11 +22,50 @@ type HmrOutput = Exclude< undefined > +export class MemoryFiles { + private files = new Map< + string, + string | Uint8Array | (() => string | Uint8Array) + >() + + get size(): number { + return this.files.size + } + + get(file: string): string | Uint8Array | undefined { + const result = this.files.get(file) + if (result === undefined) { + return undefined + } + if (typeof result === 'function') { + const content = result() + this.files.set(file, content) + return content + } + return result + } + + set( + file: string, + content: string | Uint8Array | (() => string | Uint8Array), + ): void { + this.files.set(file, content) + } + + has(file: string): boolean { + return this.files.has(file) + } + + clear(): void { + this.files.clear() + } +} + export class FullBundleDevEnvironment extends DevEnvironment { private state: BundleState = { type: 'initial' } watchFiles = new Set() - memoryFiles = new Map() + memoryFiles = new MemoryFiles() constructor( name: string, @@ -299,14 +338,13 @@ export class FullBundleDevEnvironment extends DevEnvironment { ) { try { const startTime = Date.now() - const newMemoryFiles = new Map() + const newMemoryFiles = new Map string | Uint8Array>() for (const outputOpts of arraify(outOpts)) { const output = await bundle.generate(outputOpts) if (signal.aborted) return for (const outputFile of output.output) { - newMemoryFiles.set( - outputFile.fileName, + newMemoryFiles.set(outputFile.fileName, () => outputFile.type === 'chunk' ? outputFile.code : outputFile.source, ) } From 75a607b1bcd4e60c7cf2e43f070209e6ee48047a Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 5 Aug 2025 20:39:42 +0900 Subject: [PATCH 29/35] wip: disable renderBuiltUrl in dev --- packages/vite/src/node/config.ts | 4 ++++ packages/vite/src/node/plugins/asset.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 202f772e916b38..1cfcb032d700df 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -1790,6 +1790,10 @@ export async function resolveConfig( configDefaults.experimental, config.experimental ?? {}, ) + if (command === 'serve' && experimental.fullBundleMode) { + // full bundle mode does not support experimental.renderBuiltUrl + experimental.renderBuiltUrl = undefined + } resolved = { configFile: configFile ? normalizePath(configFile) : undefined, diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index c8abc3372ecf1f..88400718a2aa05 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -473,7 +473,7 @@ async function fileToBuiltUrl( throw new Error('unreachable') }, ) - if (typeof outputUrl === 'object') throw new Error('not supported') + if (typeof outputUrl === 'object') throw new Error('unreachable') url = outputUrl } else { url = `__VITE_ASSET__${referenceId}__${postfix ? `$_${postfix}__` : ``}` From 7ed9c4d24dbdbeea7c712b3f5f37f6bf6d5db346 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:30:17 +0900 Subject: [PATCH 30/35] wip: pass path as-is to `hmrInvalidate` --- .../src/node/server/environments/fullBundleEnvironment.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 8eb540daf749a6..24a03e6239b51b 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -1,4 +1,3 @@ -import path from 'node:path' import type { RolldownBuild, RolldownOptions } from 'rolldown' import type { Update } from 'types/hmrPayload' import colors from 'picocolors' @@ -12,7 +11,7 @@ import { getHmrImplementation } from '../../plugins/clientInjections' import { DevEnvironment, type DevEnvironmentContext } from '../environment' import type { ResolvedConfig } from '../../config' import type { ViteDevServer } from '../../server' -import { arraify, createDebugger, normalizePath } from '../../utils' +import { arraify, createDebugger } from '../../utils' import { prepareError } from '../middlewares/error' const debug = createDebugger('vite:full-bundle-mode') @@ -218,7 +217,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { try { // NOTE: only single outputOptions is supported here hmrOutput = await this.state.bundle.hmrInvalidate( - normalizePath(path.join(this.config.root, m.path)), + m.path, m.firstInvalidatedBy, ) } catch (e) { From 7e9e6c8ddf3d03e96b98e3708780e1a2b84e8b53 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:15:41 +0900 Subject: [PATCH 31/35] wip: full bundle dev env --- .../server/environments/fullBundleEnvironment.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 24a03e6239b51b..461e53e5e32e3d 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -62,6 +62,7 @@ export class MemoryFiles { export class FullBundleDevEnvironment extends DevEnvironment { private state: BundleState = { type: 'initial' } + private invalidateCalledModules = new Set() watchFiles = new Set() memoryFiles = new MemoryFiles() @@ -160,6 +161,8 @@ export class FullBundleDevEnvironment extends DevEnvironment { patched: this.state.patched, } return + } finally { + this.invalidateCalledModules.clear() } if (hmrOutput.every((output) => output.type === 'Noop')) { @@ -189,13 +192,20 @@ export class FullBundleDevEnvironment extends DevEnvironment { firstInvalidatedBy: string }): void { ;(async () => { + if (this.invalidateCalledModules.has(m.path)) { + debug?.( + `${this.state.type.toUpperCase()}: invalidate received, but ignored because it was already invalidated`, + ) + return + } + if ( this.state.type === 'initial' || this.state.type === 'bundling' || this.state.type === 'bundle-error' ) { debug?.( - `${this.state.type.toUpperCase()}: invalidate received, but ignored`, + `${this.state.type.toUpperCase()}: invalidate received, but ignored because the state type has changed`, ) return } @@ -212,6 +222,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { bundle: this.state.bundle, patched: this.state.patched, } + this.invalidateCalledModules.add(m.path) let hmrOutput: HmrOutput try { @@ -430,7 +441,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { path: boundary.boundary, acceptedPath: boundary.acceptedVia, firstInvalidatedBy, - timestamp: 0, + timestamp: Date.now(), } }) this.hot.send({ From 7e091e01f3bb58bf3829ed9671c5e95b5f7afa97 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:21:33 +0900 Subject: [PATCH 32/35] wip: full bundle dev env --- .../vite/src/node/server/middlewares/htmlFallback.ts | 9 +++++---- packages/vite/src/node/server/middlewares/indexHtml.ts | 6 ++++-- .../vite/src/node/server/middlewares/memoryFiles.ts | 10 +++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/node/server/middlewares/htmlFallback.ts b/packages/vite/src/node/server/middlewares/htmlFallback.ts index 54239902de8ea7..6729c924cf8283 100644 --- a/packages/vite/src/node/server/middlewares/htmlFallback.ts +++ b/packages/vite/src/node/server/middlewares/htmlFallback.ts @@ -1,7 +1,7 @@ import path from 'node:path' import fs from 'node:fs' import type { Connect } from 'dep-types/connect' -import { createDebugger } from '../../utils' +import { createDebugger, joinUrlSegments } from '../../utils' import { cleanUrl } from '../../../shared/utils' import type { DevEnvironment } from '../environment' import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment' @@ -20,8 +20,9 @@ export function htmlFallbackMiddleware( function checkFileExists(relativePath: string) { return ( - memoryFiles?.has(relativePath) ?? - fs.existsSync(path.join(root, relativePath)) + memoryFiles?.has( + relativePath.slice(1), // remove first / + ) ?? fs.existsSync(path.join(root, relativePath)) ) } @@ -57,7 +58,7 @@ export function htmlFallbackMiddleware( } // trailing slash should check for fallback index.html else if (pathname.endsWith('/')) { - if (checkFileExists(path.join(pathname, 'index.html'))) { + if (checkFileExists(joinUrlSegments(pathname, 'index.html'))) { const newUrl = url + 'index.html' debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`) req.url = newUrl diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index 1901521ff1f5fb..ccf9dbf7140268 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -457,8 +457,10 @@ export function indexHtmlMiddleware( // htmlFallbackMiddleware appends '.html' to URLs if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') { if (fullBundleEnv) { - const cleanedUrl = cleanUrl(url).slice(1) // remove first / - let content = fullBundleEnv.memoryFiles.get(cleanedUrl) + const pathname = decodeURIComponent(url) + const filePath = pathname.slice(1) // remove first / + + let content = fullBundleEnv.memoryFiles.get(filePath) if (!content && fullBundleEnv.memoryFiles.size !== 0) { return next() } diff --git a/packages/vite/src/node/server/middlewares/memoryFiles.ts b/packages/vite/src/node/server/middlewares/memoryFiles.ts index 5a28eb84c00302..76d4bdabb60bea 100644 --- a/packages/vite/src/node/server/middlewares/memoryFiles.ts +++ b/packages/vite/src/node/server/middlewares/memoryFiles.ts @@ -17,13 +17,17 @@ export function memoryFilesMiddleware( const headers = server.config.server.headers return function viteMemoryFilesMiddleware(req, res, next) { - const cleanedUrl = cleanUrl(req.url!).slice(1) // remove first / + const cleanedUrl = cleanUrl(req.url!) if (cleanedUrl.endsWith('.html')) { return next() } - const file = memoryFiles.get(cleanedUrl) + + const pathname = decodeURIComponent(cleanedUrl) + const filePath = pathname.slice(1) // remove first / + + const file = memoryFiles.get(filePath) if (file) { - const mime = mrmime.lookup(cleanedUrl) + const mime = mrmime.lookup(filePath) if (mime) { res.setHeader('Content-Type', mime) } From 122223eb116be32c463772eb822b9a9e8c67e757 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:24:20 +0900 Subject: [PATCH 33/35] wip: full bundle dev env --- .../src/node/server/environments/fullBundleEnvironment.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 461e53e5e32e3d..007834bf67690e 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -13,6 +13,7 @@ import type { ResolvedConfig } from '../../config' import type { ViteDevServer } from '../../server' import { arraify, createDebugger } from '../../utils' import { prepareError } from '../middlewares/error' +import { getShortName } from '../hmr' const debug = createDebugger('vite:full-bundle-mode') @@ -412,6 +413,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { ) { if (hmrOutput.type === 'Noop') return + const shortFile = getShortName(file, this.config.root) if (hmrOutput.type === 'FullReload') { this.triggerGenerateBundle({ options, bundle }) @@ -419,13 +421,13 @@ export class FullBundleDevEnvironment extends DevEnvironment { ? colors.dim(` (${hmrOutput.reason})`) : '' this.logger.info( - colors.green(`trigger page reload `) + colors.dim(file) + reason, + colors.green(`trigger page reload `) + colors.dim(shortFile) + reason, { clear: !firstInvalidatedBy, timestamp: true }, ) return } - debug?.(`handle hmr output for ${file}`, { + debug?.(`handle hmr output for ${shortFile}`, { ...hmrOutput, code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, }) From 73198745a079f90f9c21e54bd8e3ed779f6e2e84 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:09:46 +0900 Subject: [PATCH 34/35] wip: full bundle dev env --- packages/vite/src/client/client.ts | 2 +- packages/vite/src/node/server/middlewares/error.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 5eb8dc03e3c269..9c9fd3641492d1 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -600,7 +600,7 @@ export function injectQuery(url: string, queryToInject: string): string { export { ErrorOverlay } -if (isFullBundleMode) { +if (isFullBundleMode && typeof DevRuntime !== 'undefined') { class ViteDevRuntime extends DevRuntime { override createModuleHotContext(moduleId: string) { const ctx = createHotContext(moduleId) diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index ea4ac9a7f43137..de1374d83c7f2f 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -71,7 +71,6 @@ export function errorMiddleware( if (allowNext) { next() } else { - // TODO: support error overlay res.statusCode = 500 res.end(` From d098bfca96503b31fe78abeb3e46cfdd8efcd3f4 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:07:02 +0900 Subject: [PATCH 35/35] wip: full bundle dev env --- packages/vite/src/node/plugins/asset.ts | 4 ++++ .../environments/fullBundleEnvironment.ts | 22 +++++++++++++++---- playground/hmr/vite.config.ts | 16 ++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 88400718a2aa05..27454f4cc9ed86 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -304,6 +304,10 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } } }, + + watchChange(id) { + assetCache.get(this.environment)?.delete(id) + }, } } diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 007834bf67690e..78b29b3ed112ec 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -1,6 +1,7 @@ import type { RolldownBuild, RolldownOptions } from 'rolldown' import type { Update } from 'types/hmrPayload' import colors from 'picocolors' +import type { FSWatcher } from 'chokidar' import { ChunkMetadataMap, clearLine, @@ -11,7 +12,7 @@ import { getHmrImplementation } from '../../plugins/clientInjections' import { DevEnvironment, type DevEnvironmentContext } from '../environment' import type { ResolvedConfig } from '../../config' import type { ViteDevServer } from '../../server' -import { arraify, createDebugger } from '../../utils' +import { arraify, createDebugger, tryStatSync } from '../../utils' import { prepareError } from '../middlewares/error' import { getShortName } from '../hmr' @@ -65,6 +66,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { private state: BundleState = { type: 'initial' } private invalidateCalledModules = new Set() + watcher!: FSWatcher watchFiles = new Set() memoryFiles = new MemoryFiles() @@ -82,8 +84,9 @@ export class FullBundleDevEnvironment extends DevEnvironment { super(name, config, { ...context, disableDepsOptimizer: true }) } - override async listen(_server: ViteDevServer): Promise { + override async listen(server: ViteDevServer): Promise { this.hot.listen() + this.watcher = server.watcher debug?.('INITIAL: setup bundle options') const rollupOptions = await this.getRolldownOptions() @@ -368,8 +371,19 @@ export class FullBundleDevEnvironment extends DevEnvironment { } // TODO: should this be done for hmr patch file generation? - for (const file of await bundle.watchFiles) { - this.watchFiles.add(file) + const bundleWatchFiles = new Set(await bundle.watchFiles) + for (const file of this.watchFiles) { + if (!bundleWatchFiles.has(file)) { + this.watcher.unwatch(file) + } + } + for (const file of bundleWatchFiles) { + if (!this.watchFiles.has(file)) { + if (tryStatSync(file)) { + this.watcher.add(file) + } + this.watchFiles.add(file) + } } if (signal.aborted) return const postGenerateTime = Date.now() diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index 9ee8024ee2bf44..556d986cd938a3 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -8,6 +8,22 @@ export default defineConfig({ hmrPartialAccept: true, }, build: { + rollupOptions: { + input: [ + path.resolve(import.meta.dirname, './index.html'), + path.resolve(import.meta.dirname, './missing-import/index.html'), + path.resolve( + import.meta.dirname, + './unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', + ), + path.resolve(import.meta.dirname, './counter/index.html'), + path.resolve( + import.meta.dirname, + './self-accept-within-circular/index.html', + ), + path.resolve(import.meta.dirname, './css-deps/index.html'), + ], + }, assetsInlineLimit(filePath) { if (filePath.endsWith('logo-no-inline.svg')) { return false