From f4d438d305472b6fd4dbe014a8bf5a527606c65b 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/71] 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 4110e04ae0b1d2..da39f1dba0a291 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -556,6 +556,13 @@ export interface ExperimentalOptions { * @default 'v1' */ enableNativePlugin?: boolean | 'resolver' | 'v1' + /** + * Enable full bundle mode in dev. + * + * @experimental + * @default false + */ + fullBundleMode?: boolean } export interface LegacyOptions { @@ -770,6 +777,7 @@ const configDefaults = Object.freeze({ renderBuiltUrl: undefined, hmrPartialAccept: false, enableNativePlugin: process.env._VITE_TEST_JS_PLUGIN ? false : 'v1', + fullBundleMode: false, }, future: { removePluginHookHandleHotUpdate: undefined, From 2d3fe616098f2e5e85b5fc5b405a96cd4a17f05f 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/71] 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 b2f95d4f3809d6..8ee8ad55d125f6 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, }, - }) + ) + await 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, - }) - await stopProfiler(logger.info) - process.exit(1) - } - }) + }, + ) // build cli From 563f5d63600c3cf9eecdf6f385fcd15255e9c16a 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/71] 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 da39f1dba0a291..03602d37bac94d 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -641,6 +641,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 */ @@ -1803,6 +1805,7 @@ export async function resolveConfig( cacheDir, command, mode, + isBundled: config.experimental?.fullBundleMode || isBuild, isWorker: false, mainConfig: null, bundleChain: [], From 1b479ebc78c46ca0cbf6af13d8ec44947370cd15 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/71] 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 c45d14ae6a50c1..5912d1759f46c1 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 e2fde3e66b3d47..9a556e998d6cad 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -419,6 +419,7 @@ export function resolveBuildEnvironmentOptions( raw: BuildEnvironmentOptions, logger: Logger, consumer: 'client' | 'server' | undefined, + isProduction: boolean, ): ResolvedBuildEnvironmentOptions { const deprecatedPolyfillModulePreload = raw.polyfillModulePreload const { polyfillModulePreload, ...rest } = raw @@ -439,7 +440,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 03602d37bac94d..242e57b333ff05 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -861,6 +861,7 @@ function resolveEnvironmentOptions( preserveSymlinks: boolean, forceOptimizeDeps: boolean | undefined, logger: Logger, + isProduction: boolean, environmentName: string, // Backward compatibility isSsrTargetWebworkerSet?: boolean, @@ -924,6 +925,7 @@ function resolveEnvironmentOptions( options.build ?? {}, logger, consumer, + isProduction, ), plugins: undefined!, // to be resolved later // will be set by `setOptimizeDepsPluginNames` later @@ -1510,6 +1512,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 @@ -1525,6 +1557,7 @@ export async function resolveConfig( resolvedDefaultResolve.preserveSymlinks, inlineConfig.forceOptimizeDeps, logger, + isProduction, environmentName, config.ssr?.target === 'webworker', config.server?.preTransformRequests, @@ -1549,6 +1582,7 @@ export async function resolveConfig( config.build ?? {}, logger, undefined, + isProduction, ) // Backward compatibility: merge config.environments.ssr back into config.ssr @@ -1569,36 +1603,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 eff02371823befe685ad46c276c9c44f13b8b074 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/71] 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 0ba0672011b87f..ea13da41d0d783 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -106,7 +106,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 99b6a328c9de6c..515ade9114082c 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 1a94f3f088647dd94f7a37f7c3f7c8f1b9f8103f 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/71] 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 5912d1759f46c1..c45d14ae6a50c1 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 9a556e998d6cad..e2fde3e66b3d47 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -419,7 +419,6 @@ export function resolveBuildEnvironmentOptions( raw: BuildEnvironmentOptions, logger: Logger, consumer: 'client' | 'server' | undefined, - isProduction: boolean, ): ResolvedBuildEnvironmentOptions { const deprecatedPolyfillModulePreload = raw.polyfillModulePreload const { polyfillModulePreload, ...rest } = raw @@ -440,7 +439,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 242e57b333ff05..03602d37bac94d 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -861,7 +861,6 @@ function resolveEnvironmentOptions( preserveSymlinks: boolean, forceOptimizeDeps: boolean | undefined, logger: Logger, - isProduction: boolean, environmentName: string, // Backward compatibility isSsrTargetWebworkerSet?: boolean, @@ -925,7 +924,6 @@ function resolveEnvironmentOptions( options.build ?? {}, logger, consumer, - isProduction, ), plugins: undefined!, // to be resolved later // will be set by `setOptimizeDepsPluginNames` later @@ -1512,36 +1510,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 @@ -1557,7 +1525,6 @@ export async function resolveConfig( resolvedDefaultResolve.preserveSymlinks, inlineConfig.forceOptimizeDeps, logger, - isProduction, environmentName, config.ssr?.target === 'webworker', config.server?.preTransformRequests, @@ -1582,7 +1549,6 @@ export async function resolveConfig( config.build ?? {}, logger, undefined, - isProduction, ) // Backward compatibility: merge config.environments.ssr back into config.ssr @@ -1603,6 +1569,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 a3c06adeed680338cae3fc7709f7615abdbc6e9c 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/71] 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 ea13da41d0d783..0ba0672011b87f 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -106,7 +106,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 515ade9114082c..99b6a328c9de6c 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 e4c8e652701c0235965bd80101028645654f140b 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/71] 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 10392ee9888dae..9514e16f814ff7 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -145,7 +145,7 @@ export class DevEnvironment extends BaseEnvironment { this.hot.on( 'vite:invalidate', async ({ path, message, firstInvalidatedBy }) => { - invalidateModule(this, { + this.invalidateModule({ path, message, firstInvalidatedBy, @@ -246,6 +246,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].filter((imp) => imp !== mod), // ignore self-imports + mod.lastHMRTimestamp, + m.firstInvalidatedBy, + ) + } + } + async close(): Promise { this._closing = true @@ -287,39 +317,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].filter((imp) => imp !== mod), // ignore self-imports - mod.lastHMRTimestamp, - m.firstInvalidatedBy, - ) - } -} - const callCrawlEndIfIdleAfterMs = 50 interface CrawlEndFinder { From 84734e37a77f5ae07570c38cb35ce5168c55c5b7 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/71] 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 e2fde3e66b3d47..49b9c38a8b7c5a 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -419,6 +419,7 @@ export function resolveBuildEnvironmentOptions( raw: BuildEnvironmentOptions, logger: Logger, consumer: 'client' | 'server' | undefined, + isFullBundledDev: boolean, ): ResolvedBuildEnvironmentOptions { const deprecatedPolyfillModulePreload = raw.polyfillModulePreload const { polyfillModulePreload, ...rest } = raw @@ -439,7 +440,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 03602d37bac94d..e28899701b39e9 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -862,6 +862,7 @@ function resolveEnvironmentOptions( forceOptimizeDeps: boolean | undefined, logger: Logger, environmentName: string, + isFullBundledDev: boolean, // Backward compatibility isSsrTargetWebworkerSet?: boolean, preTransformRequests?: boolean, @@ -924,6 +925,7 @@ function resolveEnvironmentOptions( options.build ?? {}, logger, consumer, + isFullBundledDev, ), plugins: undefined!, // to be resolved later // will be set by `setOptimizeDepsPluginNames` later @@ -1510,6 +1512,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 @@ -1526,6 +1531,7 @@ export async function resolveConfig( inlineConfig.forceOptimizeDeps, logger, environmentName, + isFullBundledDev, config.ssr?.target === 'webworker', config.server?.preTransformRequests, ) @@ -1549,6 +1555,7 @@ export async function resolveConfig( config.build ?? {}, logger, undefined, + isFullBundledDev, ) // Backward compatibility: merge config.environments.ssr back into config.ssr From 22b318a2a449399e5960d6a63a19aace26f3aa91 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/71] 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 49b9c38a8b7c5a..a259eeb9197212 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -501,6 +501,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ pre: Plugin[] post: Plugin[] }> { + const isBuild = config.command === 'build' return { pre: [ completeAmdWrapPlugin(), @@ -518,7 +519,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ...(config.isWorker ? [webWorkerPostPlugin(config)] : []), ], post: [ - ...buildImportAnalysisPlugin(config), + ...(isBuild ? buildImportAnalysisPlugin(config) : []), ...(config.nativePluginEnabledLevel >= 1 ? [] : [buildOxcPlugin()]), ...(config.build.minify === 'esbuild' ? [buildEsbuildPlugin()] : []), terserPlugin(config), From 2b817e341ec49cba525fbb1c51818783459b1b8e 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/71] 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 1fc8c2b522fdb3..171b6f88bc2aca 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)) @@ -593,3 +616,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 a259eeb9197212..c9a09fa8c22585 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -506,7 +506,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ pre: [ completeAmdWrapPlugin(), completeSystemWrapPlugin(), - ...(!config.isWorker ? [prepareOutDirPlugin()] : []), + ...(isBuild && !config.isWorker ? [prepareOutDirPlugin()] : []), perEnvironmentPlugin( 'vite:rollup-options-plugins', async (environment) => @@ -522,8 +522,8 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ...(isBuild ? buildImportAnalysisPlugin(config) : []), ...(config.nativePluginEnabledLevel >= 1 ? [] : [buildOxcPlugin()]), ...(config.build.minify === 'esbuild' ? [buildEsbuildPlugin()] : []), - terserPlugin(config), - ...(!config.isWorker + ...(isBuild ? [terserPlugin(config)] : []), + ...(isBuild && !config.isWorker ? [ licensePlugin(), manifestPlugin(config), @@ -565,10 +565,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 @@ -886,7 +886,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) @@ -1052,7 +1052,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 e28899701b39e9..20625e2e0652c7 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -125,6 +125,7 @@ import { basePluginContextMeta, } from './server/pluginContainer' import { nodeResolveWithVite } from './nodeResolve' +import { FullBundleDevEnvironment } from './server/environments/fullBundleEnvironment' const debug = createDebugger('vite:config', { depth: 10 }) const promisifiedRealpath = promisify(fs.realpath) @@ -238,6 +239,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 6790b4582fbac0..0ccb432c050d0b 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' @@ -34,66 +35,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' @@ -122,3 +64,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 3f321fd551d5b1..69756320b5115c 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -586,9 +586,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 63c7ec66236f29..5e3de9e3f31aba 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 @@ -135,7 +138,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. @@ -221,6 +224,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 a41f165be705fe..50af075c76fa25 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 796894d44f454a..e661f800439412 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 0ba0672011b87f..b3948df9a317ec 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({ @@ -116,7 +117,7 @@ export async function resolvePlugins( wasmFallbackPlugin(config), definePlugin(config), cssPostPlugin(config), - isBuild && buildHtmlPlugin(config), + isBundled && buildHtmlPlugin(config), workerImportMetaUrlPlugin(config), assetImportMetaUrlPlugin(config), ...buildPlugins.pre, @@ -128,7 +129,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 da5992d6a710da..a9acf64abee279 100644 --- a/packages/vite/src/node/plugins/oxc.ts +++ b/packages/vite/src/node/plugins/oxc.ts @@ -321,7 +321,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 ec3cd59735384b..c60ed7c9b129cf 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -248,6 +248,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 5b3ad98b3fc92c..7c0ec3e706fd7b 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 e311b93fd27168..c5db80725ee571 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -312,7 +312,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 9514e16f814ff7..5a428502ec43c8 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 { @@ -153,17 +155,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 5042f09ebc7545..1d80c5304f2180 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 '.' @@ -419,6 +420,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 09ec6a376374e9..8ebba4e4026f91 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() @@ -909,7 +910,9 @@ export async function _createServer( // Internal middlewares ------------------------------------------------------ - middlewares.use(cachedTransformMiddleware(server)) + if (!config.experimental.fullBundleMode) { + middlewares.use(cachedTransformMiddleware(server)) + } // proxy const { proxy } = serverConfig @@ -944,16 +947,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 08fab7c6b25694..92011b46cafae2 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 ab9356f4a3d96c..e61679807598d2 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 ( @@ -40,8 +55,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() @@ -49,8 +63,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 @@ -59,8 +72,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 180f14a6562cd2..135177d4141681 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -49,6 +49,7 @@ import { BasicMinimalPluginContext, basePluginContextMeta, } from '../pluginContainer' +import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment' import { checkLoadingAccess, respondWithAccessDenied } from './static' interface AssetNode { @@ -442,6 +443,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) { @@ -452,6 +457,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 943b339db7be7120b99f48d5a2ce39f0738414bd 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/71] 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 135177d4141681..da60f09b39d43b 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -50,6 +50,7 @@ import { basePluginContextMeta, } from '../pluginContainer' import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment' +import { getHmrImplementation } from '../../plugins/clientInjections' import { checkLoadingAccess, respondWithAccessDenied } from './static' interface AssetNode { @@ -459,10 +460,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) @@ -531,3 +533,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 d315e9c6857746f5889c829ead0f6bd008619969 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/71] 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 171b6f88bc2aca..70fa5dd6e31563 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' @@ -618,67 +619,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 5a428502ec43c8..db5f0b70c62e55 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -250,7 +250,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 1d80c5304f2180..88b8e51eb90c51 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -423,7 +423,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 da60f09b39d43b..646036d735b3c9 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -444,9 +444,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 ...` @@ -458,13 +458,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 87a1f1761c700127dc6f27be86a4f6bb3368b0ab 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/71] 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 970dc1de74d653..9a39d00c540abe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -853,6 +853,8 @@ importers: playground/hmr: {} + playground/hmr-full-bundle-mode: {} + playground/hmr-ssr: {} playground/html: {} From f4dfd85e071cfbbab87449edbed9b3d7d0d03936 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/71] 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 4ec192676dce0a..f11e1ac77c6cad 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -240,20 +240,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 @@ -311,7 +315,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) @@ -460,7 +464,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 16e75b9d064f4d666bb4a06dff75f184701a00d8 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/71] 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 d3efbed907d49fbe530d8efe83de07564d522376 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/71] 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 87f25f2d53cefae30b1de404e443e5a9e1bb73dd 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/71] 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 8ebba4e4026f91..19e44622795565 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -996,10 +996,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 51689aa2ffab633702cea16138669d1b6f18ab1e 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/71] 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 4f8d4f320f53d6601cfe12877c209ec46c849dda 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/71] 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 06dfd9dd20b9fa5e58a3fb9bba9f69933ee3182e 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/71] 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 fbfdab9871f1ec537b7e676852a0608e889c5721 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/71] 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 c60ed7c9b129cf..eb6892319f8ef0 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -236,7 +236,7 @@ export function oxcResolvePlugin( overrideEnvConfig: (ResolvedConfig & ResolvedEnvironmentOptions) | undefined, ): Plugin[] { return [ - ...(!resolveOptions.isBuild + ...(resolveOptions.optimizeDeps && !resolveOptions.isBuild ? [optimizerResolvePlugin(resolveOptions)] : []), ...perEnvironmentOrWorkerPlugin( @@ -397,6 +397,12 @@ function optimizerResolvePlugin( return { name: 'vite:resolve-dev', + applyToEnvironment(environment) { + return ( + !environment.config.experimental.fullBundleMode && + !isDepOptimizationDisabled(environment.config.optimizeDeps) + ) + }, resolveId: { filter: { id: { From 805786a390019b97fa1a183f529e1a04681064f0 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/71] 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 8ee8ad55d125f6..3b000a31c6e799 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 e1c7da8bc69d600112a1c9bf0c24d910c12e1973 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/71] 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 50af075c76fa25..9200f393ec7907 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 10a8c138de89d365b66a27e6460f771058fbcb38 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/71] 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 5e3de9e3f31aba..153b230b66b1f0 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 b3bc1c3ee0d48e..b7d3354a8394db 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 bf28f02f625361dc032e1a633835c8f61c6dbefb 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/71] perf: skip worker renderChunk in dev --- packages/vite/src/node/plugins/worker.ts | 112 ++++++++++++----------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index c5db80725ee571..344dd1f0035de9 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -540,58 +540,68 @@ 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 workerOutputCache = workerOutputCaches.get( - config.mainConfig || config, - )! - - while ((match = workerAssetUrlRE.exec(code))) { - const [full, hash] = match - const filename = workerOutputCache.getEntryFilenameFromHash(hash) - if (!filename) { - this.warn(`Could not find worker asset for hash: ${hash}`) - continue - } - 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 workerOutputCache = workerOutputCaches.get( + config.mainConfig || config, + )! + + while ((match = workerAssetUrlRE.exec(code))) { + const [full, hash] = match + const filename = + workerOutputCache.getEntryFilenameFromHash(hash) + if (!filename) { + this.warn(`Could not find worker asset for hash: ${hash}`) + continue + } + 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 a10b54430be5d8dd4d41e9b8e9bd168968449a3f 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/71] 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 2b5dfcbc6d85e8c78be6a55029837f1e449f92b8 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/71] 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 0db5728aa5de4c3e77e27860ffa40241257e948e 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/71] 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 20625e2e0652c7..cda19600ee8b9e 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -1805,6 +1805,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 f11e1ac77c6cad..78a7394e6a78bb 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -476,7 +476,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 0052ee5e3f901b771a16304d79f9f286fbfafec0 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/71] 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 d521a95c8fff2f84686b37f7cb9c6dd12248248b 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/71] 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 6dd1f261ed8b62a71be0293489fdd6e06c410a0f 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/71] 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 e61679807598d2..24d08f9537171c 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)) ) } @@ -63,7 +64,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 646036d735b3c9..60fc104ad7bdd4 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -459,8 +459,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 ed84e396afd9a34f12592ea664df83110562f50e 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/71] 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 d3ddce654c70c5a82ba3065fc2a3765695eea46f 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/71] 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 70fa5dd6e31563..7c9c1f5a902154 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -618,7 +618,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 92011b46cafae2..08fab7c6b25694 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 1e290043ff2d71d987de1f4f9eca061427cd39d9 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/71] 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 78a7394e6a78bb..17ff9c7ca7e68d 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -307,6 +307,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 28e28d723d5af0..4c782273b9d053 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -9,6 +9,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 From 987c321229af4a40f6c83f6efb24a6abcf4c63af Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:35:42 +0900 Subject: [PATCH 36/71] wip: update --- .../vite/src/node/server/environments/fullBundleEnvironment.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 78b29b3ed112ec..7263abfd596b31 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -311,12 +311,14 @@ export class FullBundleDevEnvironment extends DevEnvironment { output.entryFileNames = 'assets/[name].js' output.chunkFileNames = 'assets/[name]-[hash].js' output.assetFileNames = 'assets/[name]-[hash][extname]' + output.minify = false } } else { rolldownOptions.output ??= {} rolldownOptions.output.entryFileNames = 'assets/[name].js' rolldownOptions.output.chunkFileNames = 'assets/[name]-[hash].js' rolldownOptions.output.assetFileNames = 'assets/[name]-[hash][extname]' + rolldownOptions.output.minify = false } return rolldownOptions From ea715dc77557735c5bbfe65703e55c223cdf5c6a Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:55:53 +0900 Subject: [PATCH 37/71] wip: update --- packages/vite/src/node/plugins/css.ts | 624 +++++++++++++------------- 1 file changed, 324 insertions(+), 300 deletions(-) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 69756320b5115c..06a015fe185590 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -634,327 +634,351 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { }, }, - async renderChunk(code, chunk, opts, meta) { - let chunkCSS: string | undefined - const renderedModules = new Proxy( - {} as Record, - { - get(_target, p) { - for (const name in meta.chunks) { - const modules = meta.chunks[name].modules - const module = modules[p as string] - if (module) { - return module - } - } - }, - }, - ) - // the chunk is empty if it's a dynamic entry chunk that only contains a CSS import - const isJsChunkEmpty = code === '' && !chunk.isEntry - let isPureCssChunk = chunk.exports.length === 0 - const ids = Object.keys(chunk.modules) - for (const id of ids) { - if (styles.has(id)) { - // ?transform-only is used for ?url and shouldn't be included in normal CSS chunks - if (transformOnlyRE.test(id)) { - continue - } - - // If this CSS is scoped to its importers exports, check if those importers exports - // are rendered in the chunks. If they are not, we can skip bundling this CSS. - const cssScopeTo = this.getModuleInfo(id)?.meta?.vite?.cssScopeTo - if ( - cssScopeTo && - !isCssScopeToRendered(cssScopeTo, renderedModules) - ) { - continue - } - - // a css module contains JS, so it makes this not a pure css chunk - if (cssModuleRE.test(id)) { - isPureCssChunk = false - } - - chunkCSS = (chunkCSS || '') + styles.get(id) - } else if (!isJsChunkEmpty) { - // if the module does not have a style, then it's not a pure css chunk. - // this is true because in the `transform` hook above, only modules - // that are css gets added to the `styles` map. - isPureCssChunk = false - } - } - - const publicAssetUrlMap = publicAssetUrlCache.get(config)! - - // resolve asset URL placeholders to their built file URLs - const resolveAssetUrlsInCss = ( - chunkCSS: string, - cssAssetName: string, - ) => { - const encodedPublicUrls = encodePublicUrlsInCSS(config) - - const relative = config.base === './' || config.base === '' - const cssAssetDirname = - encodedPublicUrls || relative - ? slash(getCssAssetDirname(cssAssetName)) - : undefined - - const toRelative = (filename: string) => { - // relative base + extracted CSS - const relativePath = normalizePath( - path.relative(cssAssetDirname!, filename), - ) - return relativePath[0] === '.' ? relativePath : './' + relativePath - } - - // replace asset url references with resolved url. - chunkCSS = chunkCSS.replace(assetUrlRE, (_, fileHash, postfix = '') => { - const filename = this.getFileName(fileHash) + postfix - chunk.viteMetadata!.importedAssets.add(cleanUrl(filename)) - return encodeURIPath( - toOutputFilePathInCss( - filename, - 'asset', - cssAssetName, - 'css', - config, - toRelative, - ), - ) - }) - // resolve public URL from CSS paths - if (encodedPublicUrls) { - const relativePathToPublicFromCSS = normalizePath( - path.relative(cssAssetDirname!, ''), - ) - chunkCSS = chunkCSS.replace(publicAssetUrlRE, (_, hash) => { - const publicUrl = publicAssetUrlMap.get(hash)!.slice(1) - return encodeURIPath( - toOutputFilePathInCss( - publicUrl, - 'public', - cssAssetName, - 'css', - config, - () => `${relativePathToPublicFromCSS}/${publicUrl}`, - ), + ...(config.command === 'build' + ? { + async renderChunk(code, chunk, opts, meta) { + let chunkCSS: string | undefined + const renderedModules = new Proxy( + {} as Record, + { + get(_target, p) { + for (const name in meta.chunks) { + const modules = meta.chunks[name].modules + const module = modules[p as string] + if (module) { + return module + } + } + }, + }, ) - }) - } - return chunkCSS - } - - function ensureFileExt(name: string, ext: string) { - return normalizePath( - path.format({ ...path.parse(name), base: undefined, ext }), - ) - } + // the chunk is empty if it's a dynamic entry chunk that only contains a CSS import + const isJsChunkEmpty = code === '' && !chunk.isEntry + let isPureCssChunk = chunk.exports.length === 0 + const ids = Object.keys(chunk.modules) + for (const id of ids) { + if (styles.has(id)) { + // ?transform-only is used for ?url and shouldn't be included in normal CSS chunks + if (transformOnlyRE.test(id)) { + continue + } - let s: MagicString | undefined - const urlEmitTasks: Array<{ - cssAssetName: string - originalFileName: string - content: string - start: number - end: number - }> = [] - - if (code.includes('__VITE_CSS_URL__')) { - let match: RegExpExecArray | null - cssUrlAssetRE.lastIndex = 0 - while ((match = cssUrlAssetRE.exec(code))) { - const [full, idHex] = match - const id = Buffer.from(idHex, 'hex').toString() - const originalFileName = cleanUrl(id) - const cssAssetName = ensureFileExt( - path.basename(originalFileName), - '.css', - ) - if (!styles.has(id)) { - throw new Error( - `css content for ${JSON.stringify(id)} was not found`, - ) - } + // If this CSS is scoped to its importers exports, check if those importers exports + // are rendered in the chunks. If they are not, we can skip bundling this CSS. + const cssScopeTo = + this.getModuleInfo(id)?.meta?.vite?.cssScopeTo + if ( + cssScopeTo && + !isCssScopeToRendered(cssScopeTo, renderedModules) + ) { + continue + } - let cssContent = styles.get(id)! + // a css module contains JS, so it makes this not a pure css chunk + if (cssModuleRE.test(id)) { + isPureCssChunk = false + } - cssContent = resolveAssetUrlsInCss(cssContent, cssAssetName) + chunkCSS = (chunkCSS || '') + styles.get(id) + } else if (!isJsChunkEmpty) { + // if the module does not have a style, then it's not a pure css chunk. + // this is true because in the `transform` hook above, only modules + // that are css gets added to the `styles` map. + isPureCssChunk = false + } + } - urlEmitTasks.push({ - cssAssetName, - originalFileName, - content: cssContent, - start: match.index, - end: match.index + full.length, - }) - } - } + const publicAssetUrlMap = publicAssetUrlCache.get(config)! + + // resolve asset URL placeholders to their built file URLs + const resolveAssetUrlsInCss = ( + chunkCSS: string, + cssAssetName: string, + ) => { + const encodedPublicUrls = encodePublicUrlsInCSS(config) + + const relative = config.base === './' || config.base === '' + const cssAssetDirname = + encodedPublicUrls || relative + ? slash(getCssAssetDirname(cssAssetName)) + : undefined + + const toRelative = (filename: string) => { + // relative base + extracted CSS + const relativePath = normalizePath( + path.relative(cssAssetDirname!, filename), + ) + return relativePath[0] === '.' + ? relativePath + : './' + relativePath + } - // should await even if this chunk does not include __VITE_CSS_URL__ - // so that code after this line runs in the same order - await urlEmitQueue.run(async () => - Promise.all( - urlEmitTasks.map(async (info) => { - info.content = await finalizeCss(info.content, config) - }), - ), - ) - if (urlEmitTasks.length > 0) { - const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime( - opts.format, - config.isWorker, - ) - s ||= new MagicString(code) + // replace asset url references with resolved url. + chunkCSS = chunkCSS.replace( + assetUrlRE, + (_, fileHash, postfix = '') => { + const filename = this.getFileName(fileHash) + postfix + chunk.viteMetadata!.importedAssets.add(cleanUrl(filename)) + return encodeURIPath( + toOutputFilePathInCss( + filename, + 'asset', + cssAssetName, + 'css', + config, + toRelative, + ), + ) + }, + ) + // resolve public URL from CSS paths + if (encodedPublicUrls) { + const relativePathToPublicFromCSS = normalizePath( + path.relative(cssAssetDirname!, ''), + ) + chunkCSS = chunkCSS.replace(publicAssetUrlRE, (_, hash) => { + const publicUrl = publicAssetUrlMap.get(hash)!.slice(1) + return encodeURIPath( + toOutputFilePathInCss( + publicUrl, + 'public', + cssAssetName, + 'css', + config, + () => `${relativePathToPublicFromCSS}/${publicUrl}`, + ), + ) + }) + } + return chunkCSS + } - for (const { - cssAssetName, - originalFileName, - content, - start, - end, - } of urlEmitTasks) { - const referenceId = this.emitFile({ - type: 'asset', - name: cssAssetName, - originalFileName, - source: content, - }) + function ensureFileExt(name: string, ext: string) { + return normalizePath( + path.format({ ...path.parse(name), base: undefined, ext }), + ) + } - const filename = this.getFileName(referenceId) - chunk.viteMetadata!.importedAssets.add(cleanUrl(filename)) - 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(start, end, replacementString) - } - } + let s: MagicString | undefined + const urlEmitTasks: Array<{ + cssAssetName: string + originalFileName: string + content: string + start: number + end: number + }> = [] + + if (code.includes('__VITE_CSS_URL__')) { + let match: RegExpExecArray | null + cssUrlAssetRE.lastIndex = 0 + while ((match = cssUrlAssetRE.exec(code))) { + const [full, idHex] = match + const id = Buffer.from(idHex, 'hex').toString() + const originalFileName = cleanUrl(id) + const cssAssetName = ensureFileExt( + path.basename(originalFileName), + '.css', + ) + if (!styles.has(id)) { + throw new Error( + `css content for ${JSON.stringify(id)} was not found`, + ) + } - if (chunkCSS !== undefined) { - if (isPureCssChunk && (opts.format === 'es' || opts.format === 'cjs')) { - // this is a shared CSS-only chunk that is empty. - pureCssChunks.add(chunk) - } + let cssContent = styles.get(id)! - if (this.environment.config.build.cssCodeSplit) { - if ( - (opts.format === 'es' || opts.format === 'cjs') && - !chunk.fileName.includes('-legacy') - ) { - const isEntry = chunk.isEntry && isPureCssChunk - const cssFullAssetName = ensureFileExt(chunk.name, '.css') - // if facadeModuleId doesn't exist or doesn't have a CSS extension, - // that means a JS entry file imports a CSS file. - // in this case, only use the filename for the CSS chunk name like JS chunks. - const cssAssetName = - chunk.isEntry && - (!chunk.facadeModuleId || !isCSSRequest(chunk.facadeModuleId)) - ? path.basename(cssFullAssetName) - : cssFullAssetName - const originalFileName = getChunkOriginalFileName( - chunk, - config.root, - this.environment.config.isOutputOptionsForLegacyChunks?.(opts) ?? - false, - ) + cssContent = resolveAssetUrlsInCss(cssContent, cssAssetName) - chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssAssetName) + urlEmitTasks.push({ + cssAssetName, + originalFileName, + content: cssContent, + start: match.index, + end: match.index + full.length, + }) + } + } - // wait for previous tasks as well - chunkCSS = await codeSplitEmitQueue.run(async () => { - return finalizeCss(chunkCSS!, config) - }) + // should await even if this chunk does not include __VITE_CSS_URL__ + // so that code after this line runs in the same order + await urlEmitQueue.run(async () => + Promise.all( + urlEmitTasks.map(async (info) => { + info.content = await finalizeCss(info.content, config) + }), + ), + ) + if (urlEmitTasks.length > 0) { + const toRelativeRuntime = + createToImportMetaURLBasedRelativeRuntime( + opts.format, + config.isWorker, + ) + s ||= new MagicString(code) - // emit corresponding css file - const referenceId = this.emitFile({ - type: 'asset', - name: cssAssetName, - originalFileName, - source: chunkCSS, - }) - if (isEntry) { - cssEntriesMap.get(this.environment)!.set(chunk.name, referenceId) + for (const { + cssAssetName, + originalFileName, + content, + start, + end, + } of urlEmitTasks) { + const referenceId = this.emitFile({ + type: 'asset', + name: cssAssetName, + originalFileName, + source: content, + }) + + const filename = this.getFileName(referenceId) + chunk.viteMetadata!.importedAssets.add(cleanUrl(filename)) + 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(start, end, replacementString) + } } - chunk.viteMetadata!.importedCss.add(this.getFileName(referenceId)) - } else if (this.environment.config.consumer === 'client') { - // legacy build and inline css - - // Entry chunk CSS will be collected into `chunk.viteMetadata.importedCss` - // and injected later by the `'vite:build-html'` plugin into the `index.html` - // so it will be duplicated. (https://github.com/vitejs/vite/issues/2062#issuecomment-782388010) - // But because entry chunk can be imported by dynamic import, - // we shouldn't remove the inlined CSS. (#10285) - - chunkCSS = await finalizeCss(chunkCSS, config) - let cssString = JSON.stringify(chunkCSS) - cssString = - renderAssetUrlInJS(this, chunk, opts, cssString)?.toString() || - cssString - const style = `__vite_style__` - const injectCode = - `var ${style} = document.createElement('style');` + - `${style}.textContent = ${cssString};` + - `document.head.appendChild(${style});` - - let injectionPoint: number - if (opts.format === 'iife' || opts.format === 'umd') { - const m = ( - opts.format === 'iife' ? IIFE_BEGIN_RE : UMD_BEGIN_RE - ).exec(code) - if (!m) { - this.error('Injection point for inlined CSS not found') - return + + if (chunkCSS !== undefined) { + if ( + isPureCssChunk && + (opts.format === 'es' || opts.format === 'cjs') + ) { + // this is a shared CSS-only chunk that is empty. + pureCssChunks.add(chunk) } - injectionPoint = m.index + m[0].length - } else if (opts.format === 'es') { - // legacy build - if (code.startsWith('#!')) { - let secondLinePos = code.indexOf('\n') - if (secondLinePos === -1) { - secondLinePos = 0 + + if (this.environment.config.build.cssCodeSplit) { + if ( + (opts.format === 'es' || opts.format === 'cjs') && + !chunk.fileName.includes('-legacy') + ) { + const isEntry = chunk.isEntry && isPureCssChunk + const cssFullAssetName = ensureFileExt(chunk.name, '.css') + // if facadeModuleId doesn't exist or doesn't have a CSS extension, + // that means a JS entry file imports a CSS file. + // in this case, only use the filename for the CSS chunk name like JS chunks. + const cssAssetName = + chunk.isEntry && + (!chunk.facadeModuleId || + !isCSSRequest(chunk.facadeModuleId)) + ? path.basename(cssFullAssetName) + : cssFullAssetName + const originalFileName = getChunkOriginalFileName( + chunk, + config.root, + this.environment.config.isOutputOptionsForLegacyChunks?.( + opts, + ) ?? false, + ) + + chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssAssetName) + + // wait for previous tasks as well + chunkCSS = await codeSplitEmitQueue.run(async () => { + return finalizeCss(chunkCSS!, config) + }) + + // emit corresponding css file + const referenceId = this.emitFile({ + type: 'asset', + name: cssAssetName, + originalFileName, + source: chunkCSS, + }) + if (isEntry) { + cssEntriesMap + .get(this.environment)! + .set(chunk.name, referenceId) + } + chunk.viteMetadata!.importedCss.add( + this.getFileName(referenceId), + ) + } else if (this.environment.config.consumer === 'client') { + // legacy build and inline css + + // Entry chunk CSS will be collected into `chunk.viteMetadata.importedCss` + // and injected later by the `'vite:build-html'` plugin into the `index.html` + // so it will be duplicated. (https://github.com/vitejs/vite/issues/2062#issuecomment-782388010) + // But because entry chunk can be imported by dynamic import, + // we shouldn't remove the inlined CSS. (#10285) + + chunkCSS = await finalizeCss(chunkCSS, config) + let cssString = JSON.stringify(chunkCSS) + cssString = + renderAssetUrlInJS( + this, + chunk, + opts, + cssString, + )?.toString() || cssString + const style = `__vite_style__` + const injectCode = + `var ${style} = document.createElement('style');` + + `${style}.textContent = ${cssString};` + + `document.head.appendChild(${style});` + + let injectionPoint: number + if (opts.format === 'iife' || opts.format === 'umd') { + const m = ( + opts.format === 'iife' ? IIFE_BEGIN_RE : UMD_BEGIN_RE + ).exec(code) + if (!m) { + this.error('Injection point for inlined CSS not found') + return + } + injectionPoint = m.index + m[0].length + } else if (opts.format === 'es') { + // legacy build + if (code.startsWith('#!')) { + let secondLinePos = code.indexOf('\n') + if (secondLinePos === -1) { + secondLinePos = 0 + } + injectionPoint = secondLinePos + } else { + injectionPoint = 0 + } + } else { + this.error('Non supported format') + return + } + + s ||= new MagicString(code) + s.appendRight(injectionPoint, injectCode) } - injectionPoint = secondLinePos } else { - injectionPoint = 0 + // resolve public URL from CSS paths, we need to use absolute paths + chunkCSS = resolveAssetUrlsInCss(chunkCSS, getCssBundleName()) + // finalizeCss is called for the aggregated chunk in generateBundle + + chunkCSSMap.set(chunk.fileName, chunkCSS) } - } else { - this.error('Non supported format') - return } - s ||= new MagicString(code) - s.appendRight(injectionPoint, injectCode) - } - } else { - // resolve public URL from CSS paths, we need to use absolute paths - chunkCSS = resolveAssetUrlsInCss(chunkCSS, getCssBundleName()) - // finalizeCss is called for the aggregated chunk in generateBundle - - chunkCSSMap.set(chunk.fileName, chunkCSS) - } - } - - if (s) { - if (config.build.sourcemap) { - return { - code: s.toString(), - map: s.generateMap({ hires: 'boundary' }), - } - } else { - return { code: s.toString() } + if (s) { + if (config.build.sourcemap) { + return { + code: s.toString(), + map: s.generateMap({ hires: 'boundary' }), + } + } else { + return { code: s.toString() } + } + } + return null + }, } - } - return null - }, + : {}), augmentChunkHash(chunk) { if (chunk.viteMetadata?.importedCss.size) { From 06f6d4f004c14f3a4a8f3454d33f70b0ac24fdd5 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:19:58 +0900 Subject: [PATCH 38/71] wip: update --- packages/vite/src/node/plugins/css.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 06a015fe185590..1482f31ce265c1 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -977,18 +977,18 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } return null }, - } - : {}), - augmentChunkHash(chunk) { - if (chunk.viteMetadata?.importedCss.size) { - let hash = '' - for (const id of chunk.viteMetadata.importedCss) { - hash += id + augmentChunkHash(chunk) { + if (chunk.viteMetadata?.importedCss.size) { + let hash = '' + for (const id of chunk.viteMetadata.importedCss) { + hash += id + } + return hash + } + }, } - return hash - } - }, + : {}), async generateBundle(opts, bundle) { // to avoid emitting duplicate assets for modern build and legacy build From ff33bd2ba32ea94daf0f0c340211cdf42f828614 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:13:00 +0900 Subject: [PATCH 39/71] wip: use dev api --- .../environments/fullBundleEnvironment.ts | 387 +++--------------- packages/vite/src/node/server/hmr.ts | 3 - .../src/node/server/middlewares/indexHtml.ts | 2 +- 3 files changed, 65 insertions(+), 327 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 7263abfd596b31..4eb3d87205c4a9 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -1,19 +1,13 @@ -import type { RolldownBuild, RolldownOptions } from 'rolldown' +import type { RolldownBuild } from 'rolldown' +import { type DevEngine, dev } from 'rolldown/experimental' import type { Update } from 'types/hmrPayload' import colors from 'picocolors' -import type { FSWatcher } from 'chokidar' -import { - ChunkMetadataMap, - clearLine, - enhanceRollupError, - resolveRolldownOptions, -} from '../../build' +import { ChunkMetadataMap, 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, tryStatSync } from '../../utils' -import { prepareError } from '../middlewares/error' +import { createDebugger } from '../../utils' import { getShortName } from '../hmr' const debug = createDebugger('vite:full-bundle-mode') @@ -63,11 +57,9 @@ export class MemoryFiles { } export class FullBundleDevEnvironment extends DevEnvironment { - private state: BundleState = { type: 'initial' } + private devEngine!: DevEngine private invalidateCalledModules = new Set() - watcher!: FSWatcher - watchFiles = new Set() memoryFiles = new MemoryFiles() constructor( @@ -84,106 +76,38 @@ 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() - const { rolldown } = await import('rolldown') - const bundle = await rolldown(rollupOptions) - debug?.('INITIAL: bundle created') - - debug?.('BUNDLING: trigger initial bundle') - this.triggerGenerateBundle({ options: rollupOptions, bundle }) - } - - async onFileChange( - _type: 'create' | 'update' | 'delete', - file: string, - ): Promise { - if (this.state.type === 'initial') { - return - } - - 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`, - ) - this.triggerGenerateBundle(this.state) - return - } - if (this.state.type === 'bundle-error') { - 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 - } - + // NOTE: only single outputOptions is supported here if ( - this.state.type === 'bundled' || - this.state.type === 'generating-hmr-patch' + Array.isArray(rollupOptions.output) && + rollupOptions.output.length > 1 ) { - 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`, - ) - } - - this.state = { - type: 'generating-hmr-patch', - options: this.state.options, - bundle: this.state.bundle, - patched: this.state.patched, - } - - const startTime = Date.now() - let hmrOutput: HmrOutput[] - try { - // NOTE: only single outputOptions is supported here - hmrOutput = await this.state.bundle.generateHmrPatch([file]) - } 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 - } finally { - this.invalidateCalledModules.clear() - } - - if (hmrOutput.every((output) => output.type === 'Noop')) { - 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) - } - return + throw new Error('multiple output options are not supported in dev mode') } - this.state satisfies never // exhaustive check + const outputOptions = ( + Array.isArray(rollupOptions.output) + ? rollupOptions.output[0] + : rollupOptions.output + )! + + this.devEngine = await dev(rollupOptions, outputOptions, { + onHmrUpdates: (updates, files) => { + this.invalidateCalledModules.clear() + // TODO: how to handle errors? + if (updates.every((update) => update.type === 'Noop')) { + debug?.(`ignored file change for ${files.join(', ')}`) + return + } + for (const update of updates) { + this.handleHmrOutput(files, update) + } + }, + }) + this.devEngine.run().catch(() => {}) } override async warmupRequest(_url: string): Promise { @@ -198,57 +122,21 @@ export class FullBundleDevEnvironment extends DevEnvironment { ;(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 because the state type has changed`, + 'INVALIDATE: invalidate received, but ignored because it was already invalidated', ) 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, - } + debug?.('INVALIDATE: invalidate received, re-triggering HMR') this.invalidateCalledModules.add(m.path) - let hmrOutput: HmrOutput - try { - // NOTE: only single outputOptions is supported here - hmrOutput = await this.state.bundle.hmrInvalidate( - 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 - } + // TODO: how to handle errors? + const update = await this.devEngine.invalidate( + m.path, + m.firstInvalidatedBy, + ) - if (hmrOutput.type === 'Patch') { + if (update.type === 'Patch') { this.logger.info( colors.yellow(`hmr invalidate `) + colors.dim(m.path) + @@ -258,41 +146,24 @@ export class FullBundleDevEnvironment extends DevEnvironment { } // TODO: need to check if this is enough - this.handleHmrOutput(m.path, hmrOutput, this.state, m.firstInvalidatedBy) + this.handleHmrOutput([m.path], update, m.firstInvalidatedBy) })() } - 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 + async triggerBundleRegenerationIfStale(): Promise { + const scheduled = await this.devEngine.scheduleBuildIfStale() + if (scheduled) { + debug?.(`TRIGGER: access to stale bundle, triggered bundle re-generation`) } - return false + return scheduled } 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() + // TODO: do we need close? })(), ]) } @@ -305,6 +176,21 @@ export class FullBundleDevEnvironment extends DevEnvironment { implement: await getHmrImplementation(this.getTopLevelConfig()), } + rolldownOptions.plugins = [ + rolldownOptions.plugins, + { + name: 'vite:full-bundle-mode:save-output', + generateBundle: (_, bundle) => { + this.memoryFiles.clear() + for (const outputFile of Object.values(bundle)) { + this.memoryFiles.set(outputFile.fileName, () => + outputFile.type === 'chunk' ? outputFile.code : outputFile.source, + ) + } + }, + }, + ] + // 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) { @@ -324,115 +210,17 @@ export class FullBundleDevEnvironment extends DevEnvironment { return rolldownOptions } - private triggerGenerateBundle({ - options, - bundle, - }: BundleStateCommonProperties) { - if (this.state.type === 'bundling') { - this.state.abortController.abort() - } - - 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 startTime = Date.now() - 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, () => - outputFile.type === 'chunk' ? outputFile.code : outputFile.source, - ) - } - } - const generateTime = Date.now() - - this.memoryFiles.clear() - for (const [file, code] of newMemoryFiles) { - this.memoryFiles.set(file, code) - } - - // TODO: should this be done for hmr patch file generation? - 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() - - if (this.state.type === 'initial') throw new Error('unreachable') - this.state = { - type: 'bundled', - bundle: this.state.bundle, - options: this.state.options, - patched: false, - } - 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 }) - } 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 handleHmrOutput( - file: string, + files: string[], hmrOutput: HmrOutput, - { options, bundle }: BundleStateCommonProperties, firstInvalidatedBy?: string, ) { if (hmrOutput.type === 'Noop') return - const shortFile = getShortName(file, this.config.root) + const shortFile = files + .map((file) => getShortName(file, this.config.root)) + .join(', ') if (hmrOutput.type === 'FullReload') { - this.triggerGenerateBundle({ options, bundle }) - const reason = hmrOutput.reason ? colors.dim(` (${hmrOutput.reason})`) : '' @@ -471,52 +259,5 @@ export class FullBundleDevEnvironment extends DevEnvironment { colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), { clear: !firstInvalidatedBy, timestamp: true }, ) - - this.state = { - type: 'bundled', - options, - bundle, - patched: true, - } } } - -// 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' - /** - * 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 = { - options: RolldownOptions - bundle: RolldownBuild -} diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 88b8e51eb90c51..6b3e4e46fe5bb5 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -30,7 +30,6 @@ import { BasicMinimalPluginContext, basePluginContextMeta, } from './pluginContainer' -import type { FullBundleDevEnvironment } from './environments/fullBundleEnvironment' import type { HttpServer } from '.' import { restartServerWithUrls } from '.' @@ -422,8 +421,6 @@ export async function handleHMRUpdate( if (config.experimental.fullBundleMode) { // TODO: support handleHotUpdate / hotUpdate - const environment = server.environments.client as FullBundleDevEnvironment - 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 60fc104ad7bdd4..91701c907c5e86 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -467,7 +467,7 @@ export function indexHtmlMiddleware( return next() } if ( - fullBundleEnv.triggerBundleRegenerationIfStale() || + (await fullBundleEnv.triggerBundleRegenerationIfStale()) || content === undefined ) { content = await generateFallbackHtml(server as ViteDevServer) From 2adb54ea80321b5744cf651a5abacd810f95c777 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:17:24 +0900 Subject: [PATCH 40/71] wip: update --- .../node/server/environments/fullBundleEnvironment.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 4eb3d87205c4a9..a30a0274d622bc 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -153,6 +153,10 @@ export class FullBundleDevEnvironment extends DevEnvironment { async triggerBundleRegenerationIfStale(): Promise { const scheduled = await this.devEngine.scheduleBuildIfStale() if (scheduled) { + this.devEngine.ensureCurrentBuildFinish().then(() => { + this.hot.send({ type: 'full-reload', path: '*' }) + this.logger.info(colors.green(`page reload`), { timestamp: true }) + }) debug?.(`TRIGGER: access to stale bundle, triggered bundle re-generation`) } return scheduled @@ -173,6 +177,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap) rolldownOptions.experimental ??= {} rolldownOptions.experimental.hmr = { + new: true, implement: await getHmrImplementation(this.getTopLevelConfig()), } @@ -181,7 +186,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { { name: 'vite:full-bundle-mode:save-output', generateBundle: (_, bundle) => { - this.memoryFiles.clear() + // NOTE: don't clear memoryFiles here as incremental build re-uses the files for (const outputFile of Object.values(bundle)) { this.memoryFiles.set(outputFile.fileName, () => outputFile.type === 'chunk' ? outputFile.code : outputFile.source, @@ -228,6 +233,10 @@ export class FullBundleDevEnvironment extends DevEnvironment { colors.green(`trigger page reload `) + colors.dim(shortFile) + reason, { clear: !firstInvalidatedBy, timestamp: true }, ) + this.devEngine.ensureLatestBuild().then(() => { + this.hot.send({ type: 'full-reload', path: '*' }) + this.logger.info(colors.green(`page reload`), { timestamp: true }) + }) return } From 3c58c0efc765daf6522b18ebc296888b1681ed8e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:02:17 +0900 Subject: [PATCH 41/71] wip: update --- .../environments/fullBundleEnvironment.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index a30a0274d622bc..14ccefee9d6798 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -107,7 +107,19 @@ export class FullBundleDevEnvironment extends DevEnvironment { } }, }) - this.devEngine.run().catch(() => {}) + debug?.('INITIAL: setup dev engine') + this.devEngine.run().then( + () => { + debug?.('INITIAL: run done') + }, + () => { + debug?.('INITIAL: run error') + }, + ) + this.devEngine.ensureCurrentBuildFinish().then(() => { + debug?.('INITIAL: build done') + this.hot.send({ type: 'full-reload', path: '*' }) + }) } override async warmupRequest(_url: string): Promise { @@ -152,14 +164,14 @@ export class FullBundleDevEnvironment extends DevEnvironment { async triggerBundleRegenerationIfStale(): Promise { const scheduled = await this.devEngine.scheduleBuildIfStale() - if (scheduled) { + if (scheduled === 'scheduled') { this.devEngine.ensureCurrentBuildFinish().then(() => { this.hot.send({ type: 'full-reload', path: '*' }) this.logger.info(colors.green(`page reload`), { timestamp: true }) }) debug?.(`TRIGGER: access to stale bundle, triggered bundle re-generation`) } - return scheduled + return !!scheduled } override async close(): Promise { From 67d4d4df18b84e430dd1bfcc77dc3b840e152b8b Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:35:32 +0900 Subject: [PATCH 42/71] wip: update --- .../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 14ccefee9d6798..4f9706178b5d35 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -116,12 +116,19 @@ export class FullBundleDevEnvironment extends DevEnvironment { debug?.('INITIAL: run error') }, ) - this.devEngine.ensureCurrentBuildFinish().then(() => { + this.waitForInitialBuildFinish().then(() => { debug?.('INITIAL: build done') this.hot.send({ type: 'full-reload', path: '*' }) }) } + private async waitForInitialBuildFinish(): Promise { + while (this.memoryFiles.size === 0) { + await this.devEngine.ensureCurrentBuildFinish() + await new Promise((resolve) => setTimeout(resolve, 10)) + } + } + override async warmupRequest(_url: string): Promise { // no-op } From 143fa946b6c5a0bb433c10fe6d8b9f9778bc96b0 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:49:58 +0900 Subject: [PATCH 43/71] wip: update --- .../vite/src/node/server/environments/fullBundleEnvironment.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 4f9706178b5d35..baa04a181a6562 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -106,6 +106,9 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.handleHmrOutput(files, update) } }, + watch: { + skipWrite: true, + }, }) debug?.('INITIAL: setup dev engine') this.devEngine.run().then( From 4b14df58c34bdecdc80f7c0bf68a5dde9174b22c Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:27:28 +0900 Subject: [PATCH 44/71] wip: update --- packages/vite/src/node/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 19e44622795565..d73093acd28b2e 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -503,7 +503,7 @@ export async function _createServer( ? (chokidar.watch( // config file dependencies and env file might be outside of root [ - root, + ...(config.experimental.fullBundleMode ? [] : [root]), ...config.configFileDependencies, ...getEnvFilesForMode(config.mode, config.envDir), // Watch the public directory explicitly because it might be outside From 2c40539a72859619cb399d3074c60eed0c93f345 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:18:26 +0900 Subject: [PATCH 45/71] wip: update --- .../environments/fullBundleEnvironment.ts | 75 +++++++++++++------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index baa04a181a6562..17d011a542d109 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -59,6 +59,10 @@ export class MemoryFiles { export class FullBundleDevEnvironment extends DevEnvironment { private devEngine!: DevEngine private invalidateCalledModules = new Set() + private debouncedFullReload = debounce(20, () => { + this.hot.send({ type: 'full-reload', path: '*' }) + this.logger.info(colors.green(`page reload`), { timestamp: true }) + }) memoryFiles = new MemoryFiles() @@ -96,12 +100,15 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.devEngine = await dev(rollupOptions, outputOptions, { onHmrUpdates: (updates, files) => { - this.invalidateCalledModules.clear() + if (files.length === 0) { + return + } // TODO: how to handle errors? if (updates.every((update) => update.type === 'Noop')) { debug?.(`ignored file change for ${files.join(', ')}`) return } + this.invalidateCalledModules.clear() for (const update of updates) { this.handleHmrOutput(files, update) } @@ -126,9 +133,10 @@ export class FullBundleDevEnvironment extends DevEnvironment { } private async waitForInitialBuildFinish(): Promise { + await this.devEngine.ensureCurrentBuildFinish() while (this.memoryFiles.size === 0) { - await this.devEngine.ensureCurrentBuildFinish() await new Promise((resolve) => setTimeout(resolve, 10)) + await this.devEngine.ensureCurrentBuildFinish() } } @@ -144,12 +152,14 @@ export class FullBundleDevEnvironment extends DevEnvironment { ;(async () => { if (this.invalidateCalledModules.has(m.path)) { debug?.( - 'INVALIDATE: invalidate received, but ignored because it was already invalidated', + `INVALIDATE: invalidate received from ${m.path}, but ignored because it was already invalidated`, ) return } - debug?.('INVALIDATE: invalidate received, re-triggering HMR') + debug?.( + `INVALIDATE: invalidate received from ${m.path}, re-triggering HMR`, + ) this.invalidateCalledModules.add(m.path) // TODO: how to handle errors? @@ -168,7 +178,10 @@ export class FullBundleDevEnvironment extends DevEnvironment { } // TODO: need to check if this is enough - this.handleHmrOutput([m.path], update, m.firstInvalidatedBy) + this.handleHmrOutput([m.path], update, { + firstInvalidatedBy: m.firstInvalidatedBy, + reason: m.message, + }) })() } @@ -176,8 +189,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { const scheduled = await this.devEngine.scheduleBuildIfStale() if (scheduled === 'scheduled') { this.devEngine.ensureCurrentBuildFinish().then(() => { - this.hot.send({ type: 'full-reload', path: '*' }) - this.logger.info(colors.green(`page reload`), { timestamp: true }) + this.debouncedFullReload() }) debug?.(`TRIGGER: access to stale bundle, triggered bundle re-generation`) } @@ -207,13 +219,18 @@ export class FullBundleDevEnvironment extends DevEnvironment { rolldownOptions.plugins, { name: 'vite:full-bundle-mode:save-output', - generateBundle: (_, bundle) => { - // NOTE: don't clear memoryFiles here as incremental build re-uses the files - for (const outputFile of Object.values(bundle)) { - this.memoryFiles.set(outputFile.fileName, () => - outputFile.type === 'chunk' ? outputFile.code : outputFile.source, - ) - } + generateBundle: { + order: 'post', + handler: (_, bundle) => { + // NOTE: don't clear memoryFiles here as incremental build re-uses the files + for (const outputFile of Object.values(bundle)) { + this.memoryFiles.set(outputFile.fileName, () => + outputFile.type === 'chunk' + ? outputFile.code + : outputFile.source, + ) + } + }, }, }, ] @@ -240,7 +257,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { private handleHmrOutput( files: string[], hmrOutput: HmrOutput, - firstInvalidatedBy?: string, + invalidateInformation?: { firstInvalidatedBy: string; reason?: string }, ) { if (hmrOutput.type === 'Noop') return @@ -248,16 +265,17 @@ export class FullBundleDevEnvironment extends DevEnvironment { .map((file) => getShortName(file, this.config.root)) .join(', ') if (hmrOutput.type === 'FullReload') { - const reason = hmrOutput.reason - ? colors.dim(` (${hmrOutput.reason})`) - : '' + const reason = + (hmrOutput.reason ? colors.dim(` (${hmrOutput.reason})`) : '') + + (invalidateInformation?.reason + ? colors.dim(` (${invalidateInformation.reason})`) + : '') this.logger.info( colors.green(`trigger page reload `) + colors.dim(shortFile) + reason, - { clear: !firstInvalidatedBy, timestamp: true }, + { clear: !invalidateInformation, timestamp: true }, ) this.devEngine.ensureLatestBuild().then(() => { - this.hot.send({ type: 'full-reload', path: '*' }) - this.logger.info(colors.green(`page reload`), { timestamp: true }) + this.debouncedFullReload() }) return } @@ -277,7 +295,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { url: hmrOutput.filename, path: boundary.boundary, acceptedPath: boundary.acceptedVia, - firstInvalidatedBy, + firstInvalidatedBy: invalidateInformation?.firstInvalidatedBy, timestamp: Date.now(), } }) @@ -288,7 +306,18 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.logger.info( colors.green(`hmr update `) + colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), - { clear: !firstInvalidatedBy, timestamp: true }, + { clear: !invalidateInformation, timestamp: true }, ) } } + +function debounce(time: number, cb: () => void) { + let timer: ReturnType | null + return () => { + if (timer) { + clearTimeout(timer) + timer = null + } + timer = setTimeout(cb, time) + } +} From c1ec4a539a078165ca161aed853e7ece5352cdc0 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:36:54 +0900 Subject: [PATCH 46/71] wip: update --- .../vite/src/node/server/environments/fullBundleEnvironment.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 17d011a542d109..df1ca3c9a8f609 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -242,6 +242,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { output.chunkFileNames = 'assets/[name]-[hash].js' output.assetFileNames = 'assets/[name]-[hash][extname]' output.minify = false + output.sourcemap = true } } else { rolldownOptions.output ??= {} @@ -249,6 +250,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { rolldownOptions.output.chunkFileNames = 'assets/[name]-[hash].js' rolldownOptions.output.assetFileNames = 'assets/[name]-[hash][extname]' rolldownOptions.output.minify = false + rolldownOptions.output.sourcemap = true } return rolldownOptions From 8ae8a8778c05e86c50e25cc160da2def16765928 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:37:18 +0900 Subject: [PATCH 47/71] wip: update --- .../vite/src/node/server/environments/fullBundleEnvironment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index df1ca3c9a8f609..ff6363e92d6301 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -276,7 +276,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { colors.green(`trigger page reload `) + colors.dim(shortFile) + reason, { clear: !invalidateInformation, timestamp: true }, ) - this.devEngine.ensureLatestBuild().then(() => { + this.devEngine.ensureLatestBuildOutput().then(() => { this.debouncedFullReload() }) return From 30dff1c218cb3750baaa17fbec0f0403c9d36729 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:04:19 +0900 Subject: [PATCH 48/71] fix: rebuild https://github.com/rolldown/rolldown/pull/6321 --- .../src/node/server/environments/fullBundleEnvironment.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index ff6363e92d6301..2a96ee2af91c32 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -186,14 +186,14 @@ export class FullBundleDevEnvironment extends DevEnvironment { } async triggerBundleRegenerationIfStale(): Promise { - const scheduled = await this.devEngine.scheduleBuildIfStale() - if (scheduled === 'scheduled') { - this.devEngine.ensureCurrentBuildFinish().then(() => { + const hasLatestBuildOutput = await this.devEngine.hasLatestBuildOutput() + if (!hasLatestBuildOutput) { + this.devEngine.ensureLatestBuildOutput().then(() => { this.debouncedFullReload() }) debug?.(`TRIGGER: access to stale bundle, triggered bundle re-generation`) } - return !!scheduled + return !hasLatestBuildOutput } override async close(): Promise { From 3c7a48d1b38030015877561817d93eb32fa30c3d Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:04:56 +0900 Subject: [PATCH 49/71] test: skip some https://github.com/rolldown/rolldown/issues/6319 --- .../__tests__/hmr-full-bundle-mode.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index b2629d786153d6..458cafdb74246f 100644 --- 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 @@ -21,7 +21,8 @@ if (isBuild) { }) // BUNDLED -> GENERATE_HMR_PATCH -> BUNDLING -> BUNDLE_ERROR -> BUNDLING -> BUNDLED - test('handle bundle error', async () => { + // FIXME: https://github.com/rolldown/rolldown/issues/6319 + test.skip('handle bundle error', async () => { editFile('main.js', (code) => code.replace("text('.app', 'hello')", "text('.app', 'hello'); text("), ) @@ -70,7 +71,8 @@ if (isBuild) { }) // BUNDLED -> GENERATING_HMR_PATCH -> BUNDLED - test('handle generate hmr patch error', async () => { + // FIXME: https://github.com/rolldown/rolldown/issues/6319 + test.skip('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"), From bf114420b513b9df36c55152722f300170ac392e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:28:16 +0900 Subject: [PATCH 50/71] wip: update --- packages/vite/src/node/plugins/css.ts | 2 +- .../src/node/server/environments/fullBundleEnvironment.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 1482f31ce265c1..8387bf2f8181d6 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -398,7 +398,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) } } - return [url, resolved] + return [url, cleanUrl(resolved)] } if (config.command === 'build') { const isExternal = config.build.rollupOptions.external diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 2a96ee2af91c32..92a7f3dea9c9b7 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -122,8 +122,8 @@ export class FullBundleDevEnvironment extends DevEnvironment { () => { debug?.('INITIAL: run done') }, - () => { - debug?.('INITIAL: run error') + (e) => { + debug?.('INITIAL: run error', e) }, ) this.waitForInitialBuildFinish().then(() => { From b90a21db49f5bea9bc5240b844e96fd16764b8cb Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:14:36 +0900 Subject: [PATCH 51/71] wip: reject no-cors requests --- packages/vite/src/node/server/index.ts | 3 +- .../middlewares/rejectInvalidRequest.ts | 1 + .../server/middlewares/rejectNoCorsRequest.ts | 32 +++++++++++++ .../fs-serve/__tests__/fs-serve.spec.ts | 46 +++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index d73093acd28b2e..a12b5c4944c35d 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -102,6 +102,7 @@ import type { DevEnvironment } from './environment' import { hostValidationMiddleware } from './middlewares/hostCheck' import { rejectInvalidRequestMiddleware } from './middlewares/rejectInvalidRequest' import { memoryFilesMiddleware } from './middlewares/memoryFiles' +import { rejectNoCorsRequestMiddleware } from './middlewares/rejectNoCorsRequest' const usedConfigs = new WeakSet() @@ -881,8 +882,8 @@ export async function _createServer( middlewares.use(timeMiddleware(root)) } - // disallows request that contains `#` in the URL middlewares.use(rejectInvalidRequestMiddleware()) + middlewares.use(rejectNoCorsRequestMiddleware()) // cors const { cors } = serverConfig diff --git a/packages/vite/src/node/server/middlewares/rejectInvalidRequest.ts b/packages/vite/src/node/server/middlewares/rejectInvalidRequest.ts index 943ce9bd98fa4c..b7e4f2bde15e9f 100644 --- a/packages/vite/src/node/server/middlewares/rejectInvalidRequest.ts +++ b/packages/vite/src/node/server/middlewares/rejectInvalidRequest.ts @@ -1,5 +1,6 @@ import type { Connect } from '#dep-types/connect' +// disallows request that contains `#` in the URL export function rejectInvalidRequestMiddleware(): Connect.NextHandleFunction { // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return function viteRejectInvalidRequestMiddleware(req, res, next) { diff --git a/packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts b/packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts new file mode 100644 index 00000000000000..359c499e6eac6a --- /dev/null +++ b/packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts @@ -0,0 +1,32 @@ +import type { Connect } from 'dep-types/connect' + +/** + * A middleware that rejects no-cors mode requests that are not same-origin. + * + * We should avoid untrusted sites to load the script to avoid attacks like GHSA-4v9v-hfq4-rm2v. + * This is because: + * - the path of HMR patch files / entry point files can be predictable + * - the HMR patch files may not include ESM syntax + * (if they include ESM syntax, loading as a classic script would fail) + * - the HMR runtime in the browser has the list of all loaded modules + * + * https://github.com/webpack/webpack-dev-server/security/advisories/GHSA-4v9v-hfq4-rm2v + * https://green.sapphi.red/blog/local-server-security-best-practices#_2-using-xssi-and-modifying-the-prototype + * https://green.sapphi.red/blog/local-server-security-best-practices#properly-check-the-request-origin + */ +export function rejectNoCorsRequestMiddleware(): Connect.NextHandleFunction { + // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` + return function viteRejectNoCorsRequestMiddleware(req, res, next) { + // While we can set Cross-Origin-Resource-Policy header instead of rejecting requests, + // we choose to reject the request to be safer in case the request handler has any side-effects. + if ( + req.headers['sec-fetch-mode'] === 'no-cors' && + req.headers['sec-fetch-site'] !== 'same-origin' + ) { + res.statusCode = 403 + res.end('Cross-origin requests must be made with CORS mode enabled.') + return + } + return next() + } +} diff --git a/playground/fs-serve/__tests__/fs-serve.spec.ts b/playground/fs-serve/__tests__/fs-serve.spec.ts index 996c9fd505260b..d48c6e4d17072f 100644 --- a/playground/fs-serve/__tests__/fs-serve.spec.ts +++ b/playground/fs-serve/__tests__/fs-serve.spec.ts @@ -584,3 +584,49 @@ describe.runIf(!isServe)('preview HTML', () => { .toBe('404') }) }) + +test.runIf(isServe)( + 'load script with no-cors mode from a different origin', + async () => { + const viteTestUrlUrl = new URL(viteTestUrl) + + // NOTE: fetch cannot be used here as `fetch` sets some headers automatically + const res = await new Promise((resolve, reject) => { + http + .get( + viteTestUrl + '/src/code.js', + { + headers: { + 'Sec-Fetch-Dest': 'script', + 'Sec-Fetch-Mode': 'no-cors', + 'Sec-Fetch-Site': 'same-site', + Origin: 'http://vite.dev', + Host: viteTestUrlUrl.host, + }, + }, + (res) => { + resolve(res) + }, + ) + .on('error', (e) => { + reject(e) + }) + }) + expect(res.statusCode).toBe(403) + const body = Buffer.concat(await ArrayFromAsync(res)).toString() + expect(body).toBe( + 'Cross-origin requests must be made with CORS mode enabled.', + ) + }, +) + +// Note: Array.fromAsync is only supported in Node.js 22+ +async function ArrayFromAsync( + asyncIterable: AsyncIterable, +): Promise { + const chunks = [] + for await (const chunk of asyncIterable) { + chunks.push(chunk) + } + return chunks +} From 2be13c9020ce6a1bee8da27aec1b4be006baf431 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:14:33 +0900 Subject: [PATCH 52/71] wip: update --- .../environments/fullBundleEnvironment.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 92a7f3dea9c9b7..4041c251f97d8d 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -180,7 +180,6 @@ export class FullBundleDevEnvironment extends DevEnvironment { // TODO: need to check if this is enough this.handleHmrOutput([m.path], update, { firstInvalidatedBy: m.firstInvalidatedBy, - reason: m.message, }) })() } @@ -243,6 +242,10 @@ export class FullBundleDevEnvironment extends DevEnvironment { output.assetFileNames = 'assets/[name]-[hash][extname]' output.minify = false output.sourcemap = true + // output.advancedChunks ||= {} + // output.advancedChunks.groups = [ + // { name: 'chunk', maxSize: 1024 * 1024 }, + // ] } } else { rolldownOptions.output ??= {} @@ -251,7 +254,12 @@ export class FullBundleDevEnvironment extends DevEnvironment { rolldownOptions.output.assetFileNames = 'assets/[name]-[hash][extname]' rolldownOptions.output.minify = false rolldownOptions.output.sourcemap = true + // rolldownOptions.output.advancedChunks ||= {} + // rolldownOptions.output.advancedChunks.groups = [ + // { name: 'chunk', maxSize: 1024 * 1024 }, + // ] } + // rolldownOptions.experimental.strictExecutionOrder = true return rolldownOptions } @@ -259,7 +267,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { private handleHmrOutput( files: string[], hmrOutput: HmrOutput, - invalidateInformation?: { firstInvalidatedBy: string; reason?: string }, + invalidateInformation?: { firstInvalidatedBy: string }, ) { if (hmrOutput.type === 'Noop') return @@ -267,11 +275,9 @@ export class FullBundleDevEnvironment extends DevEnvironment { .map((file) => getShortName(file, this.config.root)) .join(', ') if (hmrOutput.type === 'FullReload') { - const reason = - (hmrOutput.reason ? colors.dim(` (${hmrOutput.reason})`) : '') + - (invalidateInformation?.reason - ? colors.dim(` (${invalidateInformation.reason})`) - : '') + const reason = hmrOutput.reason + ? colors.dim(` (${hmrOutput.reason})`) + : '' this.logger.info( colors.green(`trigger page reload `) + colors.dim(shortFile) + reason, { clear: !invalidateInformation, timestamp: true }, From 4c7ac57cdf61482f4ad2012ac7b506d0c0a80214 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:32:35 +0900 Subject: [PATCH 53/71] wip: initial client concept impl --- packages/vite/src/client/client.ts | 16 ++- .../environments/fullBundleEnvironment.ts | 113 +++++++++++++++--- packages/vite/types/customEvent.d.ts | 2 + 3 files changed, 113 insertions(+), 18 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 7c9c1f5a902154..3ddeba7deebb21 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -640,5 +640,19 @@ if (isFullBundleMode && typeof DevRuntime !== 'undefined') { } } - ;(globalThis as any).__rolldown_runtime__ ??= new ViteDevRuntime() + // TODO: make this more performant + const wrappedSocket = { + readyState: WebSocket.OPEN, + send(data: string) { + const d = JSON.parse(data) + transport.send({ + type: 'custom', + event: 'vite:module-loaded', + data: { modules: d.modules }, + }) + }, + } + ;(globalThis as any).__rolldown_runtime__ ??= new ViteDevRuntime( + wrappedSocket, + ) } diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 4041c251f97d8d..0ebe1c4794a45f 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import type { RolldownBuild } from 'rolldown' import { type DevEngine, dev } from 'rolldown/experimental' import type { Update } from 'types/hmrPayload' @@ -9,6 +10,7 @@ import type { ResolvedConfig } from '../../config' import type { ViteDevServer } from '../../server' import { createDebugger } from '../../utils' import { getShortName } from '../hmr' +import type { WebSocketClient } from '../ws' const debug = createDebugger('vite:full-bundle-mode') @@ -58,7 +60,11 @@ export class MemoryFiles { export class FullBundleDevEnvironment extends DevEnvironment { private devEngine!: DevEngine - private invalidateCalledModules = new Set() + private clients = new Clients() + private invalidateCalledModules = new Map< + /* clientId */ string, + Set + >() private debouncedFullReload = debounce(20, () => { this.hot.send({ type: 'full-reload', path: '*' }) this.logger.info(colors.green(`page reload`), { timestamp: true }) @@ -80,7 +86,7 @@ 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() debug?.('INITIAL: setup bundle options') @@ -98,19 +104,39 @@ export class FullBundleDevEnvironment extends DevEnvironment { : rollupOptions.output )! + // TODO: use hot API + server.ws.on( + 'vite:module-loaded', + (payload: { modules: string[] }, client: WebSocketClient) => { + const clientId = this.clients.setupIfNeeded(client, () => { + this.devEngine.removeClient(clientId) + }) + this.devEngine.registerModules(clientId, payload.modules) + }, + ) + server.ws.on('vite:invalidate', (payload, client: WebSocketClient) => { + this.handleInvalidateModule(client, payload) + }) + this.devEngine = await dev(rollupOptions, outputOptions, { onHmrUpdates: (updates, files) => { if (files.length === 0) { return } - // TODO: how to handle errors? - if (updates.every((update) => update.type === 'Noop')) { + // TODO: fix the need to clone + const clonedUpdates = updates.map((u) => ({ + clientId: u.clientId, + update: { ...u.update }, + })) + if (clonedUpdates.every((update) => update.update.type === 'Noop')) { debug?.(`ignored file change for ${files.join(', ')}`) return } - this.invalidateCalledModules.clear() - for (const update of updates) { - this.handleHmrOutput(files, update) + // TODO: how to handle errors? + for (const { clientId, update } of clonedUpdates) { + this.invalidateCalledModules.get(clientId)?.clear() + const client = this.clients.get(clientId)! + this.handleHmrOutput(client, files, update) } }, watch: { @@ -144,13 +170,24 @@ export class FullBundleDevEnvironment extends DevEnvironment { // no-op } - protected override invalidateModule(m: { - path: string - message?: string - firstInvalidatedBy: string - }): void { + protected override invalidateModule(_m: unknown): void { + // no-op, handled via `server.ws` instead + } + + private handleInvalidateModule( + client: WebSocketClient, + m: { + path: string + message?: string + firstInvalidatedBy: string + }, + ): void { ;(async () => { - if (this.invalidateCalledModules.has(m.path)) { + const clientId = this.clients.getId(client) + if (!clientId) return + + const invalidateCalledModules = this.invalidateCalledModules.get(clientId) + if (invalidateCalledModules?.has(m.path)) { debug?.( `INVALIDATE: invalidate received from ${m.path}, but ignored because it was already invalidated`, ) @@ -160,13 +197,18 @@ export class FullBundleDevEnvironment extends DevEnvironment { debug?.( `INVALIDATE: invalidate received from ${m.path}, re-triggering HMR`, ) - this.invalidateCalledModules.add(m.path) + if (!invalidateCalledModules) { + this.invalidateCalledModules.set(clientId, new Set([])) + } + this.invalidateCalledModules.get(clientId)!.add(m.path) // TODO: how to handle errors? - const update = await this.devEngine.invalidate( + const _update = await this.devEngine.invalidate( m.path, m.firstInvalidatedBy, ) + const update = _update.find((u) => u.clientId === clientId)?.update + if (!update) return if (update.type === 'Patch') { this.logger.info( @@ -178,7 +220,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { } // TODO: need to check if this is enough - this.handleHmrOutput([m.path], update, { + this.handleHmrOutput(client, [m.path], update, { firstInvalidatedBy: m.firstInvalidatedBy, }) })() @@ -265,6 +307,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { } private handleHmrOutput( + client: WebSocketClient, files: string[], hmrOutput: HmrOutput, invalidateInformation?: { firstInvalidatedBy: string }, @@ -307,7 +350,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { timestamp: Date.now(), } }) - this.hot.send({ + client.send({ type: 'update', updates, }) @@ -319,6 +362,42 @@ export class FullBundleDevEnvironment extends DevEnvironment { } } +class Clients { + private clientToId = new Map() + private idToClient = new Map() + + setupIfNeeded(client: WebSocketClient, onClose?: () => void): string { + const id = this.clientToId.get(client) + if (id) return id + + const newId = randomUUID() + this.clientToId.set(client, newId) + this.idToClient.set(newId, client) + client.socket.once('close', () => { + this.clientToId.delete(client) + this.idToClient.delete(newId) + onClose?.() + }) + return newId + } + + get(id: string): WebSocketClient | undefined { + return this.idToClient.get(id) + } + + getId(client: WebSocketClient): string | undefined { + return this.clientToId.get(client) + } + + delete(client: WebSocketClient): void { + const id = this.clientToId.get(client) + if (id) { + this.clientToId.delete(client) + this.idToClient.delete(id) + } + } +} + function debounce(time: number, cb: () => void) { let timer: ReturnType | null return () => { diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index f142e3fecebb42..7ce03cae168d1f 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -15,6 +15,8 @@ export interface CustomEventMap { 'vite:invalidate': InvalidatePayload 'vite:ws:connect': WebSocketConnectionPayload 'vite:ws:disconnect': WebSocketConnectionPayload + // TODO: polish this + 'vite:module-loaded': { modules: string[] } // server events 'vite:client:connect': undefined From 37019e57c36815f9bb76d2d8c7ee1f5fa1356f1a Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:52:13 +0900 Subject: [PATCH 54/71] wip: update --- .../src/node/server/middlewares/indexHtml.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index 91701c907c5e86..b24e24b9567071 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -466,12 +466,24 @@ export function indexHtmlMiddleware( if (!content && fullBundleEnv.memoryFiles.size !== 0) { return next() } + const secFetchDest = req.headers['sec-fetch-dest'] if ( - (await fullBundleEnv.triggerBundleRegenerationIfStale()) || - content === undefined + [ + 'document', + 'iframe', + 'frame', + 'fencedframe', + '', + undefined, + ].includes(secFetchDest) && + ((await fullBundleEnv.triggerBundleRegenerationIfStale()) || + content === undefined) ) { content = await generateFallbackHtml(server as ViteDevServer) } + if (!content) { + return next() + } const html = typeof content === 'string' ? content : Buffer.from(content.buffer) From 0d4aa39a902ddc9f74ef662264f6e0114a6fed8e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:01:47 +0900 Subject: [PATCH 55/71] wip: set etag --- .../environments/fullBundleEnvironment.ts | 39 +++++++++++-------- .../src/node/server/middlewares/indexHtml.ts | 16 ++++---- .../node/server/middlewares/memoryFiles.ts | 11 +++++- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 0ebe1c4794a45f..dc26bd1591b25c 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -3,6 +3,7 @@ import type { RolldownBuild } from 'rolldown' import { type DevEngine, dev } from 'rolldown/experimental' import type { Update } from 'types/hmrPayload' import colors from 'picocolors' +import getEtag from 'etag' import { ChunkMetadataMap, resolveRolldownOptions } from '../../build' import { getHmrImplementation } from '../../plugins/clientInjections' import { DevEnvironment, type DevEnvironmentContext } from '../environment' @@ -19,17 +20,19 @@ type HmrOutput = Exclude< undefined > +type MemoryFile = { + source: string | Uint8Array + etag?: string +} + export class MemoryFiles { - private files = new Map< - string, - string | Uint8Array | (() => string | Uint8Array) - >() + private files = new Map MemoryFile)>() get size(): number { return this.files.size } - get(file: string): string | Uint8Array | undefined { + get(file: string): MemoryFile | undefined { const result = this.files.get(file) if (result === undefined) { return undefined @@ -42,10 +45,7 @@ export class MemoryFiles { return result } - set( - file: string, - content: string | Uint8Array | (() => string | Uint8Array), - ): void { + set(file: string, content: MemoryFile | (() => MemoryFile)): void { this.files.set(file, content) } @@ -265,11 +265,16 @@ export class FullBundleDevEnvironment extends DevEnvironment { handler: (_, bundle) => { // NOTE: don't clear memoryFiles here as incremental build re-uses the files for (const outputFile of Object.values(bundle)) { - this.memoryFiles.set(outputFile.fileName, () => - outputFile.type === 'chunk' - ? outputFile.code - : outputFile.source, - ) + this.memoryFiles.set(outputFile.fileName, () => { + const source = + outputFile.type === 'chunk' + ? outputFile.code + : outputFile.source + return { + source, + etag: getEtag(Buffer.from(source), { weak: true }), + } + }) } }, }, @@ -336,9 +341,11 @@ export class FullBundleDevEnvironment extends DevEnvironment { code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code, }) - this.memoryFiles.set(hmrOutput.filename, hmrOutput.code) + this.memoryFiles.set(hmrOutput.filename, { source: hmrOutput.code }) if (hmrOutput.sourcemapFilename && hmrOutput.sourcemap) { - this.memoryFiles.set(hmrOutput.sourcemapFilename, hmrOutput.sourcemap) + this.memoryFiles.set(hmrOutput.sourcemapFilename, { + source: hmrOutput.sourcemap, + }) } const updates: Update[] = hmrOutput.hmrBoundaries.map((boundary: any) => { return { diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index b24e24b9567071..e4b575dca349cb 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -462,8 +462,8 @@ export function indexHtmlMiddleware( const pathname = decodeURIComponent(url) const filePath = pathname.slice(1) // remove first / - let content = fullBundleEnv.memoryFiles.get(filePath) - if (!content && fullBundleEnv.memoryFiles.size !== 0) { + let file = fullBundleEnv.memoryFiles.get(filePath) + if (!file && fullBundleEnv.memoryFiles.size !== 0) { return next() } const secFetchDest = req.headers['sec-fetch-dest'] @@ -477,20 +477,22 @@ export function indexHtmlMiddleware( undefined, ].includes(secFetchDest) && ((await fullBundleEnv.triggerBundleRegenerationIfStale()) || - content === undefined) + file === undefined) ) { - content = await generateFallbackHtml(server as ViteDevServer) + file = { source: await generateFallbackHtml(server as ViteDevServer) } } - if (!content) { + if (!file) { return next() } const html = - typeof content === 'string' ? content : Buffer.from(content.buffer) + typeof file.source === 'string' + ? file.source + : Buffer.from(file.source) const headers = isDev ? server.config.server.headers : server.config.preview.headers - return send(req, res, html, 'html', { headers }) + return send(req, res, html, 'html', { headers, etag: file.etag }) } let filePath: string diff --git a/packages/vite/src/node/server/middlewares/memoryFiles.ts b/packages/vite/src/node/server/middlewares/memoryFiles.ts index 76d4bdabb60bea..27c954f1cf9995 100644 --- a/packages/vite/src/node/server/middlewares/memoryFiles.ts +++ b/packages/vite/src/node/server/middlewares/memoryFiles.ts @@ -27,6 +27,15 @@ export function memoryFilesMiddleware( const file = memoryFiles.get(filePath) if (file) { + if (file.etag) { + if (req.headers['if-none-match'] === file.etag) { + res.statusCode = 304 + res.end() + return + } + res.setHeader('Etag', file.etag) + } + const mime = mrmime.lookup(filePath) if (mime) { res.setHeader('Content-Type', mime) @@ -36,7 +45,7 @@ export function memoryFilesMiddleware( res.setHeader(name, headers[name]!) } - return res.end(file) + return res.end(file.source) } next() } From 664486cb94e424472d8582a809f75c1644c7b9af Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:22:19 +0900 Subject: [PATCH 56/71] wip: update --- .../server/environments/fullBundleEnvironment.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index dc26bd1591b25c..03f095675fd669 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -1,6 +1,9 @@ import { randomUUID } from 'node:crypto' -import type { RolldownBuild } from 'rolldown' -import { type DevEngine, dev } from 'rolldown/experimental' +import { + type BindingClientHmrUpdate, + type DevEngine, + dev, +} from 'rolldown/experimental' import type { Update } from 'types/hmrPayload' import colors from 'picocolors' import getEtag from 'etag' @@ -15,10 +18,7 @@ import type { WebSocketClient } from '../ws' const debug = createDebugger('vite:full-bundle-mode') -type HmrOutput = Exclude< - Awaited>, - undefined -> +type HmrOutput = BindingClientHmrUpdate['update'] type MemoryFile = { source: string | Uint8Array @@ -252,7 +252,6 @@ export class FullBundleDevEnvironment extends DevEnvironment { const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap) rolldownOptions.experimental ??= {} rolldownOptions.experimental.hmr = { - new: true, implement: await getHmrImplementation(this.getTopLevelConfig()), } From 37a0b6fa0937846cfeb1d59e66a072d9d12dae6c Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:34:36 +0900 Subject: [PATCH 57/71] wip: update --- packages/vite/src/node/plugins/asset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 17ff9c7ca7e68d..ac648db238dd07 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -309,7 +309,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { }, watchChange(id) { - assetCache.get(this.environment)?.delete(id) + assetCache.get(this.environment)?.delete(normalizePath(id)) }, } } From 1f7a42d9fc170fdb7512b366e109b3e7147994fd Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:24:49 +0900 Subject: [PATCH 58/71] wip: update --- .../vite/src/node/server/environments/fullBundleEnvironment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 03f095675fd669..6be05f4c677e81 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -329,7 +329,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { colors.green(`trigger page reload `) + colors.dim(shortFile) + reason, { clear: !invalidateInformation, timestamp: true }, ) - this.devEngine.ensureLatestBuildOutput().then(() => { + this.devEngine.ensureCurrentBuildFinish().then(() => { this.debouncedFullReload() }) return From 932dd3513034c1677d99ace0698e7db15df81956 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:37:12 +0900 Subject: [PATCH 59/71] wip: update --- .../environments/fullBundleEnvironment.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 6be05f4c677e81..468e767d629f89 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -119,24 +119,23 @@ export class FullBundleDevEnvironment extends DevEnvironment { }) this.devEngine = await dev(rollupOptions, outputOptions, { - onHmrUpdates: (updates, files) => { - if (files.length === 0) { + onHmrUpdates: (result) => { + if (result instanceof Error) { + // TODO: handle error return } - // TODO: fix the need to clone - const clonedUpdates = updates.map((u) => ({ - clientId: u.clientId, - update: { ...u.update }, - })) - if (clonedUpdates.every((update) => update.update.type === 'Noop')) { - debug?.(`ignored file change for ${files.join(', ')}`) + const { updates, changedFiles } = result + if (changedFiles.length === 0) { return } - // TODO: how to handle errors? - for (const { clientId, update } of clonedUpdates) { + if (updates.every((update) => update.update.type === 'Noop')) { + debug?.(`ignored file change for ${changedFiles.join(', ')}`) + return + } + for (const { clientId, update } of updates) { this.invalidateCalledModules.get(clientId)?.clear() const client = this.clients.get(clientId)! - this.handleHmrOutput(client, files, update) + this.handleHmrOutput(client, changedFiles, update) } }, watch: { From 043ffa67432f0e9837cdadfe3a7f7764a15326ad Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:38:26 +0900 Subject: [PATCH 60/71] wip: update --- .../node/server/environments/fullBundleEnvironment.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 468e767d629f89..82c375d623d9d2 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -237,13 +237,8 @@ export class FullBundleDevEnvironment extends DevEnvironment { } override async close(): Promise { - await Promise.all([ - super.close(), - (async () => { - this.memoryFiles.clear() - // TODO: do we need close? - })(), - ]) + this.memoryFiles.clear() + await Promise.all([super.close(), this.devEngine.close()]) } private async getRolldownOptions() { From f8e30be0ecc1b0070540699483ae2919198bbef4 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:59:19 +0900 Subject: [PATCH 61/71] wip: update --- .../environments/fullBundleEnvironment.ts | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 82c375d623d9d2..989c4efe7bf3df 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -15,6 +15,7 @@ import type { ViteDevServer } from '../../server' import { createDebugger } from '../../utils' import { getShortName } from '../hmr' import type { WebSocketClient } from '../ws' +import { prepareError } from '../middlewares/error' const debug = createDebugger('vite:full-bundle-mode') @@ -121,7 +122,13 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.devEngine = await dev(rollupOptions, outputOptions, { onHmrUpdates: (result) => { if (result instanceof Error) { - // TODO: handle error + // TODO: send to the specific client + for (const client of this.clients.getAll()) { + client.send({ + type: 'error', + err: prepareError(result), + }) + } return } const { updates, changedFiles } = result @@ -138,6 +145,33 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.handleHmrOutput(client, changedFiles, update) } }, + onOutput: (result) => { + if (result instanceof Error) { + // TODO: handle error + return + } + + // TODO: make the API a bit more JS friendly + // NOTE: don't clear memoryFiles here as incremental build re-uses the files + for (const asset of result.assets) { + this.memoryFiles.set(asset.fileName, () => { + const source = asset.source.inner + return { + source, + etag: getEtag(Buffer.from(source), { weak: true }), + } + }) + } + for (const chunk of result.chunks) { + this.memoryFiles.set(chunk.fileName, () => { + const source = chunk.code + return { + source, + etag: getEtag(Buffer.from(source), { weak: true }), + } + }) + } + }, watch: { skipWrite: true, }, @@ -249,31 +283,6 @@ export class FullBundleDevEnvironment extends DevEnvironment { implement: await getHmrImplementation(this.getTopLevelConfig()), } - rolldownOptions.plugins = [ - rolldownOptions.plugins, - { - name: 'vite:full-bundle-mode:save-output', - generateBundle: { - order: 'post', - handler: (_, bundle) => { - // NOTE: don't clear memoryFiles here as incremental build re-uses the files - for (const outputFile of Object.values(bundle)) { - this.memoryFiles.set(outputFile.fileName, () => { - const source = - outputFile.type === 'chunk' - ? outputFile.code - : outputFile.source - return { - source, - etag: getEtag(Buffer.from(source), { weak: true }), - } - }) - } - }, - }, - }, - ] - // 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) { @@ -389,6 +398,10 @@ class Clients { return this.clientToId.get(client) } + getAll(): WebSocketClient[] { + return Array.from(this.idToClient.values()) + } + delete(client: WebSocketClient): void { const id = this.clientToId.get(client) if (id) { From bf1efaa855f068360d649d869077bd21f753026e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:51:33 +0900 Subject: [PATCH 62/71] wip: update --- .../environments/fullBundleEnvironment.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 989c4efe7bf3df..3b3b6cf35bacb9 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -151,20 +151,11 @@ export class FullBundleDevEnvironment extends DevEnvironment { return } - // TODO: make the API a bit more JS friendly // NOTE: don't clear memoryFiles here as incremental build re-uses the files - for (const asset of result.assets) { - this.memoryFiles.set(asset.fileName, () => { - const source = asset.source.inner - return { - source, - etag: getEtag(Buffer.from(source), { weak: true }), - } - }) - } - for (const chunk of result.chunks) { - this.memoryFiles.set(chunk.fileName, () => { - const source = chunk.code + for (const outputFile of result.output) { + this.memoryFiles.set(outputFile.fileName, () => { + const source = + outputFile.type === 'chunk' ? outputFile.code : outputFile.source return { source, etag: getEtag(Buffer.from(source), { weak: true }), From 366a30a6f0a228a19d60215b2dcc197275af71da Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:03:05 +0900 Subject: [PATCH 63/71] wip: update --- .../server/environments/fullBundleEnvironment.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 3b3b6cf35bacb9..3080788344cea9 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -1,4 +1,5 @@ import { randomUUID } from 'node:crypto' +import { setTimeout } from 'node:timers/promises' import { type BindingClientHmrUpdate, type DevEngine, @@ -148,6 +149,9 @@ export class FullBundleDevEnvironment extends DevEnvironment { onOutput: (result) => { if (result instanceof Error) { // TODO: handle error + this.logger.error(colors.red(`✘ Build error: ${result.message}`), { + error: result, + }) return } @@ -185,7 +189,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { private async waitForInitialBuildFinish(): Promise { await this.devEngine.ensureCurrentBuildFinish() while (this.memoryFiles.size === 0) { - await new Promise((resolve) => setTimeout(resolve, 10)) + await setTimeout(10) await this.devEngine.ensureCurrentBuildFinish() } } @@ -323,7 +327,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { colors.green(`trigger page reload `) + colors.dim(shortFile) + reason, { clear: !invalidateInformation, timestamp: true }, ) - this.devEngine.ensureCurrentBuildFinish().then(() => { + this.devEngine.ensureLatestBuildOutput().then(() => { this.debouncedFullReload() }) return @@ -403,12 +407,12 @@ class Clients { } function debounce(time: number, cb: () => void) { - let timer: ReturnType | null + let timer: ReturnType | null return () => { if (timer) { - clearTimeout(timer) + globalThis.clearTimeout(timer) timer = null } - timer = setTimeout(cb, time) + timer = globalThis.setTimeout(cb, time) } } From 978c3f168bd2510bab4f1f69740f9959c902b4cd Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:10:11 +0900 Subject: [PATCH 64/71] wip: update --- .../__tests__/hmr-full-bundle-mode.spec.ts | 9 ++++----- playground/hmr/vite.config.ts | 8 +++++--- 2 files changed, 9 insertions(+), 8 deletions(-) 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 index 458cafdb74246f..7458c670554cd1 100644 --- 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 @@ -21,8 +21,7 @@ if (isBuild) { }) // BUNDLED -> GENERATE_HMR_PATCH -> BUNDLING -> BUNDLE_ERROR -> BUNDLING -> BUNDLED - // FIXME: https://github.com/rolldown/rolldown/issues/6319 - test.skip('handle bundle error', async () => { + test('handle bundle error', async () => { editFile('main.js', (code) => code.replace("text('.app', 'hello')", "text('.app', 'hello'); text("), ) @@ -71,8 +70,7 @@ if (isBuild) { }) // BUNDLED -> GENERATING_HMR_PATCH -> BUNDLED - // FIXME: https://github.com/rolldown/rolldown/issues/6319 - test.skip('handle generate hmr patch error', async () => { + 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"), @@ -101,7 +99,8 @@ if (isBuild) { }) // BUNDLED -> GENERATING_HMR_PATCH -> GENERATING_HMR_PATCH -> BUNDLED - test('continuous generate hmr patch', async () => { + // FIXME: https://github.com/rolldown/rolldown/issues/6648 + test.skip('continuous generate hmr patch', async () => { editFile('hmr.js', (code) => code.replace( "const foo = 'hello'", diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index 4c782273b9d053..21b0b1aa296a06 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'vite' import type { Plugin } from 'vite' import { TestCssLinkPlugin } from './css-link/plugin' -export default defineConfig({ +export default defineConfig(({ command }) => ({ experimental: { hmrPartialAccept: true, }, @@ -12,7 +12,9 @@ export default defineConfig({ rollupOptions: { input: [ path.resolve(import.meta.dirname, './index.html'), - path.resolve(import.meta.dirname, './missing-import/index.html'), + ...(command === 'build' + ? [] + : [path.resolve(import.meta.dirname, './missing-import/index.html')]), path.resolve( import.meta.dirname, './unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', @@ -57,7 +59,7 @@ export default defineConfig({ TestCssLinkPlugin(), hotEventsPlugin(), ], -}) +})) function virtualPlugin(): Plugin { let num = 0 From 60a54696b769c496103e57716c80f7aea53667cd Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:31:06 +0900 Subject: [PATCH 65/71] wip: update --- .../src/node/server/environments/fullBundleEnvironment.ts | 4 ++-- packages/vite/src/node/server/middlewares/memoryFiles.ts | 2 +- .../vite/src/node/server/middlewares/rejectNoCorsRequest.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 3080788344cea9..0f1ecd3e5c1565 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -5,9 +5,9 @@ import { type DevEngine, dev, } from 'rolldown/experimental' -import type { Update } from 'types/hmrPayload' import colors from 'picocolors' import getEtag from 'etag' +import type { Update } from '#types/hmrPayload' import { ChunkMetadataMap, resolveRolldownOptions } from '../../build' import { getHmrImplementation } from '../../plugins/clientInjections' import { DevEnvironment, type DevEnvironmentContext } from '../environment' @@ -72,7 +72,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { this.logger.info(colors.green(`page reload`), { timestamp: true }) }) - memoryFiles = new MemoryFiles() + memoryFiles: MemoryFiles = new MemoryFiles() constructor( name: string, diff --git a/packages/vite/src/node/server/middlewares/memoryFiles.ts b/packages/vite/src/node/server/middlewares/memoryFiles.ts index 27c954f1cf9995..152989eb031cb7 100644 --- a/packages/vite/src/node/server/middlewares/memoryFiles.ts +++ b/packages/vite/src/node/server/middlewares/memoryFiles.ts @@ -1,5 +1,5 @@ -import type { Connect } from 'dep-types/connect' import * as mrmime from 'mrmime' +import type { Connect } from '#dep-types/connect' import { cleanUrl } from '../../../shared/utils' import type { ViteDevServer } from '..' import { FullBundleDevEnvironment } from '../environments/fullBundleEnvironment' diff --git a/packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts b/packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts index 359c499e6eac6a..29c67af9dab9f1 100644 --- a/packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts +++ b/packages/vite/src/node/server/middlewares/rejectNoCorsRequest.ts @@ -1,4 +1,4 @@ -import type { Connect } from 'dep-types/connect' +import type { Connect } from '#dep-types/connect' /** * A middleware that rejects no-cors mode requests that are not same-origin. From 96e60c9ce4f898a130124703eecea1bd8fdba857 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:50:06 +0900 Subject: [PATCH 66/71] wip: update --- packages/vite/src/client/client.ts | 20 +++++++------------ .../vite/src/node/plugins/clientInjections.ts | 2 +- packages/vite/src/node/plugins/define.ts | 1 - .../environments/fullBundleEnvironment.ts | 10 ---------- .../src/node/server/middlewares/indexHtml.ts | 20 +++++++++++++++++-- .../middlewares/rejectInvalidRequest.ts | 4 +++- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 3ddeba7deebb21..44c375cdab865c 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -1,4 +1,3 @@ -/// import type { ErrorPayload, HotPayload } from '#types/hmrPayload' import type { ViteHotContext } from '#types/hot' import { HMRClient, HMRContext } from '../shared/hmr' @@ -618,24 +617,19 @@ export function injectQuery(url: string, queryToInject: string): string { export { ErrorOverlay } +// TODO: proper types +declare let DevRuntime: { new (socket: any): any } | undefined + if (isFullBundleMode && typeof DevRuntime !== 'undefined') { class ViteDevRuntime extends DevRuntime { - override createModuleHotContext(moduleId: string) { + 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) + // @ts-expect-error TODO: support CSS properly + ctx._internal = { updateStyle, removeStyle } return ctx } - override applyUpdates(_boundaries: string[]): void { - // TODO: how should this be handled? + applyUpdates(_boundaries: string[]): void { // noop, handled in the HMR client } } diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 0ccb432c050d0b..a456cf900b69ca 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -140,7 +140,7 @@ export async function getHmrImplementation( const replacer = await createClientConfigValueReplacer(config) return ( replacer(content) - // the rolldown runtime shouldn't be importer a module + // the rolldown runtime cannot import a module .replace(/import\s*['"]@vite\/env['"]/, '') ) } diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 153b230b66b1f0..d8a610062f34dc 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -224,7 +224,6 @@ 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/server/environments/fullBundleEnvironment.ts b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts index 0f1ecd3e5c1565..b4291d39541fa0 100644 --- a/packages/vite/src/node/server/environments/fullBundleEnvironment.ts +++ b/packages/vite/src/node/server/environments/fullBundleEnvironment.ts @@ -247,7 +247,6 @@ export class FullBundleDevEnvironment extends DevEnvironment { ) } - // TODO: need to check if this is enough this.handleHmrOutput(client, [m.path], update, { firstInvalidatedBy: m.firstInvalidatedBy, }) @@ -286,10 +285,6 @@ export class FullBundleDevEnvironment extends DevEnvironment { output.assetFileNames = 'assets/[name]-[hash][extname]' output.minify = false output.sourcemap = true - // output.advancedChunks ||= {} - // output.advancedChunks.groups = [ - // { name: 'chunk', maxSize: 1024 * 1024 }, - // ] } } else { rolldownOptions.output ??= {} @@ -298,12 +293,7 @@ export class FullBundleDevEnvironment extends DevEnvironment { rolldownOptions.output.assetFileNames = 'assets/[name]-[hash][extname]' rolldownOptions.output.minify = false rolldownOptions.output.sourcemap = true - // rolldownOptions.output.advancedChunks ||= {} - // rolldownOptions.output.advancedChunks.groups = [ - // { name: 'chunk', maxSize: 1024 * 1024 }, - // ] } - // rolldownOptions.experimental.strictExecutionOrder = true return rolldownOptions } diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index e4b575dca349cb..98ee520bbdca58 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -565,10 +565,26 @@ async function generateFallbackHtml(server: ViteDevServer) { ${hmrRuntime.replaceAll('', '<\\/script>')}