/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/bundle/mod.rs
3 146 строк
99 KB
Nathan Whitaker
fix(bundle): respect runtime file permissions (#36107)
05 авг 2026, 00:04
Не верифицирован
05 авг 2026, 00:04
3a10142
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. mod esbuild; mod externals; mod html; mod provider; mod transform; mod wasm; 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::anyhow::Context as _; use deno_core::error::AnyError; use deno_core::futures::FutureExt as _; use deno_core::parking_lot::Mutex; use deno_core::parking_lot::RwLock; use deno_core::serde_json; use deno_core::url::Url; use deno_error::JsError; use deno_error::JsErrorClass; use deno_graph::GraphKind; use deno_graph::ModuleErrorKind; use deno_graph::ModuleGraph; 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::CheckSpecifierKind; use deno_runtime::deno_permissions::OpenAccessKind; 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 indexmap::IndexSet; 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::PathsInErrorsExt; use crate::args::BundleFlags; use crate::args::Flags; use crate::args::TypeCheckMode; use crate::factory::CliFactory; use crate::file_fetcher::CliFileFetcher; use crate::graph_container::CheckSpecifiersOptions; use crate::graph_container::CollectSpecifiersOptions; 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::tools::bundle::externals::ExternalsMatcher; use crate::util::file_watcher::WatcherRestartMode; use crate::util::fs::canonicalize_path; static DISABLE_HACK: LazyLock<bool> = LazyLock::new(|| std::env::var("NO_DENO_BUNDLE_HACK").is_err()); pub async fn prepare_inputs( resolver: &CliResolver, npm_resolver: &CliNpmResolver, node_resolver: &CliNodeResolver, init_cwd: &Path, bundle_flags: &BundleFlags, plugin_handler: &mut DenoPluginHandler, ) -> Result<BundlerInput, AnyError> { let resolved_entrypoints = resolve_entrypoints( resolver, init_cwd, &bundle_flags.entrypoints, !plugin_handler.is_runtime_api, )?; // 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 is_html_entrypoint(url) { html_paths.push(deno_path_util::url_to_file_path(url)?); } else { script_entry_urls.push(url.clone()); } } if html_paths.is_empty() { plugin_handler .prepare_module_load(&resolved_entrypoints) .await?; let roots = resolve_roots( resolved_entrypoints, init_cwd, npm_resolver, node_resolver, ); plugin_handler.prepare_module_load(&roots).await?; let graph = plugin_handler.module_graph_container.graph(); let mut fully_resolved_roots = IndexSet::with_capacity(graph.roots.len()); for root in &graph.roots { fully_resolved_roots.insert(graph.resolve(root).clone()); } *plugin_handler.resolved_roots.write() = Arc::new(fully_resolved_roots); 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 virtual_modules = Arc::new(VirtualModules::new()); for html_path in &html_paths { let entry = html::load_html_entrypoint( init_cwd, html_path, plugin_handler .is_runtime_api .then_some(&plugin_handler.permissions), )?; 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(virtual_modules); // Prepare non-HTML entries too let _ = plugin_handler.prepare_module_load(&script_entry_urls).await; let roots = resolve_roots(script_entry_urls, init_cwd, 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; let graph = plugin_handler.module_graph_container.graph(); let mut fully_resolved_roots = IndexSet::with_capacity(graph.roots.len()); for root in &graph.roots { fully_resolved_roots.insert(graph.resolve(root).clone()); } *plugin_handler.resolved_roots.write() = Arc::new(fully_resolved_roots); Ok(BundlerInput::EntrypointsWithHtml { entries, html_pages, }) } } pub async fn bundle_init( mut flags: Arc<Flags>, bundle_flags: &BundleFlags, caller_permissions: Option<PermissionsContainer>, ) -> Result<EsbuildBundler, AnyError> { { 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(); // Module loads (including remote imports) are checked against the caller's // permissions when invoked via `Deno.bundle()`, and against the CLI's own // permissions when invoked via `deno bundle`. let (permissions, is_runtime_api) = match caller_permissions { Some(permissions) => (permissions, true), None => (factory.root_permissions_container()?.clone(), false), }; 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 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, reason = "fine because only used in output positions" )] let mut plugin_handler = Arc::new(DenoPluginHandler { file_fetcher: factory.file_fetcher()?.clone(), resolver: resolver.clone(), module_load_preparer, resolved_roots: Arc::new(RwLock::new(Arc::new(IndexSet::new()))), module_graph_container, permissions, is_runtime_api, 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(), pkg_json_resolver: factory.pkg_json_resolver()?.clone(), deferred_resolve_errors: Default::default(), virtual_modules: None, initial_cwd: deno_path_util::url_from_directory_path( cli_options.initial_cwd(), )?, }); let input = prepare_inputs( &resolver, 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 .context("failed to start esbuild")?; 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<Flags>, bundle_flags: BundleFlags, ) -> Result<(), AnyError> { { let flags_mut = Arc::make_mut(&mut flags); flags_mut.unstable_config.sloppy_imports = true; } // `--check[=all]` is documented in `bundle --help`; honour it before we // hand the graph to esbuild so type errors surface alongside (and not // after) the bundle output (denoland/deno#30159). if !matches!(flags.type_check_mode, TypeCheckMode::None) { let check_factory = CliFactory::from_flags(flags.clone()); let main_graph_container = check_factory.main_module_graph_container().await?; let specifiers = main_graph_container.collect_specifiers( &bundle_flags.entrypoints, CollectSpecifiersOptions { include_ignored_specified: false, }, )?; main_graph_container .check_specifiers( &specifiers, CheckSpecifiersOptions { allow_unknown_media_types: true, ..Default::default() }, ) .await?; } let bundler = bundle_init(flags.clone(), &bundle_flags, None).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), None, )?; if bundle_flags.declaration { emit_bundle_declarations(flags.clone(), &bundle_flags, &init_cwd).await?; } 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(()) } async fn emit_bundle_declarations( flags: Arc<Flags>, bundle_flags: &BundleFlags, init_cwd: &Path, ) -> Result<(), AnyError> { let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let real_sys = factory.sys(); let sys = real_sys.with_paths_in_errors(); // Resolve entrypoints to specifiers let resolver = factory.resolver().await?; let entrypoint_specifiers = resolve_entrypoints(resolver, init_cwd, &bundle_flags.entrypoints, true)?; // Determine root names with media types let root_names: Vec<(ModuleSpecifier, MediaType)> = entrypoint_specifiers .iter() .map(|s| (s.clone(), MediaType::from_specifier(s))) .collect(); let specifiers: Vec<ModuleSpecifier> = root_names.iter().map(|(s, _)| s.clone()).collect(); // Build the module graph for type checking let mut graph = ModuleGraph::new(GraphKind::All); let module_graph_builder = factory.module_graph_builder().await?; module_graph_builder .build_graph_roots_with_npm_resolution( &mut graph, specifiers, crate::graph_util::BuildGraphWithNpmOptions { is_dynamic: false, loader: None, npm_caching: cli_options.default_npm_caching_strategy(), }, ) .await?; // Run type checker to emit declaration files let type_checker = factory.type_checker().await?; let result = type_checker.emit_declarations( Arc::new(graph), root_names, cli_options.ts_type_lib_window(), )?; if result.diagnostics.has_diagnostic() { deno_core::anyhow::bail!( "Type checking failed when generating declarations:\n{}", result.diagnostics ); } // Index emitted .d.ts files by their original source specifier // TSC emits keys like "file:///path/to/file.d.ts" - map them back to source paths let mut dts_by_source: std::collections::HashMap<String, String> = std::collections::HashMap::new(); for (file_name, content) in &result.emitted_files { // Map "file:///path/to/mod.d.ts" -> normalized source path if let Ok(specifier) = Url::parse(file_name) && let Ok(file_path) = specifier.to_file_path() { let source_path = file_path.to_string_lossy().to_string(); dts_by_source.insert(source_path, content.clone()); } } // For each entry point, produce a single rolled-up .d.ts for entry_specifier in &entrypoint_specifiers { let entry_path = entry_specifier.to_file_path().map_err(|_| { deno_core::anyhow::anyhow!( "Entry point must be a file: {}", entry_specifier ) })?; // Find the .d.ts for this entry point let dts_path = to_dts_path(&entry_path); let entry_dts = dts_by_source .get(&dts_path.to_string_lossy().to_string()) .ok_or_else(|| { deno_core::anyhow::anyhow!( "No declaration file emitted for entry point: {}", entry_specifier ) })?; // Flatten: resolve all `export ... from "..."` re-exports by inlining // the referenced declarations from other .d.ts files. let flattened = flatten_declarations(entry_dts, &entry_path, &dts_by_source); // Determine output path for the rolled-up .d.ts let dts_output_path = if let Some(ref output) = bundle_flags.output_path { let js_path = init_cwd.join(output); to_dts_path(&js_path) } else if let Some(ref outdir) = bundle_flags.output_dir { let outdir = PathBuf::from(outdir); let stem = entry_path.file_stem().unwrap_or_default(); outdir.join(format!("{}.d.ts", stem.to_string_lossy())) } else { to_dts_path(&entry_path) }; if let Some(parent) = dts_output_path.parent() { sys.fs_create_dir_all(parent).with_context(|| { format!("Failed to create directory {}", parent.display()) })?; } sys .fs_write(&dts_output_path, flattened.as_bytes()) .with_context(|| { format!("Failed to write {}", dts_output_path.display()) })?; log::info!( "{} {}", deno_terminal::colors::green("Emit"), dts_output_path.display() ); } Ok(()) } /// Convert a source file path to its corresponding declaration file path. /// /// Mirrors TSC's declaration emit: `.mts`/`.mjs` sources produce `.d.mts`, /// `.cts`/`.cjs` produce `.d.cts`, and everything else produces `.d.ts`. Using /// the wrong extension here would make the rolled-up output reference a /// declaration file that was never emitted. fn to_dts_path(source_path: &Path) -> PathBuf { let stem = source_path.file_stem().unwrap_or_default(); let parent = source_path.parent().unwrap_or(Path::new("")); let dts_ext = match source_path.extension().and_then(|e| e.to_str()) { Some("mts" | "mjs") => "d.mts", Some("cts" | "cjs") => "d.cts", _ => "d.ts", }; parent.join(format!("{}.{}", stem.to_string_lossy(), dts_ext)) } /// Join multi-line `export { ... } from "..."` and `import type { ... } from "..."` /// statements into single lines so that line-based parsing can handle them. fn join_multiline_statements(content: &str) -> String { let mut result = Vec::new(); let mut accumulator: Option<String> = None; for line in content.lines() { if let Some(ref mut acc) = accumulator { // We're inside a multi-line statement, keep accumulating acc.push(' '); acc.push_str(line.trim()); if line.contains(';') { // Statement is complete result.push(acc.clone()); accumulator = None; } } else { let trimmed = line.trim(); // Detect start of a multi-line export/import that has `{` but no closing `}` let is_export_or_import = trimmed.starts_with("export ") || trimmed.starts_with("import "); if is_export_or_import && trimmed.contains('{') && !trimmed.contains('}') { accumulator = Some(line.trim().to_string()); } else { result.push(line.to_string()); } } } // If there's an unterminated accumulator, flush it if let Some(acc) = accumulator { result.push(acc); } result.join("\n") } /// Warn that a relative re-export could not be inlined, so the rolled-up /// declaration file keeps a dangling `from "./..."` reference. fn warn_dangling_reexport(reexport_path: &str, source_path: &Path) { log::warn!( "{} could not inline re-exported declarations from {:?} in {}; the generated .d.ts will keep a dangling relative reference", deno_terminal::colors::yellow("warning:"), reexport_path, source_path.display(), ); } /// Flatten a .d.ts file by resolving all `export ... from "..."` re-exports, /// inlining the referenced declarations from other emitted .d.ts files. /// /// This produces a single self-contained .d.ts file with no relative imports. fn flatten_declarations( entry_dts_content: &str, entry_source_path: &Path, dts_by_source: &std::collections::HashMap<String, String>, ) -> String { let entry_dir = entry_source_path.parent().unwrap_or(Path::new("")); let mut output_lines: Vec<String> = Vec::new(); // Track which declarations have already been inlined to avoid duplicates let mut inlined_files: std::collections::HashSet<String> = std::collections::HashSet::new(); // The set of files whose declarations actually get inlined into the output, // i.e. everything transitively reachable from the entry via `export ... from` // chains. An `import ... from "./relative"` line may only be dropped when its // target is in this set; otherwise the imported symbol would have no // declaration in the rolled-up output. (Membership in `dts_by_source` is not // enough: a module can be emitted yet never inlined because nothing // re-exports it.) let reexport_closure = collect_reexport_closure(entry_dts_content, entry_dir, dts_by_source); let joined = join_multiline_statements(entry_dts_content); for line in joined.lines() { let trimmed = line.trim(); // Skip amd-module directives if trimmed.starts_with("/// <amd-module") { continue; } // Handle: export { Name1, Name2 } from "./relative"; // Handle: export type { Name1, Name2 } from "./relative"; if let Some(from_path) = extract_reexport_path(trimmed) { // Resolve the relative path to absolute let resolved = resolve_relative_dts_path(entry_dir, &from_path); let resolved_str = resolved.to_string_lossy().to_string(); if let Some(referenced_dts) = dts_by_source.get(&resolved_str) { if !inlined_files.contains(&resolved_str) { inlined_files.insert(resolved_str.clone()); // Inline all declarations from the referenced file inline_declarations( referenced_dts, &resolved, dts_by_source, &reexport_closure, &mut output_lines, &mut inlined_files, ); } // The re-export line is replaced by the inlined content continue; } // A relative re-export whose declaration file was not emitted. Keeping // the line leaves a dangling `from "./..."` in the supposedly // self-contained output, so warn rather than silently shipping it. warn_dangling_reexport(&from_path, entry_source_path); } // Drop an `import ... from "./relative"` whose target is inlined into the // output (its declarations are already present). A target that is not in // the closure is left as-is rather than deleted, so we never strip an // import whose symbol has no inlined declaration. if let Some(import_path) = extract_import_path(trimmed) { let resolved = resolve_relative_dts_path(entry_dir, &import_path); let resolved_str = resolved.to_string_lossy().to_string(); if reexport_closure.contains(&resolved_str) { continue; } } output_lines.push(line.to_string()); } output_lines.join("\n") + "\n" } /// Collect the set of `.d.ts` files (keyed as in `dts_by_source`) that are /// transitively reachable from the entry through `export ... from "./relative"` /// re-export chains. These are exactly the files whose declarations get inlined /// into the rolled-up output by [`inline_declarations`]. fn collect_reexport_closure( entry_dts_content: &str, entry_dir: &Path, dts_by_source: &std::collections::HashMap<String, String>, ) -> std::collections::HashSet<String> { let mut closure = std::collections::HashSet::new(); let mut stack: Vec<(String, PathBuf)> = vec![(entry_dts_content.to_string(), entry_dir.to_path_buf())]; while let Some((content, dir)) = stack.pop() { let joined = join_multiline_statements(&content); for line in joined.lines() { let trimmed = line.trim(); if let Some(from_path) = extract_reexport_path(trimmed) { let resolved = resolve_relative_dts_path(&dir, &from_path); let resolved_str = resolved.to_string_lossy().to_string(); if !closure.contains(&resolved_str) && let Some(referenced_dts) = dts_by_source.get(&resolved_str) { closure.insert(resolved_str.clone()); let child_dir = resolved.parent().unwrap_or(Path::new("")).to_path_buf(); stack.push((referenced_dts.clone(), child_dir)); } } } } closure } /// Extract the relative path from an `export ... from "..."` line. /// Returns None if the line is not a re-export or uses a non-relative path. fn extract_reexport_path(line: &str) -> Option<String> { // Match patterns like: // export { X } from "./foo"; // export { X, Y } from "./foo.ts"; // export type { X } from "./foo"; let trimmed = line.trim(); if !trimmed.starts_with("export ") || !trimmed.contains(" from ") { return None; } // Extract the path between quotes after "from" let from_idx = trimmed.find(" from ")?; let after_from = &trimmed[from_idx + 6..]; let quote_char = after_from.chars().next()?; if quote_char != '"' && quote_char != '\'' { return None; } let end_quote = after_from[1..].find(quote_char)?; let path = &after_from[1..1 + end_quote]; // Only resolve relative paths (starting with ./ or ../) if path.starts_with("./") || path.starts_with("../") { Some(path.to_string()) } else { None } } /// Resolve a relative .d.ts path from the given directory. fn resolve_relative_dts_path(base_dir: &Path, relative: &str) -> PathBuf { let mut resolved = base_dir.join(relative); // The reference might be "./lib.ts" but the actual .d.ts is at the // path with .d.ts extension resolved = to_dts_path(&resolved); deno_path_util::normalize_path(Cow::Owned(resolved)).into_owned() } /// Extract a relative path from an `import ... from "..."` line. fn extract_import_path(line: &str) -> Option<String> { let trimmed = line.trim(); if !trimmed.starts_with("import ") || !trimmed.contains(" from ") { return None; } let from_idx = trimmed.find(" from ")?; let after_from = &trimmed[from_idx + 6..]; let quote_char = after_from.chars().next()?; if quote_char != '"' && quote_char != '\'' { return None; } let end_quote = after_from[1..].find(quote_char)?; let path = &after_from[1..1 + end_quote]; if path.starts_with("./") || path.starts_with("../") { Some(path.to_string()) } else { None } } /// Inline declarations from a .d.ts file into the output, recursively /// resolving any re-exports in the referenced file. fn inline_declarations( dts_content: &str, source_path: &Path, dts_by_source: &std::collections::HashMap<String, String>, reexport_closure: &std::collections::HashSet<String>, output_lines: &mut Vec<String>, inlined_files: &mut std::collections::HashSet<String>, ) { let source_dir = source_path.parent().unwrap_or(Path::new("")); let joined = join_multiline_statements(dts_content); for line in joined.lines() { let trimmed = line.trim(); // Skip amd-module directives if trimmed.starts_with("/// <amd-module") { continue; } // Recursively resolve re-exports from this file too if let Some(from_path) = extract_reexport_path(trimmed) { let resolved = resolve_relative_dts_path(source_dir, &from_path); let resolved_str = resolved.to_string_lossy().to_string(); if let Some(referenced_dts) = dts_by_source.get(&resolved_str) { if !inlined_files.contains(&resolved_str) { inlined_files.insert(resolved_str.clone()); inline_declarations( referenced_dts, &resolved, dts_by_source, reexport_closure, output_lines, inlined_files, ); } continue; } // A relative re-export whose declaration file was not emitted; warn so a // dangling reference in the output does not pass by unnoticed. warn_dangling_reexport(&from_path, source_path); } // Strip `import type ... from "./relative"` / `import { ... } from // "./relative"` lines only when the referenced file is actually inlined // into this rolled-up output (it's in the re-export closure). The imported // symbol's declaration is then present directly. A target that is emitted // but never inlined is intentionally left untouched, so we don't delete an // import whose symbol would otherwise vanish. if let Some(import_path) = extract_import_path(trimmed) { let resolved = resolve_relative_dts_path(source_dir, &import_path); let resolved_str = resolved.to_string_lossy().to_string(); if reexport_closure.contains(&resolved_str) { continue; // skip - declarations are inlined } } output_lines.push(line.to_string()); } } /// Bundle a single entrypoint for `deno compile --bundle` and return the /// resulting JavaScript bytes in memory. The Deno-specific `createRequire` /// shim is applied to the output so CJS `require()` calls keep working in /// the compiled binary. /// /// `external` is the set of import patterns (typically npm package names) to /// leave unbundled. For `deno compile --bundle` this is populated with the /// names of packages that ship native `.node` addons so their internal /// `require('./X.node')` calls keep their original `__dirname` context at /// runtime. pub async fn bundle_for_compile( flags: Arc<Flags>, entrypoint: String, external: Vec<String>, minify: bool, ) -> Result<Vec<u8>, AnyError> { let bundle_flags = BundleFlags { entrypoints: vec![entrypoint], output_path: None, output_dir: None, declaration: false, external, format: BundleFormat::Esm, minify, keep_names: false, code_splitting: false, inline_imports: true, packages: PackageHandling::Bundle, sourcemap: None, platform: BundlePlatform::Deno, watch: false, }; // Force bundle-style resolver config for the duration of bundling. // Without this, module-graph creation skips deep CJS files inside // `node_modules` (jiti.cjs is the canonical case) and the parent // `.mjs` trips an esbuild parse error when its import target can't // be loaded. This flips the same three resolver knobs `deno bundle` // sets (`bundle_mode`, `node_code_translator_mode` = Disabled, // `allow_json_imports` = Always) without otherwise swapping the // subcommand, so the rest of compilation keeps Compile semantics. // Disabling the code translator means the CJS analyzer's // extensionless `.node` auto-resolution no longer runs, which is why // the resolver grows a `.node` fallback (see `resolution.rs`). let mut flags = flags; { let flags_mut = Arc::make_mut(&mut flags); flags_mut.internal.force_bundle_mode = true; } let bundler = bundle_init(flags, &bundle_flags, None).await?; let response = bundler.build().await?; handle_esbuild_errors_and_warnings( &response, &bundler.cwd, &bundler.plugin_handler.take_deferred_resolve_errors(), ); if !response.errors.is_empty() { deno_core::anyhow::bail!("bundling failed"); } let output_files = collect_output_files( response.output_files.as_deref(), &bundler.cwd, bundler.input.clone(), None, )?; for file in &output_files { let processed = maybe_process_contents( file, should_replace_require_shim(bundle_flags.platform), bundle_flags.minify, )?; if !processed.is_js { continue; } return Ok( processed .into_contents() .unwrap_or_else(|| file.contents.to_vec()), ); } deno_core::anyhow::bail!("esbuild produced no JavaScript output") } fn metafile_from_response( response: &BuildResponse, ) -> Result<esbuild_client::Metafile, AnyError> { Ok(serde_json::from_str::<esbuild_client::Metafile>( response.metafile.as_deref().ok_or_else(|| { deno_core::anyhow::anyhow!("expected a metafile to be present") })?, )?) } async fn bundle_watch( flags: Arc<Flags>, 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<_>>(), 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::<Vec<_>>(); let always = html_pages .iter() .map(|p| p.path.clone()) .collect::<Vec<_>>(); 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 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 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.reload_specifiers(&changed_paths).await?; } let input = bundler.input.clone(); 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, None, )?; 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()); } // `deno bundle --watch` has no per-run exit code (and disables the // "finished" message above), so report 0 to the watcher. Ok(0) }) }, ) .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<PathBuf> { let metafile = serde_json::from_str::<esbuild_client::Metafile>( response .metafile .as_deref() .expect("metafile is required for watch mode"), ) .unwrap(); metafile .inputs .keys() .cloned() .map(PathBuf::from) .collect::<Vec<_>>() } #[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<html::HtmlEntrypoint>, }, } pub type EsbuildFlags = Vec<String>; pub struct EsbuildBundler { client: esbuild_client::ProtocolClient, plugin_handler: Arc<DenoPluginHandler>, on_end_rx: tokio::sync::mpsc::Receiver<esbuild_client::OnEndArgs>, mode: BundlingMode, cwd: PathBuf, flags: EsbuildFlags, input: BundlerInput, } impl EsbuildBundler { pub fn new( client: esbuild_client::ProtocolClient, plugin_handler: Arc<DenoPluginHandler>, mode: BundlingMode, on_end_rx: tokio::sync::mpsc::Receiver<esbuild_client::OnEndArgs>, 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<BuildResponse, AnyError> { let response: BuildResponse = self .client .send_build_request(self.make_build_request()) .await .context("failed to send build request to esbuild")? .map_err(|e| message_to_error(&e, &self.cwd))?; Ok(response) } async fn rebuild(&mut self) -> Result<BuildResponse, AnyError> { 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 .context("failed to send rebuild request to esbuild")? .map_err(|e| message_to_error(&e, &self.cwd))?; let response = self.on_end_rx.recv().await.ok_or_else(|| { deno_core::anyhow::anyhow!( "esbuild exited before the rebuild completed" ) })?; Ok(response.into()) } } } async fn reload_specifiers( &mut self, changed_paths: &[PathBuf], ) -> Result<(), AnyError> { self.reload_html_entrypoints(changed_paths)?; self.plugin_handler.reload_specifiers(changed_paths).await?; Ok(()) } fn reload_html_entrypoints( &mut self, changed_paths: &[PathBuf], ) -> Result<(), AnyError> { let BundlerInput::EntrypointsWithHtml { html_pages, .. } = &mut self.input else { return Ok(()); }; if changed_paths.is_empty() { return Ok(()); } for page in html_pages.iter_mut() { if !changed_paths .iter() .any(|changed| changed == &page.path || changed == &page.canonical_path) { continue; } let updated = html::load_html_entrypoint( &self.cwd, &page.path, self .plugin_handler .is_runtime_api .then_some(&self.plugin_handler.permissions), )?; let virtual_module_url = deno_path_util::url_from_file_path(&updated.virtual_module_path)? .to_string(); self.plugin_handler.update_virtual_module( &virtual_module_url, VirtualModule::new( updated.temp_module.as_bytes().to_vec(), esbuild_client::BuiltinLoader::Js, ), ); *page = updated; } Ok(()) } } 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+\)\[\w+\]\}\):(\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 as __deno_internal_createRequire}} from \"node:module\";var {var_name}=__deno_internal_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 as __deno_internal_createRequire } from "node:module"; var __require = __deno_internal_createRequire(import.meta.url); "#, ) .into_owned() } } // Force esbuild's CommonJS-to-ESM interop helper (`__toESM`) into "node mode" // for every platform. // // esbuild emits a node-mode interop call (`__toESM(require_x(), 1)`) only when // it can tell the importing module is *explicitly* ESM (a `.mjs`/`.mts` file or // a file under a `"type": "module"` package.json). It learns this while walking // package.json during its own resolve step. In `deno bundle` resolution and // loading are handled by our plugin, and esbuild's plugin protocol has no way // to communicate a module's type, so esbuild falls back to browser-mode interop // which respects the `__esModule` marker and does not synthesize `.default`. // // For packages whose ESM wrapper default-imports a CJS entry that sets // `module.exports.__esModule = true` (e.g. tslib's `modules/index.js`), that // means `import_x.default` ends up undefined and module init throws. This is a // property of how the source module resolves its default import (Node interop // semantics, which is what such packages are authored against), not of the // runtime target, so it applies regardless of `--platform`. We rewrite the // helper's `<isNodeMode> || !mod || !mod.__esModule` condition to always pick // the node-mode branch. See denoland/deno#34524 and denoland/deno#34837. // // The variable names differ between minified and non-minified output, but the // `.__esModule` access and surrounding shape are stable, so a single regex // covers both. fn force_node_cjs_interop(contents: &str) -> String { let re = lazy_regex::regex!(r#"\w+\s*\|\|\s*!\w+\s*\|\|\s*!\w+\.__esModule\s*\?"#); re.replace(contents, "1 ?").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::<Vec<_>>(); 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, boxed_error::Boxed, JsError)] struct BundleError(Box<BundleErrorKind>); #[derive(Debug, thiserror::Error, JsError)] #[class(generic)] enum BundleErrorKind { #[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, reason = "not used yet")] #[error("Http cache error")] HttpCache, } fn requested_type_from_map( map: &IndexMap<String, String>, ) -> 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, } } #[derive(Clone)] pub struct VirtualModule { contents: Vec<u8>, loader: esbuild_client::BuiltinLoader, } impl VirtualModule { pub fn new(contents: Vec<u8>, loader: esbuild_client::BuiltinLoader) -> Self { Self { contents, loader } } } pub struct VirtualModules { modules: RwLock<IndexMap<String, VirtualModule>>, } impl VirtualModules { pub fn new() -> Self { Self { modules: RwLock::new(IndexMap::new()), } } pub fn insert(&self, path: String, contents: VirtualModule) { self.modules.write().insert(path, contents); } pub fn get(&self, path: &str) -> Option<VirtualModule> { self.modules.read().get(path).cloned() } pub fn contains(&self, path: &str) -> bool { self.modules.read().contains_key(path) } } pub struct DeferredResolveError { path: String, error: ResolveWithGraphError, } /// Path prefix used to mark modules disabled via a `browser` map (`"foo": false`). /// `\0` keeps it from colliding with any real filesystem path. const BROWSER_DISABLED_PREFIX: &str = "\0deno-browser-disabled:"; pub struct DenoPluginHandler { file_fetcher: Arc<CliFileFetcher>, resolver: Arc<CliResolver>, module_load_preparer: Arc<ModuleLoadPreparer>, resolved_roots: Arc<RwLock<Arc<IndexSet<ModuleSpecifier>>>>, module_graph_container: Arc<MainModuleGraphContainer>, permissions: PermissionsContainer, /// Whether reads and writes must respect the calling program's permissions. /// The directly invoked `deno bundle` command is a trusted CLI path. is_runtime_api: bool, module_loader: Arc<CliDenoResolverModuleLoader>, externals_matcher: Option<ExternalsMatcher>, on_end_tx: tokio::sync::mpsc::Sender<esbuild_client::OnEndArgs>, deferred_resolve_errors: Arc<Mutex<Vec<DeferredResolveError>>>, virtual_modules: Option<Arc<VirtualModules>>, parsed_source_cache: Arc<ParsedSourceCache>, cjs_tracker: Arc<CliCjsTracker>, emitter: Arc<CliEmitter>, pkg_json_resolver: Arc<crate::node::CliPackageJsonResolver>, initial_cwd: Url, } impl DenoPluginHandler { fn take_deferred_resolve_errors(&self) -> Vec<DeferredResolveError> { std::mem::take(&mut *self.deferred_resolve_errors.lock()) } fn update_virtual_module(&self, path: &str, module: VirtualModule) { if let Some(virtual_modules) = &self.virtual_modules { virtual_modules.insert(path.to_string(), module); } } /// Determines whether the resolved file has side effects by consulting /// the closest `package.json`'s `sideEffects` field. Returns `Some(false)` /// when the file is known to be side-effect-free (which lets esbuild /// tree-shake unused exports), and `None` otherwise (esbuild's default /// is to assume side effects). fn resolve_side_effects(&self, resolved: &str) -> Option<bool> { // Only inspect concrete local file paths. URLs (jsr:, https:, data:, etc.) // are handled elsewhere and don't have a package.json context. if resolved.starts_with("jsr:") || resolved.starts_with("https:") || resolved.starts_with("http:") || resolved.starts_with("data:") || resolved.starts_with("node:") || resolved.starts_with("bun:") || resolved.starts_with("npm:") { return None; } let file_path = std::path::Path::new(resolved); if !file_path.is_absolute() { return None; } let pkg_json = self .pkg_json_resolver .get_closest_package_json(file_path) .ok() .flatten()?; match pkg_json.side_effects.as_ref()? { deno_package_json::PackageJsonSideEffects::Bool(true) => None, deno_package_json::PackageJsonSideEffects::Bool(false) => Some(false), deno_package_json::PackageJsonSideEffects::Patterns(patterns) => { let rel = file_path.strip_prefix(pkg_json.dir_path()).ok()?; let rel_str = rel.to_string_lossy().replace('\\', "/"); for pattern in patterns { if side_effects_pattern_matches(pattern, &rel_str) { return None; } } Some(false) } } } } /// Matches a path (relative to the package root, using forward slashes) /// against a `sideEffects` entry from a `package.json`. /// /// Per webpack's convention, a pattern without a leading `./`, `/`, or `*` /// is implicitly treated as `**/<pattern>`. fn side_effects_pattern_matches(pattern: &str, rel_path: &str) -> bool { let normalized = if let Some(rest) = pattern.strip_prefix("./") { rest.to_string() } else if pattern.starts_with('/') { pattern.trim_start_matches('/').to_string() } else { format!("**/{pattern}") }; match glob::Pattern::new(&normalized) { Ok(p) => p.matches_with( rel_path, glob::MatchOptions { case_sensitive: true, require_literal_separator: false, require_literal_leading_dot: false, }, ), Err(_) => false, } } #[async_trait::async_trait(?Send)] impl esbuild_client::PluginHandler for DenoPluginHandler { async fn on_resolve( &self, args: esbuild_client::OnResolveArgs, ) -> Result<Option<esbuild_client::OnResolveResult>, 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() })); } // Same-document fragment references in CSS — `url(#default#VML)`, // `url(#gradient)` for SVG paint servers, etc. — are not file imports // and must be left as-is in the output. Without this, `bundle_resolve` // tries to resolve them as package import specifiers and fails (see // denoland/deno#32232). if matches!( args.kind, esbuild_client::protocol::ImportKind::UrlToken | esbuild_client::protocol::ImportKind::ImportRule ) && args.path.starts_with('#') { 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) => { // A `browser` map entry of `false` disables the module: return a // sentinel path that `on_load` recognizes and turns into an empty // module, rather than surfacing the resolver error. if let Some(orig) = browser_map_disabled_specifier(&e) { return Ok(Some(esbuild_client::OnResolveResult { path: Some(format!("{BROWSER_DISABLED_PREFIX}{orig}")), namespace: Some("deno".into()), plugin_name: Some("deno".to_string()), ..Default::default() })); } 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:") || r.starts_with("bun:") // Always externalize native addons regardless of `--external`. // esbuild has no loader for `.node` files, and the user-facing // `*.node` pre-resolve pattern doesn't catch paths that arrive // here only after `.node`-extension auto-resolution (e.g. CJS // `require('./build/Release/foo')`). || r.ends_with(".node") || self .externals_matcher .as_ref() .map(|matcher| matcher.is_post_resolve_match(&r)) .unwrap_or(false); let side_effects = if is_external { None } else { self.resolve_side_effects(&r) }; 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, side_effects, ..Default::default() } })) } async fn on_load( &self, args: esbuild_client::OnLoadArgs, ) -> Result<Option<esbuild_client::OnLoadResult>, AnyError> { log::debug!("{}: {args:?}", deno_terminal::colors::cyan("on_load")); if args.path.starts_with(BROWSER_DISABLED_PREFIX) { return Ok(Some(esbuild_client::OnLoadResult { contents: Some(b"module.exports = {};\n".to_vec()), loader: Some(esbuild_client::BuiltinLoader::Js), ..Default::default() })); } if let Some(virtual_modules) = &self.virtual_modules && let Some(module) = virtual_modules.get(&args.path) { let contents = module.contents.clone(); let loader = module.loader; return Ok(Some(esbuild_client::OnLoadResult { contents: Some(contents), loader: Some(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<Option<esbuild_client::OnStartResult>, AnyError> { Ok(None) } async fn on_end( &self, _args: esbuild_client::OnEndArgs, ) -> Result<Option<esbuild_client::OnEndResult>, 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, boxed_error::Boxed, deno_error::JsError)] pub struct BundleLoadError(pub Box<BundleLoadErrorKind>); #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum BundleLoadErrorKind { #[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)] Permission(#[from] deno_runtime::deno_permissions::PermissionCheckError), #[class(inherit)] #[error(transparent)] ResolveWithGraph(#[from] ResolveWithGraphError), #[class(generic)] #[error("Failed to parse Wasm module: {0}")] WasmParse(String), #[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.as_kind() { BundleLoadErrorKind::LoadCodeSource(e) => match e.as_kind() { LoadCodeSourceErrorKind::LoadPreparedModule(e) => match e.as_kind() { LoadPreparedModuleErrorKind::Graph(e) => matches!( e.original().as_module_error_kind(), Some(ModuleErrorKind::UnsupportedMediaType { .. }), ), _ => false, }, _ => false, }, _ => false, } } } /// Walk a `BundleError` looking for a `BrowserMapDisabled` error returned /// by `node_resolver`. Returns the original specifier so the bundle can /// emit an empty-module stub in place of the resolution. fn browser_map_disabled_specifier(error: &BundleError) -> Option<String> { let BundleErrorKind::Resolver(resolve_err) = error.as_kind() else { return None; }; let deno_resolver::graph::ResolveWithGraphErrorKind::Resolve(e) = resolve_err.as_kind() else { return None; }; let deno_resolver::DenoResolveErrorKind::Node(node_err) = e.as_kind() else { return None; }; let node_resolver::errors::NodeResolveErrorKind::BrowserMapDisabled(disabled) = node_err.as_kind() else { return None; }; Some(disabled.specifier.clone()) } /// Whether a specifier looks like a bare (npm/node-style) specifier, i.e. not /// a relative path, absolute path, package-internal `#` import, or a URL with a /// scheme (`file:`, `data:`, `node:`, `npm:`, `jsr:`, `http(s):`, ...). fn looks_like_bare_specifier(specifier: &str) -> bool { !specifier.is_empty() && !specifier.starts_with('.') && !specifier.starts_with('/') && !specifier.starts_with('#') // Load-bearing on Windows: `Url::parse("C:/foo")` succeeds (scheme `c`), so // a drive-letter absolute path is correctly treated as not bare here. Don't // "simplify" this away or Windows absolute paths regress into the fallback. && Url::parse(specifier).is_err() } fn maybe_ignorable_resolution_error( error: &ResolveWithGraphError, ) -> Option<String> { 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::Other(other_err) = resolve_error.deref() && let Some(import_map_err) = other_err .get_ref() .downcast_ref::<import_map::ImportMapError>() && 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)?; // Raw imports are represented as external assets in the module graph. // Their contents are fetched by `bundle_load` instead of being stored // in the graph, so reloading them as ordinary graph roots loses their // requested module type (for example, `type: "css"`). Esbuild will call // `on_load` again for the changed file and fetch its current contents. if matches!( graph.get(&specifier), Some(deno_graph::Module::External(module)) if module.was_asset_load ) { continue; } specifiers_vec.push(specifier); } if !specifiers_vec.is_empty() { self .module_load_preparer .reload_specifiers( graph, specifiers_vec, false, self.permissions.clone(), ) .await?; } graph_permit.commit(); Ok(()) } fn bundle_resolve( &self, path: &str, importer: Option<&str>, resolve_dir: Option<&str>, kind: esbuild_client::protocol::ImportKind, with: IndexMap<String, String>, ) -> Result<Option<String>, BundleError> { log::debug!( "bundle_resolve: {:?} {:?} {:?} {:?} {:?}", path, importer, resolve_dir, kind, with ); if path.starts_with("data:") { return Ok(None); } 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(|_| self.initial_cwd.clone()); 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); // The graph may record an unmapped-bare-specifier error for a referrer // pulled in from outside the entrypoint's package scope (e.g. a source // file reached via a `new Worker(new URL(...))` reference next to a // `dist/`-rooted entrypoint). The package is still in the build's npm // snapshot, so resolve it by name the way Node/Bun do before giving up. if looks_like_bare_specifier(path) && let Some(url) = resolver.resolve_bare_specifier_in_npm_snapshot( path, Some(&referrer), import_kind_to_resolution_mode(kind), NodeResolutionKind::Execution, ) { log::debug!( "{}: resolved {} via npm snapshot fallback", deno_terminal::colors::cyan("op_bundle_resolve"), path ); return Ok(Some(file_path_or_url(url)?)); } 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(BundleErrorKind::Resolver(e).into()) } } } 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: self.is_runtime_api, lib: TsTypeLib::default(), permissions: self.permissions.clone(), file_permission_api_name: self .is_runtime_api .then_some("Deno.bundle()"), ext_overwrite: None, allow_unknown_media_types: true, allow_sloppy_imports_hints_for_unreferenced_roots: true, skip_graph_roots_validation: true, file_content_overrides: Default::default(), file_header_overrides: Default::default(), }, ) .await?; graph_permit.commit(); Ok(()) } /// When bundling via `Deno.bundle()`, reading a module returns its /// (transpiled) source to the calling program, so it must be gated on the /// caller's permissions. `deno bundle` reads its inputs unchecked like the /// other CLI subcommands, in which case this is a no-op. /// /// Graph preparation already performs the check before reading source. This /// second boundary covers later esbuild loads and direct asset fetches. fn check_read_permission<'a>( &self, specifier: &'a Url, ) -> Result<Cow<'a, Url>, BundleLoadError> { if self.is_runtime_api { if specifier.scheme() == "file" { let path = deno_path_util::url_to_file_path(specifier).map_err(|_| { deno_runtime::deno_permissions::PermissionCheckError::InvalidFilePath( specifier.clone(), ) })?; let checked_path = self.permissions.check_open( Cow::Owned(path), OpenAccessKind::Read, Some("Deno.bundle()"), )?; let checked_specifier = deno_path_util::url_from_file_path( &checked_path, ) .map_err(|_| { deno_runtime::deno_permissions::PermissionCheckError::InvalidFilePath( specifier.clone(), ) })?; return Ok(Cow::Owned(checked_specifier)); } else { self .permissions .check_specifier(specifier, CheckSpecifierKind::Dynamic)?; } } Ok(Cow::Borrowed(specifier)) } async fn bundle_load( &self, specifier: &str, requested_type: &RequestedModuleType<'_>, ) -> Result<Option<(Vec<u8>, 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 )?; if specifier.scheme() == "data" { return Ok(None); } 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 ); 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) }; // `specifier` is now the concrete module we're about to read the source of // (a `file:` path for local modules, including npm packages resolved into // `node_modules`). Gate the read on the caller's permissions. let checked_specifier = self.check_read_permission(&specifier)?; let graph = self.module_graph_container.graph(); let module_or_asset = self .module_loader .load(&graph, &specifier, None, requested_type, None) .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(&checked_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(&specifier, media_type, &value, None) .await?; return self .create_module_response( &graph, &specifier, media_type, source.as_bytes(), Some(requested_type), ) .await .map(Some); } else { return self .create_module_response( &graph, &specifier, 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: _, } => { let checked_specifier = self.check_read_permission(&specifier)?; LoadedModuleSource::ArcBytes( self .file_fetcher .fetch(&checked_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<u8>, 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)); } // `import sheet from "./a.css" with { type: "css" }` evaluates to a // `CSSStyleSheet` at runtime (see the custom module evaluation callback // in `runtime/worker.rs`). esbuild has no builtin loader that mirrors // this, so synthesize an equivalent JS module instead of routing the // source through esbuild's CSS loader (which would otherwise leave the // default import as an empty object). Some(RequestedModuleType::Other(ty)) if *ty == "css" => { let text = String::from_utf8_lossy(source); return Ok(( render_css_module(&text).into_bytes(), esbuild_client::BuiltinLoader::Js, )); } Some(RequestedModuleType::Other(_) | RequestedModuleType::None) | None => {} } if media_type == MediaType::Wasm { let code = wasm::render_js_wasm_module(source) .map_err(|e| BundleLoadErrorKind::WasmParse(e.to_string()))?; return Ok((code.into_bytes(), esbuild_client::BuiltinLoader::Js)); } 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 resolved_roots = self.resolved_roots.read().clone(); let code = tokio::task::spawn_blocking(move || { Self::apply_transform( &resolved_roots, &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<str>, is_known_script: Option<bool>, ) -> Result<Arc<str>, 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) } fn apply_transform( resolved_roots: &IndexSet<ModuleSpecifier>, module_graph_container: &MainModuleGraphContainer, specifier: &ModuleSpecifier, media_type: deno_ast::MediaType, code: &str, ) -> Result<String, BundleLoadError> { let graph = module_graph_container.graph(); let mut transform = transform::BundleImportMetaMainTransform::new( graph.roots.contains(specifier) || resolved_roots.contains(specifier), ); // A CJS module imported from ESM reaches us as an ESM facade the module // loader generated (`import { createRequire } ...; export default mod;`). // Parsing that under `MediaType::Cjs` forces script mode (see // deno_ast `refine_parse_mode`), which rejects the facade's import/export // statements with a parse error. Parse as JavaScript so program mode // auto-detects module vs script and handles both the facade and genuine // CJS scripts. let media_type = match media_type { deno_ast::MediaType::Cjs => deno_ast::MediaType::JavaScript, other => other, }; 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) } 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(wasm_module) => ( wasm_module.specifier.clone(), deno_ast::MediaType::Wasm, esbuild_client::BuiltinLoader::Js, ), deno_graph::Module::Npm(_) => { let req_ref = NpmPackageReqReference::from_specifier(specifier).unwrap(); let url = self.resolver.resolve_managed_npm_req_ref( &req_ref, 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<String, deno_path_util::UrlToFilePathError> { if url.scheme() == "file" { Ok( deno_path_util::url_to_file_path(&url)? .to_string_lossy() .into(), ) } else { Ok(url.into()) } } /// Renders a `with { type: "css" }` import as a JS module that constructs a /// constructable `CSSStyleSheet`, mirroring the runtime's custom module /// evaluation for CSS imports. fn render_css_module(text: &str) -> String { // `serde_json::to_string` produces a valid JS string literal with the CSS // text safely escaped. let literal = serde_json::to_string(text).unwrap(); format!( "const sheet = new CSSStyleSheet();\nsheet.replaceSync({literal});\nexport default sheet;\n" ) } 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, Jsonc => esbuild_client::BuiltinLoader::Text, Json5 => esbuild_client::BuiltinLoader::Text, Markdown => esbuild_client::BuiltinLoader::Text, 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, canonicalize: bool, ) -> Result<Url, AnyError> { 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)); // Runtime permission checks must see the caller's path spelling before // following aliases. `check_open` then returns the canonical path used for // the actual read. The trusted CLI path retains its historical eager // canonicalization. let canonicalized_path = canonicalize_path(&path)?; let path = if canonicalize { Cow::Owned(canonicalized_path) } else { path }; Ok(deno_path_util::url_from_file_path(&path)?) } } fn resolve_entrypoints( resolver: &CliResolver, init_cwd: &Path, entrypoints: &[String], canonicalize_entrypoints: bool, ) -> Result<Vec<Url>, AnyError> { let entrypoints = entrypoints .iter() .map(|e| { resolve_url_or_path_absolute(e, init_cwd, canonicalize_entrypoints) }) .collect::<Result<Vec<_>, _>>()?; 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 is_html_entrypoint(url: &Url) -> bool { url.scheme() == "file" && url.path().to_ascii_lowercase().ends_with(".html") } fn resolve_roots( entrypoints: Vec<Url>, cwd: &Path, npm_resolver: &CliNpmResolver, node_resolver: &CliNodeResolver, ) -> Vec<Url> { 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(cwd).unwrap(); let package_folder = npm_resolver .resolve_pkg_folder_from_deno_module_req(v.req(), &referrer) .unwrap(); let Ok(node_resolver::BinValue::JsFile(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<PathBuf, AnyError> { 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<String> { 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 bundle_flags.keep_names { builder.raw_flag("--keep-names"); } 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<String> { 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<Vec<u8>>, is_js: bool, } impl ProcessedContents { pub fn into_contents(self) -> Option<Vec<u8>> { self.contents } } 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<ProcessedContents, AnyError> { let path = &file.path; let is_js = is_js(path) || path.ends_with("<stdout>"); if is_js { let string = str::from_utf8(&file.contents)?; // The `createRequire` shim is Deno-specific (it injects an import from // `node:module`), so it is only applied for the Deno platform. The CJS // interop fix-up below is platform-independent and always runs. let string = if should_replace_require_shim { replace_require_shim(string, minified) } else { string.to_string() }; let string = force_node_cjs_interop(&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<String>, } 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<esbuild_client::protocol::BuildOutputFile> 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<Vec<OutputFile<'a>>, 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<OutputFile> = 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::<Vec<_>>() }) .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>, write_permissions: Option<&PermissionsContainer>, ) -> Result<Vec<OutputFileInfo>, 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("<stdout>") { deno_print::drop_write_stdout(bytes.as_ref()); continue; } let checked_path = write_permissions .map(|permissions| { permissions.check_open( Cow::Owned(path.to_path_buf()), OpenAccessKind::Write, Some("Deno.bundle()"), ) }) .transpose()?; let path = checked_path.as_deref().unwrap_or(path); if let Some(parent) = path.parent() && !exists_cache.contains(parent) { if !parent.exists() { let checked_parent = write_permissions .map(|permissions| { permissions.check_open( Cow::Owned(parent.to_path_buf()), OpenAccessKind::WriteNoFollow, Some("Deno.bundle()"), ) }) .transpose()?; std::fs::create_dir_all(checked_parent.as_deref().unwrap_or(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), )); 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!( "{:<longest$}", info.relative_path.display() )) } else { deno_terminal::colors::magenta(format!( "{:<longest$}", info.relative_path.display() )) }, deno_terminal::colors::gray( crate::display::human_size(info.size as f64,) ) )); } output.push('\n'); log::info!("{}", output); Ok(()) } #[cfg(test)] mod entrypoint_tests { use deno_core::url::Url; use super::is_html_entrypoint; #[test] fn html_entrypoint_detection_ignores_fragment() { assert!(is_html_entrypoint( &Url::parse("file:///tmp/index.html").unwrap() )); assert!(is_html_entrypoint( &Url::parse("file:///tmp/INDEX.HTML").unwrap() )); assert!(!is_html_entrypoint( &Url::parse("file:///tmp/secret.txt#.html").unwrap() )); assert!(!is_html_entrypoint( &Url::parse("https://example.com/index.html").unwrap() )); } } #[cfg(test)] mod declaration_flatten_tests { use std::collections::HashMap; use std::path::Path; use super::flatten_declarations; use super::resolve_relative_dts_path; fn dts_key(dir: &Path, rel: &str) -> String { resolve_relative_dts_path(dir, rel) .to_string_lossy() .to_string() } // A re-export chain is inlined into a single self-contained file: the // referenced declarations are pulled in and the now-redundant relative // `import type` / re-export lines are removed. #[test] fn flatten_inlines_reexport_chain_and_strips_import() { let entry = Path::new("/proj/mod.ts"); let dir = entry.parent().unwrap(); let mut map = HashMap::new(); map.insert( dts_key(dir, "./lib.ts"), "import type { Config } from \"./types.ts\";\n\ export declare class Client {\n}\n" .to_string(), ); map.insert( dts_key(dir, "./types.ts"), "export interface Config {\n}\n".to_string(), ); let entry_dts = "export { Client } from \"./lib.ts\";\n\ export type { Config } from \"./types.ts\";\n"; let out = flatten_declarations(entry_dts, entry, &map); assert!(out.contains("declare class Client")); assert!(out.contains("interface Config")); assert!( !out.contains("from \"./"), "flattened output should have no relative imports:\n{out}" ); } // Regression for the over-strip bug: a bare `import` of an in-graph module // that is emitted but never re-exported (so its declarations are NOT inlined) // must be left intact. Stripping it on the mere presence of the file in the // emitted set would leave the imported symbol undefined in the output. #[test] fn flatten_keeps_import_of_non_reexported_module() { let entry = Path::new("/proj/mod.ts"); let dir = entry.parent().unwrap(); let mut map = HashMap::new(); map.insert( dts_key(dir, "./lib.ts"), "import { Widget } from \"./internal.ts\";\n\ export declare class Client {\n widget(): Widget;\n}\n" .to_string(), ); // Emitted but only imported (for a return type), never re-exported. map.insert( dts_key(dir, "./internal.ts"), "export declare class Widget {\n}\n".to_string(), ); let entry_dts = "export { Client } from \"./lib.ts\";\n"; let out = flatten_declarations(entry_dts, entry, &map); assert!(out.contains("declare class Client")); assert!( out.contains("import { Widget }"), "import of a non-re-exported module must be preserved so the symbol \ still resolves:\n{out}" ); } }