diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 39b3e4fb15acf4..4bc57ce58f76aa 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -51,7 +51,11 @@ import { ssrManifestPlugin } from './ssr/ssrManifestPlugin' import { loadFallbackPlugin } from './plugins/loadFallback' import { findNearestPackageData } from './packages' import type { PackageCache } from './packages' -import { resolveChokidarOptions } from './watch' +import { + getResolvedOutDirs, + resolveChokidarOptions, + resolveEmptyOutDir, +} from './watch' import { completeSystemWrapPlugin } from './plugins/completeSystemWrap' import { mergeConfig } from './publicUtils' import { webWorkerPostPlugin } from './plugins/worker' @@ -655,7 +659,17 @@ export async function build( normalizedOutputs.push(buildOutputOptions(outputs)) } - const outDirs = normalizedOutputs.map(({ dir }) => resolve(dir!)) + const resolvedOutDirs = getResolvedOutDirs( + config.root, + options.outDir, + options.rollupOptions?.output, + ) + const emptyOutDir = resolveEmptyOutDir( + options.emptyOutDir, + config.root, + resolvedOutDirs, + config.logger, + ) // watch file changes with rollup if (config.build.watch) { @@ -664,6 +678,8 @@ export async function build( const resolvedChokidarOptions = resolveChokidarOptions( config, config.build.watch.chokidar, + resolvedOutDirs, + emptyOutDir, ) const { watch } = await import('rollup') @@ -680,7 +696,7 @@ export async function build( if (event.code === 'BUNDLE_START') { config.logger.info(colors.cyan(`\nbuild started...`)) if (options.write) { - prepareOutDir(outDirs, options.emptyOutDir, config) + prepareOutDir(resolvedOutDirs, emptyOutDir, config) } } else if (event.code === 'BUNDLE_END') { event.result.close() @@ -699,7 +715,7 @@ export async function build( bundle = await rollup(rollupOptions) if (options.write) { - prepareOutDir(outDirs, options.emptyOutDir, config) + prepareOutDir(resolvedOutDirs, emptyOutDir, config) } const res: RollupOutput[] = [] @@ -726,36 +742,15 @@ export async function build( } function prepareOutDir( - outDirs: string[], + outDirs: Set, emptyOutDir: boolean | null, config: ResolvedConfig, ) { - const nonDuplicateDirs = new Set(outDirs) - let outside = false - if (emptyOutDir == null) { - for (const outDir of nonDuplicateDirs) { - if ( - fs.existsSync(outDir) && - !normalizePath(outDir).startsWith(withTrailingSlash(config.root)) - ) { - // warn if outDir is outside of root - config.logger.warn( - colors.yellow( - `\n${colors.bold(`(!)`)} outDir ${colors.white( - colors.dim(outDir), - )} is not inside project root and will not be emptied.\n` + - `Use --emptyOutDir to override.\n`, - ), - ) - outside = true - break - } - } - } - for (const outDir of nonDuplicateDirs) { - if (!outside && emptyOutDir !== false && fs.existsSync(outDir)) { + const outDirsArray = [...outDirs] + for (const outDir of outDirs) { + if (emptyOutDir !== false && fs.existsSync(outDir)) { // skip those other outDirs which are nested in current outDir - const skipDirs = outDirs + const skipDirs = outDirsArray .map((dir) => { const relative = path.relative(outDir, dir) if ( diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index d36b4c309b3025..c7a73456187938 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -47,7 +47,12 @@ import type { BindCLIShortcutsOptions } from '../shortcuts' import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' import type { Logger } from '../logger' import { printServerUrls } from '../logger' -import { createNoopWatcher, resolveChokidarOptions } from '../watch' +import { + createNoopWatcher, + getResolvedOutDirs, + resolveChokidarOptions, + resolveEmptyOutDir, +} from '../watch' import { initPublicFiles } from '../publicDir' import { getEnvFilesForMode } from '../env' import type { FetchResult } from '../../runtime/types' @@ -428,10 +433,25 @@ export async function _createServer( const httpsOptions = await resolveHttpsConfig(config.server.https) const { middlewareMode } = serverConfig - const resolvedWatchOptions = resolveChokidarOptions(config, { - disableGlobbing: true, - ...serverConfig.watch, - }) + const resolvedOutDirs = getResolvedOutDirs( + config.root, + config.build.outDir, + config.build.rollupOptions?.output, + ) + const emptyOutDir = resolveEmptyOutDir( + config.build.emptyOutDir, + config.root, + resolvedOutDirs, + ) + const resolvedWatchOptions = resolveChokidarOptions( + config, + { + disableGlobbing: true, + ...serverConfig.watch, + }, + resolvedOutDirs, + emptyOutDir, + ) const middlewares = connect() as Connect.Server const httpServer = middlewareMode diff --git a/packages/vite/src/node/watch.ts b/packages/vite/src/node/watch.ts index 9c9972bdd3a471..d8eb128f1613a5 100644 --- a/packages/vite/src/node/watch.ts +++ b/packages/vite/src/node/watch.ts @@ -2,12 +2,58 @@ import { EventEmitter } from 'node:events' import path from 'node:path' import glob from 'fast-glob' import type { FSWatcher, WatchOptions } from 'dep-types/chokidar' -import { arraify } from './utils' -import type { ResolvedConfig } from '.' +import type { OutputOptions } from 'rollup' +import * as colors from 'picocolors' +import { withTrailingSlash } from '../shared/utils' +import { arraify, normalizePath } from './utils' +import type { ResolvedConfig } from './config' +import type { Logger } from './logger' + +export function getResolvedOutDirs( + root: string, + outDir: string, + outputOptions: OutputOptions[] | OutputOptions | undefined, +): Set { + const resolvedOutDir = path.resolve(root, outDir) + if (!outputOptions) return new Set([resolvedOutDir]) + + return new Set( + arraify(outputOptions).map(({ dir }) => + dir ? path.resolve(root, dir) : resolvedOutDir, + ), + ) +} + +export function resolveEmptyOutDir( + emptyOutDir: boolean | null, + root: string, + outDirs: Set, + logger?: Logger, +): boolean { + if (emptyOutDir != null) return emptyOutDir + + for (const outDir of outDirs) { + if (!normalizePath(outDir).startsWith(withTrailingSlash(root))) { + // warn if outDir is outside of root + logger?.warn( + colors.yellow( + `\n${colors.bold(`(!)`)} outDir ${colors.white( + colors.dim(outDir), + )} is not inside project root and will not be emptied.\n` + + `Use --emptyOutDir to override.\n`, + ), + ) + return false + } + } + return true +} export function resolveChokidarOptions( config: ResolvedConfig, options: WatchOptions | undefined, + resolvedOutDirs: Set, + emptyOutDir: boolean, ): WatchOptions { const { ignored: ignoredList, ...otherOptions } = options ?? {} const ignored: WatchOptions['ignored'] = [ @@ -17,9 +63,9 @@ export function resolveChokidarOptions( glob.escapePath(config.cacheDir) + '/**', ...arraify(ignoredList || []), ] - if (config.build.outDir) { + if (emptyOutDir) { ignored.push( - glob.escapePath(path.resolve(config.root, config.build.outDir)) + '/**', + ...[...resolvedOutDirs].map((outDir) => glob.escapePath(outDir) + '/**'), ) }