diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 3ef16ec019c6d5..fafd460e363d3b 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -559,20 +559,22 @@ async function doBuild( libOptions, config.logger ) + const normalizedOutputs: OutputOptions[] = [] + + if (Array.isArray(outputs)) { + for (const resolvedOutput of outputs) { + normalizedOutputs.push(buildOutputOptions(resolvedOutput)) + } + } else { + normalizedOutputs.push(buildOutputOptions(outputs)) + } + + const outDirs = normalizedOutputs.map(({ dir }) => resolve(dir!)) // watch file changes with rollup if (config.build.watch) { config.logger.info(colors.cyan(`\nwatching for file changes...`)) - const output: OutputOptions[] = [] - if (Array.isArray(outputs)) { - for (const resolvedOutput of outputs) { - output.push(buildOutputOptions(resolvedOutput)) - } - } else { - output.push(buildOutputOptions(outputs)) - } - const resolvedChokidarOptions = resolveChokidarOptions( config.build.watch.chokidar ) @@ -580,7 +582,7 @@ async function doBuild( const { watch } = await import('rollup') const watcher = watch({ ...rollupOptions, - output, + output: normalizedOutputs, watch: { ...config.build.watch, chokidar: resolvedChokidarOptions @@ -591,7 +593,7 @@ async function doBuild( if (event.code === 'BUNDLE_START') { config.logger.info(colors.cyan(`\nbuild started...`)) if (options.write) { - prepareOutDir(outDir, options.emptyOutDir, config) + prepareOutDir(outDirs, options.emptyOutDir, config) } } else if (event.code === 'BUNDLE_END') { event.result.close() @@ -610,24 +612,18 @@ async function doBuild( parallelBuilds.push(bundle) const generate = (output: OutputOptions = {}) => { - return bundle[options.write ? 'write' : 'generate']( - buildOutputOptions(output) - ) + return bundle[options.write ? 'write' : 'generate'](output) } if (options.write) { - prepareOutDir(outDir, options.emptyOutDir, config) + prepareOutDir(outDirs, options.emptyOutDir, config) } - if (Array.isArray(outputs)) { - const res = [] - for (const output of outputs) { - res.push(await generate(output)) - } - return res - } else { - return await generate(outputs) + const res = [] + for (const output of normalizedOutputs) { + res.push(await generate(output)) } + return Array.isArray(outputs) ? res : res[0] } catch (e) { outputBuildError(e) throw e @@ -635,30 +631,53 @@ async function doBuild( } function prepareOutDir( - outDir: string, + outDirs: string[], emptyOutDir: boolean | null, config: ResolvedConfig ) { - if (fs.existsSync(outDir)) { - if ( - emptyOutDir == null && - !normalizePath(outDir).startsWith(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` + const nonDuplicateDirs = new Set(outDirs) + let outside = false + if (emptyOutDir == null) { + for (const outDir of nonDuplicateDirs) { + if ( + fs.existsSync(outDir) && + !normalizePath(outDir).startsWith(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` + ) ) - ) - } else if (emptyOutDir !== false) { - emptyDir(outDir, ['.git']) + outside = true + break + } } } - if (config.publicDir && fs.existsSync(config.publicDir)) { - copyDir(config.publicDir, outDir) + for (const outDir of nonDuplicateDirs) { + if (!outside && emptyOutDir !== false && fs.existsSync(outDir)) { + // skip those other outDirs which are nested in current outDir + const skipDirs = outDirs + .map((dir) => { + const relative = path.relative(outDir, dir) + if ( + relative && + !relative.startsWith('..') && + !path.isAbsolute(relative) + ) { + return relative + } + return '' + }) + .filter(Boolean) + emptyDir(outDir, [...skipDirs, '.git']) + } + if (config.publicDir && fs.existsSync(config.publicDir)) { + copyDir(config.publicDir, outDir) + } } } diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 2c6a8c790c607c..2711a29b7eb703 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -549,16 +549,45 @@ export function isFileReadable(filename: string): boolean { } } +const splitFirstDirRE = /(.+?)[\\/](.+)/ + /** * Delete every file and subdirectory. **The given directory must exist.** - * Pass an optional `skip` array to preserve files in the root directory. + * Pass an optional `skip` array to preserve files under the root directory. */ export function emptyDir(dir: string, skip?: string[]): void { + const skipInDir: string[] = [] + let nested: Map | null = null + if (skip?.length) { + for (const file of skip) { + if (path.dirname(file) !== '.') { + const matched = file.match(splitFirstDirRE) + if (matched) { + nested ??= new Map() + const [, nestedDir, skipPath] = matched + let nestedSkip = nested.get(nestedDir) + if (!nestedSkip) { + nestedSkip = [] + nested.set(nestedDir, nestedSkip) + } + if (!nestedSkip.includes(skipPath)) { + nestedSkip.push(skipPath) + } + } + } else { + skipInDir.push(file) + } + } + } for (const file of fs.readdirSync(dir)) { - if (skip?.includes(file)) { + if (skipInDir.includes(file)) { continue } - fs.rmSync(path.resolve(dir, file), { recursive: true, force: true }) + if (nested?.has(file)) { + emptyDir(path.resolve(dir, file), nested.get(file)) + } else { + fs.rmSync(path.resolve(dir, file), { recursive: true, force: true }) + } } }