// Copyright 2018-2025 the Deno authors. MIT license. mod esbuild; mod externals; mod html; mod provider; mod transform; use std::borrow::Cow; use std::cell::RefCell; use std::ops::Deref; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use std::sync::LazyLock; use std::time::Duration; use deno_ast::EmitOptions; use deno_ast::MediaType; use deno_ast::ModuleKind; use deno_ast::ModuleSpecifier; use deno_bundle_runtime::BundleFormat; use deno_bundle_runtime::BundlePlatform; use deno_bundle_runtime::PackageHandling; use deno_bundle_runtime::SourceMapType; use deno_config::workspace::TsTypeLib; use deno_core::error::AnyError; use deno_core::futures::FutureExt as _; use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_core::url::Url; use deno_error::JsError; use deno_graph::ModuleErrorKind; use deno_graph::Position; use deno_path_util::resolve_url_or_path; use deno_resolver::cache::ParsedSourceCache; use deno_resolver::graph::ResolveWithGraphError; use deno_resolver::graph::ResolveWithGraphOptions; use deno_resolver::loader::LoadCodeSourceError; use deno_resolver::loader::LoadCodeSourceErrorKind; use deno_resolver::loader::LoadPreparedModuleErrorKind; use deno_resolver::loader::LoadedModuleOrAsset; use deno_resolver::loader::LoadedModuleSource; use deno_resolver::loader::RequestedModuleType; use deno_resolver::npm::managed::ResolvePkgFolderFromDenoModuleError; use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::npm::NpmPackageReqReference; use esbuild_client::EsbuildFlagsBuilder; use esbuild_client::EsbuildService; use esbuild_client::protocol; use esbuild_client::protocol::BuildResponse; use indexmap::IndexMap; use node_resolver::NodeResolutionKind; use node_resolver::ResolutionMode; use node_resolver::errors::PackageNotFoundError; use node_resolver::errors::PackageSubpathResolveError; pub use provider::CliBundleProvider; use sys_traits::EnvCurrentDir; use crate::args::BundleFlags; use crate::args::Flags; use crate::factory::CliFactory; use crate::file_fetcher::CliFileFetcher; use crate::graph_container::MainModuleGraphContainer; use crate::graph_container::ModuleGraphContainer; use crate::graph_container::ModuleGraphUpdatePermit; use crate::module_loader::CliDenoResolverModuleLoader; use crate::module_loader::CliEmitter; use crate::module_loader::ModuleLoadPreparer; use crate::module_loader::PrepareModuleLoadOptions; use crate::node::CliNodeResolver; use crate::npm::CliNpmResolver; use crate::resolver::CliCjsTracker; use crate::resolver::CliResolver; use crate::sys::CliSys; use crate::tools::bundle::externals::ExternalsMatcher; use crate::util::file_watcher::WatcherRestartMode; static DISABLE_HACK: LazyLock = LazyLock::new(|| std::env::var("NO_DENO_BUNDLE_HACK").is_err()); pub async fn prepare_inputs( resolver: &CliResolver, sys: CliSys, npm_resolver: &CliNpmResolver, node_resolver: &CliNodeResolver, init_cwd: &Path, bundle_flags: &BundleFlags, plugin_handler: &mut DenoPluginHandler, ) -> Result { let resolved_entrypoints = resolve_entrypoints(resolver, init_cwd, &bundle_flags.entrypoints)?; // Partition into HTML and non-HTML entrypoints let mut html_paths = Vec::new(); let mut script_entry_urls = Vec::new(); for url in &resolved_entrypoints { if url.as_str().to_lowercase().ends_with(".html") { html_paths.push(url.to_file_path().unwrap()); } else { script_entry_urls.push(url.clone()); } } if html_paths.is_empty() { let _ = plugin_handler .prepare_module_load(&resolved_entrypoints) .await; let roots = resolve_roots(resolved_entrypoints, sys, npm_resolver, node_resolver); let _ = plugin_handler.prepare_module_load(&roots).await; Ok(BundlerInput::Entrypoints( roots.into_iter().map(|e| ("".into(), e.into())).collect(), )) } else { // require an outdir when any HTML is present if bundle_flags.output_dir.is_none() { return Err(deno_core::anyhow::anyhow!( "--outdir is required when bundling HTML entrypoints", )); } if bundle_flags.output_path.is_some() { return Err(deno_core::anyhow::anyhow!( "--output is not supported with HTML entrypoints; use --outdir", )); } // Prepare HTML pages and temp entry modules let mut html_pages = Vec::new(); let mut to_cache_urls = Vec::new(); let mut entries: Vec<(String, String)> = Vec::new(); let mut virtual_modules = VirtualModules::new(); for html_path in &html_paths { let entry = html::load_html_entrypoint(init_cwd, html_path)?; let virtual_module_path = deno_path_util::url_from_file_path(&entry.virtual_module_path)?; let virtual_module_path = virtual_module_path.to_string(); virtual_modules.insert( virtual_module_path.clone(), VirtualModule::new( entry.temp_module.as_bytes().to_vec(), esbuild_client::BuiltinLoader::Js, ), ); for script in &entry.scripts { if let Some(path) = &script.resolved_path { let url = deno_path_util::url_from_file_path(path)?; to_cache_urls.push(url); } } entries.push(("".into(), virtual_module_path)); html_pages.push(entry); } plugin_handler.virtual_modules = Some(Arc::new(virtual_modules)); // Prepare non-HTML entries too let _ = plugin_handler.prepare_module_load(&script_entry_urls).await; let roots = resolve_roots(script_entry_urls, sys, npm_resolver, node_resolver); let _ = plugin_handler.prepare_module_load(&roots).await; for url in roots { entries.push(("".into(), url.into())); } // Pre-cache modules referenced by HTML pages let _ = plugin_handler.prepare_module_load(&to_cache_urls).await; Ok(BundlerInput::EntrypointsWithHtml { entries, html_pages, }) } } pub async fn bundle_init( mut flags: Arc, bundle_flags: &BundleFlags, ) -> Result { { let flags_mut = Arc::make_mut(&mut flags); flags_mut.unstable_config.sloppy_imports = true; } let factory = CliFactory::from_flags(flags.clone()); let esbuild_path = ensure_esbuild_downloaded(&factory).await?; let resolver = factory.resolver().await?.clone(); let module_load_preparer = factory.module_load_preparer().await?.clone(); let root_permissions = factory.root_permissions_container()?; let npm_resolver = factory.npm_resolver().await?; let node_resolver = factory.node_resolver().await?; let cli_options = factory.cli_options()?; let module_loader = factory.resolver_factory()?.module_loader()?; let sys = factory.sys(); let init_cwd = cli_options.initial_cwd().to_path_buf(); let module_graph_container = factory.main_module_graph_container().await?.clone(); let (on_end_tx, on_end_rx) = tokio::sync::mpsc::channel(10); #[allow(clippy::arc_with_non_send_sync)] let mut plugin_handler = Arc::new(DenoPluginHandler { file_fetcher: factory.file_fetcher()?.clone(), resolver: resolver.clone(), module_load_preparer, module_graph_container, permissions: root_permissions.clone(), module_loader: module_loader.clone(), externals_matcher: if bundle_flags.external.is_empty() { None } else { Some(ExternalsMatcher::new(&bundle_flags.external, &init_cwd)) }, on_end_tx, parsed_source_cache: factory.parsed_source_cache()?.clone(), cjs_tracker: factory.cjs_tracker()?.clone(), emitter: factory.emitter()?.clone(), deferred_resolve_errors: Default::default(), virtual_modules: None, }); let input = prepare_inputs( &resolver, sys, npm_resolver, node_resolver, &init_cwd, bundle_flags, Arc::get_mut(&mut plugin_handler).unwrap(), ) .await?; let esbuild = EsbuildService::new( esbuild_path, esbuild::ESBUILD_VERSION, plugin_handler.clone(), Default::default(), ) .await .unwrap(); let client = esbuild.client().clone(); tokio::spawn(async move { let res = esbuild.wait_for_exit().await; log::warn!("esbuild exited: {:?}", res); }); let esbuild_flags = configure_esbuild_flags( bundle_flags, matches!(input, BundlerInput::EntrypointsWithHtml { .. }), ); let bundler = EsbuildBundler::new( client, plugin_handler.clone(), match bundle_flags.watch { true => BundlingMode::Watch, false => BundlingMode::OneShot, }, on_end_rx, init_cwd.clone(), esbuild_flags, input.clone(), ); Ok(bundler) } pub async fn bundle( mut flags: Arc, bundle_flags: BundleFlags, ) -> Result<(), AnyError> { { let flags_mut = Arc::make_mut(&mut flags); flags_mut.unstable_config.sloppy_imports = true; } let bundler = bundle_init(flags.clone(), &bundle_flags).await?; let init_cwd = bundler.cwd.clone(); let start = std::time::Instant::now(); let response = bundler.build().await?; let end = std::time::Instant::now(); let duration = end.duration_since(start); if bundle_flags.watch { if !response.errors.is_empty() || !response.warnings.is_empty() { handle_esbuild_errors_and_warnings( &response, &init_cwd, &bundler.plugin_handler.take_deferred_resolve_errors(), ); if !response.errors.is_empty() { deno_core::anyhow::bail!("bundling failed"); } } return bundle_watch( flags, bundler, bundle_flags.minify, bundle_flags.platform, bundle_flags.output_dir.as_ref().map(Path::new), ) .await; } handle_esbuild_errors_and_warnings( &response, &init_cwd, &bundler.plugin_handler.take_deferred_resolve_errors(), ); if response.errors.is_empty() { let metafile = metafile_from_response(&response)?; let output_infos = process_result( &response, &init_cwd, should_replace_require_shim(bundle_flags.platform), bundle_flags.minify, bundler.input.clone(), bundle_flags.output_dir.as_ref().map(Path::new), )?; if bundle_flags.output_dir.is_some() || bundle_flags.output_path.is_some() { print_finished_message(&metafile, &output_infos, duration)?; } } if !response.errors.is_empty() { deno_core::anyhow::bail!("bundling failed"); } Ok(()) } fn metafile_from_response( response: &BuildResponse, ) -> Result { Ok(serde_json::from_str::( response.metafile.as_deref().ok_or_else(|| { deno_core::anyhow::anyhow!("expected a metafile to be present") })?, )?) } async fn bundle_watch( flags: Arc, bundler: EsbuildBundler, minified: bool, platform: BundlePlatform, output_dir: Option<&Path>, ) -> Result<(), AnyError> { let (initial_roots, always_watch) = match &bundler.input { BundlerInput::Entrypoints(entries) => ( entries .iter() .filter_map(|(_, root)| { let url = Url::parse(root).ok()?; deno_path_util::url_to_file_path(&url).ok() }) .collect::>(), vec![], ), BundlerInput::EntrypointsWithHtml { entries, html_pages, } => { let mut roots = entries .iter() .filter_map(|(_, root)| { let url = Url::parse(root).ok()?; deno_path_util::url_to_file_path(&url).ok() }) .collect::>(); let always = html_pages .iter() .map(|p| p.path.clone()) .collect::>(); roots.extend(always.iter().cloned()); (roots, always) } }; let always_watch = Rc::new(always_watch); let current_roots = Rc::new(RefCell::new(initial_roots.clone())); let input = bundler.input.clone(); let bundler = Rc::new(tokio::sync::Mutex::new(bundler)); let mut print_config = crate::util::file_watcher::PrintConfig::new_with_banner( "Watcher", "Bundle", true, ); print_config.print_finished = false; crate::util::file_watcher::watch_recv( flags, print_config, WatcherRestartMode::Automatic, move |_flags, watcher_communicator, changed_paths| { watcher_communicator.show_path_changed(changed_paths.clone()); let bundler = Rc::clone(&bundler); let current_roots = current_roots.clone(); let input = input.clone(); let always_watch = always_watch.clone(); Ok(async move { let mut bundler = bundler.lock().await; let start = std::time::Instant::now(); if let Some(changed_paths) = changed_paths { bundler .plugin_handler .reload_specifiers(&changed_paths) .await?; } let response = bundler.rebuild().await?; handle_esbuild_errors_and_warnings( &response, &bundler.cwd, &bundler.plugin_handler.take_deferred_resolve_errors(), ); if response.errors.is_empty() { let metafile = metafile_from_response(&response)?; let output_infos = process_result( &response, &bundler.cwd, should_replace_require_shim(platform), minified, input, output_dir, )?; print_finished_message(&metafile, &output_infos, start.elapsed())?; let mut new_watched = get_input_paths_for_watch(&response); new_watched.extend(always_watch.iter().cloned()); *current_roots.borrow_mut() = new_watched.clone(); let _ = watcher_communicator.watch_paths(new_watched); } else { let _ = watcher_communicator.watch_paths(current_roots.borrow().clone()); } Ok(()) }) }, ) .boxed_local() .await?; Ok(()) } pub fn should_replace_require_shim(platform: BundlePlatform) -> bool { *DISABLE_HACK && matches!(platform, BundlePlatform::Deno) } fn get_input_paths_for_watch(response: &BuildResponse) -> Vec { let metafile = serde_json::from_str::( response .metafile .as_deref() .expect("metafile is required for watch mode"), ) .unwrap(); metafile .inputs .keys() .cloned() .map(PathBuf::from) .collect::>() } #[derive(Debug, Clone, Copy, PartialEq)] pub enum BundlingMode { OneShot, Watch, } #[derive(Debug, Clone)] pub enum BundlerInput { Entrypoints(Vec<(String, String)>), EntrypointsWithHtml { entries: Vec<(String, String)>, html_pages: Vec, }, } pub type EsbuildFlags = Vec; pub struct EsbuildBundler { client: esbuild_client::ProtocolClient, plugin_handler: Arc, on_end_rx: tokio::sync::mpsc::Receiver, mode: BundlingMode, cwd: PathBuf, flags: EsbuildFlags, input: BundlerInput, } impl EsbuildBundler { pub fn new( client: esbuild_client::ProtocolClient, plugin_handler: Arc, mode: BundlingMode, on_end_rx: tokio::sync::mpsc::Receiver, cwd: PathBuf, flags: EsbuildFlags, input: BundlerInput, ) -> EsbuildBundler { EsbuildBundler { client, plugin_handler, on_end_rx, mode, cwd, flags, input, } } // When doing a watch build, we're actually enabling the // "context" mode of esbuild. That leaves esbuild running and // waits for a rebuild to be triggered. The initial build request // doesn't actually do anything, it's just registering the args/flags // we're going to use for all of the rebuilds. fn make_build_request(&self) -> protocol::BuildRequest { let entries = match &self.input { BundlerInput::Entrypoints(entries) => entries.clone(), BundlerInput::EntrypointsWithHtml { entries, .. } => entries.clone(), }; protocol::BuildRequest { entries, key: 0, flags: self.flags.clone(), write: false, stdin_contents: None.into(), stdin_resolve_dir: None.into(), abs_working_dir: self.cwd.to_string_lossy().into_owned(), context: matches!(self.mode, BundlingMode::Watch), mangle_cache: None, node_paths: vec![], plugins: Some(vec![protocol::BuildPlugin { name: "deno".into(), on_start: false, on_end: matches!(self.mode, BundlingMode::Watch), on_resolve: (vec![protocol::OnResolveSetupOptions { id: 0, filter: ".*".into(), namespace: "".into(), }]), on_load: vec![protocol::OnLoadSetupOptions { id: 0, filter: ".*".into(), namespace: "".into(), }], }]), } } async fn build(&self) -> Result { let response: BuildResponse = self .client .send_build_request(self.make_build_request()) .await .unwrap() .map_err(|e| message_to_error(&e, &self.cwd))?; Ok(response) } async fn rebuild(&mut self) -> Result { match self.mode { BundlingMode::OneShot => { panic!("rebuild not supported for one-shot mode") } BundlingMode::Watch => { log::trace!("sending rebuild request"); let _response = self .client .send_rebuild_request(0) .await .unwrap() .map_err(|e| message_to_error(&e, &self.cwd))?; let response = self.on_end_rx.recv().await.unwrap(); Ok(response.into()) } } } } fn message_to_error( message: &esbuild_client::protocol::Message, current_dir: &Path, ) -> AnyError { deno_core::anyhow::anyhow!("{}", format_message(message, current_dir)) } // TODO(nathanwhit): MASSIVE HACK // See tests::specs::bundle::requires_node_builtin for why this is needed. // Without this hack, that test would fail with "Dynamic require of "util" is not supported" fn replace_require_shim(contents: &str, minified: bool) -> String { if minified { let re = lazy_regex::regex!( r#"var (\w+)\s*=\((\w+)\s*=>typeof require<"u"\?require:typeof Proxy<"u"\?new Proxy\((\w+)\,\{get:\(\w+,\w+\)=>\(typeof require<"u"\?require:\w+\)\[l\]\}\):(\w+)\)\(function\(\w+\)\{if\(typeof require<"u"\)return require\.apply\(this\,arguments\);throw Error\('Dynamic require of "'\+\w+\+'" is not supported'\)\}\);"# ); re.replace(contents, |c: ®ex::Captures<'_>| { let var_name = c.get(1).unwrap().as_str(); format!("import{{createRequire}} from \"node:module\";var {var_name}=createRequire(import.meta.url);") }).into_owned() } else { let re = lazy_regex::regex!( r#"var __require = (/\* @__PURE__ \*/)?\s*\(\(\w+\) => typeof require !== "undefined" \? require : typeof Proxy !== "undefined" \? new Proxy\(\w+, \{\s* get: \(\w+, \w+\) => \(typeof require !== "undefined" \? require : \w+\)\[\w+\]\s*\}\) : \w+\)\(function\(\w+\) \{\s* if \(typeof require !== "undefined"\) return require\.apply\(this, arguments\);\s* throw Error\('Dynamic require of "' \+ \w+ \+ '" is not supported'\);\s*\}\);"# ); re.replace_all( contents, r#"import { createRequire } from "node:module"; var __require = createRequire(import.meta.url); "#, ) .into_owned() } } fn format_location( location: &esbuild_client::protocol::Location, current_dir: &Path, ) -> String { let url = deno_path_util::resolve_url_or_path(location.file.as_str(), current_dir) .map(|url| deno_terminal::colors::cyan(url.into())) .unwrap_or(deno_terminal::colors::cyan(location.file.clone())); format!( "{}:{}:{}", url, deno_terminal::colors::yellow(location.line), deno_terminal::colors::yellow(location.column) ) } fn format_note( note: &esbuild_client::protocol::Note, current_dir: &Path, ) -> String { format!( "{}: {}{}", deno_terminal::colors::magenta("note"), note.text, if let Some(location) = ¬e.location { format!("\n {}", format_location(location, current_dir)) } else { String::new() } ) } // not very efficient, but it's only for error messages fn add_indent(s: &str, indent: &str) -> String { let lines = s .lines() .map(|line| format!("{}{}", indent, line)) .collect::>(); lines.join("\n") } fn format_message( message: &esbuild_client::protocol::Message, current_dir: &Path, ) -> String { format!( "{}{}{}{}", message.text, if message.id.is_empty() { String::new() } else { format!("[{}] ", message.id) }, if let Some(location) = &message.location { if !message.text.contains(" at ") { format!("\n at {}", format_location(location, current_dir)) } else { String::new() } } else { String::new() }, if !message.notes.is_empty() { let mut s = String::new(); for note in &message.notes { s.push('\n'); s.push_str(&add_indent(&format_note(note, current_dir), " ")); } s } else { String::new() } ) } #[derive(Debug, thiserror::Error, JsError)] #[class(generic)] enum BundleError { #[error(transparent)] Resolver(#[from] deno_resolver::graph::ResolveWithGraphError), #[error(transparent)] Url(#[from] deno_core::url::ParseError), #[error(transparent)] ResolveNpmPkg(#[from] ResolvePkgFolderFromDenoModuleError), #[error(transparent)] SubpathResolve(#[from] PackageSubpathResolveError), #[error(transparent)] PathToUrlError(#[from] deno_path_util::PathToUrlError), #[error(transparent)] UrlToPathError(#[from] deno_path_util::UrlToFilePathError), #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] ResolveUrlOrPathError(#[from] deno_path_util::ResolveUrlOrPathError), #[error(transparent)] PrepareModuleLoad(#[from] crate::module_loader::PrepareModuleLoadError), #[error(transparent)] ResolveReqWithSubPath(#[from] deno_resolver::npm::ResolveReqWithSubPathError), #[error(transparent)] PackageReqReferenceParse( #[from] deno_semver::package::PackageReqReferenceParseError, ), #[allow(dead_code)] #[error("Http cache error")] HttpCache, } fn requested_type_from_map( map: &IndexMap, ) -> RequestedModuleType<'_> { let type_ = map.get("type").map(|s| s.as_str()); match type_ { Some("json") => RequestedModuleType::Json, Some("bytes") => RequestedModuleType::Bytes, Some("text") => RequestedModuleType::Text, Some(other) => RequestedModuleType::Other(other), None => RequestedModuleType::None, } } pub struct VirtualModule { contents: Vec, loader: esbuild_client::BuiltinLoader, } impl VirtualModule { pub fn new(contents: Vec, loader: esbuild_client::BuiltinLoader) -> Self { Self { contents, loader } } } pub struct VirtualModules { modules: IndexMap, } impl VirtualModules { pub fn new() -> Self { Self { modules: IndexMap::new(), } } pub fn insert(&mut self, path: String, contents: VirtualModule) { self.modules.insert(path, contents); } pub fn get(&self, path: &str) -> Option<&VirtualModule> { self.modules.get(path) } pub fn contains(&self, path: &str) -> bool { self.modules.contains_key(path) } } pub struct DeferredResolveError { path: String, error: ResolveWithGraphError, } pub struct DenoPluginHandler { file_fetcher: Arc, resolver: Arc, module_load_preparer: Arc, module_graph_container: Arc, permissions: PermissionsContainer, module_loader: Arc, externals_matcher: Option, on_end_tx: tokio::sync::mpsc::Sender, deferred_resolve_errors: Arc>>, virtual_modules: Option>, parsed_source_cache: Arc, cjs_tracker: Arc, emitter: Arc, } impl DenoPluginHandler { fn take_deferred_resolve_errors(&self) -> Vec { std::mem::take(&mut *self.deferred_resolve_errors.lock()) } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case")] enum PluginImportKind { EntryPoint, ImportStatement, RequireCall, DynamicImport, RequireResolve, ImportRule, ComposesFrom, UrlToken, } impl From for PluginImportKind { fn from(kind: protocol::ImportKind) -> Self { match kind { protocol::ImportKind::EntryPoint => PluginImportKind::EntryPoint, protocol::ImportKind::ImportStatement => { PluginImportKind::ImportStatement } protocol::ImportKind::RequireCall => PluginImportKind::RequireCall, protocol::ImportKind::DynamicImport => PluginImportKind::DynamicImport, protocol::ImportKind::RequireResolve => PluginImportKind::RequireResolve, protocol::ImportKind::ImportRule => PluginImportKind::ImportRule, protocol::ImportKind::ComposesFrom => PluginImportKind::ComposesFrom, protocol::ImportKind::UrlToken => PluginImportKind::UrlToken, } } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] struct PluginOnResolveArgs { path: String, importer: Option, kind: PluginImportKind, namespace: Option, resolve_dir: Option, with: IndexMap, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] struct PluginOnLoadArgs { path: String, namespace: String, suffix: String, with: IndexMap, } #[async_trait::async_trait(?Send)] impl esbuild_client::PluginHandler for DenoPluginHandler { async fn on_resolve( &self, args: esbuild_client::OnResolveArgs, ) -> Result, AnyError> { log::debug!("{}: {args:?}", deno_terminal::colors::cyan("on_resolve")); if let Some(virtual_modules) = &self.virtual_modules && virtual_modules.contains(&args.path) { return Ok(Some(esbuild_client::OnResolveResult { path: Some(args.path), plugin_name: Some("deno".to_string()), namespace: Some("deno".to_string()), ..Default::default() })); } if let Some(matcher) = &self.externals_matcher && matcher.is_pre_resolve_match(&args.path) { return Ok(Some(esbuild_client::OnResolveResult { external: Some(true), path: Some(args.path), plugin_name: Some("deno".to_string()), plugin_data: None, ..Default::default() })); } let result = self.bundle_resolve( &args.path, args.importer.as_deref(), args.resolve_dir.as_deref(), args.kind, args.with, ); let result = match result { Ok(r) => r, Err(e) => { return Ok(Some(esbuild_client::OnResolveResult { errors: Some(vec![esbuild_client::protocol::PartialMessage { id: "deno_error".into(), plugin_name: "deno".into(), text: e.to_string(), ..Default::default() }]), ..Default::default() })); } }; Ok(result.map(|r| { // TODO(nathanwhit): remap the resolved path to be relative // to the output file. It will be tricky to figure out which // output file this import will end up in. We may have to use the metafile and rewrite at the end let is_external = r.starts_with("node:") || self .externals_matcher .as_ref() .map(|matcher| matcher.is_post_resolve_match(&r)) .unwrap_or(false); esbuild_client::OnResolveResult { namespace: if r.starts_with("jsr:") || r.starts_with("https:") || r.starts_with("http:") || r.starts_with("data:") { Some("deno".into()) } else { None }, external: Some(is_external), path: Some(r), plugin_name: Some("deno".to_string()), plugin_data: None, ..Default::default() } })) } async fn on_load( &self, args: esbuild_client::OnLoadArgs, ) -> Result, AnyError> { log::debug!("{}: {args:?}", deno_terminal::colors::cyan("on_load")); if let Some(virtual_modules) = &self.virtual_modules && let Some(module) = virtual_modules.get(&args.path) { return Ok(Some(esbuild_client::OnLoadResult { contents: Some(module.contents.clone()), loader: Some(module.loader), ..Default::default() })); } let result = self .bundle_load(&args.path, &requested_type_from_map(&args.with)) .await; let result = match result { Ok(r) => r, Err(e) => { if e.is_unsupported_media_type() { return Ok(None); } return Ok(Some(esbuild_client::OnLoadResult { errors: Some(vec![esbuild_client::protocol::PartialMessage { plugin_name: "deno".into(), text: e.to_string(), ..Default::default() }]), plugin_name: Some("deno".to_string()), ..Default::default() })); } }; log::trace!( "{}: {:?}", deno_terminal::colors::magenta("on_load"), result.as_ref().map(|(code, loader)| format!( "{}: {:?}", String::from_utf8_lossy(code), loader )) ); if let Some((code, loader)) = result { Ok(Some(esbuild_client::OnLoadResult { contents: Some(code), loader: Some(loader), ..Default::default() })) } else { Ok(None) } } async fn on_start( &self, _args: esbuild_client::OnStartArgs, ) -> Result, AnyError> { Ok(None) } async fn on_end( &self, _args: esbuild_client::OnEndArgs, ) -> Result, AnyError> { self.on_end_tx.send(_args).await?; Ok(None) } } fn import_kind_to_resolution_mode( kind: esbuild_client::protocol::ImportKind, ) -> ResolutionMode { match kind { protocol::ImportKind::EntryPoint | protocol::ImportKind::ImportStatement | protocol::ImportKind::ComposesFrom | protocol::ImportKind::DynamicImport | protocol::ImportKind::ImportRule | protocol::ImportKind::UrlToken => ResolutionMode::Import, protocol::ImportKind::RequireCall | protocol::ImportKind::RequireResolve => ResolutionMode::Require, } } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum BundleLoadError { #[class(inherit)] #[error(transparent)] Fetch(#[from] deno_resolver::file_fetcher::FetchError), #[class(inherit)] #[error(transparent)] LoadCodeSource(#[from] LoadCodeSourceError), #[class(inherit)] #[error(transparent)] ResolveUrlOrPath(#[from] deno_path_util::ResolveUrlOrPathError), #[class(inherit)] #[error(transparent)] ResolveWithGraph(#[from] ResolveWithGraphError), #[class(generic)] #[error("Wasm modules are not implemented in deno bundle.")] WasmUnsupported, #[class(generic)] #[error("UTF-8 conversion error")] Utf8(#[from] std::str::Utf8Error), #[class(generic)] #[error("UTF-8 conversion error")] StringUtf8(#[from] std::string::FromUtf8Error), #[class(generic)] #[error("Parse error")] Parse(#[from] deno_ast::ParseDiagnostic), #[class(generic)] #[error("Emit error")] Emit(#[from] deno_ast::EmitError), #[class(generic)] #[error("Prepare module load error")] PrepareModuleLoad(#[from] crate::module_loader::PrepareModuleLoadError), #[class(generic)] #[error("Package.json load error")] PackageJsonLoadError(#[from] node_resolver::errors::PackageJsonLoadError), #[class(generic)] #[error("Emit parsed source helper error")] EmitParsedSourceHelperError( #[from] deno_resolver::emit::EmitParsedSourceHelperError, ), } impl BundleLoadError { pub fn is_unsupported_media_type(&self) -> bool { match self { BundleLoadError::LoadCodeSource(e) => match e.as_kind() { LoadCodeSourceErrorKind::LoadPreparedModule(e) => match e.as_kind() { LoadPreparedModuleErrorKind::Graph(e) => matches!( e.error.as_kind(), ModuleErrorKind::UnsupportedMediaType { .. }, ), _ => false, }, _ => false, }, _ => false, } } } fn maybe_ignorable_resolution_error( error: &ResolveWithGraphError, ) -> Option { if let deno_resolver::graph::ResolveWithGraphErrorKind::Resolve(e) = error.as_kind() && let deno_resolver::DenoResolveErrorKind::Node(node_err) = e.as_kind() && let node_resolver::errors::NodeResolveErrorKind::PackageResolve(pkg_err) = node_err.as_kind() && let node_resolver::errors::PackageResolveErrorKind::PackageFolderResolve( pkg_folder_err, ) = pkg_err.as_kind() && let node_resolver::errors::PackageFolderResolveErrorKind::PackageNotFound( PackageNotFoundError { package_name, .. }, ) = pkg_folder_err.as_kind() { Some(package_name.to_string()) } else if let deno_resolver::graph::ResolveWithGraphErrorKind::Resolution( deno_graph::ResolutionError::ResolverError { error: resolve_error, specifier, .. }, ) = error.as_kind() && let deno_graph::source::ResolveError::ImportMap(import_map_err) = resolve_error.deref() && let import_map::ImportMapErrorKind::UnmappedBareSpecifier(..) = import_map_err.as_kind() { Some(specifier.to_string()) } else { None } } impl DenoPluginHandler { async fn reload_specifiers( &self, specifiers: &[PathBuf], ) -> Result<(), AnyError> { let mut graph_permit = self.module_graph_container.acquire_update_permit().await; let graph = graph_permit.graph_mut(); let mut specifiers_vec = Vec::with_capacity(specifiers.len()); for specifier in specifiers { let specifier = deno_path_util::url_from_file_path(specifier)?; specifiers_vec.push(specifier); } self .module_load_preparer .reload_specifiers(graph, specifiers_vec, false, self.permissions.clone()) .await?; graph_permit.commit(); Ok(()) } #[allow(clippy::result_large_err)] fn bundle_resolve( &self, path: &str, importer: Option<&str>, resolve_dir: Option<&str>, kind: esbuild_client::protocol::ImportKind, with: IndexMap, ) -> Result, BundleError> { log::debug!( "bundle_resolve: {:?} {:?} {:?} {:?} {:?}", path, importer, resolve_dir, kind, with ); let mut resolve_dir = resolve_dir.unwrap_or("").to_string(); let resolver = self.resolver.clone(); if !resolve_dir.ends_with(std::path::MAIN_SEPARATOR) { resolve_dir.push(std::path::MAIN_SEPARATOR); } let resolve_dir_path = Path::new(&resolve_dir); let mut referrer = resolve_url_or_path(importer.unwrap_or(""), resolve_dir_path) .unwrap_or_else(|_| { Url::from_directory_path(std::env::current_dir().unwrap()).unwrap() }); if referrer.scheme() == "file" { let pth = referrer.to_file_path().unwrap(); if (pth.is_dir()) && !pth.ends_with(std::path::MAIN_SEPARATOR_STR) { referrer.set_path(&format!( "{}{}", referrer.path(), std::path::MAIN_SEPARATOR )); } } log::debug!( "{}: {} {} {} {:?}", deno_terminal::colors::magenta("op_bundle_resolve"), path, resolve_dir, referrer, import_kind_to_resolution_mode(kind) ); let graph = self.module_graph_container.graph(); let result = resolver.resolve_with_graph( &graph, path, &referrer, Position::new(0, 0), ResolveWithGraphOptions { mode: import_kind_to_resolution_mode(kind), kind: NodeResolutionKind::Execution, maintain_npm_specifiers: false, }, ); log::debug!( "{}: {:?}", deno_terminal::colors::cyan("op_bundle_resolve result"), result ); match result { Ok(specifier) => Ok(Some(file_path_or_url(specifier)?)), Err(e) => { log::debug!("{}: {:?}", deno_terminal::colors::red("error"), e); if let Some(specifier) = maybe_ignorable_resolution_error(&e) { log::debug!( "{}: resolution failed, but maybe ignorable", deno_terminal::colors::red("warn") ); self .deferred_resolve_errors .lock() .push(DeferredResolveError { path: specifier, error: e, }); // we return None here because this lets esbuild choose to ignore the failure // for fallible imports/requires return Ok(None); } Err(BundleError::Resolver(e)) } } } async fn prepare_module_load( &self, specifiers: &[ModuleSpecifier], ) -> Result<(), BundleLoadError> { let mut graph_permit = self.module_graph_container.acquire_update_permit().await; let graph: &mut deno_graph::ModuleGraph = graph_permit.graph_mut(); self .module_load_preparer .prepare_module_load( graph, specifiers, PrepareModuleLoadOptions { is_dynamic: false, lib: TsTypeLib::default(), permissions: self.permissions.clone(), ext_overwrite: None, allow_unknown_media_types: true, skip_graph_roots_validation: true, }, ) .await?; graph_permit.commit(); Ok(()) } async fn bundle_load( &self, specifier: &str, requested_type: &RequestedModuleType<'_>, ) -> Result, esbuild_client::BuiltinLoader)>, BundleLoadError> { log::debug!( "{}: {:?} {:?}", deno_terminal::colors::magenta("bundle_load"), specifier, requested_type ); let specifier = deno_path_util::resolve_url_or_path( specifier, Path::new(""), // should be absolute already, feels kind of hacky though )?; let (specifier, media_type) = if let RequestedModuleType::Bytes = requested_type { (specifier, MediaType::Unknown) } else if let RequestedModuleType::Text = requested_type { (specifier, MediaType::Unknown) } else if let Some((specifier, media_type, _)) = self.specifier_and_type_from_graph(&specifier)? { (specifier, media_type) } else { log::debug!( "{}: no specifier and type from graph for {}", deno_terminal::colors::yellow("warn"), specifier ); if specifier.scheme() == "data" { return Ok(Some(( specifier.to_string().as_bytes().to_vec(), esbuild_client::BuiltinLoader::DataUrl, ))); } let (media_type, _) = deno_media_type::resolve_media_type_and_charset_from_content_type( &specifier, None, ); if media_type == deno_media_type::MediaType::Unknown { return Ok(None); } (specifier, media_type) }; let graph = self.module_graph_container.graph(); let module_or_asset = self .module_loader .load(&graph, &specifier, None, requested_type) .await; let module_or_asset = match module_or_asset { Ok(module_or_asset) => module_or_asset, Err(e) => match e.as_kind() { LoadCodeSourceErrorKind::LoadUnpreparedModule(_) => { let file = self .file_fetcher .fetch(&specifier, &self.permissions) .await?; let media_type = MediaType::from_specifier_and_headers( &specifier, file.maybe_headers.as_ref(), ); match requested_type { RequestedModuleType::Text | RequestedModuleType::Bytes => { return self .create_module_response( &graph, &specifier, media_type, &file.source, Some(requested_type), ) .await .map(Some); } RequestedModuleType::None | RequestedModuleType::Json | RequestedModuleType::Other(_) => { if media_type.is_emittable() { let str = String::from_utf8_lossy(&file.source); let value = str.into(); let source = self .maybe_transpile(&file.url, media_type, &value, None) .await?; return self .create_module_response( &graph, &file.url, media_type, source.as_bytes(), Some(requested_type), ) .await .map(Some); } else { return self .create_module_response( &graph, &file.url, media_type, &file.source, Some(requested_type), ) .await .map(Some); } } } } _ => return Err(e.into()), }, }; let loaded_code = match module_or_asset { LoadedModuleOrAsset::Module(loaded_module) => loaded_module.source, LoadedModuleOrAsset::ExternalAsset { specifier, statically_analyzable: _, } => LoadedModuleSource::ArcBytes( self .file_fetcher .fetch(&specifier, &self.permissions) .await? .source, ), }; Ok(Some( self .create_module_response( &graph, &specifier, media_type, loaded_code.as_bytes(), Some(requested_type), ) .await?, )) } async fn create_module_response( &self, graph: &deno_graph::ModuleGraph, specifier: &Url, media_type: MediaType, source: &[u8], requested_type: Option<&RequestedModuleType<'_>>, ) -> Result<(Vec, esbuild_client::BuiltinLoader), BundleLoadError> { match requested_type { Some(RequestedModuleType::Text) => { return Ok((source.to_vec(), esbuild_client::BuiltinLoader::Text)); } Some(RequestedModuleType::Bytes) => { return Ok((source.to_vec(), esbuild_client::BuiltinLoader::Binary)); } Some(RequestedModuleType::Json) => { return Ok((source.to_vec(), esbuild_client::BuiltinLoader::Json)); } Some(RequestedModuleType::Other(_) | RequestedModuleType::None) | None => {} } if matches!( media_type, MediaType::JavaScript | MediaType::TypeScript | MediaType::Mjs | MediaType::Mts | MediaType::Cjs | MediaType::Cts | MediaType::Jsx | MediaType::Tsx ) && !graph.roots.contains(specifier) { let module_graph_container = self.module_graph_container.clone(); let specifier = specifier.clone(); let code = source.to_vec(); let code = tokio::task::spawn_blocking(move || { Self::apply_transform( &module_graph_container, &specifier, media_type, &String::from_utf8(code)?, ) }) .await .unwrap()?; Ok((code.into_bytes(), media_type_to_loader(media_type))) } else { Ok((source.to_vec(), media_type_to_loader(media_type))) } } async fn maybe_transpile( &self, specifier: &Url, media_type: MediaType, source: &Arc, is_known_script: Option, ) -> Result, BundleLoadError> { let parsed_source = self.parsed_source_cache.remove_or_parse_module( specifier, media_type, source.clone(), )?; let is_cjs = if let Some(is_known_script) = is_known_script { self.cjs_tracker.is_cjs_with_known_is_script( specifier, media_type, is_known_script, )? } else { self.cjs_tracker.is_maybe_cjs(specifier, media_type)? && parsed_source.compute_is_script() }; let module_kind = ModuleKind::from_is_cjs(is_cjs); let source = self .emitter .maybe_emit_parsed_source(parsed_source, module_kind) .await?; Ok(source) } #[allow(clippy::result_large_err)] fn apply_transform( module_graph_container: &MainModuleGraphContainer, specifier: &ModuleSpecifier, media_type: deno_ast::MediaType, code: &str, ) -> Result { let mut transform = transform::BundleImportMetaMainTransform::new( module_graph_container.graph().roots.contains(specifier), ); let parsed_source = deno_ast::parse_program_with_post_process( deno_ast::ParseParams { specifier: specifier.clone(), text: code.into(), media_type, capture_tokens: false, scope_analysis: false, maybe_syntax: None, }, |mut program, _| { use deno_ast::swc::ecma_visit::VisitMut; transform.visit_mut_program(&mut program); program }, )?; let code = deno_ast::emit( parsed_source.program_ref(), &parsed_source.comments().as_single_threaded(), &deno_ast::SourceMap::default(), &EmitOptions { source_map: deno_ast::SourceMapOption::None, ..Default::default() }, )?; Ok(code.text) } #[allow(clippy::result_large_err)] fn specifier_and_type_from_graph( &self, specifier: &ModuleSpecifier, ) -> Result< Option<( ModuleSpecifier, deno_ast::MediaType, esbuild_client::BuiltinLoader, )>, BundleLoadError, > { let graph = self.module_graph_container.graph(); let Some(module) = graph.get(specifier) else { return Ok(None); }; let (specifier, media_type, loader) = match module { deno_graph::Module::Js(js_module) => ( js_module.specifier.clone(), js_module.media_type, media_type_to_loader(js_module.media_type), ), deno_graph::Module::Json(json_module) => ( json_module.specifier.clone(), deno_ast::MediaType::Json, esbuild_client::BuiltinLoader::Json, ), deno_graph::Module::Wasm(_) => { return Err(BundleLoadError::WasmUnsupported); } deno_graph::Module::Npm(module) => { let url = self.resolver.resolve_npm_nv_ref( &module.nv_reference, None, ResolutionMode::Import, NodeResolutionKind::Execution, )?; let (media_type, _charset) = deno_media_type::resolve_media_type_and_charset_from_content_type( &url, None, ); (url, media_type, media_type_to_loader(media_type)) } deno_graph::Module::Node(_) => { return Ok(None); } deno_graph::Module::External(_) => { return Ok(None); } }; Ok(Some((specifier, media_type, loader))) } } fn file_path_or_url( url: Url, ) -> Result { if url.scheme() == "file" { Ok( deno_path_util::url_to_file_path(&url)? .to_string_lossy() .into(), ) } else { Ok(url.into()) } } fn media_type_to_loader( media_type: deno_media_type::MediaType, ) -> esbuild_client::BuiltinLoader { use deno_ast::MediaType::*; match media_type { JavaScript | Cjs | Mjs | Mts => esbuild_client::BuiltinLoader::Js, TypeScript | Cts | Dts | Dmts | Dcts => esbuild_client::BuiltinLoader::Ts, Jsx | Tsx => esbuild_client::BuiltinLoader::Jsx, Css => esbuild_client::BuiltinLoader::Css, Json => esbuild_client::BuiltinLoader::Json, SourceMap => esbuild_client::BuiltinLoader::Text, Html => esbuild_client::BuiltinLoader::Text, Sql => esbuild_client::BuiltinLoader::Text, Wasm => esbuild_client::BuiltinLoader::Binary, Unknown => esbuild_client::BuiltinLoader::Binary, // _ => esbuild_client::BuiltinLoader::External, } } fn resolve_url_or_path_absolute( specifier: &str, current_dir: &Path, ) -> Result { if deno_path_util::specifier_has_uri_scheme(specifier) { Ok(Url::parse(specifier)?) } else { let path = current_dir.join(specifier); let path = deno_path_util::normalize_path(Cow::Owned(path)); let path = path.canonicalize()?; Ok(deno_path_util::url_from_file_path(&path)?) } } fn resolve_entrypoints( resolver: &CliResolver, init_cwd: &Path, entrypoints: &[String], ) -> Result, AnyError> { let entrypoints = entrypoints .iter() .map(|e| resolve_url_or_path_absolute(e, init_cwd)) .collect::, _>>()?; let init_cwd_url = Url::from_directory_path(init_cwd).unwrap(); let mut resolved = Vec::with_capacity(entrypoints.len()); for e in &entrypoints { let r = resolver.resolve( e.as_str(), &init_cwd_url, Position::new(0, 0), ResolutionMode::Import, NodeResolutionKind::Execution, )?; resolved.push(r); } Ok(resolved) } fn resolve_roots( entrypoints: Vec, sys: CliSys, npm_resolver: &CliNpmResolver, node_resolver: &CliNodeResolver, ) -> Vec { let mut roots = Vec::with_capacity(entrypoints.len()); for url in entrypoints { let root = match NpmPackageReqReference::from_specifier(&url) { Ok(v) => { let referrer = ModuleSpecifier::from_directory_path(sys.env_current_dir().unwrap()) .unwrap(); let package_folder = npm_resolver .resolve_pkg_folder_from_deno_module_req(v.req(), &referrer) .unwrap(); let Ok(main_module) = node_resolver.resolve_binary_export(&package_folder, v.sub_path()) else { roots.push(url); continue; }; Url::from_file_path(&main_module).unwrap() } _ => url, }; roots.push(root) } roots } /// Ensure that an Esbuild binary for the current os/arch is downloaded /// and ready to use and then return path to it. async fn ensure_esbuild_downloaded( factory: &CliFactory, ) -> Result { let installer_factory = factory.npm_installer_factory()?; let deno_dir = factory.deno_dir()?; let npmrc = factory.npmrc()?; let npm_registry_info = installer_factory.registry_info_provider()?; let resolver_factory = factory.resolver_factory()?; let workspace_factory = resolver_factory.workspace_factory(); let esbuild_path = esbuild::ensure_esbuild( deno_dir, npmrc, npm_registry_info, workspace_factory.workspace_npm_link_packages()?, installer_factory.tarball_cache()?, factory.npm_cache()?, ) .await?; Ok(esbuild_path) } fn configure_esbuild_flags( bundle_flags: &BundleFlags, is_html: bool, ) -> Vec { let mut builder = EsbuildFlagsBuilder::default(); builder .bundle(bundle_flags.inline_imports) .minify(bundle_flags.minify) .splitting(bundle_flags.code_splitting) .externals(bundle_flags.external.clone()) .tree_shaking(true) .format(match bundle_flags.format { BundleFormat::Esm => esbuild_client::Format::Esm, BundleFormat::Cjs => esbuild_client::Format::Cjs, BundleFormat::Iife => esbuild_client::Format::Iife, }) .packages(match bundle_flags.packages { PackageHandling::External => esbuild_client::PackagesHandling::External, PackageHandling::Bundle => esbuild_client::PackagesHandling::Bundle, }); if let Some(sourcemap_type) = bundle_flags.sourcemap { builder.sourcemap(match sourcemap_type { SourceMapType::Linked => esbuild_client::Sourcemap::Linked, SourceMapType::Inline => esbuild_client::Sourcemap::Inline, SourceMapType::External => esbuild_client::Sourcemap::External, }); } if let Some(outdir) = bundle_flags.output_dir.clone() { builder.outdir(outdir); } else if let Some(output_path) = bundle_flags.output_path.clone() { builder.outfile(output_path); } builder.metafile(true); if is_html { builder.platform(esbuild_client::Platform::Browser); builder.splitting(true); builder.entry_names("[dir]/[name]-[hash]"); builder.chunk_names("[dir]/[name]-[hash]"); builder.asset_names("[dir]/[name]-[hash]"); builder.metafile(true); } match bundle_flags.platform { deno_bundle_runtime::BundlePlatform::Browser => { builder.platform(esbuild_client::Platform::Browser); } deno_bundle_runtime::BundlePlatform::Deno => {} } builder.build() } // extract the path from a message like "Could not resolve "path/to/file.ts"" fn esbuild_resolve_error_path( error: &esbuild_client::protocol::Message, ) -> Option { let re = lazy_regex::regex!(r#"^Could not resolve "([^"]+)"#); re.captures(error.text.as_str()) .map(|captures| captures.get(1).unwrap().as_str().to_string()) } fn handle_esbuild_errors_and_warnings( response: &BuildResponse, init_cwd: &Path, deferred_resolve_errors: &[DeferredResolveError], ) { for error in &response.errors { if let Some(path) = esbuild_resolve_error_path(error) && let Some(deferred_resolve_error) = deferred_resolve_errors.iter().find(|e| e.path == path) { let error = protocol::Message { // use our own error message, as it has more detail text: deferred_resolve_error.error.to_string(), ..error.clone() }; log::error!( "{}: {}", deno_terminal::colors::red_bold("error"), format_message(&error, init_cwd) ); continue; } log::error!( "{}: {}", deno_terminal::colors::red_bold("error"), format_message(error, init_cwd) ); } for warning in &response.warnings { log::warn!( "{}: {}", deno_terminal::colors::yellow("bundler warning"), format_message(warning, init_cwd) ); } } pub struct OutputFileInfo { relative_path: PathBuf, size: usize, is_js: bool, } pub struct ProcessedContents { contents: Option>, is_js: bool, } fn is_js(path: &Path) -> bool { if let Some(ext) = path.extension() { matches!( ext.to_string_lossy().as_ref(), "js" | "mjs" | "cjs" | "jsx" | "ts" | "tsx" | "mts" | "cts" | "dts" ) } else { false } } pub fn maybe_process_contents( file: &OutputFile<'_>, should_replace_require_shim: bool, minified: bool, ) -> Result { let path = &file.path; let is_js = is_js(path) || path.ends_with(""); if is_js { let string = str::from_utf8(&file.contents)?; let string = if should_replace_require_shim { replace_require_shim(string, minified) } else { string.to_string() }; Ok(ProcessedContents { contents: Some(string.into_bytes()), is_js, }) } else { Ok(ProcessedContents { contents: None, is_js, }) } } pub struct OutputFile<'a> { pub path: PathBuf, pub contents: Cow<'a, [u8]>, pub hash: Option, } impl<'a> std::fmt::Debug for OutputFile<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("OutputFile") .field("path", &self.path) .field("hash", &self.hash) .finish() } } impl<'a> From<&'a esbuild_client::protocol::BuildOutputFile> for OutputFile<'a> { fn from(file: &'a esbuild_client::protocol::BuildOutputFile) -> Self { OutputFile { path: PathBuf::from(&file.path), contents: Cow::Borrowed(&file.contents), hash: Some(file.hash.clone()), } } } impl<'a> From for OutputFile<'a> { fn from(file: esbuild_client::protocol::BuildOutputFile) -> Self { OutputFile { path: PathBuf::from(&file.path), contents: Cow::Owned(file.contents), hash: Some(file.hash), } } } pub fn collect_output_files<'a>( response_output_files: Option<&'a [protocol::BuildOutputFile]>, cwd: &Path, input: BundlerInput, outdir: Option<&Path>, ) -> Result>, AnyError> { let outdir = if let Some(outdir) = outdir { if outdir.is_absolute() { Some(outdir.to_path_buf()) } else { Some(cwd.join(outdir)) } } else { None }; let mut output_files: Vec = response_output_files .map(|fs| { fs.iter() .map(|f| OutputFile { path: PathBuf::from(&f.path), contents: Cow::Borrowed(&f.contents), hash: Some(f.hash.clone()), }) .collect::>() }) .unwrap_or_default(); if let BundlerInput::EntrypointsWithHtml { entries: _, html_pages, } = input { let outdir = outdir.ok_or_else(|| { deno_core::anyhow::anyhow!( "--outdir is required when bundling HTML entrypoints", ) })?; let mut html_output_files = html::HtmlOutputFiles::new(&mut output_files); for page in html_pages { page.patch_html_with_response(cwd, &outdir, &mut html_output_files)?; } } Ok(output_files) } pub fn process_result( response: &BuildResponse, cwd: &Path, should_replace_require_shim: bool, minified: bool, input: BundlerInput, outdir: Option<&Path>, ) -> Result, AnyError> { let output_files = collect_output_files(response.output_files.as_deref(), cwd, input, outdir)?; let mut exists_cache = std::collections::HashSet::new(); let mut output_infos = Vec::new(); for file in output_files.iter() { let processed_contents = maybe_process_contents(file, should_replace_require_shim, minified)?; let path = Path::new(&file.path); let relative_path = pathdiff::diff_paths(path, cwd).unwrap_or_else(|| path.to_path_buf()); let is_js = processed_contents.is_js; let bytes: Cow<'_, [u8]> = processed_contents .contents .map(Cow::Owned) .unwrap_or_else(|| Cow::Borrowed(file.contents.as_ref())); if file.path.ends_with("") { crate::display::write_to_stdout_ignore_sigpipe(bytes.as_ref())?; continue; } if let Some(parent) = path.parent() && !exists_cache.contains(parent) { if !parent.exists() { std::fs::create_dir_all(parent)?; } exists_cache.insert(parent.to_path_buf()); } output_infos.push(OutputFileInfo { relative_path, size: bytes.len(), is_js, }); std::fs::write(path, bytes.as_ref())?; } Ok(output_infos) } fn print_finished_message( metafile: &esbuild_client::Metafile, output_infos: &[OutputFileInfo], duration: Duration, ) -> Result<(), AnyError> { let mut output = String::new(); output.push_str(&format!( "{} {} module{} in {}", deno_terminal::colors::green("Bundled"), metafile.inputs.len(), if metafile.inputs.len() == 1 { "" } else { "s" }, crate::display::human_elapsed(duration.as_millis()), )); let longest = output_infos .iter() .map(|info| info.relative_path.to_string_lossy().len()) .max() .unwrap_or(0); for info in output_infos { output.push_str(&format!( "\n {} {}", if info.is_js { deno_terminal::colors::cyan(format!( "{: