diff --git a/.github/workflows/ci.generate.ts b/.github/workflows/ci.generate.ts index 430387d5ba..5ed02d3cde 100755 --- a/.github/workflows/ci.generate.ts +++ b/.github/workflows/ci.generate.ts @@ -5,7 +5,7 @@ import { stringify } from "jsr:@std/yaml@^0.221/stringify"; // Bump this number when you want to purge the cache. // Note: the tools/release/01_bump_crate_versions.ts script will update this version // automatically via regex, so ensure that this line maintains this format. -const cacheVersion = 24; +const cacheVersion = 25; const ubuntuX86Runner = "ubuntu-24.04"; const ubuntuX86XlRunner = "ubuntu-24.04-xl"; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b621e1235..39a3afe769 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -361,8 +361,8 @@ jobs: path: |- ~/.cargo/registry/index ~/.cargo/registry/cache - key: '24-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles(''Cargo.lock'') }}' - restore-keys: '24-cargo-home-${{ matrix.os }}-${{ matrix.arch }}' + key: '25-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles(''Cargo.lock'') }}' + restore-keys: '25-cargo-home-${{ matrix.os }}-${{ matrix.arch }}' if: '!(matrix.skip)' - name: Restore cache build output (PR) uses: actions/cache/restore@v4 @@ -375,7 +375,7 @@ jobs: !./target/*/*.zip !./target/*/*.tar.gz key: never_saved - restore-keys: '24-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-' + restore-keys: '25-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-' - name: Apply and update mtime cache if: '!(matrix.skip) && (!startsWith(github.ref, ''refs/tags/''))' uses: ./.github/mtime_cache @@ -685,7 +685,7 @@ jobs: !./target/*/*.zip !./target/*/*.sha256sum !./target/*/*.tar.gz - key: '24-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-${{ github.sha }}' + key: '25-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-${{ github.sha }}' publish-canary: name: publish canary runs-on: ubuntu-24.04 diff --git a/Cargo.lock b/Cargo.lock index b0e3d8b21a..6f7799bac8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,7 +1154,7 @@ dependencies = [ [[package]] name = "deno" -version = "2.0.5" +version = "2.0.6" dependencies = [ "anstream", "async-trait", @@ -1323,7 +1323,7 @@ dependencies = [ [[package]] name = "deno_bench_util" -version = "0.170.0" +version = "0.171.0" dependencies = [ "bencher", "deno_core", @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "deno_broadcast_channel" -version = "0.170.0" +version = "0.171.0" dependencies = [ "async-trait", "deno_core", @@ -1343,7 +1343,7 @@ dependencies = [ [[package]] name = "deno_cache" -version = "0.108.0" +version = "0.109.0" dependencies = [ "async-trait", "deno_core", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "deno_canvas" -version = "0.45.0" +version = "0.46.0" dependencies = [ "deno_core", "deno_webgpu", @@ -1411,7 +1411,7 @@ dependencies = [ [[package]] name = "deno_console" -version = "0.176.0" +version = "0.177.0" dependencies = [ "deno_core", ] @@ -1456,7 +1456,7 @@ checksum = "a13951ea98c0a4c372f162d669193b4c9d991512de9f2381dd161027f34b26b1" [[package]] name = "deno_cron" -version = "0.56.0" +version = "0.57.0" dependencies = [ "anyhow", "async-trait", @@ -1469,7 +1469,7 @@ dependencies = [ [[package]] name = "deno_crypto" -version = "0.190.0" +version = "0.191.0" dependencies = [ "aes", "aes-gcm", @@ -1531,7 +1531,7 @@ dependencies = [ [[package]] name = "deno_fetch" -version = "0.200.0" +version = "0.201.0" dependencies = [ "base64 0.21.7", "bytes", @@ -1564,7 +1564,7 @@ dependencies = [ [[package]] name = "deno_ffi" -version = "0.163.0" +version = "0.164.0" dependencies = [ "deno_core", "deno_permissions", @@ -1584,7 +1584,7 @@ dependencies = [ [[package]] name = "deno_fs" -version = "0.86.0" +version = "0.87.0" dependencies = [ "async-trait", "base32", @@ -1635,7 +1635,7 @@ dependencies = [ [[package]] name = "deno_http" -version = "0.174.0" +version = "0.175.0" dependencies = [ "async-compression", "async-trait", @@ -1674,7 +1674,7 @@ dependencies = [ [[package]] name = "deno_io" -version = "0.86.0" +version = "0.87.0" dependencies = [ "async-trait", "deno_core", @@ -1695,7 +1695,7 @@ dependencies = [ [[package]] name = "deno_kv" -version = "0.84.0" +version = "0.85.0" dependencies = [ "anyhow", "async-trait", @@ -1767,7 +1767,7 @@ dependencies = [ [[package]] name = "deno_napi" -version = "0.107.0" +version = "0.108.0" dependencies = [ "deno_core", "deno_permissions", @@ -1795,7 +1795,7 @@ dependencies = [ [[package]] name = "deno_net" -version = "0.168.0" +version = "0.169.0" dependencies = [ "deno_core", "deno_permissions", @@ -1812,7 +1812,7 @@ dependencies = [ [[package]] name = "deno_node" -version = "0.113.0" +version = "0.114.0" dependencies = [ "aead-gcm-stream", "aes", @@ -1961,7 +1961,7 @@ dependencies = [ [[package]] name = "deno_permissions" -version = "0.36.0" +version = "0.37.0" dependencies = [ "deno_core", "deno_path_util", @@ -1979,7 +1979,7 @@ dependencies = [ [[package]] name = "deno_resolver" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anyhow", "base32", @@ -1995,7 +1995,7 @@ dependencies = [ [[package]] name = "deno_runtime" -version = "0.185.0" +version = "0.186.0" dependencies = [ "color-print", "deno_ast", @@ -2113,7 +2113,7 @@ dependencies = [ [[package]] name = "deno_tls" -version = "0.163.0" +version = "0.164.0" dependencies = [ "deno_core", "deno_native_certs", @@ -2162,7 +2162,7 @@ dependencies = [ [[package]] name = "deno_url" -version = "0.176.0" +version = "0.177.0" dependencies = [ "deno_bench_util", "deno_console", @@ -2174,7 +2174,7 @@ dependencies = [ [[package]] name = "deno_web" -version = "0.207.0" +version = "0.208.0" dependencies = [ "async-trait", "base64-simd 0.8.0", @@ -2196,7 +2196,7 @@ dependencies = [ [[package]] name = "deno_webgpu" -version = "0.143.0" +version = "0.144.0" dependencies = [ "deno_core", "raw-window-handle", @@ -2209,7 +2209,7 @@ dependencies = [ [[package]] name = "deno_webidl" -version = "0.176.0" +version = "0.177.0" dependencies = [ "deno_bench_util", "deno_core", @@ -2217,7 +2217,7 @@ dependencies = [ [[package]] name = "deno_websocket" -version = "0.181.0" +version = "0.182.0" dependencies = [ "bytes", "deno_core", @@ -2239,7 +2239,7 @@ dependencies = [ [[package]] name = "deno_webstorage" -version = "0.171.0" +version = "0.172.0" dependencies = [ "deno_core", "deno_web", @@ -4561,7 +4561,7 @@ dependencies = [ [[package]] name = "napi_sym" -version = "0.106.0" +version = "0.107.0" dependencies = [ "quote", "serde", @@ -4616,7 +4616,7 @@ dependencies = [ [[package]] name = "node_resolver" -version = "0.15.0" +version = "0.16.0" dependencies = [ "anyhow", "async-trait", @@ -8485,9 +8485,9 @@ dependencies = [ [[package]] name = "zeromq" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0560d00172817b7f7c2265060783519c475702ae290b154115ca75e976d4d0" +checksum = "6a4528179201f6eecf211961a7d3276faa61554c82651ecc66387f68fc3004bd" dependencies = [ "async-trait", "asynchronous-codec", diff --git a/Cargo.toml b/Cargo.toml index 13140b65dd..e372e542bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,16 +48,16 @@ repository = "https://github.com/denoland/deno" deno_ast = { version = "=0.43.3", features = ["transpiling"] } deno_core = { version = "0.318.0" } -deno_bench_util = { version = "0.170.0", path = "./bench_util" } +deno_bench_util = { version = "0.171.0", path = "./bench_util" } deno_lockfile = "=0.23.1" deno_media_type = { version = "0.2.0", features = ["module_specifier"] } deno_npm = "=0.25.4" deno_path_util = "=0.2.1" -deno_permissions = { version = "0.36.0", path = "./runtime/permissions" } -deno_runtime = { version = "0.185.0", path = "./runtime" } +deno_permissions = { version = "0.37.0", path = "./runtime/permissions" } +deno_runtime = { version = "0.186.0", path = "./runtime" } deno_semver = "=0.5.16" deno_terminal = "0.2.0" -napi_sym = { version = "0.106.0", path = "./ext/napi/sym" } +napi_sym = { version = "0.107.0", path = "./ext/napi/sym" } test_util = { package = "test_server", path = "./tests/util/server" } denokv_proto = "0.8.1" @@ -66,32 +66,32 @@ denokv_remote = "0.8.1" denokv_sqlite = { default-features = false, version = "0.8.2" } # exts -deno_broadcast_channel = { version = "0.170.0", path = "./ext/broadcast_channel" } -deno_cache = { version = "0.108.0", path = "./ext/cache" } -deno_canvas = { version = "0.45.0", path = "./ext/canvas" } -deno_console = { version = "0.176.0", path = "./ext/console" } -deno_cron = { version = "0.56.0", path = "./ext/cron" } -deno_crypto = { version = "0.190.0", path = "./ext/crypto" } -deno_fetch = { version = "0.200.0", path = "./ext/fetch" } -deno_ffi = { version = "0.163.0", path = "./ext/ffi" } -deno_fs = { version = "0.86.0", path = "./ext/fs" } -deno_http = { version = "0.174.0", path = "./ext/http" } -deno_io = { version = "0.86.0", path = "./ext/io" } -deno_kv = { version = "0.84.0", path = "./ext/kv" } -deno_napi = { version = "0.107.0", path = "./ext/napi" } -deno_net = { version = "0.168.0", path = "./ext/net" } -deno_node = { version = "0.113.0", path = "./ext/node" } -deno_tls = { version = "0.163.0", path = "./ext/tls" } -deno_url = { version = "0.176.0", path = "./ext/url" } -deno_web = { version = "0.207.0", path = "./ext/web" } -deno_webgpu = { version = "0.143.0", path = "./ext/webgpu" } -deno_webidl = { version = "0.176.0", path = "./ext/webidl" } -deno_websocket = { version = "0.181.0", path = "./ext/websocket" } -deno_webstorage = { version = "0.171.0", path = "./ext/webstorage" } +deno_broadcast_channel = { version = "0.171.0", path = "./ext/broadcast_channel" } +deno_cache = { version = "0.109.0", path = "./ext/cache" } +deno_canvas = { version = "0.46.0", path = "./ext/canvas" } +deno_console = { version = "0.177.0", path = "./ext/console" } +deno_cron = { version = "0.57.0", path = "./ext/cron" } +deno_crypto = { version = "0.191.0", path = "./ext/crypto" } +deno_fetch = { version = "0.201.0", path = "./ext/fetch" } +deno_ffi = { version = "0.164.0", path = "./ext/ffi" } +deno_fs = { version = "0.87.0", path = "./ext/fs" } +deno_http = { version = "0.175.0", path = "./ext/http" } +deno_io = { version = "0.87.0", path = "./ext/io" } +deno_kv = { version = "0.85.0", path = "./ext/kv" } +deno_napi = { version = "0.108.0", path = "./ext/napi" } +deno_net = { version = "0.169.0", path = "./ext/net" } +deno_node = { version = "0.114.0", path = "./ext/node" } +deno_tls = { version = "0.164.0", path = "./ext/tls" } +deno_url = { version = "0.177.0", path = "./ext/url" } +deno_web = { version = "0.208.0", path = "./ext/web" } +deno_webgpu = { version = "0.144.0", path = "./ext/webgpu" } +deno_webidl = { version = "0.177.0", path = "./ext/webidl" } +deno_websocket = { version = "0.182.0", path = "./ext/websocket" } +deno_webstorage = { version = "0.172.0", path = "./ext/webstorage" } # resolvers -deno_resolver = { version = "0.8.0", path = "./resolvers/deno" } -node_resolver = { version = "0.15.0", path = "./resolvers/node" } +deno_resolver = { version = "0.9.0", path = "./resolvers/deno" } +node_resolver = { version = "0.16.0", path = "./resolvers/node" } aes = "=0.8.3" anyhow = "1.0.57" @@ -204,7 +204,7 @@ webpki-root-certs = "0.26.5" webpki-roots = "0.26" which = "4.2.5" yoke = { version = "0.7.4", features = ["derive"] } -zeromq = { version = "=0.4.0", default-features = false, features = ["tcp-transport", "tokio-runtime"] } +zeromq = { version = "=0.4.1", default-features = false, features = ["tcp-transport", "tokio-runtime"] } zstd = "=0.12.4" # crypto diff --git a/Releases.md b/Releases.md index 8010379309..5ce25815bd 100644 --- a/Releases.md +++ b/Releases.md @@ -6,6 +6,18 @@ https://github.com/denoland/deno/releases We also have one-line install commands at: https://github.com/denoland/deno_install +### 2.0.6 / 2024.11.10 + +- feat(ext/http): abort event when request is cancelled (#26781) +- feat(ext/http): abort signal when request is cancelled (#26761) +- feat(lsp): auto-import completions from byonm dependencies (#26680) +- fix(ext/cache): don't panic when creating cache (#26780) +- fix(ext/node): better inspector support (#26471) +- fix(fmt): don't use self-closing tags in HTML (#26754) +- fix(install): cache jsr deps from all workspace config files (#26779) +- fix(node:zlib): gzip & gzipSync should accept ArrayBuffer (#26762) +- fix: performance.timeOrigin (#26787) + ### 2.0.5 / 2024.11.05 - fix(add): better error message when adding package that only has pre-release diff --git a/bench_util/Cargo.toml b/bench_util/Cargo.toml index ff48442b3b..d6eefc3a5d 100644 --- a/bench_util/Cargo.toml +++ b/bench_util/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_bench_util" -version = "0.170.0" +version = "0.171.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 32229d816f..e98e0ac001 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno" -version = "2.0.5" +version = "2.0.6" authors.workspace = true default-run = "deno" edition.workspace = true diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 98215855c9..683a59c219 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -10,6 +10,7 @@ use super::tsc; use super::urls::url_to_uri; use crate::args::jsr_url; +use crate::lsp::logging::lsp_warn; use crate::lsp::search::PackageSearchApi; use crate::tools::lint::CliLinter; use crate::util::path::relative_specifier; @@ -747,8 +748,14 @@ pub fn ts_changes_to_edit( ) -> Result, AnyError> { let mut text_document_edits = Vec::new(); for change in changes { - let text_document_edit = change.to_text_document_edit(language_server)?; - text_document_edits.push(text_document_edit); + let edit = match change.to_text_document_edit(language_server) { + Ok(e) => e, + Err(err) => { + lsp_warn!("Couldn't covert text document edit: {:#}", err); + continue; + } + }; + text_document_edits.push(edit); } Ok(Some(lsp::WorkspaceEdit { changes: None, diff --git a/cli/npm/managed/resolvers/common/bin_entries.rs b/cli/npm/managed/resolvers/common/bin_entries.rs index 4524ce8326..e4a1845689 100644 --- a/cli/npm/managed/resolvers/common/bin_entries.rs +++ b/cli/npm/managed/resolvers/common/bin_entries.rs @@ -18,6 +18,7 @@ pub struct BinEntries<'a> { seen_names: HashMap<&'a str, &'a NpmPackageId>, /// The bin entries entries: Vec<(&'a NpmResolutionPackage, PathBuf)>, + sorted: bool, } /// Returns the name of the default binary for the given package. @@ -31,6 +32,20 @@ fn default_bin_name(package: &NpmResolutionPackage) -> &str { .map_or(package.id.nv.name.as_str(), |(_, name)| name) } +pub fn warn_missing_entrypoint( + bin_name: &str, + package_path: &Path, + entrypoint: &Path, +) { + log::warn!( + "{} Trying to set up '{}' bin for \"{}\", but the entry point \"{}\" doesn't exist.", + deno_terminal::colors::yellow("Warning"), + bin_name, + package_path.display(), + entrypoint.display() + ); +} + impl<'a> BinEntries<'a> { pub fn new() -> Self { Self::default() @@ -42,6 +57,7 @@ impl<'a> BinEntries<'a> { package: &'a NpmResolutionPackage, package_path: PathBuf, ) { + self.sorted = false; // check for a new collision, if we haven't already // found one match package.bin.as_ref().unwrap() { @@ -79,16 +95,21 @@ impl<'a> BinEntries<'a> { &str, // bin name &str, // bin script ) -> Result<(), AnyError>, + mut filter: impl FnMut(&NpmResolutionPackage) -> bool, ) -> Result<(), AnyError> { - if !self.collisions.is_empty() { + if !self.collisions.is_empty() && !self.sorted { // walking the dependency tree to find out the depth of each package // is sort of expensive, so we only do it if there's a collision sort_by_depth(snapshot, &mut self.entries, &mut self.collisions); + self.sorted = true; } let mut seen = HashSet::new(); for (package, package_path) in &self.entries { + if !filter(package) { + continue; + } if let Some(bin_entries) = &package.bin { match bin_entries { deno_npm::registry::NpmPackageVersionBinEntry::String(script) => { @@ -118,8 +139,8 @@ impl<'a> BinEntries<'a> { } /// Collect the bin entries into a vec of (name, script path) - pub fn into_bin_files( - mut self, + pub fn collect_bin_files( + &mut self, snapshot: &NpmResolutionSnapshot, ) -> Vec<(String, PathBuf)> { let mut bins = Vec::new(); @@ -131,17 +152,18 @@ impl<'a> BinEntries<'a> { bins.push((name.to_string(), package_path.join(script))); Ok(()) }, + |_| true, ) .unwrap(); bins } - /// Finish setting up the bin entries, writing the necessary files - /// to disk. - pub fn finish( + fn set_up_entries_filtered( mut self, snapshot: &NpmResolutionSnapshot, bin_node_modules_dir_path: &Path, + filter: impl FnMut(&NpmResolutionPackage) -> bool, + mut handler: impl FnMut(&EntrySetupOutcome<'_>), ) -> Result<(), AnyError> { if !self.entries.is_empty() && !bin_node_modules_dir_path.exists() { std::fs::create_dir_all(bin_node_modules_dir_path).with_context( @@ -160,18 +182,54 @@ impl<'a> BinEntries<'a> { Ok(()) }, |package, package_path, name, script| { - set_up_bin_entry( + let outcome = set_up_bin_entry( package, name, script, package_path, bin_node_modules_dir_path, - ) + )?; + handler(&outcome); + Ok(()) }, + filter, )?; Ok(()) } + + /// Finish setting up the bin entries, writing the necessary files + /// to disk. + pub fn finish( + self, + snapshot: &NpmResolutionSnapshot, + bin_node_modules_dir_path: &Path, + handler: impl FnMut(&EntrySetupOutcome<'_>), + ) -> Result<(), AnyError> { + self.set_up_entries_filtered( + snapshot, + bin_node_modules_dir_path, + |_| true, + handler, + ) + } + + /// Finish setting up the bin entries, writing the necessary files + /// to disk. + pub fn finish_only( + self, + snapshot: &NpmResolutionSnapshot, + bin_node_modules_dir_path: &Path, + handler: impl FnMut(&EntrySetupOutcome<'_>), + only: &HashSet<&NpmPackageId>, + ) -> Result<(), AnyError> { + self.set_up_entries_filtered( + snapshot, + bin_node_modules_dir_path, + |package| only.contains(&package.id), + handler, + ) + } } // walk the dependency tree to find out the depth of each package @@ -233,16 +291,17 @@ fn sort_by_depth( }); } -pub fn set_up_bin_entry( - package: &NpmResolutionPackage, - bin_name: &str, +pub fn set_up_bin_entry<'a>( + package: &'a NpmResolutionPackage, + bin_name: &'a str, #[allow(unused_variables)] bin_script: &str, - #[allow(unused_variables)] package_path: &Path, + #[allow(unused_variables)] package_path: &'a Path, bin_node_modules_dir_path: &Path, -) -> Result<(), AnyError> { +) -> Result, AnyError> { #[cfg(windows)] { set_up_bin_shim(package, bin_name, bin_node_modules_dir_path)?; + Ok(EntrySetupOutcome::Success) } #[cfg(unix)] { @@ -252,9 +311,8 @@ pub fn set_up_bin_entry( bin_script, package_path, bin_node_modules_dir_path, - )?; + ) } - Ok(()) } #[cfg(windows)] @@ -301,14 +359,39 @@ fn make_executable_if_exists(path: &Path) -> Result { Ok(true) } +pub enum EntrySetupOutcome<'a> { + #[cfg_attr(windows, allow(dead_code))] + MissingEntrypoint { + bin_name: &'a str, + package_path: &'a Path, + entrypoint: PathBuf, + package: &'a NpmResolutionPackage, + }, + Success, +} + +impl<'a> EntrySetupOutcome<'a> { + pub fn warn_if_failed(&self) { + match self { + EntrySetupOutcome::MissingEntrypoint { + bin_name, + package_path, + entrypoint, + .. + } => warn_missing_entrypoint(bin_name, package_path, entrypoint), + EntrySetupOutcome::Success => {} + } + } +} + #[cfg(unix)] -fn symlink_bin_entry( - _package: &NpmResolutionPackage, - bin_name: &str, +fn symlink_bin_entry<'a>( + package: &'a NpmResolutionPackage, + bin_name: &'a str, bin_script: &str, - package_path: &Path, + package_path: &'a Path, bin_node_modules_dir_path: &Path, -) -> Result<(), AnyError> { +) -> Result, AnyError> { use std::io; use std::os::unix::fs::symlink; let link = bin_node_modules_dir_path.join(bin_name); @@ -318,14 +401,12 @@ fn symlink_bin_entry( format!("Can't set up '{}' bin at {}", bin_name, original.display()) })?; if !found { - log::warn!( - "{} Trying to set up '{}' bin for \"{}\", but the entry point \"{}\" doesn't exist.", - deno_terminal::colors::yellow("Warning"), + return Ok(EntrySetupOutcome::MissingEntrypoint { bin_name, - package_path.display(), - original.display() - ); - return Ok(()); + package_path, + entrypoint: original, + package, + }); } let original_relative = @@ -348,7 +429,7 @@ fn symlink_bin_entry( original_relative.display() ) })?; - return Ok(()); + return Ok(EntrySetupOutcome::Success); } return Err(err).with_context(|| { format!( @@ -359,5 +440,5 @@ fn symlink_bin_entry( }); } - Ok(()) + Ok(EntrySetupOutcome::Success) } diff --git a/cli/npm/managed/resolvers/common/lifecycle_scripts.rs b/cli/npm/managed/resolvers/common/lifecycle_scripts.rs index 5735f52482..5c5755c819 100644 --- a/cli/npm/managed/resolvers/common/lifecycle_scripts.rs +++ b/cli/npm/managed/resolvers/common/lifecycle_scripts.rs @@ -10,6 +10,7 @@ use deno_runtime::deno_io::FromRawIoHandle; use deno_semver::package::PackageNv; use deno_semver::Version; use std::borrow::Cow; +use std::collections::HashSet; use std::rc::Rc; use std::path::Path; @@ -61,7 +62,7 @@ impl<'a> LifecycleScripts<'a> { } } -fn has_lifecycle_scripts( +pub fn has_lifecycle_scripts( package: &NpmResolutionPackage, package_path: &Path, ) -> bool { @@ -83,7 +84,7 @@ fn is_broken_default_install_script(script: &str, package_path: &Path) -> bool { } impl<'a> LifecycleScripts<'a> { - fn can_run_scripts(&self, package_nv: &PackageNv) -> bool { + pub fn can_run_scripts(&self, package_nv: &PackageNv) -> bool { if !self.strategy.can_run_scripts() { return false; } @@ -98,6 +99,9 @@ impl<'a> LifecycleScripts<'a> { PackagesAllowedScripts::None => false, } } + pub fn has_run_scripts(&self, package: &NpmResolutionPackage) -> bool { + self.strategy.has_run(package) + } /// Register a package for running lifecycle scripts, if applicable. /// /// `package_path` is the path containing the package's code (its root dir). @@ -110,12 +114,12 @@ impl<'a> LifecycleScripts<'a> { ) { if has_lifecycle_scripts(package, &package_path) { if self.can_run_scripts(&package.id.nv) { - if !self.strategy.has_run(package) { + if !self.has_run_scripts(package) { self .packages_with_scripts .push((package, package_path.into_owned())); } - } else if !self.strategy.has_run(package) + } else if !self.has_run_scripts(package) && (self.config.explicit_install || !self.strategy.has_warned(package)) { // Skip adding `esbuild` as it is known that it can work properly without lifecycle script @@ -149,22 +153,32 @@ impl<'a> LifecycleScripts<'a> { self, snapshot: &NpmResolutionSnapshot, packages: &[NpmResolutionPackage], - root_node_modules_dir_path: Option<&Path>, + root_node_modules_dir_path: &Path, progress_bar: &ProgressBar, ) -> Result<(), AnyError> { self.warn_not_run_scripts()?; let get_package_path = |p: &NpmResolutionPackage| self.strategy.package_path(p); let mut failed_packages = Vec::new(); + let mut bin_entries = BinEntries::new(); if !self.packages_with_scripts.is_empty() { + let package_ids = self + .packages_with_scripts + .iter() + .map(|(p, _)| &p.id) + .collect::>(); // get custom commands for each bin available in the node_modules dir (essentially // the scripts that are in `node_modules/.bin`) - let base = - resolve_baseline_custom_commands(snapshot, packages, get_package_path)?; + let base = resolve_baseline_custom_commands( + &mut bin_entries, + snapshot, + packages, + get_package_path, + )?; let init_cwd = &self.config.initial_cwd; let process_state = crate::npm::managed::npm_process_state( snapshot.as_valid_serialized(), - root_node_modules_dir_path, + Some(root_node_modules_dir_path), ); let mut env_vars = crate::task_runner::real_env_vars(); @@ -221,7 +235,7 @@ impl<'a> LifecycleScripts<'a> { custom_commands: custom_commands.clone(), init_cwd, argv: &[], - root_node_modules_dir: root_node_modules_dir_path, + root_node_modules_dir: Some(root_node_modules_dir_path), stdio: Some(crate::task_runner::TaskIo { stderr: TaskStdio::piped(), stdout: TaskStdio::piped(), @@ -262,6 +276,17 @@ impl<'a> LifecycleScripts<'a> { } self.strategy.did_run_scripts(package)?; } + + // re-set up bin entries for the packages which we've run scripts for. + // lifecycle scripts can create files that are linked to by bin entries, + // and the only reliable way to handle this is to re-link bin entries + // (this is what PNPM does as well) + bin_entries.finish_only( + snapshot, + &root_node_modules_dir_path.join(".bin"), + |outcome| outcome.warn_if_failed(), + &package_ids, + )?; } if failed_packages.is_empty() { Ok(()) @@ -281,9 +306,10 @@ impl<'a> LifecycleScripts<'a> { // take in all (non copy) packages from snapshot, // and resolve the set of available binaries to create // custom commands available to the task runner -fn resolve_baseline_custom_commands( - snapshot: &NpmResolutionSnapshot, - packages: &[NpmResolutionPackage], +fn resolve_baseline_custom_commands<'a>( + bin_entries: &mut BinEntries<'a>, + snapshot: &'a NpmResolutionSnapshot, + packages: &'a [NpmResolutionPackage], get_package_path: impl Fn(&NpmResolutionPackage) -> PathBuf, ) -> Result { let mut custom_commands = crate::task_runner::TaskCustomCommands::new(); @@ -306,6 +332,7 @@ fn resolve_baseline_custom_commands( // doing it for packages that are set up already. // realistically, scripts won't be run very often so it probably isn't too big of an issue. resolve_custom_commands_from_packages( + bin_entries, custom_commands, snapshot, packages, @@ -320,12 +347,12 @@ fn resolve_custom_commands_from_packages< 'a, P: IntoIterator, >( + bin_entries: &mut BinEntries<'a>, mut commands: crate::task_runner::TaskCustomCommands, snapshot: &'a NpmResolutionSnapshot, packages: P, get_package_path: impl Fn(&'a NpmResolutionPackage) -> PathBuf, ) -> Result { - let mut bin_entries = BinEntries::new(); for package in packages { let package_path = get_package_path(package); @@ -333,7 +360,7 @@ fn resolve_custom_commands_from_packages< bin_entries.add(package, package_path); } } - let bins = bin_entries.into_bin_files(snapshot); + let bins: Vec<(String, PathBuf)> = bin_entries.collect_bin_files(snapshot); for (bin_name, script_path) in bins { commands.insert( bin_name.clone(), @@ -356,7 +383,9 @@ fn resolve_custom_commands_from_deps( snapshot: &NpmResolutionSnapshot, get_package_path: impl Fn(&NpmResolutionPackage) -> PathBuf, ) -> Result { + let mut bin_entries = BinEntries::new(); resolve_custom_commands_from_packages( + &mut bin_entries, baseline, snapshot, package diff --git a/cli/npm/managed/resolvers/local.rs b/cli/npm/managed/resolvers/local.rs index eddb0dc9b6..50c5bd2689 100644 --- a/cli/npm/managed/resolvers/local.rs +++ b/cli/npm/managed/resolvers/local.rs @@ -55,6 +55,7 @@ use crate::util::progress_bar::ProgressMessagePrompt; use super::super::cache::NpmCache; use super::super::cache::TarballCache; use super::super::resolution::NpmResolution; +use super::common::bin_entries; use super::common::NpmPackageFsResolver; use super::common::RegistryReadPermissionChecker; @@ -329,8 +330,7 @@ async fn sync_resolution_with_fs( let mut cache_futures = FuturesUnordered::new(); let mut newest_packages_by_name: HashMap<&String, &NpmResolutionPackage> = HashMap::with_capacity(package_partitions.packages.len()); - let bin_entries = - Rc::new(RefCell::new(super::common::bin_entries::BinEntries::new())); + let bin_entries = Rc::new(RefCell::new(bin_entries::BinEntries::new())); let mut lifecycle_scripts = super::common::lifecycle_scripts::LifecycleScripts::new( lifecycle_scripts, @@ -658,7 +658,28 @@ async fn sync_resolution_with_fs( // 7. Set up `node_modules/.bin` entries for packages that need it. { let bin_entries = std::mem::take(&mut *bin_entries.borrow_mut()); - bin_entries.finish(snapshot, &bin_node_modules_dir_path)?; + bin_entries.finish( + snapshot, + &bin_node_modules_dir_path, + |setup_outcome| { + match setup_outcome { + bin_entries::EntrySetupOutcome::MissingEntrypoint { + package, + package_path, + .. + } if super::common::lifecycle_scripts::has_lifecycle_scripts( + package, + package_path, + ) && lifecycle_scripts.can_run_scripts(&package.id.nv) + && !lifecycle_scripts.has_run_scripts(package) => + { + // ignore, it might get fixed when the lifecycle scripts run. + // if not, we'll warn then + } + outcome => outcome.warn_if_failed(), + } + }, + )?; } // 8. Create symlinks for the workspace packages @@ -708,7 +729,7 @@ async fn sync_resolution_with_fs( .finish( snapshot, &package_partitions.packages, - Some(root_node_modules_dir_path), + root_node_modules_dir_path, progress_bar, ) .await?; diff --git a/ext/broadcast_channel/Cargo.toml b/ext/broadcast_channel/Cargo.toml index 0951286f71..90ac038357 100644 --- a/ext/broadcast_channel/Cargo.toml +++ b/ext/broadcast_channel/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_broadcast_channel" -version = "0.170.0" +version = "0.171.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/cache/Cargo.toml b/ext/cache/Cargo.toml index 20dc9ca48c..56fa0a527f 100644 --- a/ext/cache/Cargo.toml +++ b/ext/cache/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_cache" -version = "0.108.0" +version = "0.109.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/canvas/Cargo.toml b/ext/canvas/Cargo.toml index 5c6e44f95c..4231d7c84d 100644 --- a/ext/canvas/Cargo.toml +++ b/ext/canvas/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_canvas" -version = "0.45.0" +version = "0.46.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/console/Cargo.toml b/ext/console/Cargo.toml index bf476574ee..80f1cca84d 100644 --- a/ext/console/Cargo.toml +++ b/ext/console/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_console" -version = "0.176.0" +version = "0.177.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/cron/Cargo.toml b/ext/cron/Cargo.toml index 90b39aa0e2..966ccdc958 100644 --- a/ext/cron/Cargo.toml +++ b/ext/cron/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_cron" -version = "0.56.0" +version = "0.57.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/crypto/Cargo.toml b/ext/crypto/Cargo.toml index 890cd868ae..a5794dc68b 100644 --- a/ext/crypto/Cargo.toml +++ b/ext/crypto/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_crypto" -version = "0.190.0" +version = "0.191.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/fetch/Cargo.toml b/ext/fetch/Cargo.toml index 0b15d05b8f..56d416bbb8 100644 --- a/ext/fetch/Cargo.toml +++ b/ext/fetch/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_fetch" -version = "0.200.0" +version = "0.201.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/ffi/Cargo.toml b/ext/ffi/Cargo.toml index 27ecd1d546..295e8be846 100644 --- a/ext/ffi/Cargo.toml +++ b/ext/ffi/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_ffi" -version = "0.163.0" +version = "0.164.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/fs/Cargo.toml b/ext/fs/Cargo.toml index d1720b7bc7..e85f349b15 100644 --- a/ext/fs/Cargo.toml +++ b/ext/fs/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_fs" -version = "0.86.0" +version = "0.87.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/http/Cargo.toml b/ext/http/Cargo.toml index 1d7c4d87ed..ed98fe349c 100644 --- a/ext/http/Cargo.toml +++ b/ext/http/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_http" -version = "0.174.0" +version = "0.175.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/http/http_next.rs b/ext/http/http_next.rs index c55e868352..1251f00cc0 100644 --- a/ext/http/http_next.rs +++ b/ext/http/http_next.rs @@ -564,6 +564,7 @@ fn is_request_compressible( match accept_encoding.to_str() { // Firefox and Chrome send this -- no need to parse Ok("gzip, deflate, br") => return Compression::Brotli, + Ok("gzip, deflate, br, zstd") => return Compression::Brotli, Ok("gzip") => return Compression::GZip, Ok("br") => return Compression::Brotli, _ => (), diff --git a/ext/io/Cargo.toml b/ext/io/Cargo.toml index b791f16a3f..6ef049ff9b 100644 --- a/ext/io/Cargo.toml +++ b/ext/io/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_io" -version = "0.86.0" +version = "0.87.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/kv/Cargo.toml b/ext/kv/Cargo.toml index 59b425d3cb..1d7b91770f 100644 --- a/ext/kv/Cargo.toml +++ b/ext/kv/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_kv" -version = "0.84.0" +version = "0.85.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/kv/config.rs b/ext/kv/config.rs index 6e2e2c3a1f..7166bcbcc2 100644 --- a/ext/kv/config.rs +++ b/ext/kv/config.rs @@ -1,16 +1,17 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#[derive(Clone, Copy, Debug)] pub struct KvConfig { - pub(crate) max_write_key_size_bytes: usize, - pub(crate) max_read_key_size_bytes: usize, - pub(crate) max_value_size_bytes: usize, - pub(crate) max_read_ranges: usize, - pub(crate) max_read_entries: usize, - pub(crate) max_checks: usize, - pub(crate) max_mutations: usize, - pub(crate) max_watched_keys: usize, - pub(crate) max_total_mutation_size_bytes: usize, - pub(crate) max_total_key_size_bytes: usize, + pub max_write_key_size_bytes: usize, + pub max_read_key_size_bytes: usize, + pub max_value_size_bytes: usize, + pub max_read_ranges: usize, + pub max_read_entries: usize, + pub max_checks: usize, + pub max_mutations: usize, + pub max_watched_keys: usize, + pub max_total_mutation_size_bytes: usize, + pub max_total_key_size_bytes: usize, } impl KvConfig { diff --git a/ext/napi/Cargo.toml b/ext/napi/Cargo.toml index 064d335689..df3ec0287b 100644 --- a/ext/napi/Cargo.toml +++ b/ext/napi/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_napi" -version = "0.107.0" +version = "0.108.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/napi/sym/Cargo.toml b/ext/napi/sym/Cargo.toml index bf48d2742c..7c13a9165c 100644 --- a/ext/napi/sym/Cargo.toml +++ b/ext/napi/sym/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "napi_sym" -version = "0.106.0" +version = "0.107.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/net/Cargo.toml b/ext/net/Cargo.toml index 61bb5701ab..245deedd2d 100644 --- a/ext/net/Cargo.toml +++ b/ext/net/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_net" -version = "0.168.0" +version = "0.169.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index d830be5759..9e1a3495b5 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_node" -version = "0.113.0" +version = "0.114.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts index 5a3d4437a1..962ca86e92 100644 --- a/ext/node/polyfills/internal/errors.ts +++ b/ext/node/polyfills/internal/errors.ts @@ -18,7 +18,7 @@ */ import { primordials } from "ext:core/mod.js"; -const { JSONStringify, SymbolFor } = primordials; +const { JSONStringify, SafeArrayIterator, SymbolFor } = primordials; import { format, inspect } from "ext:deno_node/internal/util/inspect.mjs"; import { codes } from "ext:deno_node/internal/error_codes.ts"; import { @@ -1874,6 +1874,11 @@ export class ERR_SOCKET_CLOSED extends NodeError { super("ERR_SOCKET_CLOSED", `Socket is closed`); } } +export class ERR_SOCKET_CONNECTION_TIMEOUT extends NodeError { + constructor() { + super("ERR_SOCKET_CONNECTION_TIMEOUT", `Socket connection timeout`); + } +} export class ERR_SOCKET_DGRAM_IS_CONNECTED extends NodeError { constructor() { super("ERR_SOCKET_DGRAM_IS_CONNECTED", `Already connected`); @@ -2633,11 +2638,30 @@ export function aggregateTwoErrors( } return innerError || outerError; } + +export class NodeAggregateError extends AggregateError { + code: string; + constructor(errors, message) { + super(new SafeArrayIterator(errors), message); + this.code = errors[0]?.code; + } + + get [kIsNodeError]() { + return true; + } + + // deno-lint-ignore adjacent-overload-signatures + get ["constructor"]() { + return AggregateError; + } +} + codes.ERR_IPC_CHANNEL_CLOSED = ERR_IPC_CHANNEL_CLOSED; codes.ERR_INVALID_ARG_TYPE = ERR_INVALID_ARG_TYPE; codes.ERR_INVALID_ARG_VALUE = ERR_INVALID_ARG_VALUE; codes.ERR_OUT_OF_RANGE = ERR_OUT_OF_RANGE; codes.ERR_SOCKET_BAD_PORT = ERR_SOCKET_BAD_PORT; +codes.ERR_SOCKET_CONNECTION_TIMEOUT = ERR_SOCKET_CONNECTION_TIMEOUT; codes.ERR_BUFFER_OUT_OF_BOUNDS = ERR_BUFFER_OUT_OF_BOUNDS; codes.ERR_UNKNOWN_ENCODING = ERR_UNKNOWN_ENCODING; codes.ERR_PARSE_ARGS_INVALID_OPTION_VALUE = ERR_PARSE_ARGS_INVALID_OPTION_VALUE; diff --git a/ext/node/polyfills/internal/net.ts b/ext/node/polyfills/internal/net.ts index 144612626f..a3dcb3ed21 100644 --- a/ext/node/polyfills/internal/net.ts +++ b/ext/node/polyfills/internal/net.ts @@ -95,4 +95,5 @@ export function makeSyncWrite(fd: number) { }; } +export const kReinitializeHandle = Symbol("kReinitializeHandle"); export const normalizedArgsSymbol = Symbol("normalizedArgs"); diff --git a/ext/node/polyfills/internal_binding/uv.ts b/ext/node/polyfills/internal_binding/uv.ts index aa468a0a58..6cd70a7e85 100644 --- a/ext/node/polyfills/internal_binding/uv.ts +++ b/ext/node/polyfills/internal_binding/uv.ts @@ -530,10 +530,12 @@ export function mapSysErrnoToUvErrno(sysErrno: number): number { export const UV_EAI_MEMORY = codeMap.get("EAI_MEMORY")!; export const UV_EBADF = codeMap.get("EBADF")!; +export const UV_ECANCELED = codeMap.get("ECANCELED")!; export const UV_EEXIST = codeMap.get("EEXIST"); export const UV_EINVAL = codeMap.get("EINVAL")!; export const UV_ENOENT = codeMap.get("ENOENT"); export const UV_ENOTSOCK = codeMap.get("ENOTSOCK")!; +export const UV_ETIMEDOUT = codeMap.get("ETIMEDOUT")!; export const UV_UNKNOWN = codeMap.get("UNKNOWN")!; export function errname(errno: number): string { diff --git a/ext/node/polyfills/net.ts b/ext/node/polyfills/net.ts index 48e1d0de87..2b01125190 100644 --- a/ext/node/polyfills/net.ts +++ b/ext/node/polyfills/net.ts @@ -31,6 +31,7 @@ import { isIP, isIPv4, isIPv6, + kReinitializeHandle, normalizedArgsSymbol, } from "ext:deno_node/internal/net.ts"; import { Duplex } from "node:stream"; @@ -50,9 +51,11 @@ import { ERR_SERVER_ALREADY_LISTEN, ERR_SERVER_NOT_RUNNING, ERR_SOCKET_CLOSED, + ERR_SOCKET_CONNECTION_TIMEOUT, errnoException, exceptionWithHostPort, genericNodeError, + NodeAggregateError, uvExceptionWithHostPort, } from "ext:deno_node/internal/errors.ts"; import type { ErrnoException } from "ext:deno_node/internal/errors.ts"; @@ -80,6 +83,7 @@ import { Buffer } from "node:buffer"; import type { LookupOneOptions } from "ext:deno_node/internal/dns/utils.ts"; import { validateAbortSignal, + validateBoolean, validateFunction, validateInt32, validateNumber, @@ -100,13 +104,25 @@ import { ShutdownWrap } from "ext:deno_node/internal_binding/stream_wrap.ts"; import { assert } from "ext:deno_node/_util/asserts.ts"; import { isWindows } from "ext:deno_node/_util/os.ts"; import { ADDRCONFIG, lookup as dnsLookup } from "node:dns"; -import { codeMap } from "ext:deno_node/internal_binding/uv.ts"; +import { + codeMap, + UV_ECANCELED, + UV_ETIMEDOUT, +} from "ext:deno_node/internal_binding/uv.ts"; import { guessHandleType } from "ext:deno_node/internal_binding/util.ts"; import { debuglog } from "ext:deno_node/internal/util/debuglog.ts"; import type { DuplexOptions } from "ext:deno_node/_stream.d.ts"; import type { BufferEncoding } from "ext:deno_node/_global.d.ts"; import type { Abortable } from "ext:deno_node/_events.d.ts"; import { channel } from "node:diagnostics_channel"; +import { primordials } from "ext:core/mod.js"; + +const { + ArrayPrototypeIncludes, + ArrayPrototypePush, + FunctionPrototypeBind, + MathMax, +} = primordials; let debug = debuglog("net", (fn) => { debug = fn; @@ -120,6 +136,9 @@ const kBytesWritten = Symbol("kBytesWritten"); const DEFAULT_IPV4_ADDR = "0.0.0.0"; const DEFAULT_IPV6_ADDR = "::"; +let autoSelectFamilyDefault = true; +let autoSelectFamilyAttemptTimeoutDefault = 250; + type Handle = TCP | Pipe; interface HandleOptions { @@ -214,6 +233,8 @@ interface TcpSocketConnectOptions extends ConnectOptions { hints?: number; family?: number; lookup?: LookupFunction; + autoSelectFamily?: boolean | undefined; + autoSelectFamilyAttemptTimeout?: number | undefined; } interface IpcSocketConnectOptions extends ConnectOptions { @@ -316,12 +337,6 @@ export function _normalizeArgs(args: unknown[]): NormalizedArgs { return arr; } -function _isTCPConnectWrap( - req: TCPConnectWrap | PipeConnectWrap, -): req is TCPConnectWrap { - return "localAddress" in req && "localPort" in req; -} - function _afterConnect( status: number, // deno-lint-ignore no-explicit-any @@ -372,7 +387,7 @@ function _afterConnect( socket.connecting = false; let details; - if (_isTCPConnectWrap(req)) { + if (req.localAddress && req.localPort) { details = req.localAddress + ":" + req.localPort; } @@ -384,7 +399,7 @@ function _afterConnect( details, ); - if (_isTCPConnectWrap(req)) { + if (details) { ex.localAddress = req.localAddress; ex.localPort = req.localPort; } @@ -393,6 +408,107 @@ function _afterConnect( } } +function _createConnectionError(req, status) { + let details; + + if (req.localAddress && req.localPort) { + details = req.localAddress + ":" + req.localPort; + } + + const ex = exceptionWithHostPort( + status, + "connect", + req.address, + req.port, + details, + ); + if (details) { + ex.localAddress = req.localAddress; + ex.localPort = req.localPort; + } + + return ex; +} + +function _afterConnectMultiple( + context, + current, + status, + handle, + req, + readable, + writable, +) { + debug( + "connect/multiple: connection attempt to %s:%s completed with status %s", + req.address, + req.port, + status, + ); + + // Make sure another connection is not spawned + clearTimeout(context[kTimeout]); + + // One of the connection has completed and correctly dispatched but after timeout, ignore this one + if (status === 0 && current !== context.current - 1) { + debug( + "connect/multiple: ignoring successful but timedout connection to %s:%s", + req.address, + req.port, + ); + handle.close(); + return; + } + + const self = context.socket; + + // Some error occurred, add to the list of exceptions + if (status !== 0) { + const ex = _createConnectionError(req, status); + ArrayPrototypePush(context.errors, ex); + + self.emit( + "connectionAttemptFailed", + req.address, + req.port, + req.addressType, + ex, + ); + + // Try the next address, unless we were aborted + if (context.socket.connecting) { + _internalConnectMultiple(context, status === UV_ECANCELED); + } + + return; + } + + _afterConnect(status, self._handle, req, readable, writable); +} + +function _internalConnectMultipleTimeout(context, req, handle) { + debug( + "connect/multiple: connection to %s:%s timed out", + req.address, + req.port, + ); + context.socket.emit( + "connectionAttemptTimeout", + req.address, + req.port, + req.addressType, + ); + + req.oncomplete = undefined; + ArrayPrototypePush(context.errors, _createConnectionError(req, UV_ETIMEDOUT)); + handle.close(); + + // Try the next address, unless we were aborted + if (context.socket.connecting) { + _internalConnectMultiple(context); + } +} + function _checkBindError(err: number, port: number, handle: TCP) { // EADDRINUSE may not be reported until we call `listen()` or `connect()`. // To complicate matters, a failed `bind()` followed by `listen()` or `connect()` @@ -495,6 +611,131 @@ function _internalConnect( } } +function _internalConnectMultiple(context, canceled?: boolean) { + clearTimeout(context[kTimeout]); + const self = context.socket; + + // We were requested to abort. Stop all operations + if (self._aborted) { + return; + } + + // All connections have been tried without success, destroy with error + if (canceled || context.current === context.addresses.length) { + if (context.errors.length === 0) { + self.destroy(new ERR_SOCKET_CONNECTION_TIMEOUT()); + return; + } + + self.destroy(new NodeAggregateError(context.errors)); + return; + } + + assert(self.connecting); + + const current = context.current++; + + if (current > 0) { + self[kReinitializeHandle](new TCP(TCPConstants.SOCKET)); + } + + const { localPort, port, flags } = context; + const { address, family: addressType } = context.addresses[current]; + let localAddress; + let err; + + if (localPort) { + if (addressType === 4) { + localAddress = DEFAULT_IPV4_ADDR; + err = self._handle.bind(localAddress, localPort); + } else { // addressType === 6 + localAddress = DEFAULT_IPV6_ADDR; + err = self._handle.bind6(localAddress, localPort, flags); + } + + debug( + "connect/multiple: binding to localAddress: %s and localPort: %d (addressType: %d)", + localAddress, + localPort, + addressType, + ); + + err = _checkBindError(err, localPort, self._handle); + if (err) { + ArrayPrototypePush( + context.errors, + exceptionWithHostPort(err, "bind", localAddress, localPort), + ); + _internalConnectMultiple(context); + return; + } + } + + debug( + "connect/multiple: attempting to connect to %s:%d (addressType: %d)", + address, + port, + addressType, + ); + self.emit("connectionAttempt", address, port, addressType); + + const req = new TCPConnectWrap(); + req.oncomplete = FunctionPrototypeBind( + _afterConnectMultiple, + undefined, + context, + current, + ); + req.address = address; + req.port = port; + req.localAddress = localAddress; + req.localPort = localPort; + req.addressType = addressType; + + ArrayPrototypePush( + self.autoSelectFamilyAttemptedAddresses, + `${address}:${port}`, + ); + + if (addressType === 4) { + err = self._handle.connect(req, address, port); + } else { + err = self._handle.connect6(req, address, port); + } + + if (err) { + const sockname = self._getsockname(); + let details; + + if (sockname) { + details = sockname.address + ":" + sockname.port; + } + + const ex = exceptionWithHostPort(err, "connect", address, port, details); + ArrayPrototypePush(context.errors, ex); + + self.emit("connectionAttemptFailed", address, port, addressType, ex); + _internalConnectMultiple(context); + return; + } + + if (current < context.addresses.length - 1) { + debug( + "connect/multiple: setting the attempt timeout to %d ms", + context.timeout, + ); + + // If the attempt has not returned an error, start the connection timer + context[kTimeout] = setTimeout( + _internalConnectMultipleTimeout, + context.timeout, + context, + req, + self._handle, + ); + } +} + // Provide a better error message when we call end() as a result // of the other side sending a FIN. The standard "write after end" // is overly vague, and makes it seem like the user's code is to blame. @@ -597,7 +838,7 @@ function _lookupAndConnect( ) { const { localAddress, localPort } = options; const host = options.host || "localhost"; - let { port } = options; + let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options; if (localAddress && !isIP(localAddress)) { throw new ERR_INVALID_IP_ADDRESS(localAddress); @@ -621,6 +862,22 @@ function _lookupAndConnect( port |= 0; + if (autoSelectFamily != null) { + validateBoolean(autoSelectFamily, "options.autoSelectFamily"); + } else { + autoSelectFamily = autoSelectFamilyDefault; + } + + if (autoSelectFamilyAttemptTimeout !== undefined) { + validateInt32(autoSelectFamilyAttemptTimeout); + + if (autoSelectFamilyAttemptTimeout < 10) { + autoSelectFamilyAttemptTimeout = 10; + } + } else { + autoSelectFamilyAttemptTimeout = autoSelectFamilyAttemptTimeoutDefault; + } + // If host is an IP, skip performing a lookup const addressType = isIP(host); if (addressType) { @@ -649,6 +906,7 @@ function _lookupAndConnect( const dnsOpts = { family: options.family, hints: options.hints || 0, + all: false, }; if ( @@ -665,6 +923,31 @@ function _lookupAndConnect( self._host = host; const lookup = options.lookup || dnsLookup; + if ( + dnsOpts.family !== 4 && dnsOpts.family !== 6 && !localAddress && + autoSelectFamily + ) { + debug("connect: autodetecting"); + + dnsOpts.all = true; + defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () { + _lookupAndConnectMultiple( + self, + asyncIdSymbol, + lookup, + host, + options, + dnsOpts, + port, + localAddress, + localPort, + autoSelectFamilyAttemptTimeout, + ); + }); + + return; + } + defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () { lookup( host, @@ -719,6 +1002,143 @@ function _lookupAndConnect( }); } +function _lookupAndConnectMultiple( + self: Socket, + asyncIdSymbol: number, + // deno-lint-ignore no-explicit-any + lookup: any, + host: string, + options: TcpSocketConnectOptions, + dnsopts, + port: number, + localAddress: string, + localPort: number, + timeout: number | undefined, +) { + defaultTriggerAsyncIdScope(self[asyncIdSymbol], function emitLookup() { + lookup(host, dnsopts, function emitLookup(err, addresses) { + // It's possible we were destroyed while looking this up. + // XXX it would be great if we could cancel the promise returned by + // the look up. + if (!self.connecting) { + return; + } else if (err) { + self.emit("lookup", err, undefined, undefined, host); + + // net.createConnection() creates a net.Socket object and immediately + // calls net.Socket.connect() on it (that's us). There are no event + // listeners registered yet so defer the error event to the next tick. + nextTick(_connectErrorNT, self, err); + return; + } + + // Filter addresses by only keeping the one which are either IPv4 or IPV6. + // The first valid address determines which group has preference on the + // alternate family sorting which happens later. + const validAddresses = [[], []]; + const validIps = [[], []]; + let destinations; + for (let i = 0, l = addresses.length; i < l; i++) { + const address = addresses[i]; + const { address: ip, family: addressType } = address; + self.emit("lookup", err, ip, addressType, host); + // It's possible we were destroyed while looking this up. + if (!self.connecting) { + return; + } + if (isIP(ip) && (addressType === 4 || addressType === 6)) { + destinations ||= addressType === 6 ? { 6: 0, 4: 1 } : { 4: 0, 6: 1 }; + + const destination = destinations[addressType]; + + // Only try an address once + if (!ArrayPrototypeIncludes(validIps[destination], ip)) { + ArrayPrototypePush(validAddresses[destination], address); + ArrayPrototypePush(validIps[destination], ip); + } + } + } + + // When no AAAA or A records are available, fail on the first one + if (!validAddresses[0].length && !validAddresses[1].length) { + const { address: firstIp, family: firstAddressType } = addresses[0]; + + if (!isIP(firstIp)) { + err = new ERR_INVALID_IP_ADDRESS(firstIp); + nextTick(_connectErrorNT, self, err); + } else if (firstAddressType !== 4 && firstAddressType !== 6) { + err = new ERR_INVALID_ADDRESS_FAMILY( + firstAddressType, + options.host, + options.port, + ); + nextTick(_connectErrorNT, self, err); + } + + return; + } + + // Sort addresses alternating families + const toAttempt = []; + for ( + let i = 0, + l = MathMax(validAddresses[0].length, validAddresses[1].length); + i < l; + i++ + ) { + if (i in validAddresses[0]) { + ArrayPrototypePush(toAttempt, validAddresses[0][i]); + } + if (i in validAddresses[1]) { + ArrayPrototypePush(toAttempt, validAddresses[1][i]); + } + } + + if (toAttempt.length === 1) { + debug( + "connect/multiple: only one address found, switching back to single connection", + ); + const { address: ip, family: addressType } = toAttempt[0]; + + self._unrefTimer(); + defaultTriggerAsyncIdScope( + self[asyncIdSymbol], + _internalConnect, + self, + ip, + port, + addressType, + localAddress, + localPort, + ); + + return; + } + + self.autoSelectFamilyAttemptedAddresses = []; + debug("connect/multiple: will try the following addresses", toAttempt); + + const context = { + socket: self, + addresses: toAttempt, + current: 0, + port, + localPort, + timeout, + [kTimeout]: null, + errors: [], + }; + + self._unrefTimer(); + defaultTriggerAsyncIdScope( + self[asyncIdSymbol], + _internalConnectMultiple, + context, + ); + }); + }); +} + function _afterShutdown(this: ShutdownWrap) { // deno-lint-ignore no-explicit-any const self: any = this.handle[ownerSymbol]; @@ -777,6 +1197,7 @@ export class Socket extends Duplex { _host: string | null = null; // deno-lint-ignore no-explicit-any _parent: any = null; + autoSelectFamilyAttemptedAddresses: AddressInfo[] | undefined = undefined; constructor(options: SocketOptions | number) { if (typeof options === "number") { @@ -1546,6 +1967,16 @@ export class Socket extends Duplex { set _handle(v: Handle | null) { this[kHandle] = v; } + + // deno-lint-ignore no-explicit-any + [kReinitializeHandle](handle: any) { + this._handle?.close(); + + this._handle = handle; + this._handle[ownerSymbol] = this; + + _initSocketHandle(this); + } } export const Stream = Socket; @@ -1593,6 +2024,33 @@ export function connect(...args: unknown[]) { export const createConnection = connect; +/** https://docs.deno.com/api/node/net/#namespace_getdefaultautoselectfamily */ +export function getDefaultAutoSelectFamily() { + return autoSelectFamilyDefault; +} + +/** https://docs.deno.com/api/node/net/#namespace_setdefaultautoselectfamily */ +export function setDefaultAutoSelectFamily(value: boolean) { + validateBoolean(value, "value"); + autoSelectFamilyDefault = value; +} + +/** https://docs.deno.com/api/node/net/#namespace_getdefaultautoselectfamilyattempttimeout */ +export function getDefaultAutoSelectFamilyAttemptTimeout() { + return autoSelectFamilyAttemptTimeoutDefault; +} + +/** https://docs.deno.com/api/node/net/#namespace_setdefaultautoselectfamilyattempttimeout */ +export function setDefaultAutoSelectFamilyAttemptTimeout(value: number) { + validateInt32(value, "value", 1); + + if (value < 10) { + value = 10; + } + + autoSelectFamilyAttemptTimeoutDefault = value; +} + export interface ListenOptions extends Abortable { fd?: number; port?: number | undefined; @@ -2478,15 +2936,19 @@ export { BlockList, isIP, isIPv4, isIPv6, SocketAddress }; export default { _createServerHandle, _normalizeArgs, - isIP, - isIPv4, - isIPv6, BlockList, - SocketAddress, connect, createConnection, createServer, + getDefaultAutoSelectFamily, + getDefaultAutoSelectFamilyAttemptTimeout, + isIP, + isIPv4, + isIPv6, Server, + setDefaultAutoSelectFamily, + setDefaultAutoSelectFamilyAttemptTimeout, Socket, + SocketAddress, Stream, }; diff --git a/ext/tls/Cargo.toml b/ext/tls/Cargo.toml index 89a78f4b62..943fc8413e 100644 --- a/ext/tls/Cargo.toml +++ b/ext/tls/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_tls" -version = "0.163.0" +version = "0.164.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/url/Cargo.toml b/ext/url/Cargo.toml index 7f3fa97613..557a4669e6 100644 --- a/ext/url/Cargo.toml +++ b/ext/url/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_url" -version = "0.176.0" +version = "0.177.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/web/Cargo.toml b/ext/web/Cargo.toml index faba8e9fb2..db28d0e578 100644 --- a/ext/web/Cargo.toml +++ b/ext/web/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_web" -version = "0.207.0" +version = "0.208.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/webgpu/Cargo.toml b/ext/webgpu/Cargo.toml index c682965d5e..f23bb8371e 100644 --- a/ext/webgpu/Cargo.toml +++ b/ext/webgpu/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_webgpu" -version = "0.143.0" +version = "0.144.0" authors = ["the Deno authors"] edition.workspace = true license = "MIT" diff --git a/ext/webidl/Cargo.toml b/ext/webidl/Cargo.toml index 1e8dd77cf0..8c3f6f6128 100644 --- a/ext/webidl/Cargo.toml +++ b/ext/webidl/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_webidl" -version = "0.176.0" +version = "0.177.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/websocket/01_websocket.js b/ext/websocket/01_websocket.js index 58f4773101..468999b95d 100644 --- a/ext/websocket/01_websocket.js +++ b/ext/websocket/01_websocket.js @@ -28,6 +28,7 @@ const { ArrayPrototypePush, ArrayPrototypeShift, ArrayPrototypeSome, + Error, ErrorPrototypeToString, ObjectDefineProperties, ObjectPrototypeIsPrototypeOf, @@ -488,8 +489,11 @@ class WebSocket extends EventTarget { /* error */ this[_readyState] = CLOSED; + const message = op_ws_get_error(rid); + const error = new Error(message); const errorEv = new ErrorEvent("error", { - message: op_ws_get_error(rid), + error, + message, }); this.dispatchEvent(errorEv); diff --git a/ext/websocket/Cargo.toml b/ext/websocket/Cargo.toml index 6724cea2c1..61f1f5959c 100644 --- a/ext/websocket/Cargo.toml +++ b/ext/websocket/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_websocket" -version = "0.181.0" +version = "0.182.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/webstorage/Cargo.toml b/ext/webstorage/Cargo.toml index c1d2092f13..01e23ab839 100644 --- a/ext/webstorage/Cargo.toml +++ b/ext/webstorage/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_webstorage" -version = "0.171.0" +version = "0.172.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/resolvers/deno/Cargo.toml b/resolvers/deno/Cargo.toml index 060b3e7895..24d50587b3 100644 --- a/resolvers/deno/Cargo.toml +++ b/resolvers/deno/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_resolver" -version = "0.8.0" +version = "0.9.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/resolvers/node/Cargo.toml b/resolvers/node/Cargo.toml index 6cbfc717d5..6c2407ad9f 100644 --- a/resolvers/node/Cargo.toml +++ b/resolvers/node/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "node_resolver" -version = "0.15.0" +version = "0.16.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a509c711ee..ba236de149 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_runtime" -version = "0.185.0" +version = "0.186.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/runtime/errors.rs b/runtime/errors.rs index 5716944b7e..0c26e0e47a 100644 --- a/runtime/errors.rs +++ b/runtime/errors.rs @@ -623,7 +623,7 @@ fn get_ffi_call_error_class(e: &CallError) -> &'static str { fn get_webstorage_class_name(e: &WebStorageError) -> &'static str { match e { WebStorageError::ContextNotSupported => "DOMExceptionNotSupportedError", - WebStorageError::Sqlite(_) => todo!(), + WebStorageError::Sqlite(_) => "Error", WebStorageError::Io(e) => get_io_error_class(e), WebStorageError::StorageExceeded => "DOMExceptionQuotaExceededError", } diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs index de3141f1f9..ee2f660dcc 100644 --- a/runtime/ops/process.rs +++ b/runtime/ops/process.rs @@ -756,14 +756,17 @@ fn check_run_permission( if !env_var_names.is_empty() { // we don't allow users to launch subprocesses with any LD_ or DYLD_* // env vars set because this allows executing code (ex. LD_PRELOAD) - return Err(CheckRunPermissionError::Other(deno_core::error::custom_error( - "NotCapable", - format!( - "Requires --allow-all permissions to spawn subprocess with {} environment variable{}.", - env_var_names.join(", "), - if env_var_names.len() != 1 { "s" } else { "" } - ) - ))); + return Err(CheckRunPermissionError::Other( + deno_core::error::custom_error( + "NotCapable", + format!( + "Requires --allow-run permissions to spawn subprocess with {0} environment variable{1}. Alternatively, spawn with {2} environment variable{1} unset.", + env_var_names.join(", "), + if env_var_names.len() != 1 { "s" } else { "" }, + if env_var_names.len() != 1 { "these" } else { "the" } + ), + ), + )); } permissions.check_run(cmd, api_name)?; } diff --git a/runtime/permissions/Cargo.toml b/runtime/permissions/Cargo.toml index 5fcc8a3e13..e088eb8ce5 100644 --- a/runtime/permissions/Cargo.toml +++ b/runtime/permissions/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_permissions" -version = "0.36.0" +version = "0.37.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/runtime/permissions/lib.rs b/runtime/permissions/lib.rs index 6480f4bf58..71ef7d2289 100644 --- a/runtime/permissions/lib.rs +++ b/runtime/permissions/lib.rs @@ -40,8 +40,8 @@ pub use prompter::PromptResponse; #[derive(Debug, thiserror::Error)] #[error("Requires {access}, {}", format_permission_error(.name))] pub struct PermissionDeniedError { - access: String, - name: &'static str, + pub access: String, + pub name: &'static str, } fn format_permission_error(name: &'static str) -> String { @@ -1461,7 +1461,7 @@ pub struct SysDescriptor(String); impl SysDescriptor { pub fn parse(kind: String) -> Result { match kind.as_str() { - "hostname" | "osRelease" | "osUptime" | "loadavg" + "hostname" | "inspector" | "osRelease" | "osUptime" | "loadavg" | "networkInterfaces" | "systemMemoryInfo" | "uid" | "gid" | "cpus" | "homedir" | "getegid" | "statfs" | "getPriority" | "setPriority" | "userInfo" => Ok(Self(kind)), diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index 16951d9ede..664adaedfa 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -77,6 +77,7 @@ "test-fs-rmdir-recursive.js", "test-fs-write-file.js", "test-http-url.parse-https.request.js", + "test-net-autoselectfamily.js", "test-net-better-error-messages-path.js", "test-net-connect-buffer.js", "test-net-connect-buffer2.js", @@ -404,6 +405,7 @@ "test-http-url.parse-only-support-http-https-protocol.js", "test-icu-transcode.js", "test-net-access-byteswritten.js", + "test-net-autoselectfamily.js", "test-net-better-error-messages-listen-path.js", "test-net-better-error-messages-path.js", "test-net-better-error-messages-port-hostname.js", diff --git a/tests/node_compat/runner/TODO.md b/tests/node_compat/runner/TODO.md index 231a4f62c9..27c2ef3e78 100644 --- a/tests/node_compat/runner/TODO.md +++ b/tests/node_compat/runner/TODO.md @@ -1767,7 +1767,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co - [parallel/test-net-autoselectfamily-commandline-option.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-commandline-option.js) - [parallel/test-net-autoselectfamily-default.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-default.js) - [parallel/test-net-autoselectfamily-ipv4first.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-ipv4first.js) -- [parallel/test-net-autoselectfamily.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily.js) - [parallel/test-net-better-error-messages-listen.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-better-error-messages-listen.js) - [parallel/test-net-binary.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-binary.js) - [parallel/test-net-bind-twice.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-bind-twice.js) diff --git a/tests/node_compat/test.ts b/tests/node_compat/test.ts index f6db4ee1ae..6cb41d2e45 100644 --- a/tests/node_compat/test.ts +++ b/tests/node_compat/test.ts @@ -19,6 +19,7 @@ import { magenta } from "@std/fmt/colors"; import { pooledMap } from "@std/async/pool"; import { dirname, fromFileUrl, join } from "@std/path"; import { assertEquals, fail } from "@std/assert"; +import { distinct } from "@std/collections"; import { config, getPathsFromTestSuites, @@ -36,6 +37,9 @@ const testPaths = partitionParallelTestPaths( getPathsFromTestSuites(config.ignore), ), ); +testPaths.sequential = distinct(testPaths.sequential); +testPaths.parallel = distinct(testPaths.parallel); + const cwd = new URL(".", import.meta.url); const windowsIgnorePaths = new Set( getPathsFromTestSuites(config.windowsIgnore), diff --git a/tests/node_compat/test/common/index.js b/tests/node_compat/test/common/index.js index d2165aecd0..d358ffce5b 100644 --- a/tests/node_compat/test/common/index.js +++ b/tests/node_compat/test/common/index.js @@ -473,6 +473,7 @@ const pwdCommand = isWindows ? module.exports = { allowGlobals, + defaultAutoSelectFamilyAttemptTimeout: 2500, expectsError, expectWarning, getArrayBufferViews, diff --git a/tests/node_compat/test/parallel/test-net-autoselectfamily.js b/tests/node_compat/test/parallel/test-net-autoselectfamily.js new file mode 100644 index 0000000000..3b520e6c80 --- /dev/null +++ b/tests/node_compat/test/parallel/test-net-autoselectfamily.js @@ -0,0 +1,312 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually. + +'use strict'; + +const common = require('../common'); +const { parseDNSPacket, writeDNSPacket } = require('../common/dns'); + +const assert = require('assert'); +const dgram = require('dgram'); +const { Resolver } = require('dns'); +const { createConnection, createServer } = require('net'); + +// Test that happy eyeballs algorithm is properly implemented. + +// Purposely not using setDefaultAutoSelectFamilyAttemptTimeout here to test the +// parameter is correctly used in options. +// +// Some of the machines in the CI need more time to establish connection +const autoSelectFamilyAttemptTimeout = common.defaultAutoSelectFamilyAttemptTimeout; + +function _lookup(resolver, hostname, options, cb) { + resolver.resolve(hostname, 'ANY', (err, replies) => { + assert.notStrictEqual(options.family, 4); + + if (err) { + return cb(err); + } + + const hosts = replies + .map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 })) + .sort((a, b) => b.family - a.family); + + if (options.all === true) { + return cb(null, hosts); + } + + return cb(null, hosts[0].address, hosts[0].family); + }); +} + +function createDnsServer(ipv6Addrs, ipv4Addrs, cb) { + if (!Array.isArray(ipv6Addrs)) { + ipv6Addrs = [ipv6Addrs]; + } + + if (!Array.isArray(ipv4Addrs)) { + ipv4Addrs = [ipv4Addrs]; + } + + // Create a DNS server which replies with a AAAA and a A record for the same host + const socket = dgram.createSocket('udp4'); + + // TODO(kt3k): We use common.mustCallAtLeast instead of common.mustCall + // because Deno sends multiple requests to the DNS server. + // This can be addressed if Deno.resolveDns supports ANY record type. + // See https://github.com/denoland/deno/issues/14492 + socket.on('message', common.mustCallAtLeast((msg, { address, port }) => { + const parsed = parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + socket.send(writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: [ + ...ipv6Addrs.map((address) => ({ type: 'AAAA', address, ttl: 123, domain: 'example.org' })), + ...ipv4Addrs.map((address) => ({ type: 'A', address, ttl: 123, domain: 'example.org' })), + ] + }), port, address); + })); + + socket.bind(0, () => { + const resolver = new Resolver(); + resolver.setServers([`127.0.0.1:${socket.address().port}`]); + + cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) }); + }); +} + +// Test that IPV4 is reached if IPV6 is not reachable +{ + createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) { + const ipv4Server = createServer((socket) => { + socket.on('data', common.mustCall(() => { + socket.write('response-ipv4'); + socket.end(); + })); + }); + + ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { + const port = ipv4Server.address().port; + + const connection = createConnection({ + host: 'example.org', + port: port, + lookup, + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout, + }); + + let response = ''; + connection.setEncoding('utf-8'); + + connection.on('ready', common.mustCall(() => { + assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]); + })); + + connection.on('data', (chunk) => { + response += chunk; + }); + + connection.on('end', common.mustCall(() => { + assert.strictEqual(response, 'response-ipv4'); + ipv4Server.close(); + dnsServer.close(); + })); + + connection.write('request'); + })); + })); +} + +// Test that only the last successful connection is established. +{ + createDnsServer( + ['2606:4700::6810:85e5', '2606:4700::6810:84e5', "::1"], + // TODO(kt3k): Comment out ipv4 addresses to make the test pass faster. + // Enable this when Deno.connect() call becomes cancellable. + // See https://github.com/denoland/deno/issues/26819 + // ['104.20.22.46', '104.20.23.46', '127.0.0.1'], + ['127.0.0.1'], + common.mustCall(function({ dnsServer, lookup }) { + const ipv4Server = createServer((socket) => { + socket.on('data', common.mustCall(() => { + socket.write('response-ipv4'); + socket.end(); + })); + }); + + ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { + const port = ipv4Server.address().port; + + const connection = createConnection({ + host: 'example.org', + port: port, + lookup, + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout, + }); + + let response = ''; + connection.setEncoding('utf-8'); + + connection.on('ready', common.mustCall(() => { + assert.deepStrictEqual( + connection.autoSelectFamilyAttemptedAddresses, + [ + `2606:4700::6810:85e5:${port}`, + `104.20.22.46:${port}`, + `2606:4700::6810:84e5:${port}`, + `104.20.23.46:${port}`, + `::1:${port}`, + `127.0.0.1:${port}`, + ] + ); + })); + + connection.on('data', (chunk) => { + response += chunk; + }); + + connection.on('end', common.mustCall(() => { + assert.strictEqual(response, 'response-ipv4'); + ipv4Server.close(); + dnsServer.close(); + })); + + connection.write('request'); + })); + }) + ); +} + +// Test that IPV4 is NOT reached if IPV6 is reachable +if (common.hasIPv6) { + createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) { + const ipv4Server = createServer((socket) => { + socket.on('data', common.mustNotCall(() => { + socket.write('response-ipv4'); + socket.end(); + })); + }); + + const ipv6Server = createServer((socket) => { + socket.on('data', common.mustCall(() => { + socket.write('response-ipv6'); + socket.end(); + })); + }); + + ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { + const port = ipv4Server.address().port; + + ipv6Server.listen(port, '::1', common.mustCall(() => { + const connection = createConnection({ + host: 'example.org', + port, + lookup, + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout, + }); + + let response = ''; + connection.setEncoding('utf-8'); + + connection.on('ready', common.mustCall(() => { + assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]); + })); + + connection.on('data', (chunk) => { + response += chunk; + }); + + connection.on('end', common.mustCall(() => { + assert.strictEqual(response, 'response-ipv6'); + ipv4Server.close(); + ipv6Server.close(); + dnsServer.close(); + })); + + connection.write('request'); + })); + })); + })); +} + +// Test that when all errors are returned when no connections succeeded +{ + createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) { + const connection = createConnection({ + host: 'example.org', + port: 10, + lookup, + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout, + }); + + connection.on('ready', common.mustNotCall()); + connection.on('error', common.mustCall((error) => { + assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, ['::1:10', '127.0.0.1:10']); + assert.strictEqual(error.constructor.name, 'AggregateError'); + assert.strictEqual(error.errors.length, 2); + + const errors = error.errors.map((e) => e.message); + assert.ok(errors.includes('connect ECONNREFUSED 127.0.0.1:10')); + + if (common.hasIPv6) { + assert.ok(errors.includes('connect ECONNREFUSED ::1:10')); + } + + dnsServer.close(); + })); + })); +} + +// Test that the option can be disabled +{ + createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) { + const ipv4Server = createServer((socket) => { + socket.on('data', common.mustCall(() => { + socket.write('response-ipv4'); + socket.end(); + })); + }); + + ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { + const port = ipv4Server.address().port; + + const connection = createConnection({ + host: 'example.org', + port, + lookup, + autoSelectFamily: false, + }); + + connection.on('ready', common.mustNotCall()); + connection.on('error', common.mustCall((error) => { + assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined); + + if (common.hasIPv6) { + assert.strictEqual(error.code, 'ECONNREFUSED'); + assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`); + } else if (error.code === 'EAFNOSUPPORT') { + assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`); + } else if (error.code === 'EUNATCH') { + assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`); + } else { + assert.strictEqual(error.code, 'EADDRNOTAVAIL'); + assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`); + } + + ipv4Server.close(); + dnsServer.close(); + })); + })); + })); +} diff --git a/tests/registry/npm/@denotest/bin-created-by-lifecycle/1.0.0/install.mjs b/tests/registry/npm/@denotest/bin-created-by-lifecycle/1.0.0/install.mjs new file mode 100644 index 0000000000..31020fcdf9 --- /dev/null +++ b/tests/registry/npm/@denotest/bin-created-by-lifecycle/1.0.0/install.mjs @@ -0,0 +1,3 @@ +import * as fs from "node:fs"; + +fs.writeFileSync("./testbin.js", "#!/usr/bin/env node\nconsole.log('run testbin');"); \ No newline at end of file diff --git a/tests/registry/npm/@denotest/bin-created-by-lifecycle/1.0.0/package.json b/tests/registry/npm/@denotest/bin-created-by-lifecycle/1.0.0/package.json new file mode 100644 index 0000000000..ad8dea002e --- /dev/null +++ b/tests/registry/npm/@denotest/bin-created-by-lifecycle/1.0.0/package.json @@ -0,0 +1,10 @@ +{ + "name": "@denotest/bin-created-by-lifecycle", + "version": "1.0.0", + "scripts": { + "install": "node install.mjs" + }, + "bin": { + "testbin": "testbin.js" + } +} \ No newline at end of file diff --git a/tests/specs/npm/bin_entry_created_by_lifecycle/__test__.jsonc b/tests/specs/npm/bin_entry_created_by_lifecycle/__test__.jsonc new file mode 100644 index 0000000000..665aec823d --- /dev/null +++ b/tests/specs/npm/bin_entry_created_by_lifecycle/__test__.jsonc @@ -0,0 +1,29 @@ +{ + "tempDir": true, + "tests": { + "all_at_once": { + "steps": [ + { + "args": "install --allow-scripts", + "output": "all_at_once_install.out" + }, + { "args": "task run-testbin", "output": "run_testbin.out" } + ] + }, + "separate_steps": { + "steps": [ + { "if": "unix", "args": "install", "output": "install_warn.out" }, + { + "if": "windows", + "args": "install", + "output": "install_warn_windows.out" + }, + { + "args": "install --allow-scripts", + "output": "Initialize @denotest/bin-created-by-lifecycle@1.0.0: running 'install' script\n" + }, + { "args": "task run-testbin", "output": "run_testbin.out" } + ] + } + } +} diff --git a/tests/specs/npm/bin_entry_created_by_lifecycle/all_at_once_install.out b/tests/specs/npm/bin_entry_created_by_lifecycle/all_at_once_install.out new file mode 100644 index 0000000000..bfaba3caf2 --- /dev/null +++ b/tests/specs/npm/bin_entry_created_by_lifecycle/all_at_once_install.out @@ -0,0 +1,4 @@ +Download http://localhost:4260/@denotest%2fbin-created-by-lifecycle +Download http://localhost:4260/@denotest/bin-created-by-lifecycle/1.0.0.tgz +Initialize @denotest/bin-created-by-lifecycle@1.0.0 +Initialize @denotest/bin-created-by-lifecycle@1.0.0: running 'install' script diff --git a/tests/specs/npm/bin_entry_created_by_lifecycle/install_warn.out b/tests/specs/npm/bin_entry_created_by_lifecycle/install_warn.out new file mode 100644 index 0000000000..864a3f6f51 --- /dev/null +++ b/tests/specs/npm/bin_entry_created_by_lifecycle/install_warn.out @@ -0,0 +1,10 @@ +Download http://localhost:4260/@denotest%2fbin-created-by-lifecycle +Download http://localhost:4260/@denotest/bin-created-by-lifecycle/1.0.0.tgz +Initialize @denotest/bin-created-by-lifecycle@1.0.0 +Warning Trying to set up 'testbin' bin for "[WILDCARD]bin-created-by-lifecycle", but the entry point "[WILDCARD]testbin.js" doesn't exist. +Warning The following packages contained npm lifecycle scripts (preinstall/install/postinstall) that were not executed: +┠─ npm:@denotest/bin-created-by-lifecycle@1.0.0 +┃ +┠─ This may cause the packages to not work correctly. +┖─ To run lifecycle scripts, use the `--allow-scripts` flag with `deno install`: + deno install --allow-scripts=npm:@denotest/bin-created-by-lifecycle@1.0.0 diff --git a/tests/specs/npm/bin_entry_created_by_lifecycle/install_warn_windows.out b/tests/specs/npm/bin_entry_created_by_lifecycle/install_warn_windows.out new file mode 100644 index 0000000000..6838088735 --- /dev/null +++ b/tests/specs/npm/bin_entry_created_by_lifecycle/install_warn_windows.out @@ -0,0 +1,9 @@ +Download http://localhost:4260/@denotest%2fbin-created-by-lifecycle +Download http://localhost:4260/@denotest/bin-created-by-lifecycle/1.0.0.tgz +Initialize @denotest/bin-created-by-lifecycle@1.0.0 +Warning The following packages contained npm lifecycle scripts (preinstall/install/postinstall) that were not executed: +┠─ npm:@denotest/bin-created-by-lifecycle@1.0.0 +┃ +┠─ This may cause the packages to not work correctly. +┖─ To run lifecycle scripts, use the `--allow-scripts` flag with `deno install`: + deno install --allow-scripts=npm:@denotest/bin-created-by-lifecycle@1.0.0 diff --git a/tests/specs/npm/bin_entry_created_by_lifecycle/package.json b/tests/specs/npm/bin_entry_created_by_lifecycle/package.json new file mode 100644 index 0000000000..9a8941ed9c --- /dev/null +++ b/tests/specs/npm/bin_entry_created_by_lifecycle/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "@denotest/bin-created-by-lifecycle": "1.0.0" + }, + "scripts": { + "run-testbin": "testbin" + } +} diff --git a/tests/specs/npm/bin_entry_created_by_lifecycle/run_testbin.out b/tests/specs/npm/bin_entry_created_by_lifecycle/run_testbin.out new file mode 100644 index 0000000000..a03f8bc58e --- /dev/null +++ b/tests/specs/npm/bin_entry_created_by_lifecycle/run_testbin.out @@ -0,0 +1,2 @@ +Task run-testbin testbin +run testbin diff --git a/tests/specs/run/ld_preload/env_arg.out b/tests/specs/run/ld_preload/env_arg.out index 945737e65b..d87a1115c6 100644 --- a/tests/specs/run/ld_preload/env_arg.out +++ b/tests/specs/run/ld_preload/env_arg.out @@ -1,8 +1,8 @@ -NotCapable: Requires --allow-all permissions to spawn subprocess with LD_PRELOAD environment variable. +NotCapable: Requires --allow-run permissions to spawn subprocess with LD_PRELOAD environment variable. Alternatively, spawn with the environment variable unset. [WILDCARD] name: "NotCapable" } -NotCapable: Requires --allow-all permissions to spawn subprocess with LD_PRELOAD environment variable. +NotCapable: Requires --allow-run permissions to spawn subprocess with LD_PRELOAD environment variable. Alternatively, spawn with the environment variable unset. [WILDCARD] name: "NotCapable" } diff --git a/tests/specs/run/ld_preload/env_arg.ts b/tests/specs/run/ld_preload/env_arg.ts index d7ca1073df..fe043d56dc 100644 --- a/tests/specs/run/ld_preload/env_arg.ts +++ b/tests/specs/run/ld_preload/env_arg.ts @@ -1,5 +1,5 @@ try { - new Deno.Command("echo", { + new Deno.Command("curl", { env: { "LD_PRELOAD": "./libpreload.so", }, @@ -10,7 +10,7 @@ try { try { Deno.run({ - cmd: ["echo"], + cmd: ["curl"], env: { "LD_PRELOAD": "./libpreload.so", }, diff --git a/tests/specs/run/ld_preload/set_with_allow_env.out b/tests/specs/run/ld_preload/set_with_allow_env.out index f89582d6c8..570515fc00 100644 --- a/tests/specs/run/ld_preload/set_with_allow_env.out +++ b/tests/specs/run/ld_preload/set_with_allow_env.out @@ -1,8 +1,8 @@ -NotCapable: Requires --allow-all permissions to spawn subprocess with LD_PRELOAD environment variable. +NotCapable: Requires --allow-run permissions to spawn subprocess with LD_PRELOAD environment variable. Alternatively, spawn with the environment variable unset. [WILDCARD] name: "NotCapable" } -NotCapable: Requires --allow-all permissions to spawn subprocess with DYLD_FALLBACK_LIBRARY_PATH, LD_PRELOAD environment variables. +NotCapable: Requires --allow-run permissions to spawn subprocess with DYLD_FALLBACK_LIBRARY_PATH, LD_PRELOAD environment variables. Alternatively, spawn with these environment variables unset. [WILDCARD] name: "NotCapable" } diff --git a/tests/unit/websocket_test.ts b/tests/unit/websocket_test.ts index 7db876b177..3aafe8da22 100644 --- a/tests/unit/websocket_test.ts +++ b/tests/unit/websocket_test.ts @@ -453,7 +453,8 @@ Deno.test("invalid server", async () => { const { promise, resolve } = Promise.withResolvers(); const ws = new WebSocket("ws://localhost:2121"); let err = false; - ws.onerror = () => { + ws.onerror = (e) => { + assert("error" in e); err = true; }; ws.onclose = () => { diff --git a/tests/unit_node/http2_test.ts b/tests/unit_node/http2_test.ts index 7473a487ad..c540c90f7e 100644 --- a/tests/unit_node/http2_test.ts +++ b/tests/unit_node/http2_test.ts @@ -10,6 +10,11 @@ import * as net from "node:net"; import { assert, assertEquals } from "@std/assert"; import { curlRequest } from "../unit/test_util.ts"; +// Increase the timeout for the auto select family to avoid flakiness +net.setDefaultAutoSelectFamilyAttemptTimeout( + net.getDefaultAutoSelectFamilyAttemptTimeout() * 30, +); + for (const url of ["http://localhost:4246", "https://localhost:4247"]) { Deno.test(`[node/http2 client] ${url}`, { ignore: Deno.build.os === "windows",