deno/cli/tools/bundle/mod.rs
Nathan Whitaker 4e4bbf2fcc
feat(bundle): support html entrypoint (#29856)
For instance

`deno bundle --outdir dist index.html`

It will find scripts referenced in the html, bundle them, and then
update the paths in index.html for the bundled assets.

Right now it doesn't handle other assets (from `link` elements), but it
could
2025-09-09 12:18:10 -07:00

2030 lines
58 KiB
Rust

// 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<bool> =
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<BundlerInput, AnyError> {
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<Flags>,
bundle_flags: &BundleFlags,
) -> 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();
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<Flags>,
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<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 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<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
.unwrap()
.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
.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: &regex::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) = &note.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, 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<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,
}
}
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: IndexMap<String, VirtualModule>,
}
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<CliFileFetcher>,
resolver: Arc<CliResolver>,
module_load_preparer: Arc<ModuleLoadPreparer>,
module_graph_container: Arc<MainModuleGraphContainer>,
permissions: PermissionsContainer,
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>,
}
impl DenoPluginHandler {
fn take_deferred_resolve_errors(&self) -> Vec<DeferredResolveError> {
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<protocol::ImportKind> 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<String>,
kind: PluginImportKind,
namespace: Option<String>,
resolve_dir: Option<String>,
with: IndexMap<String, String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct PluginOnLoadArgs {
path: String,
namespace: String,
suffix: String,
with: IndexMap<String, String>,
}
#[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()
}));
}
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<Option<esbuild_client::OnLoadResult>, 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<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, 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<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::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<String, String>,
) -> Result<Option<String>, 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<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
)?;
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<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));
}
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<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)
}
#[allow(clippy::result_large_err)]
fn apply_transform(
module_graph_container: &MainModuleGraphContainer,
specifier: &ModuleSpecifier,
media_type: deno_ast::MediaType,
code: &str,
) -> Result<String, BundleLoadError> {
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<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())
}
}
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<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));
let path = path.canonicalize()?;
Ok(deno_path_util::url_from_file_path(&path)?)
}
}
fn resolve_entrypoints(
resolver: &CliResolver,
init_cwd: &Path,
entrypoints: &[String],
) -> Result<Vec<Url>, AnyError> {
let entrypoints = entrypoints
.iter()
.map(|e| resolve_url_or_path_absolute(e, init_cwd))
.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 resolve_roots(
entrypoints: Vec<Url>,
sys: CliSys,
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(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<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 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,
}
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)?;
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<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>,
) -> 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>") {
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!(
"{:<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(())
}