diff --git a/crates/pack-api/src/project.rs b/crates/pack-api/src/project.rs index 0cc4f4ea8..1b4f070f6 100644 --- a/crates/pack-api/src/project.rs +++ b/crates/pack-api/src/project.rs @@ -18,7 +18,6 @@ use pack_core::{ }; use serde::{Deserialize, Serialize}; use std::{ - fs, path::{Path, PathBuf}, time::Duration, }; @@ -1375,18 +1374,6 @@ impl Project { ) -> Result<()> { let span = tracing::trace_span!("emitting"); async move { - // clean dist director if configured - if self.config().output().await?.clean.is_some_and(|c| c) { - let this = self.await?; - let dist_dir = self.dist_dir().await?; - - // Construct the complete absolute path by combining project_path and dist_dir - let dist_path = Path::new(&this.project_path).join(&*dist_dir); - - if let Err(e) = clean_directory(&dist_path) { - tracing::debug!("Failed to clean dist directory: {}", e); - } - } let client_root = self.client_root().owned().await?; let client_output = self.dist_root().owned().await?; let output_root = self.output_fs().root().owned().await?; @@ -1824,35 +1811,6 @@ async fn all_assets_from_entries_operation( Ok(all_assets_from_entries(assets)) } -fn clean_directory(dist_path: &Path) -> Result<()> { - let canonical_path = fs::canonicalize(dist_path) - .with_context(|| format!("Failed to canonicalize path: {}", dist_path.display()))?; - - if canonical_path.exists() { - tracing::info!("Cleaning dist directory: {}", canonical_path.display()); - - // Read directory entries - for entry in fs::read_dir(&canonical_path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - if let Err(e) = fs::remove_dir_all(&path) { - tracing::warn!("Failed to remove directory {}: {}", path.display(), e); - } - } else if let Err(e) = fs::remove_file(&path) { - tracing::warn!("Failed to remove file {}: {}", path.display(), e); - } - } - - tracing::info!("Dist directory cleaned successfully"); - } else { - tracing::debug!("Dist directory does not exist, skipping clean"); - } - - Ok(()) -} - #[cfg(test)] mod tests { use super::strip_root_prefix; diff --git a/packages/pack/src/commands/build.ts b/packages/pack/src/commands/build.ts index 1485f9cde..63e1a7cd1 100644 --- a/packages/pack/src/commands/build.ts +++ b/packages/pack/src/commands/build.ts @@ -7,11 +7,12 @@ import { BundleOptions } from "../config/types"; import { resolveBundleOptions, WebpackConfig } from "../config/webpackCompat"; import { projectFactory } from "../core/project"; import { HtmlPlugin } from "../plugins/HtmlPlugin"; +import { cleanOutput, getOutputPath } from "../utils/cleanOutput"; import { blockStdout, getPackPath } from "../utils/common"; import { findRootDir } from "../utils/findRoot"; import { getInitialAssetsFromStats } from "../utils/getInitialAssets"; import { processHtmlEntry } from "../utils/htmlEntry"; -import { normalizePath } from "../utils/normalize-path"; +import { normalizePath } from "../utils/normalizePath"; import { useWorkerThreads } from "../utils/runtimePluginStratety"; import { validateEntryPaths } from "../utils/validateEntry"; import { xcodeProfilingReady } from "../utils/xcodeProfile"; @@ -46,6 +47,7 @@ async function buildInternal( const persistentCaching = bundleOptions.config.persistentCaching ?? false; processHtmlEntry(bundleOptions.config, resolvedProjectPath); validateEntryPaths(bundleOptions.config, resolvedProjectPath); + await cleanOutput(bundleOptions.config, resolvedProjectPath); const createProject = projectFactory(); const project = await createProject( @@ -98,8 +100,7 @@ async function buildInternal( if (htmlConfigs.length > 0) { const assets = { js: [] as string[], css: [] as string[] }; - const outputDir = - bundleOptions.config.output?.path || path.join(process.cwd(), "dist"); + const outputDir = getOutputPath(bundleOptions.config, resolvedProjectPath); if (assets.js.length === 0 && assets.css.length === 0) { const discovered = getInitialAssetsFromStats(outputDir); @@ -116,7 +117,9 @@ async function buildInternal( } if (process.env.ANALYZE) { - await analyzeBundle(bundleOptions.config.output?.path || "dist"); + await analyzeBundle( + getOutputPath(bundleOptions.config, resolvedProjectPath), + ); } await project.shutdown(); diff --git a/packages/pack/src/commands/dev.ts b/packages/pack/src/commands/dev.ts index 172f652c6..8d72372d0 100644 --- a/packages/pack/src/commands/dev.ts +++ b/packages/pack/src/commands/dev.ts @@ -19,12 +19,12 @@ import { } from "../config/webpackCompat"; import type { HotReloaderInterface } from "../core/hmr"; import { createHotReloader } from "../core/hmr"; -import { createHttpProxyMiddleware } from "../core/proxy-hono"; +import { createHttpProxyMiddleware } from "../core/proxyHono"; +import { getOutputPath } from "../utils/cleanOutput"; import { blockStdout, getPackPath } from "../utils/common"; import { findRootDir } from "../utils/findRoot"; import { createSelfSignedCertificate } from "../utils/mkcert"; import { printServerInfo } from "../utils/printServerInfo"; -import { useWorkerThreads } from "../utils/runtimePluginStratety"; import { xcodeProfilingReady } from "../utils/xcodeProfile"; // --- Path helpers (same logic as dev.ts, not exported) --- @@ -242,10 +242,7 @@ async function runDev( ); await hotReloader.start(); - const distRoot = path.resolve( - projectPathResolved, - options.config?.output?.path || "./dist", - ); + const distRoot = getOutputPath(options.config, projectPathResolved); const publicPath = options.config?.output?.publicPath; // Skip prefix stripping for "runtime" and when publicPath is absent (match dev.ts). const normalizedPrefix = diff --git a/packages/pack/src/core/hmr.ts b/packages/pack/src/core/hmr.ts index dae56a7a7..3cbcd7946 100644 --- a/packages/pack/src/core/hmr.ts +++ b/packages/pack/src/core/hmr.ts @@ -10,15 +10,15 @@ import { import { IncomingMessage } from "http"; import { nanoid } from "nanoid"; import type { Socket } from "net"; -import path from "path"; import { Duplex } from "stream"; import { WebSocketServer } from "ws"; import { BundleOptions } from "../config/types"; import { HtmlPlugin } from "../plugins/HtmlPlugin"; +import { cleanOutput, getOutputPath } from "../utils/cleanOutput"; import { debounce, getPackPath, processIssues } from "../utils/common"; import { getInitialAssetsFromStats } from "../utils/getInitialAssets"; import { processHtmlEntry } from "../utils/htmlEntry"; -import { normalizePath } from "../utils/normalize-path"; +import { normalizePath } from "../utils/normalizePath"; import { useWorkerThreads } from "../utils/runtimePluginStratety"; import { validateEntryPaths } from "../utils/validateEntry"; import { projectFactory } from "./project"; @@ -112,6 +112,7 @@ export async function createHotReloader( const resolvedRootPath = rootPath || projectPath || process.cwd(); processHtmlEntry(bundleOptions.config, resolvedProjectPath); validateEntryPaths(bundleOptions.config, resolvedProjectPath); + await cleanOutput(bundleOptions.config, resolvedProjectPath); const createProject = projectFactory(); @@ -226,8 +227,7 @@ export async function createHotReloader( return; } - const outputDir = - bundleOptions.config.output?.path || path.join(process.cwd(), "dist"); + const outputDir = getOutputPath(bundleOptions.config, resolvedProjectPath); const publicPath = bundleOptions.config.output?.publicPath; const assets = getInitialAssetsFromStats(outputDir); diff --git a/packages/pack/src/core/project.ts b/packages/pack/src/core/project.ts index 741172f75..eea45b32a 100644 --- a/packages/pack/src/core/project.ts +++ b/packages/pack/src/core/project.ts @@ -20,7 +20,7 @@ import { TurbopackRuleConfigItem, } from "../config/types"; import { getPackPath, rustifyEnv } from "../utils/common"; -import { normalizePath } from "../utils/normalize-path"; +import { normalizePath } from "../utils/normalizePath"; import { runLoaderWorkerPool } from "./loaderWorkerPool"; import { Endpoint, diff --git a/packages/pack/src/core/proxy-hono.ts b/packages/pack/src/core/proxyHono.ts similarity index 100% rename from packages/pack/src/core/proxy-hono.ts rename to packages/pack/src/core/proxyHono.ts diff --git a/packages/pack/src/utils/cleanOutput.ts b/packages/pack/src/utils/cleanOutput.ts new file mode 100644 index 000000000..e2aa42f65 --- /dev/null +++ b/packages/pack/src/utils/cleanOutput.ts @@ -0,0 +1,40 @@ +import fs from "fs"; +import path from "path"; +import type { ConfigComplete } from "../config/types"; + +export function getOutputPath(config: ConfigComplete, projectPath: string) { + return path.resolve(projectPath, config.output?.path || "dist"); +} + +export async function cleanOutput(config: ConfigComplete, projectPath: string) { + if (!config.output?.clean) { + return; + } + + const outputPath = getOutputPath(config, projectPath); + let entries: fs.Dirent[]; + + try { + entries = await fs.promises.readdir(outputPath, { withFileTypes: true }); + } catch (error) { + if (isNodeError(error) && error.code === "ENOENT") { + return; + } + throw error; + } + + await Promise.all( + entries.map((entry) => + fs.promises.rm(path.join(outputPath, entry.name), { + force: true, + maxRetries: 3, + recursive: true, + retryDelay: 50, + }), + ), + ); +} + +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; +} diff --git a/packages/pack/src/utils/normalize-path.ts b/packages/pack/src/utils/normalizePath.ts similarity index 100% rename from packages/pack/src/utils/normalize-path.ts rename to packages/pack/src/utils/normalizePath.ts