feat(bundle, unstable): bundling backed by esbuild (#29470)
Some checks are pending
ci / pre-build (push) Waiting to run
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / build wasm32 (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions

todo:
- [ ] cleanup cli, decide what flags we want to commit to
- [x] decide what to do about node addons - (you can mark them external
via `--external`)
- [x] move `esbuild_rs` to the `denoland` org
- [x] figure out the dynamic require issue
- [x] figure out how to test this
- [x] clean up / revert all the random changes
This commit is contained in:
Nathan Whitaker 2025-06-07 12:20:10 -07:00 committed by GitHub
parent 1323aca15e
commit 7a837f9fdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1548 additions and 156 deletions

2
.gitignore vendored
View file

@ -43,3 +43,5 @@ Untitled*.ipynb
# playwright browser binary cache
/.ms-playwright
**/.claude/settings.local.json

112
Cargo.lock generated
View file

@ -193,9 +193,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.95"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arbitrary"
@ -1493,6 +1493,7 @@ dependencies = [
"dprint-plugin-jupyter",
"dprint-plugin-markdown",
"dprint-plugin-typescript",
"esbuild_client",
"eszip",
"fancy-regex",
"faster-hex",
@ -1503,7 +1504,7 @@ dependencies = [
"http-body 1.0.0",
"http-body-util",
"import_map",
"indexmap 2.8.0",
"indexmap 2.9.0",
"jsonc-parser",
"lazy-regex",
"libc",
@ -1692,7 +1693,7 @@ dependencies = [
"deno_media_type",
"deno_path_util",
"http 1.1.0",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"once_cell",
"parking_lot",
@ -1732,7 +1733,7 @@ dependencies = [
"glob",
"ignore",
"import_map",
"indexmap 2.8.0",
"indexmap 2.9.0",
"jsonc-parser",
"log",
"percent-encoding",
@ -1771,7 +1772,7 @@ dependencies = [
"deno_path_util",
"deno_unsync",
"futures",
"indexmap 2.8.0",
"indexmap 2.9.0",
"libc",
"parking_lot",
"percent-encoding",
@ -1863,7 +1864,7 @@ dependencies = [
"handlebars",
"html-escape",
"import_map",
"indexmap 2.8.0",
"indexmap 2.9.0",
"itoa",
"js-sys",
"lazy_static",
@ -2015,7 +2016,7 @@ dependencies = [
"encoding_rs",
"futures",
"import_map",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"monch",
"once_cell",
@ -2147,7 +2148,7 @@ dependencies = [
"deno_terminal 0.2.2",
"env_logger",
"faster-hex",
"indexmap 2.8.0",
"indexmap 2.9.0",
"libsui",
"log",
"node_resolver",
@ -2358,7 +2359,7 @@ dependencies = [
"deno_lockfile",
"deno_semver",
"futures",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"monch",
"serde",
@ -2442,7 +2443,7 @@ version = "0.226.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28b12489187c71fa123731cc783d48beb17ae5df04da991909cc2ae5a3d0ef9"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"proc-macro-rules",
"proc-macro2",
"quote",
@ -2484,7 +2485,7 @@ dependencies = [
"deno_error",
"deno_path_util",
"deno_semver",
"indexmap 2.8.0",
"indexmap 2.9.0",
"serde",
"serde_json",
"sys_traits",
@ -2594,7 +2595,7 @@ dependencies = [
"futures",
"http 1.1.0",
"import_map",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"node_resolver",
"once_cell",
@ -2854,7 +2855,7 @@ dependencies = [
"deno_core",
"deno_error",
"deno_unsync",
"indexmap 2.8.0",
"indexmap 2.9.0",
"raw-window-handle",
"serde",
"serde_json",
@ -3003,7 +3004,7 @@ dependencies = [
"deno_snapshots",
"deno_terminal 0.2.2",
"import_map",
"indexmap 2.8.0",
"indexmap 2.9.0",
"libsui",
"log",
"memmap2",
@ -3088,18 +3089,18 @@ dependencies = [
[[package]]
name = "derive_builder"
version = "0.20.0"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.0"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
@ -3109,9 +3110,9 @@ dependencies = [
[[package]]
name = "derive_builder_macro"
version = "0.20.0"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.87",
@ -3255,7 +3256,7 @@ dependencies = [
"anyhow",
"bumpalo",
"hashbrown 0.15.2",
"indexmap 2.8.0",
"indexmap 2.9.0",
"rustc-hash 2.1.1",
"serde",
"unicode-width 0.2.0",
@ -3451,7 +3452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cede2bb1b07dd598d269f973792c43e0cd92686d3b452bd6e01d7a8eb01211"
dependencies = [
"debug-ignore",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"thiserror 1.0.69",
"zerocopy",
@ -3571,6 +3572,23 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8"
[[package]]
name = "esbuild_client"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241d235e88635b3622ee664b3ed9bf70cb286b3067cff2ea2b9be11c7e2f8d19"
dependencies = [
"anyhow",
"async-trait",
"deno_unsync",
"derive_builder",
"indexmap 2.9.0",
"log",
"parking_lot",
"paste",
"tokio",
]
[[package]]
name = "eszip"
version = "0.92.0"
@ -3587,7 +3605,7 @@ dependencies = [
"deno_semver",
"futures",
"hashlink 0.8.4",
"indexmap 2.8.0",
"indexmap 2.9.0",
"serde",
"serde_json",
"sha2",
@ -4042,7 +4060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
dependencies = [
"fallible-iterator",
"indexmap 2.8.0",
"indexmap 2.9.0",
"stable_deref_trait",
]
@ -4189,7 +4207,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.8.0",
"indexmap 2.9.0",
"slab",
"tokio",
"tokio-util",
@ -4208,7 +4226,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.1.0",
"indexmap 2.8.0",
"indexmap 2.9.0",
"slab",
"tokio",
"tokio-util",
@ -4869,7 +4887,7 @@ checksum = "f315e535cb94a0e80704278d630990bb48834c8c8d976acf0a2f6bc8fede7c38"
dependencies = [
"boxed_error",
"deno_error",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"percent-encoding",
"serde",
@ -4890,9 +4908,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@ -5583,7 +5601,7 @@ dependencies = [
"cfg_aliases",
"codespan-reporting",
"hexf-parse",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"rustc-hash 1.1.0",
"serde",
@ -6146,9 +6164,9 @@ dependencies = [
[[package]]
name = "paste"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "path-clean"
@ -6248,7 +6266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset 0.4.2",
"indexmap 2.8.0",
"indexmap 2.9.0",
]
[[package]]
@ -6258,7 +6276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset 0.5.7",
"indexmap 2.8.0",
"indexmap 2.9.0",
]
[[package]]
@ -6631,7 +6649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1a341ae463320e9f8f34adda49c8a85d81d4e8f34cce4397fb0350481552224"
dependencies = [
"chrono",
"indexmap 2.8.0",
"indexmap 2.9.0",
"quick-xml",
"strip-ansi-escapes",
"thiserror 1.0.69",
@ -7446,7 +7464,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"itoa",
"memchr",
"ryu",
@ -7906,7 +7924,7 @@ checksum = "ebb953a99152e2a62f6b84d6c144fc4adf9c406093b093046e90a681a76d93dd"
dependencies = [
"anyhow",
"crc",
"indexmap 2.8.0",
"indexmap 2.9.0",
"is-macro",
"once_cell",
"parking_lot",
@ -7963,7 +7981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01bfcbbdea182bdda93713aeecd997749ae324686bf7944f54d128e56be4ea9"
dependencies = [
"anyhow",
"indexmap 2.8.0",
"indexmap 2.9.0",
"serde",
"serde_json",
"swc_config_macro",
@ -8111,7 +8129,7 @@ checksum = "6856da3da598f4da001b7e4ce225ee8970bc9d5cbaafcaf580190cf0a6031ec5"
dependencies = [
"better_scoped_tls",
"bitflags 2.8.0",
"indexmap 2.8.0",
"indexmap 2.9.0",
"once_cell",
"par-core",
"phf",
@ -8160,7 +8178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2b14c8f6c1c82b7556d7534de44714401901fcb9b618f817172015565ce7d47"
dependencies = [
"dashmap",
"indexmap 2.8.0",
"indexmap 2.9.0",
"once_cell",
"par-core",
"petgraph 0.7.1",
@ -8205,7 +8223,7 @@ checksum = "baae39c70229103a72090119887922fc5e32f934f5ca45c0423a5e65dac7e549"
dependencies = [
"base64 0.22.1",
"dashmap",
"indexmap 2.8.0",
"indexmap 2.9.0",
"once_cell",
"rustc-hash 2.1.1",
"serde",
@ -8248,7 +8266,7 @@ version = "13.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ed837406d5dbbfbf5792b1dc90964245a0cf659753d4745fe177ffebe8598b9"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"num_cpus",
"once_cell",
"par-core",
@ -8848,7 +8866,7 @@ version = "0.22.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"toml_datetime",
"winnow 0.7.1",
]
@ -9293,7 +9311,7 @@ checksum = "97599c400fc79925922b58303e98fcb8fa88f573379a08ddb652e72cbd2e70f6"
dependencies = [
"bitflags 2.8.0",
"encoding_rs",
"indexmap 2.8.0",
"indexmap 2.9.0",
"num-bigint",
"serde",
"thiserror 1.0.69",
@ -9568,7 +9586,7 @@ dependencies = [
"bitflags 2.8.0",
"cfg_aliases",
"document-features",
"indexmap 2.8.0",
"indexmap 2.9.0",
"log",
"naga",
"once_cell",
@ -10231,7 +10249,7 @@ dependencies = [
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap 2.8.0",
"indexmap 2.9.0",
"memchr",
"thiserror 2.0.12",
]

View file

@ -120,6 +120,7 @@ dprint-plugin-json.workspace = true
dprint-plugin-jupyter.workspace = true
dprint-plugin-markdown.workspace = true
dprint-plugin-typescript.workspace = true
esbuild_client = { version = "0.1.1" }
fancy-regex.workspace = true
faster-hex.workspace = true
# If you disable the default __vendored_zlib_ng feature above, you _must_ be able to link against `-lz`.

View file

@ -464,12 +464,57 @@ pub struct CleanFlags {
pub dry_run: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BundleFlags {
pub entrypoints: Vec<String>,
pub output_path: Option<String>,
pub output_dir: Option<String>,
pub external: Vec<String>,
pub format: BundleFormat,
pub minify: bool,
pub code_splitting: bool,
pub one_file: bool,
pub packages: PackageHandling,
}
#[derive(Clone, Debug, Eq, PartialEq, Copy)]
pub enum BundleFormat {
Esm,
Cjs,
Iife,
}
impl std::fmt::Display for BundleFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BundleFormat::Esm => write!(f, "esm"),
BundleFormat::Cjs => write!(f, "cjs"),
BundleFormat::Iife => write!(f, "iife"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Copy)]
pub enum PackageHandling {
Bundle,
External,
}
impl std::fmt::Display for PackageHandling {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PackageHandling::Bundle => write!(f, "bundle"),
PackageHandling::External => write!(f, "external"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DenoSubcommand {
Add(AddFlags),
Remove(RemoveFlags),
Bench(BenchFlags),
Bundle,
Bundle(BundleFlags),
Cache(CacheFlags),
Check(CheckFlags),
Clean(CleanFlags),
@ -1404,7 +1449,7 @@ pub fn flags_from_vec(args: Vec<OsString>) -> clap::error::Result<Flags> {
"add" => add_parse(&mut flags, &mut m)?,
"remove" => remove_parse(&mut flags, &mut m),
"bench" => bench_parse(&mut flags, &mut m)?,
"bundle" => bundle_parse(&mut flags, &mut m),
"bundle" => bundle_parse(&mut flags, &mut m)?,
"cache" => cache_parse(&mut flags, &mut m)?,
"check" => check_parse(&mut flags, &mut m)?,
"clean" => clean_parse(&mut flags, &mut m),
@ -1866,10 +1911,105 @@ If you specify a directory instead of a file, the path is expanded to all contai
}
fn bundle_subcommand() -> Command {
command("bundle", "`deno bundle` was removed in Deno 2.
fn format_parser(s: &str) -> Result<BundleFormat, clap::Error> {
match s {
"esm" => Ok(BundleFormat::Esm),
"cjs" => Ok(BundleFormat::Cjs),
"iife" => Ok(BundleFormat::Iife),
_ => Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)),
}
}
fn packages_parser(s: &str) -> Result<PackageHandling, clap::Error> {
match s {
"bundle" => Ok(PackageHandling::Bundle),
"external" => Ok(PackageHandling::External),
_ => Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)),
}
}
command(
"bundle",
"Output a single JavaScript file with all dependencies.
See the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs.deno.com/runtime/manual/advanced/migrate_deprecations", UnstableArgsConfig::ResolutionOnly)
.hide(true)
deno bundle https://deno.land/std/examples/colors.ts colors.bundle.js
If no output file is given, the output is written to standard output:
deno bundle https://deno.land/std/examples/colors.ts
",
UnstableArgsConfig::ResolutionOnly,
)
.defer(|cmd| {
compile_args(cmd)
.arg(check_arg(false))
.arg(
Arg::new("file")
.num_args(1..)
.required_unless_present("help")
.value_hint(ValueHint::FilePath),
)
.arg(
Arg::new("output")
.long("output")
.short('o')
.help("Output path`")
.num_args(1)
.value_parser(value_parser!(String))
.value_hint(ValueHint::FilePath),
)
.arg(
Arg::new("outdir")
.long("outdir")
.help("Output directory for bundled files")
.num_args(1)
.value_parser(value_parser!(String))
.value_hint(ValueHint::DirPath),
)
.arg(
Arg::new("external")
.long("external")
.action(ArgAction::Append)
.num_args(1)
.value_parser(value_parser!(String)),
)
.arg(
Arg::new("format")
.long("format")
.value_parser(clap::builder::ValueParser::new(format_parser))
.default_value("esm"),
)
.arg(
Arg::new("packages")
.long("packages")
.help("How to handle packages. Accepted values are 'bundle' or 'external'")
.value_parser(clap::builder::ValueParser::new(packages_parser))
.default_value("bundle"),
)
.arg(
Arg::new("minify")
.long("minify")
.help("Minify the output")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("code-splitting")
.long("code-splitting")
.help("Enable code splitting")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("one-file")
.long("one-file")
.help("Bundle into one file")
.require_equals(true)
.default_value("true")
.default_missing_value("true")
.value_parser(value_parser!(bool))
.num_args(0..=1)
.action(ArgAction::Set),
)
.arg(allow_scripts_arg())
.arg(allow_import_arg())
})
}
fn cache_subcommand() -> Command {
@ -4677,8 +4817,30 @@ fn bench_parse(
Ok(())
}
fn bundle_parse(flags: &mut Flags, _matches: &mut ArgMatches) {
flags.subcommand = DenoSubcommand::Bundle;
fn bundle_parse(
flags: &mut Flags,
matches: &mut ArgMatches,
) -> clap::error::Result<()> {
let file = matches.remove_many::<String>("file").unwrap();
let output = matches.remove_one::<String>("output");
let outdir = matches.remove_one::<String>("outdir");
compile_args_without_check_parse(flags, matches)?;
unstable_args_parse(flags, matches, UnstableArgsConfig::ResolutionAndRuntime);
flags.subcommand = DenoSubcommand::Bundle(BundleFlags {
entrypoints: file.collect(),
output_path: output,
output_dir: outdir,
external: matches
.remove_many::<String>("external")
.map(|f| f.collect::<Vec<_>>())
.unwrap_or_default(),
format: matches.remove_one::<BundleFormat>("format").unwrap(),
packages: matches.remove_one::<PackageHandling>("packages").unwrap(),
minify: matches.get_flag("minify"),
code_splitting: matches.get_flag("code-splitting"),
one_file: matches.get_flag("one-file"),
});
Ok(())
}
fn cache_parse(

View file

@ -755,6 +755,12 @@ impl CliFactory {
self.cjs_module_export_analyzer().await?;
Ok(Arc::new(NodeCodeTranslator::new(
module_export_analyzer.clone(),
match self.cli_options()?.sub_command() {
DenoSubcommand::Bundle(_) => {
node_resolver::analyze::NodeCodeTranslatorMode::Bundling
}
_ => node_resolver::analyze::NodeCodeTranslatorMode::ModuleLoader,
},
)))
}
.boxed_local(),
@ -1014,24 +1020,14 @@ impl CliFactory {
.await
}
pub async fn create_cli_main_worker_factory_with_roots(
pub async fn create_module_loader_factory(
&self,
roots: LibWorkerFactoryRoots,
) -> Result<CliMainWorkerFactory, AnyError> {
) -> Result<CliModuleLoaderFactory, AnyError> {
let cli_options = self.cli_options()?;
let fs = self.fs();
let node_resolver = self.node_resolver().await?;
let npm_resolver = self.npm_resolver().await?;
let cli_npm_resolver = self.npm_resolver().await?.clone();
let in_npm_pkg_checker = self.in_npm_pkg_checker()?;
let maybe_file_watcher_communicator = if cli_options.has_hmr() {
Some(self.watcher_communicator.clone().unwrap())
} else {
None
};
let node_code_translator = self.node_code_translator().await?;
let cjs_tracker = self.cjs_tracker()?.clone();
let pkg_json_resolver = self.pkg_json_resolver()?;
let npm_req_resolver = self.npm_req_resolver()?;
let workspace_factory = self.workspace_factory()?;
let npm_registry_permission_checker = {
@ -1080,6 +1076,25 @@ impl CliFactory {
maybe_eszip_loader,
);
Ok(module_loader_factory)
}
pub async fn create_cli_main_worker_factory_with_roots(
&self,
roots: LibWorkerFactoryRoots,
) -> Result<CliMainWorkerFactory, AnyError> {
let cli_options = self.cli_options()?;
let fs = self.fs();
let node_resolver = self.node_resolver().await?;
let npm_resolver = self.npm_resolver().await?;
let maybe_file_watcher_communicator = if cli_options.has_hmr() {
Some(self.watcher_communicator.clone().unwrap())
} else {
None
};
let pkg_json_resolver = self.pkg_json_resolver()?;
let module_loader_factory = self.create_module_loader_factory().await?;
let lib_main_worker_factory = LibMainWorkerFactory::new(
self.blob_store().clone(),
if cli_options.code_cache_enabled() {

View file

@ -127,7 +127,15 @@ async fn run_subcommand(
tools::bench::run_benchmarks(flags, bench_flags).await
}
}),
DenoSubcommand::Bundle => exit_with_message("⚠️ `deno bundle` was removed in Deno 2.\n\nSee the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs.deno.com/runtime/manual/advanced/migrate_deprecations", 1),
DenoSubcommand::Bundle(bundle_flags) => {
spawn_subcommand(async {
log::warn!(
"⚠️ {} is experimental and subject to changes",
colors::cyan("deno bundle")
);
tools::bundle::bundle(flags, bundle_flags).await
})
},
DenoSubcommand::Deploy => {
spawn_subcommand(async { tools::deploy::deploy(flags, roots).await })
}

View file

@ -817,8 +817,10 @@ pub async fn run(
pkg_json_resolver.clone(),
sys.clone(),
));
let node_code_translator =
Arc::new(NodeCodeTranslator::new(cjs_module_export_analyzer));
let node_code_translator = Arc::new(NodeCodeTranslator::new(
cjs_module_export_analyzer,
node_resolver::analyze::NodeCodeTranslatorMode::ModuleLoader,
));
let workspace_resolver = {
let import_map = match metadata.workspace_resolver.import_map {
Some(import_map) => Some(

118
cli/tools/bundle/esbuild.rs Normal file
View file

@ -0,0 +1,118 @@
// Copyright 2018-2025 the Deno authors. MIT license.
use std::path::PathBuf;
use std::sync::Arc;
use deno_core::anyhow;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_npm::npm_rc::ResolvedNpmRc;
use deno_npm::registry::NpmRegistryApi;
use deno_npm_cache::TarballCache;
use deno_resolver::workspace::WorkspaceNpmPatchPackages;
use deno_semver::package::PackageNv;
use crate::cache::DenoDir;
use crate::npm::CliNpmCache;
use crate::npm::CliNpmCacheHttpClient;
use crate::npm::CliNpmRegistryInfoProvider;
use crate::sys::CliSys;
pub const ESBUILD_VERSION: &str = "0.25.5";
fn esbuild_platform() -> &'static str {
match (std::env::consts::ARCH, std::env::consts::OS) {
("x86_64", "linux") => "linux-x64",
("aarch64", "linux") => "linux-arm64",
("x86_64", "macos" | "apple") => "darwin-x64",
("aarch64", "macos" | "apple") => "darwin-arm64",
("x86_64", "windows") => "win32-x64",
("aarch64", "windows") => "win32-arm64",
_ => panic!(
"Unsupported platform: {} {}",
std::env::consts::ARCH,
std::env::consts::OS
),
}
}
pub async fn ensure_esbuild(
deno_dir: &DenoDir,
npmrc: &ResolvedNpmRc,
npm_registry_info: &Arc<CliNpmRegistryInfoProvider>,
workspace_patch_packages: &Arc<WorkspaceNpmPatchPackages>,
tarball_cache: &Arc<TarballCache<CliNpmCacheHttpClient, CliSys>>,
npm_cache: &CliNpmCache,
) -> Result<PathBuf, AnyError> {
let target = esbuild_platform();
let mut esbuild_path = deno_dir
.dl_folder_path()
.join(format!("esbuild-{}", ESBUILD_VERSION))
.join(format!("esbuild-{}", target));
if cfg!(windows) {
esbuild_path.set_extension("exe");
}
if esbuild_path.exists() {
return Ok(esbuild_path);
}
let pkg_name = format!("@esbuild/{}", target);
let nv =
PackageNv::from_str(&format!("{}@{}", pkg_name, ESBUILD_VERSION)).unwrap();
let api = npm_registry_info.as_npm_registry_api();
let info = api.package_info(&pkg_name).await?;
let version_info = info.version_info(&nv, &workspace_patch_packages.0)?;
if let Some(dist) = &version_info.dist {
let registry_url = npmrc.get_registry_url(&nv.name);
let package_folder =
npm_cache.package_folder_for_nv_and_url(&nv, registry_url);
let existed = package_folder.exists();
if !existed {
tarball_cache
.ensure_package(&nv, dist)
.await
.with_context(|| {
format!(
"failed to download esbuild package tarball {} from {}",
nv, dist.tarball
)
})?;
}
let path = if cfg!(windows) {
package_folder.join("esbuild.exe")
} else {
package_folder.join("bin").join("esbuild")
};
std::fs::create_dir_all(esbuild_path.parent().unwrap()).with_context(
|| {
format!(
"failed to create directory {}",
esbuild_path.parent().unwrap().display()
)
},
)?;
std::fs::copy(&path, &esbuild_path).with_context(|| {
format!(
"failed to copy esbuild binary from {} to {}",
path.display(),
esbuild_path.display()
)
})?;
if !existed {
std::fs::remove_dir_all(&package_folder).with_context(|| {
format!("failed to remove directory {}", package_folder.display())
})?;
}
Ok(esbuild_path)
} else {
anyhow::bail!(
"could not get fetch esbuild binary; download it manually and copy it to {}",
esbuild_path.display()
);
}
}

703
cli/tools/bundle/mod.rs Normal file
View file

@ -0,0 +1,703 @@
// Copyright 2018-2025 the Deno authors. MIT license.
mod esbuild;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use deno_ast::ModuleSpecifier;
use deno_config::deno_json::TsTypeLib;
use deno_core::error::AnyError;
use deno_core::resolve_url_or_path;
use deno_core::url::Url;
use deno_core::ModuleLoader;
use deno_error::JsError;
use deno_graph::Position;
use deno_lib::worker::ModuleLoaderFactory;
use deno_resolver::npm::managed::ResolvePkgFolderFromDenoModuleError;
use deno_runtime::deno_permissions::PermissionsContainer;
use deno_semver::npm::NpmPackageReqReference;
use esbuild_client::protocol;
use esbuild_client::EsbuildFlagsBuilder;
use esbuild_client::EsbuildService;
use indexmap::IndexMap;
use node_resolver::errors::PackageSubpathResolveError;
use node_resolver::NodeResolutionKind;
use node_resolver::ResolutionMode;
use regex::Regex;
use sys_traits::EnvCurrentDir;
use crate::args::BundleFlags;
use crate::args::BundleFormat;
use crate::args::Flags;
use crate::args::PackageHandling;
use crate::factory::CliFactory;
use crate::graph_container::MainModuleGraphContainer;
use crate::graph_container::ModuleGraphContainer;
use crate::graph_container::ModuleGraphUpdatePermit;
use crate::module_loader::ModuleLoadPreparer;
use crate::module_loader::PrepareModuleLoadOptions;
use crate::node::CliNodeResolver;
use crate::npm::CliNpmResolver;
use crate::resolver::CliResolver;
/// Given a set of pattern indicating files to mark as external,
/// return a regex that matches any of those patterns.
///
/// For instance given, `--external="*.node" --external="*.wasm"`, the regex will match
/// any path that ends with `.node` or `.wasm`.
pub fn externals_regex(external: &[String]) -> Regex {
let mut regex_str = String::new();
for (i, e) in external.iter().enumerate() {
if i > 0 {
regex_str.push('|');
}
regex_str.push_str("(^");
if e.starts_with("/") {
regex_str.push_str(".*");
}
regex_str.push_str(&regex::escape(e).replace("\\*", ".*"));
regex_str.push(')');
}
regex::Regex::new(&regex_str).unwrap()
}
pub async fn bundle(
flags: Arc<Flags>,
bundle_flags: BundleFlags,
) -> Result<(), AnyError> {
let factory = CliFactory::from_flags(flags);
let installer_factory = factory.npm_installer_factory()?;
let npmrc = factory.npmrc()?;
let deno_dir = factory.deno_dir()?;
let resolver_factory = factory.resolver_factory()?;
let workspace_factory = resolver_factory.workspace_factory();
let npm_registry_info = installer_factory.registry_info_provider()?;
let esbuild_path = esbuild::ensure_esbuild(
deno_dir,
npmrc,
npm_registry_info,
workspace_factory.workspace_npm_patch_packages()?,
installer_factory.tarball_cache()?,
factory.npm_cache()?,
)
.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?.clone();
let node_resolver = factory.node_resolver().await?.clone();
let cli_options = factory.cli_options()?;
let module_loader = factory
.create_module_loader_factory()
.await?
.create_for_main(root_permissions.clone())
.module_loader;
let sys = factory.sys();
let init_cwd = cli_options.initial_cwd().canonicalize()?;
#[allow(clippy::arc_with_non_send_sync)]
let plugin_handler = Arc::new(DenoPluginHandler {
resolver: resolver.clone(),
module_load_preparer,
module_graph_container: factory
.main_module_graph_container()
.await?
.clone(),
permissions: root_permissions.clone(),
npm_resolver: npm_resolver.clone(),
node_resolver: node_resolver.clone(),
module_loader: module_loader.clone(),
// TODO(nathanwhit): look at the external patterns to give diagnostics for probably incorrect patterns
externals_regex: if bundle_flags.external.is_empty() {
None
} else {
Some(externals_regex(&bundle_flags.external))
},
});
let start = std::time::Instant::now();
let entrypoint = bundle_flags
.entrypoints
.first()
.iter()
.map(|e| resolve_url_or_path(e, &init_cwd).unwrap())
.collect::<Vec<_>>();
let resolved = {
let mut resolved = vec![];
let init_cwd_url = Url::from_directory_path(&init_cwd).unwrap();
for e in &entrypoint {
let r = resolver
.resolve(
e.as_str(),
&init_cwd_url,
Position::new(0, 0),
ResolutionMode::Import,
NodeResolutionKind::Execution,
)
.unwrap();
resolved.push(r);
}
resolved
};
let _ = plugin_handler.prepare_module_load(&resolved).await;
let roots = resolved
.into_iter()
.map(|url| {
if let Ok(v) = NpmPackageReqReference::from_specifier(&url) {
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 main_module = node_resolver
.resolve_binary_export(&package_folder, v.sub_path())
.unwrap();
Url::from_file_path(&main_module).unwrap()
} else {
url
}
})
.collect::<Vec<_>>();
let _ = plugin_handler.prepare_module_load(&roots).await;
let esbuild = EsbuildService::new(
esbuild_path,
esbuild::ESBUILD_VERSION,
plugin_handler.clone(),
)
.await
.unwrap();
let client = esbuild.client().clone();
{
tokio::spawn(async move {
let res = esbuild.wait_for_exit().await;
log::warn!("esbuild exited: {:?}", res);
});
}
let mut builder = EsbuildFlagsBuilder::default();
builder
.bundle(bundle_flags.one_file)
.minify(bundle_flags.minify)
.splitting(bundle_flags.code_splitting)
.external(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(outdir) = bundle_flags.output_dir.clone() {
builder.outdir(outdir);
} else if let Some(output_path) = bundle_flags.output_path.clone() {
builder.outfile(output_path);
}
let flags = builder.build().unwrap();
let entries = roots.into_iter().map(|e| ("".into(), e.into())).collect();
let response = client
.send_build_request(protocol::BuildRequest {
entries,
key: 0,
flags: flags.to_flags(),
write: true,
stdin_contents: None.into(),
stdin_resolve_dir: None.into(),
abs_working_dir: init_cwd.to_string_lossy().to_string(),
context: false,
mangle_cache: None,
node_paths: vec![],
plugins: Some(vec![protocol::BuildPlugin {
name: "deno".into(),
on_start: false,
on_end: false,
on_resolve: (vec![protocol::OnResolveSetupOptions {
id: 0,
filter: ".*".into(),
namespace: "".into(),
}]),
on_load: vec![protocol::OnLoadSetupOptions {
id: 0,
filter: ".*".into(),
namespace: "".into(),
}],
}]),
})
.await
.unwrap();
for error in &response.errors {
log::error!(
"{}: {}",
deno_terminal::colors::red("bundler error"),
format_message(error)
);
}
for warning in &response.warnings {
log::warn!(
"{}: {}",
deno_terminal::colors::yellow("bundler warning"),
format_message(warning)
);
}
if let Some(stdout) = response.write_to_stdout {
let stdout = replace_require_shim(&String::from_utf8_lossy(&stdout));
#[allow(clippy::print_stdout)]
{
println!("{}", stdout);
}
} else if response.errors.is_empty() {
if bundle_flags.output_dir.is_none()
&& std::env::var("NO_DENO_BUNDLE_HACK").is_err()
&& bundle_flags.output_path.is_some()
{
let out = bundle_flags.output_path.as_ref().unwrap();
let contents = std::fs::read_to_string(out).unwrap();
let contents = replace_require_shim(&contents);
std::fs::write(out, contents).unwrap();
}
log::info!(
"{}",
deno_terminal::colors::green(format!(
"bundled in {}",
crate::display::human_elapsed(start.elapsed().as_millis()),
))
);
}
if !response.errors.is_empty() {
deno_core::anyhow::bail!("bundling failed");
}
Ok(())
}
// 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) -> String {
contents.replace(
r#"var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});"#,
r#"import { createRequire } from "node:module";
var __require = createRequire(import.meta.url);
"#,
)
}
fn format_message(message: &esbuild_client::protocol::Message) -> String {
format!(
"{}{}",
message.text,
if let Some(location) = &message.location {
format!(
"\n at {} {}:{}",
location.file, location.line, location.column
)
} 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,
}
struct DenoPluginHandler {
resolver: Arc<CliResolver>,
module_load_preparer: Arc<ModuleLoadPreparer>,
module_graph_container: Arc<MainModuleGraphContainer>,
permissions: PermissionsContainer,
npm_resolver: CliNpmResolver,
node_resolver: Arc<CliNodeResolver>,
module_loader: Rc<dyn ModuleLoader>,
externals_regex: Option<Regex>,
}
#[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(reg) = &self.externals_regex {
if reg.is_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,
)?;
Ok(result.map(|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(
r.starts_with("node:")
|| self
.externals_regex
.as_ref()
.map(|reg| reg.is_match(&r))
.unwrap_or(false),
),
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> {
let result = self.bundle_load(&args.path, "").await?;
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)
}
}
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,
}
}
impl DenoPluginHandler {
fn bundle_resolve(
&self,
path: &str,
importer: Option<&str>,
resolve_dir: Option<&str>,
kind: esbuild_client::protocol::ImportKind,
// TODO: use this / store it for later usage when loading
with: IndexMap<String, String>,
) -> Result<Option<String>, AnyError> {
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),
import_kind_to_resolution_mode(kind),
NodeResolutionKind::Execution,
);
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);
Err(BundleError::Resolver(e).into())
}
}
}
async fn prepare_module_load(
&self,
specifiers: &[ModuleSpecifier],
) -> Result<(), AnyError> {
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,
resolve_dir: &str,
) -> Result<Option<(Vec<u8>, esbuild_client::BuiltinLoader)>, AnyError> {
log::debug!(
"{}: {:?} {:?}",
deno_terminal::colors::magenta("bundle_load"),
specifier,
resolve_dir
);
let resolve_dir = Path::new(&resolve_dir);
let specifier = deno_core::resolve_url_or_path(specifier, resolve_dir)?;
let (specifier, loader) = if let Some((specifier, loader)) =
self.specifier_and_type_from_graph(&specifier)?
{
(specifier, loader)
} 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_to_loader(media_type))
};
let loaded = self.module_loader.load(
&specifier,
None,
false,
deno_core::RequestedModuleType::None,
);
match loaded {
deno_core::ModuleLoadResponse::Sync(module_source) => {
Ok(Some((module_source?.code.as_bytes().to_vec(), loader)))
}
deno_core::ModuleLoadResponse::Async(pin) => {
let pin = pin.await?;
Ok(Some((pin.code.as_bytes().to_vec(), loader)))
}
}
}
fn specifier_and_type_from_graph(
&self,
specifier: &ModuleSpecifier,
) -> Result<Option<(ModuleSpecifier, esbuild_client::BuiltinLoader)>, AnyError>
{
let graph = self.module_graph_container.graph();
let Some(module) = graph.get(specifier) else {
return Ok(None);
};
let (specifier, loader) = match module {
deno_graph::Module::Js(js_module) => (
js_module.specifier.clone(),
media_type_to_loader(js_module.media_type),
),
deno_graph::Module::Json(json_module) => (
json_module.specifier.clone(),
esbuild_client::BuiltinLoader::Json,
),
deno_graph::Module::Wasm(_) => todo!(),
deno_graph::Module::Npm(module) => {
let package_folder = self
.npm_resolver
.as_managed()
.unwrap() // byonm won't create a Module::Npm
.resolve_pkg_folder_from_deno_module(module.nv_reference.nv())?;
let path = self
.node_resolver
.resolve_package_subpath_from_deno_module(
&package_folder,
module.nv_reference.sub_path(),
None,
ResolutionMode::Import,
NodeResolutionKind::Execution,
)?;
let url = path.clone().into_url()?;
let (media_type, _charset) =
deno_media_type::resolve_media_type_and_charset_from_content_type(
&url, None,
);
(url, media_type_to_loader(media_type))
}
deno_graph::Module::Node(_) => {
return Ok(None);
}
deno_graph::Module::External(_) => {
return Ok(None);
}
};
Ok(Some((specifier, loader)))
}
}
fn file_path_or_url(url: &Url) -> Result<String, AnyError> {
if url.scheme() == "file" {
Ok(
deno_path_util::url_to_file_path(url)?
.to_string_lossy()
.into(),
)
} else {
Ok(url.to_string())
}
}
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,
}
}

View file

@ -1,6 +1,7 @@
// Copyright 2018-2025 the Deno authors. MIT license.
pub mod bench;
pub mod bundle;
pub mod check;
pub mod clean;
pub mod compile;

View file

@ -261,6 +261,14 @@ impl<
if referrer.scheme() == "file"
&& self.in_npm_pkg_checker.in_npm_package(referrer)
{
log::debug!(
"{}: specifier={} referrer={} mode={:?} kind={:?}",
deno_terminal::colors::magenta("resolving in npm package"),
raw_specifier,
referrer,
resolution_mode,
resolution_kind
);
return node_resolver
.resolve(raw_specifier, referrer, resolution_mode, resolution_kind)
.and_then(|res| {

View file

@ -528,6 +528,13 @@ pub struct NodeCodeTranslator<
TNpmPackageFolderResolver,
TSys,
>,
mode: NodeCodeTranslatorMode,
}
#[derive(Debug, Clone, Copy)]
pub enum NodeCodeTranslatorMode {
Bundling,
ModuleLoader,
}
impl<
@ -553,9 +560,11 @@ impl<
TNpmPackageFolderResolver,
TSys,
>,
mode: NodeCodeTranslatorMode,
) -> Self {
Self {
module_export_analyzer,
mode,
}
}
@ -570,32 +579,37 @@ impl<
entry_specifier: &Url,
source: Option<Cow<'a, str>>,
) -> Result<Cow<'a, str>, TranslateCjsToEsmError> {
let analysis = self
.module_export_analyzer
.analyze_all_exports(entry_specifier, source)
.await?;
let all_exports = if matches!(self.mode, NodeCodeTranslatorMode::Bundling) {
// let the bundler handle it instead of the module loader
return Ok(source.unwrap());
} else {
let analysis = self
.module_export_analyzer
.analyze_all_exports(entry_specifier, source)
.await?;
let all_exports = match analysis {
ResolvedCjsAnalysis::Esm(source) => return Ok(source),
ResolvedCjsAnalysis::Cjs(all_exports) => all_exports,
match analysis {
ResolvedCjsAnalysis::Esm(source) => return Ok(source),
ResolvedCjsAnalysis::Cjs(all_exports) => all_exports,
}
};
// todo(dsherret): use capacity_builder here to remove all these heap
// allocations and make the string writing faster
let mut temp_var_count = 0;
let mut source = vec![
r#"import {createRequire as __internalCreateRequire, Module as __internalModule } from "node:module";
const require = __internalCreateRequire(import.meta.url);"#
.to_string(),
];
r#"import {createRequire as __internalCreateRequire, Module as __internalModule } from "node:module";
const require = __internalCreateRequire(import.meta.url);"#
.to_string(),
];
source.push(format!(
r#"let mod;
if (import.meta.main) {{
mod = __internalModule._load("{0}", null, true)
}} else {{
mod = require("{0}");
}}"#,
if (import.meta.main) {{
mod = __internalModule._load("{0}", null, true)
}} else {{
mod = require("{0}");
}}"#,
url_to_file_path(entry_specifier)
.unwrap()
.to_str()

View file

@ -215,5 +215,5 @@ pub static FEATURE_DESCRIPTIONS: &[UnstableFeatureDescription] = &[
kind: UnstableFeatureKind::Runtime,
config_option: ConfigFileOption::SameAsFlagName,
env_var: None,
},
}
];

View file

@ -0,0 +1,113 @@
{
"tempDir": true,
"tests": {
"npm_specifier": {
"steps": [
{
"args": "i -e main.ts",
"output": "[WILDCARD]"
},
{
"args": "run -A main.ts",
"output": "Hello, world!\n"
},
{
"args": "bundle --output=out.js main.ts",
"output": "[WILDCARD]\nbundled in [WILDCARD]s\n"
},
{
"args": "clean",
"output": "[WILDCARD]"
},
{
"args": "run --no-lock --cached-only --no-config -A out.js",
"output": "Hello, world!\n"
}
]
},
"npm_specifier_with_import_map": {
"steps": [
{
"args": "i npm:chalk",
"output": "[WILDCARD]"
},
{
"args": "run -A main2.ts",
"output": "Hello, world!\n"
},
{
"args": "bundle --output=out.js main2.ts",
"output": "[WILDCARD]\nbundled in [WILDCARD]s\n"
},
{
"args": "clean",
"output": "[WILDCARD]"
},
{
"args": "run --no-lock --cached-only --no-config -A out.js",
"output": "Hello, world!\n"
}
]
},
"jsr_specifier": {
"steps": [
{
"args": "i -e main_jsr.ts",
"output": "[WILDCARD]"
},
{
"args": "bundle --output=out.js main_jsr.ts",
"output": "[WILDCARD]\nbundled in [WILDCARD]s\n"
},
{
"args": "clean",
"output": "[WILDCARD]"
},
{
"args": "run --no-lock --cached-only --no-config -A out.js",
"output": "2\n"
}
]
},
"requires_node_builtin": {
"steps": [
{
"args": "bundle --output=out.js uses_node_builtin.cjs",
"output": "[WILDCARD]\nbundled in [WILDCARD]s\n"
},
{
"args": "run --no-lock --cached-only --no-config -A out.js",
"output": "{ a: 1, b: 'hello' }\n"
}
]
},
"json_import": {
"steps": [
{
"args": "bundle --output=out.js imports_json.ts",
"output": "[WILDCARD]\nbundled in [WILDCARD]s\n"
},
{
"args": ["eval", "console.log(Deno.readTextFileSync('./out.js'))"],
"output": "imports_json.out"
},
{
"args": "run --no-lock --cached-only --no-config -A out.js",
"output": "{ hi: \"bye\", thing: { other: \"thing\" } }\n"
}
]
},
"sloppy_imports": {
"steps": [
{
"args": "bundle --unstable-sloppy-imports --output=out.js sloppy.ts",
"output": "[WILDCARD]\nbundled in [WILDCARD]s\n"
},
{
"args": "run --no-lock --cached-only --no-config -A out.js",
"output": "{ hi: \"bye\", thing: { other: \"thing\" } }\n"
}
]
}
}
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,6 @@
{
"hi": "bye",
"thing": {
"other": "thing"
}
}

View file

@ -0,0 +1,11 @@
// [WILDCARD]foo.json
var foo_default = {
hi: "bye",
thing: {
other: "thing"
}
};
// [WILDCARD]imports_json.ts
console.log(foo_default);

View file

@ -0,0 +1,3 @@
import foo from "./foo.json" with { type: "json" };
console.log(foo);

View file

@ -0,0 +1,2 @@
import chalk from "npm:chalk";
console.log(chalk.green("Hello, world!"));

View file

@ -0,0 +1,3 @@
import chalk from "chalk";
console.log(chalk.green("Hello, world!"));

View file

@ -0,0 +1,3 @@
import { subtract } from "jsr:@denotest/subtract";
console.log(subtract(3, 1));

View file

@ -0,0 +1 @@
import "./imports_json.js";

View file

@ -0,0 +1,6 @@
const { inspect } = require("util");
console.log(inspect({
a: 1,
b: "hello",
}));

View file

@ -1,13 +0,0 @@
{
"steps": [
{
"args": "bundle",
"output": "bundle.out",
"exitCode": 1
},
{
"args": "bundle --help",
"output": "bundle_help.out"
}
]
}

View file

@ -1,3 +0,0 @@
error: ⚠️ `deno bundle` was removed in Deno 2.
See the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs.deno.com/runtime/manual/advanced/migrate_deprecations

View file

@ -1,11 +0,0 @@
`deno bundle` was removed in Deno 2.
See the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs.deno.com/runtime/manual/advanced/migrate_deprecations
Usage: deno bundle [OPTIONS]
Options:
-h, --help[=<CONTEXT>] [possible values: unstable, full]
-q, --quiet Suppress diagnostic output
--unstable The `--unstable` flag has been deprecated. Use granular `--unstable-*` flags instead
To view the list of individual unstable feature flags, run this command again with --help=unstable

View file

@ -14,12 +14,14 @@ use once_cell::sync::Lazy;
use parking_lot::Mutex;
use tar::Builder;
use crate::root_path;
use crate::tests_path;
use crate::PathRef;
pub const DENOTEST_SCOPE_NAME: &str = "@denotest";
pub const DENOTEST2_SCOPE_NAME: &str = "@denotest2";
pub const DENOTEST3_SCOPE_NAME: &str = "@denotest3";
pub const ESBUILD_VERSION: &str = "0.25.5";
pub static PUBLIC_TEST_NPM_REGISTRY: Lazy<TestNpmRegistry> = Lazy::new(|| {
TestNpmRegistry::new(
@ -197,6 +199,15 @@ impl TestNpmRegistry {
}
}
let prefix1 = format!("/{}/", "@esbuild");
let prefix2 = format!("/{}%2f", "@esbuild");
let maybe_package_name_with_path = uri_path
.strip_prefix(&prefix1)
.or_else(|| uri_path.strip_prefix(&prefix2));
if let Some(package_name_with_path) = maybe_package_name_with_path {
return Some(("@esbuild", package_name_with_path));
}
None
}
}
@ -255,11 +266,210 @@ fn append_dir_all<W: std::io::Write>(
Ok(())
}
fn create_package_version_info(
version_folder: &PathRef,
version: &str,
package_name: &str,
registry_hostname: &str,
) -> Result<(Vec<u8>, serde_json::Map<String, serde_json::Value>)> {
let tarball_bytes = create_tarball_from_dir(version_folder.as_path())?;
let mut dist = serde_json::Map::new();
if package_name != "@denotest/no-shasums" {
let tarball_checksum = get_tarball_checksum(&tarball_bytes);
dist.insert(
"integrity".to_string(),
format!("sha512-{tarball_checksum}").into(),
);
dist.insert("shasum".to_string(), "dummy-value".into());
}
dist.insert(
"tarball".to_string(),
format!("{registry_hostname}/{package_name}/{version}.tgz").into(),
);
let package_json_path = version_folder.join("package.json");
let package_json_bytes = fs::read(&package_json_path).with_context(|| {
format!("Error reading package.json at {}", package_json_path)
})?;
let package_json_text = String::from_utf8_lossy(&package_json_bytes);
let mut version_info: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&package_json_text)?;
version_info.insert("dist".to_string(), dist.into());
Ok((tarball_bytes, version_info))
}
fn get_esbuild_platform_info(
platform_name: &str,
) -> Option<(&'static str, &'static str, bool)> {
match platform_name {
"linux-x64" => Some(("esbuild-x64", "linux64", false)),
"linux-arm64" => Some(("esbuild-aarch64", "linux64", false)),
"darwin-x64" => Some(("esbuild-x64", "mac", false)),
"darwin-arm64" => Some(("esbuild-aarch64", "mac", false)),
"win32-x64" => Some(("esbuild-x64.exe", "win", true)),
"win32-arm64" => Some(("esbuild-arm64.exe", "win", true)),
_ => None,
}
}
fn setup_esbuild_binary(
package_dir: &Path,
esbuild_prebuilt: &Path,
is_windows: bool,
) -> Result<&'static str> {
let binary_name = if is_windows { "esbuild.exe" } else { "esbuild" };
if is_windows {
std::fs::copy(esbuild_prebuilt, package_dir.join(binary_name))?;
Ok(binary_name)
} else {
let bin_dir = package_dir.join("bin");
std::fs::create_dir_all(&bin_dir)?;
let binary_path = bin_dir.join(binary_name);
std::fs::copy(esbuild_prebuilt, &binary_path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&binary_path)?.permissions();
perms.set_mode(0o755); // rwxr-xr-x
std::fs::set_permissions(&binary_path, perms)?;
}
Ok("bin/esbuild")
}
}
fn create_tarball_from_dir(package_dir: &Path) -> Result<Vec<u8>> {
let mut tarball_bytes = Vec::new();
{
let mut encoder =
GzEncoder::new(&mut tarball_bytes, Compression::default());
{
let mut builder = Builder::new(&mut encoder);
append_dir_all(&mut builder, Path::new("package"), package_dir)?;
builder.finish()?;
}
encoder.finish()?;
}
Ok(tarball_bytes)
}
fn create_npm_registry_response(
package_name: &str,
version: &str,
description: &str,
bin_path: &str,
tarball_bytes: Vec<u8>,
registry_hostname: &str,
) -> Result<CustomNpmPackage> {
let tarball_checksum = get_tarball_checksum(&tarball_bytes);
let mut dist = serde_json::Map::new();
dist.insert(
"integrity".to_string(),
format!("sha512-{tarball_checksum}").into(),
);
dist.insert("shasum".to_string(), "dummy-value".into());
dist.insert(
"tarball".to_string(),
format!("{registry_hostname}/{package_name}/{version}.tgz").into(),
);
let mut version_info = serde_json::Map::new();
version_info.insert("name".to_string(), package_name.into());
version_info.insert("version".to_string(), version.into());
version_info.insert("description".to_string(), description.into());
version_info.insert("bin".to_string(), bin_path.into());
version_info.insert("dist".to_string(), dist.into());
let mut versions = serde_json::Map::new();
versions.insert(version.to_string(), version_info.into());
let mut dist_tags = serde_json::Map::new();
dist_tags.insert("latest".to_string(), version.into());
let mut registry_file = serde_json::Map::new();
registry_file.insert("name".to_string(), package_name.into());
registry_file.insert("versions".to_string(), versions.into());
registry_file.insert("dist-tags".to_string(), dist_tags.into());
let mut tarballs = HashMap::new();
tarballs.insert(version.to_string(), tarball_bytes);
Ok(CustomNpmPackage {
registry_file: serde_json::to_string(&registry_file)?,
tarballs,
})
}
fn create_esbuild_package(
registry_hostname: &str,
package_name: &str,
) -> Result<Option<CustomNpmPackage>> {
let platform_name = package_name.strip_prefix("@esbuild/").unwrap();
let (bin_name, folder, is_windows) =
match get_esbuild_platform_info(platform_name) {
Some(info) => info,
None => return Ok(None),
};
let esbuild_prebuilt = root_path()
.join("third_party/prebuilt")
.join(folder)
.join(bin_name);
if !esbuild_prebuilt.exists() {
return Ok(None);
}
let temp_dir = tempfile::tempdir()?;
let package_dir = temp_dir.path().join("package");
std::fs::create_dir_all(&package_dir)?;
let bin_path =
setup_esbuild_binary(&package_dir, esbuild_prebuilt.as_path(), is_windows)?;
let package_json = serde_json::json!({
"name": package_name,
"version": ESBUILD_VERSION,
"description": format!("The {} binary for esbuild", platform_name),
"bin": bin_path
});
std::fs::write(
package_dir.join("package.json"),
serde_json::to_string_pretty(&package_json)?,
)?;
let tarball_bytes = create_tarball_from_dir(&package_dir)?;
let package = create_npm_registry_response(
package_name,
ESBUILD_VERSION,
&format!("The {} binary for esbuild", platform_name),
bin_path,
tarball_bytes,
registry_hostname,
)?;
Ok(Some(package))
}
fn get_npm_package(
registry_hostname: &str,
local_path: &str,
package_name: &str,
) -> Result<Option<CustomNpmPackage>> {
if package_name.starts_with("@esbuild/") {
if let Some(esbuild_package) =
create_esbuild_package(registry_hostname, package_name)?
{
return Ok(Some(esbuild_package));
}
}
let registry_hostname = if package_name == "@denotest/tarballs-privateserver2"
{
"http://localhost:4262"
@ -288,51 +498,14 @@ fn get_npm_package(
let version = entry.file_name().to_string_lossy().to_string();
let version_folder = package_folder.join(&version);
// create the tarball
let mut tarball_bytes = Vec::new();
{
let mut encoder =
GzEncoder::new(&mut tarball_bytes, Compression::default());
{
let mut builder = Builder::new(&mut encoder);
append_dir_all(
&mut builder,
Path::new("package"),
version_folder.as_path(),
)
.with_context(|| {
format!("Error adding tarball for directory {}", version_folder,)
})?;
builder.finish()?;
}
encoder.finish()?;
}
// create the registry file JSON for this version
let mut dist = serde_json::Map::new();
if package_name != "@denotest/no-shasums" {
let tarball_checksum = get_tarball_checksum(&tarball_bytes);
dist.insert(
"integrity".to_string(),
format!("sha512-{tarball_checksum}").into(),
);
dist.insert("shasum".to_string(), "dummy-value".into());
}
dist.insert(
"tarball".to_string(),
format!("{registry_hostname}/{package_name}/{version}.tgz").into(),
);
let (tarball_bytes, mut version_info) = create_package_version_info(
&version_folder,
&version,
package_name,
registry_hostname,
)?;
tarballs.insert(version.clone(), tarball_bytes);
let package_json_path = version_folder.join("package.json");
let package_json_bytes =
fs::read(&package_json_path).with_context(|| {
format!("Error reading package.json at {}", package_json_path)
})?;
let package_json_text = String::from_utf8_lossy(&package_json_bytes);
let mut version_info: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&package_json_text)?;
version_info.insert("dist".to_string(), dist.into());
if let Some(maybe_optional_deps) = version_info.get("optionalDependencies")
{

View file

@ -29,6 +29,7 @@ use super::string_body;
use super::ServerKind;
use super::ServerOptions;
use crate::npm;
use crate::root_path;
pub fn public_npm_registry(port: u16) -> Vec<LocalBoxFuture<'static, ()>> {
run_npm_server(port, "npm registry server error", {
@ -100,6 +101,7 @@ async fn run_npm_server_for_addr<F, S>(
F: Fn(Request<hyper::body::Incoming>) -> S + Copy + 'static,
S: Future<Output = HandlerOutput> + 'static,
{
ensure_esbuild_prebuilt().await.unwrap();
run_server(
ServerOptions {
addr,
@ -378,3 +380,45 @@ async fn download_npm_registry_file(
std::fs::write(testdata_file_path, bytes)?;
Ok(())
}
const PREBUILT_URL: &str = "https://raw.githubusercontent.com/denoland/deno_third_party/de0d517e6f703fb4735b7aa5806f69fbdbb1d907/prebuilt/";
async fn ensure_esbuild_prebuilt() -> Result<(), anyhow::Error> {
let bin_name = match (std::env::consts::ARCH, std::env::consts::OS) {
("x86_64", "linux" | "macos" | "apple") => "esbuild-x64",
("aarch64", "linux" | "macos" | "apple") => "esbuild-aarch64",
("x86_64", "windows") => "esbuild-x64.exe",
("aarch64", "windows") => "esbuild-arm64.exe",
_ => return Err(anyhow::anyhow!("unsupported platform")),
};
let folder = match std::env::consts::OS {
"linux" => "linux64",
"windows" => "win",
"macos" | "apple" => "mac",
_ => return Err(anyhow::anyhow!("unsupported platform")),
};
let esbuild_prebuilt = root_path()
.join("third_party/prebuilt")
.join(folder)
.join(bin_name);
if esbuild_prebuilt.exists() {
return Ok(());
}
let url = format!("{PREBUILT_URL}{folder}/{bin_name}");
let response = reqwest::get(url).await?;
let bytes = response.bytes().await?;
tokio::fs::create_dir_all(esbuild_prebuilt.parent()).await?;
tokio::fs::write(&esbuild_prebuilt, bytes).await?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = tokio::fs::metadata(&esbuild_prebuilt).await?.permissions();
perms.set_mode(0o755); // rwxr-xr-x
tokio::fs::set_permissions(&esbuild_prebuilt, perms).await?;
}
Ok(())
}