diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8528988ae8ad54..3e4e45686d1b61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,6 +124,9 @@ jobs: - name: Test serve run: pnpm run test-serve + - name: Test full bundle mode serve + run: pnpm run test-full-bundle-mode + - name: Test build run: pnpm run test-build diff --git a/package.json b/package.json index 13cf92b7201e4c..bf7ace197b04e0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "format": "prettier --write --cache .", "lint": "eslint --cache .", "typecheck": "tsc -p scripts --noEmit && pnpm -r --parallel run typecheck", - "test": "pnpm test-unit && pnpm test-serve && pnpm test-build", + "test": "pnpm test-unit && pnpm test-serve && pnpm run test-full-bundle-mode && pnpm test-build", + "test-full-bundle-mode": "VITE_TEST_FULL_BUNDLE_MODE=1 vitest run -c vitest.config.e2e.ts", "test-serve": "vitest run -c vitest.config.e2e.ts", "test-build": "VITE_TEST_BUILD=1 vitest run -c vitest.config.e2e.ts", "test-unit": "vitest run", @@ -98,6 +99,7 @@ "packageManager": "pnpm@10.10.0", "pnpm": { "overrides": { + "vitest>vite": "npm:vite@^6.2.6", "vite": "workspace:rolldown-vite@*" }, "patchedDependencies": { diff --git a/packages/vite/index.cjs b/packages/vite/index.cjs index ec356c719ff4b9..61edace193cb82 100644 --- a/packages/vite/index.cjs +++ b/packages/vite/index.cjs @@ -13,6 +13,7 @@ Object.assign(module.exports, require('./dist/node-cjs/publicUtils.cjs')) const asyncFunctions = [ 'build', 'createServer', + 'createServerWithResolvedConfig', 'preview', 'transformWithEsbuild', 'transformWithOxc', diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 1db4a49aa2556a..aa5f9d6e4f43a8 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -7,6 +7,7 @@ import { } from '../shared/moduleRunnerTransport' import { createHMRHandler } from '../shared/hmrHandler' import { ErrorOverlay, overlayId } from './overlay' +import './hmrModuleRunner' import '@vite/env' // injected by the hmr plugin when served @@ -20,6 +21,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...') @@ -140,20 +142,36 @@ const hmrClient = new HMRClient( }, transport, async function importUpdatedModule({ + url, acceptedPath, timestamp, explicitImportRequired, isWithinCircularImport, }) { - const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`) - const importPromise = import( - /* @vite-ignore */ - base + - acceptedPathWithoutQuery.slice(1) + - `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${ - query ? `&${query}` : '' - }` - ) + function importModuleWithFullBundleMode() { + const importPromise = import(base + url) + return importPromise.then(() => + // @ts-expect-error globalThis.__rolldown_runtime__ + globalThis.__rolldown_runtime__.loadExports(acceptedPath), + ) + } + + function importModule() { + const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`) + const importPromise = import( + /* @vite-ignore */ + base + + acceptedPathWithoutQuery.slice(1) + + `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${ + query ? `&${query}` : '' + }` + ) + return importPromise + } + + const importPromise = __FULL_BUNDLE_MODE__ + ? importModuleWithFullBundleMode() + : importModule() if (isWithinCircularImport) { importPromise.catch(() => { console.info( @@ -165,6 +183,7 @@ const hmrClient = new HMRClient( } return await importPromise }, + __FULL_BUNDLE_MODE__, ) transport.connect!(createHMRHandler(handleMessage)) @@ -436,7 +455,7 @@ export function removeStyle(id: string): void { } export function createHotContext(ownerPath: string): ViteHotContext { - return new HMRContext(hmrClient, ownerPath) + return new HMRContext(hmrClient, ownerPath, __FULL_BUNDLE_MODE__) } /** diff --git a/packages/vite/src/client/hmrModuleRunner.ts b/packages/vite/src/client/hmrModuleRunner.ts new file mode 100644 index 00000000000000..2c9f95ec736a5c --- /dev/null +++ b/packages/vite/src/client/hmrModuleRunner.ts @@ -0,0 +1,63 @@ +import { createHotContext } from './client' + +declare const __FULL_BUNDLE_MODE__: boolean + +if (__FULL_BUNDLE_MODE__) { + 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) { + return createHotContext(moduleId) + } + + 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/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 339b6a6a8908e6..de74899548b306 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -77,6 +77,7 @@ export class ModuleRunner { resolvedHmrLogger, this.transport, ({ acceptedPath }) => this.import(acceptedPath), + false, ) if (!this.transport.connect) { throw new Error( @@ -391,7 +392,7 @@ export class ModuleRunner { throw new Error(`[module runner] HMR client was closed.`) } this.debug?.('[module runner] creating hmr context for', mod.url) - hotContext ||= new HMRContext(this.hmrClient, mod.url) + hotContext ||= new HMRContext(this.hmrClient, mod.url, false) return hotContext }, set: (value) => { diff --git a/packages/vite/src/node/__tests__/plugins/import.spec.ts b/packages/vite/src/node/__tests__/plugins/import.spec.ts index c43bfed53115a8..63c82af2bad023 100644 --- a/packages/vite/src/node/__tests__/plugins/import.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/import.spec.ts @@ -75,10 +75,7 @@ describe('transformCjsImport', () => { ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + - 'const react = ((m) => m?.__esModule ? m : {\n' + - '\t...typeof m === "object" && !Array.isArray(m) || typeof m === "function" ? m : {},\n' + - '\tdefault: m\n' + - '})(__vite__cjsImport0_react)', + 'const react = ((m) => m?.__esModule ? m : { ...typeof m === "object" && !Array.isArray(m) || typeof m === "function" ? m : {}, default: m })(__vite__cjsImport0_react)', ) }) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 476b01ec8f30d3..17f17feb24af5d 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -32,6 +32,7 @@ import type { RollupCommonJSOptions } from 'dep-types/commonjs' import type { RollupDynamicImportVarsOptions } from 'dep-types/dynamicImportVars' import type { EsbuildTarget } from 'types/internal/esbuildOptions' import type { ChunkMetadata } from 'types/metadata' +import type { Update } from 'types/hmrPayload' import { withTrailingSlash } from '../shared/utils' import { DEFAULT_ASSETS_INLINE_LIMIT, @@ -62,6 +63,7 @@ import { mergeWithDefaults, normalizePath, partialEncodeURIPath, + setupSIGTERMListener, unique, } from './utils' import { perEnvironmentPlugin, resolveEnvironmentPlugins } from './plugin' @@ -89,6 +91,9 @@ import { import type { Plugin } from './plugin' import type { RollupPluginHooks } from './typeUtils' import { buildOxcPlugin } from './plugins/oxc' +import type { ViteDevServer } from './server' +import { getHmrImplement } from './plugins/clientInjections' +// import { buildModuleGraphPlugin } from './server/buildModuleGraph' export interface BuildEnvironmentOptions { /** @@ -555,22 +560,23 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ export async function build( inlineConfig: InlineConfig = {}, ): Promise { - const builder = await createBuilder(inlineConfig, true) + const builder = await createBuilder(inlineConfig, true, 'build') const environment = Object.values(builder.environments)[0] if (!environment) throw new Error('No environment found') return builder.build(environment) } function resolveConfigToBuild( + command: 'build' | 'serve', inlineConfig: InlineConfig = {}, patchConfig?: (config: ResolvedConfig) => void, patchPlugins?: (resolvedPlugins: Plugin[]) => void, ): Promise { return resolveConfig( inlineConfig, - 'build', - 'production', - 'production', + command, + command === 'serve' ? 'development' : 'production', + command === 'serve' ? 'development' : 'production', false, patchConfig, patchPlugins, @@ -582,8 +588,9 @@ function resolveConfigToBuild( **/ async function buildEnvironment( environment: BuildEnvironment, + server?: ViteDevServer, ): Promise { - const { root, packageCache } = environment.config + const { root, packageCache, experimental, command } = environment.config const options = environment.config.build const libOptions = options.lib const { logger } = environment @@ -642,6 +649,10 @@ async function buildEnvironment( injectEnvironmentToHooks(environment, chunkMetadataMap, p), ) + // if (server) { + // plugins.push(buildModuleGraphPlugin(server)) + // } + const rollupOptions: RolldownOptions = { // preserveEntrySignatures: ssr // ? 'allow-extension' @@ -649,6 +660,7 @@ async function buildEnvironment( // ? 'strict' // : false, // cache: options.watch ? undefined : false, + // cwd: root, ...options.rollupOptions, output: options.rollupOptions.output, input, @@ -667,6 +679,17 @@ async function buildEnvironment( ...options.rollupOptions.moduleTypes, '.css': 'js', }, + experimental: { + ...options.rollupOptions.experimental, + hmr: server + ? { + implement: await getHmrImplement(environment.config), + } + : false, + }, + treeshake: experimental.fullBundleMode + ? false + : options.rollupOptions.treeshake, } /** @@ -810,11 +833,13 @@ async function buildEnvironment( (isSsrTargetWebworkerEnvironment && (typeof input === 'string' || Object.keys(input).length === 1)), minify: - options.minify === 'oxc' - ? true - : options.minify === false - ? 'dce-only' - : false, + experimental.fullBundleMode && command === 'serve' + ? false + : options.minify === 'oxc' + ? true + : options.minify === false + ? 'dce-only' + : false, ...output, } } @@ -857,6 +882,7 @@ async function buildEnvironment( resolvedOutDirs, emptyOutDir, environment.config.cacheDir, + !!experimental.fullBundleMode, ) const { watch } = await import('rolldown') @@ -895,13 +921,148 @@ async function buildEnvironment( prepareOutDir(resolvedOutDirs, emptyOutDir, environment) } - const res: RolldownOutput[] = [] - for (const output of normalizedOutputs) { - res.push(await bundle[options.write ? 'write' : 'generate'](output)) - } + const res = await build() + logger.info( `${colors.green(`✓ built in ${displayTime(Date.now() - startTime)}`)}`, ) + + async function build() { + const res: RolldownOutput[] = [] + for (const output of normalizedOutputs) { + // TODO(underfin): using the generate at development build could be improve performance. + res.push(await bundle![options.write ? 'write' : 'generate'](output)) + } + + if (server) { + // watching the files + for (const file of await bundle!.watchFiles) { + if (path.isAbsolute(file) && fs.existsSync(file)) { + server.watcher.add(file) + } + } + + // Write the output files to memory + for (const output of res) { + for (const outputFile of output.output) { + server.memoryFiles[outputFile.fileName] = + outputFile.type === 'chunk' ? outputFile.code : outputFile.source + } + } + } + return res + } + + if (server) { + async function handleHmrOutput(hmrOutput: any, file: string) { + if (hmrOutput.fullReload) { + if (!hmrOutput.firstInvalidatedBy) { + await build() + } + server!.ws.send({ + type: 'full-reload', + }) + const reason = hmrOutput.fullReloadReason + ? colors.dim(` (${hmrOutput.fullReloadReason})`) + : '' + logger.info( + colors.green(`page reload `) + colors.dim(file) + reason, + { + clear: !hmrOutput.firstInvalidatedBy, + timestamp: true, + }, + ) + return + } + + if (hmrOutput.code) { + server!.memoryFiles[hmrOutput.filename] = hmrOutput.code + if (hmrOutput.sourcemap) { + server!.memoryFiles[hmrOutput.sourcemapFilename] = + hmrOutput.sourcemap + } + const updates = hmrOutput.hmrBoundaries.map((boundary: any) => { + return { + type: 'js-update', + url: hmrOutput.filename, + path: boundary.boundary, + acceptedPath: boundary.acceptedVia, + firstInvalidatedBy: hmrOutput.firstInvalidatedBy, + timestamp: 0, + } + }) as Update[] + server!.ws.send({ + type: 'update', + updates, + }) + logger.info( + colors.green(`hmr update `) + + colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), + { clear: !hmrOutput.firstInvalidatedBy, timestamp: true }, + ) + } + } + + let debouncedBuild: NodeJS.Timeout | undefined + + function debounceBuild() { + cancelBuild() + debouncedBuild = setTimeout(async () => { + const startTime = Date.now() + await build() + logger.info( + `${colors.green(`✓ rebuilt in ${displayTime(Date.now() - startTime)}`)}`, + ) + }, 20) + } + + function cancelBuild() { + if (debouncedBuild) clearTimeout(debouncedBuild) + } + + server.watcher.on('change', async (file) => { + // The playground/hmr test `plugin hmr remove custom events` need to skip the change of unused files. + if (!(await bundle!.watchFiles).includes(file)) { + return + } + + const hmrOutput = (await bundle!.generateHmrPatch([file]))! + + await handleHmrOutput(hmrOutput, file) + + if (hmrOutput.code) { + debounceBuild() + } + }) + + server.hot.on( + 'vite:invalidate', + async ({ path: file, message, firstInvalidatedBy }) => { + // cancel the debounce build util the hmr invalidate is done. + cancelBuild() + const hmrOutput = (await bundle!.hmrInvalidate( + path.join(root, file), + firstInvalidatedBy, + ))! + if (hmrOutput) { + if (hmrOutput.isSelfAccepting) { + logger.info( + colors.yellow(`hmr invalidate `) + + colors.dim(file) + + (message ? ` ${message}` : ''), + { timestamp: true }, + ) + await handleHmrOutput(hmrOutput, file) + + if (hmrOutput.code) { + debounceBuild() + } + } + } + }, + ) + } + return Array.isArray(outputs) ? res : res[0] } catch (e) { enhanceRollupError(e) @@ -914,7 +1075,20 @@ async function buildEnvironment( } throw e } finally { - if (bundle) await bundle.close() + // Note: close the bundle will make the rolldown hmr invalid, so dev build need to disable it. + if (server) { + const closeBundleAndExit = async (_: unknown, exitCode?: number) => { + try { + if (bundle) await bundle.close() + } finally { + process.exitCode ??= exitCode ? 128 + exitCode : undefined + process.exit() + } + } + setupSIGTERMListener(closeBundleAndExit) + } else { + if (bundle) await bundle.close() + } } } @@ -1628,9 +1802,10 @@ export class BuildEnvironment extends BaseEnvironment { export interface ViteBuilder { environments: Record config: ResolvedConfig - buildApp(): Promise + buildApp(server?: ViteDevServer): Promise build( environment: BuildEnvironment, + server?: ViteDevServer, ): Promise } @@ -1649,12 +1824,15 @@ export interface BuilderOptions { * @experimental */ sharedPlugins?: boolean - buildApp?: (builder: ViteBuilder) => Promise + buildApp?: (builder: ViteBuilder, server?: ViteDevServer) => Promise } -async function defaultBuildApp(builder: ViteBuilder): Promise { +async function defaultBuildApp( + builder: ViteBuilder, + server?: ViteDevServer, +): Promise { for (const environment of Object.values(builder.environments)) { - await builder.build(environment) + await builder.build(environment, server) } } @@ -1683,6 +1861,7 @@ export type ResolvedBuilderOptions = Required export async function createBuilder( inlineConfig: InlineConfig = {}, useLegacyBuilder: null | boolean = false, + command: 'build' | 'serve' = 'build', ): Promise { const patchConfig = (resolved: ResolvedConfig) => { if (!(useLegacyBuilder ?? !resolved.builder)) return @@ -1696,7 +1875,7 @@ export async function createBuilder( ...resolved.environments[environmentName].build, } } - const config = await resolveConfigToBuild(inlineConfig, patchConfig) + const config = await resolveConfigToBuild(command, inlineConfig, patchConfig) useLegacyBuilder ??= !config.builder const configBuilder = config.builder ?? resolveBuilderOptions({})! @@ -1705,11 +1884,11 @@ export async function createBuilder( const builder: ViteBuilder = { environments, config, - async buildApp() { - return configBuilder.buildApp(builder) + async buildApp(server?: ViteDevServer) { + return configBuilder.buildApp(builder, server) }, - async build(environment: BuildEnvironment) { - return buildEnvironment(environment) + async build(environment: BuildEnvironment, server?: ViteDevServer) { + return buildEnvironment(environment, server) }, } @@ -1759,6 +1938,7 @@ export async function createBuilder( } } environmentConfig = await resolveConfigToBuild( + command, inlineConfig, patchConfig, patchPlugins, diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 29401d0e588343..0fd1288500c95d 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -32,6 +32,7 @@ interface GlobalCLIOptions { mode?: string force?: boolean w?: boolean + fullBundleMode?: boolean } interface BuilderCLIOptions { @@ -97,6 +98,7 @@ function cleanGlobalCLIOptions( delete ret.mode delete ret.force delete ret.w + delete ret.fullBundleMode // convert the sourcemap option to a boolean if necessary if ('sourcemap' in ret) { @@ -176,93 +178,148 @@ 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, - }) - - if (!server.httpServer) { - throw new Error('HTTP server not available') + .option( + '--fullBundleMode', + `[boolean] Enable bundle at dev to instead of esm dev server`, + ) + // TODO(underfin): Consider how to merge the build option into dev command. + .action( + async ( + root: string, + options: BuildEnvironmentOptions & + BuilderCLIOptions & + ServerOptions & + GlobalCLIOptions, + ) => { + filterDuplicateOptions(options) + // output structure is preserved even after bundling so require() + // is ok here + async function createServerWithFullBundleMode() { + const { createServerWithResolvedConfig } = await import('./server') + + const { createBuilder } = await import('./build') + + const buildOptions: BuildEnvironmentOptions = + cleanBuilderCLIOptions(options) + + const inlineConfig: InlineConfig = { + root, + base: options.base, + mode: options.mode, + configFile: options.config, + configLoader: options.configLoader, + logLevel: options.logLevel, + clearScreen: options.clearScreen, + build: buildOptions, + experimental: { + fullBundleMode: true, + }, + ...(options.app ? { builder: {} } : {}), + } + const builder = await createBuilder(inlineConfig, null, 'serve') + + const server = await createServerWithResolvedConfig(builder.config) + + if (!server.httpServer) { + throw new Error('HTTP server not available') + } + // Need to make sure the server port and then start build. + await server.listen() + + await builder.buildApp(server) + + return server } - await server.listen() + async function createServer() { + const { createServer } = await import('./server') + 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, + }) + await server.listen() + return server + } + + try { + const server = options.fullBundleMode + ? await createServerWithFullBundleMode() + : await createServer() - 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 - - info( - `\n ${colors.green( - `${colors.bold('ROLLDOWN-VITE')} v${VERSION}`, - )}${modeString} ${startupDurationString}\n`, - { - clear: !hasExistingLogs, - }, - ) + const hasExistingLogs = + process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0 - 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() + 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.bindCLIShortcuts({ print: true, customShortcuts }) + } catch (e) { + const logger = createLogger(options.logLevel) + logger.error( + colors.red(`error when starting dev server:\n${e.stack}`), + { + error: e, }, - }) + ) + stopProfiler(logger.info) + process.exit(1) } - server.bindCLIShortcuts({ print: true, customShortcuts }) - } catch (e) { - const logger = createLogger(options.logLevel) - logger.error(colors.red(`error when starting dev server:\n${e.stack}`), { - error: e, - }) - stopProfiler(logger.info) - process.exit(1) - } - }) + }, + ) // build cli @@ -322,7 +379,7 @@ cli build: buildOptions, ...(options.app ? { builder: {} } : {}), } - const builder = await createBuilder(inlineConfig, null) + const builder = await createBuilder(inlineConfig, null, 'build') await builder.buildApp() } catch (e) { createLogger(options.logLevel).error( diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 62c0db4b81d5ed..0a247cf83db918 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -525,6 +525,13 @@ export interface ExperimentalOptions { * @default false */ enableNativePlugin?: boolean | 'resolver' + /** + * Enable full bundle mode at dev. + * + * @experimental + * @default false + */ + fullBundleMode?: boolean } export interface LegacyOptions { @@ -1310,7 +1317,9 @@ export async function resolveConfig( const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawPlugins) - const isBuild = command === 'build' + const isBuild = config.experimental?.fullBundleMode + ? mode === 'production' + : command === 'build' // run config hooks const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts index 6f43238ded87e2..f698fbbb633a12 100644 --- a/packages/vite/src/node/idResolver.ts +++ b/packages/vite/src/node/idResolver.ts @@ -56,6 +56,9 @@ export function createIdResolver( ): Promise { let pluginContainer = pluginContainerMap.get(environment) if (!pluginContainer) { + const isBuild = config.experimental?.fullBundleMode + ? config.mode === 'production' + : config.command === 'build' pluginContainer = await createEnvironmentPluginContainer( environment as Environment, [ @@ -66,7 +69,7 @@ export function createIdResolver( { root: config.root, isProduction: config.isProduction, - isBuild: config.command === 'build', + isBuild, asSrc: true, preferRelative: false, tryIndex: true, @@ -80,7 +83,7 @@ export function createIdResolver( resolvePlugin({ root: config.root, isProduction: config.isProduction, - isBuild: config.command === 'build', + isBuild, asSrc: true, preferRelative: false, tryIndex: true, diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 1286b2ca2c7c9d..e770ccd022d17d 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -11,7 +11,7 @@ export { } from './config' export { perEnvironmentPlugin } from './plugin' export { perEnvironmentState } from './environment' -export { createServer } from './server' +export { createServer, createServerWithResolvedConfig } from './server' export { preview } from './preview' export { build, createBuilder } from './build' diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index c94c8c8272e8c2..aacd40720868cd 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -229,10 +229,17 @@ export function assetPlugin(config: ResolvedConfig): Plugin { // Force rollup to keep this module from being shared between other entry points if it's an entrypoint. // If the resulting chunk is empty, it will be removed in generateBundle. moduleSideEffects: - config.command === 'build' && this.getModuleInfo(id)?.isEntry + (config.command === 'build' || + !!config.experimental.fullBundleMode) && + this.getModuleInfo(id)?.isEntry ? 'no-treeshake' : false, - meta: config.command === 'build' ? { 'vite:asset': true } : undefined, + meta: + config.command === 'build' || !!config.experimental.fullBundleMode + ? { + 'vite:asset': true, + } + : undefined, moduleType: 'js', // NOTE: needs to be set to avoid double `export default` in `.txt`s } }, @@ -287,7 +294,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { // do not emit assets for SSR build if ( - config.command === 'build' && + (config.command === 'build' || !!config.experimental.fullBundleMode) && !this.environment.config.build.emitAssets ) { for (const file in bundle) { @@ -309,7 +316,10 @@ export async function fileToUrl( id: string, ): Promise { const { environment } = pluginContext - if (environment.config.command === 'serve') { + if ( + environment.config.command === 'serve' && + !environment.config.experimental.fullBundleMode + ) { return fileToDevUrl(environment, id) } else { return fileToBuiltUrl(pluginContext, id) @@ -380,7 +390,7 @@ export function publicFileToBuiltUrl( url: string, config: ResolvedConfig, ): string { - if (config.command !== 'build') { + if (config.command !== 'build' && !config.experimental.fullBundleMode) { // We don't need relative base or renderBuiltUrl support during dev return joinUrlSegments(config.decodedBase, url) } diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index b488e4c1c9415b..b65741304afe4d 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -38,7 +38,9 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { ...config.resolve, root: config.root, isProduction: config.isProduction, - isBuild: config.command === 'build', + isBuild: config.experimental?.fullBundleMode + ? config.mode === 'production' + : config.command === 'build', packageCache: config.packageCache, asSrc: true, } diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index f59f7a77e9acee..93c784b2d28b98 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' @@ -15,8 +16,6 @@ const normalizedEnvEntry = normalizePath(ENV_ENTRY) * @server-only */ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { - let injectConfigValues: (code: string) => string - const getDefineReplacer = perEnvironmentState((environment) => { const userDefine: Record = {} for (const key in environment.config.define) { @@ -32,74 +31,12 @@ 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) - } - }, async transform(code, id, options) { // TODO: Remove options?.ssr, Vitest currently hijacks this plugin const ssr = options?.ssr ?? this.environment.config.consumer === 'server' if (id === normalizedClientEntry || id === normalizedEnvEntry) { const defineReplacer = getDefineReplacer(this) - return defineReplacer(injectConfigValues(code)) + return defineReplacer(await replaceClientConfigValues(code, config)) } else if (!ssr && code.includes('process.env.NODE_ENV')) { // replace process.env.NODE_ENV instead of defining a global // for it to avoid shimming a `process` object during dev, @@ -121,3 +58,80 @@ function escapeReplacement(value: string | number | boolean | null) { const jsonValue = JSON.stringify(value) return () => jsonValue } + +async function replaceClientConfigValues( + code: string, + config: ResolvedConfig, +): Promise { + 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 + .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 getHmrImplement(config: ResolvedConfig): Promise { + const content = fs.readFileSync(normalizedClientEntry, 'utf-8') + return ( + (await replaceClientConfigValues(content, config)) + // the rolldown runtime shouldn't be importer a module + .replace(`import '@vite/env'`, '') + ) +} diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index a1bc24e90bd0a5..be7354309fa3fd 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -286,7 +286,7 @@ const postcssConfigCache = new WeakMap< >() function encodePublicUrlsInCSS(config: ResolvedConfig) { - return config.command === 'build' + return config.command === 'build' || config.experimental.fullBundleMode } const cssUrlAssetRE = /__VITE_CSS_URL__([\da-f]+)__/g @@ -295,7 +295,9 @@ const cssUrlAssetRE = /__VITE_CSS_URL__([\da-f]+)__/g * Plugin applied before user plugins */ export function cssPlugin(config: ResolvedConfig): Plugin { - const isBuild = config.command === 'build' + const isBuild = config.experimental.fullBundleMode + ? config.mode === 'production' + : config.command === 'build' let moduleCache: Map> const idResolver = createBackCompatIdResolver(config, { @@ -402,7 +404,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { if (fragment) resolved += '#' + fragment return [await fileToUrl(this, resolved), resolved] } - if (config.command === 'build') { + if (config.command === 'build' || config.experimental.fullBundleMode) { const isExternal = config.build.rollupOptions.external ? resolveUserExternal( config.build.rollupOptions.external, @@ -577,7 +579,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { !inlined && dataToEsm(modules, { namedExports: true, preferConst: true }) - if (config.command === 'serve') { + // TODO(underfin): full bundle mode css hmr + if (config.command === 'serve' && !config.experimental.fullBundleMode) { const getContentWithSourcemap = async (content: string) => { if (config.css.devSourcemap) { const sourcemap = this.getCombinedSourcemap() @@ -3514,7 +3517,7 @@ async function compileLightningCSS( }, minify: config.isProduction && !!config.build.cssMinify, sourceMap: - config.command === 'build' + config.command === 'build' || config.experimental.fullBundleMode ? !!config.build.sourcemap : config.css.devSourcemap, analyzeDependencies: true, diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index a3ea472eab094f..64b99a0c6fc99b 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -12,7 +12,8 @@ const importMetaEnvMarker = '__vite_import_meta_env__' const importMetaEnvKeyReCache = new Map() export function definePlugin(config: ResolvedConfig): Plugin { - const isBuild = config.command === 'build' + const isBuild = + config.command === 'build' || !!config.experimental.fullBundleMode const isBuildLib = isBuild && config.build.lib // ignore replace process.env in lib build @@ -34,7 +35,10 @@ export function definePlugin(config: ResolvedConfig): Plugin { const importMetaEnvKeys: Record = {} const importMetaFallbackKeys: Record = {} if (isBuild) { - importMetaKeys['import.meta.hot'] = `undefined` + // import.meta.hot need to avoid replace undefined at full bundle mode + if (!config.experimental.fullBundleMode) { + importMetaKeys['import.meta.hot'] = `undefined` + } for (const key in config.env) { const val = JSON.stringify(config.env[key]) importMetaKeys[`import.meta.env.${key}`] = val @@ -205,7 +209,8 @@ export async function replaceDefine( sourceType: 'module', define, sourcemap: - environment.config.command === 'build' + environment.config.command === 'build' || + environment.config.experimental.fullBundleMode ? !!environment.config.build.sourcemap : true, }) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index ae1af8e3034a76..7ceb1a2035fde9 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -53,7 +53,8 @@ export async function resolvePlugins( normalPlugins: Plugin[], postPlugins: Plugin[], ): Promise { - const isBuild = config.command === 'build' + const isBuild = + config.command === 'build' || !!config.experimental.fullBundleMode const isWorker = config.isWorker const buildPlugins = isBuild ? await (await import('../build')).resolveBuildPlugins(config) @@ -87,10 +88,7 @@ export async function resolvePlugins( ? perEnvironmentPlugin( 'native:modulepreload-polyfill', (environment) => { - if ( - config.command !== 'build' || - environment.config.consumer !== 'client' - ) + if (!isBuild || environment.config.consumer !== 'client') return false return nativeModulePreloadPolyfillPlugin() }, @@ -190,7 +188,7 @@ export async function resolvePlugins( ...buildPlugins.post, - // internal server-only plugins are always applied after everything else + // // internal server-only plugins are always applied after everything else ...(isBuild ? [] : [ diff --git a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts index d0558fd44f6ff4..9477953480cc5e 100644 --- a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts +++ b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts @@ -22,7 +22,7 @@ export function modulePreloadPolyfillPlugin(config: ResolvedConfig): Plugin { handler(_id) { // `isModernFlag` is only available during build since it is resolved by `vite:build-import-analysis` if ( - config.command !== 'build' || + (config.command !== 'build' && !config.experimental.fullBundleMode) || this.environment.config.consumer !== 'client' ) { return '' diff --git a/packages/vite/src/node/plugins/oxc.ts b/packages/vite/src/node/plugins/oxc.ts index e5ee70155d90e5..f3bb0c2d47b9f5 100644 --- a/packages/vite/src/node/plugins/oxc.ts +++ b/packages/vite/src/node/plugins/oxc.ts @@ -487,6 +487,7 @@ export function resolveOxcTranspileOptions( sourceType: format === 'es' ? 'module' : 'script', target: target || undefined, sourcemap: !!config.build.sourcemap, + jsx: undefined, } } diff --git a/packages/vite/src/node/plugins/reporter.ts b/packages/vite/src/node/plugins/reporter.ts index ed74b45c570476..0a6b4a1fb40805 100644 --- a/packages/vite/src/node/plugins/reporter.ts +++ b/packages/vite/src/node/plugins/reporter.ts @@ -292,41 +292,45 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { : {}), renderStart() { - chunksReporter(this).reset() + if (config.command === 'build' && !config.experimental.fullBundleMode) { + chunksReporter(this).reset() + } }, renderChunk(_, chunk, options) { - if (!options.inlineDynamicImports) { - for (const id of chunk.moduleIds) { - const module = this.getModuleInfo(id) - if (!module) continue - // When a dynamic importer shares a chunk with the imported module, - // warn that the dynamic imported module will not be moved to another chunk (#12850). - if (module.importers.length && module.dynamicImporters.length) { - // Filter out the intersection of dynamic importers and sibling modules in - // the same chunk. The intersecting dynamic importers' dynamic import is not - // expected to work. Note we're only detecting the direct ineffective - // dynamic import here. - const detectedIneffectiveDynamicImport = - module.dynamicImporters.some( - (id) => !isInNodeModules(id) && chunk.moduleIds.includes(id), - ) - if (detectedIneffectiveDynamicImport) { - this.warn( - `\n(!) ${ - module.id - } is dynamically imported by ${module.dynamicImporters.join( - ', ', - )} but also statically imported by ${module.importers.join( - ', ', - )}, dynamic import will not move module into another chunk.\n`, - ) + if (config.command === 'build' && !config.experimental.fullBundleMode) { + if (!options.inlineDynamicImports) { + for (const id of chunk.moduleIds) { + const module = this.getModuleInfo(id) + if (!module) continue + // When a dynamic importer shares a chunk with the imported module, + // warn that the dynamic imported module will not be moved to another chunk (#12850). + if (module.importers.length && module.dynamicImporters.length) { + // Filter out the intersection of dynamic importers and sibling modules in + // the same chunk. The intersecting dynamic importers' dynamic import is not + // expected to work. Note we're only detecting the direct ineffective + // dynamic import here. + const detectedIneffectiveDynamicImport = + module.dynamicImporters.some( + (id) => !isInNodeModules(id) && chunk.moduleIds.includes(id), + ) + if (detectedIneffectiveDynamicImport) { + this.warn( + `\n(!) ${ + module.id + } is dynamically imported by ${module.dynamicImporters.join( + ', ', + )} but also statically imported by ${module.importers.join( + ', ', + )}, dynamic import will not move module into another chunk.\n`, + ) + } } } } - } - chunksReporter(this).register() + chunksReporter(this).register() + } }, generateBundle() { @@ -334,7 +338,9 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { }, async writeBundle({ dir }, output) { - await chunksReporter(this).log(output, dir) + if (config.command === 'build' && !config.experimental.fullBundleMode) { + await chunksReporter(this).log(output, dir) + } }, } } diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 3c3a5c54a810e2..3ece6891ee5ddb 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -285,7 +285,8 @@ export function webWorkerPostPlugin(): Plugin { } export function webWorkerPlugin(config: ResolvedConfig): Plugin { - const isBuild = config.command === 'build' + const isBuild = + config.command === 'build' || config.experimental.fullBundleMode const isWorker = config.isWorker return { diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 742fb2e27b11c8..d9e34a9882f1e8 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -184,14 +184,15 @@ const workerImportMetaUrlRE = /new\s+(?:Worker|SharedWorker).+new\s+URL.+import\.meta\.url/s export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { - const isBuild = config.command === 'build' + const isBuild = + config.command === 'build' || !!config.experimental.fullBundleMode let workerResolver: ResolveIdFn const fsResolveOptions: InternalResolveOptions = { ...config.resolve, root: config.root, isProduction: config.isProduction, - isBuild: config.command === 'build', + isBuild, packageCache: config.packageCache, asSrc: true, } diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 184d96bc53c7b4..e774c94c718283 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -246,7 +246,9 @@ export async function preview( // html fallback if (config.appType === 'spa' || config.appType === 'mpa') { - app.use(htmlFallbackMiddleware(distDir, config.appType === 'spa')) + app.use( + htmlFallbackMiddleware(distDir, config.appType === 'spa', false, {}), + ) } // apply post server hooks from plugins diff --git a/packages/vite/src/node/server/buildModuleGraph.ts b/packages/vite/src/node/server/buildModuleGraph.ts new file mode 100644 index 00000000000000..82af0de6053911 --- /dev/null +++ b/packages/vite/src/node/server/buildModuleGraph.ts @@ -0,0 +1,252 @@ +import type { ModuleInfo } from 'rolldown' +import type { ViteDevServer } from '..' +import type { Plugin } from '../plugin' +import { cleanUrl } from '../../shared/utils' +import type { TransformResult } from './transformRequest' +import type { EnvironmentModuleNode, ResolvedUrl } from './moduleGraph' + +export class BuildModuleNode { + _id: string + _file: string + _importers = new Set() + constructor(id: string, file: string) { + this._id = id + this._file = file + } + + // TODO using id also could be work + get url(): string { + return this._id + } + set url(_value: string) { + throw new Error('BuildModuleNode set url is not support') + } + + get id(): string | null { + return this._id + } + set id(_value: string | null) { + throw new Error('BuildModuleNode set id is not support') + } + get file(): string | null { + return this._file + } + set file(_value: string | null) { + throw new Error('BuildModuleNode set file is not support') + } + // eslint-disable-next-line @typescript-eslint/class-literal-property-style + get type(): 'js' | 'css' { + return 'js' + } + // `info` needs special care as it's defined as a proxy in `pluginContainer`, + // so we also merge it as a proxy too + get info(): ModuleInfo | undefined { + throw new Error('BuildModuleNode get info is not support') + } + get meta(): Record | undefined { + throw new Error('BuildModuleNode get meta is not support') + } + get importers(): Set { + throw this._importers + } + set importers(importers: Set) { + this._importers = importers + } + get clientImportedModules(): Set { + throw new Error('BuildModuleNode get clientImportedModules is not support') + } + get ssrImportedModules(): Set { + throw new Error('BuildModuleNode get ssrImportedModules is not support') + } + get importedModules(): Set { + throw new Error('BuildModuleNode get importedModules is not support') + } + get acceptedHmrDeps(): Set { + throw new Error('BuildModuleNode get acceptedHmrDeps is not support') + } + get acceptedHmrExports(): Set | null { + throw new Error('BuildModuleNode get acceptedHmrExports is not support') + } + get importedBindings(): Map> | null { + throw new Error('BuildModuleNode get importedBindings is not support') + } + get isSelfAccepting(): boolean | undefined { + throw new Error('BuildModuleNode get isSelfAccepting is not support') + } + get transformResult(): TransformResult | null { + throw new Error('BuildModuleNode get transformResult is not support') + } + set transformResult(_value: TransformResult | null) { + throw new Error('BuildModuleNode set transformResult is not support') + } + get ssrTransformResult(): TransformResult | null { + throw new Error('BuildModuleNode get ssrTransformResult is not support') + } + set ssrTransformResult(_value: TransformResult | null) { + throw new Error('BuildModuleNode set ssrTransformResult is not support') + } + get ssrModule(): Record | null { + throw new Error('BuildModuleNode get ssrModule is not support') + } + get ssrError(): Error | null { + throw new Error('BuildModuleNode get ssrError is not support') + } + get lastHMRTimestamp(): number { + throw new Error('BuildModuleNode get lastHMRTimestamp is not support') + } + set lastHMRTimestamp(_value: number) { + throw new Error('BuildModuleNode set lastHMRTimestamp is not support') + } + get lastInvalidationTimestamp(): number { + throw new Error( + 'BuildModuleNode get lastInvalidationTimestamp is not support', + ) + } + get invalidationState(): TransformResult | 'HARD_INVALIDATED' | undefined { + throw new Error('BuildModuleNode get invalidationState is not support') + } + get ssrInvalidationState(): TransformResult | 'HARD_INVALIDATED' | undefined { + throw new Error('BuildModuleNode get ssrInvalidationState is not support') + } +} + +export class BuildModuleGraph { + idToModuleMap = new Map() + fileToModulesMap = new Map>() + + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor() {} + + getModuleById(id: string): BuildModuleNode | undefined { + return this.idToModuleMap.get(id) + } + + async getModuleByUrl( + _url: string, + _ssr?: boolean, + ): Promise { + throw new Error('BuildModuleGraph getModuleByUrl is not support') + } + + getModulesByFile(file: string): Set | undefined { + return this.fileToModulesMap.get(file) + } + + onFileChange(_file: string): void { + throw new Error('BuildModuleGraph onFileChange is not support') + } + + onFileDelete(_file: string): void { + throw new Error('BuildModuleGraph onFileDelete is not support') + } + + invalidateModule( + _mod: BuildModuleNode, + _seen = new Set(), + _timestamp: number = Date.now(), + _isHmr: boolean = false, + /** @internal */ + _softInvalidate = false, + ): void { + throw new Error('BuildModuleGraph invalidateModule is not support') + } + + invalidateAll(): void { + throw new Error('BuildModuleGraph invalidateAll is not support') + } + + async ensureEntryFromUrl( + _rawUrl: string, + _ssr?: boolean, + _setIsSelfAccepting = true, + ): Promise { + throw new Error('BuildModuleGraph invalidateAll is not support') + } + + createFileOnlyEntry(_file: string): BuildModuleNode { + throw new Error('BuildModuleGraph createFileOnlyEntry is not support') + } + + async resolveUrl(_url: string, _ssr?: boolean): Promise { + throw new Error('BuildModuleGraph resolveUrl is not support') + } + + updateModuleTransformResult( + _mod: BuildModuleNode, + _result: TransformResult | null, + _ssr?: boolean, + ): void { + throw new Error( + 'BuildModuleGraph updateModuleTransformResult is not support', + ) + } + + getModuleByEtag(_etag: string): BuildModuleNode | undefined { + throw new Error('BuildModuleGraph getModuleByEtag is not support') + } + + getBackwardCompatibleBrowserModuleNode( + _clientModule: EnvironmentModuleNode, + ): BuildModuleNode { + throw new Error( + 'BuildModuleGraph getBackwardCompatibleBrowserModuleNode is not support', + ) + } + + getBackwardCompatibleServerModuleNode( + _ssrModule: EnvironmentModuleNode, + ): BuildModuleNode { + throw new Error( + 'BuildModuleGraph getBackwardCompatibleServerModuleNode is not support', + ) + } + + getBackwardCompatibleModuleNode( + _mod: EnvironmentModuleNode, + ): BuildModuleNode { + throw new Error( + 'BuildModuleGraph getBackwardCompatibleModuleNode is not support', + ) + } + + getBackwardCompatibleModuleNodeDual( + _clientModule?: EnvironmentModuleNode, + _ssrModule?: EnvironmentModuleNode, + ): BuildModuleNode { + throw new Error( + 'BuildModuleGraph getBackwardCompatibleModuleNodeDual is not support', + ) + } +} + +export function buildModuleGraphPlugin(server: ViteDevServer): Plugin { + return { + name: 'build-module-graph-plugin', + renderStart() { + const moduleGraph = server.moduleGraph as BuildModuleGraph + for (const id of this.getModuleIds()) { + // const moduleInfo = this.getModuleInfo(id)! + const file = cleanUrl(id) + const moduleNode = new BuildModuleNode(id, file) + moduleGraph.idToModuleMap.set(id, moduleNode) + + moduleGraph.fileToModulesMap.set(file, new Set([moduleNode])) + let fileMappedModules = moduleGraph.fileToModulesMap.get(file) + if (!fileMappedModules) { + fileMappedModules = new Set() + moduleGraph.fileToModulesMap.set(file, fileMappedModules) + } + fileMappedModules.add(moduleNode) + } + for (const id of this.getModuleIds()) { + const moduleInfo = this.getModuleInfo(id)! + const moduleNode = moduleGraph.idToModuleMap.get(id) + moduleNode!.importers = new Set( + moduleInfo.importers.map( + (importerId) => moduleGraph.idToModuleMap.get(importerId)!, + ), + ) + } + }, + } +} diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 3ce3311e60a501..9c075d8900402b 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -134,19 +134,22 @@ export class DevEnvironment extends BaseEnvironment { }, }) - this.hot.on( - 'vite:invalidate', - async ({ path, message, firstInvalidatedBy }) => { - invalidateModule(this, { - path, - message, - firstInvalidatedBy, - }) - }, - ) + const { optimizeDeps, experimental } = this.config + + if (!experimental.fullBundleMode) { + this.hot.on( + 'vite:invalidate', + async ({ path, message, firstInvalidatedBy }) => { + invalidateModule(this, { + path, + message, + firstInvalidatedBy, + }) + }, + ) + } - const { optimizeDeps } = this.config - if (context.depsOptimizer) { + if (context.depsOptimizer && !experimental.fullBundleMode) { this.depsOptimizer = context.depsOptimizer } else if (isDepOptimizationDisabled(optimizeDeps)) { this.depsOptimizer = undefined @@ -188,8 +191,10 @@ export class DevEnvironment extends BaseEnvironment { */ async listen(server: ViteDevServer): Promise { this.hot.listen() - await this.depsOptimizer?.init() - warmupFiles(server, this) + if (!this.config.experimental.fullBundleMode) { + await this.depsOptimizer?.init() + warmupFiles(server, this) + } } fetchModule( diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 4f3154ad615547..03e2232b65461b 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -27,6 +27,7 @@ import type { EnvironmentModuleNode } from './moduleGraph' import type { ModuleNode } from './mixedModuleGraph' import type { DevEnvironment } from './environment' import { prepareError } from './middlewares/error' +import type { BuildModuleNode } from './buildModuleGraph' import type { HttpServer } from '.' import { restartServerWithUrls } from '.' @@ -64,7 +65,7 @@ export interface HotUpdateOptions { export interface HmrContext { file: string timestamp: number - modules: Array + modules: Array read: () => string | Promise server: ViteDevServer } @@ -453,7 +454,9 @@ export async function handleHMRUpdate( hotMap.set(environment, { options }) } - const mixedMods = new Set(mixedModuleGraph.getModulesByFile(file)) + const mixedMods = new Set( + mixedModuleGraph.getModulesByFile(file), + ) const mixedHmrContext: HmrContext = { ...contextMeta, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 63c338e4f9039f..d86f591eed47d4 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -99,7 +99,9 @@ import { transformRequest } from './transformRequest' import { searchForPackageRoot, searchForWorkspaceRoot } from './searchRoot' import type { DevEnvironment } from './environment' import { hostCheckMiddleware } from './middlewares/hostCheck' +import { memoryFilesMiddleware } from './middlewares/memoryFiles' import { rejectInvalidRequestMiddleware } from './middlewares/rejectInvalidRequest' +import type { BuildModuleGraph } from './buildModuleGraph' export interface ServerOptions extends CommonServerOptions { /** @@ -294,7 +296,7 @@ export interface ViteDevServer { * Module graph that tracks the import relationships, url to file mapping * and hmr state. */ - moduleGraph: ModuleGraph + moduleGraph: ModuleGraph | BuildModuleGraph /** * The resolved urls Vite prints on the CLI (URL-encoded). Returns `null` * in middleware mode or if the server is not listening on any port. @@ -419,6 +421,10 @@ export interface ViteDevServer { * @internal */ _ssrCompatModuleRunner?: ModuleRunner + /** + * @internal + */ + memoryFiles: Record } export interface ResolvedServerUrls { @@ -426,21 +432,26 @@ export interface ResolvedServerUrls { network: string[] } -export function createServer( +export async function createServer( inlineConfig: InlineConfig = {}, ): Promise { - return _createServer(inlineConfig, { listen: true }) + const config = await resolveConfig(inlineConfig, 'serve') + return _createServer(config, { listen: true }) +} + +export function createServerWithResolvedConfig( + config: ResolvedConfig, +): Promise { + return _createServer(config, { listen: true }) } export async function _createServer( - inlineConfig: InlineConfig = {}, + config: ResolvedConfig, options: { listen: boolean previousEnvironments?: Record }, ): Promise { - const config = await resolveConfig(inlineConfig, 'serve') - const initPublicFilesPromise = initPublicFiles(config) const { root, server: serverConfig } = config @@ -465,6 +476,7 @@ export async function _createServer( resolvedOutDirs, emptyOutDir, config.cacheDir, + !!config.experimental.fullBundleMode, ) const middlewares = connect() as Connect.Server @@ -520,10 +532,14 @@ export async function _createServer( // Backward compatibility - let moduleGraph = new ModuleGraph({ - client: () => environments.client.moduleGraph, - ssr: () => environments.ssr.moduleGraph, - }) + let moduleGraph = + // config.experimental.fullBundleMode + // ? new BuildModuleGraph() + // : + new ModuleGraph({ + client: () => environments.client.moduleGraph, + ssr: () => environments.ssr.moduleGraph, + }) const pluginContainer = createPluginContainer(environments) const closeHttpServer = createServerCloseFn(httpServer) @@ -553,6 +569,7 @@ export async function _createServer( } let server: ViteDevServer = { + memoryFiles: {}, config, middlewares, httpServer, @@ -785,6 +802,7 @@ export async function _createServer( } } + // TODO(underfin): handle this const onFileAddUnlink = async (file: string, isUnlink: boolean) => { file = normalizePath(file) reloadOnTsconfigChange(server, file) @@ -821,8 +839,10 @@ export async function _createServer( watcher.on('change', async (file) => { file = normalizePath(file) + // TODO(underfin): handle ts config.json change at full bundle mode reloadOnTsconfigChange(server, file) + // TODO(underfin): watchChange hooks how to migrate at full bundle mode await pluginContainer.watchChange(file, { event: 'update' }) // invalidate module graph cache on file change for (const environment of Object.values(server.environments)) { @@ -874,7 +894,9 @@ export async function _createServer( middlewares.use(hostCheckMiddleware(config, false)) } - middlewares.use(cachedTransformMiddleware(server)) + if (!config.experimental.fullBundleMode) { + middlewares.use(cachedTransformMiddleware(server)) + } // proxy const { proxy } = serverConfig @@ -909,16 +931,28 @@ export async function _createServer( middlewares.use(servePublicMiddleware(server, publicFiles)) } - // main transform middleware - middlewares.use(transformMiddleware(server)) + if (config.experimental.fullBundleMode) { + // serve memory output dist files + middlewares.use(memoryFilesMiddleware(server, false)) + } 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', + !!config.experimental.fullBundleMode, + server.memoryFiles, + ), + ) } // run post config hooks @@ -927,8 +961,12 @@ export async function _createServer( postHooks.forEach((fn) => fn && fn()) if (config.appType === 'spa' || config.appType === 'mpa') { - // transform index.html - middlewares.use(indexHtmlMiddleware(root, server)) + if (config.experimental.fullBundleMode) { + middlewares.use(memoryFilesMiddleware(server, true)) + } else { + // transform index.html + middlewares.use(indexHtmlMiddleware(root, server)) + } // handle 404s middlewares.use(notFoundMiddleware()) @@ -950,7 +988,9 @@ export async function _createServer( // 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) { + await environments.client.pluginContainer.buildStart() + } // ensure ws server started if (onListen || options.listen) { @@ -1190,7 +1230,8 @@ async function restartServer(server: ViteDevServer) { let newServer: ViteDevServer | null = null try { // delay ws server listen - newServer = await _createServer(inlineConfig, { + const config = await resolveConfig(inlineConfig, 'serve') + newServer = await _createServer(config, { listen: false, previousEnvironments: server.environments, }) diff --git a/packages/vite/src/node/server/middlewares/htmlFallback.ts b/packages/vite/src/node/server/middlewares/htmlFallback.ts index b61b44bf061180..9796f06d4f51b9 100644 --- a/packages/vite/src/node/server/middlewares/htmlFallback.ts +++ b/packages/vite/src/node/server/middlewares/htmlFallback.ts @@ -9,6 +9,8 @@ const debug = createDebugger('vite:html-fallback') export function htmlFallbackMiddleware( root: string, spaFallback: boolean, + fullBundleMode: boolean, + memoryFiles: Record, ): Connect.NextHandleFunction { // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return function viteHtmlFallbackMiddleware(req, _res, next) { @@ -31,6 +33,12 @@ export function htmlFallbackMiddleware( const url = cleanUrl(req.url!) const pathname = decodeURIComponent(url) + function checkFileExists(htmlPathName: string) { + return fullBundleMode + ? memoryFiles[htmlPathName] + : fs.existsSync(path.join(root, htmlPathName)) + } + // .html files are not handled by serveStaticMiddleware // so we need to check if the file exists if (pathname.endsWith('.html')) { @@ -43,8 +51,7 @@ export function htmlFallbackMiddleware( } // trailing slash should check for fallback index.html else if (pathname.endsWith('/')) { - const filePath = path.join(root, pathname, 'index.html') - if (fs.existsSync(filePath)) { + if (checkFileExists(path.join(pathname, 'index.html'))) { const newUrl = url + 'index.html' debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`) req.url = newUrl @@ -53,8 +60,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/memoryFiles.ts b/packages/vite/src/node/server/middlewares/memoryFiles.ts new file mode 100644 index 00000000000000..5c7cf2680cdb70 --- /dev/null +++ b/packages/vite/src/node/server/middlewares/memoryFiles.ts @@ -0,0 +1,37 @@ +import type { Connect } from 'dep-types/connect' +import * as mrmime from 'mrmime' +import { cleanUrl } from '../../../shared/utils' +import type { ViteDevServer } from '..' + +export function memoryFilesMiddleware( + server: ViteDevServer, + handleHtml: boolean, +): Connect.NextHandleFunction { + return function viteMemoryFilesMiddleware(req, res, next) { + const cleanedUrl = cleanUrl(req.url!).slice(1) // remove first / + if (cleanedUrl.endsWith('.html') && !handleHtml) { + return next() + } + const file = server.memoryFiles[cleanedUrl] + if (file) { + if (cleanedUrl.endsWith('.js')) { + res.setHeader('Content-Type', 'text/javascript') + } else { + const mime = mrmime.lookup(cleanedUrl) + if (mime) { + res.setHeader('Content-Type', mime) + } + } + + const headers = server.config.server.headers + if (headers) { + for (const name in headers) { + res.setHeader(name, headers[name]!) + } + } + + return res.end(file) + } + next() + } +} diff --git a/packages/vite/src/node/watch.ts b/packages/vite/src/node/watch.ts index 63f61db51da254..b3ba7cef736bd4 100644 --- a/packages/vite/src/node/watch.ts +++ b/packages/vite/src/node/watch.ts @@ -53,6 +53,7 @@ export function resolveChokidarOptions( resolvedOutDirs: Set, emptyOutDir: boolean, cacheDir: string, + fullBundleMode: boolean, ): WatchOptions { const { ignored: ignoredList, ...otherOptions } = options ?? {} const ignored: WatchOptions['ignored'] = [ @@ -62,7 +63,8 @@ export function resolveChokidarOptions( escapePath(cacheDir) + '/**', ...arraify(ignoredList || []), ] - if (emptyOutDir) { + // TODO(underfin): revert it if the dev build only write output to memory at full bundle mode. + if (emptyOutDir || fullBundleMode) { ignored.push( ...[...resolvedOutDirs].map((outDir) => escapePath(outDir) + '/**'), ) diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index 85b0916c5f4f5f..5a054124e31a4a 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -27,6 +27,7 @@ export class HMRContext implements ViteHotContext { constructor( private hmrClient: HMRClient, private ownerPath: string, + private fullBundleMode: boolean, ) { if (!hmrClient.dataMap.has(ownerPath)) { hmrClient.dataMap.set(ownerPath, {}) @@ -99,18 +100,21 @@ export class HMRContext implements ViteHotContext { invalidate(message: string): void { const firstInvalidatedBy = this.hmrClient.currentFirstInvalidatedBy ?? this.ownerPath + const ownerPath = this.fullBundleMode + ? `/${this.ownerPath}` + : this.ownerPath this.hmrClient.notifyListeners('vite:invalidate', { - path: this.ownerPath, + path: ownerPath, message, firstInvalidatedBy, }) this.send('vite:invalidate', { - path: this.ownerPath, + path: ownerPath, message, firstInvalidatedBy, }) this.hmrClient.logger.debug( - `invalidate ${this.ownerPath}${message ? `: ${message}` : ''}`, + `invalidate ${ownerPath}${message ? `: ${message}` : ''}`, ) } @@ -151,6 +155,15 @@ export class HMRContext implements ViteHotContext { this.hmrClient.send({ type: 'custom', event, data }) } + async getExports(url: string): Promise { + return this.fullBundleMode + ? Promise.resolve().then(() => + // @ts-expect-error __rolldown_runtime__ + __rolldown_runtime__.loadExports(this.ownerPath), + ) + : import(url) + } + private acceptDeps( deps: string[], callback: HotCallback['fn'] = () => {}, @@ -181,6 +194,7 @@ export class HMRClient { private transport: NormalizedModuleRunnerTransport, // This allows implementing reloading via different methods depending on the environment private importUpdatedModule: (update: Update) => Promise, + private fullBundleMode: boolean, ) {} public async notifyListeners( @@ -296,7 +310,13 @@ export class HMRClient { ), ) } - const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}` + const loggedPath = this.fullBundleMode + ? isSelfUpdate + ? `/${path}` + : `/${acceptedPath} via /${path}` + : isSelfUpdate + ? path + : `${acceptedPath} via ${path}` this.logger.debug(`hot updated: ${loggedPath}`) } finally { this.currentFirstInvalidatedBy = undefined diff --git a/packages/vite/types/hmrPayload.d.ts b/packages/vite/types/hmrPayload.d.ts index 0cbd649f7279da..36aac97895df42 100644 --- a/packages/vite/types/hmrPayload.d.ts +++ b/packages/vite/types/hmrPayload.d.ts @@ -24,6 +24,7 @@ export interface UpdatePayload { export interface Update { type: 'js-update' | 'css-update' + url?: string // the hmr chunk url, it only exists for full bundle mode path: string acceptedPath: string timestamp: number diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index 19d4257fa17b26..88c9b06e5bac57 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -217,30 +217,35 @@ if (!isBuild) { ) }) - test('soft invalidate', async () => { - const el = await page.$('.soft-invalidation') - expect(await el.textContent()).toBe( - 'soft-invalidation/index.js is transformed 1 times. child is bar', - ) - editFile('soft-invalidation/child.js', (code) => - code.replace('bar', 'updated'), - ) - await untilUpdated( - () => el.textContent(), - 'soft-invalidation/index.js is transformed 1 times. child is updated', - ) + // The file will be transformed twice at rolldown hmr and rebuild. + // It is a performance improvement at vite, it should be ignored at rolldown-vite full bundle mode. + // Other, not sure why the times is 10, but using the client to check the times is 2. + if (!process.env.VITE_TEST_FULL_BUNDLE_MODE) { + test('soft invalidate', async () => { + const el = await page.$('.soft-invalidation') + expect(await el.textContent()).toBe( + 'soft-invalidation/index.js is transformed 1 times. child is bar', + ) + editFile('soft-invalidation/child.js', (code) => + code.replace('bar', 'updated'), + ) + await untilUpdated( + () => el.textContent(), + 'soft-invalidation/index.js is transformed 1 times. child is updated', + ) - editFile('soft-invalidation/index.js', (code) => - code.replace('child is', 'child is now'), - ) - editFile('soft-invalidation/child.js', (code) => - code.replace('updated', 'updated?'), - ) - await untilUpdated( - () => el.textContent(), - 'soft-invalidation/index.js is transformed 2 times. child is now updated?', - ) - }) + editFile('soft-invalidation/index.js', (code) => + code.replace('child is', 'child is now'), + ) + editFile('soft-invalidation/child.js', (code) => + code.replace('updated', 'updated?'), + ) + await untilUpdated( + () => el.textContent(), + 'soft-invalidation/index.js is transformed 2 times. child is now updated?', + ) + }) + } test('invalidate in circular dep should not trigger infinite HMR', async () => { const el = await page.$('.invalidation-circular-deps') @@ -272,6 +277,9 @@ if (!isBuild) { await untilUpdated(() => el.textContent(), 'edited') }) + // TODO(underfin): the customFile tests maybe make test hang. + // The `customFile.js` is not inside module graph, it couldn't be seen also hasn't hmr. + // Here only trigger hmr events. test('plugin hmr remove custom events', async () => { const el = await page.$('.toRemove') editFile('customFile.js', (code) => code.replace('custom', 'edited')) @@ -285,85 +293,96 @@ if (!isBuild) { await untilUpdated(() => el.textContent(), '3') }) - test('full-reload encodeURI path', async () => { - await page.goto( - viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', - ) - const el = await page.$('#app') - expect(await el.textContent()).toBe('title') - editFile('unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', (code) => - code.replace('title', 'title2'), - ) - await page.waitForEvent('load') - await untilUpdated( - async () => (await page.$('#app')).textContent(), - 'title2', - ) - }) + // BreakChange: The html is not a entry, it couldn't be seen also hasn't hmr. + if (!process.env.VITE_TEST_FULL_BUNDLE_MODE) { + test('full-reload encodeURI path', async () => { + await page.goto( + viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', + ) + const el = await page.$('#app') + expect(await el.textContent()).toBe('title') + editFile('unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', (code) => + code.replace('title', 'title2'), + ) + await page.waitForEvent('load') + await untilUpdated( + async () => (await page.$('#app')).textContent(), + 'title2', + ) + }) + } + + // TODO css hmr + if (!process.env.VITE_TEST_FULL_BUNDLE_MODE) { + test('CSS update preserves query params', async () => { + await page.goto(viteTestUrl) + + editFile('global.css', (code) => code.replace('white', 'tomato')) + + const elprev = await page.$('.css-prev') + const elpost = await page.$('.css-post') + await untilUpdated(() => elprev.textContent(), 'param=required') + await untilUpdated(() => elpost.textContent(), 'param=required') + const textprev = await elprev.textContent() + const textpost = await elpost.textContent() + expect(textprev).not.toBe(textpost) + expect(textprev).not.toMatch('direct') + expect(textpost).not.toMatch('direct') + }) - test('CSS update preserves query params', async () => { - await page.goto(viteTestUrl) + test('it swaps out link tags', async () => { + await page.goto(viteTestUrl) - editFile('global.css', (code) => code.replace('white', 'tomato')) - - const elprev = await page.$('.css-prev') - const elpost = await page.$('.css-post') - await untilUpdated(() => elprev.textContent(), 'param=required') - await untilUpdated(() => elpost.textContent(), 'param=required') - const textprev = await elprev.textContent() - const textpost = await elpost.textContent() - expect(textprev).not.toBe(textpost) - expect(textprev).not.toMatch('direct') - expect(textpost).not.toMatch('direct') - }) + editFile('global.css', (code) => code.replace('white', 'tomato')) - test('it swaps out link tags', async () => { - await page.goto(viteTestUrl) - - editFile('global.css', (code) => code.replace('white', 'tomato')) + let el = await page.$('.link-tag-added') + await untilUpdated(() => el.textContent(), 'yes') - let el = await page.$('.link-tag-added') - await untilUpdated(() => el.textContent(), 'yes') + el = await page.$('.link-tag-removed') + await untilUpdated(() => el.textContent(), 'yes') - el = await page.$('.link-tag-removed') - await untilUpdated(() => el.textContent(), 'yes') + expect((await page.$$('link')).length).toBe(1) + }) + } - expect((await page.$$('link')).length).toBe(1) - }) + // TODO(underfin): recheck it + if (!process.env.VITE_TEST_FULL_BUNDLE_MODE) { + test('not loaded dynamic import', async () => { + await page.goto(viteTestUrl + '/counter/index.html', { + waitUntil: 'load', + }) - test('not loaded dynamic import', async () => { - await page.goto(viteTestUrl + '/counter/index.html', { waitUntil: 'load' }) - - let btn = await page.$('button') - expect(await btn.textContent()).toBe('Counter 0') - await btn.click() - expect(await btn.textContent()).toBe('Counter 1') - - // Modifying `index.ts` triggers a page reload, as expected - const indexTsLoadPromise = page.waitForEvent('load') - editFile('counter/index.ts', (code) => code) - await indexTsLoadPromise - btn = await page.$('button') - expect(await btn.textContent()).toBe('Counter 0') - - await btn.click() - expect(await btn.textContent()).toBe('Counter 1') - - // #7561 - // `dep.ts` defines `import.module.hot.accept` and has not been loaded. - // Therefore, modifying it has no effect (doesn't trigger a page reload). - // (Note that, a dynamic import that is never loaded and that does not - // define `accept.module.hot.accept` may wrongfully trigger a full page - // reload, see discussion at #7561.) - const depTsLoadPromise = page.waitForEvent('load', { timeout: 1000 }) - editFile('counter/dep.ts', (code) => code) - await expect(depTsLoadPromise).rejects.toThrow( - /page\.waitForEvent: Timeout \d+ms exceeded while waiting for event "load"/, - ) + let btn = await page.$('button') + expect(await btn.textContent()).toBe('Counter 0') + await btn.click() + expect(await btn.textContent()).toBe('Counter 1') + + // Modifying `index.ts` triggers a page reload, as expected + const indexTsLoadPromise = page.waitForEvent('load') + editFile('counter/index.ts', (code) => code) + await indexTsLoadPromise + btn = await page.$('button') + expect(await btn.textContent()).toBe('Counter 0') + + await btn.click() + expect(await btn.textContent()).toBe('Counter 1') + + // #7561 + // `dep.ts` defines `import.module.hot.accept` and has not been loaded. + // Therefore, modifying it has no effect (doesn't trigger a page reload). + // (Note that, a dynamic import that is never loaded and that does not + // define `accept.module.hot.accept` may wrongfully trigger a full page + // reload, see discussion at #7561.) + const depTsLoadPromise = page.waitForEvent('load', { timeout: 1000 }) + editFile('counter/dep.ts', (code) => code) + await expect(depTsLoadPromise).rejects.toThrow( + /page\.waitForEvent: Timeout \d+ms exceeded while waiting for event "load"/, + ) - btn = await page.$('button') - expect(await btn.textContent()).toBe('Counter 1') - }) + btn = await page.$('button') + expect(await btn.textContent()).toBe('Counter 1') + }) + } // #2255 test('importing reloaded', async () => { @@ -391,711 +410,725 @@ if (!isBuild) { ) }) - describe('acceptExports', () => { - const HOT_UPDATED = /hot updated/ - const CONNECTED = /connected/ - - const baseDir = 'accept-exports' - - describe('when all used exports are accepted', () => { - const testDir = baseDir + '/main-accepted' - - const fileName = 'target.ts' - const file = `${testDir}/${fileName}` - const url = '/' + file - - let dep = 'dep0' - - beforeAll(async () => { - await untilBrowserLogAfter( - () => page.goto(`${viteTestUrl}/${testDir}/`), - [CONNECTED, />>>>>>/], - (logs) => { - expect(logs).toContain(`<<<<<< A0 B0 D0 ; ${dep}`) - expect(logs).toContain('>>>>>> A0 D0') - }, - ) - }) - - it('the callback is called with the new version the module', async () => { - const callbackFile = `${testDir}/callback.ts` - const callbackUrl = '/' + callbackFile - - await untilBrowserLogAfter( - () => { - editFile(callbackFile, (code) => - code - .replace("x = 'X'", "x = 'Y'") - .replace('reloaded >>>', 'reloaded (2) >>>'), - ) - }, - HOT_UPDATED, - (logs) => { - expect(logs).toEqual([ - 'reloaded >>> Y', - `[vite] hot updated: ${callbackUrl}`, - ]) - }, - ) + if (!process.env.VITE_TEST_FULL_BUNDLE_MODE) { + describe('acceptExports', () => { + const HOT_UPDATED = /hot updated/ + const CONNECTED = /connected/ - await untilBrowserLogAfter( - () => { - editFile(callbackFile, (code) => code.replace("x = 'Y'", "x = 'Z'")) - }, - HOT_UPDATED, - (logs) => { - expect(logs).toEqual([ - 'reloaded (2) >>> Z', - `[vite] hot updated: ${callbackUrl}`, - ]) - }, - ) - }) + const baseDir = 'accept-exports' - it('stops HMR bubble on dependency change', async () => { - const depFileName = 'dep.ts' - const depFile = `${testDir}/${depFileName}` + describe('when all used exports are accepted', () => { + const testDir = baseDir + '/main-accepted' - await untilBrowserLogAfter( - () => { - editFile(depFile, (code) => code.replace('dep0', (dep = 'dep1'))) - }, - HOT_UPDATED, - (logs) => { - expect(logs).toEqual([ - `<<<<<< A0 B0 D0 ; ${dep}`, - `[vite] hot updated: ${url}`, - ]) - }, - ) - }) + const fileName = 'target.ts' + const file = `${testDir}/${fileName}` + const url = '/' + file - it('accepts itself and refreshes on change', async () => { - await untilBrowserLogAfter( - () => { - editFile(file, (code) => code.replace(/(\b[A-Z])0/g, '$11')) - }, - HOT_UPDATED, - (logs) => { - expect(logs).toEqual([ - `<<<<<< A1 B1 D1 ; ${dep}`, - `[vite] hot updated: ${url}`, - ]) - }, - ) - }) + let dep = 'dep0' - it('accepts itself and refreshes on 2nd change', async () => { - await untilBrowserLogAfter( - () => { - editFile(file, (code) => - code - .replace(/(\b[A-Z])1/g, '$12') - .replace( - "acceptExports(['a', 'default']", - "acceptExports(['b', 'default']", - ), - ) - }, - HOT_UPDATED, - (logs) => { - expect(logs).toEqual([ - `<<<<<< A2 B2 D2 ; ${dep}`, - `[vite] hot updated: ${url}`, - ]) - }, - ) - }) + beforeAll(async () => { + await untilBrowserLogAfter( + () => page.goto(`${viteTestUrl}/${testDir}/`), + [CONNECTED, />>>>>>/], + (logs) => { + expect(logs).toContain(`<<<<<< A0 B0 D0 ; ${dep}`) + expect(logs).toContain('>>>>>> A0 D0') + }, + ) + }) - it('does not accept itself anymore after acceptedExports change', async () => { - await untilBrowserLogAfter( - async () => { - editFile(file, (code) => code.replace(/(\b[A-Z])2/g, '$13')) - await page.waitForEvent('load') - }, - [CONNECTED, />>>>>>/], - (logs) => { - expect(logs).toContain(`<<<<<< A3 B3 D3 ; ${dep}`) - expect(logs).toContain('>>>>>> A3 D3') - }, - ) - }) - }) + it('the callback is called with the new version the module', async () => { + const callbackFile = `${testDir}/callback.ts` + const callbackUrl = '/' + callbackFile - describe('when some used exports are not accepted', () => { - const testDir = baseDir + '/main-non-accepted' + await untilBrowserLogAfter( + () => { + editFile(callbackFile, (code) => + code + .replace("x = 'X'", "x = 'Y'") + .replace('reloaded >>>', 'reloaded (2) >>>'), + ) + }, + HOT_UPDATED, + (logs) => { + expect(logs).toEqual([ + 'reloaded >>> Y', + `[vite] hot updated: ${callbackUrl}`, + ]) + }, + ) - const namedFileName = 'named.ts' - const namedFile = `${testDir}/${namedFileName}` - const defaultFileName = 'default.ts' - const defaultFile = `${testDir}/${defaultFileName}` - const depFileName = 'dep.ts' - const depFile = `${testDir}/${depFileName}` + await untilBrowserLogAfter( + () => { + editFile(callbackFile, (code) => + code.replace("x = 'Y'", "x = 'Z'"), + ) + }, + HOT_UPDATED, + (logs) => { + expect(logs).toEqual([ + 'reloaded (2) >>> Z', + `[vite] hot updated: ${callbackUrl}`, + ]) + }, + ) + }) - const a = 'A0' - let dep = 'dep0' + it('stops HMR bubble on dependency change', async () => { + const depFileName = 'dep.ts' + const depFile = `${testDir}/${depFileName}` - beforeAll(async () => { - await untilBrowserLogAfter( - () => page.goto(`${viteTestUrl}/${testDir}/`), - [CONNECTED, />>>>>>/], - (logs) => { - expect(logs).toContain(`<<< named: ${a} ; ${dep}`) - expect(logs).toContain(`<<< default: def0`) - expect(logs).toContain(`>>>>>> ${a} def0`) - }, - ) - }) + await untilBrowserLogAfter( + () => { + editFile(depFile, (code) => code.replace('dep0', (dep = 'dep1'))) + }, + HOT_UPDATED, + (logs) => { + expect(logs).toEqual([ + `<<<<<< A0 B0 D0 ; ${dep}`, + `[vite] hot updated: ${url}`, + ]) + }, + ) + }) - it('does not stop the HMR bubble on change to dep', async () => { - await untilBrowserLogAfter( - async () => { - editFile(depFile, (code) => code.replace('dep0', (dep = 'dep1'))) - await page.waitForEvent('load') - }, - [CONNECTED, />>>>>>/], - (logs) => { - expect(logs).toContain(`<<< named: ${a} ; ${dep}`) - }, - ) - }) + it('accepts itself and refreshes on change', async () => { + await untilBrowserLogAfter( + () => { + editFile(file, (code) => code.replace(/(\b[A-Z])0/g, '$11')) + }, + HOT_UPDATED, + (logs) => { + expect(logs).toEqual([ + `<<<<<< A1 B1 D1 ; ${dep}`, + `[vite] hot updated: ${url}`, + ]) + }, + ) + }) - describe('does not stop the HMR bubble on change to self', () => { - it('with named exports', async () => { + it('accepts itself and refreshes on 2nd change', async () => { await untilBrowserLogAfter( - async () => { - editFile(namedFile, (code) => code.replace(a, 'A1')) - await page.waitForEvent('load') + () => { + editFile(file, (code) => + code + .replace(/(\b[A-Z])1/g, '$12') + .replace( + "acceptExports(['a', 'default']", + "acceptExports(['b', 'default']", + ), + ) }, - [CONNECTED, />>>>>>/], + HOT_UPDATED, (logs) => { - expect(logs).toContain(`<<< named: A1 ; ${dep}`) + expect(logs).toEqual([ + `<<<<<< A2 B2 D2 ; ${dep}`, + `[vite] hot updated: ${url}`, + ]) }, ) }) - it('with default export', async () => { + it('does not accept itself anymore after acceptedExports change', async () => { await untilBrowserLogAfter( async () => { - editFile(defaultFile, (code) => code.replace('def0', 'def1')) + editFile(file, (code) => code.replace(/(\b[A-Z])2/g, '$13')) await page.waitForEvent('load') }, [CONNECTED, />>>>>>/], (logs) => { - expect(logs).toContain(`<<< default: def1`) + expect(logs).toContain(`<<<<<< A3 B3 D3 ; ${dep}`) + expect(logs).toContain('>>>>>> A3 D3') }, ) }) }) - }) - - test('accepts itself when imported for side effects only (no bindings imported)', async () => { - const testDir = baseDir + '/side-effects' - const file = 'side-effects.ts' - await untilBrowserLogAfter( - () => page.goto(`${viteTestUrl}/${testDir}/`), - [CONNECTED, />>>/], - (logs) => { - expect(logs).toContain('>>> side FX') - }, - ) + describe('when some used exports are not accepted', () => { + const testDir = baseDir + '/main-non-accepted' - await untilBrowserLogAfter( - () => { - editFile(`${testDir}/${file}`, (code) => - code.replace('>>> side FX', '>>> side FX !!'), - ) - }, - HOT_UPDATED, - (logs) => { - expect(logs).toEqual([ - '>>> side FX !!', - `[vite] hot updated: /${testDir}/${file}`, - ]) - }, - ) - }) + const namedFileName = 'named.ts' + const namedFile = `${testDir}/${namedFileName}` + const defaultFileName = 'default.ts' + const defaultFile = `${testDir}/${defaultFileName}` + const depFileName = 'dep.ts' + const depFile = `${testDir}/${depFileName}` - describe('acceptExports([])', () => { - const testDir = baseDir + '/unused-exports' + const a = 'A0' + let dep = 'dep0' - test('accepts itself if no exports are imported', async () => { - const fileName = 'unused.ts' - const file = `${testDir}/${fileName}` - const url = '/' + file + beforeAll(async () => { + await untilBrowserLogAfter( + () => page.goto(`${viteTestUrl}/${testDir}/`), + [CONNECTED, />>>>>>/], + (logs) => { + expect(logs).toContain(`<<< named: ${a} ; ${dep}`) + expect(logs).toContain(`<<< default: def0`) + expect(logs).toContain(`>>>>>> ${a} def0`) + }, + ) + }) - await untilBrowserLogAfter( - () => page.goto(`${viteTestUrl}/${testDir}/`), - [CONNECTED, '-- unused --'], - (logs) => { - expect(logs).toContain('-- unused --') - }, - ) + it('does not stop the HMR bubble on change to dep', async () => { + await untilBrowserLogAfter( + async () => { + editFile(depFile, (code) => code.replace('dep0', (dep = 'dep1'))) + await page.waitForEvent('load') + }, + [CONNECTED, />>>>>>/], + (logs) => { + expect(logs).toContain(`<<< named: ${a} ; ${dep}`) + }, + ) + }) - await untilBrowserLogAfter( - () => { - editFile(file, (code) => - code.replace('-- unused --', '-> unused <-'), + describe('does not stop the HMR bubble on change to self', () => { + it('with named exports', async () => { + await untilBrowserLogAfter( + async () => { + editFile(namedFile, (code) => code.replace(a, 'A1')) + await page.waitForEvent('load') + }, + [CONNECTED, />>>>>>/], + (logs) => { + expect(logs).toContain(`<<< named: A1 ; ${dep}`) + }, ) - }, - HOT_UPDATED, - (logs) => { - expect(logs).toEqual(['-> unused <-', `[vite] hot updated: ${url}`]) - }, - ) + }) + + it('with default export', async () => { + await untilBrowserLogAfter( + async () => { + editFile(defaultFile, (code) => code.replace('def0', 'def1')) + await page.waitForEvent('load') + }, + [CONNECTED, />>>>>>/], + (logs) => { + expect(logs).toContain(`<<< default: def1`) + }, + ) + }) + }) }) - test("doesn't accept itself if any of its exports is imported", async () => { - const fileName = 'used.ts' - const file = `${testDir}/${fileName}` + test('accepts itself when imported for side effects only (no bindings imported)', async () => { + const testDir = baseDir + '/side-effects' + const file = 'side-effects.ts' await untilBrowserLogAfter( () => page.goto(`${viteTestUrl}/${testDir}/`), - [CONNECTED, '-- used --'], + [CONNECTED, />>>/], (logs) => { - expect(logs).toContain('-- used --') - expect(logs).toContain('used:foo0') + expect(logs).toContain('>>> side FX') }, ) await untilBrowserLogAfter( - async () => { - editFile(file, (code) => - code.replace('foo0', 'foo1').replace('-- used --', '-> used <-'), + () => { + editFile(`${testDir}/${file}`, (code) => + code.replace('>>> side FX', '>>> side FX !!'), ) - await page.waitForEvent('load') }, - [CONNECTED, /used:foo/], + HOT_UPDATED, (logs) => { - expect(logs).toContain('-> used <-') - expect(logs).toContain('used:foo1') + expect(logs).toEqual([ + '>>> side FX !!', + `[vite] hot updated: /${testDir}/${file}`, + ]) }, ) }) - }) - describe('indiscriminate imports: import *', () => { - const testStarExports = (testDirName: string) => { - const testDir = `${baseDir}/${testDirName}` + describe('acceptExports([])', () => { + const testDir = baseDir + '/unused-exports' - it('accepts itself if all its exports are accepted', async () => { - const fileName = 'deps-all-accepted.ts' + test('accepts itself if no exports are imported', async () => { + const fileName = 'unused.ts' const file = `${testDir}/${fileName}` const url = '/' + file await untilBrowserLogAfter( () => page.goto(`${viteTestUrl}/${testDir}/`), - [CONNECTED, '>>> ready <<<'], + [CONNECTED, '-- unused --'], (logs) => { - expect(logs).toContain('loaded:all:a0b0c0default0') - expect(logs).toContain('all >>>>>> a0, b0, c0') + expect(logs).toContain('-- unused --') }, ) await untilBrowserLogAfter( () => { - editFile(file, (code) => code.replace(/([abc])0/g, '$11')) + editFile(file, (code) => + code.replace('-- unused --', '-> unused <-'), + ) }, HOT_UPDATED, (logs) => { expect(logs).toEqual([ - 'all >>>>>> a1, b1, c1', - `[vite] hot updated: ${url}`, - ]) - }, - ) - - await untilBrowserLogAfter( - () => { - editFile(file, (code) => code.replace(/([abc])1/g, '$12')) - }, - HOT_UPDATED, - (logs) => { - expect(logs).toEqual([ - 'all >>>>>> a2, b2, c2', + '-> unused <-', `[vite] hot updated: ${url}`, ]) }, ) }) - it("doesn't accept itself if one export is not accepted", async () => { - const fileName = 'deps-some-accepted.ts' + test("doesn't accept itself if any of its exports is imported", async () => { + const fileName = 'used.ts' const file = `${testDir}/${fileName}` await untilBrowserLogAfter( () => page.goto(`${viteTestUrl}/${testDir}/`), - [CONNECTED, '>>> ready <<<'], + [CONNECTED, '-- used --'], (logs) => { - expect(logs).toContain('loaded:some:a0b0c0default0') - expect(logs).toContain('some >>>>>> a0, b0, c0') + expect(logs).toContain('-- used --') + expect(logs).toContain('used:foo0') }, ) await untilBrowserLogAfter( async () => { - const loadPromise = page.waitForEvent('load') - editFile(file, (code) => code.replace(/([abc])0/g, '$11')) - await loadPromise + editFile(file, (code) => + code + .replace('foo0', 'foo1') + .replace('-- used --', '-> used <-'), + ) + await page.waitForEvent('load') }, - [CONNECTED, '>>> ready <<<'], + [CONNECTED, /used:foo/], (logs) => { - expect(logs).toContain('loaded:some:a1b1c1default0') - expect(logs).toContain('some >>>>>> a1, b1, c1') + expect(logs).toContain('-> used <-') + expect(logs).toContain('used:foo1') }, ) }) - } + }) + + describe('indiscriminate imports: import *', () => { + const testStarExports = (testDirName: string) => { + const testDir = `${baseDir}/${testDirName}` + + it('accepts itself if all its exports are accepted', async () => { + const fileName = 'deps-all-accepted.ts' + const file = `${testDir}/${fileName}` + const url = '/' + file + + await untilBrowserLogAfter( + () => page.goto(`${viteTestUrl}/${testDir}/`), + [CONNECTED, '>>> ready <<<'], + (logs) => { + expect(logs).toContain('loaded:all:a0b0c0default0') + expect(logs).toContain('all >>>>>> a0, b0, c0') + }, + ) + + await untilBrowserLogAfter( + () => { + editFile(file, (code) => code.replace(/([abc])0/g, '$11')) + }, + HOT_UPDATED, + (logs) => { + expect(logs).toEqual([ + 'all >>>>>> a1, b1, c1', + `[vite] hot updated: ${url}`, + ]) + }, + ) - describe('import * from ...', () => testStarExports('star-imports')) + await untilBrowserLogAfter( + () => { + editFile(file, (code) => code.replace(/([abc])1/g, '$12')) + }, + HOT_UPDATED, + (logs) => { + expect(logs).toEqual([ + 'all >>>>>> a2, b2, c2', + `[vite] hot updated: ${url}`, + ]) + }, + ) + }) + + it("doesn't accept itself if one export is not accepted", async () => { + const fileName = 'deps-some-accepted.ts' + const file = `${testDir}/${fileName}` + + await untilBrowserLogAfter( + () => page.goto(`${viteTestUrl}/${testDir}/`), + [CONNECTED, '>>> ready <<<'], + (logs) => { + expect(logs).toContain('loaded:some:a0b0c0default0') + expect(logs).toContain('some >>>>>> a0, b0, c0') + }, + ) + + await untilBrowserLogAfter( + async () => { + const loadPromise = page.waitForEvent('load') + editFile(file, (code) => code.replace(/([abc])0/g, '$11')) + await loadPromise + }, + [CONNECTED, '>>> ready <<<'], + (logs) => { + expect(logs).toContain('loaded:some:a1b1c1default0') + expect(logs).toContain('some >>>>>> a1, b1, c1') + }, + ) + }) + } - describe('dynamic import(...)', () => testStarExports('dynamic-imports')) + describe('import * from ...', () => testStarExports('star-imports')) + + describe('dynamic import(...)', () => + testStarExports('dynamic-imports')) + }) }) - }) - test('css in html hmr', async () => { - await page.goto(viteTestUrl) - expect(await getBg('.import-image')).toMatch('icon') - await page.goto(viteTestUrl + '/foo/', { waitUntil: 'load' }) - expect(await getBg('.import-image')).toMatch('icon') - - const loadPromise = page.waitForEvent('load') - editFile('index.html', (code) => code.replace('url("./icon.png")', '')) - await loadPromise - expect(await getBg('.import-image')).toMatch('') - }) + test('css in html hmr', async () => { + await page.goto(viteTestUrl) + expect(await getBg('.import-image')).toMatch('icon') + await page.goto(viteTestUrl + '/foo/', { waitUntil: 'load' }) + expect(await getBg('.import-image')).toMatch('icon') - test('HTML', async () => { - await page.goto(viteTestUrl + '/counter/index.html') - let btn = await page.$('button') - expect(await btn.textContent()).toBe('Counter 0') + const loadPromise = page.waitForEvent('load') + editFile('index.html', (code) => code.replace('url("./icon.png")', '')) + await loadPromise + expect(await getBg('.import-image')).toMatch('') + }) - const loadPromise = page.waitForEvent('load') - editFile('counter/index.html', (code) => - code.replace('Counter', 'Compteur'), - ) - await loadPromise - btn = await page.$('button') - expect(await btn.textContent()).toBe('Compteur 0') - }) + test('HTML', async () => { + await page.goto(viteTestUrl + '/counter/index.html') + let btn = await page.$('button') + expect(await btn.textContent()).toBe('Counter 0') - test('handle virtual module updates', async () => { - await page.goto(viteTestUrl) - const el = await page.$('.virtual') - expect(await el.textContent()).toBe('[success]0') - editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]')) - await untilUpdated(async () => { + const loadPromise = page.waitForEvent('load') + editFile('counter/index.html', (code) => + code.replace('Counter', 'Compteur'), + ) + await loadPromise + btn = await page.$('button') + expect(await btn.textContent()).toBe('Compteur 0') + }) + + test('handle virtual module updates', async () => { + await page.goto(viteTestUrl) const el = await page.$('.virtual') - return await el.textContent() - }, '[wow]') - }) + expect(await el.textContent()).toBe('[success]0') + editFile('importedVirtual.js', (code) => + code.replace('[success]', '[wow]'), + ) + await untilUpdated(async () => { + const el = await page.$('.virtual') + return await el.textContent() + }, '[wow]') + }) - test('invalidate virtual module', async () => { - await page.goto(viteTestUrl) - const el = await page.$('.virtual') - expect(await el.textContent()).toBe('[wow]0') - const btn = await page.$('.virtual-update') - btn.click() - await untilUpdated(async () => { + test('invalidate virtual module', async () => { + await page.goto(viteTestUrl) const el = await page.$('.virtual') - return await el.textContent() - }, '[wow]1') - }) + expect(await el.textContent()).toBe('[wow]0') + const btn = await page.$('.virtual-update') + btn.click() + await untilUpdated(async () => { + const el = await page.$('.virtual') + return await el.textContent() + }, '[wow]1') + }) - test('handle virtual module accept updates', async () => { - await page.goto(viteTestUrl) - const el = await page.$('.virtual-dep') - expect(await el.textContent()).toBe('0') - editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]')) - await untilUpdated(async () => { + test('handle virtual module accept updates', async () => { + await page.goto(viteTestUrl) const el = await page.$('.virtual-dep') - return await el.textContent() - }, '[wow]') - }) + expect(await el.textContent()).toBe('0') + editFile('importedVirtual.js', (code) => + code.replace('[success]', '[wow]'), + ) + await untilUpdated(async () => { + const el = await page.$('.virtual-dep') + return await el.textContent() + }, '[wow]') + }) - test('invalidate virtual module and accept', async () => { - await page.goto(viteTestUrl) - const el = await page.$('.virtual-dep') - expect(await el.textContent()).toBe('0') - const btn = await page.$('.virtual-update-dep') - btn.click() - await untilUpdated(async () => { + test('invalidate virtual module and accept', async () => { + await page.goto(viteTestUrl) const el = await page.$('.virtual-dep') - return await el.textContent() - }, '[wow]2') - }) - - test('keep hmr reload after missing import on server startup', async () => { - const file = 'missing-import/a.js' - const importCode = "import 'missing-modules'" - const unImportCode = `// ${importCode}` - - await untilBrowserLogAfter( - () => - page.goto(viteTestUrl + '/missing-import/index.html', { - waitUntil: 'load', - }), - /connected/, // wait for HMR connection - ) + expect(await el.textContent()).toBe('0') + const btn = await page.$('.virtual-update-dep') + btn.click() + await untilUpdated(async () => { + const el = await page.$('.virtual-dep') + return await el.textContent() + }, '[wow]2') + }) - await untilBrowserLogAfter(async () => { - const loadPromise = page.waitForEvent('load') - editFile(file, (code) => code.replace(importCode, unImportCode)) - await loadPromise - }, ['missing test', /connected/]) + test('keep hmr reload after missing import on server startup', async () => { + const file = 'missing-import/a.js' + const importCode = "import 'missing-modules'" + const unImportCode = `// ${importCode}` - await untilBrowserLogAfter(async () => { - const loadPromise = page.waitForEvent('load') - editFile(file, (code) => code.replace(unImportCode, importCode)) - await loadPromise - }, [/500/, /connected/]) - }) + await untilBrowserLogAfter( + () => + page.goto(viteTestUrl + '/missing-import/index.html', { + waitUntil: 'load', + }), + /connected/, // wait for HMR connection + ) - test('should hmr when file is deleted and restored', async () => { - await page.goto(viteTestUrl) + await untilBrowserLogAfter(async () => { + const loadPromise = page.waitForEvent('load') + editFile(file, (code) => code.replace(importCode, unImportCode)) + await loadPromise + }, ['missing test', /connected/]) + + await untilBrowserLogAfter(async () => { + const loadPromise = page.waitForEvent('load') + editFile(file, (code) => code.replace(unImportCode, importCode)) + await loadPromise + }, [/500/, /connected/]) + }) - const parentFile = 'file-delete-restore/parent.js' - const childFile = 'file-delete-restore/child.js' + test('should hmr when file is deleted and restored', async () => { + await page.goto(viteTestUrl) - await untilUpdated( - () => page.textContent('.file-delete-restore'), - 'parent:child', - ) + const parentFile = 'file-delete-restore/parent.js' + const childFile = 'file-delete-restore/child.js' - editFile(childFile, (code) => - code.replace("value = 'child'", "value = 'child1'"), - ) - await untilUpdated( - () => page.textContent('.file-delete-restore'), - 'parent:child1', - ) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:child', + ) - // delete the file - editFile(parentFile, (code) => - code.replace( - "export { value as childValue } from './child'", - "export const childValue = 'not-child'", - ), - ) - const originalChildFileCode = readFile(childFile) - await Promise.all([ - untilBrowserLogAfter( - () => removeFile(childFile), - `${childFile} is disposed`, - ), - untilUpdated( + editFile(childFile, (code) => + code.replace("value = 'child'", "value = 'child1'"), + ) + await untilUpdated( () => page.textContent('.file-delete-restore'), - 'parent:not-child', - ), - ]) + 'parent:child1', + ) - await untilBrowserLogAfter(async () => { - const loadPromise = page.waitForEvent('load') - addFile(childFile, originalChildFileCode) + // delete the file editFile(parentFile, (code) => code.replace( - "export const childValue = 'not-child'", "export { value as childValue } from './child'", + "export const childValue = 'not-child'", ), ) - await loadPromise - }, [/connected/]) - await untilUpdated( - () => page.textContent('.file-delete-restore'), - 'parent:child', - ) - }) - - test('delete file should not break hmr', async () => { - await page.goto(viteTestUrl) + const originalChildFileCode = readFile(childFile) + await Promise.all([ + untilBrowserLogAfter( + () => removeFile(childFile), + `${childFile} is disposed`, + ), + untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:not-child', + ), + ]) - await untilUpdated( - () => page.textContent('.intermediate-file-delete-display'), - 'count is 1', - ) + await untilBrowserLogAfter(async () => { + const loadPromise = page.waitForEvent('load') + addFile(childFile, originalChildFileCode) + editFile(parentFile, (code) => + code.replace( + "export const childValue = 'not-child'", + "export { value as childValue } from './child'", + ), + ) + await loadPromise + }, [/connected/]) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:child', + ) + }) - // add state - await page.click('.intermediate-file-delete-increment') - await untilUpdated( - () => page.textContent('.intermediate-file-delete-display'), - 'count is 2', - ) + test('delete file should not break hmr', async () => { + await page.goto(viteTestUrl) - // update import, hmr works - editFile('intermediate-file-delete/index.js', (code) => - code.replace("from './re-export.js'", "from './display.js'"), - ) - editFile('intermediate-file-delete/display.js', (code) => - code.replace('count is ${count}', 'count is ${count}!'), - ) - await untilUpdated( - () => page.textContent('.intermediate-file-delete-display'), - 'count is 2!', - ) + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 1', + ) - // remove unused file, page reload because it's considered entry point now - removeFile('intermediate-file-delete/re-export.js') - await untilUpdated( - () => page.textContent('.intermediate-file-delete-display'), - 'count is 1!', - ) + // add state + await page.click('.intermediate-file-delete-increment') + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 2', + ) - // re-add state - await page.click('.intermediate-file-delete-increment') - await untilUpdated( - () => page.textContent('.intermediate-file-delete-display'), - 'count is 2!', - ) + // update import, hmr works + editFile('intermediate-file-delete/index.js', (code) => + code.replace("from './re-export.js'", "from './display.js'"), + ) + editFile('intermediate-file-delete/display.js', (code) => + code.replace('count is ${count}', 'count is ${count}!'), + ) + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 2!', + ) - // hmr works after file deletion - editFile('intermediate-file-delete/display.js', (code) => - code.replace('count is ${count}!', 'count is ${count}'), - ) - await untilUpdated( - () => page.textContent('.intermediate-file-delete-display'), - 'count is 2', - ) - }) + // remove unused file, page reload because it's considered entry point now + removeFile('intermediate-file-delete/re-export.js') + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 1!', + ) - test('deleted file should trigger dispose and prune callbacks', async () => { - await page.goto(viteTestUrl) + // re-add state + await page.click('.intermediate-file-delete-increment') + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 2!', + ) - const parentFile = 'file-delete-restore/parent.js' - const childFile = 'file-delete-restore/child.js' - const originalChildFileCode = readFile(childFile) + // hmr works after file deletion + editFile('intermediate-file-delete/display.js', (code) => + code.replace('count is ${count}!', 'count is ${count}'), + ) + await untilUpdated( + () => page.textContent('.intermediate-file-delete-display'), + 'count is 2', + ) + }) - await untilBrowserLogAfter( - () => { - // delete the file - editFile(parentFile, (code) => - code.replace( - "export { value as childValue } from './child'", - "export const childValue = 'not-child'", - ), - ) - removeFile(childFile) - }, - [ - 'file-delete-restore/child.js is disposed', - 'file-delete-restore/child.js is pruned', - ], - false, - ) - await untilUpdated( - () => page.textContent('.file-delete-restore'), - 'parent:not-child', - ) + test('deleted file should trigger dispose and prune callbacks', async () => { + await page.goto(viteTestUrl) - // restore the file - addFile(childFile, originalChildFileCode) - editFile(parentFile, (code) => - code.replace( - "export const childValue = 'not-child'", - "export { value as childValue } from './child'", - ), - ) - await untilUpdated( - () => page.textContent('.file-delete-restore'), - 'parent:child', - ) - }) + const parentFile = 'file-delete-restore/parent.js' + const childFile = 'file-delete-restore/child.js' + const originalChildFileCode = readFile(childFile) - test('import.meta.hot?.accept', async () => { - await page.goto(viteTestUrl) + await untilBrowserLogAfter( + () => { + // delete the file + editFile(parentFile, (code) => + code.replace( + "export { value as childValue } from './child'", + "export const childValue = 'not-child'", + ), + ) + removeFile(childFile) + }, + [ + 'file-delete-restore/child.js is disposed', + 'file-delete-restore/child.js is pruned', + ], + false, + ) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:not-child', + ) - const el = await page.$('.optional-chaining') - await untilBrowserLogAfter( - () => - editFile('optional-chaining/child.js', (code) => - code.replace('const foo = 1', 'const foo = 2'), + // restore the file + addFile(childFile, originalChildFileCode) + editFile(parentFile, (code) => + code.replace( + "export const childValue = 'not-child'", + "export { value as childValue } from './child'", ), - '(optional-chaining) child update', - ) - await untilUpdated(() => el.textContent(), '2') - }) + ) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:child', + ) + }) - test('hmr works for self-accepted module within circular imported files', async () => { - await page.goto(viteTestUrl + '/self-accept-within-circular/index.html') - const el = await page.$('.self-accept-within-circular') - expect(await el.textContent()).toBe('c') - editFile('self-accept-within-circular/c.js', (code) => - code.replace(`export const c = 'c'`, `export const c = 'cc'`), - ) - await untilUpdated( - () => page.textContent('.self-accept-within-circular'), - 'cc', - ) - expect(serverLogs.length).greaterThanOrEqual(1) - // Should still keep hmr update, but it'll error on the browser-side and will refresh itself. - // Match on full log not possible because of color markers - expect(serverLogs.at(-1)!).toContain('hmr update') - }) + test('import.meta.hot?.accept', async () => { + await page.goto(viteTestUrl) - test('hmr should not reload if no accepted within circular imported files', async () => { - await page.goto(viteTestUrl + '/circular/index.html') - const el = await page.$('.circular') - expect(await el.textContent()).toBe( - 'mod-a -> mod-b -> mod-c -> mod-a (expected error)', - ) - editFile('circular/mod-b.js', (code) => - code.replace(`mod-b ->`, `mod-b (edited) ->`), - ) - await untilUpdated( - () => el.textContent(), - 'mod-a -> mod-b (edited) -> mod-c -> mod-a (expected error)', - ) - }) + const el = await page.$('.optional-chaining') + await untilBrowserLogAfter( + () => + editFile('optional-chaining/child.js', (code) => + code.replace('const foo = 1', 'const foo = 2'), + ), + '(optional-chaining) child update', + ) + await untilUpdated(() => el.textContent(), '2') + }) - test('not inlined assets HMR', async () => { - await page.goto(viteTestUrl) - const el = await page.$('#logo-no-inline') - await untilBrowserLogAfter( - () => - editFile('logo-no-inline.svg', (code) => - code.replace('height="30px"', 'height="40px"'), - ), - /Logo-no-inline updated/, - ) - await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40') - }) + test('hmr works for self-accepted module within circular imported files', async () => { + await page.goto(viteTestUrl + '/self-accept-within-circular/index.html') + const el = await page.$('.self-accept-within-circular') + expect(await el.textContent()).toBe('c') + editFile('self-accept-within-circular/c.js', (code) => + code.replace(`export const c = 'c'`, `export const c = 'cc'`), + ) + await untilUpdated( + () => page.textContent('.self-accept-within-circular'), + 'cc', + ) + expect(serverLogs.length).greaterThanOrEqual(1) + // Should still keep hmr update, but it'll error on the browser-side and will refresh itself. + // Match on full log not possible because of color markers + expect(serverLogs.at(-1)!).toContain('hmr update') + }) - test('inlined assets HMR', async () => { - await page.goto(viteTestUrl) - const el = await page.$('#logo') - await untilBrowserLogAfter( - () => - editFile('logo.svg', (code) => - code.replace('height="30px"', 'height="40px"'), - ), - /Logo updated/, - ) - await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40') - }) + test('hmr should not reload if no accepted within circular imported files', async () => { + await page.goto(viteTestUrl + '/circular/index.html') + const el = await page.$('.circular') + expect(await el.textContent()).toBe( + 'mod-a -> mod-b -> mod-c -> mod-a (expected error)', + ) + editFile('circular/mod-b.js', (code) => + code.replace(`mod-b ->`, `mod-b (edited) ->`), + ) + await untilUpdated( + () => el.textContent(), + 'mod-a -> mod-b (edited) -> mod-c -> mod-a (expected error)', + ) + }) - test('CSS HMR with this.addWatchFile', async () => { - await page.goto(viteTestUrl + '/css-deps/index.html') - expect(await getColor('.css-deps')).toMatch('red') - editFile('css-deps/dep.js', (code) => code.replace(`red`, `green`)) - await untilUpdated(() => getColor('.css-deps'), 'green') - }) + test('not inlined assets HMR', async () => { + await page.goto(viteTestUrl) + const el = await page.$('#logo-no-inline') + await untilBrowserLogAfter( + () => + editFile('logo-no-inline.svg', (code) => + code.replace('height="30px"', 'height="40px"'), + ), + /Logo-no-inline updated/, + ) + await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40') + }) + + test('inlined assets HMR', async () => { + await page.goto(viteTestUrl) + const el = await page.$('#logo') + await untilBrowserLogAfter( + () => + editFile('logo.svg', (code) => + code.replace('height="30px"', 'height="40px"'), + ), + /Logo updated/, + ) + await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40') + }) - test('hmr should happen after missing file is created', async () => { - const file = 'missing-file/a.js' - const code = 'console.log("a.js")' + test('CSS HMR with this.addWatchFile', async () => { + await page.goto(viteTestUrl + '/css-deps/index.html') + expect(await getColor('.css-deps')).toMatch('red') + editFile('css-deps/dep.js', (code) => code.replace(`red`, `green`)) + await untilUpdated(() => getColor('.css-deps'), 'green') + }) - await untilBrowserLogAfter( - () => - page.goto(viteTestUrl + '/missing-file/index.html', { - waitUntil: 'load', - }), - /connected/, // wait for HMR connection - ) + test('hmr should happen after missing file is created', async () => { + const file = 'missing-file/a.js' + const code = 'console.log("a.js")' - await untilBrowserLogAfter(async () => { - const loadPromise = page.waitForEvent('load') - addFile(file, code) - await loadPromise - }, [/connected/, 'a.js']) - }) + await untilBrowserLogAfter( + () => + page.goto(viteTestUrl + '/missing-file/index.html', { + waitUntil: 'load', + }), + /connected/, // wait for HMR connection + ) + + await untilBrowserLogAfter(async () => { + const loadPromise = page.waitForEvent('load') + addFile(file, code) + await loadPromise + }, [/connected/, 'a.js']) + }) + } } diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index 9ee8024ee2bf44..dbf6776bd84edd 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -13,6 +13,43 @@ export default defineConfig({ return false } }, + rollupOptions: { + input: [ + path.resolve( + import.meta.dirname, + 'accept-exports/dynamic-imports/index.html', + ), + path.resolve( + import.meta.dirname, + 'accept-exports/export-from/index.html', + ), + path.resolve( + import.meta.dirname, + 'accept-exports/main-accepted/index.html', + ), + path.resolve( + import.meta.dirname, + 'accept-exports/main-non-accepted/index.html', + ), + path.resolve( + import.meta.dirname, + 'accept-exports/side-effects/index.html', + ), + path.resolve( + import.meta.dirname, + 'accept-exports/star-imports/index.html', + ), + path.resolve( + import.meta.dirname, + 'accept-exports/unused-exports/index.html', + ), + path.resolve(import.meta.dirname, 'index.html'), + ], + // TODO(underfin): find a nice way + // The vite root not using `cwd` option call rolldown, the rolldown register module is base on process.cwd to calculate path. + // make full bundle mode print the hmr path base on the dirname + cwd: import.meta.dirname, + }, }, plugins: [ { diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index bee9ced78f9bf3..07756b025b4265 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -15,6 +15,7 @@ import { build, createBuilder, createServer, + createServerWithResolvedConfig, loadConfigFromFile, mergeConfig, preview, @@ -227,6 +228,9 @@ async function loadConfig(configEnv: ConfigEnv) { // tests are flaky when `emptyOutDir` is `true` emptyOutDir: false, }, + experimental: { + fullBundleMode: !!process.env.VITE_TEST_FULL_BUNDLE_MODE, + }, customLogger: createInMemoryLogger(serverLogs), } return mergeConfig(options, config || {}) @@ -238,7 +242,16 @@ export async function startDefaultServe(): Promise { if (!isBuild) { process.env.VITE_INLINE = 'inline-serve' const config = await loadConfig({ command: 'serve', mode: 'development' }) - viteServer = server = await (await createServer(config)).listen() + + if (process.env.VITE_TEST_FULL_BUNDLE_MODE) { + const builder = await createBuilder(config, null, 'serve') + viteServer = server = await createServerWithResolvedConfig(builder.config) + await server.listen() + await builder.buildApp(server) + } else { + viteServer = server = await (await createServer(config)).listen() + } + viteTestUrl = stripTrailingSlashIfNeeded( server.resolvedUrls.local[0], server.config.base, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78b70c6568bbfc..4a7000f4afc406 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: + vitest>vite: npm:vite@^6.2.6 vite: workspace:rolldown-vite@* packageExtensionsChecksum: sha256-BLDZCgUIohvBXMHo3XFOlGLzGXRyK3sDU0nMBRk9APY= @@ -137,7 +138,7 @@ importers: version: link:packages/vite vitest: specifier: ^3.1.3 - version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.18) + version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.0(source-map-js@1.2.1))(sass@1.89.0)(stylus@0.64.0)(sugarss@5.0.0(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) docs: devDependencies: @@ -7533,6 +7534,46 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite@6.2.6: + resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitepress-plugin-group-icons@1.5.2: resolution: {integrity: sha512-zen07KxZ83y3eecou4EraaEgwIriwHaB5Q0cHAmS4yO1UZEQvbljTylHPqiJ7LNkV39U8VehfcyquAJXg/26LA==} @@ -9926,13 +9967,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.3(vite@packages+vite)': + '@vitest/mocker@3.1.3(vite@6.2.6(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.0(source-map-js@1.2.1))(sass@1.89.0)(stylus@0.64.0)(sugarss@5.0.0(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: link:packages/vite + vite: 6.2.6(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.0(source-map-js@1.2.1))(sass@1.89.0)(stylus@0.64.0)(sugarss@5.0.0(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) '@vitest/pretty-format@3.1.3': dependencies: @@ -13844,6 +13885,25 @@ snapshots: transitivePeerDependencies: - supports-color + vite@6.2.6(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.0(source-map-js@1.2.1))(sass@1.89.0)(stylus@0.64.0)(sugarss@5.0.0(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0): + dependencies: + esbuild: 0.25.0 + postcss: 8.5.3 + rollup: 4.34.9 + optionalDependencies: + '@types/node': 22.15.18 + fsevents: 2.3.3 + jiti: 2.4.2 + less: 4.3.0 + lightningcss: 1.30.1 + sass: 1.89.0 + sass-embedded: 1.89.0(source-map-js@1.2.1) + stylus: 0.64.0 + sugarss: 5.0.0(postcss@8.5.3) + terser: 5.39.0 + tsx: 4.19.4 + yaml: 2.8.0 + vitepress-plugin-group-icons@1.5.2: dependencies: '@iconify-json/logos': 1.2.4 @@ -13909,10 +13969,10 @@ snapshots: - typescript - universal-cookie - vitest@3.1.3(@types/debug@4.1.12)(@types/node@22.15.18): + vitest@3.1.3(@types/debug@4.1.12)(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.0(source-map-js@1.2.1))(sass@1.89.0)(stylus@0.64.0)(sugarss@5.0.0(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.3 - '@vitest/mocker': 3.1.3(vite@packages+vite) + '@vitest/mocker': 3.1.3(vite@6.2.6(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.0(source-map-js@1.2.1))(sass@1.89.0)(stylus@0.64.0)(sugarss@5.0.0(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.3 '@vitest/runner': 3.1.3 '@vitest/snapshot': 3.1.3 @@ -13929,15 +13989,25 @@ snapshots: tinyglobby: 0.2.13 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: link:packages/vite + vite: 6.2.6(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.0(source-map-js@1.2.1))(sass@1.89.0)(stylus@0.64.0)(sugarss@5.0.0(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) vite-node: 3.1.3 why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.15.18 transitivePeerDependencies: + - jiti + - less + - lightningcss - msw + - sass + - sass-embedded + - stylus + - sugarss - supports-color + - terser + - tsx + - yaml void-elements@3.1.0: {} diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index f9e21a1c2fee67..784921a55ee061 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -12,7 +12,13 @@ export default defineConfig({ }, }, test: { - include: ['./playground/**/*.spec.[tj]s'], + include: process.env.VITE_TEST_FULL_BUNDLE_MODE + ? [ + './playground/define/**/*.spec.[tj]s', + './playground/hmr-root/**/*.spec.[tj]s', + './playground/hmr/**/*.spec.[tj]s', + ] + : ['./playground/**/*.spec.[tj]s'], exclude: [ './playground/legacy/**/*.spec.[tj]s', // system format ...(isBuild