diff --git a/.gitignore b/.gitignore index ae49382be0..7016f18e53 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ Untitled*.ipynb # playwright browser binary cache /.ms-playwright + +**/.claude/settings.local.json diff --git a/Cargo.lock b/Cargo.lock index 94a480bcaa..f6d423ad1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ab14d4c74e..f824eede57 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -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`. diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 03e2beb578..828067f352 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -464,12 +464,57 @@ pub struct CleanFlags { pub dry_run: bool, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BundleFlags { + pub entrypoints: Vec, + pub output_path: Option, + pub output_dir: Option, + pub external: Vec, + 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) -> clap::error::Result { "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 { + 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 { + 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::("file").unwrap(); + let output = matches.remove_one::("output"); + let outdir = matches.remove_one::("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::("external") + .map(|f| f.collect::>()) + .unwrap_or_default(), + format: matches.remove_one::("format").unwrap(), + packages: matches.remove_one::("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( diff --git a/cli/factory.rs b/cli/factory.rs index 94bc458a86..ad47fabb50 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -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 { + ) -> Result { 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 { + 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() { diff --git a/cli/main.rs b/cli/main.rs index c30d6f05e3..986c384bb3 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -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 }) } diff --git a/cli/rt/run.rs b/cli/rt/run.rs index 825b2a31fd..43ba983f08 100644 --- a/cli/rt/run.rs +++ b/cli/rt/run.rs @@ -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( diff --git a/cli/tools/bundle/esbuild.rs b/cli/tools/bundle/esbuild.rs new file mode 100644 index 0000000000..0ff4e94ea6 --- /dev/null +++ b/cli/tools/bundle/esbuild.rs @@ -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, + workspace_patch_packages: &Arc, + tarball_cache: &Arc>, + npm_cache: &CliNpmCache, +) -> Result { + 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() + ); + } +} diff --git a/cli/tools/bundle/mod.rs b/cli/tools/bundle/mod.rs new file mode 100644 index 0000000000..3d4a22fb14 --- /dev/null +++ b/cli/tools/bundle/mod.rs @@ -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(®ex::escape(e).replace("\\*", ".*")); + regex_str.push(')'); + } + regex::Regex::new(®ex_str).unwrap() +} + +pub async fn bundle( + flags: Arc, + 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::>(); + 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::>(); + 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, + module_load_preparer: Arc, + module_graph_container: Arc, + permissions: PermissionsContainer, + npm_resolver: CliNpmResolver, + node_resolver: Arc, + module_loader: Rc, + externals_regex: Option, +} + +#[async_trait::async_trait(?Send)] +impl esbuild_client::PluginHandler for DenoPluginHandler { + async fn on_resolve( + &self, + args: esbuild_client::OnResolveArgs, + ) -> Result, AnyError> { + log::debug!("{}: {args:?}", deno_terminal::colors::cyan("on_resolve")); + if let Some(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, 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, 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, + ) -> Result, 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, 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, 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 { + 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, + } +} diff --git a/cli/tools/mod.rs b/cli/tools/mod.rs index d7c78344d1..f9bae28961 100644 --- a/cli/tools/mod.rs +++ b/cli/tools/mod.rs @@ -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; diff --git a/resolvers/deno/lib.rs b/resolvers/deno/lib.rs index 8dc6f5a04d..a348d6a508 100644 --- a/resolvers/deno/lib.rs +++ b/resolvers/deno/lib.rs @@ -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| { diff --git a/resolvers/node/analyze.rs b/resolvers/node/analyze.rs index 1b11ab96da..24f04eb807 100644 --- a/resolvers/node/analyze.rs +++ b/resolvers/node/analyze.rs @@ -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>, ) -> Result, 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() diff --git a/runtime/features/data.rs b/runtime/features/data.rs index b413ae32d1..4bed5e02c4 100644 --- a/runtime/features/data.rs +++ b/runtime/features/data.rs @@ -215,5 +215,5 @@ pub static FEATURE_DESCRIPTIONS: &[UnstableFeatureDescription] = &[ kind: UnstableFeatureKind::Runtime, config_option: ConfigFileOption::SameAsFlagName, env_var: None, - }, + } ]; diff --git a/tests/specs/bundle/main/__test__.jsonc b/tests/specs/bundle/main/__test__.jsonc new file mode 100644 index 0000000000..060d9f725a --- /dev/null +++ b/tests/specs/bundle/main/__test__.jsonc @@ -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" + } + ] + } + } +} diff --git a/tests/specs/bundle/main/deno.jsonc b/tests/specs/bundle/main/deno.jsonc new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/tests/specs/bundle/main/deno.jsonc @@ -0,0 +1,2 @@ +{ +} diff --git a/tests/specs/bundle/main/foo.json b/tests/specs/bundle/main/foo.json new file mode 100644 index 0000000000..044fc3e373 --- /dev/null +++ b/tests/specs/bundle/main/foo.json @@ -0,0 +1,6 @@ +{ + "hi": "bye", + "thing": { + "other": "thing" + } +} diff --git a/tests/specs/bundle/main/imports_json.out b/tests/specs/bundle/main/imports_json.out new file mode 100644 index 0000000000..3e7902ad61 --- /dev/null +++ b/tests/specs/bundle/main/imports_json.out @@ -0,0 +1,11 @@ +// [WILDCARD]foo.json +var foo_default = { + hi: "bye", + thing: { + other: "thing" + } +}; + +// [WILDCARD]imports_json.ts +console.log(foo_default); + diff --git a/tests/specs/bundle/main/imports_json.ts b/tests/specs/bundle/main/imports_json.ts new file mode 100644 index 0000000000..cbdc58d79a --- /dev/null +++ b/tests/specs/bundle/main/imports_json.ts @@ -0,0 +1,3 @@ +import foo from "./foo.json" with { type: "json" }; + +console.log(foo); diff --git a/tests/specs/bundle/main/main.ts b/tests/specs/bundle/main/main.ts new file mode 100644 index 0000000000..4f53025fec --- /dev/null +++ b/tests/specs/bundle/main/main.ts @@ -0,0 +1,2 @@ +import chalk from "npm:chalk"; +console.log(chalk.green("Hello, world!")); diff --git a/tests/specs/bundle/main/main2.ts b/tests/specs/bundle/main/main2.ts new file mode 100644 index 0000000000..0c97508d49 --- /dev/null +++ b/tests/specs/bundle/main/main2.ts @@ -0,0 +1,3 @@ +import chalk from "chalk"; + +console.log(chalk.green("Hello, world!")); diff --git a/tests/specs/bundle/main/main_jsr.ts b/tests/specs/bundle/main/main_jsr.ts new file mode 100644 index 0000000000..324345581f --- /dev/null +++ b/tests/specs/bundle/main/main_jsr.ts @@ -0,0 +1,3 @@ +import { subtract } from "jsr:@denotest/subtract"; + +console.log(subtract(3, 1)); diff --git a/tests/specs/bundle/main/sloppy.ts b/tests/specs/bundle/main/sloppy.ts new file mode 100644 index 0000000000..ffd74207bb --- /dev/null +++ b/tests/specs/bundle/main/sloppy.ts @@ -0,0 +1 @@ +import "./imports_json.js"; diff --git a/tests/specs/bundle/main/uses_node_builtin.cjs b/tests/specs/bundle/main/uses_node_builtin.cjs new file mode 100644 index 0000000000..8978eb103d --- /dev/null +++ b/tests/specs/bundle/main/uses_node_builtin.cjs @@ -0,0 +1,6 @@ +const { inspect } = require("util"); + +console.log(inspect({ + a: 1, + b: "hello", +})); diff --git a/tests/specs/bundle/removed/__test__.jsonc b/tests/specs/bundle/removed/__test__.jsonc deleted file mode 100644 index b33842de28..0000000000 --- a/tests/specs/bundle/removed/__test__.jsonc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "steps": [ - { - "args": "bundle", - "output": "bundle.out", - "exitCode": 1 - }, - { - "args": "bundle --help", - "output": "bundle_help.out" - } - ] -} diff --git a/tests/specs/bundle/removed/bundle.out b/tests/specs/bundle/removed/bundle.out deleted file mode 100644 index d1d8d00d60..0000000000 --- a/tests/specs/bundle/removed/bundle.out +++ /dev/null @@ -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 diff --git a/tests/specs/bundle/removed/bundle_help.out b/tests/specs/bundle/removed/bundle_help.out deleted file mode 100644 index c8068a6d6f..0000000000 --- a/tests/specs/bundle/removed/bundle_help.out +++ /dev/null @@ -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[=] [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 diff --git a/tests/util/server/src/npm.rs b/tests/util/server/src/npm.rs index 5fba423e9e..ae3af4c3b8 100644 --- a/tests/util/server/src/npm.rs +++ b/tests/util/server/src/npm.rs @@ -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 = 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( Ok(()) } +fn create_package_version_info( + version_folder: &PathRef, + version: &str, + package_name: &str, + registry_hostname: &str, +) -> Result<(Vec, serde_json::Map)> { + 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 = + 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> { + 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, + registry_hostname: &str, +) -> Result { + 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(®istry_file)?, + tarballs, + }) +} + +fn create_esbuild_package( + registry_hostname: &str, + package_name: &str, +) -> Result> { + 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> { + 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 = - 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") { diff --git a/tests/util/server/src/servers/npm_registry.rs b/tests/util/server/src/servers/npm_registry.rs index 9cf6ce5db8..d7e11e8b05 100644 --- a/tests/util/server/src/servers/npm_registry.rs +++ b/tests/util/server/src/servers/npm_registry.rs @@ -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> { run_npm_server(port, "npm registry server error", { @@ -100,6 +101,7 @@ async fn run_npm_server_for_addr( F: Fn(Request) -> S + Copy + 'static, S: Future + '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(()) +}