From ecf055f89865e1c7d0636dc6bf41ed4d64fbd2d1 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Mon, 15 Jul 2024 22:05:14 +0000 Subject: [PATCH 01/20] build(cargo): pin rust toolchain version to `1.79.0` --- .devcontainer/devcontainer.json | 2 +- Dockerfile | 2 +- rust-toolchain.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e869090bb..b7691a786 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "dockerfile": "Dockerfile", "context": "..", "args": { - "ONNXRUNTIME_VERSION": "1.17.0" + "ONNXRUNTIME_VERSION": "1.17.0", } }, "features": { diff --git a/Dockerfile b/Dockerfile index 37992b4d1..6f77b77b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.4 -FROM rust:1.77.2-bookworm as builder +FROM rust:1.79.0-bookworm as builder ARG TARGETPLATFORM ARG GIT_V_VERSION ARG ONNXRUNTIME_VERSION=1.17.0 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index fb0f94604..4bd6c51da 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.77.2" \ No newline at end of file +channel = "1.79.0" \ No newline at end of file From e914380e971669e7bf431c1353e47212f41cace0 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Mon, 15 Jul 2024 22:06:43 +0000 Subject: [PATCH 02/20] chore(devcontainer): add deno lsp support --- .devcontainer/Dockerfile | 8 +++++++- .devcontainer/devcontainer.json | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index bb16757cc..af28a8fe9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,6 +2,7 @@ FROM mcr.microsoft.com/devcontainers/rust:dev-1-bookworm ARG TARGETPLATFORM ARG ONNXRUNTIME_VERSION +ARG DENO_VERSION RUN apt-get update && apt-get install -y build-essential cmake libclang-dev lldb \ nodejs npm hyperfine @@ -17,4 +18,9 @@ RUN mkdir -p /etc/sb_ai && cp -r /tmp/models /etc/sb_ai/models ENV ORT_DYLIB_PATH=/usr/local/bin/libonnxruntime.so ENV SB_AI_MODELS_DIR=/etc/sb_ai/models -RUN curl -fsSL https://ollama.com/install.sh | sh \ No newline at end of file +# Ollama +RUN curl -fsSL https://ollama.com/install.sh | sh + +# Deno +RUN curl -fsSL https://deno.land/install.sh | sh +RUN deno upgrade --version $DENO_VERSION \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b7691a786..4f1628bca 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,6 +5,7 @@ "context": "..", "args": { "ONNXRUNTIME_VERSION": "1.17.0", + "DENO_VERSION": "1.45.2" } }, "features": { @@ -31,7 +32,8 @@ "eamodio.gitlens", "ms-azuretools.vscode-docker", "ms-vscode.hexeditor", - "vadimcn.vscode-lldb" + "vadimcn.vscode-lldb", + "denoland.vscode-deno" ] } } From 84734227af77cd65744f9d597cb98df4b1bff982 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Mon, 15 Jul 2024 22:48:36 +0000 Subject: [PATCH 03/20] chore: reformatting `Cargo.toml` of all workspace members --- Cargo.toml | 58 ++++++----- crates/base/Cargo.toml | 155 +++++++++++++++-------------- crates/base_mem_check/Cargo.toml | 1 + crates/cli/Cargo.toml | 18 ++-- crates/cpu_timer/Cargo.toml | 21 ++-- crates/event_worker/Cargo.toml | 4 +- crates/http_utils/Cargo.toml | 8 +- crates/npm/Cargo.toml | 21 ++-- crates/sb_ai/Cargo.toml | 17 ++-- crates/sb_core/Cargo.toml | 40 ++++---- crates/sb_env/Cargo.toml | 3 +- crates/sb_fs/Cargo.toml | 16 +-- crates/sb_graph/Cargo.toml | 21 ++-- crates/sb_module_loader/Cargo.toml | 16 +-- crates/sb_os/Cargo.toml | 4 +- crates/sb_workers/Cargo.toml | 17 ++-- 16 files changed, 230 insertions(+), 190 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af987db05..086f728ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [workspace] +resolver = "2" members = [ "./crates/base", + "./crates/base_rt", "./crates/base_mem_check", + "./crates/http_utils", "./crates/cli", "./crates/sb_workers", "./crates/sb_env", @@ -15,42 +18,46 @@ members = [ "./crates/sb_fs", "./crates/sb_ai" ] -resolver = "2" [workspace.dependencies] -url = { version = "2.3.1" } -eszip = "=0.68.2" -log = "0.4.20" -anyhow = { version = "1.0.57" } -libc = { version = "0.2.144" } deno_ast = { version = "=0.38.1", features = ["transpiling"] } -deno_broadcast_channel = { version = "0.143.0" } -deno_core = { version = "0.278.0" } -deno_console = { version = "0.149.0" } -deno_crypto = { version = "0.163.0" } -deno_fetch = { version = "0.173.0" } +deno_broadcast_channel = "0.143.0" +deno_core = "0.278.0" +deno_console = "0.149.0" +deno_crypto = "0.163.0" +deno_fetch = "0.173.0" deno_fs = { version = "0.59.0", features = ["sync_fs"] } deno_config = "=0.16.3" deno_io = "0.59.0" deno_webgpu = "0.116.0" deno_canvas = "0.18.0" deno_graph = "=0.74.5" -deno_http = { version = "0.146.0" } +deno_http = "0.146.0" deno_media_type = { version = "0.1.4", features = ["module_specifier"] } -deno_net = { version = "0.141.0" } +deno_net = "0.141.0" deno_npm = "0.18.0" -deno_url = { version = "0.149.0" } +deno_url = "0.149.0" deno_semver = "0.5.4" -deno_tls = { version = "0.136.0"} -deno_webidl = { version = "0.149.0" } -deno_web = { version = "0.180.0" } -deno_websocket = { version = "0.154.0" } -deno_webstorage = { version = "0.144.0" } +deno_tls = "0.136.0" +deno_webidl = "0.149.0" +deno_web = "0.180.0" +deno_websocket = "0.154.0" +deno_webstorage = "0.144.0" +deno_cache_dir = "=0.6.1" + +# hazmat needed for PrehashSigner in ext/node +rsa = { version = "0.9.3", default-features = false, features = ["std", "pem", "hazmat"] } + +url = "2.3.1" +eszip = "=0.68.2" +log = "0.4.20" +anyhow = "1.0.57" +libc = "0.2.144" enum-as-inner = "0.6.0" serde = { version = "1.0.149", features = ["derive"] } hyper = "0.14.26" tokio = { version = "1.36.0", features = ["full"] } -bytes = { version = "1.4.0" } +bytes = "1.4.0" once_cell = "1.17.1" thiserror = "1.0.40" deno_lockfile = "0.19.0" @@ -62,19 +69,18 @@ regex = "^1.7.0" fs3 = "0.5.0" tokio-util = "0.7.4" uuid = { version = "1.3.0", features = ["v4"] } -rsa = { version = "0.9.3", default-features = false, features = ["std", "pem", "hazmat"] } # hazmat needed for PrehashSigner in ext/node monch = "=0.5.0" reqwest = { version = "0.11.20", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli", "socks", "json"] } ring = "^0.17.0" -urlencoding = { version = "2.1.2" } +urlencoding = "2.1.2" import_map = { version = "=0.18.0", features = ["ext"] } base64 = "0.21.4" -futures = { version = "0.3.28" } -futures-util = { version = "0.3.28" } -ctor = { version = "0.2.6" } +futures = "0.3.28" +futures-util = "0.3.28" +ctor = "0.2.6" fastwebsockets = { version = "0.4.4", features = ["upgrade"] } percent-encoding = "=2.3.1" -scopeguard = { version = "1.2.0" } +scopeguard = "1.2.0" glob = "0.3.1" httparse = "1.8" http = "0.2" diff --git a/crates/base/Cargo.toml b/crates/base/Cargo.toml index cb0c725cb..2c4800564 100644 --- a/crates/base/Cargo.toml +++ b/crates/base/Cargo.toml @@ -3,86 +3,110 @@ name = "base" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +deno_ast.workspace = true +deno_fs.workspace = true +deno_io.workspace = true +deno_core.workspace = true +deno_console.workspace = true +deno_config.workspace = true +deno_crypto.workspace = true +deno_fetch.workspace = true +deno_http.workspace = true +deno_net.workspace = true +deno_url.workspace = true +deno_tls.workspace = true +deno_webidl.workspace = true +deno_web.workspace = true +deno_websocket.workspace = true +deno_broadcast_channel.workspace = true +deno_canvas.workspace = true +deno_webgpu.workspace = true +deno_semver.workspace = true +deno_npm.workspace = true + base_rt = { version = "0.1.0", path = "../base_rt" } base_mem_check = { version = "0.1.0", path = "../base_mem_check" } + +cpu_timer = { version = "0.1.0", path = "../cpu_timer" } http_utils = { version = "0.1.0", path = "../http_utils" } +event_worker = { version = "0.1.0", path = "../event_worker" } + +sb_workers = { version = "0.1.0", path = "../sb_workers" } +sb_env = { version = "0.1.0", path = "../sb_env" } +sb_core = { version = "0.1.0", path = "../sb_core" } +sb_os = { version = "0.1.0", path = "../sb_os" } +sb_npm = { version = "0.1.0", path = "../npm" } +sb_graph = { version = "0.1.0", path = "../sb_graph" } +sb_module_loader = { version = "0.1.0", path = "../sb_module_loader" } +sb_node = { version = "0.1.0", path = "../node" } +sb_ai = { version = "0.1.0", path = "../sb_ai" } +sb_fs = { version = "0.1.0", path = "../sb_fs" } + async-trait.workspace = true thiserror.workspace = true monch.workspace = true once_cell.workspace = true -deno_semver.workspace = true -deno_npm.workspace = true -cpu_timer = { version = "0.1.0", path = "../cpu_timer" } -anyhow = { workspace = true } -bytes = { workspace = true } -cityhash = { version = "0.1.1" } -deno_ast = { workspace = true } -deno_fs.workspace = true -deno_io = { workspace = true } -deno_core.workspace = true -deno_console = { workspace = true } -deno_config = { workspace = true } -deno_crypto = { workspace = true } -deno_fetch = { workspace = true } -deno_http = { workspace = true } -deno_net = { workspace = true } -deno_url = { workspace = true } -deno_tls = { workspace = true } -deno_webidl = { workspace = true } -deno_web = { workspace = true } -deno_websocket = { workspace = true } -httparse = { workspace = true } +anyhow.workspace = true +bytes.workspace = true +httparse.workspace = true hyper = { workspace = true, features = ["full", "backports"] } -http = { version = "0.2" } +http.workspace = true import_map.workspace = true -log = { workspace = true } +log.workspace = true reqwest.workspace = true serde = { workspace = true, features = ["derive"] } -tokio = { workspace = true } +tokio.workspace = true tokio-util = { workspace = true, features = ["rt"] } -tokio-rustls = { version = "0.25.0" } -rustls-pemfile = { version = "2.1.0" } -futures-util = { workspace = true } +futures-util.workspace = true url.workspace = true -event_worker = { version = "0.1.0", path = "../event_worker" } -sb_workers = { version = "0.1.0", path = "../sb_workers" } -sb_env = { version = "0.1.0", path = "../sb_env" } -sb_core = { version = "0.1.0", path = "../sb_core" } -sb_os = { version = "0.1.0", path = "../sb_os" } -sb_npm = { version = "0.1.0", path = "../npm" } -sb_graph = { version = "0.1.0", path = "../sb_graph" } -sb_module_loader = { version = "0.1.0", path = "../sb_module_loader" } -uuid = { workspace = true } -deno_broadcast_channel.workspace = true -sb_node = { version = "0.1.0", path = "../node" } +uuid.workspace = true eszip.workspace = true -notify = { version = "6.1.1", default-features = false, features = ["macos_kqueue"] } -flume = { version = "0.11.0" } enum-as-inner.workspace = true urlencoding.workspace = true scopeguard.workspace = true -pin-project = { version = "1.1.3" } -ctor = { workspace = true } -deno_canvas.workspace = true -deno_webgpu.workspace = true -fastwebsockets = { workspace = true } -sb_ai = { version = "0.1.0", path = "../sb_ai" } -sb_fs = { version = "0.1.0", path = "../sb_fs" } +ctor.workspace = true +fastwebsockets.workspace = true + +notify = { version = "6.1.1", default-features = false, features = ["macos_kqueue"] } tls-listener = { version = "0.10", features = ["rustls"] } -cooked-waker = { version = "5" } +flume = "0.11.0" +pin-project = "1.1.3" +cooked-waker = "5" +cityhash = "0.1.1" +tokio-rustls = "0.25.0" +rustls-pemfile = "2.1.0" [dev-dependencies] tokio-util = { workspace = true, features = ["rt", "compat"] } reqwest = { workspace = true, features = ["multipart"] } -serial_test = { version = "3.0.0" } + +serial_test = "3.0.0" async-tungstenite = { version = "0.25.0", default-features = false } tungstenite = { version = "0.21.0", default-features = false, features = ["handshake"] } tracing-subscriber = { workspace = true, features = ["env-filter", "tracing-log"] } [build-dependencies] +deno_ast.workspace = true +deno_fs.workspace = true +deno_io.workspace = true +deno_console.workspace = true +deno_crypto.workspace = true +deno_fetch.workspace = true +deno_http.workspace = true +deno_net.workspace = true +deno_url.workspace = true +deno_tls.workspace = true +deno_webidl.workspace = true +deno_web.workspace = true +deno_websocket.workspace = true +deno_broadcast_channel.workspace = true +deno_core.workspace = true +deno_canvas.workspace = true +deno_webgpu.workspace = true + +event_worker = { version = "0.1.0", path = "../event_worker" } + sb_core = { version = "0.1.0", path = "../sb_core" } sb_npm = { version = "0.1.0", path = "../npm" } sb_graph = { version = "0.1.0", path = "../sb_graph" } @@ -91,33 +115,16 @@ sb_env = { version = "0.1.0", path = "../sb_env" } sb_os = { version = "0.1.0", path = "../sb_os" } sb_node = { version = "0.1.0", path = "../node" } sb_ai = { version = "0.1.0", path = "../sb_ai" } -anyhow = { workspace = true } -bytes = { workspace = true } -deno_ast = { workspace = true } -deno_fs.workspace = true -deno_io = { workspace = true } -deno_console = { workspace = true } -deno_crypto = { workspace = true } -deno_fetch = { workspace = true } -deno_http = { workspace = true } -deno_net = { workspace = true } -deno_url = { workspace = true } -deno_tls = { workspace = true } -deno_webidl = { workspace = true } -deno_web = { workspace = true } -deno_websocket = { workspace = true } + +anyhow.workspace = true +bytes.workspace = true hyper = { workspace = true, features = ["full"] } -http = { workspace = true } -log = { workspace = true } +http.workspace = true +log.workspace = true reqwest.workspace = true serde = { workspace = true, features = ["derive"] } tokio.workspace = true url.workspace = true -event_worker = { version = "0.1.0", path = "../event_worker" } -deno_broadcast_channel.workspace = true -deno_core.workspace = true -deno_canvas.workspace = true -deno_webgpu.workspace = true [features] termination-signal-ext = [] \ No newline at end of file diff --git a/crates/base_mem_check/Cargo.toml b/crates/base_mem_check/Cargo.toml index fef887fca..9d12c4581 100644 --- a/crates/base_mem_check/Cargo.toml +++ b/crates/base_mem_check/Cargo.toml @@ -5,4 +5,5 @@ edition = "2021" [dependencies] deno_core.workspace = true + serde.workspace = true \ No newline at end of file diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 905d11aee..86e5c0beb 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -9,20 +9,24 @@ name = "edge-runtime" path = "src/main.rs" [dependencies] -anyhow = { workspace = true } -base = { path = "../base" } -deno_core = { workspace = true } -clap = { version = "4.0.29", features = ["cargo", "string", "env"] } -env_logger = "0.10.0" +deno_core.workspace = true + +base = { version = "0.1.0", path = "../base" } + +sb_graph = { version = "0.1.0", path = "../sb_graph" } + +anyhow.workspace = true log.workspace = true -sb_graph = { path = "../sb_graph" } tokio.workspace = true glob.workspace = true once_cell.workspace = true + +clap = { version = "4.0.29", features = ["cargo", "string", "env"] } tracing-subscriber = { workspace = true, optional = true, features = ["env-filter", "tracing-log"] } +env_logger = "0.10.0" [build-dependencies] -dotenv-build = { version = "0.1.1" } +dotenv-build = "0.1.1" [features] tracing = ["dep:tracing-subscriber"] \ No newline at end of file diff --git a/crates/cpu_timer/Cargo.toml b/crates/cpu_timer/Cargo.toml index 02a62f9dd..53cbf5848 100644 --- a/crates/cpu_timer/Cargo.toml +++ b/crates/cpu_timer/Cargo.toml @@ -3,16 +3,15 @@ name = "cpu_timer" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -anyhow = { workspace = true } -libc = { workspace = true } -nix = { version = "0.26.2", features = ["signal"] } -tokio = { workspace = true } -log = { workspace = true } -ctor = { workspace = true } -futures = { workspace = true } -signal-hook = { version = "0.3.17" } +libc.workspace = true +anyhow.workspace = true +tokio.workspace = true +log.workspace = true +ctor.workspace = true +futures.workspace = true +once_cell.workspace = true + +signal-hook = "0.3.17" signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } -once_cell = { workspace = true } +nix = { version = "0.26.2", features = ["signal"] } diff --git a/crates/event_worker/Cargo.toml b/crates/event_worker/Cargo.toml index b812de68c..25e364ba4 100644 --- a/crates/event_worker/Cargo.toml +++ b/crates/event_worker/Cargo.toml @@ -11,8 +11,10 @@ license = "MIT" path = "lib.rs" [dependencies] -base_mem_check = { version = "0.1.0", path = "../base_mem_check" } deno_core.workspace = true + +base_mem_check = { version = "0.1.0", path = "../base_mem_check" } + uuid.workspace = true serde.workspace = true anyhow.workspace = true diff --git a/crates/http_utils/Cargo.toml b/crates/http_utils/Cargo.toml index 2710533d3..d5716857b 100644 --- a/crates/http_utils/Cargo.toml +++ b/crates/http_utils/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -tokio = { workspace = true } +tokio.workspace = true tokio-util = { workspace = true, features = ["rt"] } -futures-util = { workspace = true } -bytes = { workspace = true } hyper = { workspace = true, features = ["full"] } -http = { workspace = true } \ No newline at end of file +futures-util.workspace = true +bytes.workspace = true +http.workspace = true \ No newline at end of file diff --git a/crates/npm/Cargo.toml b/crates/npm/Cargo.toml index be15bbbac..25bfb8148 100644 --- a/crates/npm/Cargo.toml +++ b/crates/npm/Cargo.toml @@ -11,26 +11,29 @@ license = "MIT" path = "lib.rs" [dependencies] -sb_core = { version = "0.1.0", path = "../sb_core" } -deno_ast.workspace = true deno_core.workspace = true +deno_ast.workspace = true deno_npm.workspace = true deno_fs.workspace = true deno_semver.workspace = true +deno_lockfile.workspace = true +deno_graph.workspace = true + +sb_core = { version = "0.1.0", path = "../sb_core" } +sb_node = { version = "0.1.0", path = "../node" } + once_cell.workspace = true async-trait.workspace = true -base32 = "=0.4.0" -deno_lockfile.workspace = true tar.workspace = true flate2.workspace = true ring.workspace = true -sb_node = { version = "0.1.0", path = "../node" } serde.workspace = true percent-encoding.workspace = true -hex = "0.4" base64.workspace = true -bincode = "=1.3.3" thiserror.workspace = true log.workspace = true -deno_graph.workspace = true -indexmap.workspace = true \ No newline at end of file +indexmap.workspace = true + +base32 = "=0.4.0" +hex = "0.4" +bincode = "=1.3.3" \ No newline at end of file diff --git a/crates/sb_ai/Cargo.toml b/crates/sb_ai/Cargo.toml index e93114507..d9fdb0de5 100644 --- a/crates/sb_ai/Cargo.toml +++ b/crates/sb_ai/Cargo.toml @@ -10,15 +10,18 @@ license = "MIT" path = "lib.rs" [dependencies] -anyhow.workspace = true deno_core.workspace = true -log = { workspace = true } + +anyhow.workspace = true +log.workspace = true serde.workspace = true -ort = { git = "https://github.com/pykeio/ort", default-features = false, features = [ "ndarray", "half", "load-dynamic" ] } +tokio.workspace = true +once_cell.workspace = true +tracing.workspace = true + ndarray = "0.15" ndarray-linalg = "0.15" -tokenizers = { version = ">=0.13.4", default-features = false, features = [ "onig" ] } rand = "0.8" -tokio.workspace = true -once_cell.workspace = true -tracing.workspace = true \ No newline at end of file +tokenizers = { version = ">=0.13.4", default-features = false, features = [ "onig" ] } + +ort = { git = "https://github.com/pykeio/ort", default-features = false, features = [ "ndarray", "half", "load-dynamic" ] } diff --git a/crates/sb_core/Cargo.toml b/crates/sb_core/Cargo.toml index b231d1048..60d1171cb 100644 --- a/crates/sb_core/Cargo.toml +++ b/crates/sb_core/Cargo.toml @@ -11,6 +11,7 @@ license = "MIT" path = "lib.rs" [dependencies] +deno_core.workspace = true deno_ast = { workspace = true, features = ["transpiling", "bundler", "cjs", "codegen", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] } deno_net.workspace = true deno_web.workspace = true @@ -18,36 +19,33 @@ deno_fetch.workspace = true deno_fs.workspace = true deno_permissions = { version = "0.9.0" } deno_websocket.workspace = true +deno_http.workspace = true +deno_tls.workspace = true +deno_crypto.workspace = true +deno_graph.workspace = true +deno_io.workspace = true +deno_cache_dir.workspace = true +deno_webstorage.workspace = true + +base_rt = { version = "0.1.0", path = "../base_rt" } +base_mem_check = { version = "0.1.0", path = "../base_mem_check" } + +sb_node = { version = "0.1.0", path = "../node" } + +libc.workspace = true anyhow.workspace = true -deno_core.workspace = true tokio.workspace = true -deno_http.workspace = true hyper.workspace = true serde.workspace = true bytes.workspace = true -deno_tls.workspace = true thiserror.workspace = true -base_rt = { version = "0.1.0", path = "../base_rt" } -base_mem_check = { version = "0.1.0", path = "../base_mem_check" } -sb_node = { version = "0.1.0", path = "../node" } -deno_crypto.workspace = true fs3.workspace = true log.workspace = true tokio-util.workspace = true ring.workspace = true -deno_graph.workspace = true -deno_io.workspace = true once_cell.workspace = true import_map.workspace = true -data-url = { version= "=0.3.0" } -cache_control = { version = "=0.2.0" } -chrono = { version = "=0.4.22", default-features = false, features = ["clock"] } -deno_cache_dir = "=0.6.1" -libc = { workspace = true } -twox-hash = { version = "=1.6.3" } -deno_webstorage.workspace = true indexmap.workspace = true -encoding_rs = { version = "=0.8.33" } base64.workspace = true futures.workspace = true percent-encoding.workspace = true @@ -55,7 +53,13 @@ scopeguard.workspace = true enum-as-inner.workspace = true httparse.workspace = true http.workspace = true -memmem = "0.1" faster-hex.workspace = true tracing.workspace = true fqdn = "0.3.4" + +data-url = "=0.3.0" +cache_control = "=0.2.0" +chrono = { version = "=0.4.22", default-features = false, features = ["clock"] } +twox-hash = "=1.6.3" +encoding_rs = "=0.8.33" +memmem = "0.1" diff --git a/crates/sb_env/Cargo.toml b/crates/sb_env/Cargo.toml index ec7357197..796e9c19f 100644 --- a/crates/sb_env/Cargo.toml +++ b/crates/sb_env/Cargo.toml @@ -11,6 +11,7 @@ license = "MIT" path = "lib.rs" [dependencies] -sb_core = { version = "0.1.0", path = "../sb_core" } deno_core.workspace = true + +sb_core = { version = "0.1.0", path = "../sb_core" } sb_node = { version = "0.1.0", path = "../node" } diff --git a/crates/sb_fs/Cargo.toml b/crates/sb_fs/Cargo.toml index e3edfdc59..20d22245e 100644 --- a/crates/sb_fs/Cargo.toml +++ b/crates/sb_fs/Cargo.toml @@ -10,25 +10,27 @@ license = "MIT" path = "lib.rs" [dependencies] +deno_core.workspace = true +deno_semver.workspace = true +deno_ast.workspace = true +deno_fs.workspace = true +deno_npm.workspace = true +deno_io.workspace = true + sb_core = { version = "0.1.0", path = "../sb_core" } sb_node = { version = "0.1.0", path = "../node" } sb_npm = { version = "0.1.0", path = "../npm" } sb_eszip_shared = { version = "0.1.0", path = "../sb_eszip_shared" } -deno_semver.workspace = true + anyhow.workspace = true -deno_core.workspace = true -eszip.workspace = true import_map.workspace = true log.workspace = true serde.workspace = true tokio.workspace = true futures.workspace = true -deno_ast.workspace = true -deno_fs.workspace = true -deno_npm.workspace = true once_cell.workspace = true -deno_io.workspace = true thiserror.workspace = true async-trait.workspace = true +eszip.workspace = true url.workspace = true rkyv = { workspace = true, features = ["validation"] } diff --git a/crates/sb_graph/Cargo.toml b/crates/sb_graph/Cargo.toml index e52bdd310..35d174a69 100644 --- a/crates/sb_graph/Cargo.toml +++ b/crates/sb_graph/Cargo.toml @@ -10,28 +10,29 @@ license = "MIT" path = "lib.rs" [dependencies] +deno_core.workspace = true +deno_ast.workspace = true +deno_fs.workspace = true +deno_npm.workspace = true +deno_semver.workspace = true +deno_web.workspace = true +deno_lockfile.workspace = true +deno_config.workspace = true + sb_core = { version = "0.1.0", path = "../sb_core" } sb_node = { version = "0.1.0", path = "../node" } sb_npm = { version = "0.1.0", path = "../npm" } sb_fs = { version = "0.1.0", path = "../sb_fs" } sb_eszip_shared = { version = "0.1.0", path = "../sb_eszip_shared" } -deno_semver.workspace = true anyhow.workspace = true -deno_core.workspace = true -eszip.workspace = true import_map.workspace = true log.workspace = true serde.workspace = true tokio.workspace = true -deno_ast.workspace = true -deno_fs.workspace = true -deno_npm.workspace = true once_cell.workspace = true -deno_web.workspace = true urlencoding.workspace = true -deno_lockfile.workspace = true -deno_config.workspace = true glob.workspace = true +eszip.workspace = true futures.workspace = true sha2.workspace = true scopeguard.workspace = true @@ -41,4 +42,4 @@ hashlink = { version = "0.8" } pathdiff = { version = "0.2" } [dev-dependencies] -tempfile.workspace = true \ No newline at end of file +tempfile.workspace = true diff --git a/crates/sb_module_loader/Cargo.toml b/crates/sb_module_loader/Cargo.toml index 339857373..6b95a2c0a 100644 --- a/crates/sb_module_loader/Cargo.toml +++ b/crates/sb_module_loader/Cargo.toml @@ -10,26 +10,28 @@ license = "MIT" path = "lib.rs" [dependencies] +deno_core.workspace = true +deno_semver.workspace = true +deno_ast.workspace = true +deno_fs.workspace = true +deno_npm.workspace = true +deno_tls.workspace = true + sb_core = { version = "0.1.0", path = "../sb_core" } sb_node = { version = "0.1.0", path = "../node" } sb_npm = { version = "0.1.0", path = "../npm" } sb_graph = { version = "0.1.0", path = "../sb_graph" } sb_fs = { version = "0.1.0", path = "../sb_fs" } sb_eszip_shared = { version = "0.1.0", path = "../sb_eszip_shared" } -deno_semver.workspace = true + anyhow.workspace = true -deno_core.workspace = true -eszip.workspace = true import_map.workspace = true log.workspace = true serde.workspace = true tokio.workspace = true -deno_ast.workspace = true -deno_fs.workspace = true -deno_npm.workspace = true once_cell.workspace = true -deno_tls.workspace = true monch.workspace = true base64.workspace = true tracing.workspace = true +eszip.workspace = true futures-util.workspace = true diff --git a/crates/sb_os/Cargo.toml b/crates/sb_os/Cargo.toml index d8a505cdf..90dfdb1c0 100644 --- a/crates/sb_os/Cargo.toml +++ b/crates/sb_os/Cargo.toml @@ -11,7 +11,9 @@ license = "MIT" path = "lib.rs" [dependencies] -sb_core = { version = "0.1.0", path = "../sb_core" } deno_core.workspace = true + +sb_core = { version = "0.1.0", path = "../sb_core" } + libc.workspace = true serde.workspace = true diff --git a/crates/sb_workers/Cargo.toml b/crates/sb_workers/Cargo.toml index 51882f514..2e98039f3 100644 --- a/crates/sb_workers/Cargo.toml +++ b/crates/sb_workers/Cargo.toml @@ -11,11 +11,19 @@ license = "MIT" path = "lib.rs" [dependencies] +deno_core.workspace = true +deno_http.workspace = true +deno_config.workspace = true + +http_utils = { version = "0.1.0", path = "../http_utils" } +event_worker = { version = "0.1.0", path = "../event_worker" } + +sb_core = { version = "0.1.0", path = "../sb_core" } +sb_graph = { version = "0.1.0", path = "../sb_graph" } + anyhow.workspace = true uuid.workspace = true -deno_core.workspace = true tokio.workspace = true -deno_http.workspace = true hyper.workspace = true serde.workspace = true bytes.workspace = true @@ -25,8 +33,3 @@ futures-util.workspace = true tokio-util.workspace = true thiserror.workspace = true scopeguard.workspace = true -http_utils = { version = "0.1.0", path = "../http_utils" } -event_worker = { version = "0.1.0", path = "../event_worker" } -sb_graph = { version = "0.1.0", path = "../sb_graph" } -sb_core = { version = "0.1.0", path = "../sb_core" } -deno_config.workspace = true \ No newline at end of file From 3643e76be0413658c2df9fd0b8463d2937e9da8a Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 16 Jul 2024 13:38:38 +0900 Subject: [PATCH 04/20] stamp: sync with `ext/node` --- crates/node/analyze.rs | 286 ++-- crates/node/clippy.toml | 3 + crates/node/errors.rs | 557 ++++-- crates/node/global.rs | 77 +- crates/node/lib.rs | 101 +- crates/node/ops/blocklist.rs | 297 ++++ crates/node/ops/buffer.rs | 13 + crates/node/ops/crypto/digest.rs | 311 +++- crates/node/ops/crypto/md5_sha1.rs | 94 + crates/node/ops/crypto/mod.rs | 261 ++- crates/node/ops/crypto/x509.rs | 4 +- crates/node/ops/fs.rs | 202 ++- crates/node/ops/http2.rs | 69 +- crates/node/ops/mod.rs | 2 + crates/node/ops/os/cpus.rs | 2 +- crates/node/ops/os/mod.rs | 37 +- crates/node/ops/require.rs | 94 +- crates/node/ops/v8.rs | 46 - crates/node/ops/zlib/brotli.rs | 303 ++-- crates/node/ops/zlib/mod.rs | 106 +- crates/node/package_json.rs | 277 +-- crates/node/polyfill.rs | 1 + crates/node/polyfills/01_require.js | 12 +- crates/node/polyfills/02_init.js | 34 +- crates/node/polyfills/_brotli.js | 65 +- crates/node/polyfills/_fs/_fs_cp.js | 16 +- crates/node/polyfills/_fs/_fs_dir.ts | 37 +- crates/node/polyfills/_fs/_fs_dirent.ts | 11 +- crates/node/polyfills/_fs/_fs_futimes.ts | 16 +- crates/node/polyfills/_fs/_fs_lchown.ts | 61 + crates/node/polyfills/_fs/_fs_lstat.ts | 24 +- crates/node/polyfills/_fs/_fs_lutimes.ts | 85 + crates/node/polyfills/_fs/_fs_read.ts | 4 - crates/node/polyfills/_fs/_fs_readdir.ts | 10 +- crates/node/polyfills/_fs/_fs_utimes.ts | 13 +- crates/node/polyfills/_fs/_fs_write.mjs | 6 +- crates/node/polyfills/_fs/_fs_writeFile.ts | 20 +- crates/node/polyfills/_http_common.ts | 15 +- crates/node/polyfills/_process/process.ts | 40 +- crates/node/polyfills/_process/streams.mjs | 44 +- crates/node/polyfills/_stream.mjs | 222 +-- crates/node/polyfills/_utils.ts | 68 +- crates/node/polyfills/_zlib_binding.mjs | 28 +- crates/node/polyfills/assert.ts | 33 +- crates/node/polyfills/async_hooks.ts | 2 + crates/node/polyfills/buffer.ts | 2 + crates/node/polyfills/constants.ts | 188 ++ crates/node/polyfills/diagnostics_channel.js | 430 +++++ crates/node/polyfills/diagnostics_channel.ts | 93 - crates/node/polyfills/fs.ts | 25 +- crates/node/polyfills/http.ts | 606 ++++--- crates/node/polyfills/http2.ts | 1508 +++++++++++++---- crates/node/polyfills/internal/blocklist.mjs | 227 +++ crates/node/polyfills/internal/buffer.mjs | 88 +- .../node/polyfills/internal/child_process.ts | 19 +- crates/node/polyfills/internal/cli_table.ts | 5 +- crates/node/polyfills/internal/crypto/hash.ts | 232 +-- crates/node/polyfills/internal/crypto/hkdf.ts | 7 +- crates/node/polyfills/internal/crypto/keys.ts | 15 + .../node/polyfills/internal/crypto/pbkdf2.ts | 76 +- crates/node/polyfills/internal/crypto/sig.ts | 9 - .../node/polyfills/internal/crypto/types.ts | 2 +- crates/node/polyfills/internal/errors.ts | 27 +- crates/node/polyfills/internal/fixed_queue.ts | 8 +- crates/node/polyfills/internal/fs/handle.ts | 14 +- crates/node/polyfills/internal/fs/utils.mjs | 37 +- crates/node/polyfills/internal/util.mjs | 36 + crates/node/polyfills/internal/validators.mjs | 15 +- .../node/polyfills/internal_binding/_utils.ts | 10 +- crates/node/polyfills/net.ts | 6 +- crates/node/polyfills/os.ts | 32 +- crates/node/polyfills/path/_util.ts | 6 +- crates/node/polyfills/perf_hooks.ts | 24 +- crates/node/polyfills/process.ts | 648 +++---- crates/node/polyfills/string_decoder.ts | 25 +- crates/node/polyfills/testing.ts | 34 +- crates/node/polyfills/timers.ts | 14 +- crates/node/polyfills/url.ts | 28 +- crates/node/polyfills/v8.ts | 43 +- crates/node/polyfills/worker_threads.ts | 48 +- crates/node/resolution.rs | 980 ++++++----- 81 files changed, 6450 insertions(+), 3126 deletions(-) create mode 100644 crates/node/ops/blocklist.rs create mode 100644 crates/node/ops/buffer.rs create mode 100644 crates/node/ops/crypto/md5_sha1.rs create mode 100644 crates/node/polyfills/_fs/_fs_lchown.ts create mode 100644 crates/node/polyfills/_fs/_fs_lutimes.ts create mode 100644 crates/node/polyfills/diagnostics_channel.js delete mode 100644 crates/node/polyfills/diagnostics_channel.ts create mode 100644 crates/node/polyfills/internal/blocklist.mjs diff --git a/crates/node/analyze.rs b/crates/node/analyze.rs index 0f7497f3b..0c4568f2d 100644 --- a/crates/node/analyze.rs +++ b/crates/node/analyze.rs @@ -1,37 +1,28 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::collections::BTreeSet; use std::collections::HashSet; -use std::collections::VecDeque; use std::path::Path; use std::path::PathBuf; use deno_core::anyhow; use deno_core::anyhow::Context; +use deno_core::futures::future::LocalBoxFuture; +use deno_core::futures::stream::FuturesUnordered; +use deno_core::futures::FutureExt; +use deno_core::futures::StreamExt; use deno_core::ModuleSpecifier; use once_cell::sync::Lazy; use deno_core::error::AnyError; +use crate::package_json::load_pkg_json; use crate::path::to_file_specifier; use crate::resolution::NodeResolverRc; use crate::NodeModuleKind; -use crate::NodePermissions; use crate::NodeResolutionMode; use crate::NpmResolverRc; -use crate::PackageJson; use crate::PathClean; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum CliCjsAnalysis { - /// The module was found to be an ES module. - Esm, - /// The module was CJS. - Cjs { - exports: Vec, - reexports: Vec, - }, -} #[derive(Debug, Clone)] pub enum CjsAnalysis { @@ -48,6 +39,7 @@ pub struct CjsAnalysisExports { } /// Code analyzer for CJS and ESM files. +#[async_trait::async_trait(?Send)] pub trait CjsCodeAnalyzer { /// Analyzes CommonJs code for exports and reexports, which is /// then used to determine the wrapper ESM module exports. @@ -56,7 +48,7 @@ pub trait CjsCodeAnalyzer { /// already has it. If the source is needed by the implementation, /// then it can use the provided source, or otherwise load it if /// necessary. - fn analyze_cjs( + async fn analyze_cjs( &self, specifier: &ModuleSpecifier, maybe_source: Option, @@ -91,16 +83,17 @@ impl NodeCodeTranslator { /// For all discovered reexports the analysis will be performed recursively. /// /// If successful a source code for equivalent ES module is returned. - pub fn translate_cjs_to_esm( + pub async fn translate_cjs_to_esm( &self, - specifier: &ModuleSpecifier, + entry_specifier: &ModuleSpecifier, source: Option, - permissions: &dyn NodePermissions, ) -> Result { let mut temp_var_count = 0; - let mut handled_reexports: HashSet = HashSet::default(); - let analysis = self.cjs_code_analyzer.analyze_cjs(specifier, source)?; + let analysis = self + .cjs_code_analyzer + .analyze_cjs(entry_specifier, source) + .await?; let analysis = match analysis { CjsAnalysis::Esm(source) => return Ok(source), @@ -113,73 +106,29 @@ impl NodeCodeTranslator { .to_string(), ]; - let mut all_exports = analysis - .exports - .iter() - .map(|s| s.to_string()) - .collect::>(); - - // (request, referrer) - let mut reexports_to_handle = VecDeque::new(); - for reexport in analysis.reexports { - reexports_to_handle.push_back((reexport, specifier.clone())); - } - - while let Some((reexport, referrer)) = reexports_to_handle.pop_front() { - if handled_reexports.contains(&reexport) { - continue; + // use a BTreeSet to make the output deterministic for v8's code cache + let mut all_exports = analysis.exports.into_iter().collect::>(); + + if !analysis.reexports.is_empty() { + let mut errors = Vec::new(); + self.analyze_reexports( + entry_specifier, + analysis.reexports, + &mut all_exports, + &mut errors, + ) + .await; + + // surface errors afterwards in a deterministic way + if !errors.is_empty() { + errors.sort_by_cached_key(|e| e.to_string()); + return Err(errors.remove(0)); } - - handled_reexports.insert(reexport.to_string()); - - // First, resolve the reexport specifier - let reexport_specifier = self.resolve( - &reexport, - &referrer, - // FIXME(bartlomieju): check if these conditions are okay, probably - // should be `deno-require`, because `deno` is already used in `esm_resolver.rs` - &["deno", "require", "default"], - NodeResolutionMode::Execution, - permissions, - )?; - - // Second, resolve its exports and re-exports - let analysis = self - .cjs_code_analyzer - .analyze_cjs(&reexport_specifier, None) - .with_context(|| { - format!( - "Could not load '{}' ({}) referenced from {}", - reexport, reexport_specifier, referrer - ) - })?; - let analysis = match analysis { - CjsAnalysis::Esm(_) => { - // todo(dsherret): support this once supporting requiring ES modules - return Err(anyhow::anyhow!( - "Cannot require ES module '{}' from '{}'", - reexport_specifier, - specifier - )); - } - CjsAnalysis::Cjs(analysis) => analysis, - }; - - for reexport in analysis.reexports { - reexports_to_handle.push_back((reexport, reexport_specifier.clone())); - } - - all_exports.extend( - analysis - .exports - .into_iter() - .filter(|e| e.as_str() != "default"), - ); } source.push(format!( "const mod = require(\"{}\");", - specifier + entry_specifier .to_file_path() .unwrap() .to_str() @@ -206,13 +155,134 @@ impl NodeCodeTranslator { Ok(translated_source) } + async fn analyze_reexports<'a>( + &'a self, + entry_specifier: &url::Url, + reexports: Vec, + all_exports: &mut BTreeSet, + // this goes through the modules concurrently, so collect + // the errors in order to be deterministic + errors: &mut Vec, + ) { + struct Analysis { + reexport_specifier: url::Url, + referrer: url::Url, + analysis: CjsAnalysis, + } + + type AnalysisFuture<'a> = LocalBoxFuture<'a, Result>; + + let mut handled_reexports: HashSet = HashSet::default(); + handled_reexports.insert(entry_specifier.clone()); + let mut analyze_futures: FuturesUnordered> = FuturesUnordered::new(); + let cjs_code_analyzer = &self.cjs_code_analyzer; + let mut handle_reexports = + |referrer: url::Url, + reexports: Vec, + analyze_futures: &mut FuturesUnordered>, + errors: &mut Vec| { + // 1. Resolve the re-exports and start a future to analyze each one + for reexport in reexports { + let result = self.resolve( + &reexport, + &referrer, + // FIXME(bartlomieju): check if these conditions are okay, probably + // should be `deno-require`, because `deno` is already used in `esm_resolver.rs` + &["deno", "require", "default"], + NodeResolutionMode::Execution, + ); + let reexport_specifier = match result { + Ok(specifier) => specifier, + Err(err) => { + errors.push(err); + continue; + } + }; + + if !handled_reexports.insert(reexport_specifier.clone()) { + continue; + } + + let referrer = referrer.clone(); + let future = async move { + let analysis = cjs_code_analyzer + .analyze_cjs(&reexport_specifier, None) + .await + .with_context(|| { + format!( + "Could not load '{}' ({}) referenced from {}", + reexport, reexport_specifier, referrer + ) + })?; + + Ok(Analysis { + reexport_specifier, + referrer, + analysis, + }) + } + .boxed_local(); + analyze_futures.push(future); + } + }; + + handle_reexports( + entry_specifier.clone(), + reexports, + &mut analyze_futures, + errors, + ); + + while let Some(analysis_result) = analyze_futures.next().await { + // 2. Look at the analysis result and resolve its exports and re-exports + let Analysis { + reexport_specifier, + referrer, + analysis, + } = match analysis_result { + Ok(analysis) => analysis, + Err(err) => { + errors.push(err); + continue; + } + }; + match analysis { + CjsAnalysis::Esm(_) => { + // todo(dsherret): support this once supporting requiring ES modules + errors.push(anyhow::anyhow!( + "Cannot require ES module '{}' from '{}'", + reexport_specifier, + referrer, + )); + } + CjsAnalysis::Cjs(analysis) => { + if !analysis.reexports.is_empty() { + handle_reexports( + reexport_specifier.clone(), + analysis.reexports, + &mut analyze_futures, + errors, + ); + } + + all_exports.extend( + analysis + .exports + .into_iter() + .filter(|e| e.as_str() != "default"), + ); + } + } + } + } + + // todo(dsherret): what is going on here? Isn't this a bunch of duplicate code? fn resolve( &self, specifier: &str, referrer: &ModuleSpecifier, conditions: &[&str], mode: NodeResolutionMode, - permissions: &dyn NodePermissions, ) -> Result { if specifier.starts_with('/') { todo!(); @@ -233,31 +303,26 @@ impl NodeCodeTranslator { let (package_specifier, package_subpath) = parse_specifier(specifier).unwrap(); // todo(dsherret): use not_found error on not found here - let module_dir = self.npm_resolver.resolve_package_folder_from_package( - package_specifier.as_str(), - referrer, - mode, - )?; + let module_dir = self + .npm_resolver + .resolve_package_folder_from_package(package_specifier.as_str(), referrer)?; let package_json_path = module_dir.join("package.json"); - let package_json = PackageJson::load( - &*self.fs, - &*self.npm_resolver, - permissions, - package_json_path.clone(), - )?; - if package_json.exists { + let maybe_package_json = load_pkg_json(&*self.fs, &package_json_path)?; + if let Some(package_json) = maybe_package_json { if let Some(exports) = &package_json.exports { - return self.node_resolver.package_exports_resolve( - &package_json_path, - &package_subpath, - exports, - referrer, - NodeModuleKind::Esm, - conditions, - mode, - permissions, - ); + return self + .node_resolver + .package_exports_resolve( + &package_json_path, + &package_subpath, + exports, + Some(referrer), + NodeModuleKind::Esm, + conditions, + mode, + ) + .map_err(AnyError::from); } // old school @@ -266,13 +331,8 @@ impl NodeCodeTranslator { if self.fs.is_dir_sync(&d) { // subdir might have a package.json that specifies the entrypoint let package_json_path = d.join("package.json"); - let package_json = PackageJson::load( - &*self.fs, - &*self.npm_resolver, - permissions, - package_json_path, - )?; - if package_json.exists { + let maybe_package_json = load_pkg_json(&*self.fs, &package_json_path)?; + if let Some(package_json) = maybe_package_json { if let Some(main) = package_json.main(NodeModuleKind::Cjs) { return Ok(to_file_specifier(&d.join(main).clean())); } diff --git a/crates/node/clippy.toml b/crates/node/clippy.toml index 31d9d7d47..02fd259d0 100644 --- a/crates/node/clippy.toml +++ b/crates/node/clippy.toml @@ -38,3 +38,6 @@ disallowed-methods = [ { path = "std::fs::symlink_metadata", reason = "File system operations should be done using FileSystem trait" }, { path = "std::fs::write", reason = "File system operations should be done using FileSystem trait" }, ] +disallowed-types = [ + { path = "std::sync::Arc", reason = "use deno_fs::sync::MaybeArc instead" }, +] diff --git a/crates/node/errors.rs b/crates/node/errors.rs index 352b5e3c8..3d311c9ff 100644 --- a/crates/node/errors.rs +++ b/crates/node/errors.rs @@ -1,165 +1,460 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::borrow::Cow; use std::path::PathBuf; use deno_core::error::generic_error; -use deno_core::error::type_error; use deno_core::error::AnyError; -use deno_core::url::Url; +use deno_core::ModuleSpecifier; +use thiserror::Error; +use crate::NodeModuleKind; use crate::NodeResolutionMode; -pub fn err_invalid_module_specifier( - request: &str, - reason: &str, - maybe_base: Option, -) -> AnyError { - let mut msg = format!("[ERR_INVALID_MODULE_SPECIFIER] Invalid module \"{request}\" {reason}"); +macro_rules! kinded_err { + ($name:ident, $kind_name:ident) => { + #[derive(Error, Debug)] + #[error(transparent)] + pub struct $name(pub Box<$kind_name>); - if let Some(base) = maybe_base { - msg = format!("{msg} imported from {base}"); - } + impl $name { + pub fn as_kind(&self) -> &$kind_name { + &self.0 + } + + pub fn into_kind(self) -> $kind_name { + *self.0 + } + } - type_error(msg) + impl From for $name + where + $kind_name: From, + { + fn from(err: E) -> Self { + $name(Box::new($kind_name::from(err))) + } + } + }; } -#[allow(unused)] -pub fn err_invalid_package_config( - path: &str, - maybe_base: Option, - maybe_message: Option, -) -> AnyError { - let mut msg = format!("[ERR_INVALID_PACKAGE_CONFIG] Invalid package config {path}"); +kinded_err!( + ResolvePkgSubpathFromDenoModuleError, + ResolvePkgSubpathFromDenoModuleErrorKind +); - if let Some(base) = maybe_base { - msg = format!("{msg} while importing {base}"); - } +#[derive(Debug, Error)] +pub enum ResolvePkgSubpathFromDenoModuleErrorKind { + #[error(transparent)] + PackageSubpathResolve(#[from] PackageSubpathResolveError), + #[error(transparent)] + UrlToNodeResolution(#[from] UrlToNodeResolutionError), +} - if let Some(message) = maybe_message { - msg = format!("{msg}. {message}"); - } +// todo(https://github.com/denoland/deno_core/issues/810): make this a TypeError +#[derive(Debug, Clone, Error)] +#[error( + "[ERR_INVALID_MODULE_SPECIFIER] Invalid module '{}' {}{}", + request, + reason, + maybe_referrer.as_ref().map(|referrer| format!(" imported from '{}'", referrer)).unwrap_or_default() +)] +pub struct InvalidModuleSpecifierError { + pub request: String, + pub reason: Cow<'static, str>, + pub maybe_referrer: Option, +} - generic_error(msg) +#[derive(Debug, Error)] +pub enum LegacyMainResolveError { + #[error(transparent)] + PathToDeclarationUrl(PathToDeclarationUrlError), } -pub fn err_module_not_found(path: &str, base: &str, typ: &str) -> AnyError { - generic_error(format!( - "[ERR_MODULE_NOT_FOUND] Cannot find {typ} \"{path}\" imported from \"{base}\"" - )) +kinded_err!(PackageFolderResolveError, PackageFolderResolveErrorKind); + +#[derive(Debug, Error)] +pub enum PackageFolderResolveErrorKind { + #[error( + "Could not find package '{}' from referrer '{}'{}.", + package_name, + referrer, + referrer_extra.as_ref().map(|r| format!(" ({})", r)).unwrap_or_default() + )] + NotFoundPackage { + package_name: String, + referrer: ModuleSpecifier, + /// Extra information about the referrer. + referrer_extra: Option, + }, + #[error( + "Could not find referrer npm package '{}'{}.", + referrer, + referrer_extra.as_ref().map(|r| format!(" ({})", r)).unwrap_or_default() + )] + NotFoundReferrer { + referrer: ModuleSpecifier, + /// Extra information about the referrer. + referrer_extra: Option, + }, + #[error("Failed resolving '{package_name}' from referrer '{referrer}'.")] + Io { + package_name: String, + referrer: ModuleSpecifier, + #[source] + source: std::io::Error, + }, } -pub fn err_invalid_package_target( - pkg_path: &str, - key: &str, - target: &str, - is_import: bool, - maybe_referrer: Option, -) -> AnyError { - let rel_error = !is_import && !target.is_empty() && !target.starts_with("./"); - let mut msg = "[ERR_INVALID_PACKAGE_TARGET]".to_string(); - let pkg_json_path = PathBuf::from(pkg_path).join("package.json"); - - if key == "." { - assert!(!is_import); - msg = format!( - "{} Invalid \"exports\" main target {} defined in the package config {}", - msg, - target, - pkg_json_path.display() - ) - } else { - let ie = if is_import { "imports" } else { "exports" }; - msg = format!( - "{} Invalid \"{}\" target {} defined for '{}' in the package config {}", - msg, - ie, - target, - key, - pkg_json_path.display() - ) - }; +kinded_err!(PackageSubpathResolveError, PackageSubpathResolveErrorKind); - if let Some(base) = maybe_referrer { - msg = format!("{msg} imported from {base}"); - }; - if rel_error { - msg = format!("{msg}; target must start with \"./\""); - } +#[derive(Debug, Error)] +pub enum PackageSubpathResolveErrorKind { + #[error(transparent)] + PkgJsonLoad(#[from] deno_config::package_json::PackageJsonLoadError), + #[error(transparent)] + PackageFolderResolve(#[from] PackageFolderResolveError), + #[error(transparent)] + DirNotFound(AnyError), + #[error(transparent)] + Exports(PackageExportsResolveError), + #[error(transparent)] + LegacyMain(LegacyMainResolveError), + #[error(transparent)] + LegacyExact(PathToDeclarationUrlError), +} - generic_error(msg) +#[derive(Debug, Error)] +#[error( + "Target '{}' not found from '{}'{}{}.", + target, + pkg_json_path.display(), + maybe_referrer.as_ref().map(|r| + format!( + " from{} referrer {}", + match referrer_kind { + NodeModuleKind::Esm => "", + NodeModuleKind::Cjs => " cjs", + }, + r + ) + ).unwrap_or_default(), + match mode { + NodeResolutionMode::Execution => "", + NodeResolutionMode::Types => " for types", + } +)] +pub struct PackageTargetNotFoundError { + pub pkg_json_path: PathBuf, + pub target: String, + pub maybe_referrer: Option, + pub referrer_kind: NodeModuleKind, + pub mode: NodeResolutionMode, } -pub fn err_package_path_not_exported( - mut pkg_path: String, - subpath: &str, - maybe_referrer: Option, - mode: NodeResolutionMode, -) -> AnyError { - let mut msg = "[ERR_PACKAGE_PATH_NOT_EXPORTED]".to_string(); +kinded_err!(PackageTargetResolveError, PackageTargetResolveErrorKind); + +#[derive(Debug, Error)] +pub enum PackageTargetResolveErrorKind { + #[error(transparent)] + NotFound(#[from] PackageTargetNotFoundError), + #[error(transparent)] + InvalidPackageTarget(#[from] InvalidPackageTargetError), + #[error(transparent)] + InvalidModuleSpecifier(#[from] InvalidModuleSpecifierError), + #[error(transparent)] + PackageResolve(#[from] PackageResolveError), + #[error(transparent)] + PathToDeclarationUrl(#[from] PathToDeclarationUrlError), +} + +kinded_err!(PackageExportsResolveError, PackageExportsResolveErrorKind); + +#[derive(Debug, Error)] +pub enum PackageExportsResolveErrorKind { + #[error(transparent)] + PackagePathNotExported(#[from] PackagePathNotExportedError), + #[error(transparent)] + PackageTargetResolve(#[from] PackageTargetResolveError), +} + +#[derive(Debug, Error)] +pub enum PathToDeclarationUrlError { + #[error(transparent)] + SubPath(#[from] PackageSubpathResolveError), +} + +kinded_err!(ClosestPkgJsonError, ClosestPkgJsonErrorKind); + +#[derive(Debug, Error)] +pub enum ClosestPkgJsonErrorKind { + #[error("Failed canonicalizing package.json directory '{dir_path}'.")] + CanonicalizingDir { + dir_path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error(transparent)] + Load(#[from] deno_config::package_json::PackageJsonLoadError), +} + +#[derive(Debug, Error)] +#[error("TypeScript files are not supported in npm packages: {specifier}")] +pub struct TypeScriptNotSupportedInNpmError { + pub specifier: ModuleSpecifier, +} + +kinded_err!(UrlToNodeResolutionError, UrlToNodeResolutionErrorKind); + +#[derive(Debug, Error)] +pub enum UrlToNodeResolutionErrorKind { + #[error(transparent)] + TypeScriptNotSupported(#[from] TypeScriptNotSupportedInNpmError), + #[error(transparent)] + ClosestPkgJson(#[from] ClosestPkgJsonError), +} + +// todo(https://github.com/denoland/deno_core/issues/810): make this a TypeError +#[derive(Debug, Error)] +#[error( + "[ERR_PACKAGE_IMPORT_NOT_DEFINED] Package import specifier \"{}\" is not defined{}{}", + name, + package_json_path.as_ref().map(|p| format!(" in package {}", p.display())).unwrap_or_default(), + maybe_referrer.as_ref().map(|r| format!(" imported from '{}'", r)).unwrap_or_default(), +)] +pub struct PackageImportNotDefinedError { + pub name: String, + pub package_json_path: Option, + pub maybe_referrer: Option, +} + +kinded_err!(PackageImportsResolveError, PackageImportsResolveErrorKind); + +#[derive(Debug, Error)] +pub enum PackageImportsResolveErrorKind { + #[error(transparent)] + ClosestPkgJson(ClosestPkgJsonError), + #[error(transparent)] + InvalidModuleSpecifier(#[from] InvalidModuleSpecifierError), + #[error(transparent)] + NotDefined(#[from] PackageImportNotDefinedError), + #[error(transparent)] + Target(#[from] PackageTargetResolveError), +} + +kinded_err!(PackageResolveError, PackageResolveErrorKind); + +#[derive(Debug, Error)] +pub enum PackageResolveErrorKind { + #[error(transparent)] + ClosestPkgJson(#[from] ClosestPkgJsonError), + #[error(transparent)] + InvalidModuleSpecifier(#[from] InvalidModuleSpecifierError), + #[error(transparent)] + ExportsResolve(#[from] PackageExportsResolveError), + #[error(transparent)] + SubpathResolve(#[from] PackageSubpathResolveError), +} + +#[derive(Debug, Error)] +pub enum NodeResolveError { + #[error("Failed joining '{path}' from '{base}'.")] + RelativeJoinError { + path: String, + base: ModuleSpecifier, + #[source] + source: url::ParseError, + }, + #[error(transparent)] + PackageImportsResolve(#[from] PackageImportsResolveError), + #[error(transparent)] + UnsupportedEsmUrlScheme(#[from] UnsupportedEsmUrlSchemeError), + #[error("Failed resolving specifier from data url referrer.")] + DataUrlReferrerFailed { + #[source] + source: url::ParseError, + }, + #[error(transparent)] + PackageResolve(#[from] PackageResolveError), + #[error(transparent)] + PathToDeclarationUrl(#[from] PathToDeclarationUrlError), + #[error(transparent)] + UrlToNodeResolution(#[from] UrlToNodeResolutionError), + #[error(transparent)] + FinalizeResolution(#[from] FinalizeResolutionError), +} + +kinded_err!(FinalizeResolutionError, FinalizeResolutionErrorKind); + +#[derive(Debug, Error)] +pub enum FinalizeResolutionErrorKind { + #[error(transparent)] + InvalidModuleSpecifierError(#[from] InvalidModuleSpecifierError), + #[error(transparent)] + ModuleNotFound(#[from] ModuleNotFoundError), + #[error(transparent)] + UnsupportedDirImport(#[from] UnsupportedDirImportError), +} - #[cfg(windows)] - { - if !pkg_path.ends_with('\\') { - pkg_path.push('\\'); +#[derive(Debug, Error)] +#[error( + "[ERR_MODULE_NOT_FOUND] Cannot find {} '{}'{}", + typ, + specifier, + maybe_referrer.as_ref().map(|referrer| format!(" imported from '{}'", referrer)).unwrap_or_default() +)] +pub struct ModuleNotFoundError { + pub specifier: ModuleSpecifier, + pub maybe_referrer: Option, + pub typ: &'static str, +} + +#[derive(Debug, Error)] +#[error( + "[ERR_UNSUPPORTED_DIR_IMPORT] Directory import '{}' is not supported resolving ES modules{}", + dir_url, + maybe_referrer.as_ref().map(|referrer| format!(" imported from '{}'", referrer)).unwrap_or_default(), +)] +pub struct UnsupportedDirImportError { + pub dir_url: ModuleSpecifier, + pub maybe_referrer: Option, +} + +#[derive(Debug)] +pub struct InvalidPackageTargetError { + pub pkg_json_path: PathBuf, + pub sub_path: String, + pub target: String, + pub is_import: bool, + pub maybe_referrer: Option, +} + +impl std::error::Error for InvalidPackageTargetError {} + +impl std::fmt::Display for InvalidPackageTargetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let rel_error = + !self.is_import && !self.target.is_empty() && !self.target.starts_with("./"); + f.write_str("[ERR_INVALID_PACKAGE_TARGET]")?; + + if self.sub_path == "." { + assert!(!self.is_import); + write!( + f, + " Invalid \"exports\" main target {} defined in the package config {}", + self.target, + self.pkg_json_path.display() + )?; + } else { + let ie = if self.is_import { "imports" } else { "exports" }; + write!( + f, + " Invalid \"{}\" target {} defined for '{}' in the package config {}", + ie, + self.target, + self.sub_path, + self.pkg_json_path.display() + )?; + }; + + if let Some(referrer) = &self.maybe_referrer { + write!(f, " imported from '{}'", referrer)?; } - } - #[cfg(not(windows))] - { - if !pkg_path.ends_with('/') { - pkg_path.push('/'); + if rel_error { + write!(f, "; target must start with \"./\"")?; } + Ok(()) } +} - let types_msg = match mode { - NodeResolutionMode::Execution => String::new(), - NodeResolutionMode::Types => " for types".to_string(), - }; - if subpath == "." { - msg = format!("{msg} No \"exports\" main defined{types_msg} in '{pkg_path}package.json'"); - } else { - msg = format!("{msg} Package subpath '{subpath}' is not defined{types_msg} by \"exports\" in '{pkg_path}package.json'"); - }; +#[derive(Debug)] +pub struct PackagePathNotExportedError { + pub pkg_json_path: PathBuf, + pub subpath: String, + pub maybe_referrer: Option, + pub mode: NodeResolutionMode, +} - if let Some(referrer) = maybe_referrer { - msg = format!("{msg} imported from '{referrer}'"); - } +impl std::error::Error for PackagePathNotExportedError {} - generic_error(msg) -} +impl std::fmt::Display for PackagePathNotExportedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("[ERR_PACKAGE_PATH_NOT_EXPORTED]")?; -pub fn err_package_import_not_defined( - specifier: &str, - package_path: Option, - base: &str, -) -> AnyError { - let mut msg = format!( - "[ERR_PACKAGE_IMPORT_NOT_DEFINED] Package import specifier \"{specifier}\" is not defined" - ); + let types_msg = match self.mode { + NodeResolutionMode::Execution => String::new(), + NodeResolutionMode::Types => " for types".to_string(), + }; + if self.subpath == "." { + write!( + f, + " No \"exports\" main defined{} in '{}'", + types_msg, + self.pkg_json_path.display() + )?; + } else { + write!( + f, + " Package subpath '{}' is not defined{} by \"exports\" in '{}'", + self.subpath, + types_msg, + self.pkg_json_path.display() + )?; + }; - if let Some(package_path) = package_path { - let pkg_json_path = PathBuf::from(package_path).join("package.json"); - msg = format!("{} in package {}", msg, pkg_json_path.display()); + if let Some(referrer) = &self.maybe_referrer { + write!(f, " imported from '{}'", referrer)?; + } + Ok(()) } +} - msg = format!("{msg} imported from {base}"); +#[derive(Debug, Clone, Error)] +#[error( + "[ERR_UNSUPPORTED_ESM_URL_SCHEME] Only file and data URLS are supported by the default ESM loader.{} Received protocol '{}'", + if cfg!(windows) && url_scheme.len() == 2 { " On Windows, absolute path must be valid file:// URLS."} else { "" }, + url_scheme +)] +pub struct UnsupportedEsmUrlSchemeError { + pub url_scheme: String, +} - type_error(msg) +#[derive(Debug, Error)] +pub enum ResolvePkgJsonBinExportError { + #[error(transparent)] + PkgJsonLoad(#[from] deno_config::package_json::PackageJsonLoadError), + #[error("Failed resolving binary export. '{}' did not exist", pkg_json_path.display())] + MissingPkgJson { pkg_json_path: PathBuf }, + #[error("Failed resolving binary export. {message}")] + InvalidBinProperty { message: String }, + #[error(transparent)] + UrlToNodeResolution(#[from] UrlToNodeResolutionError), } -pub fn err_unsupported_dir_import(path: &str, base: &str) -> AnyError { - generic_error(format!( - "[ERR_UNSUPPORTED_DIR_IMPORT] Directory import '{path}' is not supported resolving ES modules imported from {base}" - )) +#[derive(Debug, Error)] +pub enum ResolveBinaryCommandsError { + #[error(transparent)] + PkgJsonLoad(#[from] deno_config::package_json::PackageJsonLoadError), + #[error("'{}' did not have a name", pkg_json_path.display())] + MissingPkgJsonName { pkg_json_path: PathBuf }, } -pub fn err_unsupported_esm_url_scheme(url: &Url) -> AnyError { - let mut msg = "[ERR_UNSUPPORTED_ESM_URL_SCHEME] Only file and data URLS are supported by the default ESM loader".to_string(); +#[allow(unused)] +pub fn err_invalid_package_config( + path: &str, + maybe_base: Option, + maybe_message: Option, +) -> AnyError { + let mut msg = format!("[ERR_INVALID_PACKAGE_CONFIG] Invalid package config {path}"); + + if let Some(base) = maybe_base { + msg = format!("{msg} while importing {base}"); + } - if cfg!(window) && url.scheme().len() == 2 { - msg = format!("{msg}. On Windows, absolute path must be valid file:// URLs"); + if let Some(message) = maybe_message { + msg = format!("{msg}. {message}"); } - msg = format!("{}. Received protocol '{}'", msg, url.scheme()); generic_error(msg) } @@ -171,18 +466,22 @@ mod test { fn types_resolution_package_path_not_exported() { let separator_char = if cfg!(windows) { '\\' } else { '/' }; assert_eq!( - err_package_path_not_exported( - "test_path".to_string(), - "./jsx-runtime", - None, - NodeResolutionMode::Types, - ) - .to_string(), + PackagePathNotExportedError { + pkg_json_path: PathBuf::from("test_path").join("package.json"), + subpath: "./jsx-runtime".to_string(), + maybe_referrer: None, + mode: NodeResolutionMode::Types + }.to_string(), format!("[ERR_PACKAGE_PATH_NOT_EXPORTED] Package subpath './jsx-runtime' is not defined for types by \"exports\" in 'test_path{separator_char}package.json'") ); assert_eq!( - err_package_path_not_exported("test_path".to_string(), ".", None, NodeResolutionMode::Types,).to_string(), - format!("[ERR_PACKAGE_PATH_NOT_EXPORTED] No \"exports\" main defined for types in 'test_path{separator_char}package.json'") - ); + PackagePathNotExportedError { + pkg_json_path: PathBuf::from("test_path").join("package.json"), + subpath: ".".to_string(), + maybe_referrer: None, + mode: NodeResolutionMode::Types + }.to_string(), + format!("[ERR_PACKAGE_PATH_NOT_EXPORTED] No \"exports\" main defined for types in 'test_path{separator_char}package.json'") + ); } } diff --git a/crates/node/global.rs b/crates/node/global.rs index 894d60cc1..3d4549305 100644 --- a/crates/node/global.rs +++ b/crates/node/global.rs @@ -1,13 +1,12 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::mem::MaybeUninit; -use std::rc::Rc; use deno_core::v8; use deno_core::v8::GetPropertyNamesArgs; use deno_core::v8::MapFnTo; -use crate::NodeResolver; +use crate::resolution::NodeResolverRc; // NOTE(bartlomieju): somehow calling `.map_fn_to()` multiple times on a function // returns two different pointers. That shouldn't be the case as `.map_fn_to()` @@ -16,13 +15,13 @@ use crate::NodeResolver; // these mapped functions per-thread. We should revisit it in the future and // ideally remove altogether. thread_local! { - pub static GETTER_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = getter.map_fn_to(); - pub static SETTER_MAP_FN: v8::GenericNamedPropertySetterCallback<'static> = setter.map_fn_to(); - pub static QUERY_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = query.map_fn_to(); - pub static DELETER_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = deleter.map_fn_to(); - pub static ENUMERATOR_MAP_FN: v8::GenericNamedPropertyEnumeratorCallback<'static> = enumerator.map_fn_to(); - pub static DEFINER_MAP_FN: v8::GenericNamedPropertyDefinerCallback<'static> = definer.map_fn_to(); - pub static DESCRIPTOR_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = descriptor.map_fn_to(); + pub static GETTER_MAP_FN: v8::NamedPropertyGetterCallback<'static> = getter.map_fn_to(); + pub static SETTER_MAP_FN: v8::NamedPropertySetterCallback<'static> = setter.map_fn_to(); + pub static QUERY_MAP_FN: v8::NamedPropertyGetterCallback<'static> = query.map_fn_to(); + pub static DELETER_MAP_FN: v8::NamedPropertyGetterCallback<'static> = deleter.map_fn_to(); + pub static ENUMERATOR_MAP_FN: v8::NamedPropertyEnumeratorCallback<'static> = enumerator.map_fn_to(); + pub static DEFINER_MAP_FN: v8::NamedPropertyDefinerCallback<'static> = definer.map_fn_to(); + pub static DESCRIPTOR_MAP_FN: v8::NamedPropertyGetterCallback<'static> = descriptor.map_fn_to(); } /// Convert an ASCII string to a UTF-16 byte encoding of the string. @@ -252,12 +251,12 @@ fn current_mode(scope: &mut v8::HandleScope) -> Mode { }; let op_state = deno_core::JsRuntime::op_state_from(scope); let op_state = op_state.borrow(); - let Some(node_resolver) = op_state.try_borrow::>() else { + let Some(node_resolver) = op_state.try_borrow::() else { return Mode::Deno; }; let mut buffer = [MaybeUninit::uninit(); 2048]; let str = v8_string.to_rust_cow_lossy(scope, &mut buffer); - if node_resolver.in_npm_package_with_cache(str) { + if str.starts_with("node:") || node_resolver.in_npm_package_with_cache(str) { Mode::Node } else { Mode::Deno @@ -269,9 +268,9 @@ pub fn getter<'s>( key: v8::Local<'s, v8::Name>, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, -) { +) -> v8::Intercepted { if !is_managed_key(scope, key) { - return; + return v8::Intercepted::No; }; let this = args.this(); @@ -287,16 +286,21 @@ pub fn getter<'s>( let reflect_get = v8::Local::new(scope, reflect_get); let inner = v8::Local::new(scope, inner); + if !inner.has_own_property(scope, key).unwrap_or(false) { + return v8::Intercepted::No; + } + let undefined = v8::undefined(scope); let Some(value) = reflect_get.call( scope, undefined.into(), &[inner.into(), key.into(), this.into()], ) else { - return; + return v8::Intercepted::No; }; rv.set(value); + v8::Intercepted::Yes } pub fn setter<'s>( @@ -305,9 +309,9 @@ pub fn setter<'s>( value: v8::Local<'s, v8::Value>, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, -) { +) -> v8::Intercepted { if !is_managed_key(scope, key) { - return; + return v8::Intercepted::No; }; let this = args.this(); @@ -330,10 +334,11 @@ pub fn setter<'s>( undefined.into(), &[inner.into(), key.into(), value, this.into()], ) else { - return; + return v8::Intercepted::No; }; rv.set(success); + v8::Intercepted::Yes } pub fn query<'s>( @@ -341,9 +346,9 @@ pub fn query<'s>( key: v8::Local<'s, v8::Name>, _args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, -) { +) -> v8::Intercepted { if !is_managed_key(scope, key) { - return; + return v8::Intercepted::No; }; let mode = current_mode(scope); @@ -355,14 +360,15 @@ pub fn query<'s>( let inner = v8::Local::new(scope, inner); let Some(true) = inner.has_own_property(scope, key) else { - return; + return v8::Intercepted::No; }; let Some(attributes) = inner.get_property_attributes(scope, key.into()) else { - return; + return v8::Intercepted::No; }; rv.set_uint32(attributes.as_u32()); + v8::Intercepted::Yes } pub fn deleter<'s>( @@ -370,9 +376,9 @@ pub fn deleter<'s>( key: v8::Local<'s, v8::Name>, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, -) { +) -> v8::Intercepted { if !is_managed_key(scope, key) { - return; + return v8::Intercepted::No; }; let mode = current_mode(scope); @@ -385,17 +391,18 @@ pub fn deleter<'s>( let inner = v8::Local::new(scope, inner); let Some(success) = inner.delete(scope, key.into()) else { - return; + return v8::Intercepted::No; }; if args.should_throw_on_error() && !success { let message = v8::String::new(scope, "Cannot delete property").unwrap(); let exception = v8::Exception::type_error(scope, message); scope.throw_exception(exception); - return; + return v8::Intercepted::Yes; } rv.set_bool(success); + v8::Intercepted::Yes } pub fn enumerator<'s>( @@ -431,10 +438,10 @@ pub fn definer<'s>( key: v8::Local<'s, v8::Name>, descriptor: &v8::PropertyDescriptor, args: v8::PropertyCallbackArguments<'s>, - mut rv: v8::ReturnValue, -) { + _rv: v8::ReturnValue, +) -> v8::Intercepted { if !is_managed_key(scope, key) { - return; + return v8::Intercepted::No; }; let mode = current_mode(scope); @@ -447,17 +454,16 @@ pub fn definer<'s>( let inner = v8::Local::new(scope, inner); let Some(success) = inner.define_property(scope, key, descriptor) else { - return; + return v8::Intercepted::No; }; if args.should_throw_on_error() && !success { let message = v8::String::new(scope, "Cannot define property").unwrap(); let exception = v8::Exception::type_error(scope, message); scope.throw_exception(exception); - return; } - rv.set_bool(success); + v8::Intercepted::Yes } pub fn descriptor<'s>( @@ -465,9 +471,9 @@ pub fn descriptor<'s>( key: v8::Local<'s, v8::Name>, _args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, -) { +) -> v8::Intercepted { if !is_managed_key(scope, key) { - return; + return v8::Intercepted::No; }; let mode = current_mode(scope); @@ -483,12 +489,13 @@ pub fn descriptor<'s>( let Some(descriptor) = inner.get_own_property_descriptor(scope, key) else { scope.rethrow().expect("to have caught an exception"); - return; + return v8::Intercepted::Yes; }; if descriptor.is_undefined() { - return; + return v8::Intercepted::No; } rv.set(descriptor); + v8::Intercepted::Yes } diff --git a/crates/node/lib.rs b/crates/node/lib.rs index a13923b8e..a13591ccd 100644 --- a/crates/node/lib.rs +++ b/crates/node/lib.rs @@ -6,8 +6,6 @@ use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; use deno_core::error::AnyError; use deno_core::located_script_name; @@ -34,10 +32,11 @@ mod path; mod polyfill; mod resolution; +pub use deno_config::package_json::PackageJson; pub use ops::ipc::ChildPipeFd; pub use ops::ipc::IpcJsonStreamResource; -pub use ops::v8::VM_CONTEXT_INDEX; -pub use package_json::PackageJson; +pub use package_json::load_pkg_json; +pub use package_json::PackageJsonThreadLocalCache; pub use path::PathClean; pub use polyfill::is_builtin_node_module; pub use polyfill::SUPPORTED_BUILTIN_NODE_MODULES; @@ -47,6 +46,7 @@ pub use resolution::NodeModuleKind; pub use resolution::NodeResolution; pub use resolution::NodeResolutionMode; pub use resolution::NodeResolver; +use resolution::NodeResolverRc; use crate::global::global_object_middleware; use crate::global::global_template_middleware; @@ -54,44 +54,76 @@ use crate::global::global_template_middleware; pub trait NodePermissions { fn check_net_url(&mut self, url: &Url, api_name: &str) -> Result<(), AnyError>; #[inline(always)] - fn check_read(&self, path: &Path) -> Result<(), AnyError> { + fn check_read(&mut self, path: &Path) -> Result<(), AnyError> { self.check_read_with_api_name(path, None) } - fn check_read_with_api_name(&self, path: &Path, api_name: Option<&str>) - -> Result<(), AnyError>; - fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError>; + fn check_read_with_api_name( + &mut self, + path: &Path, + api_name: Option<&str>, + ) -> Result<(), AnyError>; + fn check_sys(&mut self, kind: &str, api_name: &str) -> Result<(), AnyError>; fn check_write_with_api_name( - &self, + &mut self, path: &Path, api_name: Option<&str>, ) -> Result<(), AnyError>; } -pub(crate) struct AllowAllNodePermissions; +pub struct AllowAllNodePermissions; impl NodePermissions for AllowAllNodePermissions { fn check_net_url(&mut self, _url: &Url, _api_name: &str) -> Result<(), AnyError> { Ok(()) } fn check_read_with_api_name( - &self, + &mut self, _path: &Path, _api_name: Option<&str>, ) -> Result<(), AnyError> { Ok(()) } fn check_write_with_api_name( - &self, + &mut self, _path: &Path, _api_name: Option<&str>, ) -> Result<(), AnyError> { Ok(()) } - fn check_sys(&self, _kind: &str, _api_name: &str) -> Result<(), AnyError> { + fn check_sys(&mut self, _kind: &str, _api_name: &str) -> Result<(), AnyError> { Ok(()) } } +impl NodePermissions for deno_permissions::PermissionsContainer { + #[inline(always)] + fn check_net_url(&mut self, url: &Url, api_name: &str) -> Result<(), AnyError> { + deno_permissions::PermissionsContainer::check_net_url(self, url, api_name) + } + + #[inline(always)] + fn check_read_with_api_name( + &mut self, + path: &Path, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + deno_permissions::PermissionsContainer::check_read_with_api_name(self, path, api_name) + } + + #[inline(always)] + fn check_write_with_api_name( + &mut self, + path: &Path, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + deno_permissions::PermissionsContainer::check_write_with_api_name(self, path, api_name) + } + + fn check_sys(&mut self, kind: &str, api_name: &str) -> Result<(), AnyError> { + deno_permissions::PermissionsContainer::check_sys(self, kind, api_name) + } +} + #[allow(clippy::disallowed_types)] pub type NpmResolverRc = deno_fs::sync::MaybeArc; @@ -111,8 +143,7 @@ pub trait NpmResolver: std::fmt::Debug + MaybeSend + MaybeSync { &self, specifier: &str, referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result; + ) -> Result; fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool; @@ -134,7 +165,7 @@ pub trait NpmResolver: std::fmt::Debug + MaybeSend + MaybeSync { fn ensure_read_permission( &self, - permissions: &dyn NodePermissions, + permissions: &mut dyn NodePermissions, path: &Path, ) -> Result<(), AnyError>; } @@ -174,6 +205,17 @@ deno_core::extension!(deno_node, deps = [ deno_io, deno_fs ], parameters = [P: NodePermissions], ops = [ + ops::blocklist::op_socket_address_parse, + ops::blocklist::op_socket_address_get_serialization, + + ops::blocklist::op_blocklist_new, + ops::blocklist::op_blocklist_add_address, + ops::blocklist::op_blocklist_add_range, + ops::blocklist::op_blocklist_add_subnet, + ops::blocklist::op_blocklist_check, + + ops::buffer::op_is_ascii, + ops::buffer::op_is_utf8, ops::crypto::op_node_create_decipheriv, ops::crypto::op_node_cipheriv_encrypt, ops::crypto::op_node_cipheriv_final, @@ -260,7 +302,6 @@ deno_core::extension!(deno_node, ops::zlib::op_zlib_close, ops::zlib::op_zlib_close_if_pending, ops::zlib::op_zlib_write, - ops::zlib::op_zlib_write_async, ops::zlib::op_zlib_init, ops::zlib::op_zlib_reset, ops::zlib::brotli::op_brotli_compress, @@ -280,7 +321,6 @@ deno_core::extension!(deno_node, ops::http2::op_http2_client_get_response, ops::http2::op_http2_client_get_response_body_chunk, ops::http2::op_http2_client_send_data, - ops::http2::op_http2_client_end_stream, ops::http2::op_http2_client_reset_stream, ops::http2::op_http2_client_send_trailers, ops::http2::op_http2_client_get_response_trailers, @@ -425,6 +465,7 @@ deno_core::extension!(deno_node, "internal_binding/uv.ts", "internal/assert.mjs", "internal/async_hooks.ts", + "internal/blocklist.mjs", "internal/buffer.mjs", "internal/child_process.ts", "internal/cli_table.ts", @@ -519,7 +560,7 @@ deno_core::extension!(deno_node, "node:constants" = "constants.ts", "node:crypto" = "crypto.ts", "node:dgram" = "dgram.ts", - "node:diagnostics_channel" = "diagnostics_channel.ts", + "node:diagnostics_channel" = "diagnostics_channel.js", "node:dns" = "dns.ts", "node:dns/promises" = "dns/promises.ts", "node:domain" = "domain.ts", @@ -561,18 +602,21 @@ deno_core::extension!(deno_node, "node:zlib" = "zlib.ts", ], options = { + maybe_node_resolver: Option, maybe_npm_resolver: Option, fs: deno_fs::FileSystemRc, }, state = |state, options| { - let fs = options.fs; - state.put(fs.clone()); - if let Some(npm_resolver) = options.maybe_npm_resolver { + // you should provide both of these or neither + debug_assert_eq!(options.maybe_node_resolver.is_some(), options.maybe_npm_resolver.is_some()); + + state.put(options.fs.clone()); + + if let Some(node_resolver) = &options.maybe_node_resolver { + state.put(node_resolver.clone()); + } + if let Some(npm_resolver) = &options.maybe_npm_resolver { state.put(npm_resolver.clone()); - state.put(Rc::new(NodeResolver::new( - fs, - npm_resolver, - ))) } }, global_template_middleware = global_template_middleware, @@ -641,8 +685,3 @@ pub fn load_cjs_module( js_runtime.execute_script(located_script_name!(), source_code)?; Ok(()) } - -#[allow(clippy::disallowed_types)] -pub fn allow_all() -> Arc { - Arc::new(AllowAllNodePermissions) -} diff --git a/crates/node/ops/blocklist.rs b/crates/node/ops/blocklist.rs new file mode 100644 index 000000000..1858270fc --- /dev/null +++ b/crates/node/ops/blocklist.rs @@ -0,0 +1,297 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::cell::RefCell; +use std::collections::HashSet; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::net::SocketAddr; + +use deno_core::anyhow::anyhow; +use deno_core::anyhow::bail; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::OpState; + +use ipnetwork::IpNetwork; +use ipnetwork::Ipv4Network; +use ipnetwork::Ipv6Network; +use serde::Serialize; + +pub struct BlockListResource { + blocklist: RefCell, +} + +impl deno_core::GarbageCollected for BlockListResource {} + +#[derive(Serialize)] +struct SocketAddressSerialization(String, String); + +#[op2(fast)] +pub fn op_socket_address_parse( + state: &mut OpState, + #[string] addr: &str, + #[smi] port: u16, + #[string] family: &str, +) -> Result { + let ip = addr.parse::()?; + let parsed: SocketAddr = SocketAddr::new(ip, port); + let parsed_ip_str = parsed.ip().to_string(); + let family_correct = family.eq_ignore_ascii_case("ipv4") && parsed.is_ipv4() + || family.eq_ignore_ascii_case("ipv6") && parsed.is_ipv6(); + + if family_correct { + let family_is_lowercase = family[..3].chars().all(char::is_lowercase); + if family_is_lowercase && parsed_ip_str == addr { + Ok(true) + } else { + state.put::(SocketAddressSerialization( + parsed_ip_str, + family.to_lowercase(), + )); + Ok(false) + } + } else { + Err(anyhow!("Invalid address")) + } +} + +#[op2] +#[serde] +pub fn op_socket_address_get_serialization( + state: &mut OpState, +) -> Result { + Ok(state.take::()) +} + +#[op2] +#[cppgc] +pub fn op_blocklist_new() -> BlockListResource { + let blocklist = BlockList::new(); + BlockListResource { + blocklist: RefCell::new(blocklist), + } +} + +#[op2(fast)] +pub fn op_blocklist_add_address( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, +) -> Result<(), AnyError> { + wrap.blocklist.borrow_mut().add_address(addr) +} + +#[op2(fast)] +pub fn op_blocklist_add_range( + #[cppgc] wrap: &BlockListResource, + #[string] start: &str, + #[string] end: &str, +) -> Result { + wrap.blocklist.borrow_mut().add_range(start, end) +} + +#[op2(fast)] +pub fn op_blocklist_add_subnet( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, + #[smi] prefix: u8, +) -> Result<(), AnyError> { + wrap.blocklist.borrow_mut().add_subnet(addr, prefix) +} + +#[op2(fast)] +pub fn op_blocklist_check( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, + #[string] r#type: &str, +) -> Result { + wrap.blocklist.borrow().check(addr, r#type) +} + +struct BlockList { + rules: HashSet, +} + +impl BlockList { + pub fn new() -> Self { + BlockList { + rules: HashSet::new(), + } + } + + fn map_addr_add_network(&mut self, addr: IpAddr, prefix: Option) -> Result<(), AnyError> { + match addr { + IpAddr::V4(addr) => { + let ipv4_prefix = prefix.unwrap_or(32); + self.rules + .insert(IpNetwork::V4(Ipv4Network::new(addr, ipv4_prefix)?)); + + let ipv6_mapped = addr.to_ipv6_mapped(); + let ipv6_prefix = 96 + ipv4_prefix; // IPv4-mapped IPv6 address prefix starts at 96 + self.rules + .insert(IpNetwork::V6(Ipv6Network::new(ipv6_mapped, ipv6_prefix)?)); + } + IpAddr::V6(addr) => { + if let Some(ipv4_mapped) = addr.to_ipv4_mapped() { + let ipv4_prefix = prefix.map(|v| v.clamp(96, 128) - 96).unwrap_or(32); + self.rules + .insert(IpNetwork::V4(Ipv4Network::new(ipv4_mapped, ipv4_prefix)?)); + } + + let ipv6_prefix = prefix.unwrap_or(128); + self.rules + .insert(IpNetwork::V6(Ipv6Network::new(addr, ipv6_prefix)?)); + } + }; + Ok(()) + } + + pub fn add_address(&mut self, address: &str) -> Result<(), AnyError> { + let ip: IpAddr = address.parse()?; + self.map_addr_add_network(ip, None)?; + Ok(()) + } + + pub fn add_range(&mut self, start: &str, end: &str) -> Result { + let start_ip: IpAddr = start.parse()?; + let end_ip: IpAddr = end.parse()?; + + match (start_ip, end_ip) { + (IpAddr::V4(start), IpAddr::V4(end)) => { + let start_u32: u32 = start.into(); + let end_u32: u32 = end.into(); + if end_u32 < start_u32 { + // Indicates invalid range. + return Ok(false); + } + for ip in start_u32..=end_u32 { + let addr: Ipv4Addr = ip.into(); + self.map_addr_add_network(IpAddr::V4(addr), None)?; + } + } + (IpAddr::V6(start), IpAddr::V6(end)) => { + let start_u128: u128 = start.into(); + let end_u128: u128 = end.into(); + if end_u128 < start_u128 { + // Indicates invalid range. + return Ok(false); + } + for ip in start_u128..=end_u128 { + let addr: Ipv6Addr = ip.into(); + self.map_addr_add_network(IpAddr::V6(addr), None)?; + } + } + _ => bail!("IP version mismatch between start and end addresses"), + } + Ok(true) + } + + pub fn add_subnet(&mut self, addr: &str, prefix: u8) -> Result<(), AnyError> { + let ip: IpAddr = addr.parse()?; + self.map_addr_add_network(ip, Some(prefix))?; + Ok(()) + } + + pub fn check(&self, addr: &str, r#type: &str) -> Result { + let addr: IpAddr = addr.parse()?; + let family = r#type.to_lowercase(); + if family == "ipv4" && addr.is_ipv4() || family == "ipv6" && addr.is_ipv6() { + Ok(self.rules.iter().any(|net| net.contains(addr))) + } else { + Err(anyhow!("Invalid address")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_address() { + // Single IPv4 address + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:1", "ipv6").unwrap()); + + // Single IPv6 address + let mut block_list = BlockList::new(); + block_list.add_address("2001:db8::1").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + } + + #[test] + fn test_add_range() { + // IPv4 range + let mut block_list = BlockList::new(); + block_list.add_range("192.168.0.1", "192.168.0.3").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.2", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.3", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:1", "ipv6").unwrap()); + + // IPv6 range + let mut block_list = BlockList::new(); + block_list.add_range("2001:db8::1", "2001:db8::3").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::2", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::3", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + } + + #[test] + fn test_add_subnet() { + // IPv4 subnet + let mut block_list = BlockList::new(); + block_list.add_subnet("192.168.0.0", 24).unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.255", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:0", "ipv6").unwrap()); + + // IPv6 subnet + let mut block_list = BlockList::new(); + block_list.add_subnet("2001:db8::", 64).unwrap(); + block_list.add_subnet("::ffff:127.0.0.1", 128).unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::ffff", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + + // Check host addresses of IPv4 mapped IPv6 address + let mut block_list = BlockList::new(); + block_list.add_subnet("1.1.1.0", 30).unwrap(); + assert!(block_list.check("::ffff:1.1.1.1", "ipv6").unwrap()); + assert!(!block_list.check("::ffff:1.1.1.4", "ipv6").unwrap()); + } + + #[test] + fn test_check() { + // Check IPv4 presence + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + + // Check IPv6 presence + let mut block_list = BlockList::new(); + block_list.add_address("2001:db8::1").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + + // Check IPv4 not present + let block_list = BlockList::new(); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + + // Check IPv6 not present + let block_list = BlockList::new(); + assert!(!block_list.check("2001:db8::1", "ipv6").unwrap()); + + // Check invalid IP version + let block_list = BlockList::new(); + assert!(block_list.check("192.168.0.1", "ipv6").is_err()); + + // Check invalid type + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "invalid_type").is_err()); + } +} diff --git a/crates/node/ops/buffer.rs b/crates/node/ops/buffer.rs new file mode 100644 index 000000000..8135196df --- /dev/null +++ b/crates/node/ops/buffer.rs @@ -0,0 +1,13 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::op2; + +#[op2(fast)] +pub fn op_is_ascii(#[buffer] buf: &[u8]) -> bool { + buf.is_ascii() +} + +#[op2(fast)] +pub fn op_is_utf8(#[buffer] buf: &[u8]) -> bool { + std::str::from_utf8(buf).is_ok() +} diff --git a/crates/node/ops/crypto/digest.rs b/crates/node/ops/crypto/digest.rs index 6a77dfcc2..4c5adcb77 100644 --- a/crates/node/ops/crypto/digest.rs +++ b/crates/node/ops/crypto/digest.rs @@ -1,130 +1,287 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; +use deno_core::error::generic_error; use deno_core::error::AnyError; -use deno_core::Resource; +use deno_core::GarbageCollected; use digest::Digest; use digest::DynDigest; -use std::borrow::Cow; +use digest::ExtendableOutput; +use digest::Update; use std::cell::RefCell; use std::rc::Rc; -pub enum Hash { - Md4(Box), - Md5(Box), - Ripemd160(Box), - Sha1(Box), - Sha224(Box), - Sha256(Box), - Sha384(Box), - Sha512(Box), +pub struct Hasher { + pub hash: Rc>>, } -pub struct Context { - pub hash: Rc>, -} +impl GarbageCollected for Hasher {} + +impl Hasher { + pub fn new(algorithm: &str, output_length: Option) -> Result { + let hash = Hash::new(algorithm, output_length)?; -impl Context { - pub fn new(algorithm: &str) -> Result { Ok(Self { - hash: Rc::new(RefCell::new(Hash::new(algorithm)?)), + hash: Rc::new(RefCell::new(Some(hash))), }) } - pub fn update(&self, data: &[u8]) { - self.hash.borrow_mut().update(data); + pub fn update(&self, data: &[u8]) -> bool { + if let Some(hash) = self.hash.borrow_mut().as_mut() { + hash.update(data); + true + } else { + false + } } - pub fn digest(self) -> Result, AnyError> { - let hash = - Rc::try_unwrap(self.hash).map_err(|_| type_error("Hash context is already in use"))?; + pub fn digest(&self) -> Option> { + let hash = self.hash.borrow_mut().take()?; + Some(hash.digest_and_drop()) + } - let hash = hash.into_inner(); - Ok(hash.digest_and_drop()) + pub fn clone_inner(&self, output_length: Option) -> Result, AnyError> { + let hash = self.hash.borrow(); + let Some(hash) = hash.as_ref() else { + return Ok(None); + }; + let hash = hash.clone_hash(output_length)?; + Ok(Some(Self { + hash: Rc::new(RefCell::new(Some(hash))), + })) } } -impl Clone for Context { - fn clone(&self) -> Self { - Self { - hash: Rc::new(RefCell::new(self.hash.borrow().clone())), - } +macro_rules! match_fixed_digest { + ($algorithm_name:expr, fn <$type:ident>() $body:block, _ => $other:block) => { + match $algorithm_name { + "blake2b512" => { + type $type = ::blake2::Blake2b512; + $body + } + "blake2s256" => { + type $type = ::blake2::Blake2s256; + $body + } + _ => match_fixed_digest_with_eager_block_buffer!($algorithm_name, fn <$type>() $body, _ => $other) } + }; } +pub(crate) use match_fixed_digest; -impl Resource for Context { - fn name(&self) -> Cow { - "cryptoDigest".into() +macro_rules! match_fixed_digest_with_eager_block_buffer { + ($algorithm_name:expr, fn <$type:ident>() $body:block, _ => $other:block) => { + match $algorithm_name { + "rsa-sm3" | "sm3" | "sm3withrsaencryption" => { + type $type = ::sm3::Sm3; + $body + } + "md5-sha1" => { + type $type = crate::ops::crypto::md5_sha1::Md5Sha1; + $body + } + _ => match_fixed_digest_with_oid!($algorithm_name, fn <$type>() $body, _ => $other) } + }; +} +pub(crate) use match_fixed_digest_with_eager_block_buffer; + +macro_rules! match_fixed_digest_with_oid { + ($algorithm_name:expr, fn <$type:ident>() $body:block, _ => $other:block) => { + match $algorithm_name { + "rsa-md5" | "md5" | "md5withrsaencryption" | "ssl3-md5" => { + type $type = ::md5::Md5; + $body + } + "rsa-ripemd160" | "ripemd" | "ripemd160" | "ripemd160withrsa" | "rmd160" => { + type $type = ::ripemd::Ripemd160; + $body + } + "rsa-sha1" + | "rsa-sha1-2" + | "sha1" + | "sha1-2" + | "sha1withrsaencryption" + | "ssl3-sha1" => { + type $type = ::sha1::Sha1; + $body + } + "rsa-sha224" | "sha224" | "sha224withrsaencryption" => { + type $type = ::sha2::Sha224; + $body + } + "rsa-sha256" | "sha256" | "sha256withrsaencryption" => { + type $type = ::sha2::Sha256; + $body + } + "rsa-sha384" | "sha384" | "sha384withrsaencryption" => { + type $type = ::sha2::Sha384; + $body + } + "rsa-sha512" | "sha512" | "sha512withrsaencryption" => { + type $type = ::sha2::Sha512; + $body + } + "rsa-sha512/224" | "sha512-224" | "sha512-224withrsaencryption" => { + type $type = ::sha2::Sha512_224; + $body + } + "rsa-sha512/256" | "sha512-256" | "sha512-256withrsaencryption" => { + type $type = ::sha2::Sha512_256; + $body + } + "rsa-sha3-224" | "id-rsassa-pkcs1-v1_5-with-sha3-224" | "sha3-224" => { + type $type = ::sha3::Sha3_224; + $body + } + "rsa-sha3-256" | "id-rsassa-pkcs1-v1_5-with-sha3-256" | "sha3-256" => { + type $type = ::sha3::Sha3_256; + $body + } + "rsa-sha3-384" | "id-rsassa-pkcs1-v1_5-with-sha3-384" | "sha3-384" => { + type $type = ::sha3::Sha3_384; + $body + } + "rsa-sha3-512" | "id-rsassa-pkcs1-v1_5-with-sha3-512" | "sha3-512" => { + type $type = ::sha3::Sha3_512; + $body + } + _ => $other, + } + }; +} + +pub(crate) use match_fixed_digest_with_oid; + +pub enum Hash { + FixedSize(Box), + + Shake128(Box, /* output_length: */ Option), + Shake256(Box, /* output_length: */ Option), } use Hash::*; impl Hash { - pub fn new(algorithm_name: &str) -> Result { - Ok(match algorithm_name { - "md4" => Md4(Default::default()), - "md5" => Md5(Default::default()), - "ripemd160" => Ripemd160(Default::default()), - "sha1" => Sha1(Default::default()), - "sha224" => Sha224(Default::default()), - "sha256" => Sha256(Default::default()), - "sha384" => Sha384(Default::default()), - "sha512" => Sha512(Default::default()), - _ => return Err(type_error("unsupported algorithm")), - }) + pub fn new(algorithm_name: &str, output_length: Option) -> Result { + match algorithm_name { + "shake128" => return Ok(Shake128(Default::default(), output_length)), + "shake256" => return Ok(Shake256(Default::default(), output_length)), + _ => {} + } + + let algorithm = match_fixed_digest!( + algorithm_name, + fn () { + let digest: D = Digest::new(); + if let Some(length) = output_length { + if length != digest.output_size() { + return Err(generic_error( + "Output length mismatch for non-extendable algorithm", + )); + } + } + FixedSize(Box::new(digest)) + }, + _ => { + return Err(generic_error(format!( + "Digest method not supported: {algorithm_name}" + ))) + } + ); + + Ok(algorithm) } pub fn update(&mut self, data: &[u8]) { match self { - Md4(context) => Digest::update(&mut **context, data), - Md5(context) => Digest::update(&mut **context, data), - Ripemd160(context) => Digest::update(&mut **context, data), - Sha1(context) => Digest::update(&mut **context, data), - Sha224(context) => Digest::update(&mut **context, data), - Sha256(context) => Digest::update(&mut **context, data), - Sha384(context) => Digest::update(&mut **context, data), - Sha512(context) => Digest::update(&mut **context, data), + FixedSize(context) => DynDigest::update(&mut **context, data), + Shake128(context, _) => Update::update(&mut **context, data), + Shake256(context, _) => Update::update(&mut **context, data), }; } pub fn digest_and_drop(self) -> Box<[u8]> { match self { - Md4(context) => context.finalize(), - Md5(context) => context.finalize(), - Ripemd160(context) => context.finalize(), - Sha1(context) => context.finalize(), - Sha224(context) => context.finalize(), - Sha256(context) => context.finalize(), - Sha384(context) => context.finalize(), - Sha512(context) => context.finalize(), + FixedSize(context) => context.finalize(), + + // The default output lengths align with Node.js + Shake128(context, output_length) => context.finalize_boxed(output_length.unwrap_or(16)), + Shake256(context, output_length) => context.finalize_boxed(output_length.unwrap_or(32)), } } + pub fn clone_hash(&self, output_length: Option) -> Result { + let hash = match self { + FixedSize(context) => { + if let Some(length) = output_length { + if length != context.output_size() { + return Err(generic_error( + "Output length mismatch for non-extendable algorithm", + )); + } + } + FixedSize(context.box_clone()) + } + + Shake128(context, _) => Shake128(context.clone(), output_length), + Shake256(context, _) => Shake256(context.clone(), output_length), + }; + Ok(hash) + } + pub fn get_hashes() -> Vec<&'static str> { vec![ - "md4", + "RSA-MD5", + "RSA-RIPEMD160", + "RSA-SHA1", + "RSA-SHA1-2", + "RSA-SHA224", + "RSA-SHA256", + "RSA-SHA3-224", + "RSA-SHA3-256", + "RSA-SHA3-384", + "RSA-SHA3-512", + "RSA-SHA384", + "RSA-SHA512", + "RSA-SHA512/224", + "RSA-SHA512/256", + "RSA-SM3", + "blake2b512", + "blake2s256", + "id-rsassa-pkcs1-v1_5-with-sha3-224", + "id-rsassa-pkcs1-v1_5-with-sha3-256", + "id-rsassa-pkcs1-v1_5-with-sha3-384", + "id-rsassa-pkcs1-v1_5-with-sha3-512", "md5", + "md5-sha1", + "md5WithRSAEncryption", + "ripemd", "ripemd160", + "ripemd160WithRSA", + "rmd160", "sha1", + "sha1WithRSAEncryption", "sha224", + "sha224WithRSAEncryption", "sha256", + "sha256WithRSAEncryption", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", "sha384", + "sha384WithRSAEncryption", "sha512", + "sha512-224", + "sha512-224WithRSAEncryption", + "sha512-256", + "sha512-256WithRSAEncryption", + "sha512WithRSAEncryption", + "shake128", + "shake256", + "sm3", + "sm3WithRSAEncryption", + "ssl3-md5", + "ssl3-sha1", ] } } - -impl Clone for Hash { - fn clone(&self) -> Self { - match self { - Md4(_) => Md4(Default::default()), - Md5(_) => Md5(Default::default()), - Ripemd160(_) => Ripemd160(Default::default()), - Sha1(_) => Sha1(Default::default()), - Sha224(_) => Sha224(Default::default()), - Sha256(_) => Sha256(Default::default()), - Sha384(_) => Sha384(Default::default()), - Sha512(_) => Sha512(Default::default()), - } - } -} diff --git a/crates/node/ops/crypto/md5_sha1.rs b/crates/node/ops/crypto/md5_sha1.rs new file mode 100644 index 000000000..ae31f7902 --- /dev/null +++ b/crates/node/ops/crypto/md5_sha1.rs @@ -0,0 +1,94 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use core::fmt; +use digest::core_api::AlgorithmName; +use digest::core_api::BlockSizeUser; +use digest::core_api::Buffer; +use digest::core_api::BufferKindUser; +use digest::core_api::CoreWrapper; +use digest::core_api::FixedOutputCore; +use digest::core_api::OutputSizeUser; +use digest::core_api::UpdateCore; +use digest::HashMarker; +use digest::Output; +use digest::Reset; + +pub type Md5Sha1 = CoreWrapper; + +pub struct Md5Sha1Core { + md5: md5::Md5Core, + sha1: sha1::Sha1Core, +} + +impl HashMarker for Md5Sha1Core {} + +impl BlockSizeUser for Md5Sha1Core { + type BlockSize = sec1::consts::U64; +} + +impl BufferKindUser for Md5Sha1Core { + type BufferKind = digest::block_buffer::Eager; +} + +impl OutputSizeUser for Md5Sha1Core { + type OutputSize = sec1::consts::U36; +} + +impl UpdateCore for Md5Sha1Core { + #[inline] + fn update_blocks(&mut self, blocks: &[digest::core_api::Block]) { + self.md5.update_blocks(blocks); + self.sha1.update_blocks(blocks); + } +} + +impl FixedOutputCore for Md5Sha1Core { + #[inline] + fn finalize_fixed_core(&mut self, buffer: &mut Buffer, out: &mut Output) { + let mut md5_output = Output::::default(); + self.md5 + .finalize_fixed_core(&mut buffer.clone(), &mut md5_output); + let mut sha1_output = Output::::default(); + self.sha1.finalize_fixed_core(buffer, &mut sha1_output); + out[..16].copy_from_slice(&md5_output); + out[16..].copy_from_slice(&sha1_output); + } +} + +impl Default for Md5Sha1Core { + #[inline] + fn default() -> Self { + Self { + md5: Default::default(), + sha1: Default::default(), + } + } +} + +impl Clone for Md5Sha1Core { + #[inline] + fn clone(&self) -> Self { + Self { + md5: self.md5.clone(), + sha1: self.sha1.clone(), + } + } +} + +impl Reset for Md5Sha1Core { + #[inline] + fn reset(&mut self) { + *self = Default::default(); + } +} + +impl AlgorithmName for Md5Sha1Core { + fn write_alg_name(f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Md5Sha1") + } +} + +impl fmt::Debug for Md5Sha1Core { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Md5Sha1Core { ... }") + } +} diff --git a/crates/node/ops/crypto/mod.rs b/crates/node/ops/crypto/mod.rs index 825dd9da6..0837afef8 100644 --- a/crates/node/ops/crypto/mod.rs +++ b/crates/node/ops/crypto/mod.rs @@ -7,7 +7,6 @@ use deno_core::serde_v8::BigInt as V8BigInt; use deno_core::unsync::spawn_blocking; use deno_core::JsBuffer; use deno_core::OpState; -use deno_core::ResourceId; use deno_core::StringOrBuffer; use deno_core::ToJsBuffer; use elliptic_curve::sec1::ToEncodedPoint; @@ -47,9 +46,13 @@ use spki::EncodePublicKey; mod cipher; mod dh; mod digest; +mod md5_sha1; mod primes; pub mod x509; +use self::digest::match_fixed_digest_with_eager_block_buffer; +use self::digest::match_fixed_digest_with_oid; + #[op2(fast)] pub fn op_node_check_prime(#[bigint] num: i64, #[number] checks: usize) -> bool { primes::is_probably_prime(&BigInt::from(num), checks) @@ -85,15 +88,13 @@ pub fn op_node_check_prime_bytes_async( ) } -#[op2(fast)] -#[smi] -pub fn op_node_create_hash(state: &mut OpState, #[string] algorithm: &str) -> u32 { - state - .resource_table - .add(match digest::Context::new(algorithm) { - Ok(context) => context, - Err(_) => return 0, - }) +#[op2] +#[cppgc] +pub fn op_node_create_hash( + #[string] algorithm: &str, + output_length: Option, +) -> Result { + digest::Hasher::new(algorithm, output_length.map(|l| l as usize)) } #[op2] @@ -103,58 +104,35 @@ pub fn op_node_get_hashes() -> Vec<&'static str> { } #[op2(fast)] -pub fn op_node_hash_update(state: &mut OpState, #[smi] rid: u32, #[buffer] data: &[u8]) -> bool { - let context = match state.resource_table.get::(rid) { - Ok(context) => context, - _ => return false, - }; - context.update(data); - true +pub fn op_node_hash_update(#[cppgc] hasher: &digest::Hasher, #[buffer] data: &[u8]) -> bool { + hasher.update(data) } #[op2(fast)] -pub fn op_node_hash_update_str(state: &mut OpState, #[smi] rid: u32, #[string] data: &str) -> bool { - let context = match state.resource_table.get::(rid) { - Ok(context) => context, - _ => return false, - }; - context.update(data.as_bytes()); - true +pub fn op_node_hash_update_str(#[cppgc] hasher: &digest::Hasher, #[string] data: &str) -> bool { + hasher.update(data.as_bytes()) } #[op2] -#[serde] -pub fn op_node_hash_digest( - state: &mut OpState, - #[smi] rid: ResourceId, -) -> Result { - let context = state.resource_table.take::(rid)?; - let context = - Rc::try_unwrap(context).map_err(|_| type_error("Hash context is already in use"))?; - Ok(context.digest()?.into()) +#[buffer] +pub fn op_node_hash_digest(#[cppgc] hasher: &digest::Hasher) -> Option> { + hasher.digest() } #[op2] #[string] -pub fn op_node_hash_digest_hex( - state: &mut OpState, - #[smi] rid: ResourceId, -) -> Result { - let context = state.resource_table.take::(rid)?; - let context = - Rc::try_unwrap(context).map_err(|_| type_error("Hash context is already in use"))?; - let digest = context.digest()?; - Ok(faster_hex::hex_string(&digest)) +pub fn op_node_hash_digest_hex(#[cppgc] hasher: &digest::Hasher) -> Option { + let digest = hasher.digest()?; + Some(faster_hex::hex_string(&digest)) } -#[op2(fast)] -#[smi] +#[op2] +#[cppgc] pub fn op_node_hash_clone( - state: &mut OpState, - #[smi] rid: ResourceId, -) -> Result { - let context = state.resource_table.get::(rid)?; - Ok(state.resource_table.add(context.as_ref().clone())) + #[cppgc] hasher: &digest::Hasher, + output_length: Option, +) -> Result, AnyError> { + hasher.clone_inner(output_length.map(|l| l as usize)) } #[op2] @@ -380,31 +358,30 @@ pub fn op_node_sign( RSA_ENCRYPTION_OID => { use rsa::pkcs1v15::SigningKey; let key = RsaPrivateKey::from_pkcs1_der(pkey)?; - Ok(match digest_type { - "sha224" => { - let signing_key = SigningKey::::new(key); - signing_key.sign_prehash(digest)?.to_vec() - } - "sha256" => { - let signing_key = SigningKey::::new(key); - signing_key.sign_prehash(digest)?.to_vec() - } - "sha384" => { - let signing_key = SigningKey::::new(key); - signing_key.sign_prehash(digest)?.to_vec() - } - "sha512" => { - let signing_key = SigningKey::::new(key); - signing_key.sign_prehash(digest)?.to_vec() - } - _ => { - return Err(type_error(format!( - "Unknown digest algorithm: {}", - digest_type - ))) - } + + // md5-sha1 is special, because it's not wrapped in an ASN.1 DigestInfo + // (so has no prefix). + // See https://github.com/openssl/openssl/blob/af82623d32962b3eff5b0f0b0dedec5eb730b231/crypto/rsa/rsa_sign.c#L285 + if digest_type == "md5-sha1" { + let signing_key = SigningKey::::new_unprefixed(key); + let signature = signing_key.sign_prehash(digest)?.to_vec(); + return Ok(signature.into()); } - .into()) + + let signature = match_fixed_digest_with_oid!( + digest_type, + fn () { + let signing_key = SigningKey::::new(key); + signing_key.sign_prehash(digest)?.to_vec() + }, + _ => { + return Err(type_error(format!( + "digest not allowed for RSA signature: {}", + digest_type + ))) + } + ); + Ok(signature.into()) } // signature structure encoding is DER by default for DSA and ECDSA. // @@ -448,29 +425,34 @@ pub fn op_node_verify( ))) } }; - Ok(match digest_type { - "sha224" => VerifyingKey::::new(key) - .verify_prehash(digest, &signature.try_into()?) - .is_ok(), - "sha256" => VerifyingKey::::new(key) - .verify_prehash(digest, &signature.try_into()?) - .is_ok(), - "sha384" => VerifyingKey::::new(key) - .verify_prehash(digest, &signature.try_into()?) - .is_ok(), - "sha512" => VerifyingKey::::new(key) + + // md5-sha1 is special, because it's not wrapped in an ASN.1 DigestInfo + // (so has no prefix). + // See https://github.com/openssl/openssl/blob/af82623d32962b3eff5b0f0b0dedec5eb730b231/crypto/rsa/rsa_sign.c#L285 + if digest_type == "md5-sha1" { + let verifying_key = VerifyingKey::::new_unprefixed(key); + let verified = verifying_key .verify_prehash(digest, &signature.try_into()?) - .is_ok(), - _ => { - return Err(type_error(format!( - "Unknown digest algorithm: {}", - digest_type - ))) - } - }) + .is_ok(); + return Ok(verified); + } + + Ok(match_fixed_digest_with_oid!( + digest_type, + fn () { + let verifying_key = VerifyingKey::::new(key); + verifying_key.verify_prehash(digest, &signature.try_into()?).is_ok() + }, + _ => { + return Err(type_error(format!( + "digest not allowed for RSA signature: {}", + digest_type + ))) + } + )) } _ => Err(type_error(format!( - "Verifying with {} keys is not supported yet", + "Verifying with {} keys is not supported", key_type ))), } @@ -480,28 +462,22 @@ fn pbkdf2_sync( password: &[u8], salt: &[u8], iterations: u32, - digest: &str, + algorithm_name: &str, derived_key: &mut [u8], ) -> Result<(), AnyError> { - macro_rules! pbkdf2_hmac { - ($digest:ty) => {{ - pbkdf2::pbkdf2_hmac::<$digest>(password, salt, iterations, derived_key) - }}; - } - - match digest { - "md4" => pbkdf2_hmac!(md4::Md4), - "md5" => pbkdf2_hmac!(md5::Md5), - "ripemd160" => pbkdf2_hmac!(ripemd::Ripemd160), - "sha1" => pbkdf2_hmac!(sha1::Sha1), - "sha224" => pbkdf2_hmac!(sha2::Sha224), - "sha256" => pbkdf2_hmac!(sha2::Sha256), - "sha384" => pbkdf2_hmac!(sha2::Sha384), - "sha512" => pbkdf2_hmac!(sha2::Sha512), - _ => return Err(type_error("Unknown digest")), - } - - Ok(()) + match_fixed_digest_with_eager_block_buffer!( + algorithm_name, + fn () { + pbkdf2::pbkdf2_hmac::(password, salt, iterations, derived_key); + Ok(()) + }, + _ => { + Err(type_error(format!( + "unsupported digest: {}", + algorithm_name + ))) + } + ) } #[op2] @@ -550,50 +526,40 @@ pub async fn op_node_generate_secret_async(#[smi] len: i32) -> ToJsBuffer { } fn hkdf_sync( - hash: &str, + digest_algorithm: &str, ikm: &[u8], salt: &[u8], info: &[u8], okm: &mut [u8], ) -> Result<(), AnyError> { - macro_rules! hkdf { - ($hash:ty) => {{ - let hk = Hkdf::<$hash>::new(Some(salt), ikm); - hk.expand(info, okm) - .map_err(|_| type_error("HKDF-Expand failed"))?; - }}; - } - - match hash { - "md4" => hkdf!(md4::Md4), - "md5" => hkdf!(md5::Md5), - "ripemd160" => hkdf!(ripemd::Ripemd160), - "sha1" => hkdf!(sha1::Sha1), - "sha224" => hkdf!(sha2::Sha224), - "sha256" => hkdf!(sha2::Sha256), - "sha384" => hkdf!(sha2::Sha384), - "sha512" => hkdf!(sha2::Sha512), - _ => return Err(type_error("Unknown digest")), - } - - Ok(()) + match_fixed_digest_with_eager_block_buffer!( + digest_algorithm, + fn () { + let hk = Hkdf::::new(Some(salt), ikm); + hk.expand(info, okm) + .map_err(|_| type_error("HKDF-Expand failed")) + }, + _ => { + Err(type_error(format!("Unsupported digest: {}", digest_algorithm))) + } + ) } #[op2(fast)] pub fn op_node_hkdf( - #[string] hash: &str, + #[string] digest_algorithm: &str, #[buffer] ikm: &[u8], #[buffer] salt: &[u8], #[buffer] info: &[u8], #[buffer] okm: &mut [u8], ) -> Result<(), AnyError> { - hkdf_sync(hash, ikm, salt, info, okm) + hkdf_sync(digest_algorithm, ikm, salt, info, okm) } #[op2(async)] #[serde] pub async fn op_node_hkdf_async( - #[string] hash: String, + #[string] digest_algorithm: String, #[buffer] ikm: JsBuffer, #[buffer] salt: JsBuffer, #[buffer] info: JsBuffer, @@ -601,7 +567,7 @@ pub async fn op_node_hkdf_async( ) -> Result { spawn_blocking(move || { let mut okm = vec![0u8; okm_len]; - hkdf_sync(&hash, &ikm, &salt, &info, &mut okm)?; + hkdf_sync(&digest_algorithm, &ikm, &salt, &info, &mut okm)?; Ok(okm.into()) }) .await? @@ -1354,6 +1320,7 @@ pub const EC_OID: const_oid::ObjectIdentifier = // } pub struct PssPrivateKeyParameters<'a> { pub hash_algorithm: rsa::pkcs8::AlgorithmIdentifierRef<'a>, + #[allow(dead_code)] pub mask_gen_algorithm: rsa::pkcs8::AlgorithmIdentifierRef<'a>, pub salt_length: u32, } @@ -1414,7 +1381,13 @@ fn parse_private_key( ) -> Result { match format { "pem" => { - let (_, doc) = pkcs8::SecretDocument::from_pem(std::str::from_utf8(key).unwrap())?; + let pem = std::str::from_utf8(key).map_err(|err| { + type_error(format!( + "Invalid PEM private key: not valid utf8 starting at byte {}", + err.valid_up_to() + )) + })?; + let (_, doc) = pkcs8::SecretDocument::from_pem(pem)?; Ok(doc) } "der" => { @@ -1514,7 +1487,13 @@ pub fn op_node_create_private_key( fn parse_public_key(key: &[u8], format: &str, type_: &str) -> Result { match format { "pem" => { - let (label, doc) = pkcs8::Document::from_pem(std::str::from_utf8(key).unwrap())?; + let pem = std::str::from_utf8(key).map_err(|err| { + type_error(format!( + "Invalid PEM private key: not valid utf8 starting at byte {}", + err.valid_up_to() + )) + })?; + let (label, doc) = pkcs8::Document::from_pem(pem)?; if label != "PUBLIC KEY" { return Err(type_error("Invalid PEM label")); } diff --git a/crates/node/ops/crypto/x509.rs b/crates/node/ops/crypto/x509.rs index 178df015e..bc47de35f 100644 --- a/crates/node/ops/crypto/x509.rs +++ b/crates/node/ops/crypto/x509.rs @@ -19,6 +19,8 @@ pub(crate) struct Certificate { cert: X509Certificate<'static>, } +impl deno_core::GarbageCollected for Certificate {} + impl Certificate { fn fingerprint(&self) -> Option { self.pem.as_ref().map(|pem| { @@ -63,7 +65,7 @@ pub fn op_node_x509_parse<'s>( _buf: buf.to_vec(), // SAFETY: Extending the lifetime of the certificate. Backing buffer is // owned by the resource. - cert: unsafe { std::mem::transmute(cert) }, + cert: unsafe { std::mem::transmute::, X509Certificate<'_>>(cert) }, pem, }; diff --git a/crates/node/ops/fs.rs b/crates/node/ops/fs.rs index 440cde5d0..ff7819da8 100644 --- a/crates/node/ops/fs.rs +++ b/crates/node/ops/fs.rs @@ -93,4 +93,204 @@ pub struct StatFs { } #[op2(fast)] -pub fn op_node_statfs(_state: Rc>, #[string] _path: String, _bigint: bool) {} +pub fn op_node_statfs(_state: Rc>, #[string] _path: String, _bigint: bool) { + // { + // let mut state = state.borrow_mut(); + // state + // .borrow_mut::

() + // .check_read_with_api_name(Path::new(&path), Some("node:fs.statfs"))?; + // state + // .borrow_mut::

() + // .check_sys("statfs", "node:fs.statfs")?; + // } + // #[cfg(unix)] + // { + // use std::ffi::OsStr; + // use std::os::unix::ffi::OsStrExt; + + // let path = OsStr::new(&path); + // let mut cpath = path.as_bytes().to_vec(); + // cpath.push(0); + // if bigint { + // #[cfg(not(target_os = "macos"))] + // // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. + // let (code, result) = unsafe { + // let mut result: libc::statfs64 = std::mem::zeroed(); + // (libc::statfs64(cpath.as_ptr() as _, &mut result), result) + // }; + // #[cfg(target_os = "macos")] + // // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. + // let (code, result) = unsafe { + // let mut result: libc::statfs = std::mem::zeroed(); + // (libc::statfs(cpath.as_ptr() as _, &mut result), result) + // }; + // if code == -1 { + // return Err(std::io::Error::last_os_error().into()); + // } + // Ok(StatFs { + // typ: result.f_type as _, + // bsize: result.f_bsize as _, + // blocks: result.f_blocks as _, + // bfree: result.f_bfree as _, + // bavail: result.f_bavail as _, + // files: result.f_files as _, + // ffree: result.f_ffree as _, + // }) + // } else { + // // SAFETY: `cpath` is NUL-terminated and result is pointer to valid statfs memory. + // let (code, result) = unsafe { + // let mut result: libc::statfs = std::mem::zeroed(); + // (libc::statfs(cpath.as_ptr() as _, &mut result), result) + // }; + // if code == -1 { + // return Err(std::io::Error::last_os_error().into()); + // } + // Ok(StatFs { + // typ: result.f_type as _, + // bsize: result.f_bsize as _, + // blocks: result.f_blocks as _, + // bfree: result.f_bfree as _, + // bavail: result.f_bavail as _, + // files: result.f_files as _, + // ffree: result.f_ffree as _, + // }) + // } + // } + // #[cfg(windows)] + // { + // use deno_core::anyhow::anyhow; + // use std::ffi::OsStr; + // use std::os::windows::ffi::OsStrExt; + // use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceW; + + // let _ = bigint; + // // Using a vfs here doesn't make sense, it won't align with the windows API + // // call below. + // #[allow(clippy::disallowed_methods)] + // let path = Path::new(&path).canonicalize()?; + // let root = path + // .ancestors() + // .last() + // .ok_or(anyhow!("Path has no root."))?; + // let mut root = OsStr::new(root).encode_wide().collect::>(); + // root.push(0); + // let mut sectors_per_cluster = 0; + // let mut bytes_per_sector = 0; + // let mut available_clusters = 0; + // let mut total_clusters = 0; + // let mut code = 0; + // let mut retries = 0; + // // We retry here because libuv does: https://github.com/libuv/libuv/blob/fa6745b4f26470dae5ee4fcbb1ee082f780277e0/src/win/fs.c#L2705 + // while code == 0 && retries < 2 { + // // SAFETY: Normal GetDiskFreeSpaceW usage. + // code = unsafe { + // GetDiskFreeSpaceW( + // root.as_ptr(), + // &mut sectors_per_cluster, + // &mut bytes_per_sector, + // &mut available_clusters, + // &mut total_clusters, + // ) + // }; + // retries += 1; + // } + // if code == 0 { + // return Err(std::io::Error::last_os_error().into()); + // } + // Ok(StatFs { + // typ: 0, + // bsize: (bytes_per_sector * sectors_per_cluster) as _, + // blocks: total_clusters as _, + // bfree: available_clusters as _, + // bavail: available_clusters as _, + // files: 0, + // ffree: 0, + // }) + // } + // #[cfg(not(any(unix, windows)))] + // { + // let _ = path; + // let _ = bigint; + // Err(anyhow!("Unsupported platform.")) + // } +} + +#[op2(fast)] +pub fn op_node_lutimes_sync( + _state: &mut OpState, + #[string] _path: &str, + #[number] _atime_secs: i64, + #[smi] _atime_nanos: u32, + #[number] _mtime_secs: i64, + #[smi] _mtime_nanos: u32, +) { + // let path = Path::new(path); + + // state + // .borrow_mut::

() + // .check_write_with_api_name(path, Some("node:fs.lutimes"))?; + + // let fs = state.borrow::(); + // fs.lutime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)?; + // Ok(()) +} + +#[op2(async)] +pub async fn op_node_lutimes( + _state: Rc>, + #[string] _path: String, + #[number] _atime_secs: i64, + #[smi] _atime_nanos: u32, + #[number] _mtime_secs: i64, + #[smi] _mtime_nanos: u32, +) { + // let path = PathBuf::from(path); + + // let fs = { + // let mut state = state.borrow_mut(); + // state + // .borrow_mut::

() + // .check_write_with_api_name(&path, Some("node:fs.lutimesSync"))?; + // state.borrow::().clone() + // }; + + // fs.lutime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + // .await?; + + // Ok(()) +} + +#[op2(fast)] +pub fn op_node_lchown_sync( + _state: &mut OpState, + #[string] _path: String, + _uid: Option, + _gid: Option, +) { + // let path = PathBuf::from(path); + // state + // .borrow_mut::

() + // .check_write_with_api_name(&path, Some("node:fs.lchownSync"))?; + // let fs = state.borrow::(); + // fs.lchown_sync(&path, uid, gid)?; + // Ok(()) +} + +#[op2(async)] +pub async fn op_node_lchown( + _state: Rc>, + #[string] _path: String, + _uid: Option, + _gid: Option, +) { + // let path = PathBuf::from(path); + // let fs = { + // let mut state = state.borrow_mut(); + // state + // .borrow_mut::

() + // .check_write_with_api_name(&path, Some("node:fs.lchown"))?; + // state.borrow::().clone() + // }; + // fs.lchown_async(path, uid, gid).await?; + // Ok(()) +} diff --git a/crates/node/ops/http2.rs b/crates/node/ops/http2.rs index 7dcf3679a..c0d49e39e 100644 --- a/crates/node/ops/http2.rs +++ b/crates/node/ops/http2.rs @@ -24,12 +24,13 @@ use deno_core::ResourceId; use deno_net::raw::take_network_stream_resource; use deno_net::raw::NetworkStream; use h2; +use h2::Reason; use h2::RecvStream; -use http_v02; -use http_v02::request::Parts; -use http_v02::HeaderMap; -use http_v02::Response; -use http_v02::StatusCode; +use http; +use http::request::Parts; +use http::HeaderMap; +use http::Response; +use http::StatusCode; use reqwest::header::HeaderName; use reqwest::header::HeaderValue; use url::Url; @@ -301,7 +302,7 @@ pub async fn op_http2_client_request( let url = url.join(&pseudo_path)?; - let mut req = http_v02::Request::builder() + let mut req = http::Request::builder() .uri(url.as_str()) .method(pseudo_method.as_str()); @@ -334,6 +335,7 @@ pub async fn op_http2_client_send_data( state: Rc>, #[smi] stream_rid: ResourceId, #[buffer] data: JsBuffer, + end_of_stream: bool, ) -> Result<(), AnyError> { let resource = state .borrow() @@ -341,24 +343,7 @@ pub async fn op_http2_client_send_data( .get::(stream_rid)?; let mut stream = RcRef::map(&resource, |r| &r.stream).borrow_mut().await; - // TODO(bartlomieju): handle end of stream - stream.send_data(data.to_vec().into(), false)?; - Ok(()) -} - -#[op2(async)] -pub async fn op_http2_client_end_stream( - state: Rc>, - #[smi] stream_rid: ResourceId, -) -> Result<(), AnyError> { - let resource = state - .borrow() - .resource_table - .get::(stream_rid)?; - let mut stream = RcRef::map(&resource, |r| &r.stream).borrow_mut().await; - - // TODO(bartlomieju): handle end of stream - stream.send_data(BufView::empty(), true)?; + stream.send_data(data.to_vec().into(), end_of_stream)?; Ok(()) } @@ -389,7 +374,7 @@ pub async fn op_http2_client_send_trailers( .get::(stream_rid)?; let mut stream = RcRef::map(&resource, |r| &r.stream).borrow_mut().await; - let mut trailers_map = http_v02::HeaderMap::new(); + let mut trailers_map = http::HeaderMap::new(); for (name, value) in trailers { trailers_map.insert( HeaderName::from_bytes(&name).unwrap(), @@ -414,7 +399,7 @@ pub struct Http2ClientResponse { pub async fn op_http2_client_get_response( state: Rc>, #[smi] stream_rid: ResourceId, -) -> Result { +) -> Result<(Http2ClientResponse, bool), AnyError> { let resource = state .borrow() .resource_table @@ -429,6 +414,7 @@ pub async fn op_http2_client_get_response( for (key, val) in parts.headers.iter() { res_headers.push((key.as_str().into(), val.as_bytes().into())); } + let end_stream = body.is_end_stream(); let (trailers_tx, trailers_rx) = tokio::sync::oneshot::channel(); let body_rid = state @@ -439,11 +425,14 @@ pub async fn op_http2_client_get_response( trailers_rx: AsyncRefCell::new(Some(trailers_rx)), trailers_tx: AsyncRefCell::new(Some(trailers_tx)), }); - Ok(Http2ClientResponse { - headers: res_headers, - body_rid, - status_code: status.into(), - }) + Ok(( + Http2ClientResponse { + headers: res_headers, + body_rid, + status_code: status.into(), + }, + end_stream, + )) } enum DataOrTrailers { @@ -481,7 +470,7 @@ fn poll_data_or_trailers( pub async fn op_http2_client_get_response_body_chunk( state: Rc>, #[smi] body_rid: ResourceId, -) -> Result<(Option>, bool), AnyError> { +) -> Result<(Option>, bool, bool), AnyError> { let resource = state .borrow() .resource_table @@ -489,9 +478,19 @@ pub async fn op_http2_client_get_response_body_chunk( let mut body = RcRef::map(&resource, |r| &r.body).borrow_mut().await; loop { - match poll_fn(|cx| poll_data_or_trailers(cx, &mut body)).await? { + let result = poll_fn(|cx| poll_data_or_trailers(cx, &mut body)).await; + if let Err(err) = result { + let reason = err.reason(); + if let Some(reason) = reason { + if reason == Reason::CANCEL { + return Ok((None, false, true)); + } + } + return Err(err.into()); + } + match result.unwrap() { DataOrTrailers::Data(data) => { - return Ok((Some(data.to_vec()), false)); + return Ok((Some(data.to_vec()), false, false)); } DataOrTrailers::Trailers(trailers) => { if let Some(trailers_tx) = RcRef::map(&resource, |r| &r.trailers_tx) @@ -509,7 +508,7 @@ pub async fn op_http2_client_get_response_body_chunk( .borrow_mut() .await .take(); - return Ok((None, true)); + return Ok((None, true, false)); } }; } diff --git a/crates/node/ops/mod.rs b/crates/node/ops/mod.rs index 4e1c23d21..d8a7f506c 100644 --- a/crates/node/ops/mod.rs +++ b/crates/node/ops/mod.rs @@ -1,5 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +pub mod blocklist; +pub mod buffer; pub mod crypto; pub mod fs; pub mod http; diff --git a/crates/node/ops/os/cpus.rs b/crates/node/ops/os/cpus.rs index f08092055..ad49a0fa2 100644 --- a/crates/node/ops/os/cpus.rs +++ b/crates/node/ops/os/cpus.rs @@ -101,7 +101,7 @@ pub fn cpu_info() -> Option> { cpu.times.irq = 0; - cpu.model = model.clone(); + cpu.model.clone_from(&model); cpu.speed = cpu_speed / 1000000; } diff --git a/crates/node/ops/os/mod.rs b/crates/node/ops/os/mod.rs index c7d3c35b5..1b1a3d83b 100644 --- a/crates/node/ops/os/mod.rs +++ b/crates/node/ops/os/mod.rs @@ -47,7 +47,7 @@ where { { let permissions = state.borrow_mut::

(); - permissions.check_sys("userInfo", "node:os.userInfo()")?; + permissions.check_sys("username", "node:os.userInfo()")?; } Ok(deno_whoami::username()) @@ -60,7 +60,7 @@ where { { let permissions = state.borrow_mut::

(); - permissions.check_sys("geteuid", "node:os.geteuid()")?; + permissions.check_sys("uid", "node:os.geteuid()")?; } #[cfg(windows)] @@ -72,6 +72,25 @@ where Ok(euid) } +#[op2(fast)] +pub fn op_getegid

(state: &mut OpState) -> Result +where + P: NodePermissions + 'static, +{ + { + let permissions = state.borrow_mut::

(); + permissions.check_sys("getegid", "node:os.getegid()")?; + } + + #[cfg(windows)] + let egid = 0; + #[cfg(unix)] + // SAFETY: Call to libc getegid. + let egid = unsafe { libc::getegid() }; + + Ok(egid) +} + #[op2] #[serde] pub fn op_cpus

(state: &mut OpState) -> Result, AnyError> @@ -85,3 +104,17 @@ where cpus::cpu_info().ok_or_else(|| type_error("Failed to get cpu info")) } + +#[op2] +#[string] +pub fn op_homedir

(state: &mut OpState) -> Result, AnyError> +where + P: NodePermissions + 'static, +{ + { + let permissions = state.borrow_mut::

(); + permissions.check_sys("homedir", "node:os.homedir()")?; + } + + Ok(home::home_dir().map(|path| path.to_string_lossy().to_string())) +} diff --git a/crates/node/ops/require.rs b/crates/node/ops/require.rs index 797493bf1..2b74f9724 100644 --- a/crates/node/ops/require.rs +++ b/crates/node/ops/require.rs @@ -16,10 +16,10 @@ use std::path::PathBuf; use std::rc::Rc; use crate::resolution; +use crate::resolution::NodeResolverRc; use crate::NodeModuleKind; use crate::NodePermissions; use crate::NodeResolutionMode; -use crate::NodeResolver; use crate::NpmResolverRc; use crate::PackageJson; @@ -27,8 +27,8 @@ fn ensure_read_permission

(state: &mut OpState, file_path: &Path) -> Result<() where P: NodePermissions + 'static, { - let resolver = state.borrow::(); - let permissions = state.borrow::

(); + let resolver = state.borrow::().clone(); + let permissions = state.borrow_mut::

(); resolver.ensure_read_permission(permissions, file_path) } @@ -191,7 +191,6 @@ pub fn op_require_resolve_deno_dir( &request, &ModuleSpecifier::from_file_path(&parent_filename) .unwrap_or_else(|_| panic!("Url::from_file_path: [{:?}]", parent_filename)), - NodeResolutionMode::Execution, ) .ok() .map(|p| p.to_string_lossy().to_string()) @@ -369,13 +368,9 @@ where return Ok(None); } - let node_resolver = state.borrow::>(); - let permissions = state.borrow::

(); + let node_resolver = state.borrow::(); let pkg = node_resolver - .get_closest_package_json( - &Url::from_file_path(parent_path.unwrap()).unwrap(), - permissions, - ) + .get_closest_package_json(&Url::from_file_path(parent_path.unwrap()).unwrap()) .ok() .flatten(); if pkg.is_none() { @@ -407,11 +402,10 @@ where &pkg.path, &expansion, exports, - &referrer, + Some(&referrer), NodeModuleKind::Cjs, resolution::REQUIRE_CONDITIONS, NodeResolutionMode::Execution, - permissions, )?; Ok(Some(if r.scheme() == "file" { url_to_file_path_string(&r)? @@ -435,7 +429,7 @@ where let file_path = PathBuf::from(file_path); ensure_read_permission::

(state, &file_path)?; let fs = state.borrow::(); - Ok(fs.read_text_file_sync(&file_path, None)?) + Ok(fs.read_text_file_lossy_sync(&file_path, None)?) } #[op2] @@ -466,8 +460,7 @@ where { let fs = state.borrow::(); let npm_resolver = state.borrow::(); - let node_resolver = state.borrow::>(); - let permissions = state.borrow::

(); + let node_resolver = state.borrow::(); let pkg_path = if npm_resolver.in_npm_package_at_file_path(&PathBuf::from(&modules_path)) && !uses_local_node_modules_dir @@ -482,29 +475,30 @@ where original } }; - let pkg = node_resolver - .load_package_json(permissions, PathBuf::from(&pkg_path).join("package.json"))?; + let Some(pkg) = + node_resolver.load_package_json(&PathBuf::from(&pkg_path).join("package.json"))? + else { + return Ok(None); + }; + let Some(exports) = &pkg.exports else { + return Ok(None); + }; - if let Some(exports) = &pkg.exports { - let referrer = Url::from_file_path(parent_path).unwrap(); - let r = node_resolver.package_exports_resolve( - &pkg.path, - &format!(".{expansion}"), - exports, - &referrer, - NodeModuleKind::Cjs, - resolution::REQUIRE_CONDITIONS, - NodeResolutionMode::Execution, - permissions, - )?; - Ok(Some(if r.scheme() == "file" { - url_to_file_path_string(&r)? - } else { - r.to_string() - })) + let referrer = Url::from_file_path(parent_path).unwrap(); + let r = node_resolver.package_exports_resolve( + &pkg.path, + &format!(".{expansion}"), + exports, + Some(&referrer), + NodeModuleKind::Cjs, + resolution::REQUIRE_CONDITIONS, + NodeResolutionMode::Execution, + )?; + Ok(Some(if r.scheme() == "file" { + url_to_file_path_string(&r)? } else { - Ok(None) - } + r.to_string() + })) } #[op2] @@ -517,11 +511,11 @@ where P: NodePermissions + 'static, { ensure_read_permission::

(state, PathBuf::from(&filename).parent().unwrap())?; - let node_resolver = state.borrow::>(); - let permissions = state.borrow::

(); + let node_resolver = state.borrow::().clone(); node_resolver - .get_closest_package_json(&Url::from_file_path(filename).unwrap(), permissions) + .get_closest_package_json(&Url::from_file_path(filename).unwrap()) .map(|maybe_pkg| maybe_pkg.map(|pkg| (*pkg).clone())) + .map_err(AnyError::from) } #[op2] @@ -533,13 +527,17 @@ pub fn op_require_read_package_scope

( where P: NodePermissions + 'static, { - let node_resolver = state.borrow::>(); - let permissions = state.borrow::

(); + let node_resolver = state.borrow::().clone(); let package_json_path = PathBuf::from(package_json_path); + if package_json_path.file_name() != Some("package.json".as_ref()) { + // permissions: do not allow reading a non-package.json file + return None; + } node_resolver - .load_package_json(permissions, package_json_path) - .map(|pkg| (*pkg).clone()) + .load_package_json(&package_json_path) .ok() + .flatten() + .map(|pkg| (*pkg).clone()) } #[op2] @@ -554,11 +552,8 @@ where { let referrer_path = PathBuf::from(&referrer_filename); ensure_read_permission::

(state, &referrer_path)?; - let node_resolver = state.borrow::>(); - let permissions = state.borrow::

(); - let Some(pkg) = - node_resolver.get_closest_package_json_from_path(&referrer_path, permissions)? - else { + let node_resolver = state.borrow::(); + let Some(pkg) = node_resolver.get_closest_package_json_from_path(&referrer_path)? else { return Ok(None); }; @@ -566,12 +561,11 @@ where let referrer_url = deno_core::url::Url::from_file_path(&referrer_filename).unwrap(); let url = node_resolver.package_imports_resolve( &request, - &referrer_url, + Some(&referrer_url), NodeModuleKind::Cjs, Some(&pkg), resolution::REQUIRE_CONDITIONS, NodeResolutionMode::Execution, - permissions, )?; Ok(Some(url_to_file_path_string(&url)?)) } else { diff --git a/crates/node/ops/v8.rs b/crates/node/ops/v8.rs index 42a1a8db6..5bc9d0ad0 100644 --- a/crates/node/ops/v8.rs +++ b/crates/node/ops/v8.rs @@ -1,5 +1,4 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use deno_core::v8; @@ -28,48 +27,3 @@ pub fn op_v8_get_heap_statistics(scope: &mut v8::HandleScope, #[buffer] buffer: buffer[12] = stats.used_global_handles_size() as f64; buffer[13] = stats.external_memory() as f64; } - -pub const VM_CONTEXT_INDEX: usize = 0; - -fn make_context<'a>(scope: &mut v8::HandleScope<'a>) -> v8::Local<'a, v8::Context> { - let scope = &mut v8::EscapableHandleScope::new(scope); - let context = v8::Context::from_snapshot(scope, VM_CONTEXT_INDEX).unwrap(); - scope.escape(context) -} - -#[op2] -pub fn op_vm_run_in_new_context<'a>( - scope: &mut v8::HandleScope<'a>, - script: v8::Local, - ctx_val: v8::Local, -) -> Result, AnyError> { - let _ctx_obj = if ctx_val.is_undefined() || ctx_val.is_null() { - v8::Object::new(scope) - } else { - ctx_val.try_into()? - }; - - let ctx = make_context(scope); - - let scope = &mut v8::ContextScope::new(scope, ctx); - - let tc_scope = &mut v8::TryCatch::new(scope); - let script = match v8::Script::compile(tc_scope, script, None) { - Some(s) => s, - None => { - assert!(tc_scope.has_caught()); - tc_scope.rethrow(); - return Ok(v8::undefined(tc_scope).into()); - } - }; - - Ok(match script.run(tc_scope) { - Some(result) => result, - None => { - assert!(tc_scope.has_caught()); - tc_scope.rethrow(); - - v8::undefined(tc_scope).into() - } - }) -} diff --git a/crates/node/ops/zlib/brotli.rs b/crates/node/ops/zlib/brotli.rs index 96c6df987..64ee2898e 100644 --- a/crates/node/ops/zlib/brotli.rs +++ b/crates/node/ops/zlib/brotli.rs @@ -1,9 +1,13 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use brotli::enc::backward_references::BrotliEncoderMode; +use brotli::enc::encode::BrotliEncoderCompress; +use brotli::enc::encode::BrotliEncoderOperation; use brotli::enc::encode::BrotliEncoderParameter; -use brotli::ffi::compressor::*; -use brotli::ffi::decompressor::ffi::interface::BrotliDecoderResult; -use brotli::ffi::decompressor::ffi::BrotliDecoderState; -use brotli::ffi::decompressor::*; +use brotli::enc::encode::BrotliEncoderStateStruct; +use brotli::writer::StandardAlloc; +use brotli::BrotliDecompressStream; +use brotli::BrotliResult; +use brotli::BrotliState; use brotli::Decompressor; use deno_core::error::type_error; use deno_core::error::AnyError; @@ -12,14 +16,20 @@ use deno_core::JsBuffer; use deno_core::OpState; use deno_core::Resource; use deno_core::ToJsBuffer; +use std::cell::RefCell; use std::io::Read; fn encoder_mode(mode: u32) -> Result { - if mode > 6 { - return Err(type_error("Invalid encoder mode")); - } - // SAFETY: mode is a valid discriminant for BrotliEncoderMode - unsafe { Ok(std::mem::transmute::(mode)) } + Ok(match mode { + 0 => BrotliEncoderMode::BROTLI_MODE_GENERIC, + 1 => BrotliEncoderMode::BROTLI_MODE_TEXT, + 2 => BrotliEncoderMode::BROTLI_MODE_FONT, + 3 => BrotliEncoderMode::BROTLI_FORCE_LSB_PRIOR, + 4 => BrotliEncoderMode::BROTLI_FORCE_MSB_PRIOR, + 5 => BrotliEncoderMode::BROTLI_FORCE_UTF8_PRIOR, + 6 => BrotliEncoderMode::BROTLI_FORCE_SIGNED_PRIOR, + _ => return Err(type_error("Invalid encoder mode")), + }) } #[op2(fast)] @@ -31,24 +41,22 @@ pub fn op_brotli_compress( #[smi] lgwin: i32, #[smi] mode: u32, ) -> Result { - let in_buffer = buffer.as_ptr(); - let in_size = buffer.len(); - let out_buffer = out.as_mut_ptr(); + let mode = encoder_mode(mode)?; let mut out_size = out.len(); - // SAFETY: in_size and in_buffer, out_size and out_buffer are valid for this call. - if unsafe { - BrotliEncoderCompress( - quality, - lgwin, - encoder_mode(mode)?, - in_size, - in_buffer, - &mut out_size as *mut usize, - out_buffer, - ) - } != 1 - { + let result = BrotliEncoderCompress( + StandardAlloc::default(), + &mut StandardAlloc::default(), + quality, + lgwin, + mode, + buffer.len(), + buffer, + &mut out_size, + out, + &mut |_, _, _, _| (), + ); + if result != 1 { return Err(type_error("Failed to compress")); } @@ -80,28 +88,25 @@ pub async fn op_brotli_compress_async( #[smi] lgwin: i32, #[smi] mode: u32, ) -> Result { + let mode = encoder_mode(mode)?; tokio::task::spawn_blocking(move || { - let in_buffer = input.as_ptr(); - let in_size = input.len(); - - let mut out = vec![0u8; max_compressed_size(in_size)]; - let out_buffer = out.as_mut_ptr(); + let input = &*input; + let mut out = vec![0u8; max_compressed_size(input.len())]; let mut out_size = out.len(); - // SAFETY: in_size and in_buffer, out_size and out_buffer - // are valid for this call. - if unsafe { - BrotliEncoderCompress( - quality, - lgwin, - encoder_mode(mode)?, - in_size, - in_buffer, - &mut out_size as *mut usize, - out_buffer, - ) - } != 1 - { + let result = BrotliEncoderCompress( + StandardAlloc::default(), + &mut StandardAlloc::default(), + quality, + lgwin, + mode, + input.len(), + input, + &mut out_size, + &mut out, + &mut |_, _, _, _| (), + ); + if result != 1 { return Err(type_error("Failed to compress")); } @@ -112,35 +117,23 @@ pub async fn op_brotli_compress_async( } struct BrotliCompressCtx { - inst: *mut BrotliEncoderState, + inst: RefCell>, } impl Resource for BrotliCompressCtx {} -impl Drop for BrotliCompressCtx { - fn drop(&mut self) { - // SAFETY: `self.inst` is the current brotli encoder instance. - // It is not used after the following call. - unsafe { BrotliEncoderDestroyInstance(self.inst) }; - } -} - #[op2] #[smi] pub fn op_create_brotli_compress(state: &mut OpState, #[serde] params: Vec<(u8, i32)>) -> u32 { - let inst = - // SAFETY: Creates a brotli encoder instance for default allocators. - unsafe { BrotliEncoderCreateInstance(None, None, std::ptr::null_mut()) }; + let mut inst = BrotliEncoderStateStruct::new(StandardAlloc::default()); for (key, value) in params { - // SAFETY: `key` can range from 0-255. - // Any valid u32 can be used for the `value`. - unsafe { - BrotliEncoderSetParameter(inst, encoder_param(key), value as u32); - } + inst.set_parameter(encoder_param(key), value as u32); } - state.resource_table.add(BrotliCompressCtx { inst }) + state.resource_table.add(BrotliCompressCtx { + inst: RefCell::new(inst), + }) } fn encoder_param(param: u8) -> BrotliEncoderParameter { @@ -157,30 +150,25 @@ pub fn op_brotli_compress_stream( #[buffer] output: &mut [u8], ) -> Result { let ctx = state.resource_table.get::(rid)?; - - // SAFETY: TODO(littledivy) - unsafe { - let mut available_in = input.len(); - let mut next_in = input.as_ptr(); - let mut available_out = output.len(); - let mut next_out = output.as_mut_ptr(); - - if BrotliEncoderCompressStream( - ctx.inst, - BrotliEncoderOperation::BROTLI_OPERATION_PROCESS, - &mut available_in, - &mut next_in, - &mut available_out, - &mut next_out, - std::ptr::null_mut(), - ) != 1 - { - return Err(type_error("Failed to compress")); - } - - // On progress, next_out is advanced and available_out is reduced. - Ok(output.len() - available_out) + let mut inst = ctx.inst.borrow_mut(); + let mut output_offset = 0; + + let result = inst.compress_stream( + BrotliEncoderOperation::BROTLI_OPERATION_PROCESS, + &mut input.len(), + input, + &mut 0, + &mut output.len(), + output, + &mut output_offset, + &mut None, + &mut |_, _, _, _| (), + ); + if !result { + return Err(type_error("Failed to compress")); } + + Ok(output_offset) } #[op2(fast)] @@ -191,29 +179,25 @@ pub fn op_brotli_compress_stream_end( #[buffer] output: &mut [u8], ) -> Result { let ctx = state.resource_table.get::(rid)?; - - // SAFETY: TODO(littledivy) - unsafe { - let mut available_out = output.len(); - let mut next_out = output.as_mut_ptr(); - let mut total_out = 0; - - if BrotliEncoderCompressStream( - ctx.inst, - BrotliEncoderOperation::BROTLI_OPERATION_FINISH, - &mut 0, - std::ptr::null_mut(), - &mut available_out, - &mut next_out, - &mut total_out, - ) != 1 - { - return Err(type_error("Failed to compress")); - } - - // On finish, next_out is advanced and available_out is reduced. - Ok(output.len() - available_out) + let mut inst = ctx.inst.borrow_mut(); + let mut output_offset = 0; + + let result = inst.compress_stream( + BrotliEncoderOperation::BROTLI_OPERATION_FINISH, + &mut 0, + &[], + &mut 0, + &mut output.len(), + output, + &mut output_offset, + &mut None, + &mut |_, _, _, _| (), + ); + if !result { + return Err(type_error("Failed to compress")); } + + Ok(output_offset) } fn brotli_decompress(buffer: &[u8]) -> Result { @@ -238,25 +222,22 @@ pub async fn op_brotli_decompress_async( } struct BrotliDecompressCtx { - inst: *mut BrotliDecoderState, + inst: RefCell>, } impl Resource for BrotliDecompressCtx {} -impl Drop for BrotliDecompressCtx { - fn drop(&mut self) { - // SAFETY: TODO(littledivy) - unsafe { CBrotliDecoderDestroyInstance(self.inst) }; - } -} - #[op2(fast)] #[smi] pub fn op_create_brotli_decompress(state: &mut OpState) -> u32 { - let inst = - // SAFETY: TODO(littledivy) - unsafe { CBrotliDecoderCreateInstance(None, None, std::ptr::null_mut()) }; - state.resource_table.add(BrotliDecompressCtx { inst }) + let inst = BrotliState::new( + StandardAlloc::default(), + StandardAlloc::default(), + StandardAlloc::default(), + ); + state.resource_table.add(BrotliDecompressCtx { + inst: RefCell::new(inst), + }) } #[op2(fast)] @@ -268,32 +249,24 @@ pub fn op_brotli_decompress_stream( #[buffer] output: &mut [u8], ) -> Result { let ctx = state.resource_table.get::(rid)?; - - // SAFETY: TODO(littledivy) - unsafe { - let mut available_in = input.len(); - let mut next_in = input.as_ptr(); - let mut available_out = output.len(); - let mut next_out = output.as_mut_ptr(); - - if matches!( - CBrotliDecoderDecompressStream( - ctx.inst, - &mut available_in, - &mut next_in, - &mut available_out, - &mut next_out, - std::ptr::null_mut(), - ), - BrotliDecoderResult::BROTLI_DECODER_RESULT_ERROR - ) { - let ec = CBrotliDecoderGetErrorCode(ctx.inst) as i32; - return Err(type_error(format!("Failed to decompress, error {ec}"))); - } - - // On progress, next_out is advanced and available_out is reduced. - Ok(output.len() - available_out) + let mut inst = ctx.inst.borrow_mut(); + let mut output_offset = 0; + + let result = BrotliDecompressStream( + &mut input.len(), + &mut 0, + input, + &mut output.len(), + &mut output_offset, + output, + &mut 0, + &mut inst, + ); + if matches!(result, BrotliResult::ResultFailure) { + return Err(type_error("Failed to decompress")); } + + Ok(output_offset) } #[op2(fast)] @@ -304,30 +277,22 @@ pub fn op_brotli_decompress_stream_end( #[buffer] output: &mut [u8], ) -> Result { let ctx = state.resource_table.get::(rid)?; - - // SAFETY: TODO(littledivy) - unsafe { - let mut available_out = output.len(); - let mut next_out = output.as_mut_ptr(); - let mut available_in = 0; - let mut next_in = []; - let mut total_out = 0; - - if matches!( - CBrotliDecoderDecompressStream( - ctx.inst, - &mut available_in, - next_in.as_mut_ptr(), - &mut available_out, - &mut next_out, - &mut total_out, - ), - BrotliDecoderResult::BROTLI_DECODER_RESULT_ERROR - ) { - return Err(type_error("Failed to decompress")); - } - - // On finish, next_out is advanced and available_out is reduced. - Ok(output.len() - available_out) + let mut inst = ctx.inst.borrow_mut(); + let mut output_offset = 0; + + let result = BrotliDecompressStream( + &mut 0, + &mut 0, + &[], + &mut output.len(), + &mut output_offset, + output, + &mut 0, + &mut inst, + ); + if matches!(result, BrotliResult::ResultFailure) { + return Err(type_error("Failed to decompress")); } + + Ok(output_offset) } diff --git a/crates/node/ops/zlib/mod.rs b/crates/node/ops/zlib/mod.rs index 2c64fb395..b4d601335 100644 --- a/crates/node/ops/zlib/mod.rs +++ b/crates/node/ops/zlib/mod.rs @@ -1,13 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::bad_resource_id; use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::op2; -use deno_core::OpState; use std::borrow::Cow; use std::cell::RefCell; -use std::future::Future; -use std::rc::Rc; use zlib::*; mod alloc; @@ -29,14 +25,6 @@ fn check(condition: bool, msg: &str) -> Result<(), AnyError> { } } -#[inline] -fn zlib(state: &mut OpState, handle: u32) -> Result, AnyError> { - state - .resource_table - .get::(handle) - .map_err(|_| bad_resource_id()) -} - #[derive(Default)] struct ZlibInner { dictionary: Option>, @@ -151,7 +139,7 @@ impl ZlibInner { self.err = self.strm.inflate(self.flush); // TODO(@littledivy): Use if let chain when it is stable. // https://github.com/rust-lang/rust/issues/53667 - // + // // Data was encoded with dictionary if let (Z_NEED_DICT, Some(dictionary)) = (self.err, &self.dictionary) { self.err = self.strm.inflate_set_dictionary(dictionary); @@ -235,18 +223,20 @@ impl ZlibInner { } struct Zlib { - inner: RefCell, + inner: RefCell>, } +impl deno_core::GarbageCollected for Zlib {} + impl deno_core::Resource for Zlib { fn name(&self) -> Cow { "zlib".into() } } -#[op2(fast)] -#[smi] -pub fn op_zlib_new(state: &mut OpState, #[smi] mode: i32) -> Result { +#[op2] +#[cppgc] +pub fn op_zlib_new(#[smi] mode: i32) -> Result { let mode = Mode::try_from(mode)?; let inner = ZlibInner { @@ -254,15 +244,17 @@ pub fn op_zlib_new(state: &mut OpState, #[smi] mode: i32) -> Result Result<(), AnyError> { - let resource = zlib(state, handle)?; - let mut zlib = resource.inner.borrow_mut(); +pub fn op_zlib_close(#[cppgc] resource: &Zlib) -> Result<(), AnyError> { + let mut resource = resource.inner.borrow_mut(); + let zlib = resource + .as_mut() + .ok_or_else(|| type_error("zlib not initialized"))?; // If there is a pending write, defer the close until the write is done. zlib.close()?; @@ -270,44 +262,11 @@ pub fn op_zlib_close(state: &mut OpState, #[smi] handle: u32) -> Result<(), AnyE Ok(()) } -#[allow(clippy::too_many_arguments)] -#[op2(async)] -#[serde] -pub fn op_zlib_write_async( - state: Rc>, - #[smi] handle: u32, - #[smi] flush: i32, - #[buffer] input: &[u8], - #[smi] in_off: u32, - #[smi] in_len: u32, - #[buffer] out: &mut [u8], - #[smi] out_off: u32, - #[smi] out_len: u32, -) -> Result>, AnyError> { - let mut state_mut = state.borrow_mut(); - let resource = zlib(&mut state_mut, handle)?; - - let mut strm = resource.inner.borrow_mut(); - let flush = Flush::try_from(flush)?; - strm.start_write(input, in_off, in_len, out, out_off, out_len, flush)?; - - let state = state.clone(); - Ok(async move { - let mut state_mut = state.borrow_mut(); - let resource = zlib(&mut state_mut, handle)?; - let mut zlib = resource.inner.borrow_mut(); - - zlib.do_write(flush)?; - Ok((zlib.err, zlib.strm.avail_out, zlib.strm.avail_in)) - }) -} - #[allow(clippy::too_many_arguments)] #[op2(fast)] #[smi] pub fn op_zlib_write( - state: &mut OpState, - #[smi] handle: u32, + #[cppgc] resource: &Zlib, #[smi] flush: i32, #[buffer] input: &[u8], #[smi] in_off: u32, @@ -317,9 +276,11 @@ pub fn op_zlib_write( #[smi] out_len: u32, #[buffer] result: &mut [u32], ) -> Result { - let resource = zlib(state, handle)?; - let mut zlib = resource.inner.borrow_mut(); + let zlib = zlib + .as_mut() + .ok_or_else(|| type_error("zlib not initialized"))?; + let flush = Flush::try_from(flush)?; zlib.start_write(input, in_off, in_len, out, out_off, out_len, flush)?; zlib.do_write(flush)?; @@ -333,16 +294,17 @@ pub fn op_zlib_write( #[op2(fast)] #[smi] pub fn op_zlib_init( - state: &mut OpState, - #[smi] handle: u32, + #[cppgc] resource: &Zlib, #[smi] level: i32, #[smi] window_bits: i32, #[smi] mem_level: i32, #[smi] strategy: i32, #[buffer] dictionary: &[u8], ) -> Result { - let resource = zlib(state, handle)?; let mut zlib = resource.inner.borrow_mut(); + let zlib = zlib + .as_mut() + .ok_or_else(|| type_error("zlib not initialized"))?; check((8..=15).contains(&window_bits), "invalid windowBits")?; check((-1..=9).contains(&level), "invalid level")?; @@ -379,27 +341,31 @@ pub fn op_zlib_init( #[op2(fast)] #[smi] -pub fn op_zlib_reset(state: &mut OpState, #[smi] handle: u32) -> Result { - let resource = zlib(state, handle)?; - +pub fn op_zlib_reset(#[cppgc] resource: &Zlib) -> Result { let mut zlib = resource.inner.borrow_mut(); + let zlib = zlib + .as_mut() + .ok_or_else(|| type_error("zlib not initialized"))?; + zlib.reset_stream()?; Ok(zlib.err) } #[op2(fast)] -pub fn op_zlib_close_if_pending(state: &mut OpState, #[smi] handle: u32) -> Result<(), AnyError> { - let resource = zlib(state, handle)?; +pub fn op_zlib_close_if_pending(#[cppgc] resource: &Zlib) -> Result<(), AnyError> { let pending_close = { let mut zlib = resource.inner.borrow_mut(); + let zlib = zlib + .as_mut() + .ok_or_else(|| type_error("zlib not initialized"))?; + zlib.write_in_progress = false; zlib.pending_close }; if pending_close { - drop(resource); - if let Ok(res) = state.resource_table.take_any(handle) { - res.close(); + if let Some(mut res) = resource.inner.borrow_mut().take() { + let _ = res.close(); } } diff --git a/crates/node/package_json.rs b/crates/node/package_json.rs index 65764dab7..9158a291f 100644 --- a/crates/node/package_json.rs +++ b/crates/node/package_json.rs @@ -1,265 +1,54 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use crate::NodeModuleKind; -use crate::NodePermissions; - -use super::NpmResolver; - -use deno_core::anyhow; -use deno_core::anyhow::bail; -use deno_core::error::AnyError; -use deno_core::serde_json; -use deno_core::serde_json::Map; -use deno_core::serde_json::Value; -use deno_core::ModuleSpecifier; -use indexmap::IndexMap; -use serde::Serialize; +use deno_config::package_json::PackageJson; +use deno_config::package_json::PackageJsonLoadError; +use deno_config::package_json::PackageJsonRc; +use deno_fs::DenoConfigFsAdapter; use std::cell::RefCell; use std::collections::HashMap; use std::io::ErrorKind; +use std::path::Path; use std::path::PathBuf; -use std::rc::Rc; +// use a thread local cache so that workers have their own distinct cache thread_local! { - static CACHE: RefCell>> = RefCell::new(HashMap::new()); + static CACHE: RefCell> = RefCell::new(HashMap::new()); } -#[derive(Clone, Debug, Serialize)] -pub struct PackageJson { - pub exists: bool, - pub exports: Option>, - pub imports: Option>, - pub bin: Option, - main: Option, // use .main(...) - module: Option, // use .main(...) - pub name: Option, - pub version: Option, - pub path: PathBuf, - pub typ: String, - pub types: Option, - pub dependencies: Option>, - pub dev_dependencies: Option>, - pub scripts: Option>, -} - -impl PackageJson { - pub fn empty(path: PathBuf) -> PackageJson { - PackageJson { - exists: false, - exports: None, - imports: None, - bin: None, - main: None, - module: None, - name: None, - version: None, - path, - typ: "none".to_string(), - types: None, - dependencies: None, - dev_dependencies: None, - scripts: None, - } - } - - pub fn load( - fs: &dyn deno_fs::FileSystem, - resolver: &dyn NpmResolver, - permissions: &dyn NodePermissions, - path: PathBuf, - ) -> Result, AnyError> { - resolver.ensure_read_permission(permissions, &path)?; - Self::load_skip_read_permission(fs, path) - } - - pub fn load_skip_read_permission( - fs: &dyn deno_fs::FileSystem, - path: PathBuf, - ) -> Result, AnyError> { - assert!(path.is_absolute()); - - if CACHE.with(|cache| cache.borrow().contains_key(&path)) { - return Ok(CACHE.with(|cache| cache.borrow()[&path].clone())); - } - - let source = match fs.read_text_file_sync(&path, None) { - Ok(source) => source, - Err(err) if err.kind() == ErrorKind::NotFound => { - return Ok(Rc::new(PackageJson::empty(path))); - } - Err(err) => bail!( - "Error loading package.json at {}. {:#}", - path.display(), - AnyError::from(err), - ), - }; - - let package_json = Rc::new(Self::load_from_string(path, source)?); - CACHE.with(|cache| { - cache - .borrow_mut() - .insert(package_json.path.clone(), package_json.clone()); - }); - Ok(package_json) - } - - pub fn load_from_string(path: PathBuf, source: String) -> Result { - if source.trim().is_empty() { - return Ok(PackageJson::empty(path)); - } - - let package_json: Value = serde_json::from_str(&source).map_err(|err| { - anyhow::anyhow!("malformed package.json: {}\n at {}", err, path.display()) - })?; - Self::load_from_value(path, package_json) - } - - pub fn load_from_value( - path: PathBuf, - package_json: serde_json::Value, - ) -> Result { - let imports_val = package_json.get("imports"); - let main_val = package_json.get("main"); - let module_val = package_json.get("module"); - let name_val = package_json.get("name"); - let version_val = package_json.get("version"); - let type_val = package_json.get("type"); - let bin = package_json.get("bin").map(ToOwned::to_owned); - let exports = package_json.get("exports").and_then(|exports| { - Some(if is_conditional_exports_main_sugar(exports) { - let mut map = Map::new(); - map.insert(".".to_string(), exports.to_owned()); - map - } else { - exports.as_object()?.to_owned() - }) - }); - - let imports = imports_val - .and_then(|imp| imp.as_object()) - .map(|imp| imp.to_owned()); - let main = main_val.and_then(|s| s.as_str()).map(|s| s.to_string()); - let name = name_val.and_then(|s| s.as_str()).map(|s| s.to_string()); - let version = version_val.and_then(|s| s.as_str()).map(|s| s.to_string()); - let module = module_val.and_then(|s| s.as_str()).map(|s| s.to_string()); - - let dependencies = package_json.get("dependencies").and_then(|d| { - if d.is_object() { - let deps: IndexMap = serde_json::from_value(d.to_owned()).unwrap(); - Some(deps) - } else { - None - } - }); - let dev_dependencies = package_json.get("devDependencies").and_then(|d| { - if d.is_object() { - let deps: IndexMap = serde_json::from_value(d.to_owned()).unwrap(); - Some(deps) - } else { - None - } - }); - - let scripts: Option> = package_json - .get("scripts") - .and_then(|d| serde_json::from_value(d.to_owned()).ok()); - - // Ignore unknown types for forwards compatibility - let typ = if let Some(t) = type_val { - if let Some(t) = t.as_str() { - if t != "module" && t != "commonjs" { - "none".to_string() - } else { - t.to_string() - } - } else { - "none".to_string() - } - } else { - "none".to_string() - }; - - // for typescript, it looks for "typings" first, then "types" - let types = package_json - .get("typings") - .or_else(|| package_json.get("types")) - .and_then(|t| t.as_str().map(|s| s.to_string())); - - let package_json = PackageJson { - exists: true, - path, - main, - name, - version, - module, - typ, - types, - exports, - imports, - bin, - dependencies, - dev_dependencies, - scripts, - }; - - Ok(package_json) - } - - pub fn main(&self, referrer_kind: NodeModuleKind) -> Option<&str> { - let main = if referrer_kind == NodeModuleKind::Esm && self.typ == "module" { - self.module.as_ref().or(self.main.as_ref()) - } else { - self.main.as_ref() - }; - main.map(|m| m.trim()).filter(|m| !m.is_empty()) - } +pub struct PackageJsonThreadLocalCache; - pub fn specifier(&self) -> ModuleSpecifier { - ModuleSpecifier::from_file_path(&self.path).unwrap() +impl PackageJsonThreadLocalCache { + pub fn clear() { + CACHE.with(|cache| cache.borrow_mut().clear()); } } -fn is_conditional_exports_main_sugar(exports: &Value) -> bool { - if exports.is_string() || exports.is_array() { - return true; +impl deno_config::package_json::PackageJsonCache for PackageJsonThreadLocalCache { + fn get(&self, path: &Path) -> Option { + CACHE.with(|cache| cache.borrow().get(path).cloned()) } - if exports.is_null() || !exports.is_object() { - return false; - } - - let exports_obj = exports.as_object().unwrap(); - let mut is_conditional_sugar = false; - let mut i = 0; - for key in exports_obj.keys() { - let cur_is_conditional_sugar = key.is_empty() || !key.starts_with('.'); - if i == 0 { - is_conditional_sugar = cur_is_conditional_sugar; - i += 1; - } else if is_conditional_sugar != cur_is_conditional_sugar { - panic!( - "\"exports\" cannot contains some keys starting with \'.\' and some not. - The exports object must either be an object of package subpath keys - or an object of main entry condition name keys only." - ) - } + fn set(&self, path: PathBuf, package_json: PackageJsonRc) { + CACHE.with(|cache| cache.borrow_mut().insert(path, package_json)); } - - is_conditional_sugar } -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn null_exports_should_not_crash() { - let package_json = PackageJson::load_from_string( - PathBuf::from("/package.json"), - r#"{ "exports": null }"#.to_string(), - ) - .unwrap(); - - assert!(package_json.exports.is_none()); +/// Helper to load a package.json file using the thread local cache +/// in deno_node. +pub fn load_pkg_json( + fs: &dyn deno_fs::FileSystem, + path: &Path, +) -> Result, PackageJsonLoadError> { + let result = PackageJson::load_from_path( + path, + &DenoConfigFsAdapter::new(fs), + Some(&PackageJsonThreadLocalCache), + ); + match result { + Ok(pkg_json) => Ok(Some(pkg_json)), + Err(PackageJsonLoadError::Io { source, .. }) if source.kind() == ErrorKind::NotFound => { + Ok(None) + } + Err(err) => Err(err), } } diff --git a/crates/node/polyfill.rs b/crates/node/polyfill.rs index 54f3110a1..ea8ce9093 100644 --- a/crates/node/polyfill.rs +++ b/crates/node/polyfill.rs @@ -71,6 +71,7 @@ generate_builtin_node_module_lists! { "querystring", "repl", "readline", + "readline/promises", "stream", "stream/consumers", "stream/promises", diff --git a/crates/node/polyfills/01_require.js b/crates/node/polyfills/01_require.js index 6ec982f33..a0eddc773 100644 --- a/crates/node/polyfills/01_require.js +++ b/crates/node/polyfills/01_require.js @@ -475,6 +475,7 @@ function Module(id = "", parent) { updateChildren(parent, this, false); this.filename = null; this.loaded = false; + this.parent = parent; this.children = []; } @@ -1054,7 +1055,7 @@ Module._extensions[".js"] = function (module, filename) { if (StringPrototypeEndsWith(filename, ".js")) { const pkg = op_require_read_closest_package_json(filename); - if (pkg && pkg.exists && pkg.typ === "module") { + if (pkg && pkg.typ === "module") { throw createRequireEsmError( filename, moduleParentCache.get(module)?.filename, @@ -1262,6 +1263,15 @@ internals.requireImpl = { nativeModuleExports, }; +/** + * @param {string} path + * @returns {SourceMap | undefined} + */ +export function findSourceMap(_path) { + // TODO(@marvinhagemeister): Stub implementation for now to unblock ava + return undefined; +} + export { builtinModules, createRequire, isBuiltin, Module }; export const _cache = Module._cache; export const _extensions = Module._extensions; diff --git a/crates/node/polyfills/02_init.js b/crates/node/polyfills/02_init.js index 84c58490f..3e8e83996 100644 --- a/crates/node/polyfills/02_init.js +++ b/crates/node/polyfills/02_init.js @@ -10,14 +10,16 @@ import "node:module"; let initialized = false; -function initialize( - usesLocalNodeModulesDir, - argv0, - runningOnMainThread, - workerId, - maybeWorkerMetadata, - warmup = false, -) { +function initialize(args) { + const { + usesLocalNodeModulesDir, + argv0, + runningOnMainThread, + workerId, + maybeWorkerMetadata, + nodeDebug, + warmup = false, + } = args; if (!warmup) { if (initialized) { throw Error("Node runtime already initialized"); @@ -33,14 +35,18 @@ function initialize( argv0, Deno.args, Deno.version, - Deno.env.get("NODE_DEBUG") ?? "", + nodeDebug, ); + // @andreespirela, no we shouldn't be using these worker threads -/* internals.__initWorkerThreads( - runningOnMainThread, - workerId, - maybeWorkerMetadata, - );*/ + /* + internals.__initWorkerThreads( + runningOnMainThread, + workerId, + maybeWorkerMetadata, + ); + */ + internals.__setupChildProcessIpcChannel(); // `Deno[Deno.internal].requireImpl` will be unreachable after this line. delete internals.requireImpl; diff --git a/crates/node/polyfills/_brotli.js b/crates/node/polyfills/_brotli.js index 8b1f53012..1524bf85c 100644 --- a/crates/node/polyfills/_brotli.js +++ b/crates/node/polyfills/_brotli.js @@ -1,9 +1,18 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - -import { core } from "ext:core/mod.js"; +import { core, primordials } from "ext:core/mod.js"; +const { + Uint8Array, + PromisePrototypeThen, + PromisePrototypeCatch, + ObjectValues, + TypedArrayPrototypeSlice, + TypedArrayPrototypeSubarray, + TypedArrayPrototypeGetByteLength, + DataViewPrototypeGetBuffer, + TypedArrayPrototypeGetBuffer, +} = primordials; +const { isTypedArray, isDataView, close } = core; import { op_brotli_compress, op_brotli_compress_async, @@ -28,8 +37,10 @@ const toU8 = (input) => { return enc.encode(input); } - if (input.buffer) { - return new Uint8Array(input.buffer); + if (isTypedArray(input)) { + return new Uint8Array(TypedArrayPrototypeGetBuffer(input)); + } else if (isDataView(input)) { + return new Uint8Array(DataViewPrototypeGetBuffer(input)); } return input; @@ -52,18 +63,20 @@ export class BrotliDecompress extends Transform { // TODO(littledivy): use `encoding` argument transform(chunk, _encoding, callback) { const input = toU8(chunk); - const output = new Uint8Array(chunk.byteLength); + const output = new Uint8Array(TypedArrayPrototypeGetByteLength(chunk)); const avail = op_brotli_decompress_stream(context, input, output); - this.push(output.slice(0, avail)); + // deno-lint-ignore prefer-primordials + this.push(TypedArrayPrototypeSlice(output, 0, avail)); callback(); }, flush(callback) { const output = new Uint8Array(1024); let avail; while ((avail = op_brotli_decompress_stream_end(context, output)) > 0) { - this.push(output.slice(0, avail)); + // deno-lint-ignore prefer-primordials + this.push(TypedArrayPrototypeSlice(output, 0, avail)); } - core.close(context); + close(context); callback(); }, }); @@ -84,7 +97,8 @@ export class BrotliCompress extends Transform { const output = new Uint8Array(brotliMaxCompressedSize(input.length)); const written = op_brotli_compress_stream(context, input, output); if (written > 0) { - this.push(output.slice(0, written)); + // deno-lint-ignore prefer-primordials + this.push(TypedArrayPrototypeSlice(output, 0, written)); } callback(); }, @@ -92,14 +106,15 @@ export class BrotliCompress extends Transform { const output = new Uint8Array(1024); let avail; while ((avail = op_brotli_compress_stream_end(context, output)) > 0) { - this.push(output.slice(0, avail)); + // deno-lint-ignore prefer-primordials + this.push(TypedArrayPrototypeSlice(output, 0, avail)); } - core.close(context); + close(context); callback(); }, }); - const params = Object.values(options?.params ?? {}); + const params = ObjectValues(options?.params ?? {}); this.#context = op_create_brotli_compress(params); const context = this.#context; } @@ -144,9 +159,13 @@ export function brotliCompress( } const { quality, lgwin, mode } = oneOffCompressOptions(options); - op_brotli_compress_async(buf, quality, lgwin, mode) - .then((result) => callback(null, Buffer.from(result))) - .catch((err) => callback(err)); + PromisePrototypeCatch( + PromisePrototypeThen( + op_brotli_compress_async(buf, quality, lgwin, mode), + (result) => callback(null, Buffer.from(result)), + ), + (err) => callback(err), + ); } export function brotliCompressSync( @@ -158,14 +177,18 @@ export function brotliCompressSync( const { quality, lgwin, mode } = oneOffCompressOptions(options); const len = op_brotli_compress(buf, output, quality, lgwin, mode); - return Buffer.from(output.subarray(0, len)); + return Buffer.from(TypedArrayPrototypeSubarray(output, 0, len)); } export function brotliDecompress(input) { const buf = toU8(input); - return op_brotli_decompress_async(buf) - .then((result) => callback(null, Buffer.from(result))) - .catch((err) => callback(err)); + return PromisePrototypeCatch( + PromisePrototypeThen( + op_brotli_decompress_async(buf), + (result) => callback(null, Buffer.from(result)), + ), + (err) => callback(err), + ); } export function brotliDecompressSync(input) { diff --git a/crates/node/polyfills/_fs/_fs_cp.js b/crates/node/polyfills/_fs/_fs_cp.js index 0d8c4d644..52dd8f505 100644 --- a/crates/node/polyfills/_fs/_fs_cp.js +++ b/crates/node/polyfills/_fs/_fs_cp.js @@ -1,15 +1,14 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file prefer-primordials - +import { primordials } from "ext:core/mod.js"; import { op_node_cp, op_node_cp_sync } from "ext:core/ops"; - import { getValidatedPath, validateCpOptions, } from "ext:deno_node/internal/fs/utils.mjs"; import { promisify } from "ext:deno_node/internal/util.mjs"; +const { PromisePrototypeThen } = primordials; + export function cpSync(src, dest, options) { validateCpOptions(options); const srcPath = getValidatedPath(src, "src"); @@ -27,10 +26,11 @@ export function cp(src, dest, options, callback) { const srcPath = getValidatedPath(src, "src"); const destPath = getValidatedPath(dest, "dest"); - op_node_cp( - srcPath, - destPath, - ).then( + PromisePrototypeThen( + op_node_cp( + srcPath, + destPath, + ), (res) => callback(null, res), (err) => callback(err, null), ); diff --git a/crates/node/polyfills/_fs/_fs_dir.ts b/crates/node/polyfills/_fs/_fs_dir.ts index acb56e84b..ed051fb0b 100644 --- a/crates/node/polyfills/_fs/_fs_dir.ts +++ b/crates/node/polyfills/_fs/_fs_dir.ts @@ -1,13 +1,22 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - +import { primordials } from "ext:core/mod.js"; import Dirent from "ext:deno_node/_fs/_fs_dirent.ts"; import { assert } from "ext:deno_node/_util/asserts.ts"; import { ERR_MISSING_ARGS } from "ext:deno_node/internal/errors.ts"; import { TextDecoder } from "ext:deno_web/08_text_encoding.js"; +const { + Promise, + ObjectPrototypeIsPrototypeOf, + Uint8ArrayPrototype, + PromisePrototypeThen, + SymbolAsyncIterator, + ArrayIteratorPrototypeNext, + AsyncGeneratorPrototypeNext, + SymbolIterator, +} = primordials; + export default class Dir { #dirPath: string | Uint8Array; #syncIterator!: Iterator | null; @@ -21,7 +30,7 @@ export default class Dir { } get path(): string { - if (this.#dirPath instanceof Uint8Array) { + if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, this.#dirPath)) { return new TextDecoder().decode(this.#dirPath); } return this.#dirPath; @@ -31,12 +40,12 @@ export default class Dir { read(callback?: (...args: any[]) => void): Promise { return new Promise((resolve, reject) => { if (!this.#asyncIterator) { - this.#asyncIterator = Deno.readDir(this.path)[Symbol.asyncIterator](); + this.#asyncIterator = Deno.readDir(this.path)[SymbolAsyncIterator](); } assert(this.#asyncIterator); - this.#asyncIterator - .next() - .then((iteratorResult) => { + PromisePrototypeThen( + AsyncGeneratorPrototypeNext(this.#asyncIterator), + (iteratorResult) => { resolve( iteratorResult.done ? null : new Dirent(iteratorResult.value), ); @@ -46,21 +55,23 @@ export default class Dir { iteratorResult.done ? null : new Dirent(iteratorResult.value), ); } - }, (err) => { + }, + (err) => { if (callback) { callback(err); } reject(err); - }); + }, + ); }); } readSync(): Dirent | null { if (!this.#syncIterator) { - this.#syncIterator = Deno.readDirSync(this.path)![Symbol.iterator](); + this.#syncIterator = Deno.readDirSync(this.path)![SymbolIterator](); } - const iteratorResult = this.#syncIterator.next(); + const iteratorResult = ArrayIteratorPrototypeNext(this.#syncIterator); if (iteratorResult.done) { return null; } else { @@ -92,7 +103,7 @@ export default class Dir { //No op } - async *[Symbol.asyncIterator](): AsyncIterableIterator { + async *[SymbolAsyncIterator](): AsyncIterableIterator { try { while (true) { const dirent: Dirent | null = await this.read(); diff --git a/crates/node/polyfills/_fs/_fs_dirent.ts b/crates/node/polyfills/_fs/_fs_dirent.ts index 155d5cb03..a24de19a1 100644 --- a/crates/node/polyfills/_fs/_fs_dirent.ts +++ b/crates/node/polyfills/_fs/_fs_dirent.ts @@ -2,7 +2,7 @@ import { notImplemented } from "ext:deno_node/_utils.ts"; export default class Dirent { - constructor(private entry: Deno.DirEntry) {} + constructor(private entry: Deno.DirEntry & { parentPath: string }) { } isBlockDevice(): boolean { notImplemented("Deno does not yet support identification of block devices"); @@ -43,4 +43,13 @@ export default class Dirent { get name(): string | null { return this.entry.name; } + + get parentPath(): string { + return this.entry.parentPath; + } + + /** @deprecated */ + get path(): string { + return this.parentPath; + } } diff --git a/crates/node/polyfills/_fs/_fs_futimes.ts b/crates/node/polyfills/_fs/_fs_futimes.ts index cc4e35b0b..98cd1066c 100644 --- a/crates/node/polyfills/_fs/_fs_futimes.ts +++ b/crates/node/polyfills/_fs/_fs_futimes.ts @@ -5,6 +5,9 @@ import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts"; import { FsFile } from "ext:deno_fs/30_fs.js"; +import { validateInteger } from "ext:deno_node/internal/validators.mjs"; +import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; +import { toUnixTimestamp } from "ext:deno_node/internal/fs/utils.mjs"; function getValidTime( time: number | string | Date, @@ -23,7 +26,7 @@ function getValidTime( ); } - return time; + return toUnixTimestamp(time); } export function futimes( @@ -35,6 +38,11 @@ export function futimes( if (!callback) { throw new Deno.errors.InvalidData("No callback function supplied"); } + if (typeof fd !== "number") { + throw new ERR_INVALID_ARG_TYPE("fd", "number", fd); + } + + validateInteger(fd, "fd", 0, 2147483647); atime = getValidTime(atime, "atime"); mtime = getValidTime(mtime, "mtime"); @@ -51,6 +59,12 @@ export function futimesSync( atime: number | string | Date, mtime: number | string | Date, ) { + if (typeof fd !== "number") { + throw new ERR_INVALID_ARG_TYPE("fd", "number", fd); + } + + validateInteger(fd, "fd", 0, 2147483647); + atime = getValidTime(atime, "atime"); mtime = getValidTime(mtime, "mtime"); diff --git a/crates/node/polyfills/_fs/_fs_lchown.ts b/crates/node/polyfills/_fs/_fs_lchown.ts new file mode 100644 index 000000000..8611c8021 --- /dev/null +++ b/crates/node/polyfills/_fs/_fs_lchown.ts @@ -0,0 +1,61 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +// TODO(petamoriken): enable prefer-primordials for node polyfills +// deno-lint-ignore-file prefer-primordials + +import { + type CallbackWithError, + makeCallback, +} from "ext:deno_node/_fs/_fs_common.ts"; +import { + getValidatedPath, + kMaxUserId, +} from "ext:deno_node/internal/fs/utils.mjs"; +import * as pathModule from "node:path"; +import { validateInteger } from "ext:deno_node/internal/validators.mjs"; +import type { Buffer } from "node:buffer"; +import { promisify } from "ext:deno_node/internal/util.mjs"; +import { op_node_lchown, op_node_lchown_sync } from "ext:core/ops"; + +/** + * Asynchronously changes the owner and group + * of a file, without following symlinks. + */ +export function lchown( + path: string | Buffer | URL, + uid: number, + gid: number, + callback: CallbackWithError, +) { + callback = makeCallback(callback); + path = getValidatedPath(path).toString(); + validateInteger(uid, "uid", -1, kMaxUserId); + validateInteger(gid, "gid", -1, kMaxUserId); + + op_node_lchown(pathModule.toNamespacedPath(path), uid, gid).then( + () => callback(null), + callback, + ); +} + +export const lchownPromise = promisify(lchown) as ( + path: string | Buffer | URL, + uid: number, + gid: number, +) => Promise; + +/** + * Synchronously changes the owner and group + * of a file, without following symlinks. + */ +export function lchownSync( + path: string | Buffer | URL, + uid: number, + gid: number, +) { + path = getValidatedPath(path).toString(); + validateInteger(uid, "uid", -1, kMaxUserId); + validateInteger(gid, "gid", -1, kMaxUserId); + + op_node_lchown_sync(pathModule.toNamespacedPath(path), uid, gid); +} diff --git a/crates/node/polyfills/_fs/_fs_lstat.ts b/crates/node/polyfills/_fs/_fs_lstat.ts index c8cdfc4e4..6ce401444 100644 --- a/crates/node/polyfills/_fs/_fs_lstat.ts +++ b/crates/node/polyfills/_fs/_fs_lstat.ts @@ -3,6 +3,7 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials +import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; import { BigIntStats, CFISBIS, @@ -56,16 +57,31 @@ export const lstatPromise = promisify(lstat) as ( export function lstatSync(path: string | URL): Stats; export function lstatSync( path: string | URL, - options: { bigint: false }, + options: { bigint: false; throwIfNoEntry?: boolean }, ): Stats; export function lstatSync( path: string | URL, - options: { bigint: true }, + options: { bigint: true; throwIfNoEntry?: boolean }, ): BigIntStats; export function lstatSync( path: string | URL, options?: statOptions, ): Stats | BigIntStats { - const origin = Deno.lstatSync(path); - return CFISBIS(origin, options?.bigint || false); + try { + const origin = Deno.lstatSync(path); + return CFISBIS(origin, options?.bigint || false); + } catch (err) { + if ( + options?.throwIfNoEntry === false && + err instanceof Deno.errors.NotFound + ) { + return; + } + + if (err instanceof Error) { + throw denoErrorToNodeError(err, { syscall: "stat" }); + } else { + throw err; + } + } } diff --git a/crates/node/polyfills/_fs/_fs_lutimes.ts b/crates/node/polyfills/_fs/_fs_lutimes.ts new file mode 100644 index 000000000..2475c5714 --- /dev/null +++ b/crates/node/polyfills/_fs/_fs_lutimes.ts @@ -0,0 +1,85 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +// deno-lint-ignore-file prefer-primordials + +import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts"; +import { type Buffer } from "node:buffer"; +import { primordials } from "ext:core/mod.js"; +import { op_node_lutimes, op_node_lutimes_sync } from "ext:core/ops"; +import { promisify } from "ext:deno_node/internal/util.mjs"; +import { + getValidatedPath, + toUnixTimestamp, +} from "ext:deno_node/internal/fs/utils.mjs"; + +const { MathTrunc } = primordials; + +type TimeLike = number | string | Date; +type PathLike = string | Buffer | URL; + +function getValidUnixTime( + value: TimeLike, + name: string, +): [number, number] { + if (typeof value === "string") { + value = Number(value); + } + + if ( + typeof value === "number" && + (Number.isNaN(value) || !Number.isFinite(value)) + ) { + throw new Deno.errors.InvalidData( + `invalid ${name}, must not be infinity or NaN`, + ); + } + + const unixSeconds = toUnixTimestamp(value); + + const seconds = MathTrunc(unixSeconds); + const nanoseconds = MathTrunc((unixSeconds * 1e3) - (seconds * 1e3)) * 1e6; + + return [ + seconds, + nanoseconds, + ]; +} + +export function lutimes( + path: PathLike, + atime: TimeLike, + mtime: TimeLike, + callback: CallbackWithError, +): void { + if (!callback) { + throw new Error("No callback function supplied"); + } + const [atimeSecs, atimeNanos] = getValidUnixTime(atime, "atime"); + const [mtimeSecs, mtimeNanos] = getValidUnixTime(mtime, "mtime"); + + path = getValidatedPath(path).toString(); + + op_node_lutimes(path, atimeSecs, atimeNanos, mtimeSecs, mtimeNanos).then( + () => callback(null), + callback, + ); +} + +export function lutimesSync( + path: PathLike, + atime: TimeLike, + mtime: TimeLike, +): void { + const { 0: atimeSecs, 1: atimeNanos } = getValidUnixTime(atime, "atime"); + const { 0: mtimeSecs, 1: mtimeNanos } = getValidUnixTime(mtime, "mtime"); + + path = getValidatedPath(path).toString(); + + op_node_lutimes_sync(path, atimeSecs, atimeNanos, mtimeSecs, mtimeNanos); +} + +export const lutimesPromise = promisify(lutimes) as ( + path: PathLike, + atime: TimeLike, + mtime: TimeLike, +) => Promise; diff --git a/crates/node/polyfills/_fs/_fs_read.ts b/crates/node/polyfills/_fs/_fs_read.ts index cf0c5e51d..e25f41e76 100644 --- a/crates/node/polyfills/_fs/_fs_read.ts +++ b/crates/node/polyfills/_fs/_fs_read.ts @@ -88,10 +88,6 @@ export function read( if ( !(opt.buffer instanceof Buffer) && !(opt.buffer instanceof Uint8Array) ) { - if (opt.buffer === null) { - // @ts-ignore: Intentionally create TypeError for passing test-fs-read.js#L87 - length = opt.buffer.byteLength; - } throw new ERR_INVALID_ARG_TYPE("buffer", [ "Buffer", "TypedArray", diff --git a/crates/node/polyfills/_fs/_fs_readdir.ts b/crates/node/polyfills/_fs/_fs_readdir.ts index f8c0a59d6..3b314227d 100644 --- a/crates/node/polyfills/_fs/_fs_readdir.ts +++ b/crates/node/polyfills/_fs/_fs_readdir.ts @@ -11,7 +11,7 @@ import { getValidatedPath } from "ext:deno_node/internal/fs/utils.mjs"; import { Buffer } from "node:buffer"; import { promisify } from "ext:deno_node/internal/util.mjs"; -function toDirent(val: Deno.DirEntry): Dirent { +function toDirent(val: Deno.DirEntry & { parentPath: string }): Dirent { return new Dirent(val); } @@ -67,13 +67,15 @@ export function readdir( } try { - asyncIterableToCallback(Deno.readDir(path.toString()), (val, done) => { + path = path.toString(); + asyncIterableToCallback(Deno.readDir(path), (val, done) => { if (typeof path !== "string") return; if (done) { callback(null, result); return; } if (options?.withFileTypes) { + val.parentPath = path; result.push(toDirent(val)); } else result.push(decode(val.name)); }, (e) => { @@ -130,8 +132,10 @@ export function readdirSync( } try { - for (const file of Deno.readDirSync(path.toString())) { + path = path.toString(); + for (const file of Deno.readDirSync(path)) { if (options?.withFileTypes) { + file.parentPath = path; result.push(toDirent(file)); } else result.push(decode(file.name)); } diff --git a/crates/node/polyfills/_fs/_fs_utimes.ts b/crates/node/polyfills/_fs/_fs_utimes.ts index 1d0e5c1ff..3fff4a462 100644 --- a/crates/node/polyfills/_fs/_fs_utimes.ts +++ b/crates/node/polyfills/_fs/_fs_utimes.ts @@ -4,13 +4,16 @@ // deno-lint-ignore-file prefer-primordials import type { CallbackWithError } from "ext:deno_node/_fs/_fs_common.ts"; -import { pathFromURL } from "ext:deno_web/00_infra.js"; import { promisify } from "ext:deno_node/internal/util.mjs"; +import { + getValidatedPath, + toUnixTimestamp, +} from "ext:deno_node/internal/fs/utils.mjs"; function getValidTime( time: number | string | Date, name: string, -): number | Date { +): number { if (typeof time === "string") { time = Number(time); } @@ -24,7 +27,7 @@ function getValidTime( ); } - return time; + return toUnixTimestamp(time); } export function utimes( @@ -33,7 +36,7 @@ export function utimes( mtime: number | string | Date, callback: CallbackWithError, ) { - path = path instanceof URL ? pathFromURL(path) : path; + path = getValidatedPath(path).toString(); if (!callback) { throw new Deno.errors.InvalidData("No callback function supplied"); @@ -56,7 +59,7 @@ export function utimesSync( atime: number | string | Date, mtime: number | string | Date, ) { - path = path instanceof URL ? pathFromURL(path) : path; + path = getValidatedPath(path).toString(); atime = getValidTime(atime, "atime"); mtime = getValidTime(mtime, "mtime"); diff --git a/crates/node/polyfills/_fs/_fs_write.mjs b/crates/node/polyfills/_fs/_fs_write.mjs index aa23805bf..1ad6ac492 100644 --- a/crates/node/polyfills/_fs/_fs_write.mjs +++ b/crates/node/polyfills/_fs/_fs_write.mjs @@ -13,7 +13,6 @@ import * as io from "ext:deno_io/12_io.js"; import * as fs from "ext:deno_fs/30_fs.js"; import { getValidatedFd, - showStringCoercionDeprecation, validateOffsetLengthWrite, validateStringAfterArrayBufferView, } from "ext:deno_node/internal/fs/utils.mjs"; @@ -114,9 +113,6 @@ export function write(fd, buffer, offset, length, position, callback) { // `fs.write(fd, string[, position[, encoding]], callback)` validateStringAfterArrayBufferView(buffer, "buffer"); - if (typeof buffer !== "string") { - showStringCoercionDeprecation(); - } if (typeof position !== "function") { if (typeof offset === "function") { @@ -128,7 +124,7 @@ export function write(fd, buffer, offset, length, position, callback) { length = "utf-8"; } - const str = String(buffer); + const str = buffer; validateEncoding(str, length); callback = maybeCallback(position); buffer = Buffer.from(str, length); diff --git a/crates/node/polyfills/_fs/_fs_writeFile.ts b/crates/node/polyfills/_fs/_fs_writeFile.ts index 60b31897e..f0014c36d 100644 --- a/crates/node/polyfills/_fs/_fs_writeFile.ts +++ b/crates/node/polyfills/_fs/_fs_writeFile.ts @@ -20,7 +20,6 @@ import { denoErrorToNodeError, } from "ext:deno_node/internal/errors.ts"; import { - showStringCoercionDeprecation, validateStringAfterArrayBufferView, } from "ext:deno_node/internal/fs/utils.mjs"; import { promisify } from "ext:deno_node/internal/util.mjs"; @@ -32,8 +31,7 @@ interface Writer { export function writeFile( pathOrRid: string | number | URL, - // deno-lint-ignore ban-types - data: string | Uint8Array | Object, + data: string | Uint8Array, optOrCallback: Encodings | CallbackWithError | WriteFileOptions | undefined, callback?: CallbackWithError, ) { @@ -61,10 +59,7 @@ export function writeFile( if (!ArrayBuffer.isView(data)) { validateStringAfterArrayBufferView(data, "data"); - if (typeof data !== "string") { - showStringCoercionDeprecation(); - } - data = Buffer.from(String(data), encoding); + data = Buffer.from(data, encoding); } const isRid = typeof pathOrRid === "number"; @@ -101,15 +96,13 @@ export function writeFile( export const writeFilePromise = promisify(writeFile) as ( pathOrRid: string | number | URL, - // deno-lint-ignore ban-types - data: string | Uint8Array | Object, + data: string | Uint8Array, options?: Encodings | WriteFileOptions, ) => Promise; export function writeFileSync( pathOrRid: string | number | URL, - // deno-lint-ignore ban-types - data: string | Uint8Array | Object, + data: string | Uint8Array, options?: Encodings | WriteFileOptions, ) { pathOrRid = pathOrRid instanceof URL ? pathFromURL(pathOrRid) : pathOrRid; @@ -127,10 +120,7 @@ export function writeFileSync( if (!ArrayBuffer.isView(data)) { validateStringAfterArrayBufferView(data, "data"); - if (typeof data !== "string") { - showStringCoercionDeprecation(); - } - data = Buffer.from(String(data), encoding); + data = Buffer.from(data, encoding); } const isRid = typeof pathOrRid === "number"; diff --git a/crates/node/polyfills/_http_common.ts b/crates/node/polyfills/_http_common.ts index d4822960e..24dae6f30 100644 --- a/crates/node/polyfills/_http_common.ts +++ b/crates/node/polyfills/_http_common.ts @@ -1,20 +1,19 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright Joyent and Node contributors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - -const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; +import { primordials } from "ext:core/mod.js"; +const { RegExpPrototypeTest, SafeRegExp } = primordials; +const tokenRegExp = new SafeRegExp(/^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/); /** * Verifies that the given val is a valid HTTP token * per the rules defined in RFC 7230 * See https://tools.ietf.org/html/rfc7230#section-3.2.6 */ function checkIsHttpToken(val: string) { - return tokenRegExp.test(val); + return RegExpPrototypeTest(tokenRegExp, val); } -const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; +const headerCharRegex = new SafeRegExp(/[^\t\x20-\x7e\x80-\xff]/); /** * True if val contains an invalid field-vchar * field-value = *( field-content / obs-fold ) @@ -22,10 +21,10 @@ const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; * field-vchar = VCHAR / obs-text */ function checkInvalidHeaderChar(val: string) { - return headerCharRegex.test(val); + return RegExpPrototypeTest(headerCharRegex, val); } -export const chunkExpression = /(?:^|\W)chunked(?:$|\W)/i; +export const chunkExpression = new SafeRegExp(/(?:^|\W)chunked(?:$|\W)/i); export { checkInvalidHeaderChar as _checkInvalidHeaderChar, checkIsHttpToken as _checkIsHttpToken, diff --git a/crates/node/polyfills/_process/process.ts b/crates/node/polyfills/_process/process.ts index 57450a521..5abde1cf0 100644 --- a/crates/node/polyfills/_process/process.ts +++ b/crates/node/polyfills/_process/process.ts @@ -1,13 +1,23 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - // The following are all the process APIs that don't depend on the stream module // They have to be split this way to prevent a circular dependency -import { core } from "ext:core/mod.js"; +import { core, primordials } from "ext:core/mod.js"; +const { + Error, + ObjectGetOwnPropertyNames, + String, + ReflectOwnKeys, + ArrayPrototypeIncludes, + Object, + Proxy, + ObjectPrototype, + ObjectPrototypeIsPrototypeOf, + TypeErrorPrototype, +} = primordials; +const { build } = core; import { nextTick as _nextTick } from "ext:deno_node/_next_tick.ts"; import { _exiting } from "ext:deno_node/_process/exiting.ts"; @@ -15,11 +25,11 @@ import * as fs from "ext:deno_fs/30_fs.js"; /** Returns the operating system CPU architecture for which the Deno binary was compiled */ export function arch(): string { - if (core.build.arch == "x86_64") { + if (build.arch == "x86_64") { return "x64"; - } else if (core.build.arch == "aarch64") { + } else if (build.arch == "aarch64") { return "arm64"; - } else if (core.build.arch == "riscv64gc") { + } else if (build.arch == "riscv64gc") { return "riscv64"; } else { throw Error("unreachable"); @@ -41,14 +51,18 @@ function denoEnvGet(name: string) { try { return Deno.env.get(name); } catch (e) { - if (e instanceof TypeError || e instanceof Deno.errors.PermissionDenied) { + if ( + ObjectPrototypeIsPrototypeOf(TypeErrorPrototype, e) || + // TODO(iuioiua): Use `PermissionDeniedPrototype` when it's available + ObjectPrototypeIsPrototypeOf(Deno.errors.PermissionDenied.prototype, e) + ) { return undefined; } throw e; } } -const OBJECT_PROTO_PROP_NAMES = Object.getOwnPropertyNames(Object.prototype); +const OBJECT_PROTO_PROP_NAMES = ObjectGetOwnPropertyNames(ObjectPrototype); /** * https://nodejs.org/api/process.html#process_process_env * Requires env permissions @@ -66,13 +80,13 @@ export const env: InstanceType & Record = return envValue; } - if (OBJECT_PROTO_PROP_NAMES.includes(prop)) { + if (ArrayPrototypeIncludes(OBJECT_PROTO_PROP_NAMES, prop)) { return target[prop]; } return envValue; }, - ownKeys: () => Reflect.ownKeys(Deno.env.toObject()), + ownKeys: () => ReflectOwnKeys(Deno.env.toObject()), getOwnPropertyDescriptor: (_target, name) => { const value = denoEnvGet(String(name)); if (value) { @@ -88,6 +102,10 @@ export const env: InstanceType & Record = return true; // success }, has: (_target, prop) => typeof denoEnvGet(String(prop)) === "string", + deleteProperty(_target, key) { + Deno.env.delete(String(key)); + return true; + }, }); /** diff --git a/crates/node/polyfills/_process/streams.mjs b/crates/node/polyfills/_process/streams.mjs index f50e20588..6f289fb77 100644 --- a/crates/node/polyfills/_process/streams.mjs +++ b/crates/node/polyfills/_process/streams.mjs @@ -1,8 +1,17 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. // Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials +import { primordials } from "ext:core/mod.js"; +const { + Uint8ArrayPrototype, + Error, + ObjectDefineProperties, + ObjectDefineProperty, + TypedArrayPrototypeSlice, + PromisePrototypeThen, + ObjectValues, + ObjectPrototypeIsPrototypeOf, +} = primordials; import { Buffer } from "node:buffer"; import { @@ -26,7 +35,11 @@ export function createWritableStdioStream(writer, name, warmup = false) { ); return; } - writer.writeSync(buf instanceof Uint8Array ? buf : Buffer.from(buf, enc)); + writer.writeSync( + ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, buf) + ? buf + : Buffer.from(buf, enc), + ); cb(); }, destroy(err, cb) { @@ -39,8 +52,10 @@ export function createWritableStdioStream(writer, name, warmup = false) { }); let fd = -1; + // deno-lint-ignore prefer-primordials if (writer instanceof io.Stdout) { fd = io.STDOUT_RID; + // deno-lint-ignore prefer-primordials } else if (writer instanceof io.Stderr) { fd = io.STDERR_RID; } @@ -48,7 +63,7 @@ export function createWritableStdioStream(writer, name, warmup = false) { stream.destroySoon = stream.destroy; stream._isStdio = true; stream.once("close", () => writer?.close()); - Object.defineProperties(stream, { + ObjectDefineProperties(stream, { columns: { enumerable: true, configurable: true, @@ -69,7 +84,7 @@ export function createWritableStdioStream(writer, name, warmup = false) { enumerable: true, configurable: true, value: () => - writer?.isTerminal() ? Object.values(Deno.consoleSize?.()) : undefined, + writer?.isTerminal() ? ObjectValues(Deno.consoleSize?.()) : undefined, }, }); @@ -107,14 +122,12 @@ function _guessStdinType(fd) { const _read = function (size) { const p = Buffer.alloc(size || 16 * 1024); - io.stdin?.read(p).then( - (length) => { - this.push(length === null ? null : p.slice(0, length)); - }, - (error) => { - this.destroy(error); - }, - ); + PromisePrototypeThen(io.stdin?.read(p), (length) => { + // deno-lint-ignore prefer-primordials + this.push(length === null ? null : TypedArrayPrototypeSlice(p, 0, length)); + }, (error) => { + this.destroy(error); + }); }; let readStream; @@ -182,13 +195,14 @@ export const initStdin = (warmup = false) => { // Provide a dummy contentless input for e.g. non-console // Windows applications. stdin = new Readable({ read() {} }); + // deno-lint-ignore prefer-primordials stdin.push(null); } } stdin.on("close", () => io.stdin?.close()); stdin.fd = io.stdin ? io.STDIN_RID : -1; - Object.defineProperty(stdin, "isTTY", { + ObjectDefineProperty(stdin, "isTTY", { enumerable: true, configurable: true, get() { @@ -201,7 +215,7 @@ export const initStdin = (warmup = false) => { stdin._isRawMode = enable; return stdin; }; - Object.defineProperty(stdin, "isRaw", { + ObjectDefineProperty(stdin, "isRaw", { enumerable: true, configurable: true, get() { diff --git a/crates/node/polyfills/_stream.mjs b/crates/node/polyfills/_stream.mjs index 591f8bb51..bceaf60d4 100644 --- a/crates/node/polyfills/_stream.mjs +++ b/crates/node/polyfills/_stream.mjs @@ -265,8 +265,7 @@ var require_validators = __commonJS({ ) { throw new ERR_OUT_OF_RANGE( name, - `${min != null ? `>= ${min}` : ""}${ - min != null && max != null ? " && " : "" + `${min != null ? `>= ${min}` : ""}${min != null && max != null ? " && " : "" }${max != null ? `<= ${max}` : ""}`, value, ); @@ -310,7 +309,7 @@ var require_validators = __commonJS({ if ( !nullable && value === null || !allowArray && ArrayIsArray(value) || typeof value !== "object" && - (!allowFunction || typeof value !== "function") + (!allowFunction || typeof value !== "function") ) { throw new ERR_INVALID_ARG_TYPE(name, "Object", value); } @@ -445,12 +444,12 @@ var require_utils = __commonJS({ typeof obj.on === "function" && (!strict || typeof obj.pause === "function" && - typeof obj.resume === "function") && + typeof obj.resume === "function") && (!obj._writableState || ((_obj$_readableState = obj._readableState) === null || - _obj$_readableState === void 0 - ? void 0 - : _obj$_readableState.readable) !== false) && // Duplex + _obj$_readableState === void 0 + ? void 0 + : _obj$_readableState.readable) !== false) && // Duplex (!obj._writableState || obj._readableState)); } function isWritableNodeStream(obj) { @@ -459,9 +458,9 @@ var require_utils = __commonJS({ typeof obj.on === "function" && (!obj._readableState || ((_obj$_writableState = obj._writableState) === null || - _obj$_writableState === void 0 - ? void 0 - : _obj$_writableState.writable) !== false)); + _obj$_writableState === void 0 + ? void 0 + : _obj$_writableState.writable) !== false)); } function isDuplexNodeStream(obj) { return !!(obj && typeof obj.pipe === "function" && obj._readableState && @@ -636,11 +635,11 @@ var require_utils = __commonJS({ return stream.writableErrored; } return (_stream$_writableStat = - (_stream$_writableStat2 = stream._writableState) === null || - _stream$_writableStat2 === void 0 - ? void 0 - : _stream$_writableStat2.errored) !== null && - _stream$_writableStat !== void 0 + (_stream$_writableStat2 = stream._writableState) === null || + _stream$_writableStat2 === void 0 + ? void 0 + : _stream$_writableStat2.errored) !== null && + _stream$_writableStat !== void 0 ? _stream$_writableStat : null; } @@ -653,11 +652,11 @@ var require_utils = __commonJS({ return stream.readableErrored; } return (_stream$_readableStat = - (_stream$_readableStat2 = stream._readableState) === null || - _stream$_readableStat2 === void 0 - ? void 0 - : _stream$_readableStat2.errored) !== null && - _stream$_readableStat !== void 0 + (_stream$_readableStat2 = stream._readableState) === null || + _stream$_readableStat2 === void 0 + ? void 0 + : _stream$_readableStat2.errored) !== null && + _stream$_readableStat !== void 0 ? _stream$_readableStat : null; } @@ -672,11 +671,11 @@ var require_utils = __commonJS({ const rState = stream._readableState; if ( typeof (wState === null || wState === void 0 - ? void 0 - : wState.closed) === "boolean" || + ? void 0 + : wState.closed) === "boolean" || typeof (rState === null || rState === void 0 - ? void 0 - : rState.closed) === "boolean" + ? void 0 + : rState.closed) === "boolean" ) { return (wState === null || wState === void 0 ? void 0 @@ -702,8 +701,8 @@ var require_utils = __commonJS({ return typeof stream._consuming === "boolean" && typeof stream._dumped === "boolean" && ((_stream$req = stream.req) === null || _stream$req === void 0 - ? void 0 - : _stream$req.upgradeOrConnect) === void 0; + ? void 0 + : _stream$req.upgradeOrConnect) === void 0; } function willEmitClose(stream) { if (!isNodeStream(stream)) { @@ -720,7 +719,7 @@ var require_utils = __commonJS({ var _stream$kIsDisturbed; return !!(stream && ((_stream$kIsDisturbed = stream[kIsDisturbed]) !== null && - _stream$kIsDisturbed !== void 0 + _stream$kIsDisturbed !== void 0 ? _stream$kIsDisturbed : stream.readableDidRead || stream.readableAborted)); } @@ -737,42 +736,42 @@ var require_utils = __commonJS({ _stream$_writableStat4; return !!(stream && ((_ref = - (_ref2 = - (_ref3 = - (_ref4 = - (_ref5 = - (_stream$kIsErrored = - stream[kIsErrored]) !== null && - _stream$kIsErrored !== void 0 - ? _stream$kIsErrored - : stream.readableErrored) !== null && - _ref5 !== void 0 - ? _ref5 - : stream.writableErrored) !== null && - _ref4 !== void 0 - ? _ref4 - : (_stream$_readableStat3 = - stream._readableState) === null || - _stream$_readableStat3 === void 0 - ? void 0 - : _stream$_readableStat3.errorEmitted) !== null && - _ref3 !== void 0 - ? _ref3 - : (_stream$_writableStat3 = stream._writableState) === - null || _stream$_writableStat3 === void 0 - ? void 0 - : _stream$_writableStat3.errorEmitted) !== null && - _ref2 !== void 0 - ? _ref2 - : (_stream$_readableStat4 = stream._readableState) === null || - _stream$_readableStat4 === void 0 + (_ref2 = + (_ref3 = + (_ref4 = + (_ref5 = + (_stream$kIsErrored = + stream[kIsErrored]) !== null && + _stream$kIsErrored !== void 0 + ? _stream$kIsErrored + : stream.readableErrored) !== null && + _ref5 !== void 0 + ? _ref5 + : stream.writableErrored) !== null && + _ref4 !== void 0 + ? _ref4 + : (_stream$_readableStat3 = + stream._readableState) === null || + _stream$_readableStat3 === void 0 ? void 0 - : _stream$_readableStat4.errored) !== null && _ref !== void 0 + : _stream$_readableStat3.errorEmitted) !== null && + _ref3 !== void 0 + ? _ref3 + : (_stream$_writableStat3 = stream._writableState) === + null || _stream$_writableStat3 === void 0 + ? void 0 + : _stream$_writableStat3.errorEmitted) !== null && + _ref2 !== void 0 + ? _ref2 + : (_stream$_readableStat4 = stream._readableState) === null || + _stream$_readableStat4 === void 0 + ? void 0 + : _stream$_readableStat4.errored) !== null && _ref !== void 0 ? _ref : (_stream$_writableStat4 = stream._writableState) === null || - _stream$_writableStat4 === void 0 - ? void 0 - : _stream$_writableStat4.errored)); + _stream$_writableStat4 === void 0 + ? void 0 + : _stream$_writableStat4.errored)); } module.exports = { kDestroyed, @@ -843,11 +842,11 @@ var require_end_of_stream = __commonJS({ validateAbortSignal(options.signal, "options.signal"); callback = once(callback); const readable = (_options$readable = options.readable) !== null && - _options$readable !== void 0 + _options$readable !== void 0 ? _options$readable : isReadableNodeStream(stream); const writable = (_options$writable = options.writable) !== null && - _options$writable !== void 0 + _options$writable !== void 0 ? _options$writable : isWritableNodeStream(stream); if (!isNodeStream(stream)) { @@ -1046,7 +1045,7 @@ var require_operators = __commonJS({ } if ( (options === null || options === void 0 ? void 0 : options.signal) != - null + null ) { validateAbortSignal(options.signal, "options.signal"); } @@ -1079,9 +1078,9 @@ var require_operators = __commonJS({ options === null || options === void 0 ? void 0 : (_options$signal2 = options.signal) === null || - _options$signal2 === void 0 - ? void 0 - : _options$signal2.addEventListener("abort", abort); + _options$signal2 === void 0 + ? void 0 + : _options$signal2.addEventListener("abort", abort); let next; let resume; let done = false; @@ -1139,9 +1138,9 @@ var require_operators = __commonJS({ options === null || options === void 0 ? void 0 : (_options$signal3 = options.signal) === null || - _options$signal3 === void 0 - ? void 0 - : _options$signal3.removeEventListener("abort", abort); + _options$signal3 === void 0 + ? void 0 + : _options$signal3.removeEventListener("abort", abort); } } pump(); @@ -1184,7 +1183,7 @@ var require_operators = __commonJS({ } if ( (options === null || options === void 0 ? void 0 : options.signal) != - null + null ) { validateAbortSignal(options.signal, "options.signal"); } @@ -1271,7 +1270,7 @@ var require_operators = __commonJS({ } if ( (options === null || options === void 0 ? void 0 : options.signal) != - null + null ) { validateAbortSignal(options.signal, "options.signal"); } @@ -1333,7 +1332,7 @@ var require_operators = __commonJS({ } if ( (options === null || options === void 0 ? void 0 : options.signal) != - null + null ) { validateAbortSignal(options.signal, "options.signal"); } @@ -1377,7 +1376,7 @@ var require_operators = __commonJS({ } if ( (options === null || options === void 0 ? void 0 : options.signal) != - null + null ) { validateAbortSignal(options.signal, "options.signal"); } @@ -1412,7 +1411,7 @@ var require_operators = __commonJS({ } if ( (options === null || options === void 0 ? void 0 : options.signal) != - null + null ) { validateAbortSignal(options.signal, "options.signal"); } @@ -1769,7 +1768,7 @@ var require_events = __commonJS({ if (typeof listener !== "function") { throw new TypeError( 'The "listener" argument must be of type Function. Received type ' + - typeof listener, + typeof listener, ); } } @@ -1782,7 +1781,7 @@ var require_events = __commonJS({ if (typeof arg !== "number" || arg < 0 || NumberIsNaN(arg)) { throw new RangeError( 'The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + - arg + ".", + arg + ".", ); } defaultMaxListeners = arg; @@ -1802,7 +1801,7 @@ var require_events = __commonJS({ if (typeof n !== "number" || n < 0 || NumberIsNaN(n)) { throw new RangeError( 'The value of "n" is out of range. It must be a non-negative number. Received ' + - n + ".", + n + ".", ); } this._maxListeners = n; @@ -1896,8 +1895,8 @@ var require_events = __commonJS({ existing.warned = true; var w = new Error( "Possible EventEmitter memory leak detected. " + existing.length + - " " + String(type) + - " listeners added. Use emitter.setMaxListeners() to increase limit", + " " + String(type) + + " listeners added. Use emitter.setMaxListeners() to increase limit", ); w.name = "MaxListenersExceededWarning"; w.emitter = target; @@ -2150,7 +2149,7 @@ var require_events = __commonJS({ } else { throw new TypeError( 'The "emitter" argument must be of type EventEmitter. Received type ' + - typeof emitter, + typeof emitter, ); } } @@ -2483,8 +2482,8 @@ var require_state = __commonJS({ return options.highWaterMark != null ? options.highWaterMark : isDuplex - ? options[duplexKey] - : null; + ? options[duplexKey] + : null; } function getDefaultHighWaterMark(objectMode) { return objectMode ? 16 : 16 * 1024; @@ -2637,7 +2636,7 @@ var require_from = __commonJS({ } } async function next() { - for (;;) { + for (; ;) { try { const { value, done } = isAsync ? await iterator.next() @@ -3464,8 +3463,8 @@ var require_readable = __commonJS({ if ( (error || (options === null || options === void 0 - ? void 0 - : options.destroyOnReturn) !== false) && + ? void 0 + : options.destroyOnReturn) !== false) && (error === void 0 || stream._readableState.autoDestroy) ) { destroyImpl.destroyer(stream, null); @@ -3648,8 +3647,8 @@ var require_readable = __commonJS({ } else if (state.autoDestroy) { const wState = stream._writableState; const autoDestroy = !wState || wState.autoDestroy && // We don't expect the writable to ever 'finish' - // if writable is explicitly set to false. - (wState.finished || wState.writable === false); + // if writable is explicitly set to false. + (wState.finished || wState.writable === false); if (autoDestroy) { stream.destroy(); } @@ -3690,9 +3689,9 @@ var require_readable = __commonJS({ return new Readable({ objectMode: (_ref = (_src$readableObjectMo = src.readableObjectMode) !== null && - _src$readableObjectMo !== void 0 - ? _src$readableObjectMo - : src.objectMode) !== null && _ref !== void 0 + _src$readableObjectMo !== void 0 + ? _src$readableObjectMo + : src.objectMode) !== null && _ref !== void 0 ? _ref : true, ...options, @@ -3754,7 +3753,14 @@ var require_writable = __commonJS({ this.destroyed = false; const noDecode = !!(options && options.decodeStrings === false); this.decodeStrings = !noDecode; - this.defaultEncoding = options && options.defaultEncoding || "utf8"; + const defaultEncoding = options?.defaultEncoding; + if (defaultEncoding == null) { + this.defaultEncoding = 'utf8'; + } else if (Buffer2.isEncoding(defaultEncoding)) { + this.defaultEncoding = defaultEncoding; + } else { + throw new ERR_UNKNOWN_ENCODING(defaultEncoding); + } this.length = 0; this.writing = false; this.corked = 0; @@ -3845,10 +3851,12 @@ var require_writable = __commonJS({ const state = stream._writableState; if (typeof encoding === "function") { cb = encoding; - encoding = state.defaultEncoding; + // Simulates https://github.com/nodejs/node/commit/dbed0319ac438dcbd6e92483f3280b1dc6767e00 + encoding = state.objectMode ? undefined : state.defaultEncoding; } else { if (!encoding) { - encoding = state.defaultEncoding; + // Simulates https://github.com/nodejs/node/commit/dbed0319ac438dcbd6e92483f3280b1dc6767e00 + encoding = state.objectMode ? undefined : state.defaultEncoding; } else if (encoding !== "buffer" && !Buffer2.isEncoding(encoding)) { throw new ERR_UNKNOWN_ENCODING(encoding); } @@ -4031,7 +4039,7 @@ var require_writable = __commonJS({ } while (count-- > 0) { state.pendingcb--; - cb(); + cb(null); } if (state.destroyed) { errorBuffer(state); @@ -4158,8 +4166,10 @@ var require_writable = __commonJS({ err = new ERR_STREAM_DESTROYED("end"); } if (typeof cb === "function") { - if (err || state.finished) { + if (err) { process.nextTick(cb, err); + } else if (state.finished) { + process.nextTick(cb, null); } else { state[kOnFinished].push(cb); } @@ -4246,14 +4256,14 @@ var require_writable = __commonJS({ state.finished = true; const onfinishCallbacks = state[kOnFinished].splice(0); for (let i = 0; i < onfinishCallbacks.length; i++) { - onfinishCallbacks[i](); + onfinishCallbacks[i](null); } stream.emit("finish"); if (state.autoDestroy) { const rState = stream._readableState; const autoDestroy = !rState || rState.autoDestroy && // We don't expect the readable to ever 'end' - // if readable is explicitly set to false. - (rState.endEmitted || rState.readable === false); + // if readable is explicitly set to false. + (rState.endEmitted || rState.readable === false); if (autoDestroy) { stream.destroy(); } @@ -4534,21 +4544,21 @@ var require_duplexify = __commonJS({ } if ( typeof (body === null || body === void 0 ? void 0 : body.writable) === - "object" || + "object" || typeof (body === null || body === void 0 ? void 0 : body.readable) === - "object" + "object" ) { const readable = body !== null && body !== void 0 && body.readable ? isReadableNodeStream( - body === null || body === void 0 ? void 0 : body.readable, - ) + body === null || body === void 0 ? void 0 : body.readable, + ) ? body === null || body === void 0 ? void 0 : body.readable : duplexify(body.readable) : void 0; const writable = body !== null && body !== void 0 && body.writable ? isWritableNodeStream( - body === null || body === void 0 ? void 0 : body.writable, - ) + body === null || body === void 0 ? void 0 : body.writable, + ) ? body === null || body === void 0 ? void 0 : body.writable : duplexify(body.writable) : void 0; @@ -6277,7 +6287,7 @@ function newReadableStreamFromStreamReadable( cleanup(); // This is a protection against non-standard, legacy streams // that happen to emit an error event again after finished is called. - streamReadable.on("error", () => {}); + streamReadable.on("error", () => { }); if (error) { return controller.error(error); } @@ -6344,7 +6354,7 @@ function newWritableStreamFromStreamWritable(streamWritable) { cleanup(); // This is a protection against non-standard, legacy streams // that happen to emit an error event again after finished is called. - streamWritable.on("error", () => {}); + streamWritable.on("error", () => { }); if (error != null) { if (backpressurePromise !== undefined) { backpressurePromise.reject(error); diff --git a/crates/node/polyfills/_utils.ts b/crates/node/polyfills/_utils.ts index 4a52f4441..88f7c19e7 100644 --- a/crates/node/polyfills/_utils.ts +++ b/crates/node/polyfills/_utils.ts @@ -1,8 +1,19 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - +import { primordials } from "ext:core/mod.js"; +const { + Error, + PromisePrototypeThen, + ArrayPrototypePop, + StringPrototypeToLowerCase, + NumberIsInteger, + ObjectGetOwnPropertyNames, + ReflectGetOwnPropertyDescriptor, + ObjectDefineProperty, + NumberIsSafeInteger, + FunctionPrototypeApply, + SafeArrayIterator, +} = primordials; import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js"; import { errorMap } from "ext:deno_node/internal_binding/uv.ts"; import { codes } from "ext:deno_node/internal/error_codes.ts"; @@ -53,9 +64,10 @@ export function intoCallbackAPI( // deno-lint-ignore no-explicit-any ...args: any[] ) { - func(...args).then( - (value) => cb && cb(null, value), - (err) => cb && cb(err), + PromisePrototypeThen( + func(...new SafeArrayIterator(args)), + (value: T) => cb && cb(null, value), + (err: MaybeNull) => cb && cb(err), ); } @@ -67,15 +79,18 @@ export function intoCallbackAPIWithIntercept( // deno-lint-ignore no-explicit-any ...args: any[] ) { - func(...args).then( - (value) => cb && cb(null, interceptor(value)), - (err) => cb && cb(err), + PromisePrototypeThen( + func(...new SafeArrayIterator(args)), + (value: T1) => cb && cb(null, interceptor(value)), + (err: MaybeNull) => cb && cb(err), ); } export function spliceOne(list: string[], index: number) { - for (; index + 1 < list.length; index++) list[index] = list[index + 1]; - list.pop(); + for (; index + 1 < list.length; index++) { + list[index] = list[index + 1]; + } + ArrayPrototypePop(list); } // Taken from: https://github.com/nodejs/node/blob/ba684805b6c0eded76e5cd89ee00328ac7a59365/lib/internal/util.js#L125 @@ -95,12 +110,15 @@ function slowCases(enc: string): TextEncodings | undefined { case 4: if (enc === "UTF8") return "utf8"; if (enc === "ucs2" || enc === "UCS2") return "utf16le"; - enc = `${enc}`.toLowerCase(); + enc = StringPrototypeToLowerCase(enc); if (enc === "utf8") return "utf8"; if (enc === "ucs2") return "utf16le"; break; case 3: - if (enc === "hex" || enc === "HEX" || `${enc}`.toLowerCase() === "hex") { + if ( + enc === "hex" || enc === "HEX" || + StringPrototypeToLowerCase(enc) === "hex" + ) { return "hex"; } break; @@ -110,7 +128,7 @@ function slowCases(enc: string): TextEncodings | undefined { if (enc === "UTF-8") return "utf8"; if (enc === "ASCII") return "ascii"; if (enc === "UCS-2") return "utf16le"; - enc = `${enc}`.toLowerCase(); + enc = StringPrototypeToLowerCase(enc); if (enc === "utf-8") return "utf8"; if (enc === "ascii") return "ascii"; if (enc === "ucs-2") return "utf16le"; @@ -120,7 +138,7 @@ function slowCases(enc: string): TextEncodings | undefined { if (enc === "latin1" || enc === "binary") return "latin1"; if (enc === "BASE64") return "base64"; if (enc === "LATIN1" || enc === "BINARY") return "latin1"; - enc = `${enc}`.toLowerCase(); + enc = StringPrototypeToLowerCase(enc); if (enc === "base64") return "base64"; if (enc === "latin1" || enc === "binary") return "latin1"; break; @@ -128,7 +146,7 @@ function slowCases(enc: string): TextEncodings | undefined { if ( enc === "utf16le" || enc === "UTF16LE" || - `${enc}`.toLowerCase() === "utf16le" + StringPrototypeToLowerCase(enc) === "utf16le" ) { return "utf16le"; } @@ -137,7 +155,7 @@ function slowCases(enc: string): TextEncodings | undefined { if ( enc === "utf-16le" || enc === "UTF-16LE" || - `${enc}`.toLowerCase() === "utf-16le" + StringPrototypeToLowerCase(enc) === "utf-16le" ) { return "utf16le"; } @@ -154,7 +172,7 @@ export function validateIntegerRange( max = 2147483647, ) { // The defaults for min and max correspond to the limits of 32-bit integers. - if (!Number.isInteger(value)) { + if (!NumberIsInteger(value)) { throw new Error(`${name} must be 'an integer' but was ${value}`); } @@ -175,26 +193,26 @@ export function once( return function (this: unknown, ...args: OptionalSpread) { if (called) return; called = true; - callback.apply(this, args); + FunctionPrototypeApply(callback, this, args); }; } -export function makeMethodsEnumerable(klass: { new (): unknown }) { +export function makeMethodsEnumerable(klass: { new(): unknown }) { const proto = klass.prototype; - for (const key of Object.getOwnPropertyNames(proto)) { + const names = ObjectGetOwnPropertyNames(proto); + for (let i = 0; i < names.length; i++) { + const key = names[i]; const value = proto[key]; if (typeof value === "function") { - const desc = Reflect.getOwnPropertyDescriptor(proto, key); + const desc = ReflectGetOwnPropertyDescriptor(proto, key); if (desc) { desc.enumerable = true; - Object.defineProperty(proto, key, desc); + ObjectDefineProperty(proto, key, desc); } } } } -const NumberIsSafeInteger = Number.isSafeInteger; - /** * Returns a system error name from an error code number. * @param code error code number diff --git a/crates/node/polyfills/_zlib_binding.mjs b/crates/node/polyfills/_zlib_binding.mjs index 729af9b35..118f9edc4 100644 --- a/crates/node/polyfills/_zlib_binding.mjs +++ b/crates/node/polyfills/_zlib_binding.mjs @@ -50,8 +50,8 @@ import { op_zlib_new, op_zlib_reset, op_zlib_write, - op_zlib_write_async, } from "ext:core/ops"; +import process from "node:process"; const writeResult = new Uint32Array(2); @@ -124,18 +124,20 @@ class Zlib { out_off, out_len, ) { - op_zlib_write_async( - this.#handle, - flush ?? Z_NO_FLUSH, - input, - in_off, - in_len, - out, - out_off, - out_len, - ).then(([err, availOut, availIn]) => { - if (this.#checkError(err)) { - this.callback(availIn, availOut); + process.nextTick(() => { + const res = this.writeSync( + flush ?? Z_NO_FLUSH, + input, + in_off, + in_len, + out, + out_off, + out_len, + ); + + if (res) { + const [availOut, availIn] = res; + this.callback(availOut, availIn); } }); diff --git a/crates/node/polyfills/assert.ts b/crates/node/polyfills/assert.ts index 3d7660487..3f8cea30d 100644 --- a/crates/node/polyfills/assert.ts +++ b/crates/node/polyfills/assert.ts @@ -17,6 +17,9 @@ import { ERR_MISSING_ARGS, } from "ext:deno_node/internal/errors.ts"; import { isDeepEqual } from "ext:deno_node/internal/util/comparisons.ts"; +import { primordials } from "ext:core/mod.js"; + +const { ObjectPrototypeIsPrototypeOf } = primordials; function innerFail(obj: { actual?: unknown; @@ -435,8 +438,7 @@ function doesNotMatch( } throw new AssertionError({ message: message || - `The "string" argument must be of type string. Received type ${typeof string} (${ - inspect(string) + `The "string" argument must be of type string. Received type ${typeof string} (${inspect(string) })`, actual: string, expected: regexp, @@ -582,7 +584,7 @@ function doesNotReject( } return promise.then( - () => {}, + () => { }, (e) => gotUnwantedException(e, error, message, doesNotReject), ); } @@ -633,16 +635,14 @@ function gotUnwantedException( } else { if (message) { throw new AssertionError({ - message: `Got unwanted exception: ${message}\nActual message: "${ - e ? e.message : String(e) - }"`, + message: `Got unwanted exception: ${message}\nActual message: "${e ? e.message : String(e) + }"`, operator: operator.name, }); } throw new AssertionError({ - message: `Got unwanted exception.\nActual message: "${ - e ? e.message : String(e) - }"`, + message: `Got unwanted exception.\nActual message: "${e ? e.message : String(e) + }"`, operator: operator.name, }); } @@ -744,8 +744,8 @@ function validateThrownError( error = undefined; } if ( - error instanceof Function && error.prototype !== undefined && - error.prototype instanceof Error + typeof error === "function" && + (error === Error || ObjectPrototypeIsPrototypeOf(Error, error)) ) { // error is a constructor if (e instanceof error) { @@ -766,13 +766,11 @@ function validateThrownError( return true; } throw createAssertionError({ - message: `The ${ - options.validationFunctionName + message: `The ${options.validationFunctionName ? `"${options.validationFunctionName}" validation` : "validation" - } function is expected to return "true". Received ${ - inspect(received) - }\n\nCaught error:\n\n${e}`, + } function is expected to return "true". Received ${inspect(received) + }\n\nCaught error:\n\n${e}`, actual: e, expected: error, operator: options.operator.name, @@ -785,8 +783,7 @@ function validateThrownError( } throw createAssertionError({ message: - `The input did not match the regular expression ${error.toString()}. Input:\n\n'${ - String(e) + `The input did not match the regular expression ${error.toString()}. Input:\n\n'${String(e) }'\n`, actual: e, expected: error, diff --git a/crates/node/polyfills/async_hooks.ts b/crates/node/polyfills/async_hooks.ts index e8960c4dc..aeac3191a 100644 --- a/crates/node/polyfills/async_hooks.ts +++ b/crates/node/polyfills/async_hooks.ts @@ -207,6 +207,8 @@ export class AsyncResource { } } + emitDestroy() { } + bind(fn: (...args: unknown[]) => unknown, thisArg = this) { validateFunction(fn, "fn"); const frame = AsyncContextFrame.current(); diff --git a/crates/node/polyfills/buffer.ts b/crates/node/polyfills/buffer.ts index 5925475c4..c5a910cb4 100644 --- a/crates/node/polyfills/buffer.ts +++ b/crates/node/polyfills/buffer.ts @@ -7,6 +7,8 @@ export { Buffer, constants, default, + isAscii, + isUtf8, kMaxLength, kStringMaxLength, SlowBuffer, diff --git a/crates/node/polyfills/constants.ts b/crates/node/polyfills/constants.ts index 5ea078dbd..691621edc 100644 --- a/crates/node/polyfills/constants.ts +++ b/crates/node/polyfills/constants.ts @@ -4,6 +4,8 @@ import { constants as fsConstants } from "node:fs"; import { constants as osConstants } from "node:os"; +import { constants as cryptoConstants } from "node:crypto"; +import { constants as zlibConstants } from "node:zlib"; export default { ...fsConstants, @@ -11,6 +13,8 @@ export default { ...osConstants.errno, ...osConstants.signals, ...osConstants.priority, + ...cryptoConstants, + ...zlibConstants, }; export const { @@ -180,3 +184,187 @@ export const { SIGXCPU, SIGXFSZ, } = osConstants.signals; +export const { + OPENSSL_VERSION_NUMBER, + SSL_OP_ALL, + SSL_OP_ALLOW_NO_DHE_KEX, + SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION, + SSL_OP_CIPHER_SERVER_PREFERENCE, + SSL_OP_CISCO_ANYCONNECT, + SSL_OP_COOKIE_EXCHANGE, + SSL_OP_CRYPTOPRO_TLSEXT_BUG, + SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS, + SSL_OP_EPHEMERAL_RSA, + SSL_OP_LEGACY_SERVER_CONNECT, + SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER, + SSL_OP_MICROSOFT_SESS_ID_BUG, + SSL_OP_MSIE_SSLV2_RSA_PADDING, + SSL_OP_NETSCAPE_CA_DN_BUG, + SSL_OP_NETSCAPE_CHALLENGE_BUG, + SSL_OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG, + SSL_OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG, + SSL_OP_NO_COMPRESSION, + SSL_OP_NO_ENCRYPT_THEN_MAC, + SSL_OP_NO_QUERY_MTU, + SSL_OP_NO_RENEGOTIATION, + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION, + SSL_OP_NO_SSLv2, + SSL_OP_NO_SSLv3, + SSL_OP_NO_TICKET, + SSL_OP_NO_TLSv1, + SSL_OP_NO_TLSv1_1, + SSL_OP_NO_TLSv1_2, + SSL_OP_NO_TLSv1_3, + SSL_OP_PKCS1_CHECK_1, + SSL_OP_PKCS1_CHECK_2, + SSL_OP_PRIORITIZE_CHACHA, + SSL_OP_SINGLE_DH_USE, + SSL_OP_SINGLE_ECDH_USE, + SSL_OP_SSLEAY_080_CLIENT_DH_BUG, + SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG, + SSL_OP_TLS_BLOCK_PADDING_BUG, + SSL_OP_TLS_D5_BUG, + SSL_OP_TLS_ROLLBACK_BUG, + ENGINE_METHOD_RSA, + ENGINE_METHOD_DSA, + ENGINE_METHOD_DH, + ENGINE_METHOD_RAND, + ENGINE_METHOD_EC, + ENGINE_METHOD_CIPHERS, + ENGINE_METHOD_DIGESTS, + ENGINE_METHOD_PKEY_METHS, + ENGINE_METHOD_PKEY_ASN1_METHS, + ENGINE_METHOD_ALL, + ENGINE_METHOD_NONE, + DH_CHECK_P_NOT_SAFE_PRIME, + DH_CHECK_P_NOT_PRIME, + DH_UNABLE_TO_CHECK_GENERATOR, + DH_NOT_SUITABLE_GENERATOR, + ALPN_ENABLED, + RSA_PKCS1_PADDING, + RSA_SSLV23_PADDING, + RSA_NO_PADDING, + RSA_PKCS1_OAEP_PADDING, + RSA_X931_PADDING, + RSA_PKCS1_PSS_PADDING, + RSA_PSS_SALTLEN_DIGEST, + RSA_PSS_SALTLEN_MAX_SIGN, + RSA_PSS_SALTLEN_AUTO, + defaultCoreCipherList, + TLS1_VERSION, + TLS1_1_VERSION, + TLS1_2_VERSION, + TLS1_3_VERSION, + POINT_CONVERSION_COMPRESSED, + POINT_CONVERSION_UNCOMPRESSED, + POINT_CONVERSION_HYBRID, +} = cryptoConstants; +export const { + Z_NO_FLUSH, + Z_PARTIAL_FLUSH, + Z_SYNC_FLUSH, + Z_FULL_FLUSH, + Z_FINISH, + Z_BLOCK, + Z_OK, + Z_STREAM_END, + Z_NEED_DICT, + Z_ERRNO, + Z_STREAM_ERROR, + Z_DATA_ERROR, + Z_MEM_ERROR, + Z_BUF_ERROR, + Z_VERSION_ERROR, + Z_NO_COMPRESSION, + Z_BEST_SPEED, + Z_BEST_COMPRESSION, + Z_DEFAULT_COMPRESSION, + Z_FILTERED, + Z_HUFFMAN_ONLY, + Z_RLE, + Z_FIXED, + Z_DEFAULT_STRATEGY, + ZLIB_VERNUM, + DEFLATE, + INFLATE, + GZIP, + GUNZIP, + DEFLATERAW, + INFLATERAW, + UNZIP, + BROTLI_DECODE, + BROTLI_ENCODE, + Z_MIN_WINDOWBITS, + Z_MAX_WINDOWBITS, + Z_DEFAULT_WINDOWBITS, + Z_MIN_CHUNK, + Z_MAX_CHUNK, + Z_DEFAULT_CHUNK, + Z_MIN_MEMLEVEL, + Z_MAX_MEMLEVEL, + Z_DEFAULT_MEMLEVEL, + Z_MIN_LEVEL, + Z_MAX_LEVEL, + Z_DEFAULT_LEVEL, + BROTLI_OPERATION_PROCESS, + BROTLI_OPERATION_FLUSH, + BROTLI_OPERATION_FINISH, + BROTLI_OPERATION_EMIT_METADATA, + BROTLI_PARAM_MODE, + BROTLI_MODE_GENERIC, + BROTLI_MODE_TEXT, + BROTLI_MODE_FONT, + BROTLI_DEFAULT_MODE, + BROTLI_PARAM_QUALITY, + BROTLI_MIN_QUALITY, + BROTLI_MAX_QUALITY, + BROTLI_DEFAULT_QUALITY, + BROTLI_PARAM_LGWIN, + BROTLI_MIN_WINDOW_BITS, + BROTLI_MAX_WINDOW_BITS, + BROTLI_LARGE_MAX_WINDOW_BITS, + BROTLI_DEFAULT_WINDOW, + BROTLI_PARAM_LGBLOCK, + BROTLI_MIN_INPUT_BLOCK_BITS, + BROTLI_MAX_INPUT_BLOCK_BITS, + BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING, + BROTLI_PARAM_SIZE_HINT, + BROTLI_PARAM_LARGE_WINDOW, + BROTLI_PARAM_NPOSTFIX, + BROTLI_PARAM_NDIRECT, + BROTLI_DECODER_RESULT_ERROR, + BROTLI_DECODER_RESULT_SUCCESS, + BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT, + BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT, + BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION, + BROTLI_DECODER_PARAM_LARGE_WINDOW, + BROTLI_DECODER_NO_ERROR, + BROTLI_DECODER_SUCCESS, + BROTLI_DECODER_NEEDS_MORE_INPUT, + BROTLI_DECODER_NEEDS_MORE_OUTPUT, + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE, + BROTLI_DECODER_ERROR_FORMAT_RESERVED, + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE, + BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET, + BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME, + BROTLI_DECODER_ERROR_FORMAT_CL_SPACE, + BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE, + BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT, + BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1, + BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2, + BROTLI_DECODER_ERROR_FORMAT_TRANSFORM, + BROTLI_DECODER_ERROR_FORMAT_DICTIONARY, + BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS, + BROTLI_DECODER_ERROR_FORMAT_PADDING_1, + BROTLI_DECODER_ERROR_FORMAT_PADDING_2, + BROTLI_DECODER_ERROR_FORMAT_DISTANCE, + BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET, + BROTLI_DECODER_ERROR_INVALID_ARGUMENTS, + BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES, + BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS, + BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP, + BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1, + BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2, + BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES, + BROTLI_DECODER_ERROR_UNREACHABLE, +} = zlibConstants; diff --git a/crates/node/polyfills/diagnostics_channel.js b/crates/node/polyfills/diagnostics_channel.js new file mode 100644 index 000000000..72b7b09d3 --- /dev/null +++ b/crates/node/polyfills/diagnostics_channel.js @@ -0,0 +1,430 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +// TODO(petamoriken): enable prefer-primordials for node polyfills +// deno-lint-ignore-file prefer-primordials ban-untagged-todo + +import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; +import { validateFunction } from "ext:deno_node/internal/validators.mjs"; +import { nextTick } from "node:process"; +import { primordials } from "ext:core/mod.js"; + +const { + ArrayPrototypeAt, + ArrayPrototypeIndexOf, + ArrayPrototypePush, + ArrayPrototypeSplice, + ObjectDefineProperty, + ObjectGetPrototypeOf, + ObjectSetPrototypeOf, + Promise, + PromisePrototypeThen, + PromiseReject, + PromiseResolve, + ReflectApply, + SafeFinalizationRegistry, + SafeMap, + SymbolHasInstance, +} = primordials; +import { WeakReference } from "ext:deno_node/internal/util.mjs"; + +// Can't delete when weakref count reaches 0 as it could increment again. +// Only GC can be used as a valid time to clean up the channels map. +class WeakRefMap extends SafeMap { + #finalizers = new SafeFinalizationRegistry((key) => { + this.delete(key); + }); + + set(key, value) { + this.#finalizers.register(value, key); + return super.set(key, new WeakReference(value)); + } + + get(key) { + return super.get(key)?.get(); + } + + incRef(key) { + return super.get(key)?.incRef(); + } + + decRef(key) { + return super.get(key)?.decRef(); + } +} + +function markActive(channel) { + ObjectSetPrototypeOf(channel, ActiveChannel.prototype); + channel._subscribers = []; + channel._stores = new SafeMap(); +} + +function maybeMarkInactive(channel) { + // When there are no more active subscribers or bound, restore to fast prototype. + if (!channel._subscribers.length && !channel._stores.size) { + ObjectSetPrototypeOf(channel, Channel.prototype); + channel._subscribers = undefined; + channel._stores = undefined; + } +} + +function defaultTransform(data) { + return data; +} + +function wrapStoreRun(store, data, next, transform = defaultTransform) { + return () => { + let context; + try { + context = transform(data); + } catch (err) { + nextTick(() => { + // TODO(bartlomieju): in Node.js this is using `triggerUncaughtException` API, need + // to clarify if we need that or if just throwing the error is enough here. + throw err; + // triggerUncaughtException(err, false); + }); + return next(); + } + + return store.run(context, next); + }; +} + +class ActiveChannel { + subscribe(subscription) { + validateFunction(subscription, "subscription"); + ArrayPrototypePush(this._subscribers, subscription); + channels.incRef(this.name); + } + + unsubscribe(subscription) { + const index = ArrayPrototypeIndexOf(this._subscribers, subscription); + if (index === -1) return false; + + ArrayPrototypeSplice(this._subscribers, index, 1); + + channels.decRef(this.name); + maybeMarkInactive(this); + + return true; + } + + bindStore(store, transform) { + const replacing = this._stores.has(store); + if (!replacing) channels.incRef(this.name); + this._stores.set(store, transform); + } + + unbindStore(store) { + if (!this._stores.has(store)) { + return false; + } + + this._stores.delete(store); + + channels.decRef(this.name); + maybeMarkInactive(this); + + return true; + } + + get hasSubscribers() { + return true; + } + + publish(data) { + for (let i = 0; i < (this._subscribers?.length || 0); i++) { + try { + const onMessage = this._subscribers[i]; + onMessage(data, this.name); + } catch (err) { + nextTick(() => { + // TODO(bartlomieju): in Node.js this is using `triggerUncaughtException` API, need + // to clarify if we need that or if just throwing the error is enough here. + throw err; + // triggerUncaughtException(err, false); + }); + } + } + } + + runStores(data, fn, thisArg, ...args) { + let run = () => { + this.publish(data); + return ReflectApply(fn, thisArg, args); + }; + + for (const entry of this._stores.entries()) { + const store = entry[0]; + const transform = entry[1]; + run = wrapStoreRun(store, data, run, transform); + } + + return run(); + } +} + +class Channel { + constructor(name) { + this._subscribers = undefined; + this._stores = undefined; + this.name = name; + + channels.set(name, this); + } + + static [SymbolHasInstance](instance) { + const prototype = ObjectGetPrototypeOf(instance); + return prototype === Channel.prototype || + prototype === ActiveChannel.prototype; + } + + subscribe(subscription) { + markActive(this); + this.subscribe(subscription); + } + + unsubscribe() { + return false; + } + + bindStore(store, transform) { + markActive(this); + this.bindStore(store, transform); + } + + unbindStore() { + return false; + } + + get hasSubscribers() { + return false; + } + + publish() { } + + runStores(_data, fn, thisArg, ...args) { + return ReflectApply(fn, thisArg, args); + } +} + +const channels = new WeakRefMap(); + +export function channel(name) { + const channel = channels.get(name); + if (channel) return channel; + + if (typeof name !== "string" && typeof name !== "symbol") { + throw new ERR_INVALID_ARG_TYPE("channel", ["string", "symbol"], name); + } + + return new Channel(name); +} + +export function subscribe(name, subscription) { + return channel(name).subscribe(subscription); +} + +export function unsubscribe(name, subscription) { + return channel(name).unsubscribe(subscription); +} + +export function hasSubscribers(name) { + const channel = channels.get(name); + if (!channel) return false; + + return channel.hasSubscribers; +} + +const traceEvents = [ + "start", + "end", + "asyncStart", + "asyncEnd", + "error", +]; + +function assertChannel(value, name) { + if (!(value instanceof Channel)) { + throw new ERR_INVALID_ARG_TYPE(name, ["Channel"], value); + } +} + +function tracingChannelFrom(nameOrChannels, name) { + if (typeof nameOrChannels === "string") { + return channel(`tracing:${nameOrChannels}:${name}`); + } + + if (typeof nameOrChannels === "object" && nameOrChannels !== null) { + const channel = nameOrChannels[name]; + assertChannel(channel, `nameOrChannels.${name}`); + return channel; + } + + throw new ERR_INVALID_ARG_TYPE("nameOrChannels", [ + "string", + "object", + "Channel", + ], nameOrChannels); +} + +class TracingChannel { + constructor(nameOrChannels) { + for (const eventName of traceEvents) { + ObjectDefineProperty(this, eventName, { + __proto__: null, + value: tracingChannelFrom(nameOrChannels, eventName), + }); + } + } + + get hasSubscribers() { + return this.start.hasSubscribers || + this.end.hasSubscribers || + this.asyncStart.hasSubscribers || + this.asyncEnd.hasSubscribers || + this.error.hasSubscribers; + } + + subscribe(handlers) { + for (const name of traceEvents) { + if (!handlers[name]) continue; + + this[name]?.subscribe(handlers[name]); + } + } + + unsubscribe(handlers) { + let done = true; + + for (const name of traceEvents) { + if (!handlers[name]) continue; + + if (!this[name]?.unsubscribe(handlers[name])) { + done = false; + } + } + + return done; + } + + traceSync(fn, context = {}, thisArg, ...args) { + if (!this.hasSubscribers) { + return ReflectApply(fn, thisArg, args); + } + + const { start, end, error } = this; + + return start.runStores(context, () => { + try { + const result = ReflectApply(fn, thisArg, args); + context.result = result; + return result; + } catch (err) { + context.error = err; + error.publish(context); + throw err; + } finally { + end.publish(context); + } + }); + } + + tracePromise(fn, context = {}, thisArg, ...args) { + if (!this.hasSubscribers) { + return ReflectApply(fn, thisArg, args); + } + + const { start, end, asyncStart, asyncEnd, error } = this; + + function reject(err) { + context.error = err; + error.publish(context); + asyncStart.publish(context); + // TODO: Is there a way to have asyncEnd _after_ the continuation? + asyncEnd.publish(context); + return PromiseReject(err); + } + + function resolve(result) { + context.result = result; + asyncStart.publish(context); + // TODO: Is there a way to have asyncEnd _after_ the continuation? + asyncEnd.publish(context); + return result; + } + + return start.runStores(context, () => { + try { + let promise = ReflectApply(fn, thisArg, args); + // Convert thenables to native promises + if (!(promise instanceof Promise)) { + promise = PromiseResolve(promise); + } + return PromisePrototypeThen(promise, resolve, reject); + } catch (err) { + context.error = err; + error.publish(context); + throw err; + } finally { + end.publish(context); + } + }); + } + + traceCallback(fn, position = -1, context = {}, thisArg, ...args) { + if (!this.hasSubscribers) { + return ReflectApply(fn, thisArg, args); + } + + const { start, end, asyncStart, asyncEnd, error } = this; + + function wrappedCallback(err, res) { + if (err) { + context.error = err; + error.publish(context); + } else { + context.result = res; + } + + // Using runStores here enables manual context failure recovery + asyncStart.runStores(context, () => { + try { + return ReflectApply(callback, this, arguments); + } finally { + asyncEnd.publish(context); + } + }); + } + + const callback = ArrayPrototypeAt(args, position); + validateFunction(callback, "callback"); + ArrayPrototypeSplice(args, position, 1, wrappedCallback); + + return start.runStores(context, () => { + try { + return ReflectApply(fn, thisArg, args); + } catch (err) { + context.error = err; + error.publish(context); + throw err; + } finally { + end.publish(context); + } + }); + } +} + +export function tracingChannel(nameOrChannels) { + return new TracingChannel(nameOrChannels); +} + +export default { + channel, + hasSubscribers, + subscribe, + tracingChannel, + unsubscribe, + Channel, +}; diff --git a/crates/node/polyfills/diagnostics_channel.ts b/crates/node/polyfills/diagnostics_channel.ts deleted file mode 100644 index 4f54c3431..000000000 --- a/crates/node/polyfills/diagnostics_channel.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - -import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; -import { validateFunction } from "ext:deno_node/internal/validators.mjs"; -import { nextTick } from "node:process"; - -type Subscriber = (message: unknown, name?: string) => void; - -export class Channel { - _subscribers: Subscriber[]; - name: string; - constructor(name: string) { - this._subscribers = []; - this.name = name; - } - - publish(message: unknown) { - for (const subscriber of this._subscribers) { - try { - subscriber(message, this.name); - } catch (err) { - nextTick(() => { - throw err; - }); - } - } - } - - subscribe(subscription: Subscriber) { - validateFunction(subscription, "subscription"); - - this._subscribers.push(subscription); - } - - unsubscribe(subscription: Subscriber) { - if (!this._subscribers.includes(subscription)) { - return false; - } - - this._subscribers.splice(this._subscribers.indexOf(subscription), 1); - - return true; - } - - get hasSubscribers() { - return this._subscribers.length > 0; - } -} - -const channels: Record = {}; - -export function channel(name: string) { - if (typeof name !== "string" && typeof name !== "symbol") { - throw new ERR_INVALID_ARG_TYPE("channel", ["string", "symbol"], name); - } - - if (!Object.hasOwn(channels, name)) { - channels[name] = new Channel(name); - } - - return channels[name]; -} - -export function hasSubscribers(name: string) { - if (!Object.hasOwn(channels, name)) { - return false; - } - - return channels[name].hasSubscribers; -} - -export function subscribe(name: string, subscription: Subscriber) { - const c = channel(name); - - return c.subscribe(subscription); -} - -export function unsubscribe(name: string, subscription: Subscriber) { - const c = channel(name); - - return c.unsubscribe(subscription); -} - -export default { - channel, - hasSubscribers, - subscribe, - unsubscribe, - Channel, -}; diff --git a/crates/node/polyfills/fs.ts b/crates/node/polyfills/fs.ts index f788f72b5..7a3cf4e67 100644 --- a/crates/node/polyfills/fs.ts +++ b/crates/node/polyfills/fs.ts @@ -27,8 +27,18 @@ import { fstat, fstatSync } from "ext:deno_node/_fs/_fs_fstat.ts"; import { fsync, fsyncSync } from "ext:deno_node/_fs/_fs_fsync.ts"; import { ftruncate, ftruncateSync } from "ext:deno_node/_fs/_fs_ftruncate.ts"; import { futimes, futimesSync } from "ext:deno_node/_fs/_fs_futimes.ts"; +import { + lchown, + lchownPromise, + lchownSync, +} from "ext:deno_node/_fs/_fs_lchown.ts"; import { link, linkPromise, linkSync } from "ext:deno_node/_fs/_fs_link.ts"; import { lstat, lstatPromise, lstatSync } from "ext:deno_node/_fs/_fs_lstat.ts"; +import { + lutimes, + lutimesPromise, + lutimesSync, +} from "ext:deno_node/_fs/_fs_lutimes.ts"; import { mkdir, mkdirPromise, mkdirSync } from "ext:deno_node/_fs/_fs_mkdir.ts"; import { mkdtemp, @@ -123,6 +133,7 @@ import { ReadStream, WriteStream, } from "ext:deno_node/internal/fs/streams.mjs"; +import { toUnixTimestamp as _toUnixTimestamp } from "ext:deno_node/internal/fs/utils.mjs"; const { F_OK, @@ -167,10 +178,10 @@ const promises = { unlink: unlinkPromise, chmod: chmodPromise, // lchmod: promisify(lchmod), - // lchown: promisify(lchown), + lchown: lchownPromise, chown: chownPromise, utimes: utimesPromise, - // lutimes = promisify(lutimes), + lutimes: lutimesPromise, realpath: realpathPromise, mkdtemp: mkdtempPromise, writeFile: writeFilePromise, @@ -212,10 +223,14 @@ export default { ftruncateSync, futimes, futimesSync, + lchown, + lchownSync, link, linkSync, lstat, lstatSync, + lutimes, + lutimesSync, mkdir, mkdirSync, mkdtemp, @@ -284,9 +299,13 @@ export default { WriteStream, writeSync, X_OK, + // For tests + _toUnixTimestamp, }; export { + // For tests + _toUnixTimestamp, access, accessSync, appendFile, @@ -323,6 +342,8 @@ export { linkSync, lstat, lstatSync, + lutimes, + lutimesSync, mkdir, mkdirSync, mkdtemp, diff --git a/crates/node/polyfills/http.ts b/crates/node/polyfills/http.ts index 61cc6d030..cc06163e0 100644 --- a/crates/node/polyfills/http.ts +++ b/crates/node/polyfills/http.ts @@ -3,8 +3,7 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials -import { core, internals } from "ext:core/mod.js"; -import { createDeferredPromise } from "ext:deno_node/internal/util.mjs"; +import { core, internals, primordials } from "ext:core/mod.js"; import { op_fetch_response_upgrade, op_fetch_send, @@ -40,6 +39,7 @@ import { OutgoingMessage, parseUniqueHeadersOption, validateHeaderName, + validateHeaderValue, } from "ext:deno_node/_http_outgoing.ts"; import { ok as assert } from "node:assert"; import { kOutHeaders } from "ext:deno_node/internal/http.ts"; @@ -53,12 +53,14 @@ import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; import { connResetException, ERR_HTTP_HEADERS_SENT, + ERR_HTTP_SOCKET_ASSIGNED, ERR_INVALID_ARG_TYPE, ERR_INVALID_HTTP_TOKEN, ERR_INVALID_PROTOCOL, ERR_UNESCAPED_CHARACTERS, } from "ext:deno_node/internal/errors.ts"; import { getTimerDuration } from "ext:deno_node/internal/timers.mjs"; +// import { serve, upgradeHttpRaw } from "ext:deno_http/00_serve.ts"; import { createHttpClient } from "ext:deno_fetch/22_http_client.js"; import { headersEntries } from "ext:deno_fetch/20_headers.js"; import { timerId } from "ext:deno_web/03_abort_signal.js"; @@ -67,6 +69,7 @@ import { resourceForReadableStream } from "ext:deno_web/06_streams.js"; import { TcpConn } from "ext:deno_net/01_net.js"; const { internalRidSymbol } = core; +const { ArrayIsArray } = primordials; enum STATUS_CODES { /** RFC 7231, 6.2.1 */ @@ -284,11 +287,11 @@ const kUniqueHeaders = Symbol("kUniqueHeaders"); class FakeSocket extends EventEmitter { constructor( - opts: { - encrypted?: boolean | undefined; - remotePort?: number | undefined; - remoteAddress?: string | undefined; - } = {}, + opts: { + encrypted?: boolean | undefined; + remotePort?: number | undefined; + remoteAddress?: string | undefined; + } = {}, ) { super(); this.remoteAddress = opts.remoteAddress; @@ -320,11 +323,12 @@ class ClientRequest extends OutgoingMessage { insecureHTTPParser: boolean; useChunkedEncodingByDefault: boolean; path: string; + _req: { requestRid: number; cancelHandleRid: number | null } | undefined; constructor( - input: string | URL, - options?: RequestOptions, - cb?: (res: IncomingMessageForClient) => void, + input: string | URL, + options?: RequestOptions, + cb?: (res: IncomingMessageForClient) => void, ) { super(); @@ -387,7 +391,7 @@ class ClientRequest extends OutgoingMessage { const port = options!.port = options!.port || defaultPort || 80; const host = options!.host = validateHost(options!.hostname, "hostname") || - validateHost(options!.host, "host") || "localhost"; + validateHost(options!.host, "host") || "localhost"; const setHost = options!.setHost === undefined || Boolean(options!.setHost); @@ -431,8 +435,8 @@ class ClientRequest extends OutgoingMessage { if (options!.joinDuplicateHeaders !== undefined) { validateBoolean( - options!.joinDuplicateHeaders, - "options.joinDuplicateHeaders", + options!.joinDuplicateHeaders, + "options.joinDuplicateHeaders", ); } @@ -444,12 +448,12 @@ class ClientRequest extends OutgoingMessage { } if ( - method === "GET" || - method === "HEAD" || - method === "DELETE" || - method === "OPTIONS" || - method === "TRACE" || - method === "CONNECT" + method === "GET" || + method === "HEAD" || + method === "DELETE" || + method === "OPTIONS" || + method === "TRACE" || + method === "CONNECT" ) { this.useChunkedEncodingByDefault = false; } else { @@ -504,9 +508,9 @@ class ClientRequest extends OutgoingMessage { // https://tools.ietf.org/html/rfc3986#section-3.2.2 const posColon = hostHeader.indexOf(":"); if ( - posColon !== -1 && - hostHeader.includes(":", posColon + 1) && - hostHeader.charCodeAt(0) !== 91 /* '[' */ + posColon !== -1 && + hostHeader.includes(":", posColon + 1) && + hostHeader.charCodeAt(0) !== 91 /* '[' */ ) { hostHeader = `[${hostHeader}]`; } @@ -519,9 +523,9 @@ class ClientRequest extends OutgoingMessage { if (options!.auth && !this.getHeader("Authorization")) { this.setHeader( - "Authorization", - "Basic " + - Buffer.from(options!.auth).toString("base64"), + "Authorization", + "Basic " + + Buffer.from(options!.auth).toString("base64"), ); } @@ -601,7 +605,7 @@ class ClientRequest extends OutgoingMessage { this._client = client; if ( - this.method === "POST" || this.method === "PATCH" || this.method === "PUT" + this.method === "POST" || this.method === "PATCH" || this.method === "PUT" ) { const { readable, writable } = new TransformStream({ cancel: (e) => { @@ -616,64 +620,18 @@ class ClientRequest extends OutgoingMessage { } this._req = op_node_http_request( - this.method, - url, - headers, - client[internalRidSymbol], - this._bodyWriteRid, - ); - } - - _implicitHeader() { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT("render"); - } - this._storeHeader( - this.method + " " + this.path + " HTTP/1.1\r\n", - this[kOutHeaders], + this.method, + url, + headers, + client[internalRidSymbol], + this._bodyWriteRid, ); - } - - _getClient(): Deno.HttpClient | undefined { - return undefined; - } - - // TODO(bartlomieju): handle error - onSocket(socket, _err) { - nextTick(() => { - this.socket = socket; - this.emit("socket", socket); - }); - } - - // deno-lint-ignore no-explicit-any - end(chunk?: any, encoding?: any, cb?: any): this { - if (typeof chunk === "function") { - cb = chunk; - chunk = null; - encoding = null; - } else if (typeof encoding === "function") { - cb = encoding; - encoding = null; - } - - this.finished = true; - if (chunk) { - this.write_(chunk, encoding, null, true); - } else if (!this._headerSent) { - this._contentLength = 0; - this._implicitHeader(); - this._send("", "latin1"); - } - this._bodyWriter?.close(); (async () => { try { const res = await op_fetch_send(this._req.requestRid); - try { - cb?.(); - } catch (_) { - // + if (this._req.cancelHandleRid !== null) { + core.tryClose(this._req.cancelHandleRid); } if (this._timeout) { this._timeout.removeEventListener("abort", this._timeoutCb); @@ -704,14 +662,10 @@ class ClientRequest extends OutgoingMessage { } incoming._addHeaderLines( - res.headers, - Object.entries(res.headers).flat().length, + res.headers, + Object.entries(res.headers).flat().length, ); - if (this._req.cancelHandleRid !== null) { - core.tryClose(this._req.cancelHandleRid); - } - if (incoming.upgrade) { if (this.listenerCount("upgrade") === 0) { // No listeners, so we got nothing to do @@ -724,23 +678,23 @@ class ClientRequest extends OutgoingMessage { } const upgradeRid = await op_fetch_response_upgrade( - res.responseRid, + res.responseRid, ); assert(typeof res.remoteAddrIp !== "undefined"); assert(typeof res.remoteAddrIp !== "undefined"); const conn = new TcpConn( - upgradeRid, - { - transport: "tcp", - hostname: res.remoteAddrIp, - port: res.remoteAddrIp, - }, - // TODO(bartlomieju): figure out actual values - { - transport: "tcp", - hostname: "127.0.0.1", - port: 80, - }, + upgradeRid, + { + transport: "tcp", + hostname: res.remoteAddrIp, + port: res.remoteAddrIp, + }, + // TODO(bartlomieju): figure out actual values + { + transport: "tcp", + hostname: "127.0.0.1", + port: 80, + }, ); const socket = new Socket({ handle: new TCP(constants.SERVER, conn), @@ -767,15 +721,15 @@ class ClientRequest extends OutgoingMessage { // if the request body stream errored, we want to propagate that error // instead of the original error from opFetchSend throw new TypeError( - "Failed to fetch: request body stream errored", - { - cause: this._requestSendError, - }, + "Failed to fetch: request body stream errored", + { + cause: this._requestSendError, + }, ); } if ( - err.message.includes("connection closed before message completed") + err.message.includes("connection closed before message completed") ) { // Node.js seems ignoring this error } else if (err.message.includes("The signal has been aborted")) { @@ -788,6 +742,69 @@ class ClientRequest extends OutgoingMessage { })(); } + _implicitHeader() { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT("render"); + } + this._storeHeader( + this.method + " " + this.path + " HTTP/1.1\r\n", + this[kOutHeaders], + ); + } + + _getClient(): Deno.HttpClient | undefined { + return undefined; + } + + // TODO(bartlomieju): handle error + onSocket(socket, _err) { + nextTick(() => { + this.socket = socket; + this.emit("socket", socket); + }); + } + + // deno-lint-ignore no-explicit-any + end(chunk?: any, encoding?: any, cb?: any): this { + // Do nothing if request is already destroyed. + if (this.destroyed) return this; + + if (typeof chunk === "function") { + cb = chunk; + chunk = null; + encoding = null; + } else if (typeof encoding === "function") { + cb = encoding; + encoding = null; + } + + this.finished = true; + if (chunk) { + this.write_(chunk, encoding, null, true); + } else if (!this._headerSent) { + this._contentLength = 0; + this._implicitHeader(); + this._send("", "latin1"); + } + (async () => { + try { + await this._bodyWriter?.close(); + } catch (_) { + // The readable stream resource is dropped right after + // read is complete closing the writable stream resource. + // If we try to close the writer again, it will result in an + // error which we can safely ignore. + } + try { + cb?.(); + } catch (_) { + // + } + })(); + + return this; + } + abort() { if (this.aborted) { return; @@ -809,7 +826,9 @@ class ClientRequest extends OutgoingMessage { if (rid) { core.tryClose(rid); } - if (this._req.cancelHandleRid !== null) { + + // Request might be closed before we actually made it + if (this._req !== undefined && this._req.cancelHandleRid !== null) { core.tryClose(this._req.cancelHandleRid); } // If we're aborting, we don't care about any more response data. @@ -842,8 +861,8 @@ class ClientRequest extends OutgoingMessage { path = "/" + path; } const url = new URL( - `${protocol}//${auth ? `${auth}@` : ""}${host}${port === 80 ? "" : `:${port}` - }${path}`, + `${protocol}//${auth ? `${auth}@` : ""}${host}${port === 80 ? "" : `:${port}` + }${path}`, ); url.hash = hash; return url.href; @@ -889,8 +908,8 @@ class ClientRequest extends OutgoingMessage { if (Array.isArray(value)) { if ( - (value.length < 2 || !isCookieField(key)) && - (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(key.toLowerCase())) + (value.length < 2 || !isCookieField(key)) && + (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(key.toLowerCase())) ) { // Retain for(;;) loop for performance reasons // Refs: https://github.com/nodejs/node/pull/30958 @@ -919,7 +938,7 @@ function isCookieField(s) { function isContentDispositionField(s) { return s.length === 19 && - s.toLowerCase() === "content-disposition"; + s.toLowerCase() === "content-disposition"; } const kHeaders = Symbol("kHeaders"); @@ -1330,22 +1349,28 @@ function onError(self, error, cb) { } export class ServerResponse extends NodeWritable { - statusCode?: number = undefined; + statusCode = 200; statusMessage?: string = undefined; - #headers = new Headers({}); + #headers: Record = { __proto__: null }; + #hasNonStringHeaders: boolean = false; #readable: ReadableStream; override writable = true; // used by `npm:on-finished` finished = false; headersSent = false; - #firstChunk: Chunk | null = null; #resolve: (value: Response | PromiseLike) => void; + // deno-lint-ignore no-explicit-any + #socketOverride: any | null = null; static #enqueue(controller: ReadableStreamDefaultController, chunk: Chunk) { - if (typeof chunk === "string") { - controller.enqueue(ENCODER.encode(chunk)); - } else { - controller.enqueue(chunk); + try { + if (typeof chunk === "string") { + controller.enqueue(ENCODER.encode(chunk)); + } else { + controller.enqueue(chunk); + } + } catch (_) { + // The stream might have been closed. Ignore the error. } } @@ -1356,8 +1381,8 @@ export class ServerResponse extends NodeWritable { } constructor( - resolve: (value: Response | PromiseLike) => void, - socket: FakeSocket, + resolve: (value: Response | PromiseLike) => void, + socket: FakeSocket, ) { let controller: ReadableByteStreamController; const readable = new ReadableStream({ @@ -1369,24 +1394,25 @@ export class ServerResponse extends NodeWritable { autoDestroy: true, defaultEncoding: "utf-8", emitClose: true, - write: (chunk, _encoding, cb) => { + // FIXME: writes don't work when a socket is assigned and then + // detached. + write: (chunk, encoding, cb) => { + // Writes chunks are directly written to the socket if + // one is assigned via assignSocket() + if (this.#socketOverride && this.#socketOverride.writable) { + this.#socketOverride.write(chunk, encoding); + return cb(); + } if (!this.headersSent) { - if (this.#firstChunk === null) { - this.#firstChunk = chunk; - return cb(); - } else { - ServerResponse.#enqueue(controller, this.#firstChunk); - this.#firstChunk = null; - this.respond(false); - } + ServerResponse.#enqueue(controller, chunk); + this.respond(false); + return cb(); } ServerResponse.#enqueue(controller, chunk); return cb(); }, final: (cb) => { - if (this.#firstChunk) { - this.respond(true, this.#firstChunk); - } else if (!this.headersSent) { + if (!this.headersSent) { this.respond(true); } controller.close(); @@ -1404,42 +1430,112 @@ export class ServerResponse extends NodeWritable { this.socket = socket; } - setHeader(name: string, value: string) { - this.#headers.set(name, value); + setHeader(name: string, value: string | string[]) { + if (Array.isArray(value)) { + this.#hasNonStringHeaders = true; + } + this.#headers[name] = value; + return this; + } + + appendHeader(name: string, value: string | string[]) { + if (Array.isArray(value)) { + this.#hasNonStringHeaders = true; + } + if (this.#headers[name] === undefined) { + this.#headers[name] = value; + } else { + if (!Array.isArray(this.#headers[name])) { + this.#headers[name] = [this.#headers[name]]; + } + const header = this.#headers[name]; + if (Array.isArray(value)) { + header.push(...value); + } else { + header.push(value); + } + } return this; } getHeader(name: string) { - return this.#headers.get(name) ?? undefined; + return this.#headers[name]; } removeHeader(name: string) { - return this.#headers.delete(name); + delete this.#headers[name]; } getHeaderNames() { - return Array.from(this.#headers.keys()); + return Object.keys(this.#headers); } - hasHeader(name: string) { - return this.#headers.has(name); + getHeaders(): Record { + // @ts-ignore Ignore null __proto__ + return { __proto__: null, ...this.#headers }; } - - writeHead(status: number, headers: Record = {}) { + hasHeader(name: string) { + return Object.hasOwn(this.#headers, name); + } + + writeHead( + status: number, + statusMessage?: string, + headers?: + | Record + | Array<[string, string]>, + ): this; + writeHead( + status: number, + headers?: + | Record + | Array<[string, string]>, + ): this; + writeHead( + status: number, + statusMessageOrHeaders?: + | string + | Record + | Array<[string, string]>, + maybeHeaders?: + | Record + | Array<[string, string]>, + ): this { this.statusCode = status; - for (const k in headers) { - if (Object.hasOwn(headers, k)) { - this.#headers.set(k, headers[k]); + + let headers = null; + if (typeof statusMessageOrHeaders === "string") { + this.statusMessage = statusMessageOrHeaders; + if (maybeHeaders !== undefined) { + headers = maybeHeaders; } + } else if (statusMessageOrHeaders !== undefined) { + headers = statusMessageOrHeaders; } + + if (headers !== null) { + if (ArrayIsArray(headers)) { + headers = headers as Array<[string, string]>; + for (let i = 0; i < headers.length; i++) { + this.appendHeader(headers[i][0], headers[i][1]); + } + } else { + headers = headers as Record; + for (const k in headers) { + if (Object.hasOwn(headers, k)) { + this.setHeader(k, headers[k]); + } + } + } + } + return this; } #ensureHeaders(singleChunk?: Chunk) { - if (this.statusCode === undefined) { - this.statusCode = 200; + if (this.statusCode === 200 && this.statusMessage === undefined) { this.statusMessage = "OK"; } if ( - typeof singleChunk === "string" && - !this.hasHeader("content-type") + typeof singleChunk === "string" && + !this.hasHeader("content-type") ) { this.setHeader("content-type", "text/plain;charset=UTF-8"); } @@ -1449,26 +1545,43 @@ export class ServerResponse extends NodeWritable { this.headersSent = true; this.#ensureHeaders(singleChunk); let body = singleChunk ?? (final ? null : this.#readable); - if (ServerResponse.#bodyShouldBeNull(this.statusCode!)) { + if (ServerResponse.#bodyShouldBeNull(this.statusCode)) { body = null; } + let headers: Record | [string, string][] = this + .#headers as Record; + if (this.#hasNonStringHeaders) { + headers = []; + // Guard is not needed as this is a null prototype object. + // deno-lint-ignore guard-for-in + for (const key in this.#headers) { + const entry = this.#headers[key]; + if (Array.isArray(entry)) { + for (const value of entry) { + headers.push([key, value]); + } + } else { + headers.push([key, entry]); + } + } + } this.#resolve( - new Response(body, { - headers: this.#headers, - status: this.statusCode, - statusText: this.statusMessage, - }), + new Response(body, { + headers, + status: this.statusCode, + statusText: this.statusMessage, + }), ); } // deno-lint-ignore no-explicit-any override end(chunk?: any, encoding?: any, cb?: any): this { this.finished = true; - if (!chunk && this.#headers.has("transfer-encoding")) { + if (!chunk && "transfer-encoding" in this.#headers) { // FIXME(bnoordhuis) Node sends a zero length chunked body instead, i.e., // the trailing "0\r\n", but respondWith() just hangs when I try that. - this.#headers.set("content-length", "0"); - this.#headers.delete("transfer-encoding"); + this.#headers["content-length"] = "0"; + delete this.#headers["transfer-encoding"]; } // @ts-expect-error The signature for cb is stricter than the one implemented here @@ -1483,6 +1596,20 @@ export class ServerResponse extends NodeWritable { _implicitHeader() { this.writeHead(this.statusCode); } + + assignSocket(socket) { + if (socket._httpMessage) { + throw new ERR_HTTP_SOCKET_ASSIGNED(); + } + socket._httpMessage = this; + this.#socketOverride = socket; + } + + detachSocket(socket) { + assert(socket._httpMessage === this); + socket._httpMessage = null; + this.#socketOverride = null; + } } // TODO(@AaronO): optimize @@ -1534,6 +1661,10 @@ export class IncomingMessageForServer extends NodeReadable { return "1.1"; } + set httpVersion(val) { + assert(val === "1.1"); + } + get headers() { if (!this.#headers) { this.#headers = {}; @@ -1546,10 +1677,14 @@ export class IncomingMessageForServer extends NodeReadable { return this.#headers; } + set headers(val) { + this.#headers = val; + } + get upgrade(): boolean { return Boolean( - this.#req.headers.get("connection")?.toLowerCase().includes("upgrade") && - this.#req.headers.get("upgrade"), + this.#req.headers.get("connection")?.toLowerCase().includes("upgrade") && + this.#req.headers.get("upgrade"), ); } @@ -1560,8 +1695,8 @@ export class IncomingMessageForServer extends NodeReadable { } export type ServerHandler = ( - req: IncomingMessageForServer, - res: ServerResponse, + req: IncomingMessageForServer, + res: ServerResponse, ) => void; export function Server(opts, requestListener?: ServerHandler): ServerImpl { @@ -1569,15 +1704,12 @@ export function Server(opts, requestListener?: ServerHandler): ServerImpl { } export class ServerImpl extends EventEmitter { - #httpConnections: Set = new Set(); - #listener?: Deno.Listener; - - #addr: Deno.NetAddr; + #addr: Deno.NetAddr | null = null; #hasClosed = false; - #server: Deno.Server; + #server: Deno.HttpServer; #unref = false; #ac?: AbortController; - #servePromise: any; + #serveDeferred: ReturnType>; listening = false; constructor(opts, requestListener?: ServerHandler) { @@ -1594,14 +1726,15 @@ export class ServerImpl extends EventEmitter { this._opts = opts; - this.#servePromise = createDeferredPromise(); - this.#servePromise.promise.finally(() => this.emit("close")); + this.#serveDeferred = Promise.withResolvers(); + this.#serveDeferred.promise.then(() => this.emit("close")); if (requestListener !== undefined) { this.on("request", requestListener); } } listen(...args: unknown[]): this { + // TODO(bnoordhuis) Delegate to net.Server#listen(). const normalized = _normalizeArgs(args); const options = normalized[0] as Partial; const cb = normalized[1]; @@ -1619,13 +1752,16 @@ export class ServerImpl extends EventEmitter { // TODO(bnoordhuis) Node prefers [::] when host is omitted, // we on the other hand default to 0.0.0.0. - const hostname = options.host ?? "0.0.0.0"; + let hostname = options.host ?? "0.0.0.0"; + if (hostname == "localhost") { + hostname = "127.0.0.1"; + } this.#addr = { hostname, port, } as Deno.NetAddr; this.listening = true; - this._serve(); + nextTick(() => this._serve()); return this; } @@ -1649,14 +1785,16 @@ export class ServerImpl extends EventEmitter { const { streamRid } = tag; const [upgradeRid, fenceRid] = op_http_upgrade_raw2(streamRid); const conn = new TcpConn( - upgradeRid, - info?.remoteAddr, - info?.localAddr + upgradeRid, + info?.remoteAddr, + info?.localAddr ); const socket = new Socket({ handle: new TCP(constants.SERVER, conn), }); + // Update socket held by `req`. + req.socket = socket; tag.fenceRid = fenceRid; this.emit("upgrade", req, socket, Buffer.from([])); @@ -1673,34 +1811,49 @@ export class ServerImpl extends EventEmitter { if (this.#hasClosed) { return; } + this.#ac = ac; - this.#server = Deno.serve((req) => { - return handler(req, { - remoteAddr: { - hostname: "0.0.0.0", - port: 9999 + try { + this.#server = Deno.serve({ + handler: (req) => { + return handler(req, { + remoteAddr: { + transport: "tcp", + hostname: "0.0.0.0", + port: 9999 + }, + completed: Promise.resolve(undefined) + }); + }, + // @ts-ignore Might be any without `--unstable` flag + onListen: ({ port }) => { + this.#addr!.port = port; + this.emit("listening"); } }); - }); - // - // this.#server = serve( - // { - // handler: handler as Deno.ServeHandler, - // ...this.#addr, - // signal: ac.signal, - // // @ts-ignore Might be any without `--unstable` flag - // onListen: ({ port }) => { - // this.#addr!.port = port; - // this.emit("listening"); - // }, - // ...this._additionalServeOptions?.(), - // }, - // ); + // this.#server = serve( + // { + // handler: handler as Deno.ServeHandler, + // ...this.#addr, + // signal: ac.signal, + // // @ts-ignore Might be any without `--unstable` flag + // onListen: ({ port }) => { + // this.#addr!.port = port; + // this.emit("listening"); + // }, + // ...this._additionalServeOptions?.(), + // }, + // ); + } catch (e) { + this.emit("error", e); + return; + } + if (this.#unref) { this.#server.unref(); } - this.#server.then((p) => p.finished.then(() => this.#servePromise!.resolve())); + this.#server.finished.then(() => this.#serveDeferred!.resolve()); } setTimeout() { @@ -1737,17 +1890,42 @@ export class ServerImpl extends EventEmitter { } if (listening && this.#ac) { - this.#ac.abort(); - this.#ac = undefined; + if (this.#server) { + this.#server.shutdown(); + } else if (this.#ac) { + this.#ac.abort(); + this.#ac = undefined; + } } else { - this.#servePromise!.resolve(); + this.#serveDeferred!.resolve(); } this.#server = undefined; return this; } + closeAllConnections() { + if (this.#hasClosed) { + return; + } + if (this.#ac) { + this.#ac.abort(); + this.#ac = undefined; + } + } + + closeIdleConnections() { + if (this.#hasClosed) { + return; + } + + if (this.#server) { + this.#server.shutdown(); + } + } + address() { + if (this.#addr === null) return null; return { port: this.#addr.port, address: this.#addr.hostname, @@ -1755,7 +1933,6 @@ export class ServerImpl extends EventEmitter { } } - Server.prototype = ServerImpl.prototype; export function createServer(opts, requestListener?: ServerHandler) { @@ -1764,17 +1941,17 @@ export function createServer(opts, requestListener?: ServerHandler) { /** Makes an HTTP request. */ export function request( - url: string | URL, - cb?: (res: IncomingMessageForClient) => void, + url: string | URL, + cb?: (res: IncomingMessageForClient) => void, ): ClientRequest; export function request( - opts: RequestOptions, - cb?: (res: IncomingMessageForClient) => void, + opts: RequestOptions, + cb?: (res: IncomingMessageForClient) => void, ): ClientRequest; export function request( - url: string | URL, - opts: RequestOptions, - cb?: (res: IncomingMessageForClient) => void, + url: string | URL, + opts: RequestOptions, + cb?: (res: IncomingMessageForClient) => void, ): ClientRequest; // deno-lint-ignore no-explicit-any export function request(...args: any[]) { @@ -1783,17 +1960,17 @@ export function request(...args: any[]) { /** Makes a `GET` HTTP request. */ export function get( - url: string | URL, - cb?: (res: IncomingMessageForClient) => void, + url: string | URL, + cb?: (res: IncomingMessageForClient) => void, ): ClientRequest; export function get( - opts: RequestOptions, - cb?: (res: IncomingMessageForClient) => void, + opts: RequestOptions, + cb?: (res: IncomingMessageForClient) => void, ): ClientRequest; export function get( - url: string | URL, - opts: RequestOptions, - cb?: (res: IncomingMessageForClient) => void, + url: string | URL, + opts: RequestOptions, + cb?: (res: IncomingMessageForClient) => void, ): ClientRequest; // deno-lint-ignore no-explicit-any export function get(...args: any[]) { @@ -1802,6 +1979,8 @@ export function get(...args: any[]) { return req; } +export const maxHeaderSize = 16_384; + export { Agent, ClientRequest, @@ -1810,6 +1989,8 @@ export { METHODS, OutgoingMessage, STATUS_CODES, + validateHeaderName, + validateHeaderValue, }; export default { Agent, @@ -1826,4 +2007,7 @@ export default { ServerResponse, request, get, + validateHeaderName, + validateHeaderValue, + maxHeaderSize, }; \ No newline at end of file diff --git a/crates/node/polyfills/http2.ts b/crates/node/polyfills/http2.ts index 0bd0e53f3..2a3b4f7f3 100644 --- a/crates/node/polyfills/http2.ts +++ b/crates/node/polyfills/http2.ts @@ -4,7 +4,8 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials -import { core } from "ext:core/mod.js"; +import { core, primordials } from "ext:core/mod.js"; +const { internalRidSymbol } = core; import { op_http2_client_get_response, op_http2_client_get_response_body_chunk, @@ -15,11 +16,16 @@ import { op_http2_client_send_trailers, op_http2_connect, op_http2_poll_client_connection, + op_http_set_response_trailers, } from "ext:core/ops"; import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; +import { toInnerRequest } from "ext:deno_fetch/23_request.js"; +import { Readable } from "node:stream"; import { EventEmitter } from "node:events"; import { Buffer } from "node:buffer"; +import { emitWarning } from "node:process"; +import Stream from "node:stream"; import { connect as netConnect, Server, Socket, TCP } from "node:net"; import { connect as tlsConnect } from "node:tls"; import { TypedArray } from "ext:deno_node/internal/util/types.ts"; @@ -31,7 +37,7 @@ import { } from "ext:deno_node/internal/stream_base_commons.ts"; import { FileHandle } from "node:fs/promises"; import { kStreamBaseField } from "ext:deno_node/internal_binding/stream_wrap.ts"; -import { addTrailers, serveHttpOnConnection } from "ext:deno_http/00_serve.ts"; +import { serveHttpOnConnection } from "ext:deno_http/00_serve.ts"; import { nextTick } from "ext:deno_node/_next_tick.ts"; import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; import { Duplex } from "node:stream"; @@ -41,21 +47,40 @@ import { ERR_HTTP2_CONNECT_PATH, ERR_HTTP2_CONNECT_SCHEME, ERR_HTTP2_GOAWAY_SESSION, + ERR_HTTP2_HEADERS_SENT, + ERR_HTTP2_INFO_STATUS_NOT_ALLOWED, ERR_HTTP2_INVALID_PSEUDOHEADER, ERR_HTTP2_INVALID_SESSION, ERR_HTTP2_INVALID_STREAM, + ERR_HTTP2_NO_SOCKET_MANIPULATION, ERR_HTTP2_SESSION_ERROR, + ERR_HTTP2_STATUS_INVALID, ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_STREAM_ERROR, + ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, ERR_HTTP2_TRAILERS_ALREADY_SENT, ERR_HTTP2_TRAILERS_NOT_READY, ERR_HTTP2_UNSUPPORTED_PROTOCOL, + ERR_INVALID_ARG_VALUE, ERR_INVALID_HTTP_TOKEN, ERR_SOCKET_CLOSED, + ERR_STREAM_WRITE_AFTER_END, } from "ext:deno_node/internal/errors.ts"; import { _checkIsHttpToken } from "ext:deno_node/_http_common.ts"; +const { + StringPrototypeTrim, + FunctionPrototypeBind, + ObjectKeys, + ReflectGetPrototypeOf, + ObjectAssign, + StringPrototypeToLowerCase, + ReflectApply, + ArrayIsArray, + ObjectPrototypeHasOwnProperty, +} = primordials; const kSession = Symbol("session"); +const kOptions = Symbol("options"); const kAlpnProtocol = Symbol("alpnProtocol"); const kAuthority = Symbol("authority"); const kEncrypted = Symbol("encrypted"); @@ -85,6 +110,9 @@ const STREAM_FLAGS_HEAD_REQUEST = 0x8; const STREAM_FLAGS_ABORTED = 0x10; const STREAM_FLAGS_HAS_TRAILERS = 0x20; +// Maximum number of allowed additional settings +const MAX_ADDITIONAL_SETTINGS = 10; + const SESSION_FLAGS_PENDING = 0x0; const SESSION_FLAGS_READY = 0x1; const SESSION_FLAGS_CLOSED = 0x2; @@ -171,8 +199,8 @@ export class Http2Session extends EventEmitter { } ping( - _payload: Buffer | TypedArray | DataView, - _callback: () => void, + _payload: Buffer | TypedArray | DataView, + _callback: () => void, ): boolean { notImplemented("Http2Session.ping"); return false; @@ -210,11 +238,12 @@ export class Http2Session extends EventEmitter { } goaway( - _code: number, - _lastStreamID: number, - _opaqueData: Buffer | TypedArray | DataView, + code?: number, + lastStreamID?: number, + opaqueData?: Buffer | TypedArray | DataView, ) { - warnNotImplemented("Http2Session.goaway"); + // TODO(satyarohith): create goaway op and pass the args + debugHttp2(">>> goaway - ignored args", code, lastStreamID, opaqueData); if (this[kDenoConnRid]) { core.tryClose(this[kDenoConnRid]); } @@ -231,8 +260,8 @@ export class Http2Session extends EventEmitter { if (typeof error === "number") { code = error; error = code !== constants.NGHTTP2_NO_ERROR - ? new ERR_HTTP2_SESSION_ERROR(code) - : undefined; + ? new ERR_HTTP2_SESSION_ERROR(code) + : undefined; } if (code === undefined && error != null) { code = constants.NGHTTP2_INTERNAL_ERROR; @@ -260,7 +289,7 @@ export class Http2Session extends EventEmitter { // Don't destroy if the session is not closed or there are pending or open // streams. if ( - !this.closed || state.streams.size > 0 || state.pendingStreams.size > + !this.closed || state.streams.size > 0 || state.pendingStreams.size > 0 ) { return; @@ -312,11 +341,10 @@ function closeSession(session: Http2Session, code?: number, error?: Error) { // TODO(bartlomieju): handle sockets debugHttp2( - ">>> closeSession", - session[kDenoConnRid], - session[kDenoClientRid], + ">>> closeSession", + session[kDenoConnRid], + session[kDenoClientRid], ); - console.table(Deno[Deno.internal].core.resources()); if (session[kDenoConnRid]) { core.tryClose(session[kDenoConnRid]); } @@ -333,8 +361,8 @@ export class ServerHttp2Session extends Http2Session { } altsvc( - _alt: string, - _originOrStream: number | string | URL | { origin: string }, + _alt: string, + _originOrStream: number | string | URL | { origin: string }, ) { notImplemented("ServerHttp2Session.altsvc"); } @@ -362,10 +390,10 @@ export class ClientHttp2Session extends Http2Session { #refed = true; constructor( - // deno-lint-ignore no-explicit-any - socket: any, - url: string, - options: Record, + // deno-lint-ignore no-explicit-any + socket: any, + url: string, + options: Record, ) { super(constants.NGHTTP2_SESSION_CLIENT, options); this[kPendingRequestCalls] = null; @@ -378,7 +406,7 @@ export class ClientHttp2Session extends Http2Session { const connPromise = new Promise((resolve) => { const eventName = url.startsWith("https") ? "secureConnect" : "connect"; socket.once(eventName, () => { - const rid = socket[kHandle][kStreamBaseField].rid; + const rid = socket[kHandle][kStreamBaseField][internalRidSymbol]; nextTick(() => { resolve(rid); }); @@ -398,7 +426,7 @@ export class ClientHttp2Session extends Http2Session { (async () => { try { const promise = op_http2_poll_client_connection( - this[kDenoConnRid], + this[kDenoConnRid], ); this[kPollConnPromise] = promise; if (!this.#refed) { @@ -428,8 +456,8 @@ export class ClientHttp2Session extends Http2Session { } request( - headers: Http2Headers, - options?: Record, + headers: Http2Headers, + options?: Record, ): ClientHttp2Stream { if (this.destroyed) { throw new ERR_HTTP2_INVALID_SESSION(); @@ -460,7 +488,7 @@ export class ClientHttp2Session extends Http2Session { } const connect = - headers[constants.HTTP2_HEADER_METHOD] === constants.HTTP2_METHOD_CONNECT; + headers[constants.HTTP2_HEADER_METHOD] === constants.HTTP2_METHOD_CONNECT; if (!connect || headers[constants.HTTP2_HEADER_PROTOCOL] !== undefined) { if (getAuthority(headers) === undefined) { @@ -487,21 +515,21 @@ export class ClientHttp2Session extends Http2Session { if (options.endStream === undefined) { const method = headers[constants.HTTP2_HEADER_METHOD]; options.endStream = method === constants.HTTP2_METHOD_DELETE || - method === constants.HTTP2_METHOD_GET || - method === constants.HTTP2_METHOD_HEAD; + method === constants.HTTP2_METHOD_GET || + method === constants.HTTP2_METHOD_HEAD; } else { options.endStream = !!options.endStream; } const stream = new ClientHttp2Stream( - options, - this, - this.#connectPromise, - headers, + options, + this, + this.#connectPromise, + headers, ); stream[kSentHeaders] = headers; stream[kOrigin] = `${headers[constants.HTTP2_HEADER_SCHEME]}://${ - getAuthority(headers) + getAuthority(headers) }`; if (options.endStream) { @@ -563,12 +591,14 @@ export class Http2Stream extends EventEmitter { #readerPromise: Promise>; #closed: boolean; _response: Response; + // This is required to set the trailers on the response. + _request: Request; constructor( - session: Http2Session, - headers: Promise, - controllerPromise: Promise>, - readerPromise: Promise>, + session: Http2Session, + headers: Promise, + controllerPromise: Promise>, + readerPromise: Promise>, ) { super(); this.#session = session; @@ -696,22 +726,23 @@ export class Http2Stream extends EventEmitter { return {}; } - sendTrailers(_headers: Record) { - addTrailers(this._response, [["grpc-status", "0"], ["grpc-message", "OK"]]); + sendTrailers(headers: Record) { + const request = toInnerRequest(this._request); + op_http_set_response_trailers(request.external, Object.entries(headers)); } } async function clientHttp2Request( - session, + session, + sessionConnectPromise, + headers, + options, +) { + debugHttp2( + ">>> waiting for connect promise", sessionConnectPromise, headers, options, -) { - debugHttp2( - ">>> waiting for connect promise", - sessionConnectPromise, - headers, - options, ); await sessionConnectPromise; @@ -726,16 +757,16 @@ async function clientHttp2Request( } } debugHttp2( - "waited for connect promise", - !!options.waitForTrailers, - pseudoHeaders, - reqHeaders, + "waited for connect promise", + !!options.waitForTrailers, + pseudoHeaders, + reqHeaders, ); return await op_http2_client_request( - session[kDenoClientRid], - pseudoHeaders, - reqHeaders, + session[kDenoClientRid], + pseudoHeaders, + reqHeaders, ); } @@ -746,10 +777,10 @@ export class ClientHttp2Stream extends Duplex { #encoding = "utf8"; constructor( - options: Record, - session: Http2Session, - sessionConnectPromise: Promise, - headers: Record, + options: Record, + session: Http2Session, + sessionConnectPromise: Promise, + headers: Record, ) { options.allowHalfOpen = true; options.decodeString = false; @@ -774,10 +805,10 @@ export class ClientHttp2Stream extends Duplex { this[kDenoRid] = undefined; this.#requestPromise = clientHttp2Request( - session, - sessionConnectPromise, - headers, - options, + session, + sessionConnectPromise, + headers, + options, ); debugHttp2(">>> created clienthttp2stream"); // TODO(bartlomieju): save it so we can unref @@ -788,12 +819,12 @@ export class ClientHttp2Stream extends Duplex { this[kDenoRid] = streamRid; this[kInit](streamId); debugHttp2( - ">>> after request promise", - session[kDenoClientRid], - this.#rid, + ">>> after request promise", + session[kDenoClientRid], + this.#rid, ); - const response = await op_http2_client_get_response( - this.#rid, + const [response, endStream] = await op_http2_client_get_response( + this.#rid, ); debugHttp2(">>> after get response", response); const headers = { @@ -801,7 +832,13 @@ export class ClientHttp2Stream extends Duplex { ...Object.fromEntries(response.headers), }; debugHttp2(">>> emitting response", headers); - this.emit("response", headers, 0); + this.emit( + "response", + headers, + endStream + ? constants.NGHTTP2_FLAG_END_STREAM + : constants.NGHTTP2_FLAG_NONE, + ); this[kDenoResponse] = response; this.emit("ready"); })(); @@ -897,38 +934,36 @@ export class ClientHttp2Stream extends Duplex { // TODO(bartlomieju): clean up _write(chunk, encoding, callback?: () => void) { - debugHttp2(">>> _write", callback); + debugHttp2(">>> _write", encoding, callback); if (typeof encoding === "function") { callback = encoding; - encoding = "utf8"; + encoding = this.#encoding; } let data; - if (typeof encoding === "string") { + if (encoding === "utf8") { data = ENCODER.encode(chunk); - } else { + } else if (encoding === "buffer") { + this.#encoding = encoding; data = chunk.buffer; } this.#requestPromise - .then(() => { - debugHttp2(">>> _write", this.#rid, data, encoding, callback); - return op_http2_client_send_data( - this.#rid, - data, - ); - }) - .then(() => { - callback?.(); - debugHttp2( - "this.writableFinished", - this.pending, - this.destroyed, - this.writableFinished, - ); - }) - .catch((e) => { - callback?.(e); - }); + .then(() => { + debugHttp2(">>> _write", this.#rid, data, encoding, callback); + return op_http2_client_send_data(this.#rid, new Uint8Array(data)); + }) + .then(() => { + callback?.(); + debugHttp2( + "this.writableFinished", + this.pending, + this.destroyed, + this.writableFinished, + ); + }) + .catch((e) => { + callback?.(e); + }); } // TODO(bartlomieju): finish this method @@ -943,7 +978,7 @@ export class ClientHttp2Stream extends Duplex { return; } - shutdownWritable(this, cb); + shutdownWritable(this, cb, this.#rid); } // TODO(bartlomieju): needs a proper cleanup @@ -971,24 +1006,30 @@ export class ClientHttp2Stream extends Duplex { debugHttp2(">>> read"); (async () => { - const [chunk, finished] = await op_http2_client_get_response_body_chunk( + const [chunk, finished, cancelled] = + await op_http2_client_get_response_body_chunk( this[kDenoResponse].bodyRid, - ); + ); + + if (cancelled) { + return; + } debugHttp2(">>> chunk", chunk, finished, this[kDenoResponse].bodyRid); if (chunk === null) { const trailerList = await op_http2_client_get_response_trailers( - this[kDenoResponse].bodyRid, + this[kDenoResponse].bodyRid, ); if (trailerList) { const trailers = Object.fromEntries(trailerList); this.emit("trailers", trailers); } - debugHttp2("tryClose"); + debugHttp2(">>> tryClose", this[kDenoResponse]?.bodyRid); core.tryClose(this[kDenoResponse].bodyRid); this.push(null); debugHttp2(">>> read null chunk"); + this.read(0); this[kMaybeDestroy](); return; } @@ -1033,8 +1074,8 @@ export class ClientHttp2Stream extends Duplex { debugHttp2("sending trailers", this.#rid, trailers); op_http2_client_send_trailers( - this.#rid, - trailerList, + this.#rid, + trailerList, ).then(() => { stream[kMaybeDestroy](); core.tryClose(this.#rid); @@ -1094,8 +1135,8 @@ export class ClientHttp2Stream extends Duplex { const nameForErrorCode = {}; if ( - err == null && code !== constants.NGHTTP2_NO_ERROR && - code !== constants.NGHTTP2_CANCEL + err == null && code !== constants.NGHTTP2_NO_ERROR && + code !== constants.NGHTTP2_CANCEL ) { err = new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code); } @@ -1108,11 +1149,11 @@ export class ClientHttp2Stream extends Duplex { [kMaybeDestroy](code = constants.NGHTTP2_NO_ERROR) { debugHttp2( - ">>> ClientHttp2Stream[kMaybeDestroy]", - code, - this.writableFinished, - this.readable, - this.closed, + ">>> ClientHttp2Stream[kMaybeDestroy]", + code, + this.writableFinished, + this.readable, + this.closed, ); if (code !== constants.NGHTTP2_NO_ERROR) { this._destroy(); @@ -1135,15 +1176,30 @@ export class ClientHttp2Stream extends Duplex { } } -function shutdownWritable(stream, callback) { +function shutdownWritable(stream, callback, streamRid) { debugHttp2(">>> shutdownWritable", callback); const state = stream[kState]; if (state.shutdownWritableCalled) { + debugHttp2(">>> shutdownWritable() already called"); return callback(); } state.shutdownWritableCalled = true; - onStreamTrailers(stream); - callback(); + if (state.flags & STREAM_FLAGS_HAS_TRAILERS) { + onStreamTrailers(stream); + callback(); + } else { + op_http2_client_send_data(streamRid, new Uint8Array(), true) + .then(() => { + callback(); + stream[kMaybeDestroy](); + core.tryClose(streamRid); + }) + .catch((e) => { + callback(e); + core.tryClose(streamRid); + stream._destroy(e); + }); + } // TODO(bartlomieju): might have to add "finish" event listener here, // check it. } @@ -1186,15 +1242,15 @@ function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { if (rstStreamStatus != kNoRstStream) { debugHttp2( - ">>> closeStream", - !ending, - stream.writableFinished, - code !== constants.NGHTTP2_NO_ERROR, - rstStreamStatus === kForceRstStream, + ">>> closeStream", + !ending, + stream.writableFinished, + code !== constants.NGHTTP2_NO_ERROR, + rstStreamStatus === kForceRstStream, ); if ( - !ending || stream.writableFinished || - code !== constants.NGHTTP2_NO_ERROR || rstStreamStatus === kForceRstStream + !ending || stream.writableFinished || + code !== constants.NGHTTP2_NO_ERROR || rstStreamStatus === kForceRstStream ) { finishCloseStream(stream, code); } else { @@ -1209,43 +1265,49 @@ function finishCloseStream(stream, code) { stream.push(null); stream.once("ready", () => { op_http2_client_reset_stream( - stream[kDenoRid], - code, + stream[kDenoRid], + code, ).then(() => { debugHttp2( - ">>> finishCloseStream close", - stream[kDenoRid], - stream[kDenoResponse].bodyRid, + ">>> finishCloseStream close", + stream[kDenoRid], + stream[kDenoResponse]?.bodyRid, ); core.tryClose(stream[kDenoRid]); - core.tryClose(stream[kDenoResponse].bodyRid); + if (stream[kDenoResponse]) { + core.tryClose(stream[kDenoResponse].bodyRid); + } stream.emit("close"); }); }); } else { stream.resume(); op_http2_client_reset_stream( - stream[kDenoRid], - code, + stream[kDenoRid], + code, ).then(() => { debugHttp2( - ">>> finishCloseStream close2", - stream[kDenoRid], - stream[kDenoResponse].bodyRid, + ">>> finishCloseStream close2", + stream[kDenoRid], + stream[kDenoResponse].bodyRid, ); core.tryClose(stream[kDenoRid]); - core.tryClose(stream[kDenoResponse].bodyRid); + if (stream[kDenoResponse]) { + core.tryClose(stream[kDenoResponse].bodyRid); + } nextTick(() => { stream.emit("close"); }); }).catch(() => { debugHttp2( - ">>> finishCloseStream close2 catch", - stream[kDenoRid], - stream[kDenoResponse].bodyRid, + ">>> finishCloseStream close2 catch", + stream[kDenoRid], + stream[kDenoResponse]?.bodyRid, ); core.tryClose(stream[kDenoRid]); - core.tryClose(stream[kDenoResponse].bodyRid); + if (stream[kDenoResponse]) { + core.tryClose(stream[kDenoResponse].bodyRid); + } nextTick(() => { stream.emit("close"); }); @@ -1264,15 +1326,18 @@ export class ServerHttp2Stream extends Http2Stream { #headersSent: boolean; constructor( - session: Http2Session, - headers: Promise, - controllerPromise: Promise>, - reader: ReadableStream, - body: ReadableStream, + session: Http2Session, + headers: Promise, + controllerPromise: Promise>, + reader: ReadableStream, + body: ReadableStream, + // This is required to set the trailers on the response. + req: Request, ) { super(session, headers, controllerPromise, Promise.resolve(reader)); this._deferred = Promise.withResolvers(); this.#body = body; + this._request = req; } additionalHeaders(_headers: Record) { @@ -1296,16 +1361,16 @@ export class ServerHttp2Stream extends Http2Stream { } pushStream( - _headers: Record, - _options: Record, - _callback: () => unknown, + _headers: Record, + _options: Record, + _callback: () => unknown, ) { notImplemented("ServerHttp2Stream.pushStream"); } respond( - headers: Http2Headers, - options: Record, + headers: Http2Headers, + options: Record, ) { this.#headersSent = true; const response: ResponseInit = {}; @@ -1321,28 +1386,129 @@ export class ServerHttp2Stream extends Http2Stream { } else { this.#waitForTrailers = options?.waitForTrailers; this._deferred.resolve( - this._response = new Response(this.#body, response), + this._response = new Response(this.#body, response), ); } } respondWithFD( - _fd: number | FileHandle, - _headers: Record, - _options: Record, + _fd: number | FileHandle, + _headers: Record, + _options: Record, ) { notImplemented("ServerHttp2Stream.respondWithFD"); } respondWithFile( - _path: string | Buffer | URL, - _headers: Record, - _options: Record, + _path: string | Buffer | URL, + _headers: Record, + _options: Record, ) { notImplemented("ServerHttp2Stream.respondWithFile"); } } +function setupCompat(ev) { + if (ev === "request") { + this.removeListener("newListener", setupCompat); + this.on( + "stream", + FunctionPrototypeBind( + onServerStream, + this, + this[kOptions].Http2ServerRequest, + this[kOptions].Http2ServerResponse, + ), + ); + } +} + +function onServerStream( + ServerRequest, + ServerResponse, + stream, + headers, + _flags, + rawHeaders, +) { + const request = new ServerRequest(stream, headers, undefined, rawHeaders); + const response = new ServerResponse(stream); + + // Check for the CONNECT method + const method = headers[constants.HTTP2_HEADER_METHOD]; + if (method === "CONNECT") { + if (!this.emit("connect", request, response)) { + response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED; + response.end(); + } + return; + } + + // Check for Expectations + if (headers.expect !== undefined) { + if (headers.expect === "100-continue") { + if (this.listenerCount("checkContinue")) { + this.emit("checkContinue", request, response); + } else { + response.writeContinue(); + this.emit("request", request, response); + } + } else if (this.listenerCount("checkExpectation")) { + this.emit("checkExpectation", request, response); + } else { + response.statusCode = constants.HTTP_STATUS_EXPECTATION_FAILED; + response.end(); + } + return; + } + + this.emit("request", request, response); +} + +function initializeOptions(options) { + // assertIsObject(options, 'options'); + options = { ...options }; + // assertIsObject(options.settings, 'options.settings'); + options.settings = { ...options.settings }; + + // assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings'); + if (options.remoteCustomSettings) { + options.remoteCustomSettings = [...options.remoteCustomSettings]; + if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) { + throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); + } + } + + // if (options.maxSessionInvalidFrames !== undefined) + // validateUint32(options.maxSessionInvalidFrames, 'maxSessionInvalidFrames'); + + // if (options.maxSessionRejectedStreams !== undefined) { + // validateUint32( + // options.maxSessionRejectedStreams, + // 'maxSessionRejectedStreams', + // ); + // } + + if (options.unknownProtocolTimeout !== undefined) { + // validateUint32(options.unknownProtocolTimeout, 'unknownProtocolTimeout'); + } else { + // TODO(danbev): is this a good default value? + options.unknownProtocolTimeout = 10000; + } + + // Used only with allowHTTP1 + // options.Http1IncomingMessage = options.Http1IncomingMessage || + // http.IncomingMessage; + // options.Http1ServerResponse = options.Http1ServerResponse || + // http.ServerResponse; + + options.Http2ServerRequest = options.Http2ServerRequest || + Http2ServerRequest; + options.Http2ServerResponse = options.Http2ServerResponse || + Http2ServerResponse; + return options; +} + export class Http2Server extends Server { #options: Record = {}; #abortController; @@ -1350,64 +1516,64 @@ export class Http2Server extends Server { timeout = 0; constructor( - options: Record, - requestListener: () => unknown, + options: Record, + requestListener: () => unknown, ) { + options = initializeOptions(options); super(options); + this[kOptions] = options; this.#abortController = new AbortController(); + this.on("newListener", setupCompat); + this.on( - "connection", - (conn: Deno.Conn) => { - try { - const session = new ServerHttp2Session(); - this.emit("session", session); - this.#server = serveHttpOnConnection( - conn, - this.#abortController.signal, - async (req: Request) => { - try { - const controllerDeferred = Promise.withResolvers< - ReadableStreamDefaultController - >(); - const body = new ReadableStream({ - start(controller) { - controllerDeferred.resolve(controller); - }, - }); - const headers: Http2Headers = {}; - for (const [name, value] of req.headers) { - headers[name] = value; - } - headers[constants.HTTP2_HEADER_PATH] = - new URL(req.url).pathname; - const stream = new ServerHttp2Stream( - session, - Promise.resolve(headers), - controllerDeferred.promise, - req.body, - body, - ); - session.emit("stream", stream, headers); - this.emit("stream", stream, headers); - return await stream._deferred.promise; - } catch (e) { - console.log(">>> Error in serveHttpOnConnection", e); - } - return new Response(""); - }, - () => { - console.log(">>> error"); - }, - () => {}, - ); - } catch (e) { - console.log(">>> Error in Http2Server", e); - } - }, - ); - this.on( - "newListener", - (event) => console.log(`Event in newListener: ${event}`), + "connection", + (conn: Deno.Conn) => { + try { + const session = new ServerHttp2Session(); + this.emit("session", session); + this.#server = serveHttpOnConnection( + conn, + this.#abortController.signal, + async (req: Request) => { + try { + const controllerDeferred = Promise.withResolvers< + ReadableStreamDefaultController + >(); + const body = new ReadableStream({ + start(controller) { + controllerDeferred.resolve(controller); + }, + }); + const headers: Http2Headers = {}; + for (const [name, value] of req.headers) { + headers[name] = value; + } + headers[constants.HTTP2_HEADER_PATH] = + new URL(req.url).pathname; + const stream = new ServerHttp2Stream( + session, + Promise.resolve(headers), + controllerDeferred.promise, + req.body, + body, + req, + ); + this.emit("stream", stream, headers); + return await stream._deferred.promise; + } catch (e) { + console.log(">>> Error in serveHttpOnConnection", e); + } + return new Response(""); + }, + () => { + console.log(">>> error"); + }, + () => {}, + ); + } catch (e) { + console.log(">>> Error in Http2Server", e); + } + }, ); this.#options = options; if (typeof requestListener === "function") { @@ -1420,14 +1586,6 @@ export class Http2Server extends Server { return clientHandle[kStreamBaseField]; } - close(callback?: () => unknown) { - if (callback) { - this.on("close", callback); - } - this.#abortController.abort(); - super.close(); - } - setTimeout(msecs: number, callback?: () => unknown) { this.timeout = msecs; if (callback !== undefined) { @@ -1445,8 +1603,8 @@ export class Http2SecureServer extends Server { timeout = 0; constructor( - options: Record, - requestListener: () => unknown, + options: Record, + requestListener: () => unknown, ) { super(options, function () { notImplemented("connectionListener"); @@ -1474,8 +1632,8 @@ export class Http2SecureServer extends Server { } export function createServer( - options: Record, - onRequestHandler: () => unknown, + options: Record, + onRequestHandler: () => unknown, ): Http2Server { if (typeof options === "function") { onRequestHandler = options; @@ -1485,17 +1643,17 @@ export function createServer( } export function createSecureServer( - _options: Record, - _onRequestHandler: () => unknown, + _options: Record, + _onRequestHandler: () => unknown, ): Http2SecureServer { notImplemented("http2.createSecureServer"); return new Http2SecureServer(); } export function connect( - authority: string | URL, - options: Record, - callback: (session: ClientHttp2Session) => void, + authority: string | URL, + options: Record, + callback: (session: ClientHttp2Session) => void, ): ClientHttp2Session { debugHttp2(">>> http2.connect", options); @@ -1551,7 +1709,10 @@ export function connect( case "https:": // TODO(bartlomieju): handle `initializeTLSOptions` here url = `https://${host}${port == 443 ? "" : (":" + port)}`; - socket = tlsConnect(port, host, { manualStart: true }); + socket = tlsConnect(port, host, { + manualStart: true, + ALPNProtocols: ["h2", "http/1.1"], + }); break; default: throw new ERR_HTTP2_UNSUPPORTED_PROTOCOL(protocol); @@ -1591,7 +1752,7 @@ function socketOnClose() { const state = session[kState]; state.streams.forEach((stream) => stream.close(constants.NGHTTP2_CANCEL)); state.pendingStreams.forEach((stream) => - stream.close(constants.NGHTTP2_CANCEL) + stream.close(constants.NGHTTP2_CANCEL) ); session.close(); session[kMaybeDestroy](err); @@ -1664,7 +1825,7 @@ export const constants = { HTTP2_HEADER_ACCEPT_RANGES: "accept-ranges", HTTP2_HEADER_ACCEPT: "accept", HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS: - "access-control-allow-credentials", + "access-control-allow-credentials", HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS: "access-control-allow-headers", HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS: "access-control-allow-methods", HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN: "access-control-allow-origin", @@ -1898,7 +2059,7 @@ export function getPackedSettings(_settings: Record): Buffer { } export function getUnpackedSettings( - _buffer: Buffer | TypedArray, + _buffer: Buffer | TypedArray, ): Record { notImplemented("http2.getUnpackedSettings"); return {}; @@ -1906,216 +2067,865 @@ export function getUnpackedSettings( export const sensitiveHeaders = Symbol("nodejs.http2.sensitiveHeaders"); -export class Http2ServerRequest { - constructor() { +const kBeginSend = Symbol("begin-send"); +const kStream = Symbol("stream"); +const kResponse = Symbol("response"); +const kHeaders = Symbol("headers"); +const kRawHeaders = Symbol("rawHeaders"); +const kSocket = Symbol("socket"); +const kTrailers = Symbol("trailers"); +const kRawTrailers = Symbol("rawTrailers"); +const kSetHeader = Symbol("setHeader"); +const kAppendHeader = Symbol("appendHeader"); +const kAborted = Symbol("aborted"); +const kProxySocket = Symbol("proxySocket"); +const kRequest = Symbol("request"); + +const proxySocketHandler = { + has(stream, prop) { + const ref = stream.session !== undefined ? stream.session[kSocket] : stream; + return (prop in stream) || (prop in ref); + }, + + get(stream, prop) { + switch (prop) { + case "on": + case "once": + case "end": + case "emit": + case "destroy": + return FunctionPrototypeBind(stream[prop], stream); + case "writable": + case "destroyed": + return stream[prop]; + case "readable": { + if (stream.destroyed) { + return false; + } + const request = stream[kRequest]; + return request ? request.readable : stream.readable; + } + case "setTimeout": { + const session = stream.session; + if (session !== undefined) { + return FunctionPrototypeBind(session.setTimeout, session); + } + return FunctionPrototypeBind(stream.setTimeout, stream); + } + case "write": + case "read": + case "pause": + case "resume": + throw new ERR_HTTP2_NO_SOCKET_MANIPULATION(); + default: { + const ref = stream.session !== undefined + ? stream.session[kSocket] + : stream; + const value = ref[prop]; + return typeof value === "function" + ? FunctionPrototypeBind(value, ref) + : value; + } + } + }, + getPrototypeOf(stream) { + if (stream.session !== undefined) { + return ReflectGetPrototypeOf(stream.session[kSocket]); + } + return ReflectGetPrototypeOf(stream); + }, + set(stream, prop, value) { + switch (prop) { + case "writable": + case "readable": + case "destroyed": + case "on": + case "once": + case "end": + case "emit": + case "destroy": + stream[prop] = value; + return true; + case "setTimeout": { + const session = stream.session; + if (session !== undefined) { + session.setTimeout = value; + } else { + stream.setTimeout = value; + } + return true; + } + case "write": + case "read": + case "pause": + case "resume": + throw new ERR_HTTP2_NO_SOCKET_MANIPULATION(); + default: { + const ref = stream.session !== undefined + ? stream.session[kSocket] + : stream; + ref[prop] = value; + return true; + } + } + }, +}; + +function onStreamCloseRequest() { + const req = this[kRequest]; + + if (req === undefined) { + return; } - get aborted(): boolean { - notImplemented("Http2ServerRequest.aborted"); - return false; + const state = req[kState]; + state.closed = true; + + req.push(null); + // If the user didn't interact with incoming data and didn't pipe it, + // dump it for compatibility with http1 + if (!state.didRead && !req._readableState.resumeScheduled) { + req.resume(); } - get authority(): string { - notImplemented("Http2ServerRequest.authority"); - return ""; + this[kProxySocket] = null; + this[kRequest] = undefined; + + req.emit("close"); +} + +function onStreamTimeout(kind) { + return function onStreamTimeout() { + const obj = this[kind]; + obj.emit("timeout"); + }; +} + +class Http2ServerRequest extends Readable { + readableEnded = false; + + constructor(stream, headers, options, rawHeaders) { + super({ autoDestroy: false, ...options }); + this[kState] = { + closed: false, + didRead: false, + }; + // Headers in HTTP/1 are not initialized using Object.create(null) which, + // although preferable, would simply break too much code. Ergo header + // initialization using Object.create(null) in HTTP/2 is intentional. + this[kHeaders] = headers; + this[kRawHeaders] = rawHeaders; + this[kTrailers] = {}; + this[kRawTrailers] = []; + this[kStream] = stream; + this[kAborted] = false; + stream[kProxySocket] = null; + stream[kRequest] = this; + + // Pause the stream.. + stream.on("trailers", onStreamTrailers); + stream.on("end", onStreamEnd); + stream.on("error", onStreamError); + stream.on("aborted", onStreamAbortedRequest); + stream.on("close", onStreamCloseRequest); + stream.on("timeout", onStreamTimeout(kRequest)); + this.on("pause", onRequestPause); + this.on("resume", onRequestResume); } - get complete(): boolean { - notImplemented("Http2ServerRequest.complete"); - return false; + get aborted() { + return this[kAborted]; } - get connection(): Socket /*| TlsSocket*/ { - notImplemented("Http2ServerRequest.connection"); - return {}; + get complete() { + return this[kAborted] || + this.readableEnded || + this[kState].closed || + this[kStream].destroyed; } - destroy(_error: Error) { - notImplemented("Http2ServerRequest.destroy"); + get stream() { + return this[kStream]; } - get headers(): Record { - notImplemented("Http2ServerRequest.headers"); - return {}; + get headers() { + return this[kHeaders]; } - get httpVersion(): string { - notImplemented("Http2ServerRequest.httpVersion"); - return ""; + get rawHeaders() { + return this[kRawHeaders]; } - get method(): string { - notImplemented("Http2ServerRequest.method"); - return ""; + get trailers() { + return this[kTrailers]; } - get rawHeaders(): string[] { - notImplemented("Http2ServerRequest.rawHeaders"); - return []; + get rawTrailers() { + return this[kRawTrailers]; } - get rawTrailers(): string[] { - notImplemented("Http2ServerRequest.rawTrailers"); - return []; + get httpVersionMajor() { + return 2; } - get scheme(): string { - notImplemented("Http2ServerRequest.scheme"); - return ""; + get httpVersionMinor() { + return 0; } - setTimeout(msecs: number, callback?: () => unknown) { - this.stream.setTimeout(callback, msecs); + get httpVersion() { + return "2.0"; } - get socket(): Socket /*| TlsSocket*/ { - notImplemented("Http2ServerRequest.socket"); - return {}; + get socket() { + const stream = this[kStream]; + const proxySocket = stream[kProxySocket]; + if (proxySocket === null) { + return stream[kProxySocket] = new Proxy(stream, proxySocketHandler); + } + return proxySocket; } - get stream(): Http2Stream { - notImplemented("Http2ServerRequest.stream"); - return new Http2Stream(); + get connection() { + return this.socket; } - get trailers(): Record { - notImplemented("Http2ServerRequest.trailers"); - return {}; + // _read(nread) { + // const state = this[kState]; + // assert(!state.closed); + // if (!state.didRead) { + // state.didRead = true; + // this[kStream].on("data", onStreamData); + // } else { + // nextTick(resumeStream, this[kStream]); + // } + // } + + get method() { + return this[kHeaders][constants.HTTP2_HEADER_METHOD]; } - get url(): string { - notImplemented("Http2ServerRequest.url"); - return ""; + set method(method) { + // validateString(method, "method"); + if (StringPrototypeTrim(method) === "") { + throw new ERR_INVALID_ARG_VALUE("method", method); + } + + this[kHeaders][constants.HTTP2_HEADER_METHOD] = method; + } + + get authority() { + return getAuthority(this[kHeaders]); + } + + get scheme() { + return this[kHeaders][constants.HTTP2_HEADER_SCHEME]; + } + + get url() { + return this[kHeaders][constants.HTTP2_HEADER_PATH]; + } + + set url(url) { + this[kHeaders][constants.HTTP2_HEADER_PATH] = url; + } + + setTimeout(msecs, callback) { + if (!this[kState].closed) { + this[kStream].setTimeout(msecs, callback); + } + return this; } } -export class Http2ServerResponse { - constructor() { +function onStreamEnd() { + // Cause the request stream to end as well. + const request = this[kRequest]; + if (request !== undefined) { + this[kRequest].push(null); } +} - addTrailers(_headers: Record) { - notImplemented("Http2ServerResponse.addTrailers"); +function onStreamError(_error) { + // This is purposefully left blank + // + // errors in compatibility mode are + // not forwarded to the request + // and response objects. +} + +function onRequestPause() { + this[kStream].pause(); +} + +function onRequestResume() { + this[kStream].resume(); +} + +function onStreamDrain() { + const response = this[kResponse]; + if (response !== undefined) { + response.emit("drain"); } +} - get connection(): Socket /*| TlsSocket*/ { - notImplemented("Http2ServerResponse.connection"); - return {}; +function onStreamAbortedRequest() { + const request = this[kRequest]; + if (request !== undefined && request[kState].closed === false) { + request[kAborted] = true; + request.emit("aborted"); } +} - createPushResponse( - _headers: Record, - _callback: () => unknown, - ) { - notImplemented("Http2ServerResponse.createPushResponse"); +function onStreamTrailersReady() { + this.sendTrailers(this[kResponse][kTrailers]); +} + +function onStreamCloseResponse() { + const res = this[kResponse]; + + if (res === undefined) { + return; } - end( - _data: string | Buffer | Uint8Array, - _encoding: string, - _callback: () => unknown, - ) { - notImplemented("Http2ServerResponse.end"); + const state = res[kState]; + + if (this.headRequest !== state.headRequest) { + return; } - get finished(): boolean { - notImplemented("Http2ServerResponse.finished"); - return false; + state.closed = true; + + this[kProxySocket] = null; + + this.removeListener("wantTrailers", onStreamTrailersReady); + this[kResponse] = undefined; + + res.emit("finish"); + res.emit("close"); +} + +function onStreamAbortedResponse() { + // non-op for now +} + +let statusMessageWarned = false; + +// Defines and implements an API compatibility layer on top of the core +// HTTP/2 implementation, intended to provide an interface that is as +// close as possible to the current require('http') API + +function statusMessageWarn() { + if (statusMessageWarned === false) { + emitWarning( + "Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)", + "UnsupportedWarning", + ); + statusMessageWarned = true; } +} - getHeader(_name: string): string { - notImplemented("Http2ServerResponse.getHeader"); - return ""; +function isConnectionHeaderAllowed(name, value) { + return name !== constants.HTTP2_HEADER_CONNECTION || + value === "trailers"; +} + +class Http2ServerResponse extends Stream { + writable = false; + req = null; + + constructor(stream, options) { + super(options); + this[kState] = { + closed: false, + ending: false, + destroyed: false, + headRequest: false, + sendDate: true, + statusCode: constants.HTTP_STATUS_OK, + }; + this[kHeaders] = { __proto__: null }; + this[kTrailers] = { __proto__: null }; + this[kStream] = stream; + stream[kProxySocket] = null; + stream[kResponse] = this; + this.writable = true; + this.req = stream[kRequest]; + stream.on("drain", onStreamDrain); + stream.on("aborted", onStreamAbortedResponse); + stream.on("close", onStreamCloseResponse); + stream.on("wantTrailers", onStreamTrailersReady); + stream.on("timeout", onStreamTimeout(kResponse)); + } + + // User land modules such as finalhandler just check truthiness of this + // but if someone is actually trying to use this for more than that + // then we simply can't support such use cases + get _header() { + return this.headersSent; + } + + get writableEnded() { + const state = this[kState]; + return state.ending; } - getHeaderNames(): string[] { - notImplemented("Http2ServerResponse.getHeaderNames"); - return []; + get finished() { + const state = this[kState]; + return state.ending; } - getHeaders(): Record { - notImplemented("Http2ServerResponse.getHeaders"); - return {}; + get socket() { + // This is compatible with http1 which removes socket reference + // only from ServerResponse but not IncomingMessage + if (this[kState].closed) { + return undefined; + } + + const stream = this[kStream]; + const proxySocket = stream[kProxySocket]; + if (proxySocket === null) { + return stream[kProxySocket] = new Proxy(stream, proxySocketHandler); + } + return proxySocket; } - hasHeader(_name: string) { - notImplemented("Http2ServerResponse.hasHeader"); + get connection() { + return this.socket; } - get headersSent(): boolean { - notImplemented("Http2ServerResponse.headersSent"); - return false; + get stream() { + return this[kStream]; } - removeHeader(_name: string) { - notImplemented("Http2ServerResponse.removeHeader"); + get headersSent() { + return this[kStream].headersSent; } - get req(): Http2ServerRequest { - notImplemented("Http2ServerResponse.req"); - return new Http2ServerRequest(); + get sendDate() { + return this[kState].sendDate; } - get sendDate(): boolean { - notImplemented("Http2ServerResponse.sendDate"); - return false; + set sendDate(bool) { + this[kState].sendDate = Boolean(bool); } - setHeader(_name: string, _value: string | string[]) { - notImplemented("Http2ServerResponse.setHeader"); + get writableCorked() { + return this[kStream].writableCorked; } - setTimeout(msecs: number, callback?: () => unknown) { - this.stream.setTimeout(msecs, callback); + get writableHighWaterMark() { + return this[kStream].writableHighWaterMark; } - get socket(): Socket /*| TlsSocket*/ { - notImplemented("Http2ServerResponse.socket"); - return {}; + get writableFinished() { + return this[kStream].writableFinished; } - get statusCode(): number { - notImplemented("Http2ServerResponse.statusCode"); - return 0; + get writableLength() { + return this[kStream].writableLength; + } + + get statusCode() { + return this[kState].statusCode; + } + + set statusCode(code) { + code |= 0; + if (code >= 100 && code < 200) { + throw new ERR_HTTP2_INFO_STATUS_NOT_ALLOWED(); + } + if (code < 100 || code > 599) { + throw new ERR_HTTP2_STATUS_INVALID(code); + } + this[kState].statusCode = code; + } + + setTrailer(name, value) { + // validateString(name, "name"); + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + // assertValidHeader(name, value); + this[kTrailers][name] = value; + } + + addTrailers(headers) { + const keys = ObjectKeys(headers); + let key = ""; + for (let i = 0; i < keys.length; i++) { + key = keys[i]; + this.setTrailer(key, headers[key]); + } + } + + getHeader(name) { + // validateString(name, "name"); + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + return this[kHeaders][name]; + } + + getHeaderNames() { + return ObjectKeys(this[kHeaders]); + } + + getHeaders() { + const headers = { __proto__: null }; + return ObjectAssign(headers, this[kHeaders]); + } + + hasHeader(name) { + // validateString(name, "name"); + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + return ObjectPrototypeHasOwnProperty(this[kHeaders], name); + } + + removeHeader(name) { + // validateString(name, "name"); + if (this[kStream].headersSent) { + throw new ERR_HTTP2_HEADERS_SENT(); + } + + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + + if (name === "date") { + this[kState].sendDate = false; + + return; + } + + delete this[kHeaders][name]; } - get statusMessage(): string { - notImplemented("Http2ServerResponse.statusMessage"); + setHeader(name, value) { + // validateString(name, "name"); + if (this[kStream].headersSent) { + throw new ERR_HTTP2_HEADERS_SENT(); + } + + this[kSetHeader](name, value); + } + + [kSetHeader](name, value) { + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + // assertValidHeader(name, value); + + if (!isConnectionHeaderAllowed(name, value)) { + return; + } + + if (name[0] === ":") { + assertValidPseudoHeader(name); + } else if (!_checkIsHttpToken(name)) { + this.destroy(new ERR_INVALID_HTTP_TOKEN("Header name", name)); + } + + this[kHeaders][name] = value; + } + + appendHeader(name, value) { + // validateString(name, "name"); + if (this[kStream].headersSent) { + throw new ERR_HTTP2_HEADERS_SENT(); + } + + this[kAppendHeader](name, value); + } + + [kAppendHeader](name, value) { + name = StringPrototypeToLowerCase(StringPrototypeTrim(name)); + // assertValidHeader(name, value); + + if (!isConnectionHeaderAllowed(name, value)) { + return; + } + + if (name[0] === ":") { + assertValidPseudoHeader(name); + } else if (!_checkIsHttpToken(name)) { + this.destroy(new ERR_INVALID_HTTP_TOKEN("Header name", name)); + } + + // Handle various possible cases the same as OutgoingMessage.appendHeader: + const headers = this[kHeaders]; + if (headers === null || !headers[name]) { + return this.setHeader(name, value); + } + + if (!ArrayIsArray(headers[name])) { + headers[name] = [headers[name]]; + } + + const existingValues = headers[name]; + if (ArrayIsArray(value)) { + for (let i = 0, length = value.length; i < length; i++) { + existingValues.push(value[i]); + } + } else { + existingValues.push(value); + } + } + + get statusMessage() { + statusMessageWarn(); + return ""; } - get stream(): Http2Stream { - notImplemented("Http2ServerResponse.stream"); - return new Http2Stream(); + set statusMessage(msg) { + statusMessageWarn(); } - get writableEnded(): boolean { - notImplemented("Http2ServerResponse.writableEnded"); - return false; + flushHeaders() { + const state = this[kState]; + if (!state.closed && !this[kStream].headersSent) { + this.writeHead(state.statusCode); + } } - write( - _chunk: string | Buffer | Uint8Array, - _encoding: string, - _callback: () => unknown, - ) { - notImplemented("Http2ServerResponse.write"); - return this.write; + writeHead(statusCode, statusMessage, headers) { + const state = this[kState]; + + if (state.closed || this.stream.destroyed) { + return this; + } + if (this[kStream].headersSent) { + throw new ERR_HTTP2_HEADERS_SENT(); + } + + if (typeof statusMessage === "string") { + statusMessageWarn(); + } + + if (headers === undefined && typeof statusMessage === "object") { + headers = statusMessage; + } + + let i; + if (ArrayIsArray(headers)) { + if (this[kHeaders]) { + // Headers in obj should override previous headers but still + // allow explicit duplicates. To do so, we first remove any + // existing conflicts, then use appendHeader. This is the + // slow path, which only applies when you use setHeader and + // then pass headers in writeHead too. + + // We need to handle both the tuple and flat array formats, just + // like the logic further below. + if (headers.length && ArrayIsArray(headers[0])) { + for (let n = 0; n < headers.length; n += 1) { + const key = headers[n + 0][0]; + this.removeHeader(key); + } + } else { + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0]; + this.removeHeader(key); + } + } + } + + // Append all the headers provided in the array: + if (headers.length && ArrayIsArray(headers[0])) { + for (i = 0; i < headers.length; i++) { + const header = headers[i]; + this[kAppendHeader](header[0], header[1]); + } + } else { + if (headers.length % 2 !== 0) { + throw new ERR_INVALID_ARG_VALUE("headers", headers); + } + + for (i = 0; i < headers.length; i += 2) { + this[kAppendHeader](headers[i], headers[i + 1]); + } + } + } else if (typeof headers === "object") { + const keys = ObjectKeys(headers); + let key = ""; + for (i = 0; i < keys.length; i++) { + key = keys[i]; + this[kSetHeader](key, headers[key]); + } + } + + state.statusCode = statusCode; + this[kBeginSend](); + + return this; } - writeContinue() { - notImplemented("Http2ServerResponse.writeContinue"); + cork() { + this[kStream].cork(); } - writeEarlyHints(_hints: Record) { - notImplemented("Http2ServerResponse.writeEarlyHints"); + uncork() { + this[kStream].uncork(); } - writeHead( - _statusCode: number, - _statusMessage: string, - _headers: Record, - ) { - notImplemented("Http2ServerResponse.writeHead"); + write(chunk, encoding, cb) { + const state = this[kState]; + + if (typeof encoding === "function") { + cb = encoding; + encoding = "utf8"; + } + + let err; + if (state.ending) { + err = new ERR_STREAM_WRITE_AFTER_END(); + } else if (state.closed) { + err = new ERR_HTTP2_INVALID_STREAM(); + } else if (state.destroyed) { + return false; + } + + if (err) { + if (typeof cb === "function") { + nextTick(cb, err); + } + this.destroy(err); + return false; + } + + const stream = this[kStream]; + if (!stream.headersSent) { + this.writeHead(state.statusCode); + } + return stream.write(chunk, encoding, cb); + } + + end(chunk, encoding, cb) { + const stream = this[kStream]; + const state = this[kState]; + + if (typeof chunk === "function") { + cb = chunk; + chunk = null; + } else if (typeof encoding === "function") { + cb = encoding; + encoding = "utf8"; + } + + if ( + (state.closed || state.ending) && + state.headRequest === stream.headRequest + ) { + if (typeof cb === "function") { + nextTick(cb); + } + return this; + } + + if (chunk !== null && chunk !== undefined) { + this.write(chunk, encoding); + } + + state.headRequest = stream.headRequest; + state.ending = true; + + if (typeof cb === "function") { + if (stream.writableEnded) { + this.once("finish", cb); + } else { + stream.once("finish", cb); + } + } + + if (!stream.headersSent) { + this.writeHead(this[kState].statusCode); + } + + if (this[kState].closed || stream.destroyed) { + ReflectApply(onStreamCloseResponse, stream, []); + } else { + stream.end(); + } + + return this; + } + + destroy(err) { + if (this[kState].destroyed) { + return; + } + + this[kState].destroyed = true; + this[kStream].destroy(err); + } + + setTimeout(msecs, callback) { + if (this[kState].closed) { + return; + } + this[kStream].setTimeout(msecs, callback); + } + + createPushResponse(headers, callback) { + // validateFunction(callback, "callback"); + if (this[kState].closed) { + nextTick(callback, new ERR_HTTP2_INVALID_STREAM()); + return; + } + this[kStream].pushStream(headers, {}, (err, stream, _headers, options) => { + if (err) { + callback(err); + return; + } + callback(null, new Http2ServerResponse(stream, options)); + }); + } + + [kBeginSend]() { + const state = this[kState]; + const headers = this[kHeaders]; + headers[constants.HTTP2_HEADER_STATUS] = state.statusCode; + const options = { + endStream: state.ending, + waitForTrailers: true, + sendDate: state.sendDate, + }; + this[kStream].respond(headers, options); + } + + writeContinue() { + const stream = this[kStream]; + if (stream.headersSent || this[kState].closed) { + return false; + } + stream.additionalHeaders({ + [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_CONTINUE, + }); + return true; + } + + writeEarlyHints(hints) { + // validateObject(hints, "hints"); + + const headers = { __proto__: null }; + + // const linkHeaderValue = validateLinkHeaderValue(hints.link); + + for (const key of ObjectKeys(hints)) { + if (key !== "link") { + headers[key] = hints[key]; + } + } + + // if (linkHeaderValue.length === 0) { + // return false; + // } + + const stream = this[kStream]; + + if (stream.headersSent || this[kState].closed) { + return false; + } + + stream.additionalHeaders({ + ...headers, + [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_EARLY_HINTS, + // "Link": linkHeaderValue, + }); + + return true; } } @@ -2130,4 +2940,4 @@ export default { sensitiveHeaders, Http2ServerRequest, Http2ServerResponse, -}; \ No newline at end of file +}; diff --git a/crates/node/polyfills/internal/blocklist.mjs b/crates/node/polyfills/internal/blocklist.mjs new file mode 100644 index 000000000..a9aba03b6 --- /dev/null +++ b/crates/node/polyfills/internal/blocklist.mjs @@ -0,0 +1,227 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +import { primordials } from "ext:core/mod.js"; +import { + op_blocklist_add_address, + op_blocklist_add_range, + op_blocklist_add_subnet, + op_blocklist_check, + op_blocklist_new, + op_socket_address_get_serialization, + op_socket_address_parse, +} from "ext:core/ops"; + +import { + validateInt32, + validateObject, + validatePort, + validateString, + validateUint32, +} from "ext:deno_node/internal/validators.mjs"; +import { ERR_INVALID_ARG_VALUE } from "ext:deno_node/internal/errors.ts"; +import { customInspectSymbol } from "ext:deno_node/internal/util.mjs"; +import { inspect } from "ext:deno_node/internal/util/inspect.mjs"; + +const { Symbol } = primordials; + +const internalBlockList = Symbol("blocklist"); + +class BlockList { + constructor() { + this[internalBlockList] = op_blocklist_new(); + } + + [customInspectSymbol](depth, options) { + if (depth < 0) { + return this; + } + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `BlockList ${ + inspect({ + rules: [], // TODO(satyarohith): provide the actual rules + }, opts) + }`; + } + + addAddress(address, family = "ipv4") { + if (!SocketAddress.isSocketAddress(address)) { + validateString(address, "address"); + validateString(family, "family"); + new SocketAddress({ + address, + family, + }); + } else { + address = address.address; + } + op_blocklist_add_address(this[internalBlockList], address); + } + + addRange(start, end, family = "ipv4") { + if (!SocketAddress.isSocketAddress(start)) { + validateString(start, "start"); + validateString(family, "family"); + new SocketAddress({ + address: start, + family, + }); + } else { + start = start.address; + } + if (!SocketAddress.isSocketAddress(end)) { + validateString(end, "end"); + validateString(family, "family"); + new SocketAddress({ + address: end, + family, + }); + } else { + end = end.address; + } + const ret = op_blocklist_add_range(this[internalBlockList], start, end); + if (ret === false) { + throw new ERR_INVALID_ARG_VALUE("start", start, "must come before end"); + } + } + + addSubnet(network, prefix, family = "ipv4") { + if (!SocketAddress.isSocketAddress(network)) { + validateString(network, "network"); + validateString(family, "family"); + new SocketAddress({ + address: network, + family, + }); + } else { + network = network.address; + family = network.family; + } + switch (family) { + case "ipv4": + validateInt32(prefix, "prefix", 0, 32); + break; + case "ipv6": + validateInt32(prefix, "prefix", 0, 128); + break; + } + op_blocklist_add_subnet(this[internalBlockList], network, prefix); + } + + check(address, family = "ipv4") { + if (!SocketAddress.isSocketAddress(address)) { + validateString(address, "address"); + validateString(family, "family"); + try { + new SocketAddress({ + address, + family, + }); + } catch { + // Ignore the error. If it's not a valid address, return false. + return false; + } + } else { + family = address.family; + address = address.address; + } + try { + return op_blocklist_check(this[internalBlockList], address, family); + } catch (_) { + // Node API expects false as return value if the address is invalid. + // Example: `blocklist.check("1.1.1.1", "ipv6")` should return false. + return false; + } + } + + get rules() { + // TODO(satyarohith): return the actual rules + return []; + } +} + +const kDetail = Symbol("kDetail"); + +class SocketAddress { + static isSocketAddress(value) { + return value?.[kDetail] !== undefined; + } + + constructor(options = kEmptyObject) { + validateObject(options, "options"); + let { family = "ipv4" } = options; + const { + address = (family === "ipv4" ? "127.0.0.1" : "::"), + port = 0, + flowlabel = 0, + } = options; + + if (typeof family?.toLowerCase === "function") { + // deno-lint-ignore prefer-primordials + family = family.toLowerCase(); + } + switch (family) { + case "ipv4": + break; + case "ipv6": + break; + default: + throw new ERR_INVALID_ARG_VALUE("options.family", options.family); + } + + validateString(address, "options.address"); + validatePort(port, "options.port"); + validateUint32(flowlabel, "options.flowlabel", false); + + this[kDetail] = { + address, + port, + family, + flowlabel, + }; + const useInput = op_socket_address_parse( + address, + port, + family, + ); + if (!useInput) { + const { 0: address_, 1: family_ } = op_socket_address_get_serialization(); + this[kDetail].address = address_; + this[kDetail].family = family_; + } + } + + get address() { + return this[kDetail].address; + } + + get port() { + return this[kDetail].port; + } + + get family() { + return this[kDetail].family; + } + + get flowlabel() { + // TODO(satyarohith): Implement this in Rust. + // The flow label can be changed internally. + return this[kDetail].flowlabel; + } + + toJSON() { + return { + address: this.address, + port: this.port, + family: this.family, + flowlabel: this.flowlabel, + }; + } +} + +export { BlockList, SocketAddress }; diff --git a/crates/node/polyfills/internal/buffer.mjs b/crates/node/polyfills/internal/buffer.mjs index 5c76a21a5..c32494555 100644 --- a/crates/node/polyfills/internal/buffer.mjs +++ b/crates/node/polyfills/internal/buffer.mjs @@ -6,6 +6,7 @@ // deno-lint-ignore-file prefer-primordials import { core } from "ext:core/mod.js"; +import { op_is_ascii, op_is_utf8 } from "ext:core/ops"; import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js"; import { codes } from "ext:deno_node/internal/error_codes.ts"; @@ -26,10 +27,12 @@ import { import { isAnyArrayBuffer, isArrayBufferView, + isTypedArray, } from "ext:deno_node/internal/util/types.ts"; import { normalizeEncoding } from "ext:deno_node/internal/util.mjs"; import { validateBuffer } from "ext:deno_node/internal/validators.mjs"; import { isUint8Array } from "ext:deno_node/internal/util/types.ts"; +import { ERR_INVALID_STATE, NodeError } from "ext:deno_node/internal/errors.ts"; import { forgivingBase64Encode, forgivingBase64UrlEncode, @@ -164,10 +167,7 @@ Object.setPrototypeOf(Buffer.prototype, Uint8Array.prototype); Object.setPrototypeOf(Buffer, Uint8Array); function assertSize(size) { - validateNumber(size, "size"); - if (!(size >= 0 && size <= kMaxLength)) { - throw new codes.ERR_INVALID_ARG_VALUE.RangeError("size", size); - } + validateNumber(size, "size", 0, kMaxLength); } function _alloc(size, fill, encoding) { @@ -220,12 +220,9 @@ function fromString(string, encoding) { return buf; } -function fromArrayLike(array) { - const length = array.length < 0 ? 0 : checked(array.length) | 0; - const buf = createBuffer(length); - for (let i = 0; i < length; i += 1) { - buf[i] = array[i] & 255; - } +function fromArrayLike(obj) { + const buf = new Uint8Array(obj); + Object.setPrototypeOf(buf, Buffer.prototype); return buf; } @@ -234,6 +231,7 @@ function fromObject(obj) { if (typeof obj.length !== "number") { return createBuffer(0); } + return fromArrayLike(obj); } @@ -849,7 +847,14 @@ function _base64Slice(buf, start, end) { const decoder = new TextDecoder(); function _utf8Slice(buf, start, end) { - return decoder.decode(buf.slice(start, end)); + try { + return decoder.decode(buf.slice(start, end)); + } catch (err) { + if (err instanceof TypeError) { + throw new NodeError("ERR_STRING_TOO_LONG", "String too long"); + } + throw err; + } } function _latin1Slice(buf, start, end) { @@ -2294,10 +2299,23 @@ export function boundsError(value, length, type) { ); } -export function validateNumber(value, name) { +export function validateNumber(value, name, min = undefined, max) { if (typeof value !== "number") { throw new codes.ERR_INVALID_ARG_TYPE(name, "number", value); } + + if ( + (min != null && value < min) || (max != null && value > max) || + ((min != null || max != null) && Number.isNaN(value)) + ) { + throw new codes.ERR_OUT_OF_RANGE( + name, + `${min != null ? `>= ${min}` : ""}${ + min != null && max != null ? " && " : "" + }${max != null ? `<= ${max}` : ""}`, + value, + ); + } } function checkInt(value, min, max, buf, offset, byteLength) { @@ -2536,12 +2554,58 @@ export function writeU_Int24LE(buf, value, offset, min, max) { return offset; } +export function isUtf8(input) { + if (isTypedArray(input)) { + if (input.buffer.detached) { + throw new ERR_INVALID_STATE("Cannot validate on a detached buffer"); + } + return op_is_utf8(input); + } + + if (isAnyArrayBuffer(input)) { + if (input.detached) { + throw new ERR_INVALID_STATE("Cannot validate on a detached buffer"); + } + return op_is_utf8(new Uint8Array(input)); + } + + throw new codes.ERR_INVALID_ARG_TYPE("input", [ + "ArrayBuffer", + "Buffer", + "TypedArray", + ], input); +} + +export function isAscii(input) { + if (isTypedArray(input)) { + if (input.buffer.detached) { + throw new ERR_INVALID_STATE("Cannot validate on a detached buffer"); + } + return op_is_ascii(input); + } + + if (isAnyArrayBuffer(input)) { + if (input.detached) { + throw new ERR_INVALID_STATE("Cannot validate on a detached buffer"); + } + return op_is_ascii(new Uint8Array(input)); + } + + throw new codes.ERR_INVALID_ARG_TYPE("input", [ + "ArrayBuffer", + "Buffer", + "TypedArray", + ], input); +} + export default { atob, btoa, Blob, Buffer, constants, + isAscii, + isUtf8, kMaxLength, kStringMaxLength, SlowBuffer, diff --git a/crates/node/polyfills/internal/child_process.ts b/crates/node/polyfills/internal/child_process.ts index b6137e0d1..cabae63ee 100644 --- a/crates/node/polyfills/internal/child_process.ts +++ b/crates/node/polyfills/internal/child_process.ts @@ -362,17 +362,25 @@ export class ChildProcess extends EventEmitter { } } -const supportedNodeStdioTypes: NodeStdio[] = ["pipe", "ignore", "inherit"]; +const supportedNodeStdioTypes: NodeStdio[] = [ + "pipe", + "ignore", + "inherit", + "ipc", +]; function toDenoStdio( pipe: NodeStdio | number | Stream | null | undefined, ): DenoStdio { if (pipe instanceof Stream) { return "inherit"; } + if (typeof pipe === "number") { + /* Assume it's a rid returned by fs APIs */ + return pipe; + } if ( - !supportedNodeStdioTypes.includes(pipe as NodeStdio) || - typeof pipe === "number" + !supportedNodeStdioTypes.includes(pipe as NodeStdio) ) { notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`); } @@ -385,6 +393,8 @@ function toDenoStdio( return "null"; case "inherit": return "inherit"; + case "ipc": + return "ipc_for_internal_use"; default: notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`); } @@ -1083,8 +1093,7 @@ function toDenoArgs(args: string[]): string[] { if (useRunArgs) { // -A is not ideal, but needed to propagate permissions. - // --unstable is needed for Node compat. - denoArgs.unshift("run", "-A", "--unstable"); + denoArgs.unshift("run", "-A"); } return denoArgs; diff --git a/crates/node/polyfills/internal/cli_table.ts b/crates/node/polyfills/internal/cli_table.ts index 574081ba4..9826e524f 100644 --- a/crates/node/polyfills/internal/cli_table.ts +++ b/crates/node/polyfills/internal/cli_table.ts @@ -27,11 +27,10 @@ const renderRow = (row: string[], columnWidths: number[]) => { for (let i = 0; i < row.length; i++) { const cell = row[i]; const len = getStringWidth(cell); - const needed = (columnWidths[i] - len) / 2; + const needed = columnWidths[i] - len; // round(needed) + ceil(needed) will always add up to the amount // of spaces we need while also left justifying the output. - out += " ".repeat(needed) + cell + - " ".repeat(Math.ceil(needed)); + out += cell + " ".repeat(Math.ceil(needed)); if (i !== row.length - 1) { out += tableChars.middle; } diff --git a/crates/node/polyfills/internal/crypto/hash.ts b/crates/node/polyfills/internal/crypto/hash.ts index a1d61f953..2e040be25 100644 --- a/crates/node/polyfills/internal/crypto/hash.ts +++ b/crates/node/polyfills/internal/crypto/hash.ts @@ -13,8 +13,8 @@ import { op_node_hash_update, op_node_hash_update_str, } from "ext:core/ops"; +import { primordials } from "ext:core/mod.js"; -import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; import { Buffer } from "node:buffer"; import { Transform } from "node:stream"; import { @@ -22,7 +22,11 @@ import { forgivingBase64UrlEncode as encodeToBase64Url, } from "ext:deno_web/00_infra.js"; import type { TransformOptions } from "ext:deno_node/_stream.d.ts"; -import { validateString } from "ext:deno_node/internal/validators.mjs"; +import { + validateEncoding, + validateString, + validateUint32, +} from "ext:deno_node/internal/validators.mjs"; import type { BinaryToTextEncoding, Encoding, @@ -32,119 +36,148 @@ import { KeyObject, prepareSecretKey, } from "ext:deno_node/internal/crypto/keys.ts"; +import { + ERR_CRYPTO_HASH_FINALIZED, + ERR_INVALID_ARG_TYPE, + NodeError, +} from "ext:deno_node/internal/errors.ts"; +import LazyTransform from "ext:deno_node/internal/streams/lazy_transform.mjs"; +import { + getDefaultEncoding, + toBuf, +} from "ext:deno_node/internal/crypto/util.ts"; +import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; + +const { ReflectApply, ObjectSetPrototypeOf } = primordials; -// TODO(@littledivy): Use Result instead of boolean when -// https://bugs.chromium.org/p/v8/issues/detail?id=13600 is fixed. function unwrapErr(ok: boolean) { - if (!ok) { - throw new Error("Context is not initialized"); - } + if (!ok) throw new ERR_CRYPTO_HASH_FINALIZED(); } -const coerceToBytes = (data: string | BufferSource): Uint8Array => { - if (data instanceof Uint8Array) { - return data; - } else if (typeof data === "string") { - // This assumes UTF-8, which may not be correct. - return new TextEncoder().encode(data); - } else if (ArrayBuffer.isView(data)) { - return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - } else if (data instanceof ArrayBuffer) { - return new Uint8Array(data); - } else { - throw new TypeError("expected data to be string | BufferSource"); - } -}; +declare const __hasher: unique symbol; +type Hasher = { __hasher: typeof __hasher }; -/** - * The Hash class is a utility for creating hash digests of data. It can be used in one of two ways: - * - * - As a stream that is both readable and writable, where data is written to produce a computed hash digest on the readable side, or - * - Using the hash.update() and hash.digest() methods to produce the computed hash. - * - * The crypto.createHash() method is used to create Hash instances. Hash objects are not to be created directly using the new keyword. - */ -export class Hash extends Transform { - #context: number; +const kHandle = Symbol("kHandle"); - constructor( - algorithm: string | number, - _opts?: TransformOptions, - ) { - super({ - transform(chunk: string, _encoding: string, callback: () => void) { - op_node_hash_update(context, coerceToBytes(chunk)); - callback(); - }, - flush(callback: () => void) { - this.push(this.digest(undefined)); - callback(); - }, - }); +export function Hash( + this: Hash, + algorithm: string | Hasher, + options?: { outputLength?: number }, +): Hash { + if (!(this instanceof Hash)) { + return new Hash(algorithm, options); + } + if (!(typeof algorithm === "object")) { + validateString(algorithm, "algorithm"); + } + const xofLen = typeof options === "object" && options !== null + ? options.outputLength + : undefined; + if (xofLen !== undefined) { + validateUint32(xofLen, "options.outputLength"); + } - if (typeof algorithm === "string") { - this.#context = op_node_create_hash( - algorithm.toLowerCase(), + try { + this[kHandle] = typeof algorithm === "object" + ? op_node_hash_clone(algorithm, xofLen) + : op_node_create_hash(algorithm.toLowerCase(), xofLen); + } catch (err) { + // TODO(lucacasonato): don't do this + if (err.message === "Output length mismatch for non-extendable algorithm") { + throw new NodeError( + "ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH", + "Invalid XOF digest length", ); - if (this.#context === 0) { - throw new TypeError(`Unknown hash algorithm: ${algorithm}`); - } } else { - this.#context = algorithm; + throw err; } + } + + if (this[kHandle] === null) throw new ERR_CRYPTO_HASH_FINALIZED(); + + ReflectApply(LazyTransform, this, [options]); +} - const context = this.#context; +interface Hash { + [kHandle]: object; +} + +ObjectSetPrototypeOf(Hash.prototype, LazyTransform.prototype); +ObjectSetPrototypeOf(Hash, LazyTransform); + +Hash.prototype.copy = function copy(options?: { outputLength: number }) { + return new Hash(this[kHandle], options); +}; + +Hash.prototype._transform = function _transform( + chunk: string | Buffer, + encoding: Encoding | "buffer", + callback: () => void, +) { + this.update(chunk, encoding); + callback(); +}; + +Hash.prototype._flush = function _flush(callback: () => void) { + this.push(this.digest()); + callback(); +}; + +Hash.prototype.update = function update( + data: string | Buffer, + encoding: Encoding | "buffer", +) { + encoding = encoding || getDefaultEncoding(); + + if (typeof data === "string") { + validateEncoding(data, encoding); + } else if (!isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE( + "data", + ["string", "Buffer", "TypedArray", "DataView"], + data, + ); } - copy(): Hash { - return new Hash(op_node_hash_clone(this.#context)); + if ( + typeof data === "string" && (encoding === "utf8" || encoding === "buffer") + ) { + unwrapErr(op_node_hash_update_str(this[kHandle], data)); + } else { + unwrapErr(op_node_hash_update(this[kHandle], toBuf(data, encoding))); } - /** - * Updates the hash content with the given data. - */ - update(data: string | ArrayBuffer, _encoding?: string): this { - if (typeof data === "string") { - unwrapErr(op_node_hash_update_str(this.#context, data)); - } else { - unwrapErr(op_node_hash_update(this.#context, coerceToBytes(data))); - } + return this; +}; - return this; - } +Hash.prototype.digest = function digest(outputEncoding: Encoding | "buffer") { + outputEncoding = outputEncoding || getDefaultEncoding(); + outputEncoding = `${outputEncoding}`; - /** - * Calculates the digest of all of the data. - * - * If encoding is provided a string will be returned; otherwise a Buffer is returned. - * - * Supported encodings are currently 'hex', 'binary', 'base64', 'base64url'. - */ - digest(encoding?: string): Buffer | string { - if (encoding === "hex") { - return op_node_hash_digest_hex(this.#context); - } + if (outputEncoding === "hex") { + const result = op_node_hash_digest_hex(this[kHandle]); + if (result === null) throw new ERR_CRYPTO_HASH_FINALIZED(); + return result; + } - const digest = op_node_hash_digest(this.#context); - if (encoding === undefined) { + const digest = op_node_hash_digest(this[kHandle]); + if (digest === null) throw new ERR_CRYPTO_HASH_FINALIZED(); + + // TODO(@littedivy): Fast paths for below encodings. + switch (outputEncoding) { + case "binary": + return String.fromCharCode(...digest); + case "base64": + return encodeToBase64(digest); + case "base64url": + return encodeToBase64Url(digest); + case undefined: + case "buffer": return Buffer.from(digest); - } - - // TODO(@littedivy): Fast paths for below encodings. - switch (encoding) { - case "binary": - return String.fromCharCode(...digest); - case "base64": - return encodeToBase64(digest); - case "base64url": - return encodeToBase64Url(digest); - case "buffer": - return Buffer.from(digest); - default: - return Buffer.from(digest).toString(encoding); - } + default: + return Buffer.from(digest).toString(outputEncoding); } -} +}; export function Hmac( hmac: string, @@ -171,7 +204,7 @@ class HmacImpl extends Transform { super({ transform(chunk: string, encoding: string, callback: () => void) { // deno-lint-ignore no-explicit-any - self.update(coerceToBytes(chunk), encoding as any); + self.update(Buffer.from(chunk), encoding as any); callback(); }, flush(callback: () => void) { @@ -219,9 +252,10 @@ class HmacImpl extends Transform { digest(encoding?: BinaryToTextEncoding): Buffer | string { const result = this.#hash.digest(); - return new Hash(this.#algorithm).update(this.#opad).update(result).digest( - encoding, - ); + return new Hash(this.#algorithm).update(this.#opad).update(result) + .digest( + encoding, + ); } update(data: string | ArrayBuffer, inputEncoding?: Encoding): this { diff --git a/crates/node/polyfills/internal/crypto/hkdf.ts b/crates/node/polyfills/internal/crypto/hkdf.ts index 0a8dcbb2e..cca40a3c6 100644 --- a/crates/node/polyfills/internal/crypto/hkdf.ts +++ b/crates/node/polyfills/internal/crypto/hkdf.ts @@ -23,6 +23,7 @@ import { } from "ext:deno_node/internal/crypto/util.ts"; import { createSecretKey, + getKeyMaterial, isKeyObject, KeyObject, } from "ext:deno_node/internal/crypto/keys.ts"; @@ -35,7 +36,7 @@ import { const validateParameters = hideStackFrames((hash, key, salt, info, length) => { validateString(hash, "digest"); - key = new Uint8Array(prepareKey(key)); + key = getKeyMaterial(prepareKey(key)); validateByteSource(salt, "salt"); validateByteSource(info, "info"); @@ -108,6 +109,8 @@ export function hkdf( validateFunction(callback, "callback"); + hash = hash.toLowerCase(); + op_node_hkdf_async(hash, key, salt, info, length) .then((okm) => callback(null, okm.buffer)) .catch((err) => callback(new ERR_CRYPTO_INVALID_DIGEST(err), undefined)); @@ -128,6 +131,8 @@ export function hkdfSync( length, )); + hash = hash.toLowerCase(); + const okm = new Uint8Array(length); try { op_node_hkdf(hash, key, salt, info, okm); diff --git a/crates/node/polyfills/internal/crypto/keys.ts b/crates/node/polyfills/internal/crypto/keys.ts index 8cb9ab690..ca22e12c6 100644 --- a/crates/node/polyfills/internal/crypto/keys.ts +++ b/crates/node/polyfills/internal/crypto/keys.ts @@ -4,6 +4,13 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials +import { primordials } from "ext:core/mod.js"; + +const { + ObjectDefineProperties, + SymbolToStringTag, +} = primordials; + import { op_node_create_private_key, op_node_create_public_key, @@ -209,6 +216,14 @@ export class KeyObject { } } +ObjectDefineProperties(KeyObject.prototype, { + [SymbolToStringTag]: { + __proto__: null, + configurable: true, + value: "KeyObject", + }, +}); + export interface JsonWebKeyInput { key: JsonWebKey; format: "jwk"; diff --git a/crates/node/polyfills/internal/crypto/pbkdf2.ts b/crates/node/polyfills/internal/crypto/pbkdf2.ts index 5cf102fa9..4e58cb68b 100644 --- a/crates/node/polyfills/internal/crypto/pbkdf2.ts +++ b/crates/node/polyfills/internal/crypto/pbkdf2.ts @@ -7,8 +7,19 @@ import { op_node_pbkdf2, op_node_pbkdf2_async } from "ext:core/ops"; import { Buffer } from "node:buffer"; import { HASH_DATA } from "ext:deno_node/internal/crypto/types.ts"; +import { + validateFunction, + validateString, + validateUint32, +} from "ext:deno_node/internal/validators.mjs"; +import { getArrayBufferOrView } from "ext:deno_node/internal/crypto/keys.ts"; +import { + ERR_CRYPTO_INVALID_DIGEST, + ERR_OUT_OF_RANGE, +} from "ext:deno_node/internal/errors.ts"; export const MAX_ALLOC = Math.pow(2, 30) - 1; +export const MAX_I32 = 2 ** 31 - 1; export type NormalizedAlgorithms = | "md5" @@ -29,6 +40,30 @@ export type Algorithms = | "sha384" | "sha512"; +function check( + password: HASH_DATA, + salt: HASH_DATA, + iterations: number, + keylen: number, + digest: string, +) { + validateString(digest, "digest"); + password = getArrayBufferOrView(password, "password", "buffer"); + salt = getArrayBufferOrView(salt, "salt", "buffer"); + validateUint32(iterations, "iterations", true); + validateUint32(keylen, "keylen"); + + if (iterations > MAX_I32) { + throw new ERR_OUT_OF_RANGE("iterations", `<= ${MAX_I32}`, iterations); + } + + if (keylen > MAX_I32) { + throw new ERR_OUT_OF_RANGE("keylen", `<= ${MAX_I32}`, keylen); + } + + return { password, salt, iterations, keylen, digest }; +} + /** * @param iterations Needs to be higher or equal than zero * @param keylen Needs to be higher or equal than zero but less than max allocation size (2^30) @@ -39,18 +74,21 @@ export function pbkdf2Sync( salt: HASH_DATA, iterations: number, keylen: number, - digest: Algorithms = "sha1", + digest: string, ): Buffer { - if (typeof iterations !== "number" || iterations < 0) { - throw new TypeError("Bad iterations"); - } - if (typeof keylen !== "number" || keylen < 0 || keylen > MAX_ALLOC) { - throw new TypeError("Bad key length"); - } + ({ password, salt, iterations, keylen, digest } = check( + password, + salt, + iterations, + keylen, + digest, + )); + + digest = digest.toLowerCase() as NormalizedAlgorithms; const DK = new Uint8Array(keylen); if (!op_node_pbkdf2(password, salt, iterations, digest, DK)) { - throw new Error("Invalid digest"); + throw new ERR_CRYPTO_INVALID_DIGEST(digest); } return Buffer.from(DK); @@ -66,16 +104,26 @@ export function pbkdf2( salt: HASH_DATA, iterations: number, keylen: number, - digest: Algorithms = "sha1", + digest: string, callback: (err: Error | null, derivedKey?: Buffer) => void, ) { - if (typeof iterations !== "number" || iterations < 0) { - throw new TypeError("Bad iterations"); - } - if (typeof keylen !== "number" || keylen < 0 || keylen > MAX_ALLOC) { - throw new TypeError("Bad key length"); + if (typeof digest === "function") { + callback = digest; + digest = undefined as unknown as string; } + ({ password, salt, iterations, keylen, digest } = check( + password, + salt, + iterations, + keylen, + digest, + )); + + validateFunction(callback, "callback"); + + digest = digest.toLowerCase() as NormalizedAlgorithms; + op_node_pbkdf2_async( password, salt, diff --git a/crates/node/polyfills/internal/crypto/sig.ts b/crates/node/polyfills/internal/crypto/sig.ts index 5a9611780..473670d2a 100644 --- a/crates/node/polyfills/internal/crypto/sig.ts +++ b/crates/node/polyfills/internal/crypto/sig.ts @@ -67,10 +67,6 @@ export class SignImpl extends Writable { algorithm = algorithm.toLowerCase(); - if (algorithm.startsWith("rsa-")) { - // Allows RSA-[digest_algorithm] as a valid algorithm - algorithm = algorithm.slice(4); - } this.#digestType = algorithm; this.hash = createHash(this.#digestType); } @@ -121,11 +117,6 @@ export class VerifyImpl extends Writable { algorithm = algorithm.toLowerCase(); - if (algorithm.startsWith("rsa-")) { - // Allows RSA-[digest_algorithm] as a valid algorithm - algorithm = algorithm.slice(4); - } - this.#digestType = algorithm; this.hash = createHash(this.#digestType); } diff --git a/crates/node/polyfills/internal/crypto/types.ts b/crates/node/polyfills/internal/crypto/types.ts index 2b3ce34fe..45c0ea286 100644 --- a/crates/node/polyfills/internal/crypto/types.ts +++ b/crates/node/polyfills/internal/crypto/types.ts @@ -3,7 +3,7 @@ import { Buffer } from "../../buffer.ts"; -export type HASH_DATA = string | ArrayBufferView | Buffer; +export type HASH_DATA = string | ArrayBufferView | Buffer | ArrayBuffer; export type BinaryToTextEncoding = "base64" | "base64url" | "hex" | "binary"; diff --git a/crates/node/polyfills/internal/errors.ts b/crates/node/polyfills/internal/errors.ts index c3aeff8b2..6529e9894 100644 --- a/crates/node/polyfills/internal/errors.ts +++ b/crates/node/polyfills/internal/errors.ts @@ -349,9 +349,8 @@ export class NodeErrorAbstraction extends Error { super(message); this.code = code; this.name = name; - //This number changes depending on the name of this class - //20 characters as of now - this.stack = this.stack && `${name} [${this.code}]${this.stack.slice(20)}`; + this.stack = this.stack && + `${name} [${this.code}]${this.stack.slice(this.name.length)}`; } override toString() { @@ -614,7 +613,6 @@ export class ERR_INVALID_ARG_TYPE_RANGE extends NodeRangeError { export class ERR_INVALID_ARG_TYPE extends NodeTypeError { constructor(name: string, expected: string | string[], actual: unknown) { const msg = createInvalidArgType(name, expected); - super("ERR_INVALID_ARG_TYPE", `${msg}.${invalidArgTypeHelper(actual)}`); } @@ -669,9 +667,7 @@ function invalidArgTypeHelper(input: any) { return ` Received type ${typeof input} (${inspected})`; } -export class ERR_OUT_OF_RANGE extends RangeError { - code = "ERR_OUT_OF_RANGE"; - +export class ERR_OUT_OF_RANGE extends NodeRangeError { constructor( str: string, range: string, @@ -696,15 +692,7 @@ export class ERR_OUT_OF_RANGE extends RangeError { } msg += ` It must be ${range}. Received ${received}`; - super(msg); - - const { name } = this; - // Add the error code to the name to include it in the stack trace. - this.name = `${name} [${this.code}]`; - // Access the stack to generate the error message including the error code from the name. - this.stack; - // Reset the name to the actual name. - this.name = name; + super("ERR_OUT_OF_RANGE", msg); } } @@ -2564,6 +2552,12 @@ export class ERR_HTTP_SOCKET_ASSIGNED extends NodeError { } } +export class ERR_INVALID_STATE extends NodeError { + constructor(message: string) { + super("ERR_INVALID_STATE", `Invalid state: ${message}`); + } +} + interface UvExceptionContext { syscall: string; path?: string; @@ -2824,6 +2818,7 @@ export default { ERR_INVALID_RETURN_PROPERTY, ERR_INVALID_RETURN_PROPERTY_VALUE, ERR_INVALID_RETURN_VALUE, + ERR_INVALID_STATE, ERR_INVALID_SYNC_FORK_INPUT, ERR_INVALID_THIS, ERR_INVALID_TUPLE, diff --git a/crates/node/polyfills/internal/fixed_queue.ts b/crates/node/polyfills/internal/fixed_queue.ts index d98a5e507..0a2209c8d 100644 --- a/crates/node/polyfills/internal/fixed_queue.ts +++ b/crates/node/polyfills/internal/fixed_queue.ts @@ -1,8 +1,8 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright Joyent, Inc. and other Node contributors. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials +import { primordials } from "ext:core/mod.js"; +const { ArrayFrom } = primordials; // Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two. const kSize = 2048; @@ -65,7 +65,7 @@ class FixedCircularBuffer { constructor() { this.bottom = 0; this.top = 0; - this.list = new Array(kSize); + this.list = ArrayFrom({ __proto__: null, length: kSize }); this.next = null; } @@ -111,11 +111,13 @@ export class FixedQueue { // and sets it as the new main queue. this.head = this.head.next = new FixedCircularBuffer(); } + // deno-lint-ignore prefer-primordials -- `push` is a method of `FixedCircularBuffer` this.head.push(data); } shift() { const tail = this.tail; + // deno-lint-ignore prefer-primordials -- `shift` is a method of `FixedCircularBuffer` const next = tail.shift(); if (tail.isEmpty() && tail.next !== null) { // If there is another queue, it forms the new tail. diff --git a/crates/node/polyfills/internal/fs/handle.ts b/crates/node/polyfills/internal/fs/handle.ts index ce218f24e..e422e2ba0 100644 --- a/crates/node/polyfills/internal/fs/handle.ts +++ b/crates/node/polyfills/internal/fs/handle.ts @@ -28,27 +28,27 @@ export class FileHandle extends EventEmitter { #rid: number; constructor(rid: number) { super(); - this.rid = rid; + this.#rid = rid; } get fd() { - return this.rid; + return this.#rid; } read( - buffer: Buffer, + buffer: Uint8Array, offset?: number, length?: number, position?: number | null, ): Promise; read(options?: ReadOptions): Promise; read( - bufferOrOpt: Buffer | ReadOptions, + bufferOrOpt: Uint8Array | ReadOptions, offset?: number, length?: number, position?: number | null, ): Promise { - if (bufferOrOpt instanceof Buffer) { + if (bufferOrOpt instanceof Uint8Array) { return new Promise((resolve, reject) => { read( this.fd, @@ -90,12 +90,12 @@ export class FileHandle extends EventEmitter { encoding: string, ): Promise; write( - bufferOrStr: Buffer | string, + bufferOrStr: Uint8Array | string, offsetOrPosition: number, lengthOrEncoding: number | string, position?: number, ): Promise { - if (bufferOrStr instanceof Buffer) { + if (bufferOrStr instanceof Uint8Array) { const buffer = bufferOrStr; const offset = offsetOrPosition; const length = lengthOrEncoding; diff --git a/crates/node/polyfills/internal/fs/utils.mjs b/crates/node/polyfills/internal/fs/utils.mjs index 09169127d..21c5892ce 100644 --- a/crates/node/polyfills/internal/fs/utils.mjs +++ b/crates/node/polyfills/internal/fs/utils.mjs @@ -5,6 +5,8 @@ "use strict"; +import { primordials } from "ext:core/mod.js"; +const { DatePrototypeGetTime } = primordials; import { Buffer } from "node:buffer"; import { ERR_FS_EISDIR, @@ -23,7 +25,6 @@ import { isUint8Array, } from "ext:deno_node/internal/util/types.ts"; import { once } from "ext:deno_node/internal/util.mjs"; -import { deprecate } from "node:util"; import { toPathIfFileURL } from "ext:deno_node/internal/url.ts"; import { validateAbortSignal, @@ -699,7 +700,7 @@ export function toUnixTimestamp(time, name = "time") { } if (isDate(time)) { // Convert to 123.456 UNIX timestamp - return Date.getTime(time) / 1000; + return DatePrototypeGetTime(time) / 1000; } throw new ERR_INVALID_ARG_TYPE(name, ["Date", "Time in seconds"], time); } @@ -891,7 +892,7 @@ export const validateRmOptionsSync = hideStackFrames( message: "is a directory", path, syscall: "rm", - errno: EISDIR, + errno: osConstants.errno.EISDIR, }); } } @@ -959,24 +960,13 @@ export const getValidMode = hideStackFrames((mode, type) => { export const validateStringAfterArrayBufferView = hideStackFrames( (buffer, name) => { - if (typeof buffer === "string") { - return; - } - - if ( - typeof buffer === "object" && - buffer !== null && - typeof buffer.toString === "function" && - Object.prototype.hasOwnProperty.call(buffer, "toString") - ) { - return; + if (typeof buffer !== "string") { + throw new ERR_INVALID_ARG_TYPE( + name, + ["string", "Buffer", "TypedArray", "DataView"], + buffer, + ); } - - throw new ERR_INVALID_ARG_TYPE( - name, - ["string", "Buffer", "TypedArray", "DataView"], - buffer, - ); }, ); @@ -1005,12 +995,6 @@ export const constants = { kWriteFileMaxChunkSize, }; -export const showStringCoercionDeprecation = deprecate( - () => {}, - "Implicit coercion of objects with own toString property is deprecated.", - "DEP0162", -); - export default { constants, assertEncoding, @@ -1030,7 +1014,6 @@ export default { preprocessSymlinkDestination, realpathCacheKey, getStatsFromBinding, - showStringCoercionDeprecation, stringToFlags, stringToSymlinkType, Stats, diff --git a/crates/node/polyfills/internal/util.mjs b/crates/node/polyfills/internal/util.mjs index 596599859..e6b32d17d 100644 --- a/crates/node/polyfills/internal/util.mjs +++ b/crates/node/polyfills/internal/util.mjs @@ -15,6 +15,10 @@ import { } from "ext:deno_node/internal/primordials.mjs"; import { ERR_UNKNOWN_SIGNAL } from "ext:deno_node/internal/errors.ts"; import { os } from "ext:deno_node/internal_binding/constants.ts"; +import { primordials } from "ext:core/mod.js"; +const { + SafeWeakRef, +} = primordials; export const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom"); export const kEnumerableProperty = Object.create(null); @@ -135,6 +139,38 @@ export function convertToValidSignal(signal) { throw new ERR_UNKNOWN_SIGNAL(signal); } +export class WeakReference { + #weak = null; + #strong = null; + #refCount = 0; + constructor(object) { + this.#weak = new SafeWeakRef(object); + } + + incRef() { + this.#refCount++; + if (this.#refCount === 1) { + const derefed = this.#weak.deref(); + if (derefed !== undefined) { + this.#strong = derefed; + } + } + return this.#refCount; + } + + decRef() { + this.#refCount--; + if (this.#refCount === 0) { + this.#strong = null; + } + return this.#refCount; + } + + get() { + return this.#weak.deref(); + } +} + promisify.custom = kCustomPromisifiedSymbol; export default { diff --git a/crates/node/polyfills/internal/validators.mjs b/crates/node/polyfills/internal/validators.mjs index d4cd95546..58b1a97d7 100644 --- a/crates/node/polyfills/internal/validators.mjs +++ b/crates/node/polyfills/internal/validators.mjs @@ -171,10 +171,23 @@ function validateString(value, name) { * @param {unknown} value * @param {string} name */ -function validateNumber(value, name) { +function validateNumber(value, name, min = undefined, max) { if (typeof value !== "number") { throw new codes.ERR_INVALID_ARG_TYPE(name, "number", value); } + + if ( + (min != null && value < min) || (max != null && value > max) || + ((min != null || max != null) && Number.isNaN(value)) + ) { + throw new codes.ERR_OUT_OF_RANGE( + name, + `${min != null ? `>= ${min}` : ""}${ + min != null && max != null ? " && " : "" + }${max != null ? `<= ${max}` : ""}`, + value, + ); + } } /** diff --git a/crates/node/polyfills/internal_binding/_utils.ts b/crates/node/polyfills/internal_binding/_utils.ts index a773f0a9c..74dc3cbcd 100644 --- a/crates/node/polyfills/internal_binding/_utils.ts +++ b/crates/node/polyfills/internal_binding/_utils.ts @@ -18,9 +18,13 @@ export function asciiToBytes(str: string) { } export function base64ToBytes(str: string) { - str = base64clean(str); - str = str.replaceAll("-", "+").replaceAll("_", "/"); - return forgivingBase64Decode(str); + try { + return forgivingBase64Decode(str); + } catch { + str = base64clean(str); + str = str.replaceAll("-", "+").replaceAll("_", "/"); + return forgivingBase64Decode(str); + } } const INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g; diff --git a/crates/node/polyfills/net.ts b/crates/node/polyfills/net.ts index 66b7735d9..6625ce7b5 100644 --- a/crates/node/polyfills/net.ts +++ b/crates/node/polyfills/net.ts @@ -24,6 +24,8 @@ // deno-lint-ignore-file prefer-primordials import { notImplemented } from "ext:deno_node/_utils.ts"; +import { BlockList, SocketAddress } from "ext:deno_node/internal/blocklist.mjs"; + import { EventEmitter } from "node:events"; import { isIP, @@ -2472,7 +2474,7 @@ export function createServer( return new Server(options, connectionListener); } -export { isIP, isIPv4, isIPv6 }; +export { BlockList, isIP, isIPv4, isIPv6, SocketAddress }; export default { _createServerHandle, @@ -2480,6 +2482,8 @@ export default { isIP, isIPv4, isIPv6, + BlockList, + SocketAddress, connect, createConnection, createServer, diff --git a/crates/node/polyfills/os.ts b/crates/node/polyfills/os.ts index 71b427023..1165eaa40 100644 --- a/crates/node/polyfills/os.ts +++ b/crates/node/polyfills/os.ts @@ -32,7 +32,7 @@ import { import { validateIntegerRange } from "ext:deno_node/_utils.ts"; import process from "node:process"; -import { isWindows, osType } from "ext:deno_node/_util/os.ts"; +import { isWindows } from "ext:deno_node/_util/os.ts"; import { ERR_OS_NO_HOMEDIR } from "ext:deno_node/internal/errors.ts"; import { os } from "ext:deno_node/internal_binding/constants.ts"; import { osCalls } from "ext:sb_os/os.js" @@ -183,21 +183,21 @@ export function getPriority(_pid = 0): number { /** Returns the string path of the current user's home directory. */ export function homedir(): string | null { -/* // Note: Node/libuv calls getpwuid() / GetUserProfileDirectory() when the - // environment variable isn't set but that's the (very uncommon) fallback - // path. IMO, it's okay to punt on that for now. - switch (osType) { - case "windows": - return Deno.env.get("USERPROFILE") || null; - case "linux": - case "android": - case "darwin": - case "freebsd": - case "openbsd": - return Deno.env.get("HOME") || null; - default: - throw Error("unreachable"); - }*/ + /* // Note: Node/libuv calls getpwuid() / GetUserProfileDirectory() when the + // environment variable isn't set but that's the (very uncommon) fallback + // path. IMO, it's okay to punt on that for now. + switch (osType) { + case "windows": + return Deno.env.get("USERPROFILE") || null; + case "linux": + case "android": + case "darwin": + case "freebsd": + case "openbsd": + return Deno.env.get("HOME") || null; + default: + throw Error("unreachable"); + }*/ return "/home/deno" } diff --git a/crates/node/polyfills/path/_util.ts b/crates/node/polyfills/path/_util.ts index 7be482965..9248c68ae 100644 --- a/crates/node/polyfills/path/_util.ts +++ b/crates/node/polyfills/path/_util.ts @@ -106,13 +106,17 @@ export function normalizeString( return res; } +function formatExt(ext) { + return ext ? `${ext[0] === "." ? "" : "."}${ext}` : ""; +} + export function _format( sep: string, pathObject: FormatInputPathObject, ): string { const dir: string | undefined = pathObject.dir || pathObject.root; const base: string = pathObject.base || - (pathObject.name || "") + (pathObject.ext || ""); + (pathObject.name || "") + formatExt(pathObject.ext); if (!dir) return base; if (dir === pathObject.root) return dir + base; return dir + sep + base; diff --git a/crates/node/polyfills/perf_hooks.ts b/crates/node/polyfills/perf_hooks.ts index 44ab88f98..d48635856 100644 --- a/crates/node/polyfills/perf_hooks.ts +++ b/crates/node/polyfills/perf_hooks.ts @@ -9,8 +9,15 @@ import { PerformanceEntry, } from "ext:deno_web/15_performance.js"; -// FIXME(bartlomieju) -const PerformanceObserver = undefined; +class PerformanceObserver { + observe() { + notImplemented("PerformanceObserver.observe"); + } + disconnect() { + notImplemented("PerformanceObserver.disconnect"); + } +} + const constants = {}; const performance: @@ -19,8 +26,11 @@ const performance: "clearMeasures" | "getEntries" > & { - // deno-lint-ignore no-explicit-any - eventLoopUtilization: any; + eventLoopUtilization(): { + idle: number; + active: number; + utilization: number; + }; nodeTiming: Record; // deno-lint-ignore no-explicit-any timerify: any; @@ -30,8 +40,10 @@ const performance: markResourceTiming: any; } = { clearMarks: (markName: string) => shimPerformance.clearMarks(markName), - eventLoopUtilization: () => - notImplemented("eventLoopUtilization from performance"), + eventLoopUtilization: () => { + // TODO(@marvinhagemeister): Return actual non-stubbed values + return { idle: 0, active: 0, utilization: 0 }; + }, mark: (markName: string) => shimPerformance.mark(markName), measure: ( measureName: string, diff --git a/crates/node/polyfills/process.ts b/crates/node/polyfills/process.ts index b24bf6a8e..ec8671122 100644 --- a/crates/node/polyfills/process.ts +++ b/crates/node/polyfills/process.ts @@ -7,10 +7,10 @@ import { core, internals } from "ext:core/mod.js"; import { initializeDebugEnv } from "ext:deno_node/internal/util/debuglog.ts"; import { + op_getegid, op_geteuid, op_node_process_kill, op_process_abort, - op_set_exit_code, } from "ext:core/ops"; import { warnNotImplemented } from "ext:deno_node/_utils.ts"; @@ -20,6 +20,7 @@ import { report } from "ext:deno_node/internal/process/report.ts"; import { validateString } from "ext:deno_node/internal/validators.mjs"; import { ERR_INVALID_ARG_TYPE, + ERR_OUT_OF_RANGE, ERR_UNKNOWN_SIGNAL, errnoException, } from "ext:deno_node/internal/errors.ts"; @@ -49,6 +50,7 @@ import { } from "ext:deno_node/_next_tick.ts"; import { isWindows } from "ext:deno_node/_util/os.ts"; import * as io from "ext:deno_io/12_io.js"; +import * as denoOs from "ext:runtime/30_os.js"; export let argv0 = ""; @@ -74,28 +76,31 @@ const notImplementedEvents = [ ]; export const argv: string[] = ["", ""]; -let globalProcessExitCode: number | undefined = undefined; + +// In Node, `process.exitCode` is initially `undefined` until set. +// And retains any value as long as it's nullish or number-ish. +let ProcessExitCode: undefined | null | string | number; /** https://nodejs.org/api/process.html#process_process_exit_code */ export const exit = (code?: number | string) => { if (code || code === 0) { - if (typeof code === "string") { - const parsedCode = parseInt(code); - globalProcessExitCode = isNaN(parsedCode) ? undefined : parsedCode; - } else { - globalProcessExitCode = code; - } + denoOs.setExitCode(code); + } else if (Number.isNaN(code)) { + denoOs.setExitCode(1); } + ProcessExitCode = denoOs.getExitCode(); if (!process._exiting) { process._exiting = true; // FIXME(bartlomieju): this is wrong, we won't be using syscall to exit // and thus the `unload` event will not be emitted to properly trigger "emit" // event on `process`. - process.emit("exit", process.exitCode || 0); + process.emit("exit", ProcessExitCode); } - process.reallyExit(process.exitCode || 0); + // Any valid thing `process.exitCode` set is already held in Deno.exitCode. + // At this point, we don't have to pass around Node's raw/string exit value. + process.reallyExit(ProcessExitCode); }; /** https://nodejs.org/api/process.html#processumaskmask */ @@ -259,6 +264,10 @@ memoryUsage.rss = function (): number { // Returns a negative error code than can be recognized by errnoException function _kill(pid: number, sig: number): number { + // signal 0 does not exist in constants.os.signals, thats why it have to be handled explicitly + if (sig === 0) { + return op_node_process_kill(pid, 0); + } const maybeSignal = Object.entries(constants.os.signals).find(( [_, numericCode], ) => numericCode === sig); @@ -301,6 +310,19 @@ export function kill(pid: number, sig: string | number = "SIGTERM") { return true; } +let getgid, getuid, getegid, geteuid; + +if (!isWindows) { + getgid = () => Deno.gid(); + getuid = () => Deno.uid(); + getegid = () => op_getegid(); + geteuid = () => op_geteuid(); +} + +export { getegid, geteuid, getgid, getuid }; + +const ALLOWED_FLAGS = buildAllowedFlags(); + // deno-lint-ignore no-explicit-any function uncaughtExceptionHandler(err: any, origin: string) { // The origin parameter can be 'unhandledRejection' or 'uncaughtException' @@ -316,13 +338,21 @@ function uncaughtExceptionHandler(err: any, origin: string) { let execPath: string | null = null; -class Process extends EventEmitter { - constructor() { - super(); - } +// The process class needs to be an ES5 class because it can be instantiated +// in Node without the `new` keyword. It's not a true class in Node. Popular +// test runners like Jest rely on this. +// deno-lint-ignore no-explicit-any +function Process(this: any) { + // deno-lint-ignore no-explicit-any + if (!(this instanceof Process)) return new (Process as any)(); + + EventEmitter.call(this); +} +Process.prototype = Object.create(EventEmitter.prototype); - /** https://nodejs.org/api/process.html#processrelease */ - get release() { +/** https://nodejs.org/api/process.html#processrelease */ +Object.defineProperty(Process.prototype, "release", { + get() { return { name: "node", sourceUrl: @@ -330,375 +360,391 @@ class Process extends EventEmitter { headersUrl: `https://nodejs.org/download/release/${version}/node-${version}-headers.tar.gz`, }; - } + }, +}); - /** https://nodejs.org/api/process.html#process_process_arch */ - get arch() { +/** https://nodejs.org/api/process.html#process_process_arch */ +Object.defineProperty(Process.prototype, "arch", { + get() { return arch; - } + }, +}); - get report() { +Object.defineProperty(Process.prototype, "report", { + get() { return report; - } + }, +}); - get title() { +Object.defineProperty(Process.prototype, "title", { + get() { return "deno"; - } - - set title(_value) { + }, + set(_value) { // NOTE(bartlomieju): this is a noop. Node.js doesn't guarantee that the // process name will be properly set and visible from other tools anyway. // Might revisit in the future. - } + }, +}); - /** - * https://nodejs.org/api/process.html#process_process_argv - * Read permissions are required in order to get the executable route - */ - argv = argv; +/** + * https://nodejs.org/api/process.html#process_process_argv + * Read permissions are required in order to get the executable route + */ +Process.prototype.argv = argv; - get argv0() { +Object.defineProperty(Process.prototype, "argv0", { + get() { return argv0; - } - - set argv0(_val) {} + }, + set(_val) {}, +}); - /** https://nodejs.org/api/process.html#process_process_chdir_directory */ - chdir = chdir; +/** https://nodejs.org/api/process.html#process_process_chdir_directory */ +Process.prototype.chdir = chdir; - /** https://nodejs.org/api/process.html#processconfig */ - config = { - target_defaults: {}, - variables: {}, - }; +/** https://nodejs.org/api/process.html#processconfig */ +Process.prototype.config = { + target_defaults: { + default_configuration: "Release", + }, + variables: { + llvm_version: "0.0", + enable_lto: "false", + }, +}; - /** https://nodejs.org/api/process.html#process_process_cwd */ - cwd = cwd; +/** https://nodejs.org/api/process.html#process_process_cwd */ +Process.prototype.cwd = cwd; - /** - * https://nodejs.org/api/process.html#process_process_env - * Requires env permissions - */ - env = env; +/** + * https://nodejs.org/api/process.html#process_process_env + * Requires env permissions + */ +Process.prototype.env = env; - /** https://nodejs.org/api/process.html#process_process_execargv */ - execArgv: string[] = []; +/** https://nodejs.org/api/process.html#process_process_execargv */ +Process.prototype.execArgv = []; - /** https://nodejs.org/api/process.html#process_process_exit_code */ - exit = exit; +/** https://nodejs.org/api/process.html#process_process_exit_code */ +Process.prototype.exit = exit; - /** https://nodejs.org/api/process.html#processabort */ - abort = abort; +/** https://nodejs.org/api/process.html#processabort */ +Process.prototype.abort = abort; - // Undocumented Node API that is used by `signal-exit` which in turn - // is used by `node-tap`. It was marked for removal a couple of years - // ago. See https://github.com/nodejs/node/blob/6a6b3c54022104cc110ab09044a2a0cecb8988e7/lib/internal/bootstrap/node.js#L172 - reallyExit = (code: number) => { - return Deno.exit(code || 0); - }; +// Undocumented Node API that is used by `signal-exit` which in turn +// is used by `node-tap`. It was marked for removal a couple of years +// ago. See https://github.com/nodejs/node/blob/6a6b3c54022104cc110ab09044a2a0cecb8988e7/lib/internal/bootstrap/node.js#L172 +Process.prototype.reallyExit = (code: number) => { + return Deno.exit(code || 0); +}; - _exiting = _exiting; +Process.prototype._exiting = _exiting; - /** https://nodejs.org/api/process.html#processexitcode_1 */ - get exitCode() { - return globalProcessExitCode; - } +/** https://nodejs.org/api/process.html#processexitcode_1 */ +Object.defineProperty(Process.prototype, "exitCode", { + get() { + return ProcessExitCode; + }, + set(code: number | string | null | undefined) { + let parsedCode: number; + if (code == null) { + parsedCode = 0; + } else if (typeof code === "number") { + parsedCode = code; + } else if (typeof code === "string") { + parsedCode = Number(code); + } else { + throw new ERR_INVALID_ARG_TYPE("code", "number", code); + } - set exitCode(code: number | undefined) { - globalProcessExitCode = code; - code = parseInt(code) || 0; - if (!isNaN(code)) { - op_set_exit_code(code); + if (!Number.isInteger(parsedCode)) { + throw new ERR_OUT_OF_RANGE("code", "an integer", parsedCode); } - } - // Typed as any to avoid importing "module" module for types - // deno-lint-ignore no-explicit-any - mainModule: any = undefined; + denoOs.setExitCode(parsedCode); + ProcessExitCode = code; + }, +}); - /** https://nodejs.org/api/process.html#process_process_nexttick_callback_args */ - nextTick = _nextTick; +// Typed as any to avoid importing "module" module for types +Process.prototype.mainModule = undefined; - dlopen = dlopen; +/** https://nodejs.org/api/process.html#process_process_nexttick_callback_args */ +Process.prototype.nextTick = _nextTick; - /** https://nodejs.org/api/process.html#process_process_events */ - override on(event: "exit", listener: (code: number) => void): this; - override on( - event: typeof notImplementedEvents[number], - // deno-lint-ignore ban-types - listener: Function, - ): this; +Process.prototype.dlopen = dlopen; + +/** https://nodejs.org/api/process.html#process_process_events */ +Process.prototype.on = function ( // deno-lint-ignore no-explicit-any - override on(event: string, listener: (...args: any[]) => void): this { - if (notImplementedEvents.includes(event)) { - warnNotImplemented(`process.on("${event}")`); - super.on(event, listener); - } else if (event.startsWith("SIG")) { - if (event === "SIGBREAK" && Deno.build.os !== "windows") { - // Ignores SIGBREAK if the platform is not windows. - } else if (event === "SIGTERM" && Deno.build.os === "windows") { - // Ignores SIGTERM on windows. - } else { - Deno.addSignalListener(event as Deno.Signal, listener); - } + this: any, + event: string, + // deno-lint-ignore no-explicit-any + listener: (...args: any[]) => void, +) { + if (notImplementedEvents.includes(event)) { + warnNotImplemented(`process.on("${event}")`); + EventEmitter.prototype.on.call(this, event, listener); + } else if (event.startsWith("SIG")) { + if (event === "SIGBREAK" && Deno.build.os !== "windows") { + // Ignores SIGBREAK if the platform is not windows. + } else if (event === "SIGTERM" && Deno.build.os === "windows") { + // Ignores SIGTERM on windows. } else { - super.on(event, listener); + EventEmitter.prototype.on.call(this, event, listener); + Deno.addSignalListener(event as Deno.Signal, listener); } - - return this; + } else { + EventEmitter.prototype.on.call(this, event, listener); } - override off(event: "exit", listener: (code: number) => void): this; - override off( - event: typeof notImplementedEvents[number], - // deno-lint-ignore ban-types - listener: Function, - ): this; + return this; +}; + +Process.prototype.off = function ( // deno-lint-ignore no-explicit-any - override off(event: string, listener: (...args: any[]) => void): this { - if (notImplementedEvents.includes(event)) { - warnNotImplemented(`process.off("${event}")`); - super.off(event, listener); - } else if (event.startsWith("SIG")) { - if (event === "SIGBREAK" && Deno.build.os !== "windows") { - // Ignores SIGBREAK if the platform is not windows. - } else if (event === "SIGTERM" && Deno.build.os === "windows") { - // Ignores SIGTERM on windows. - } else { - Deno.removeSignalListener(event as Deno.Signal, listener); - } + this: any, + event: string, + // deno-lint-ignore no-explicit-any + listener: (...args: any[]) => void, +) { + if (notImplementedEvents.includes(event)) { + warnNotImplemented(`process.off("${event}")`); + EventEmitter.prototype.off.call(this, event, listener); + } else if (event.startsWith("SIG")) { + if (event === "SIGBREAK" && Deno.build.os !== "windows") { + // Ignores SIGBREAK if the platform is not windows. + } else if (event === "SIGTERM" && Deno.build.os === "windows") { + // Ignores SIGTERM on windows. } else { - super.off(event, listener); + EventEmitter.prototype.off.call(this, event, listener); + Deno.removeSignalListener(event as Deno.Signal, listener); } - - return this; + } else { + EventEmitter.prototype.off.call(this, event, listener); } + return this; +}; + +Process.prototype.emit = function ( // deno-lint-ignore no-explicit-any - override emit(event: string, ...args: any[]): boolean { - if (event.startsWith("SIG")) { - if (event === "SIGBREAK" && Deno.build.os !== "windows") { - // Ignores SIGBREAK if the platform is not windows. - } else { - Deno.kill(Deno.pid, event as Deno.Signal); - } + this: any, + event: string, + // deno-lint-ignore no-explicit-any + ...args: any[] +): boolean { + if (event.startsWith("SIG")) { + if (event === "SIGBREAK" && Deno.build.os !== "windows") { + // Ignores SIGBREAK if the platform is not windows. } else { - return super.emit(event, ...args); + Deno.kill(Deno.pid, event as Deno.Signal); } - - return true; + } else { + return EventEmitter.prototype.emit.call(this, event, ...args); } - override prependListener( - event: "exit", - listener: (code: number) => void, - ): this; - override prependListener( - event: typeof notImplementedEvents[number], - // deno-lint-ignore ban-types - listener: Function, - ): this; - override prependListener( - event: string, - // deno-lint-ignore no-explicit-any - listener: (...args: any[]) => void, - ): this { - if (notImplementedEvents.includes(event)) { - warnNotImplemented(`process.prependListener("${event}")`); - super.prependListener(event, listener); - } else if (event.startsWith("SIG")) { - if (event === "SIGBREAK" && Deno.build.os !== "windows") { - // Ignores SIGBREAK if the platform is not windows. - } else { - Deno.addSignalListener(event as Deno.Signal, listener); - } + return true; +}; + +Process.prototype.prependListener = function ( + // deno-lint-ignore no-explicit-any + this: any, + event: string, + // deno-lint-ignore no-explicit-any + listener: (...args: any[]) => void, +) { + if (notImplementedEvents.includes(event)) { + warnNotImplemented(`process.prependListener("${event}")`); + EventEmitter.prototype.prependListener.call(this, event, listener); + } else if (event.startsWith("SIG")) { + if (event === "SIGBREAK" && Deno.build.os !== "windows") { + // Ignores SIGBREAK if the platform is not windows. } else { - super.prependListener(event, listener); + EventEmitter.prototype.prependListener.call(this, event, listener); + Deno.addSignalListener(event as Deno.Signal, listener); } - - return this; + } else { + EventEmitter.prototype.prependListener.call(this, event, listener); } - /** https://nodejs.org/api/process.html#process_process_pid */ - get pid() { + return this; +}; + +/** https://nodejs.org/api/process.html#process_process_pid */ +Object.defineProperty(Process.prototype, "pid", { + get() { return pid; - } + }, +}); - /** https://nodejs.org/api/process.html#processppid */ - get ppid() { +/** https://nodejs.org/api/process.html#processppid */ +Object.defineProperty(Process.prototype, "ppid", { + get() { return Deno.ppid; - } + }, +}); - /** https://nodejs.org/api/process.html#process_process_platform */ - get platform() { +/** https://nodejs.org/api/process.html#process_process_platform */ +Object.defineProperty(Process.prototype, "platform", { + get() { return platform; - } + }, +}); + +// https://nodejs.org/api/process.html#processsetsourcemapsenabledval +Process.prototype.setSourceMapsEnabled = (_val: boolean) => { + // This is a no-op in Deno. Source maps are always enabled. + // TODO(@satyarohith): support disabling source maps if needed. +}; - // https://nodejs.org/api/process.html#processsetsourcemapsenabledval - setSourceMapsEnabled(_val: boolean) { - // This is a no-op in Deno. Source maps are always enabled. - // TODO(@satyarohith): support disabling source maps if needed. +Process.prototype.addListener = function ( + // deno-lint-ignore no-explicit-any + this: any, + event: string, + // deno-lint-ignore no-explicit-any + listener: (...args: any[]) => void, +) { + if (notImplementedEvents.includes(event)) { + warnNotImplemented(`process.addListener("${event}")`); } - override addListener(event: "exit", listener: (code: number) => void): this; - override addListener( - event: typeof notImplementedEvents[number], - // deno-lint-ignore ban-types - listener: Function, - ): this; - override addListener( - event: string, - // deno-lint-ignore no-explicit-any - listener: (...args: any[]) => void, - ): this { - if (notImplementedEvents.includes(event)) { - warnNotImplemented(`process.addListener("${event}")`); - } + return this.on(event, listener); +}; - return this.on(event, listener); +Process.prototype.removeListener = function ( + // deno-lint-ignore no-explicit-any + this: any, + event: string, // deno-lint-ignore no-explicit-any + listener: (...args: any[]) => void, +) { + if (notImplementedEvents.includes(event)) { + warnNotImplemented(`process.removeListener("${event}")`); } - override removeListener( - event: "exit", - listener: (code: number) => void, - ): this; - override removeListener( - event: typeof notImplementedEvents[number], - // deno-lint-ignore ban-types - listener: Function, - ): this; - override removeListener( - event: string, - // deno-lint-ignore no-explicit-any - listener: (...args: any[]) => void, - ): this { - if (notImplementedEvents.includes(event)) { - warnNotImplemented(`process.removeListener("${event}")`); - } + return this.off(event, listener); +}; - return this.off(event, listener); - } +/** + * Returns the current high-resolution real time in a [seconds, nanoseconds] + * tuple. + * + * Note: You need to give --allow-hrtime permission to Deno to actually get + * nanoseconds precision values. If you don't give 'hrtime' permission, the returned + * values only have milliseconds precision. + * + * `time` is an optional parameter that must be the result of a previous process.hrtime() call to diff with the current time. + * + * These times are relative to an arbitrary time in the past, and not related to the time of day and therefore not subject to clock drift. The primary use is for measuring performance between intervals. + * https://nodejs.org/api/process.html#process_process_hrtime_time + */ +Process.prototype.hrtime = hrtime; - /** - * Returns the current high-resolution real time in a [seconds, nanoseconds] - * tuple. - * - * Note: You need to give --allow-hrtime permission to Deno to actually get - * nanoseconds precision values. If you don't give 'hrtime' permission, the returned - * values only have milliseconds precision. - * - * `time` is an optional parameter that must be the result of a previous process.hrtime() call to diff with the current time. - * - * These times are relative to an arbitrary time in the past, and not related to the time of day and therefore not subject to clock drift. The primary use is for measuring performance between intervals. - * https://nodejs.org/api/process.html#process_process_hrtime_time - */ - hrtime = hrtime; - - /** - * @private - * - * NodeJS internal, use process.kill instead - */ - _kill = _kill; - - /** https://nodejs.org/api/process.html#processkillpid-signal */ - kill = kill; - - memoryUsage = memoryUsage; - - /** https://nodejs.org/api/process.html#process_process_stderr */ - stderr = stderr; - - /** https://nodejs.org/api/process.html#process_process_stdin */ - stdin = stdin; - - /** https://nodejs.org/api/process.html#process_process_stdout */ - stdout = stdout; - - /** https://nodejs.org/api/process.html#process_process_version */ - version = version; - - /** https://nodejs.org/api/process.html#process_process_versions */ - versions = versions; - - /** https://nodejs.org/api/process.html#process_process_emitwarning_warning_options */ - emitWarning = emitWarning; - - binding(name: BindingName) { - return getBinding(name); - } +/** + * @private + * + * NodeJS internal, use process.kill instead + */ +Process.prototype._kill = _kill; - /** https://nodejs.org/api/process.html#processumaskmask */ - umask() { - // Always return the system default umask value. - // We don't use Deno.umask here because it has a race - // condition bug. - // See https://github.com/denoland/deno_std/issues/1893#issuecomment-1032897779 - return 0o22; - } +/** https://nodejs.org/api/process.html#processkillpid-signal */ +Process.prototype.kill = kill; - /** This method is removed on Windows */ - getgid?(): number { - return Deno.gid()!; - } +Process.prototype.memoryUsage = memoryUsage; - /** This method is removed on Windows */ - getuid?(): number { - return Deno.uid()!; - } +/** https://nodejs.org/api/process.html#process_process_stderr */ +Process.prototype.stderr = stderr; - /** This method is removed on Windows */ - geteuid?(): number { - return op_geteuid(); - } +/** https://nodejs.org/api/process.html#process_process_stdin */ +Process.prototype.stdin = stdin; + +/** https://nodejs.org/api/process.html#process_process_stdout */ +Process.prototype.stdout = stdout; + +/** https://nodejs.org/api/process.html#process_process_version */ +Process.prototype.version = version; + +/** https://nodejs.org/api/process.html#process_process_versions */ +Process.prototype.versions = versions; + +/** https://nodejs.org/api/process.html#process_process_emitwarning_warning_options */ +Process.prototype.emitWarning = emitWarning; + +Process.prototype.binding = (name: BindingName) => { + return getBinding(name); +}; + +/** https://nodejs.org/api/process.html#processumaskmask */ +Process.prototype.umask = () => { + // Always return the system default umask value. + // We don't use Deno.umask here because it has a race + // condition bug. + // See https://github.com/denoland/deno_std/issues/1893#issuecomment-1032897779 + return 0o22; +}; + +/** This method is removed on Windows */ +Process.prototype.getgid = getgid; + +/** This method is removed on Windows */ +Process.prototype.getuid = getuid; + +/** This method is removed on Windows */ +Process.prototype.getegid = getegid; - // TODO(kt3k): Implement this when we added -e option to node compat mode - _eval: string | undefined = undefined; +/** This method is removed on Windows */ +Process.prototype.geteuid = geteuid; - /** https://nodejs.org/api/process.html#processexecpath */ - get execPath() { +// TODO(kt3k): Implement this when we added -e option to node compat mode +Process.prototype._eval = undefined; + +/** https://nodejs.org/api/process.html#processexecpath */ + +Object.defineProperty(Process.prototype, "execPath", { + get() { if (execPath) { return execPath; } execPath = Deno.execPath(); return execPath; - } - - set execPath(path: string) { + }, + set(path: string) { execPath = path; - } - - setStartTime(t: number) { - this.#startTime = t; - } + }, +}); - #startTime = 0; - /** https://nodejs.org/api/process.html#processuptime */ - uptime() { - return (Date.now() - this.#startTime) / 1000; - } +/** https://nodejs.org/api/process.html#processuptime */ +Process.prototype.uptime = () => { + return Number((performance.now() / 1000).toFixed(9)); +}; - #allowedFlags = buildAllowedFlags(); - /** https://nodejs.org/api/process.html#processallowednodeenvironmentflags */ - get allowedNodeEnvironmentFlags() { - return this.#allowedFlags; - } +/** https://nodejs.org/api/process.html#processallowednodeenvironmentflags */ +Object.defineProperty(Process.prototype, "allowedNodeEnvironmentFlags", { + get() { + return ALLOWED_FLAGS; + }, +}); - features = { inspector: false }; +Process.prototype.features = { inspector: false }; - // TODO(kt3k): Get the value from --no-deprecation flag. - noDeprecation = false; -} +// TODO(kt3k): Get the value from --no-deprecation flag. +Process.prototype.noDeprecation = false; if (isWindows) { delete Process.prototype.getgid; delete Process.prototype.getuid; + delete Process.prototype.getegid; delete Process.prototype.geteuid; } /** https://nodejs.org/api/process.html#process_process */ +// @ts-ignore TS doesn't work well with ES5 classes const process = new Process(); +/* Set owned property */ +process.versions = versions; + Object.defineProperty(process, Symbol.toStringTag, { enumerable: false, writable: true, @@ -887,16 +933,12 @@ internals.__bootstrapNodeProcess = function ( ); } - process.setStartTime(Date.now()); - arch = arch_(); platform = isWindows ? "win32" : Deno.build.os; pid = Deno.pid; initializeDebugEnv(nodeDebug); - // @ts-ignore Remove setStartTime and #startTime is not modifiable - delete process.setStartTime; delete internals.__bootstrapNodeProcess; } else { // Warmup, assuming stdin/stdout/stderr are all terminals diff --git a/crates/node/polyfills/string_decoder.ts b/crates/node/polyfills/string_decoder.ts index ef83b6fc9..4a49c2e3e 100644 --- a/crates/node/polyfills/string_decoder.ts +++ b/crates/node/polyfills/string_decoder.ts @@ -20,9 +20,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - // Logic and comments translated pretty much one-to-one from node's impl // (https://github.com/nodejs/node/blob/ba06c5c509956dc413f91b755c1c93798bb700d4/src/string_decoder.cc) @@ -35,11 +32,19 @@ import { NodeError, } from "ext:deno_node/internal/errors.ts"; -import { primordials } from "ext:core/mod.js"; +import { core, primordials } from "ext:core/mod.js"; const { ArrayBufferIsView, ObjectDefineProperties, + Symbol, + MathMin, + DataViewPrototypeGetBuffer, + ObjectPrototypeIsPrototypeOf, + String, + TypedArrayPrototypeGetBuffer, + StringPrototypeToLowerCase, } = primordials; +const { isTypedArray } = core; const { MAX_STRING_LENGTH } = constants; @@ -50,7 +55,7 @@ type Any = any; function normalizeEncoding(enc?: string): string { const encoding = castEncoding(enc ?? null); if (!encoding) { - if (typeof enc !== "string" || enc.toLowerCase() !== "raw") { + if (typeof enc !== "string" || StringPrototypeToLowerCase(enc) !== "raw") { throw new ERR_UNKNOWN_ENCODING( enc as Any, ); @@ -64,7 +69,8 @@ function normalizeEncoding(enc?: string): string { */ function isBufferType(buf: Buffer) { - return buf instanceof Buffer && buf.BYTES_PER_ELEMENT; + return ObjectPrototypeIsPrototypeOf(Buffer.prototype, buf) && + buf.BYTES_PER_ELEMENT; } function normalizeBuffer(buf: Buffer) { @@ -79,7 +85,9 @@ function normalizeBuffer(buf: Buffer) { return buf; } else { return Buffer.from( - buf.buffer, + isTypedArray(buf) + ? TypedArrayPrototypeGetBuffer(buf) + : DataViewPrototypeGetBuffer(buf), ); } } @@ -94,6 +102,7 @@ function bufferToString( if (len > MAX_STRING_LENGTH) { throw new NodeError("ERR_STRING_TOO_LONG", "string exceeds maximum length"); } + // deno-lint-ignore prefer-primordials return buf.toString(encoding as Any, start, end); } @@ -136,7 +145,7 @@ function decode(this: StringDecoder, buf: Buffer) { } } - const bytesToCopy = Math.min(buf.length - bufIdx, this[kMissingBytes]); + const bytesToCopy = MathMin(buf.length - bufIdx, this[kMissingBytes]); buf.copy( this.lastChar, this[kBufferedBytes], diff --git a/crates/node/polyfills/testing.ts b/crates/node/polyfills/testing.ts index b6733f118..f081db120 100644 --- a/crates/node/polyfills/testing.ts +++ b/crates/node/polyfills/testing.ts @@ -1,15 +1,14 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - +import { primordials } from "ext:core/mod.js"; +const { PromisePrototypeThen } = primordials; import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; export function run() { notImplemented("test.run"); } -function noop() {} +function noop() { } class NodeTestContext { #denoContext: Deno.TestContext; @@ -54,17 +53,20 @@ class NodeTestContext { test(name, options, fn) { const prepared = prepareOptions(name, options, fn, {}); - return this.#denoContext.step({ - name: prepared.name, - fn: async (denoTestContext) => { - const newNodeTextContext = new NodeTestContext(denoTestContext); - await prepared.fn(newNodeTextContext); - }, - ignore: prepared.options.todo || prepared.options.skip, - sanitizeExit: false, - sanitizeOps: false, - sanitizeResources: false, - }).then(() => undefined); + return PromisePrototypeThen( + this.#denoContext.step({ + name: prepared.name, + fn: async (denoTestContext) => { + const newNodeTextContext = new NodeTestContext(denoTestContext); + await prepared.fn(newNodeTextContext); + }, + ignore: prepared.options.todo || prepared.options.skip, + sanitizeExit: false, + sanitizeOps: false, + sanitizeResources: false, + }), + () => undefined, + ); } before(_fn, _options) { @@ -127,6 +129,8 @@ function wrapTestFn(fn, resolve) { function prepareDenoTest(name, options, fn, overrides) { const prepared = prepareOptions(name, options, fn, overrides); + // TODO(iuioiua): Update once there's a primordial for `Promise.withResolvers()`. + // deno-lint-ignore prefer-primordials const { promise, resolve } = Promise.withResolvers(); const denoTestOptions = { diff --git a/crates/node/polyfills/timers.ts b/crates/node/polyfills/timers.ts index 033afd952..b96f448da 100644 --- a/crates/node/polyfills/timers.ts +++ b/crates/node/polyfills/timers.ts @@ -1,12 +1,12 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - import { primordials } from "ext:core/mod.js"; const { MapPrototypeGet, MapPrototypeDelete, + ObjectDefineProperty, + Promise, + SafeArrayIterator, } = primordials; import { @@ -32,9 +32,11 @@ export function setTimeout( return new Timeout(callback, timeout, args, false, true); } -Object.defineProperty(setTimeout, promisify.custom, { +ObjectDefineProperty(setTimeout, promisify.custom, { value: (timeout: number, ...args: unknown[]) => { - return new Promise((cb) => setTimeout(cb, timeout, ...args)); + return new Promise((cb) => + setTimeout(cb, timeout, ...new SafeArrayIterator(args)) + ); }, enumerable: true, }); @@ -74,7 +76,7 @@ export function setImmediate( cb: (...args: unknown[]) => void, ...args: unknown[] ): Timeout { - return new Immediate(cb, ...args); + return new Immediate(cb, ...new SafeArrayIterator(args)); } export function clearImmediate(immediate: Immediate) { if (immediate == null) { diff --git a/crates/node/polyfills/url.ts b/crates/node/polyfills/url.ts index 6633334ba..4eeb0381f 100644 --- a/crates/node/polyfills/url.ts +++ b/crates/node/polyfills/url.ts @@ -1352,12 +1352,16 @@ function getPathFromURLPosix(url: URL): string { * setter. * - TAB: The tab character is also stripped out by the `pathname` setter. */ -function encodePathChars(filepath: string): string { +function encodePathChars( + filepath: string, + options: { windows?: boolean }, +): string { + const windows = options.windows; if (filepath.includes("%")) { filepath = filepath.replace(percentRegEx, "%25"); } // In posix, backslash is a valid character in paths: - if (!isWindows && filepath.includes("\\")) { + if (!(windows ?? isWindows) && filepath.includes("\\")) { filepath = filepath.replace(backslashRegEx, "%5C"); } if (filepath.includes("\n")) { @@ -1376,11 +1380,17 @@ function encodePathChars(filepath: string): string { * This function ensures that `filepath` is resolved absolutely, and that the URL control characters are correctly encoded when converting into a File URL. * @see Tested in `parallel/test-url-pathtofileurl.js`. * @param filepath The file path string to convert to a file URL. + * @param options The options. * @returns The file URL object. */ -export function pathToFileURL(filepath: string): URL { +export function pathToFileURL( + filepath: string, + options: { windows?: boolean } = {}, +): URL { + validateString(filepath, "path"); + const windows = options?.windows; const outURL = new URL("file://"); - if (isWindows && filepath.startsWith("\\\\")) { + if ((windows ?? isWindows) && filepath.startsWith("\\\\")) { // UNC path format: \\server\share\resource const paths = filepath.split("\\"); if (paths.length <= 3) { @@ -1400,20 +1410,22 @@ export function pathToFileURL(filepath: string): URL { } outURL.hostname = idnaToASCII(hostname); - outURL.pathname = encodePathChars(paths.slice(3).join("/")); + outURL.pathname = encodePathChars(paths.slice(3).join("/"), { windows }); } else { - let resolved = path.resolve(filepath); + let resolved = (windows ?? isWindows) + ? path.win32.resolve(filepath) + : path.posix.resolve(filepath); // path.resolve strips trailing slashes so we must add them back const filePathLast = filepath.charCodeAt(filepath.length - 1); if ( (filePathLast === CHAR_FORWARD_SLASH || - (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) && + ((windows ?? isWindows) && filePathLast === CHAR_BACKWARD_SLASH)) && resolved[resolved.length - 1] !== path.sep ) { resolved += "/"; } - outURL.pathname = encodePathChars(resolved); + outURL.pathname = encodePathChars(resolved, { windows }); } return outURL; } diff --git a/crates/node/polyfills/v8.ts b/crates/node/polyfills/v8.ts index cad00bd7f..f06227cd5 100644 --- a/crates/node/polyfills/v8.ts +++ b/crates/node/polyfills/v8.ts @@ -14,7 +14,7 @@ import { import { Buffer } from "node:buffer"; -import { notImplemented } from "ext:deno_node/_utils.ts"; +import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; export function cachedDataVersionTag() { return op_v8_cached_data_version_tag(); @@ -79,7 +79,41 @@ export function deserialize(data) { } export class Serializer { constructor() { - notImplemented("v8.Serializer.prototype.constructor"); + warnNotImplemented("v8.Serializer.prototype.constructor"); + } + + releaseBuffer(): Buffer { + warnNotImplemented("v8.DefaultSerializer.prototype.releaseBuffer"); + return Buffer.from(""); + } + + transferArrayBuffer(_id: number, _arrayBuffer: ArrayBuffer): void { + warnNotImplemented("v8.DefaultSerializer.prototype.transferArrayBuffer"); + } + + writeDouble(_value: number): void { + warnNotImplemented("v8.DefaultSerializer.prototype.writeDouble"); + } + + writeHeader(): void { + warnNotImplemented("v8.DefaultSerializer.prototype.writeHeader"); + } + + writeRawBytes(_value: ArrayBufferView): void { + warnNotImplemented("v8.DefaultSerializer.prototype.writeRawBytes"); + } + + writeUint32(_value: number): void { + warnNotImplemented("v8.DefaultSerializer.prototype.writeUint32"); + } + + writeUint64(_hi: number, _lo: number): void { + warnNotImplemented("v8.DefaultSerializer.prototype.writeUint64"); + } + + // deno-lint-ignore no-explicit-any + writeValue(_value: any): void { + warnNotImplemented("v8.DefaultSerializer.prototype.writeValue"); } } export class Deserializer { @@ -87,9 +121,10 @@ export class Deserializer { notImplemented("v8.Deserializer.prototype.constructor"); } } -export class DefaultSerializer { +export class DefaultSerializer extends Serializer { constructor() { - notImplemented("v8.DefaultSerializer.prototype.constructor"); + warnNotImplemented("v8.DefaultSerializer.prototype.constructor"); + super(); } } export class DefaultDeserializer { diff --git a/crates/node/polyfills/worker_threads.ts b/crates/node/polyfills/worker_threads.ts index 71999dd62..c3ab4bb55 100644 --- a/crates/node/polyfills/worker_threads.ts +++ b/crates/node/polyfills/worker_threads.ts @@ -32,6 +32,9 @@ import process from "node:process"; const { JSONParse, JSONStringify, ObjectPrototypeIsPrototypeOf } = primordials; const { Error, + ObjectHasOwn, + PromiseResolve, + SafeSet, Symbol, SymbolFor, SymbolIterator, @@ -91,11 +94,11 @@ class NodeWorker extends EventEmitter { resourceLimits: Required< NonNullable > = { - maxYoungGenerationSizeMb: -1, - maxOldGenerationSizeMb: -1, - codeRangeSizeMb: -1, - stackSizeMb: 4, - }; + maxYoungGenerationSizeMb: -1, + maxOldGenerationSizeMb: -1, + codeRangeSizeMb: -1, + stackSizeMb: 4, + }; constructor(specifier: URL | string, options?: WorkerOptions) { super(); @@ -280,7 +283,8 @@ class NodeWorker extends EventEmitter { this.#status = "TERMINATED"; op_host_terminate_worker(this.#id); } - this.emit("exit", 1); + this.emit("exit", 0); + return PromiseResolve(0); } ref() { @@ -367,7 +371,7 @@ internals.__initWorkerThreads = ( defaultExport.parentPort = parentPort; defaultExport.threadId = threadId; - workerData = patchMessagePortIfFound(workerData); + patchMessagePortIfFound(workerData); parentPort.off = parentPort.removeListener = function ( this: ParentPort, @@ -385,8 +389,8 @@ internals.__initWorkerThreads = ( ) { // deno-lint-ignore no-explicit-any const _listener = (ev: any) => { - let message = ev.data; - message = patchMessagePortIfFound(message); + const message = ev.data; + patchMessagePortIfFound(message); return listener(message); }; listeners.set(listener, _listener); @@ -403,7 +407,7 @@ internals.__initWorkerThreads = ( }; // mocks - parentPort.setMaxListeners = () => {}; + parentPort.setMaxListeners = () => { }; parentPort.getMaxListeners = () => Infinity; parentPort.eventNames = () => [""]; parentPort.listenerCount = () => 0; @@ -482,7 +486,10 @@ const listeners = new SafeWeakMap< function webMessagePortToNodeMessagePort(port: MessagePort) { port.on = port.addListener = function (this: MessagePort, name, listener) { // deno-lint-ignore no-explicit-any - const _listener = (ev: any) => listener(ev.data); + const _listener = (ev: any) => { + patchMessagePortIfFound(ev.data); + listener(ev.data); + }; if (name == "message") { if (port.onmessage === null) { port.onmessage = _listener; @@ -532,19 +539,26 @@ function webMessagePortToNodeMessagePort(port: MessagePort) { return port; } +// TODO(@marvinhagemeister): Recursively iterating over all message +// properties seems slow. +// Maybe there is a way we can patch the prototype of MessagePort _only_ +// inside worker_threads? For now correctness is more important than perf. // deno-lint-ignore no-explicit-any -function patchMessagePortIfFound(data: any) { +function patchMessagePortIfFound(data: any, seen = new SafeSet()) { + if (data === null || typeof data !== "object" || seen.has(data)) { + return; + } + seen.add(data); + if (ObjectPrototypeIsPrototypeOf(MessagePortPrototype, data)) { - data = webMessagePortToNodeMessagePort(data); + webMessagePortToNodeMessagePort(data); } else { for (const obj in data as Record) { - if (ObjectPrototypeIsPrototypeOf(MessagePortPrototype, data[obj])) { - data[obj] = webMessagePortToNodeMessagePort(data[obj] as MessagePort); - break; + if (ObjectHasOwn(data, obj)) { + patchMessagePortIfFound(data[obj], seen); } } } - return data; } export { diff --git a/crates/node/resolution.rs b/crates/node/resolution.rs index 8f6c55c7b..878c8bb21 100644 --- a/crates/node/resolution.rs +++ b/crates/node/resolution.rs @@ -4,11 +4,9 @@ use std::borrow::Cow; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; -use std::rc::Rc; +use deno_config::package_json::PackageJsonRc; use deno_core::anyhow::bail; -use deno_core::anyhow::Context; -use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::serde_json::Map; use deno_core::serde_json::Value; @@ -18,11 +16,36 @@ use deno_fs::FileSystemRc; use deno_media_type::MediaType; use crate::errors; +use crate::errors::ClosestPkgJsonError; +use crate::errors::ClosestPkgJsonErrorKind; +use crate::errors::FinalizeResolutionError; +use crate::errors::InvalidModuleSpecifierError; +use crate::errors::InvalidPackageTargetError; +use crate::errors::LegacyMainResolveError; +use crate::errors::ModuleNotFoundError; +use crate::errors::NodeResolveError; +use crate::errors::PackageExportsResolveError; +use crate::errors::PackageImportNotDefinedError; +use crate::errors::PackageImportsResolveError; +use crate::errors::PackageImportsResolveErrorKind; +use crate::errors::PackagePathNotExportedError; +use crate::errors::PackageResolveError; +use crate::errors::PackageSubpathResolveError; +use crate::errors::PackageSubpathResolveErrorKind; +use crate::errors::PackageTargetNotFoundError; +use crate::errors::PackageTargetResolveError; +use crate::errors::PackageTargetResolveErrorKind; +use crate::errors::PathToDeclarationUrlError; +use crate::errors::ResolveBinaryCommandsError; +use crate::errors::ResolvePkgJsonBinExportError; +use crate::errors::ResolvePkgSubpathFromDenoModuleError; +use crate::errors::TypeScriptNotSupportedInNpmError; +use crate::errors::UnsupportedDirImportError; +use crate::errors::UnsupportedEsmUrlSchemeError; +use crate::errors::UrlToNodeResolutionError; use crate::is_builtin_node_module; use crate::path::to_file_specifier; use crate::polyfill::get_module_name_from_builtin_node_module_specifier; -use crate::AllowAllNodePermissions; -use crate::NodePermissions; use crate::NpmResolverRc; use crate::PackageJson; use crate::PathClean; @@ -30,11 +53,7 @@ use crate::PathClean; pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum NodeModuleKind { - Esm, - Cjs, -} +pub type NodeModuleKind = deno_config::package_json::NodeModuleKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NodeResolutionMode { @@ -150,9 +169,9 @@ impl NodeResolver { &self, specifier: &str, referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result, AnyError> { + ) -> Result, NodeResolveError> { // Note: if we are here, then the referrer is an esm module // TODO(bartlomieju): skipped "policy" part as we don't plan to support it @@ -172,36 +191,35 @@ impl NodeResolver { let protocol = url.scheme(); if protocol != "file" && protocol != "data" { - return Err(errors::err_unsupported_esm_url_scheme(&url)); + return Err(UnsupportedEsmUrlSchemeError { + url_scheme: protocol.to_string(), + } + .into()); } // todo(dsherret): this seems wrong if referrer.scheme() == "data" { - let url = referrer.join(specifier).map_err(AnyError::from)?; + let url = referrer + .join(specifier) + .map_err(|source| NodeResolveError::DataUrlReferrerFailed { source })?; return Ok(Some(NodeResolution::Esm(url))); } } - let url = - self.module_resolve(specifier, referrer, DEFAULT_CONDITIONS, mode, permissions)?; - let url = match url { + let maybe_url = self.module_resolve( + specifier, + referrer, + referrer_kind, + match referrer_kind { + NodeModuleKind::Esm => DEFAULT_CONDITIONS, + NodeModuleKind::Cjs => REQUIRE_CONDITIONS, + }, + mode, + )?; + let url = match maybe_url { Some(url) => url, None => return Ok(None), }; - let url = match mode { - NodeResolutionMode::Execution => url, - NodeResolutionMode::Types => { - let path = url.to_file_path().unwrap(); - // todo(16370): the module kind is not correct here. I think we need - // typescript to tell us if the referrer is esm or cjs - let maybe_decl_url = - self.path_to_declaration_url(path, referrer, NodeModuleKind::Esm, permissions)?; - match maybe_decl_url { - Some(url) => url, - None => return Ok(None), - } - } - }; let resolve_response = self.url_to_node_resolution(url)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and @@ -213,46 +231,52 @@ impl NodeResolver { &self, specifier: &str, referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, conditions: &[&str], mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result, AnyError> { + ) -> Result, NodeResolveError> { // note: if we're here, the referrer is an esm module - let url = if should_be_treated_as_relative_or_absolute_path(specifier) { - let resolved_specifier = referrer.join(specifier)?; - if mode.is_types() { - let file_path = to_file_path(&resolved_specifier); - // todo(dsherret): the node module kind is not correct and we - // should use the value provided by typescript instead - self.path_to_declaration_url(file_path, referrer, NodeModuleKind::Esm, permissions)? + let maybe_url = + if should_be_treated_as_relative_or_absolute_path(specifier) { + Some(referrer.join(specifier).map_err(|err| { + NodeResolveError::RelativeJoinError { + path: specifier.to_string(), + base: referrer.clone(), + source: err, + } + })?) + } else if specifier.starts_with('#') { + let pkg_config = self + .get_closest_package_json(referrer) + .map_err(PackageImportsResolveErrorKind::ClosestPkgJson) + .map_err(|err| PackageImportsResolveError(Box::new(err)))?; + Some(self.package_imports_resolve( + specifier, + Some(referrer), + referrer_kind, + pkg_config.as_deref(), + conditions, + mode, + )?) + } else if let Ok(resolved) = Url::parse(specifier) { + Some(resolved) } else { - Some(resolved_specifier) - } - } else if specifier.starts_with('#') { - let pkg_config = self.get_closest_package_json(referrer, permissions)?; - Some(self.package_imports_resolve( - specifier, - referrer, - NodeModuleKind::Esm, - pkg_config.as_deref(), - conditions, - mode, - permissions, - )?) - } else if let Ok(resolved) = Url::parse(specifier) { - Some(resolved) + self.package_resolve(specifier, referrer, referrer_kind, conditions, mode)? + }; + + let Some(url) = maybe_url else { + return Ok(None); + }; + + let maybe_url = if mode.is_types() { + let file_path = to_file_path(&url); + self.path_to_declaration_url(file_path, Some(referrer), referrer_kind)? } else { - self.package_resolve( - specifier, - referrer, - NodeModuleKind::Esm, - conditions, - mode, - permissions, - )? + Some(url) }; - Ok(match url { - Some(url) => Some(self.finalize_resolution(url, referrer)?), + + Ok(match maybe_url { + Some(url) => Some(self.finalize_resolution(url, Some(referrer))?), None => None, }) } @@ -260,16 +284,17 @@ impl NodeResolver { fn finalize_resolution( &self, resolved: ModuleSpecifier, - base: &ModuleSpecifier, - ) -> Result { + maybe_referrer: Option<&ModuleSpecifier>, + ) -> Result { let encoded_sep_re = lazy_regex::regex!(r"%2F|%2C"); if encoded_sep_re.is_match(resolved.path()) { - return Err(errors::err_invalid_module_specifier( - resolved.path(), - "must not include encoded \"/\" or \"\\\\\" characters", - Some(to_file_path_string(base)), - )); + return Err(errors::InvalidModuleSpecifierError { + request: resolved.to_string(), + reason: Cow::Borrowed("must not include encoded \"/\" or \"\\\\\" characters"), + maybe_referrer: maybe_referrer.map(to_file_path_string), + } + .into()); } if resolved.scheme() == "node" { @@ -296,16 +321,18 @@ impl NodeResolver { (false, false) }; if is_dir { - return Err(errors::err_unsupported_dir_import( - resolved.as_str(), - base.as_str(), - )); + return Err(UnsupportedDirImportError { + dir_url: resolved.clone(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into()); } else if !is_file { - return Err(errors::err_module_not_found( - resolved.as_str(), - base.as_str(), - "module", - )); + return Err(ModuleNotFoundError { + specifier: resolved, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + typ: "module", + } + .into()); } Ok(resolved) @@ -315,71 +342,44 @@ impl NodeResolver { &self, package_dir: &Path, package_subpath: Option<&str>, - referrer: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result, AnyError> { - let package_json_path = package_dir.join("package.json"); - let package_json = self.load_package_json(permissions, package_json_path.clone())?; + ) -> Result, ResolvePkgSubpathFromDenoModuleError> { let node_module_kind = NodeModuleKind::Esm; let package_subpath = package_subpath .map(|s| format!("./{s}")) .unwrap_or_else(|| ".".to_string()); - let maybe_resolved_url = self - .resolve_package_subpath( - &package_json, - &package_subpath, - referrer, - node_module_kind, - DEFAULT_CONDITIONS, - mode, - permissions, - ) - .with_context(|| { - format!( - "Failed resolving package subpath '{}' for '{}'", - package_subpath, - package_json.path.display() - ) - })?; + let maybe_resolved_url = self.resolve_package_dir_subpath( + package_dir, + &package_subpath, + maybe_referrer, + node_module_kind, + DEFAULT_CONDITIONS, + mode, + )?; let resolved_url = match maybe_resolved_url { - Some(resolved_path) => resolved_path, + Some(resolved_url) => resolved_url, None => return Ok(None), }; - let resolved_url = match mode { - NodeResolutionMode::Execution => resolved_url, - NodeResolutionMode::Types => { - if resolved_url.scheme() == "file" { - let path = resolved_url.to_file_path().unwrap(); - match self.path_to_declaration_url( - path, - referrer, - node_module_kind, - permissions, - )? { - Some(url) => url, - None => return Ok(None), - } - } else { - resolved_url - } - } - }; let resolve_response = self.url_to_node_resolution(resolved_url)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. Ok(Some(resolve_response)) } - pub fn resolve_binary_commands(&self, package_folder: &Path) -> Result, AnyError> { - let package_json_path = package_folder.join("package.json"); - let package_json = - self.load_package_json(&AllowAllNodePermissions, package_json_path.clone())?; + pub fn resolve_binary_commands( + &self, + package_folder: &Path, + ) -> Result, ResolveBinaryCommandsError> { + let pkg_json_path = package_folder.join("package.json"); + let Some(package_json) = self.load_package_json(&pkg_json_path)? else { + return Ok(Vec::new()); + }; Ok(match &package_json.bin { Some(Value::String(_)) => { let Some(name) = &package_json.name else { - bail!("'{}' did not have a name", package_json_path.display()); + return Err(ResolveBinaryCommandsError::MissingPkgJsonName { pkg_json_path }); }; vec![name.to_string()] } @@ -392,11 +392,16 @@ impl NodeResolver { &self, package_folder: &Path, sub_path: Option<&str>, - ) -> Result { - let package_json_path = package_folder.join("package.json"); - let package_json = - self.load_package_json(&AllowAllNodePermissions, package_json_path.clone())?; - let bin_entry = resolve_bin_entry_value(&package_json, sub_path)?; + ) -> Result { + let pkg_json_path = package_folder.join("package.json"); + let Some(package_json) = self.load_package_json(&pkg_json_path)? else { + return Err(ResolvePkgJsonBinExportError::MissingPkgJson { pkg_json_path }); + }; + let bin_entry = resolve_bin_entry_value(&package_json, sub_path).map_err(|err| { + ResolvePkgJsonBinExportError::InvalidBinProperty { + message: err.to_string(), + } + })?; let url = to_file_specifier(&package_folder.join(bin_entry)); let resolve_response = self.url_to_node_resolution(url)?; @@ -405,13 +410,15 @@ impl NodeResolver { Ok(resolve_response) } - pub fn url_to_node_resolution(&self, url: ModuleSpecifier) -> Result { + pub fn url_to_node_resolution( + &self, + url: ModuleSpecifier, + ) -> Result { let url_str = url.as_str().to_lowercase(); if url_str.starts_with("http") || url_str.ends_with(".json") { Ok(NodeResolution::Esm(url)) } else if url_str.ends_with(".js") || url_str.ends_with(".d.ts") { - let maybe_package_config = - self.get_closest_package_json(&url, &AllowAllNodePermissions)?; + let maybe_package_config = self.get_closest_package_json(&url)?; match maybe_package_config { Some(c) if c.typ == "module" => Ok(NodeResolution::Esm(url)), Some(_) => Ok(NodeResolution::CommonJs(url)), @@ -421,9 +428,7 @@ impl NodeResolver { Ok(NodeResolution::Esm(url)) } else if url_str.ends_with(".ts") || url_str.ends_with(".mts") { if self.in_npm_package(&url) { - Err(generic_error(format!( - "TypeScript files are not supported in npm packages: {url}" - ))) + Err(TypeScriptNotSupportedInNpmError { specifier: url }.into()) } else { Ok(NodeResolution::Esm(url)) } @@ -436,10 +441,9 @@ impl NodeResolver { fn path_to_declaration_url( &self, path: PathBuf, - referrer: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, referrer_kind: NodeModuleKind, - permissions: &dyn NodePermissions, - ) -> Result, AnyError> { + ) -> Result, PathToDeclarationUrlError> { fn probe_extensions( fs: &dyn deno_fs::FileSystem, path: &Path, @@ -495,23 +499,19 @@ impl NodeResolver { return Ok(Some(to_file_specifier(&path))); } if self.fs.is_dir_sync(&path) { - let package_json_path = path.join("package.json"); - if let Ok(pkg_json) = self.load_package_json(permissions, package_json_path) { - let maybe_resolution = self.resolve_package_subpath( - &pkg_json, - /* sub path */ ".", - referrer, - referrer_kind, - match referrer_kind { - NodeModuleKind::Esm => DEFAULT_CONDITIONS, - NodeModuleKind::Cjs => REQUIRE_CONDITIONS, - }, - NodeResolutionMode::Types, - permissions, - )?; - if let Some(resolution) = maybe_resolution { - return Ok(Some(resolution)); - } + let maybe_resolution = self.resolve_package_dir_subpath( + &path, + /* sub path */ ".", + maybe_referrer, + referrer_kind, + match referrer_kind { + NodeModuleKind::Esm => DEFAULT_CONDITIONS, + NodeModuleKind::Cjs => REQUIRE_CONDITIONS, + }, + NodeResolutionMode::Types, + )?; + if let Some(resolution) = maybe_resolution { + return Ok(Some(resolution)); } let index_path = path.join("index.js"); if let Some(path) = probe_extensions( @@ -534,99 +534,95 @@ impl NodeResolver { pub(super) fn package_imports_resolve( &self, name: &str, - referrer: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, referrer_kind: NodeModuleKind, referrer_pkg_json: Option<&PackageJson>, conditions: &[&str], mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result { + ) -> Result { if name == "#" || name.starts_with("#/") || name.ends_with('/') { let reason = "is not a valid internal imports specifier name"; - return Err(errors::err_invalid_module_specifier( - name, - reason, - Some(to_specifier_display_string(referrer)), - )); + return Err(errors::InvalidModuleSpecifierError { + request: name.to_string(), + reason: Cow::Borrowed(reason), + maybe_referrer: maybe_referrer.map(to_specifier_display_string), + } + .into()); } let mut package_json_path = None; if let Some(pkg_json) = &referrer_pkg_json { - if pkg_json.exists { - package_json_path = Some(pkg_json.path.clone()); - if let Some(imports) = &pkg_json.imports { - if imports.contains_key(name) && !name.contains('*') { - let target = imports.get(name).unwrap(); + package_json_path = Some(pkg_json.path.clone()); + if let Some(imports) = &pkg_json.imports { + if imports.contains_key(name) && !name.contains('*') { + let target = imports.get(name).unwrap(); + let maybe_resolved = self.resolve_package_target( + package_json_path.as_ref().unwrap(), + target, + "", + name, + maybe_referrer, + referrer_kind, + false, + true, + conditions, + mode, + )?; + if let Some(resolved) = maybe_resolved { + return Ok(resolved); + } + } else { + let mut best_match = ""; + let mut best_match_subpath = None; + for key in imports.keys() { + let pattern_index = key.find('*'); + if let Some(pattern_index) = pattern_index { + let key_sub = &key[0..=pattern_index]; + if name.starts_with(key_sub) { + let pattern_trailer = &key[pattern_index + 1..]; + if name.len() > key.len() + && name.ends_with(&pattern_trailer) + && pattern_key_compare(best_match, key) == 1 + && key.rfind('*') == Some(pattern_index) + { + best_match = key; + best_match_subpath = Some( + name[pattern_index..=(name.len() - pattern_trailer.len())] + .to_string(), + ); + } + } + } + } + + if !best_match.is_empty() { + let target = imports.get(best_match).unwrap(); let maybe_resolved = self.resolve_package_target( package_json_path.as_ref().unwrap(), target, - "", - name, - referrer, + &best_match_subpath.unwrap(), + best_match, + maybe_referrer, referrer_kind, - false, + true, true, conditions, mode, - permissions, )?; if let Some(resolved) = maybe_resolved { return Ok(resolved); } - } else { - let mut best_match = ""; - let mut best_match_subpath = None; - for key in imports.keys() { - let pattern_index = key.find('*'); - if let Some(pattern_index) = pattern_index { - let key_sub = &key[0..=pattern_index]; - if name.starts_with(key_sub) { - let pattern_trailer = &key[pattern_index + 1..]; - if name.len() > key.len() - && name.ends_with(&pattern_trailer) - && pattern_key_compare(best_match, key) == 1 - && key.rfind('*') == Some(pattern_index) - { - best_match = key; - best_match_subpath = Some( - name[pattern_index - ..=(name.len() - pattern_trailer.len())] - .to_string(), - ); - } - } - } - } - - if !best_match.is_empty() { - let target = imports.get(best_match).unwrap(); - let maybe_resolved = self.resolve_package_target( - package_json_path.as_ref().unwrap(), - target, - &best_match_subpath.unwrap(), - best_match, - referrer, - referrer_kind, - true, - true, - conditions, - mode, - permissions, - )?; - if let Some(resolved) = maybe_resolved { - return Ok(resolved); - } - } } } } } - Err(throw_import_not_defined( - name, - package_json_path.as_deref(), - referrer, - )) + Err(PackageImportNotDefinedError { + name: name.to_string(), + package_json_path, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into()) } #[allow(clippy::too_many_arguments)] @@ -636,22 +632,22 @@ impl NodeResolver { subpath: &str, match_: &str, package_json_path: &Path, - referrer: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, referrer_kind: NodeModuleKind, pattern: bool, internal: bool, conditions: &[&str], mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result { + ) -> Result { if !subpath.is_empty() && !pattern && !target.ends_with('/') { - return Err(throw_invalid_package_target( - match_, - target, - package_json_path, - internal, - referrer, - )); + return Err(InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: match_.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into()); } let invalid_segment_re = lazy_regex::regex!(r"(^|\\|/)(\.\.?|node_modules)(\\|/|$)"); let pattern_re = lazy_regex::regex!(r"\*"); @@ -679,11 +675,21 @@ impl NodeResolver { referrer_kind, conditions, mode, - permissions, ) { Ok(Some(url)) => Ok(url), - Ok(None) => Err(generic_error("not found")), - Err(err) => Err(err), + Ok(None) => Err(PackageTargetResolveErrorKind::NotFound( + PackageTargetNotFoundError { + pkg_json_path: package_json_path.to_path_buf(), + target: export_target.to_string(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + referrer_kind, + mode, + }, + ) + .into()), + Err(err) => { + Err(PackageTargetResolveErrorKind::PackageResolve(err).into()) + } }; return match result { @@ -699,33 +705,36 @@ impl NodeResolver { } } } - return Err(throw_invalid_package_target( - match_, - target, - package_json_path, - internal, - referrer, - )); + return Err(InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: match_.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into()); } if invalid_segment_re.is_match(&target[2..]) { - return Err(throw_invalid_package_target( - match_, - target, - package_json_path, - internal, - referrer, - )); + return Err(InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: match_.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into()); } let package_path = package_json_path.parent().unwrap(); let resolved_path = package_path.join(target).clean(); if !resolved_path.starts_with(package_path) { - return Err(throw_invalid_package_target( - match_, - target, - package_json_path, - internal, - referrer, - )); + return Err(InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: match_.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into()); } if subpath.is_empty() { return Ok(to_file_specifier(&resolved_path)); @@ -740,8 +749,9 @@ impl NodeResolver { request, package_json_path, internal, - referrer, - )); + maybe_referrer, + ) + .into()); } if pattern { let resolved_path_str = resolved_path.to_string_lossy(); @@ -759,31 +769,29 @@ impl NodeResolver { target: &Value, subpath: &str, package_subpath: &str, - referrer: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, referrer_kind: NodeModuleKind, pattern: bool, internal: bool, conditions: &[&str], mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result, AnyError> { + ) -> Result, PackageTargetResolveError> { if let Some(target) = target.as_str() { let url = self.resolve_package_target_string( target, subpath, package_subpath, package_json_path, - referrer, + maybe_referrer, referrer_kind, pattern, internal, conditions, mode, - permissions, )?; if mode.is_types() && url.scheme() == "file" { let path = url.to_file_path().unwrap(); - return self.path_to_declaration_url(path, referrer, referrer_kind, permissions); + return Ok(self.path_to_declaration_url(path, maybe_referrer, referrer_kind)?); } else { return Ok(Some(url)); } @@ -799,13 +807,12 @@ impl NodeResolver { target_item, subpath, package_subpath, - referrer, + maybe_referrer, referrer_kind, pattern, internal, conditions, mode, - permissions, ); match resolved_result { @@ -815,12 +822,13 @@ impl NodeResolver { continue; } Err(e) => { - let err_string = e.to_string(); - last_error = Some(e); - if err_string.starts_with("[ERR_INVALID_PACKAGE_TARGET]") { + // todo(dsherret): add codes to each error and match on that instead + if e.to_string().starts_with("[ERR_INVALID_PACKAGE_TARGET]") { + last_error = Some(e); continue; + } else { + return Err(e); } - return Err(last_error.unwrap()); } } } @@ -848,13 +856,12 @@ impl NodeResolver { condition_target, subpath, package_subpath, - referrer, + maybe_referrer, referrer_kind, pattern, internal, conditions, mode, - permissions, )?; match resolved { Some(resolved) => return Ok(Some(resolved)), @@ -868,13 +875,14 @@ impl NodeResolver { return Ok(None); } - Err(throw_invalid_package_target( - package_subpath, - &target.to_string(), - package_json_path, - internal, - referrer, - )) + Err(InvalidPackageTargetError { + pkg_json_path: package_json_path.to_path_buf(), + sub_path: package_subpath.to_string(), + target: target.to_string(), + is_import: internal, + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), + } + .into()) } #[allow(clippy::too_many_arguments)] @@ -883,12 +891,11 @@ impl NodeResolver { package_json_path: &Path, package_subpath: &str, package_exports: &Map, - referrer: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, referrer_kind: NodeModuleKind, conditions: &[&str], mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result { + ) -> Result { if package_exports.contains_key(package_subpath) && package_subpath.find('*').is_none() && !package_subpath.ends_with('/') @@ -899,22 +906,22 @@ impl NodeResolver { target, "", package_subpath, - referrer, + maybe_referrer, referrer_kind, false, false, conditions, mode, - permissions, )?; return match resolved { Some(resolved) => Ok(resolved), - None => Err(throw_exports_not_found( - package_subpath, - package_json_path, - referrer, + None => Err(PackagePathNotExportedError { + pkg_json_path: package_json_path.to_path_buf(), + subpath: package_subpath.to_string(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), mode, - )), + } + .into()), }; } @@ -959,32 +966,33 @@ impl NodeResolver { target, &best_match_subpath.unwrap(), best_match, - referrer, + maybe_referrer, referrer_kind, true, false, conditions, mode, - permissions, )?; if let Some(resolved) = maybe_resolved { return Ok(resolved); } else { - return Err(throw_exports_not_found( - package_subpath, - package_json_path, - referrer, + return Err(PackagePathNotExportedError { + pkg_json_path: package_json_path.to_path_buf(), + subpath: package_subpath.to_string(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), mode, - )); + } + .into()); } } - Err(throw_exports_not_found( - package_subpath, - package_json_path, - referrer, + Err(PackagePathNotExportedError { + pkg_json_path: package_json_path.to_path_buf(), + subpath: package_subpath.to_string(), + maybe_referrer: maybe_referrer.map(ToOwned::to_owned), mode, - )) + } + .into()) } pub(super) fn package_resolve( @@ -994,35 +1002,89 @@ impl NodeResolver { referrer_kind: NodeModuleKind, conditions: &[&str], mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result, AnyError> { + ) -> Result, PackageResolveError> { let (package_name, package_subpath, _is_scoped) = parse_npm_pkg_name(specifier, referrer)?; - let Some(package_config) = self.get_closest_package_json(referrer, permissions)? else { + let Some(package_config) = self.get_closest_package_json(referrer)? else { return Ok(None); }; // ResolveSelf - if package_config.exists && package_config.name.as_ref() == Some(&package_name) { + if package_config.name.as_ref() == Some(&package_name) { if let Some(exports) = &package_config.exports { return self .package_exports_resolve( &package_config.path, &package_subpath, exports, - referrer, + Some(referrer), referrer_kind, conditions, mode, - permissions, ) - .map(Some); + .map(Some) + .map_err(|err| err.into()); } } - let package_dir_path = - self.npm_resolver - .resolve_package_folder_from_package(&package_name, referrer, mode)?; - let package_json_path = package_dir_path.join("package.json"); + self.resolve_package_subpath_for_package( + &package_name, + &package_subpath, + referrer, + referrer_kind, + conditions, + mode, + ) + .map_err(|err| err.into()) + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_subpath_for_package( + &self, + package_name: &str, + package_subpath: &str, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result, PackageSubpathResolveError> { + let result = self.resolve_package_subpath_for_package_inner( + package_name, + package_subpath, + referrer, + referrer_kind, + conditions, + mode, + ); + if mode.is_types() && !matches!(result, Ok(Some(_))) { + // try to resolve with the @types package + let package_name = types_package_name(package_name); + if let Ok(Some(result)) = self.resolve_package_subpath_for_package_inner( + &package_name, + package_subpath, + referrer, + referrer_kind, + conditions, + mode, + ) { + return Ok(Some(result)); + } + } + result + } + + #[allow(clippy::too_many_arguments)] + fn resolve_package_subpath_for_package_inner( + &self, + package_name: &str, + package_subpath: &str, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result, PackageSubpathResolveError> { + let package_dir_path = self + .npm_resolver + .resolve_package_folder_from_package(package_name, referrer)?; // todo: error with this instead when can't find package // Err(errors::err_module_not_found( @@ -1038,29 +1100,58 @@ impl NodeResolver { // )) // Package match. - let package_json = self.load_package_json(permissions, package_json_path)?; - self.resolve_package_subpath( - &package_json, - &package_subpath, - referrer, + self.resolve_package_dir_subpath( + &package_dir_path, + package_subpath, + Some(referrer), referrer_kind, conditions, mode, - permissions, ) } + #[allow(clippy::too_many_arguments)] + fn resolve_package_dir_subpath( + &self, + package_dir_path: &Path, + package_subpath: &str, + maybe_referrer: Option<&ModuleSpecifier>, + referrer_kind: NodeModuleKind, + conditions: &[&str], + mode: NodeResolutionMode, + ) -> Result, PackageSubpathResolveError> { + let package_json_path = package_dir_path.join("package.json"); + match self.load_package_json(&package_json_path)? { + Some(pkg_json) => self.resolve_package_subpath( + &pkg_json, + package_subpath, + maybe_referrer, + referrer_kind, + conditions, + mode, + ), + None => self + .resolve_package_subpath_no_pkg_json( + package_dir_path, + package_subpath, + maybe_referrer, + referrer_kind, + mode, + ) + .map_err(|err| PackageSubpathResolveErrorKind::LegacyExact(err).into()), + } + } + #[allow(clippy::too_many_arguments)] fn resolve_package_subpath( &self, package_json: &PackageJson, package_subpath: &str, - referrer: &ModuleSpecifier, + referrer: Option<&ModuleSpecifier>, referrer_kind: NodeModuleKind, conditions: &[&str], mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result, AnyError> { + ) -> Result, PackageSubpathResolveError> { if let Some(exports) = &package_json.exports { let result = self.package_exports_resolve( &package_json.path, @@ -1070,78 +1161,94 @@ impl NodeResolver { referrer_kind, conditions, mode, - permissions, ); match result { Ok(found) => return Ok(Some(found)), Err(exports_err) => { if mode.is_types() && package_subpath == "." { - return self.legacy_main_resolve( - package_json, - referrer, - referrer_kind, - mode, - permissions, - ); + return self + .legacy_main_resolve(package_json, referrer, referrer_kind, mode) + .map_err(|err| PackageSubpathResolveErrorKind::LegacyMain(err).into()); } - return Err(exports_err); + return Err(PackageSubpathResolveErrorKind::Exports(exports_err).into()); } } } + if package_subpath == "." { - return self.legacy_main_resolve( - package_json, - referrer, - referrer_kind, - mode, - permissions, - ); + return self + .legacy_main_resolve(package_json, referrer, referrer_kind, mode) + .map_err(|err| PackageSubpathResolveErrorKind::LegacyMain(err).into()); } - let file_path = package_json.path.parent().unwrap().join(package_subpath); + self.resolve_subpath_exact( + package_json.path.parent().unwrap(), + package_subpath, + referrer, + referrer_kind, + mode, + ) + .map_err(|err| PackageSubpathResolveErrorKind::LegacyExact(err).into()) + } + + fn resolve_subpath_exact( + &self, + directory: &Path, + package_subpath: &str, + referrer: Option<&ModuleSpecifier>, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result, PathToDeclarationUrlError> { + assert_ne!(package_subpath, "."); + let file_path = directory.join(package_subpath); if mode.is_types() { - self.path_to_declaration_url(file_path, referrer, referrer_kind, permissions) + Ok(self.path_to_declaration_url(file_path, referrer, referrer_kind)?) } else { Ok(Some(to_file_specifier(&file_path))) } } + fn resolve_package_subpath_no_pkg_json( + &self, + directory: &Path, + package_subpath: &str, + referrer: Option<&ModuleSpecifier>, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result, PathToDeclarationUrlError> { + if package_subpath == "." { + Ok(self.legacy_index_resolve(directory, referrer_kind, mode)) + } else { + self.resolve_subpath_exact(directory, package_subpath, referrer, referrer_kind, mode) + } + } + pub fn get_closest_package_json( &self, url: &ModuleSpecifier, - permissions: &dyn NodePermissions, - ) -> Result>, AnyError> { + ) -> Result, ClosestPkgJsonError> { let Ok(file_path) = url.to_file_path() else { return Ok(None); }; - self.get_closest_package_json_from_path(&file_path, permissions) + self.get_closest_package_json_from_path(&file_path) } pub fn get_closest_package_json_from_path( &self, file_path: &Path, - permissions: &dyn NodePermissions, - ) -> Result>, AnyError> { - let Some(package_json_path) = self.get_closest_package_json_path(file_path)? else { - return Ok(None); - }; - self.load_package_json(permissions, package_json_path) - .map(Some) - } - - fn get_closest_package_json_path(&self, file_path: &Path) -> Result, AnyError> { + ) -> Result, ClosestPkgJsonError> { + let parent_dir = file_path.parent().unwrap(); let current_dir = - deno_core::strip_unc_prefix(self.fs.realpath_sync(file_path.parent().unwrap())?); - let mut current_dir = current_dir.as_path(); - let package_json_path = current_dir.join("package.json"); - if self.fs.exists_sync(&package_json_path) { - return Ok(Some(package_json_path)); - } - while let Some(parent) = current_dir.parent() { - current_dir = parent; + deno_core::strip_unc_prefix(self.fs.realpath_sync(parent_dir).map_err(|source| { + ClosestPkgJsonErrorKind::CanonicalizingDir { + dir_path: parent_dir.to_path_buf(), + source: source.into_io_error(), + } + })?); + for current_dir in current_dir.ancestors() { let package_json_path = current_dir.join("package.json"); - if self.fs.exists_sync(&package_json_path) { - return Ok(Some(package_json_path)); + if let Some(pkg_json) = self.load_package_json(&package_json_path)? { + return Ok(Some(pkg_json)); } } @@ -1150,25 +1257,18 @@ impl NodeResolver { pub(super) fn load_package_json( &self, - permissions: &dyn NodePermissions, - package_json_path: PathBuf, - ) -> Result, AnyError> { - PackageJson::load( - &*self.fs, - &*self.npm_resolver, - permissions, - package_json_path, - ) + package_json_path: &Path, + ) -> Result, deno_config::package_json::PackageJsonLoadError> { + crate::package_json::load_pkg_json(&*self.fs, package_json_path) } pub(super) fn legacy_main_resolve( &self, package_json: &PackageJson, - referrer: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, referrer_kind: NodeModuleKind, mode: NodeResolutionMode, - permissions: &dyn NodePermissions, - ) -> Result, AnyError> { + ) -> Result, LegacyMainResolveError> { let maybe_main = if mode.is_types() { match package_json.types.as_ref() { Some(types) => Some(types.as_str()), @@ -1177,12 +1277,9 @@ impl NodeResolver { // a corresponding declaration file if let Some(main) = package_json.main(referrer_kind) { let main = package_json.path.parent().unwrap().join(main).clean(); - let maybe_decl_url = self.path_to_declaration_url( - main, - referrer, - referrer_kind, - permissions, - )?; + let maybe_decl_url = self + .path_to_declaration_url(main, maybe_referrer, referrer_kind) + .map_err(LegacyMainResolveError::PathToDeclarationUrl)?; if let Some(path) = maybe_decl_url { return Ok(Some(path)); } @@ -1232,6 +1329,15 @@ impl NodeResolver { } } + Ok(self.legacy_index_resolve(package_json.path.parent().unwrap(), referrer_kind, mode)) + } + + fn legacy_index_resolve( + &self, + directory: &Path, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Option { let index_file_names = if mode.is_types() { // todo(dsherret): investigate exactly how typescript does this match referrer_kind { @@ -1242,19 +1348,14 @@ impl NodeResolver { vec!["index.js"] }; for index_file_name in index_file_names { - let guess = package_json - .path - .parent() - .unwrap() - .join(index_file_name) - .clean(); + let guess = directory.join(index_file_name).clean(); if self.fs.is_file_sync(&guess) { // TODO(bartlomieju): emitLegacyIndexDeprecation() - return Ok(Some(to_file_specifier(&guess))); + return Some(to_file_specifier(&guess)); } } - Ok(None) + None } } @@ -1271,7 +1372,13 @@ fn resolve_bin_entry_value<'a>( }; let bin_entry = match bin { Value::String(_) => { - if bin_name.is_some() && bin_name != package_json.name.as_deref() { + if bin_name.is_some() + && bin_name + != package_json + .name + .as_deref() + .map(|name| name.rsplit_once('/').map_or(name, |(_, name)| name)) + { None } else { Some(bin) @@ -1425,71 +1532,29 @@ fn to_specifier_display_string(url: &ModuleSpecifier) -> String { } } -fn throw_import_not_defined( - specifier: &str, - package_json_path: Option<&Path>, - base: &ModuleSpecifier, -) -> AnyError { - errors::err_package_import_not_defined( - specifier, - package_json_path.map(|p| p.parent().unwrap().display().to_string()), - &to_specifier_display_string(base), - ) -} - -fn throw_invalid_package_target( - subpath: &str, - target: &str, - package_json_path: &Path, - internal: bool, - referrer: &ModuleSpecifier, -) -> AnyError { - errors::err_invalid_package_target( - &package_json_path.parent().unwrap().display().to_string(), - subpath, - target, - internal, - Some(referrer.to_string()), - ) -} - fn throw_invalid_subpath( subpath: String, package_json_path: &Path, internal: bool, - referrer: &ModuleSpecifier, -) -> AnyError { + maybe_referrer: Option<&ModuleSpecifier>, +) -> InvalidModuleSpecifierError { let ie = if internal { "imports" } else { "exports" }; let reason = format!( "request is not a valid subpath for the \"{}\" resolution of {}", ie, package_json_path.display(), ); - errors::err_invalid_module_specifier( - &subpath, - &reason, - Some(to_specifier_display_string(referrer)), - ) -} - -fn throw_exports_not_found( - subpath: &str, - package_json_path: &Path, - referrer: &ModuleSpecifier, - mode: NodeResolutionMode, -) -> AnyError { - errors::err_package_path_not_exported( - package_json_path.parent().unwrap().display().to_string(), - subpath, - Some(to_specifier_display_string(referrer)), - mode, - ) + InvalidModuleSpecifierError { + request: subpath, + reason: Cow::Owned(reason), + maybe_referrer: maybe_referrer.map(to_specifier_display_string), + } } pub fn parse_npm_pkg_name( specifier: &str, referrer: &ModuleSpecifier, -) -> Result<(String, String, bool), AnyError> { +) -> Result<(String, String, bool), InvalidModuleSpecifierError> { let mut separator_index = specifier.find('/'); let mut valid_package_name = true; let mut is_scoped = false; @@ -1521,11 +1586,11 @@ pub fn parse_npm_pkg_name( } if !valid_package_name { - return Err(errors::err_invalid_module_specifier( - specifier, - "is not a valid package name", - Some(to_specifier_display_string(referrer)), - )); + return Err(errors::InvalidModuleSpecifierError { + request: specifier.to_string(), + reason: Cow::Borrowed("is not a valid package name"), + maybe_referrer: Some(to_specifier_display_string(referrer)), + }); } let package_subpath = if let Some(index) = separator_index { @@ -1579,6 +1644,14 @@ fn pattern_key_compare(a: &str, b: &str) -> i32 { 0 } +/// Gets the corresponding @types package for the provided package name. +fn types_package_name(package_name: &str) -> String { + debug_assert!(!package_name.starts_with("@types/")); + // Scoped packages will get two underscores for each slash + // https://github.com/DefinitelyTyped/DefinitelyTyped/tree/15f1ece08f7b498f4b9a2147c2a46e94416ca777#what-about-scoped-packages + format!("@types/{}", package_name.replace('/', "__")) +} + #[cfg(test)] mod tests { use deno_core::serde_json::json; @@ -1586,7 +1659,7 @@ mod tests { use super::*; fn build_package_json(json: Value) -> PackageJson { - PackageJson::load_from_value(PathBuf::from("/package.json"), json).unwrap() + PackageJson::load_from_value(PathBuf::from("/package.json"), json) } #[test] @@ -1758,4 +1831,13 @@ mod tests { assert_eq!(actual.to_string_lossy(), *expected); } } + + #[test] + fn test_types_package_name() { + assert_eq!(types_package_name("name"), "@types/name"); + assert_eq!( + types_package_name("@scoped/package"), + "@types/@scoped__package" + ); + } } From e5b38010fd608e29d7a19d524d3ceda53e1b7b81 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 16 Jul 2024 02:38:49 +0000 Subject: [PATCH 05/20] chore: add some crates and update dependencies --- Cargo.toml | 134 +++++++++++++++++++---------- crates/base/Cargo.toml | 19 ++-- crates/cli/Cargo.toml | 4 +- crates/deno_manifest/Cargo.toml | 3 + crates/http_utils/Cargo.toml | 6 +- crates/node/Cargo.toml | 89 ++++++++++--------- crates/npm/Cargo.toml | 5 +- crates/npm_cache/Cargo.toml | 25 ++++++ crates/sb_core/Cargo.toml | 7 +- crates/sb_fs/Cargo.toml | 1 + crates/sb_graph/Cargo.toml | 12 ++- crates/sb_module_loader/Cargo.toml | 1 + crates/sb_workers/Cargo.toml | 2 +- 13 files changed, 202 insertions(+), 106 deletions(-) create mode 100644 crates/deno_manifest/Cargo.toml create mode 100644 crates/npm_cache/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index 086f728ec..b3310d7d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "./crates/base_mem_check", "./crates/http_utils", "./crates/cli", + "./crates/deno_manifest", "./crates/sb_workers", "./crates/sb_env", "./crates/sb_core", @@ -13,6 +14,7 @@ members = [ "./crates/cpu_timer", "./crates/event_worker", "./crates/npm", + "./crates/npm_cache", "./crates/sb_graph", "./crates/sb_module_loader", "./crates/sb_fs", @@ -20,71 +22,113 @@ members = [ ] [workspace.dependencies] -deno_ast = { version = "=0.38.1", features = ["transpiling"] } -deno_broadcast_channel = "0.143.0" -deno_core = "0.278.0" -deno_console = "0.149.0" -deno_crypto = "0.163.0" -deno_fetch = "0.173.0" -deno_fs = { version = "0.59.0", features = ["sync_fs"] } -deno_config = "=0.16.3" -deno_io = "0.59.0" -deno_webgpu = "0.116.0" -deno_canvas = "0.18.0" -deno_graph = "=0.74.5" -deno_http = "0.146.0" +deno_ast = { version = "=0.40.0", features = ["transpiling"] } +deno_core = "0.293.0" +deno_broadcast_channel = "0.155.0" +deno_canvas = "0.30.0" +deno_console = "0.161.0" +deno_crypto = "0.175.0" +deno_fetch = "0.185.0" +deno_fs = { version = "0.71.0", features = ["sync_fs"] } +deno_http = "0.159.0" +deno_io = "0.71.0" +deno_net = "0.153.0" +deno_tls = "0.148.0" +deno_url = "0.161.0" +deno_web = "0.192.0" +deno_webgpu = "0.128.0" +deno_webidl = "0.161.0" +deno_websocket = "0.166.0" +deno_webstorage = "0.156.0" deno_media_type = { version = "0.1.4", features = ["module_specifier"] } -deno_net = "0.141.0" -deno_npm = "0.18.0" -deno_url = "0.149.0" -deno_semver = "0.5.4" -deno_tls = "0.136.0" -deno_webidl = "0.149.0" -deno_web = "0.180.0" -deno_websocket = "0.154.0" -deno_webstorage = "0.144.0" -deno_cache_dir = "=0.6.1" +deno_config = { version = "=0.22.2", default-features = false } +deno_graph = "=0.80.1" +deno_npm = "0.21.4" +deno_semver = "=0.5.6" +deno_cache_dir = "=0.10.0" +deno_lockfile = "0.20.0" -# hazmat needed for PrehashSigner in ext/node -rsa = { version = "0.9.3", default-features = false, features = ["std", "pem", "hazmat"] } +# crypto +hkdf = "0.12.3" +rsa = { version = "0.9.3", default-features = false, features = ["std", "pem", "hazmat"] } # hazmat needed for PrehashSigner in crates/node -url = "2.3.1" -eszip = "=0.68.2" +# unix +nix = "=0.26.2" + +# windows +winapi = "=0.3.9" +windows-sys = { version = "0.48.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem"] } + +# webcpu +wgpu-core = "0.21.1" +wgpu-types = "0.20" + +url = { version = "< 2.5.0", features = ["serde", "expose_internals"] } # upgrading past 2.4.1 may cause WPT failures +eszip = "=0.72.2" log = "0.4.20" anyhow = "1.0.57" -libc = "0.2.144" +libc = "0.2.126" +libz-sys = { version = "1.1", default-features = false } enum-as-inner = "0.6.0" serde = { version = "1.0.149", features = ["derive"] } -hyper = "0.14.26" +serde_json = "1.0.85" +hyper = { version = "=1.4.0", features = ["full"] } +hyper_v014 = { package = "hyper", version = "0.14.26", features = ["runtime", "http1"] } +hyper-util = { version = "=0.1.6", features = ["tokio", "server", "server-auto"] } tokio = { version = "1.36.0", features = ["full"] } +tokio-util = "0.7.4" bytes = "1.4.0" once_cell = "1.17.1" -thiserror = "1.0.40" -deno_lockfile = "0.19.0" +thiserror = "1.0.61" async-trait = "0.1.73" -indexmap = { version = "2.0.0", features = ["serde"] } -flate2 = "=1.0.26" +indexmap = { version = "2", features = ["serde"] } +flate2 = { version = "=1.0.26", default-features = false } tar = "=0.4.40" regex = "^1.7.0" fs3 = "0.5.0" -tokio-util = "0.7.4" uuid = { version = "1.3.0", features = ["v4"] } monch = "=0.5.0" -reqwest = { version = "0.11.20", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli", "socks", "json"] } +reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli", "socks", "json", "http2"] } # pinned because of https://github.com/seanmonstar/reqwest/pull/1955 ring = "^0.17.0" -urlencoding = "2.1.2" -import_map = { version = "=0.18.0", features = ["ext"] } +import_map = { version = "=0.20.0", features = ["ext"] } +base32 = "=0.4.0" base64 = "0.21.4" -futures = "0.3.28" -futures-util = "0.3.28" +futures = "0.3.21" +futures-util = "0.3.30" ctor = "0.2.6" -fastwebsockets = { version = "0.4.4", features = ["upgrade"] } -percent-encoding = "=2.3.1" +fastwebsockets = { version = "0.6", features = ["upgrade", "unstable-split"] } +percent-encoding = "2.3.0" scopeguard = "1.2.0" glob = "0.3.1" -httparse = "1.8" -http = "0.2" -faster-hex = "0.9.0" +httparse = "1.8.0" +http = "1.0" +http-body-util = "0.1" +http_v02 = { package = "http", version = "0.2.9" } +h2 = "0.4.4" +faster-hex = "0.9" +num-bigint = { version = "0.4", features = ["rand"] } +notify = "=5.0.0" +parking_lot = "0.12.0" +pin-project = "1.0.11" +rustls = "0.22.4" +rustls-pemfile = "2" +rustls-tokio-stream = "=0.2.23" +aes = "=0.8.3" +brotli = "6.0.0" +cbc = { version = "=0.1.2", features = ["alloc"] } +ecb = "=0.1.2" +data-encoding = "2.3.3" +elliptic-curve = { version = "0.13.4", features = ["alloc", "arithmetic", "ecdh", "std", "pem"] } +p224 = { version = "0.13.0", features = ["ecdh"] } +p256 = { version = "0.13.2", features = ["ecdh"] } +p384 = { version = "0.13.0", features = ["ecdh"] } +sha1 = { version = "0.10.6", features = ["oid"] } +sha2 = { version = "0.10.8", features = ["oid"] } +lazy-regex = "3" +rand = "=0.8.5" +signature = "2.1" +spki = "0.7.2" +urlencoding = "2.1.2" tracing = "0.1" tracing-subscriber = "0.3" sha2 = "0.10" @@ -92,8 +136,6 @@ rkyv = "0.7" tempfile = "3" [patch.crates-io] -# TODO(Nyannyacha): Patch below is temporary. Clean the line in the Deno 1.44 update. -deno_core = { git = "https://github.com/supabase/deno_core", branch = "278-supabase" } eszip = { git = "https://github.com/supabase/eszip", branch = "fix-pub-vis-0-68-2" } [profile.dind] diff --git a/crates/base/Cargo.toml b/crates/base/Cargo.toml index 2c4800564..0263ad204 100644 --- a/crates/base/Cargo.toml +++ b/crates/base/Cargo.toml @@ -50,11 +50,14 @@ once_cell.workspace = true anyhow.workspace = true bytes.workspace = true httparse.workspace = true -hyper = { workspace = true, features = ["full", "backports"] } +hyper = { workspace = true, features = ["full"] } +hyper_v014 = { workspace = true, features = ["full", "backports"] } +hyper-util = { workspace = true, features = ["server-graceful"] } http.workspace = true +http_v02.workspace = true +http-body-util.workspace = true import_map.workspace = true log.workspace = true -reqwest.workspace = true serde = { workspace = true, features = ["derive"] } tokio.workspace = true tokio-util = { workspace = true, features = ["rt"] } @@ -67,19 +70,18 @@ urlencoding.workspace = true scopeguard.workspace = true ctor.workspace = true fastwebsockets.workspace = true +notify.workspace = true +pin-project.workspace = true +rustls-pemfile.workspace = true -notify = { version = "6.1.1", default-features = false, features = ["macos_kqueue"] } +reqwest_v011 = { package = "reqwest", version = "0.11", features = ["stream", "json", "multipart"] } tls-listener = { version = "0.10", features = ["rustls"] } flume = "0.11.0" -pin-project = "1.1.3" cooked-waker = "5" -cityhash = "0.1.1" tokio-rustls = "0.25.0" -rustls-pemfile = "2.1.0" [dev-dependencies] tokio-util = { workspace = true, features = ["rt", "compat"] } -reqwest = { workspace = true, features = ["multipart"] } serial_test = "3.0.0" async-tungstenite = { version = "0.25.0", default-features = false } @@ -119,9 +121,8 @@ sb_ai = { version = "0.1.0", path = "../sb_ai" } anyhow.workspace = true bytes.workspace = true hyper = { workspace = true, features = ["full"] } -http.workspace = true +http_v02.workspace = true log.workspace = true -reqwest.workspace = true serde = { workspace = true, features = ["derive"] } tokio.workspace = true url.workspace = true diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 86e5c0beb..13cd6f8ff 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" deno_core.workspace = true base = { version = "0.1.0", path = "../base" } +deno_manifest = { path = "../deno_manifest" } sb_graph = { version = "0.1.0", path = "../sb_graph" } @@ -25,8 +26,5 @@ clap = { version = "4.0.29", features = ["cargo", "string", "env"] } tracing-subscriber = { workspace = true, optional = true, features = ["env-filter", "tracing-log"] } env_logger = "0.10.0" -[build-dependencies] -dotenv-build = "0.1.1" - [features] tracing = ["dep:tracing-subscriber"] \ No newline at end of file diff --git a/crates/deno_manifest/Cargo.toml b/crates/deno_manifest/Cargo.toml new file mode 100644 index 000000000..959e6e71f --- /dev/null +++ b/crates/deno_manifest/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "deno_manifest" +version = "1.45.2" \ No newline at end of file diff --git a/crates/http_utils/Cargo.toml b/crates/http_utils/Cargo.toml index d5716857b..c45ec5312 100644 --- a/crates/http_utils/Cargo.toml +++ b/crates/http_utils/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] tokio.workspace = true tokio-util = { workspace = true, features = ["rt"] } -hyper = { workspace = true, features = ["full"] } +hyper_v014 = { workspace = true, features = ["full"] } +http_v02.workspace = true futures-util.workspace = true -bytes.workspace = true -http.workspace = true \ No newline at end of file +bytes.workspace = true \ No newline at end of file diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index ea8018e19..230991fae 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -13,69 +13,78 @@ license = "MIT" path = "lib.rs" [dependencies] -aead-gcm-stream = "0.1" -aes = "=0.8.3" -brotli = "3.3.4" -bytes.workspace = true -cbc = { version = "=0.1.2", features = ["alloc"] } -const-oid = "0.9.5" -data-encoding = "2.3.3" deno_core.workspace = true deno_fetch.workspace = true deno_fs.workspace = true deno_media_type.workspace = true deno_net.workspace = true +deno_config = { workspace = true, default-features = false, features = ["package_json"] } deno_whoami = "0.1.0" + +libc.workspace = true +http.workspace = true +libz-sys.workspace = true +tokio.workspace = true +async-trait.workspace = true +once_cell.workspace = true +nix.workspace = true +num-bigint.workspace = true +bytes.workspace = true +faster-hex.workspace = true +indexmap.workspace = true +regex.workspace = true +reqwest.workspace = true +ring.workspace = true +rsa.workspace = true +url.workspace = true +aes.workspace = true +brotli.workspace = true +cbc.workspace = true +ecb.workspace = true +data-encoding.workspace = true +elliptic-curve.workspace = true +hkdf.workspace = true +http_v02.workspace = true +lazy-regex.workspace = true +p224.workspace = true +p256.workspace = true +p384.workspace = true +sha1.workspace = true +sha2.workspace = true +rand.workspace = true +signature.workspace = true +spki.workspace = true +winapi.workspace = true +h2.workspace = true +thiserror.workspace = true + +aead-gcm-stream = "0.1" +const-oid = "0.9.5" digest = { version = "0.10.5", features = ["core-api", "std"] } dsa = "0.6.1" -ecb = "=0.1.2" -elliptic-curve = { version = "0.13.4", features = ["alloc", "arithmetic", "ecdh", "std", "pem"] } errno = "0.2.8" -faster-hex.workspace = true -h2 = { version = "0.3.26", features = ["unstable"] } -hkdf = "0.12.3" -http_v02 = { package = "http", version = "0.2.9" } idna = "0.3.0" -indexmap.workspace = true k256 = "0.13.1" -lazy-regex = "3" -libc.workspace = true -libz-sys = { version = "1.1", default-features = false } -md-5 = "0.10.5" +md-5 = { version = "0.10.5", features = ["oid"] } md4 = "0.10.2" -nix = "=0.26.2" -num-bigint = { version = "0.4", features = ["rand"] } num-bigint-dig = "0.8.2" num-integer = "0.1.45" num-traits = "0.2.14" -once_cell.workspace = true -p224 = { version = "0.13.0", features = ["ecdh"] } -p256 = { version = "0.13.2", features = ["ecdh"] } -p384 = { version = "0.13.0", features = ["ecdh"] } path-clean = "=0.1.0" pbkdf2 = "0.12.1" pin-project-lite = "0.2.13" -rand = "=0.8.5" -regex.workspace = true -reqwest.workspace = true -ring.workspace = true -ripemd = "0.1.3" -rsa.workspace = true +ripemd = { version = "0.1.3", features = ["oid"] } scrypt = "0.11.0" sec1 = "0.7" serde = "1.0.149" -sha-1 = "0.10.0" -sha2 = { workspace = true, features = ["oid"] } -signature = "2.1" +sha3 = { version = "0.10.8", features = ["oid"] } +blake2 = "0.10.6" +sm3 = "0.4.2" simd-json = "0.13.4" -spki = "0.7.2" -tokio.workspace = true -typenum = "1.15.0" -url = { version = "< 2.5.0", features = ["serde", "expose_internals"] } -winapi = "=0.3.9" x25519-dalek = "2.0.0" x509-parser = "0.15.0" +ipnetwork = "0.20.0" [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.48.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem"] } -winapi = { version = "=0.3.9", features = ["consoleapi"] } +windows-sys.workspace = true +winapi = { workspace = true, features = ["consoleapi"] } diff --git a/crates/npm/Cargo.toml b/crates/npm/Cargo.toml index 25bfb8148..9cc0b722b 100644 --- a/crates/npm/Cargo.toml +++ b/crates/npm/Cargo.toml @@ -18,6 +18,7 @@ deno_fs.workspace = true deno_semver.workspace = true deno_lockfile.workspace = true deno_graph.workspace = true +deno_config = { workspace = true, features = ["workspace"] } sb_core = { version = "0.1.0", path = "../sb_core" } sb_node = { version = "0.1.0", path = "../node" } @@ -28,12 +29,14 @@ tar.workspace = true flate2.workspace = true ring.workspace = true serde.workspace = true +reqwest.workspace = true percent-encoding.workspace = true base64.workspace = true thiserror.workspace = true log.workspace = true +faster-hex.workspace = true indexmap.workspace = true +base32.workspace = true -base32 = "=0.4.0" hex = "0.4" bincode = "=1.3.3" \ No newline at end of file diff --git a/crates/npm_cache/Cargo.toml b/crates/npm_cache/Cargo.toml new file mode 100644 index 000000000..0bef621ae --- /dev/null +++ b/crates/npm_cache/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "npm_cache" +version = "0.1.0" +authors = ["Supabase "] +edition = "2021" +resolver = "2" +description = "We'll take care of this later" +license = "MIT" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core.workspace = true +deno_ast.workspace = true +deno_graph.workspace = true +deno_cache_dir.workspace = true +deno_web.workspace = true + +sb_core = { version = "0.1.0", path = "../sb_core" } +sb_npm = { version = "0.1.0", path = "../npm" } + +tokio.workspace = true +log.workspace = true +anyhow.workspace = true \ No newline at end of file diff --git a/crates/sb_core/Cargo.toml b/crates/sb_core/Cargo.toml index 60d1171cb..c4b57848e 100644 --- a/crates/sb_core/Cargo.toml +++ b/crates/sb_core/Cargo.toml @@ -26,9 +26,11 @@ deno_graph.workspace = true deno_io.workspace = true deno_cache_dir.workspace = true deno_webstorage.workspace = true +deno_npm.workspace = true base_rt = { version = "0.1.0", path = "../base_rt" } base_mem_check = { version = "0.1.0", path = "../base_mem_check" } +deno_manifest = { path = "../deno_manifest" } sb_node = { version = "0.1.0", path = "../node" } @@ -36,11 +38,13 @@ libc.workspace = true anyhow.workspace = true tokio.workspace = true hyper.workspace = true +hyper_v014.workspace = true +async-trait.workspace = true serde.workspace = true bytes.workspace = true -thiserror.workspace = true fs3.workspace = true log.workspace = true +rand.workspace = true tokio-util.workspace = true ring.workspace = true once_cell.workspace = true @@ -54,6 +58,7 @@ enum-as-inner.workspace = true httparse.workspace = true http.workspace = true faster-hex.workspace = true +thiserror.workspace = true tracing.workspace = true fqdn = "0.3.4" diff --git a/crates/sb_fs/Cargo.toml b/crates/sb_fs/Cargo.toml index 20d22245e..c78788733 100644 --- a/crates/sb_fs/Cargo.toml +++ b/crates/sb_fs/Cargo.toml @@ -24,6 +24,7 @@ sb_eszip_shared = { version = "0.1.0", path = "../sb_eszip_shared" } anyhow.workspace = true import_map.workspace = true +indexmap.workspace = true log.workspace = true serde.workspace = true tokio.workspace = true diff --git a/crates/sb_graph/Cargo.toml b/crates/sb_graph/Cargo.toml index 35d174a69..2cb3f5c28 100644 --- a/crates/sb_graph/Cargo.toml +++ b/crates/sb_graph/Cargo.toml @@ -14,9 +14,11 @@ deno_core.workspace = true deno_ast.workspace = true deno_fs.workspace = true deno_npm.workspace = true +deno_graph.workspace = true deno_semver.workspace = true deno_web.workspace = true deno_lockfile.workspace = true +deno_cache_dir.workspace = true deno_config.workspace = true sb_core = { version = "0.1.0", path = "../sb_core" } @@ -24,8 +26,12 @@ sb_node = { version = "0.1.0", path = "../node" } sb_npm = { version = "0.1.0", path = "../npm" } sb_fs = { version = "0.1.0", path = "../sb_fs" } sb_eszip_shared = { version = "0.1.0", path = "../sb_eszip_shared" } + +npm_cache = { version = "0.1.0", path = "../npm_cache" } + anyhow.workspace = true import_map.workspace = true +async-trait.workspace = true log.workspace = true serde.workspace = true tokio.workspace = true @@ -38,8 +44,10 @@ sha2.workspace = true scopeguard.workspace = true thiserror.workspace = true rkyv = { workspace = true, features = ["validation"] } -hashlink = { version = "0.8" } -pathdiff = { version = "0.2" } + +hashlink = "0.8" +pathdiff = "0.2" +dashmap = "5.5.3" [dev-dependencies] tempfile.workspace = true diff --git a/crates/sb_module_loader/Cargo.toml b/crates/sb_module_loader/Cargo.toml index 6b95a2c0a..b5d025732 100644 --- a/crates/sb_module_loader/Cargo.toml +++ b/crates/sb_module_loader/Cargo.toml @@ -16,6 +16,7 @@ deno_ast.workspace = true deno_fs.workspace = true deno_npm.workspace = true deno_tls.workspace = true +deno_config.workspace = true sb_core = { version = "0.1.0", path = "../sb_core" } sb_node = { version = "0.1.0", path = "../node" } diff --git a/crates/sb_workers/Cargo.toml b/crates/sb_workers/Cargo.toml index 2e98039f3..99daa5986 100644 --- a/crates/sb_workers/Cargo.toml +++ b/crates/sb_workers/Cargo.toml @@ -24,7 +24,7 @@ sb_graph = { version = "0.1.0", path = "../sb_graph" } anyhow.workspace = true uuid.workspace = true tokio.workspace = true -hyper.workspace = true +hyper_v014.workspace = true serde.workspace = true bytes.workspace = true log.workspace = true From 481ed516a94fb45ca2c9b145937583f4a5aee8c3 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 16 Jul 2024 13:40:59 +0900 Subject: [PATCH 06/20] stamp: lkg for all tests --- crates/base/build.rs | 10 +- crates/base/src/deno_runtime.rs | 6 +- crates/base/src/inspector_server.rs | 161 ++-- crates/base/src/macros/test_macros.rs | 8 +- crates/base/src/rt_worker/worker_ctx.rs | 14 +- crates/base/src/rt_worker/worker_pool.rs | 4 +- crates/base/src/server.rs | 8 +- crates/base/src/timeout.rs | 18 +- .../base/src/utils/integration_test_helper.rs | 4 +- crates/base/tests/integration_tests.rs | 8 +- crates/cli/.env.build | 1 - crates/cli/build.rs | 9 +- crates/cli/src/env.rs | 2 +- crates/cli/src/flags.rs | 2 +- crates/deno_manifest/src/lib.rs | 3 + crates/http_utils/src/utils.rs | 6 +- crates/node/lib.rs | 73 +- crates/node/ops/fs.rs | 2 +- crates/node/ops/os/mod.rs | 78 +- crates/node/polyfills/process.ts | 17 +- crates/npm/byonm.rs | 226 +++--- crates/npm/cache_dir.rs | 64 +- crates/npm/common.rs | 36 +- crates/npm/managed/{cache.rs => cache/mod.rs} | 151 ++-- crates/npm/managed/cache/registry_info.rs | 243 ++++++ crates/npm/managed/cache/tarball.rs | 230 ++++++ crates/npm/managed/cache/tarball_extract.rs | 204 +++++ crates/npm/managed/installer.rs | 120 --- crates/npm/managed/mod.rs | 396 ++++----- crates/npm/managed/package_json.rs | 147 ++-- crates/npm/managed/registry.rs | 255 ++---- crates/npm/managed/resolution.rs | 204 ++--- crates/npm/managed/resolvers/common.rs | 95 +-- crates/npm/managed/resolvers/global.rs | 131 ++- crates/npm/managed/resolvers/local.rs | 239 +++--- crates/npm/managed/resolvers/mod.rs | 21 +- .../cache => npm_cache}/fetch_cacher.rs | 172 ++-- crates/npm_cache/file_fetcher.rs | 579 +++++++++++++ crates/npm_cache/lib.rs | 5 + crates/sb_core/cache/cache_db.rs | 283 ++++--- crates/sb_core/cache/common.rs | 13 +- crates/sb_core/cache/disk_cache.rs | 2 +- crates/sb_core/cache/emit.rs | 33 +- crates/sb_core/cache/incremental.rs | 76 +- crates/sb_core/cache/mod.rs | 4 - crates/sb_core/cache/module_info.rs | 123 +-- crates/sb_core/cache/node.rs | 32 +- crates/sb_core/cert.rs | 46 +- crates/sb_core/emit.rs | 36 +- crates/sb_core/file_fetcher.rs | 664 --------------- crates/sb_core/http.rs | 4 +- crates/sb_core/js/bootstrap.js | 23 +- crates/sb_core/js/http.js | 14 +- crates/sb_core/lib.rs | 3 +- .../cjs_code_anaylzer.rs => sb_core/node.rs} | 90 ++- crates/sb_core/npm.rs | 39 + crates/sb_core/permissions.rs | 8 +- crates/sb_core/transpiler.rs | 11 +- crates/sb_core/util/errors.rs | 62 +- crates/sb_core/util/fs.rs | 96 +++ crates/sb_core/util/http_util.rs | 317 ++++++-- crates/sb_core/util/path.rs | 25 +- crates/sb_core/util/sync/async_flag.rs | 20 + crates/sb_core/util/sync/mod.rs | 14 + .../util/sync/sync_read_async_write_lock.rs | 62 ++ .../util/{sync.rs => sync/task_queue.rs} | 20 +- crates/sb_core/util/sync/value_creator.rs | 206 +++++ crates/sb_fs/file_system.rs | 39 + crates/sb_fs/lib.rs | 50 +- crates/sb_fs/static_fs.rs | 35 + crates/sb_fs/virtual_fs.rs | 137 ++-- crates/sb_graph/emitter.rs | 271 +++---- crates/sb_graph/graph_resolver.rs | 316 -------- crates/sb_graph/graph_util.rs | 544 +++++++++---- crates/sb_graph/import_map.rs | 2 +- crates/sb_graph/resolver.rs | 758 ++++++++++++++++++ crates/sb_module_loader/lib.rs | 4 +- crates/sb_module_loader/metadata.rs | 60 -- .../node/cli_node_resolver.rs | 101 --- crates/sb_module_loader/node/mod.rs | 3 - .../node/node_module_loader.rs | 130 --- crates/sb_module_loader/standalone/mod.rs | 79 +- .../standalone/standalone_module_loader.rs | 211 +++-- crates/sb_workers/context.rs | 4 +- crates/sb_workers/lib.rs | 10 +- 85 files changed, 5361 insertions(+), 3671 deletions(-) delete mode 100644 crates/cli/.env.build create mode 100644 crates/deno_manifest/src/lib.rs rename crates/npm/managed/{cache.rs => cache/mod.rs} (66%) create mode 100644 crates/npm/managed/cache/registry_info.rs create mode 100644 crates/npm/managed/cache/tarball.rs create mode 100644 crates/npm/managed/cache/tarball_extract.rs delete mode 100644 crates/npm/managed/installer.rs rename crates/{sb_core/cache => npm_cache}/fetch_cacher.rs (54%) create mode 100644 crates/npm_cache/file_fetcher.rs create mode 100644 crates/npm_cache/lib.rs delete mode 100644 crates/sb_core/file_fetcher.rs rename crates/{sb_module_loader/node/cjs_code_anaylzer.rs => sb_core/node.rs} (54%) create mode 100644 crates/sb_core/npm.rs create mode 100644 crates/sb_core/util/sync/async_flag.rs create mode 100644 crates/sb_core/util/sync/mod.rs create mode 100644 crates/sb_core/util/sync/sync_read_async_write_lock.rs rename crates/sb_core/util/{sync.rs => sync/task_queue.rs} (88%) create mode 100644 crates/sb_core/util/sync/value_creator.rs delete mode 100644 crates/sb_graph/graph_resolver.rs create mode 100644 crates/sb_graph/resolver.rs delete mode 100644 crates/sb_module_loader/node/cli_node_resolver.rs delete mode 100644 crates/sb_module_loader/node/mod.rs delete mode 100644 crates/sb_module_loader/node/node_module_loader.rs diff --git a/crates/base/build.rs b/crates/base/build.rs index 59a47ef22..f8d519b94 100644 --- a/crates/base/build.rs +++ b/crates/base/build.rs @@ -159,24 +159,24 @@ mod supabase_startup_snapshot { unreachable!("snapshotting!") } - fn check_read(&self, _path: &Path) -> Result<(), AnyError> { + fn check_read(&mut self, _path: &Path) -> Result<(), AnyError> { unreachable!("snapshotting!") } fn check_read_with_api_name( - &self, + &mut self, _path: &Path, _api_name: Option<&str>, ) -> Result<(), AnyError> { unreachable!("snapshotting!") } - fn check_sys(&self, _kind: &str, _api_name: &str) -> Result<(), AnyError> { + fn check_sys(&mut self, _kind: &str, _api_name: &str) -> Result<(), AnyError> { unreachable!("snapshotting!") } fn check_write_with_api_name( - &self, + &mut self, _path: &Path, _api_name: Option<&str>, ) -> Result<(), AnyError> { @@ -224,7 +224,7 @@ mod supabase_startup_snapshot { sb_core_net::init_ops_and_esm(), sb_core_http::init_ops_and_esm(), sb_core_http_start::init_ops_and_esm(), - deno_node::init_ops_and_esm::(None, fs), + deno_node::init_ops_and_esm::(None, None, fs), sb_core_runtime::init_ops_and_esm(None), ]; diff --git a/crates/base/src/deno_runtime.rs b/crates/base/src/deno_runtime.rs index f85b9256c..4ccc23072 100644 --- a/crates/base/src/deno_runtime.rs +++ b/crates/base/src/deno_runtime.rs @@ -17,7 +17,6 @@ use deno_core::{ }; use deno_http::DefaultHttpPropertyExtractor; use deno_tls::deno_native_certs::load_native_certs; -use deno_tls::rustls; use deno_tls::rustls::RootCertStore; use deno_tls::RootCertStoreProvider; use futures_util::future::poll_fn; @@ -367,7 +366,7 @@ where let roots = load_native_certs().expect("could not load platform certs"); for root in roots { root_cert_store - .add(&rustls::Certificate(root.0)) + .add((&*root.0).into()) .expect("Failed to add platform cert to root cert store"); } } @@ -403,6 +402,7 @@ where .await?; let RuntimeProviders { + node_resolver, npm_resolver, vfs, module_loader, @@ -471,7 +471,7 @@ where sb_core_http_start::init_ops(), // NOTE(AndresP): Order is matters. Otherwise, it will lead to hard // errors such as SIGBUS depending on the platform. - deno_node::init_ops::(Some(npm_resolver), op_fs), + deno_node::init_ops::(Some(node_resolver), Some(npm_resolver), op_fs), sb_core_runtime::init_ops(Some(main_module_url.clone())), ]; diff --git a/crates/base/src/inspector_server.rs b/crates/base/src/inspector_server.rs index cd45b1ba8..12a56eca2 100644 --- a/crates/base/src/inspector_server.rs +++ b/crates/base/src/inspector_server.rs @@ -4,6 +4,7 @@ // `https://github.com/denoland/deno/blob/v1.37.2/runtime/inspector_server.rs`. // Alias for the future `!` type. +use bytes::Bytes; use core::convert::Infallible as Never; use deno_core::futures::channel::mpsc; use deno_core::futures::channel::mpsc::UnboundedReceiver; @@ -27,6 +28,13 @@ use enum_as_inner::EnumAsInner; use fastwebsockets::Frame; use fastwebsockets::OpCode; use fastwebsockets::WebSocket; +use http_body_util::combinators::BoxBody; +use http_body_util::BodyExt; +use hyper::rt::Executor; +use hyper_util::rt::TokioIo; +use hyper_util::server::conn; +use hyper_util::server::graceful::GracefulShutdown; +use log::error; use std::cell::RefCell; use std::collections::HashMap; use std::convert::Infallible; @@ -36,6 +44,7 @@ use std::process; use std::rc::Rc; use std::sync::Arc; use std::thread; +use tokio::net::TcpListener; use tokio::sync::watch; use uuid::Uuid; @@ -167,9 +176,9 @@ where } fn handle_ws_request( - req: http::Request, + req: http::Request, inspector_map_rc: Rc>>, -) -> http::Result> { +) -> http::Result>> { let (parts, body) = req.into_parts(); let req = http::Request::from_parts(parts, ()); @@ -182,7 +191,7 @@ fn handle_ws_request( if maybe_uuid.is_none() { return http::Response::builder() .status(http::StatusCode::BAD_REQUEST) - .body("Malformed inspector UUID".into()); + .body("Malformed inspector UUID".to_string().boxed()); } // run in a block to not hold borrow to `inspector_map` for too long @@ -193,7 +202,7 @@ fn handle_ws_request( if maybe_inspector_info.is_none() { return http::Response::builder() .status(http::StatusCode::NOT_FOUND) - .body("Invalid inspector UUID".into()); + .body("Invalid inspector UUID".to_string().boxed()); } let info = maybe_inspector_info.unwrap(); @@ -206,11 +215,11 @@ fn handle_ws_request( let mut req = http::Request::from_parts(parts, body); let (resp, fut) = match fastwebsockets::upgrade::upgrade(&mut req) { - Ok(e) => e, + Ok((resp, fut)) => (resp.map(BodyExt::boxed), fut), _ => { return http::Response::builder() .status(http::StatusCode::BAD_REQUEST) - .body("Not a valid Websocket Request".into()); + .body("Not a valid Websocket Request".to_string().boxed()); } }; @@ -245,7 +254,7 @@ fn handle_ws_request( fn handle_json_request( inspector_map: Rc>>, host: Option, -) -> http::Result> { +) -> http::Result>> { let data = inspector_map .borrow() .values() @@ -254,22 +263,22 @@ fn handle_json_request( http::Response::builder() .status(http::StatusCode::OK) .header(http::header::CONTENT_TYPE, "application/json") - .body(serde_json::to_string(&data).unwrap().into()) + .body(serde_json::to_string(&data).unwrap().boxed()) } fn handle_json_version_request( version_response: Value, -) -> http::Result> { +) -> http::Result>> { http::Response::builder() .status(http::StatusCode::OK) .header(http::header::CONTENT_TYPE, "application/json") - .body(serde_json::to_string(&version_response).unwrap().into()) + .body(serde_json::to_string(&version_response).unwrap().boxed()) } async fn server( host: SocketAddr, register_inspector_rx: UnboundedReceiver, - shutdown_server_rx: oneshot::Receiver<()>, + mut shutdown_server_rx: oneshot::Receiver<()>, name: &str, ) { let inspector_map_ = Rc::new(RefCell::new(HashMap::::new())); @@ -311,69 +320,85 @@ async fn server( "V8-Version": deno_core::v8_version(), }); - let make_svc = hyper::service::make_service_fn(|_| { - let inspector_map = Rc::clone(&inspector_map_); + let service_fn = hyper::service::service_fn({ + let inspector_map = inspector_map_.clone(); let json_version_response = json_version_response.clone(); - future::ok::<_, Infallible>(hyper::service::service_fn( - move |req: http::Request| { - future::ready({ - // If the host header can make a valid URL, use it - let host = req - .headers() - .get("host") - .and_then(|host| host.to_str().ok()) - .and_then(|host| Url::parse(&format!("http://{host}")).ok()) - .and_then(|url| match (url.host(), url.port()) { - (Some(host), Some(port)) => Some(format!("{host}:{port}")), - (Some(host), None) => Some(format!("{host}")), - _ => None, - }); - match (req.method(), req.uri().path()) { - (&http::Method::GET, path) if path.starts_with("/ws/") => { - handle_ws_request(req, Rc::clone(&inspector_map)) - } - (&http::Method::GET, "/json/version") => { - handle_json_version_request(json_version_response.clone()) - } - (&http::Method::GET, "/json") => { - handle_json_request(Rc::clone(&inspector_map), host) - } - (&http::Method::GET, "/json/list") => { - handle_json_request(Rc::clone(&inspector_map), host) - } - _ => http::Response::builder() - .status(http::StatusCode::NOT_FOUND) - .body("Not Found".into()), - } - }) - }, - )) + move |req| { + // If the host header can make a valid URL, use it + let host = req + .headers() + .get("host") + .and_then(|host| host.to_str().ok()) + .and_then(|host| Url::parse(&format!("http://{host}")).ok()) + .and_then(|url| match (url.host(), url.port()) { + (Some(host), Some(port)) => Some(format!("{host}:{port}")), + (Some(host), None) => Some(format!("{host}")), + _ => None, + }); + + let resp = match (req.method(), req.uri().path()) { + (&http::Method::GET, path) if path.starts_with("/ws/") => { + handle_ws_request(req, inspector_map.clone()) + } + (&http::Method::GET, "/json/version") => { + handle_json_version_request(json_version_response.clone()) + } + (&http::Method::GET, "/json") => handle_json_request(inspector_map.clone(), host), + (&http::Method::GET, "/json/list") => { + handle_json_request(inspector_map.clone(), host) + } + _ => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .body(String::from("Not Found").boxed()), + }; + + future::ready(resp) + } }); - // Create the server manually so it can use the Local Executor - let mut server_handler = pin!(hyper::server::Builder::new( - hyper::server::conn::AddrIncoming::bind(&host).unwrap_or_else(|e| { - eprintln!("Cannot start inspector server: {e}."); - process::exit(1); - }), - hyper::server::conn::Http::new().with_executor(LocalExecutor), - ) - .serve(make_svc) - .with_graceful_shutdown(async { - shutdown_server_rx.await.ok(); - }) - .unwrap_or_else(|err| { - eprintln!("Cannot start inspector server: {err}."); + let listener = TcpListener::bind(host).await.unwrap_or_else(|e| { + eprintln!("Cannot start inspector server: {e}."); process::exit(1); - }) - .fuse()); + }); + + let graceful = GracefulShutdown::new(); + let accept_fut = async { + let executor = LocalExecutor; + let conn_builder = conn::auto::Builder::new(executor.clone()); + + loop { + let (tcp, _) = match listener.accept().await { + Ok(conn) => conn, + Err(err) => { + error!("accept error: {err}"); + continue; + } + }; + + let io = TokioIo::new(tcp); + let conn = conn_builder.serve_connection(io, service_fn.clone()); + let conn = graceful.watch(conn.into_owned()); - select! { - _ = register_inspector_handler => {}, - _ = deregister_inspector_handler => unreachable!(), - _ = server_handler => {}, + executor.execute(async move { + let _ = conn.await; + }); + } } + .fuse(); + + { + let mut accept_fut = pin!(accept_fut); + + select! { + _ = register_inspector_handler => {}, + _ = deregister_inspector_handler => unreachable!(), + _ = accept_fut => {}, + _ = shutdown_server_rx => {} + } + } + + graceful.shutdown().await; } /// The pump future takes care of forwarding messages between the websocket @@ -389,7 +414,7 @@ async fn server( /// 'futures' crate, therefore they can't participate in Tokio's cooperative /// task yielding. async fn pump_websocket_messages( - mut websocket: WebSocket, + mut websocket: WebSocket>, inbound_tx: UnboundedSender, mut outbound_rx: UnboundedReceiver, mut deregistered_watch_rx: watch::Receiver, diff --git a/crates/base/src/macros/test_macros.rs b/crates/base/src/macros/test_macros.rs index 971791236..1a8459bd4 100644 --- a/crates/base/src/macros/test_macros.rs +++ b/crates/base/src/macros/test_macros.rs @@ -38,7 +38,7 @@ macro_rules! integration_test_with_server_flag { let (tx, mut rx) = tokio::sync::mpsc::channel::(1); - let req_builder: Option = $req_builder; + let req_builder: Option = $req_builder; let tls: Option = $tls; let schema = if tls.is_some() { "https" } else { "http" }; let signal = tokio::spawn(async move { @@ -135,7 +135,7 @@ macro_rules! integration_test_with_server_flag { { return Some(resp); } else { - let resp = reqwest::get(format!("{}://localhost:{}/{}", $schema, $port, $url)).await; + let resp = reqwest_v011::get(format!("{}://localhost:{}/{}", $schema, $port, $url)).await; return Some(resp); } }; @@ -144,7 +144,7 @@ macro_rules! integration_test_with_server_flag { if let Some(req_factory) = $req_builder { return Some(req_factory.send().await); } else { - let resp = reqwest::get(format!("{}://localhost:{}/{}", $schema, $port, $url)).await; + let resp = reqwest_v011::get(format!("{}://localhost:{}/{}", $schema, $port, $url)).await; return Some(resp); } }; @@ -180,7 +180,7 @@ macro_rules! integration_test { pub mod __private { use std::future::Future; - use reqwest::{Error, RequestBuilder, Response}; + use reqwest_v011::{Error, RequestBuilder, Response}; use sb_core::SharedMetricSource; use tokio::sync::mpsc; diff --git a/crates/base/src/rt_worker/worker_ctx.rs b/crates/base/src/rt_worker/worker_ctx.rs index 0713d389b..7aa0934c6 100644 --- a/crates/base/src/rt_worker/worker_ctx.rs +++ b/crates/base/src/rt_worker/worker_ctx.rs @@ -14,12 +14,12 @@ use event_worker::events::{ BootEvent, ShutdownEvent, WorkerEventWithMetadata, WorkerEvents, WorkerMemoryUsed, }; use futures_util::pin_mut; -use http::StatusCode; +use http_v02::StatusCode; use http_utils::io::Upgraded2; use http_utils::utils::{emit_status_code, get_upgrade_type}; -use hyper::client::conn::http1; -use hyper::upgrade::OnUpgrade; -use hyper::{Body, Request, Response}; +use hyper_v014::client::conn::http1; +use hyper_v014::upgrade::OnUpgrade; +use hyper_v014::{Body, Request, Response}; use log::{debug, error}; use sb_core::{MetricSource, SharedMetricSource}; use sb_graph::{DecoratorType, EszipPayloadKind}; @@ -167,7 +167,7 @@ async fn handle_request( let res = tokio::select! { resp = request_sender.send_request(req) => resp, _ = maybe_cancel_fut => { - Ok(emit_status_code(http::StatusCode::GATEWAY_TIMEOUT, None, false)) + Ok(emit_status_code(http_v02::StatusCode::GATEWAY_TIMEOUT, None, false)) } }; @@ -191,7 +191,7 @@ async fn handle_request( if let Some(timeout_ms) = maybe_request_idle_timeout { let headers = res.headers(); - let is_streamed_response = !headers.contains_key(http::header::CONTENT_LENGTH); + let is_streamed_response = !headers.contains_key(http_v02::header::CONTENT_LENGTH); if is_streamed_response { let duration = Duration::from_millis(timeout_ms); @@ -680,7 +680,7 @@ pub async fn send_user_worker_request( exit: WorkerExit, conn_token: Option, ) -> Result, Error> { - let (res_tx, res_rx) = oneshot::channel::, hyper::Error>>(); + let (res_tx, res_rx) = oneshot::channel::, hyper_v014::Error>>(); let msg = WorkerRequestMsg { req, res_tx, diff --git a/crates/base/src/rt_worker/worker_pool.rs b/crates/base/src/rt_worker/worker_pool.rs index 8b1e327f5..216795d62 100644 --- a/crates/base/src/rt_worker/worker_pool.rs +++ b/crates/base/src/rt_worker/worker_pool.rs @@ -4,8 +4,8 @@ use crate::server::ServerFlags; use anyhow::{anyhow, bail, Context, Error}; use enum_as_inner::EnumAsInner; use event_worker::events::WorkerEventWithMetadata; -use http::Request; -use hyper::Body; +use http_v02::Request; +use hyper_v014::Body; use log::error; use sb_core::util::sync::AtomicFlag; use sb_core::SharedMetricSource; diff --git a/crates/base/src/server.rs b/crates/base/src/server.rs index 85ef4008b..00da7cdb1 100644 --- a/crates/base/src/server.rs +++ b/crates/base/src/server.rs @@ -9,7 +9,7 @@ use deno_config::JsxImportSourceConfig; use event_worker::events::WorkerEventWithMetadata; use futures_util::future::{poll_fn, BoxFuture}; use futures_util::{FutureExt, Stream}; -use hyper::{server::conn::Http, service::Service, Body, Request, Response}; +use hyper_v014::{server::conn::Http, service::Service, Body, Request, Response}; use log::{debug, error, info, trace, warn}; use rustls_pemfile::read_one_from_slice; use rustls_pemfile::Item; @@ -48,7 +48,7 @@ mod signal { } pub enum ServerEvent { - ConnectionError(hyper::Error), + ConnectionError(hyper_v014::Error), #[cfg(debug_assertions)] Draining, } @@ -160,7 +160,7 @@ impl Service> for WorkerService { let metric_src = self.metric_src.clone(); let worker_req_tx = self.worker_req_tx.clone(); let fut = async move { - let (res_tx, res_rx) = oneshot::channel::, hyper::Error>>(); + let (res_tx, res_rx) = oneshot::channel::, hyper_v014::Error>>(); let req_uri = req.uri().clone(); let msg = WorkerRequestMsg { @@ -217,7 +217,7 @@ impl Service> for WorkerService { // FIXME: add an error body Response::builder() - .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .status(http_v02::StatusCode::INTERNAL_SERVER_ERROR) .body(Body::wrap_stream(CancelOnDrop { inner: Body::empty(), cancel: Some(cancel), diff --git a/crates/base/src/timeout.rs b/crates/base/src/timeout.rs index 96a075735..670b8d6ff 100644 --- a/crates/base/src/timeout.rs +++ b/crates/base/src/timeout.rs @@ -165,11 +165,11 @@ impl Service { } } -impl hyper::service::Service for Service +impl hyper_v014::service::Service for Service where - S: hyper::service::Service>, + S: hyper_v014::service::Service>, { - type Response = hyper::Response>; + type Response = hyper_v014::Response>; type Error = S::Error; type Future = ServiceFuture; @@ -202,9 +202,9 @@ impl ServiceFuture { impl Future for ServiceFuture where - F: Future, Error>>, + F: Future, Error>>, { - type Output = Result>, Error>; + type Output = Result>, Error>; fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { let this = self.project(); @@ -228,9 +228,9 @@ impl Body { } } -impl hyper::body::HttpBody for Body +impl hyper_v014::body::HttpBody for Body where - B: hyper::body::HttpBody, + B: hyper_v014::body::HttpBody, { type Data = B::Data; type Error = B::Error; @@ -257,7 +257,7 @@ where fn poll_trailers( self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, - ) -> Poll, Self::Error>> { + ) -> Poll, Self::Error>> { self.project().inner.poll_trailers(cx) } @@ -275,7 +275,7 @@ where } } - fn size_hint(&self) -> hyper::body::SizeHint { + fn size_hint(&self) -> hyper_v014::body::SizeHint { self.inner.size_hint() } } diff --git a/crates/base/src/utils/integration_test_helper.rs b/crates/base/src/utils/integration_test_helper.rs index a668dfbb2..423b61848 100644 --- a/crates/base/src/utils/integration_test_helper.rs +++ b/crates/base/src/utils/integration_test_helper.rs @@ -18,8 +18,8 @@ use base::{ server::ServerFlags, }; use futures_util::{future::BoxFuture, Future, FutureExt}; -use http::{Request, Response}; -use hyper::Body; +use http_v02::{Request, Response}; +use hyper_v014::Body; use pin_project::pin_project; use sb_workers::context::{ diff --git a/crates/base/tests/integration_tests.rs b/crates/base/tests/integration_tests.rs index 1f9b7b637..ead528c20 100644 --- a/crates/base/tests/integration_tests.rs +++ b/crates/base/tests/integration_tests.rs @@ -1,6 +1,10 @@ #[path = "../src/utils/integration_test_helper.rs"] mod integration_test_helper; +use http_v02 as http; +use hyper_v014 as hyper; +use reqwest_v011 as reqwest; + use std::{ borrow::Cow, collections::HashMap, @@ -210,7 +214,7 @@ async fn test_not_trigger_pku_sigsegv_due_to_jit_compilation_non_cli() { .await .unwrap(); - let (res_tx, res_rx) = oneshot::channel::, hyper::Error>>(); + let (res_tx, res_rx) = oneshot::channel::, hyper_v014::Error>>(); let req = Request::builder() .uri("/slow_resp") @@ -1224,7 +1228,7 @@ async fn test_oak_file_upload( mime: Option<&str>, resp_callback: F, ) where - F: FnOnce(Result) -> R, + F: FnOnce(Result) -> R, R: Future, { let client = Client::builder().build().unwrap(); diff --git a/crates/cli/.env.build b/crates/cli/.env.build deleted file mode 100644 index a9c83d8a1..000000000 --- a/crates/cli/.env.build +++ /dev/null @@ -1 +0,0 @@ -DENO_VERSION="1.43.0" \ No newline at end of file diff --git a/crates/cli/build.rs b/crates/cli/build.rs index 1801c9643..fc091ae4b 100644 --- a/crates/cli/build.rs +++ b/crates/cli/build.rs @@ -1,13 +1,6 @@ -use std::{env, path::Path}; +use std::env; fn main() { println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap()); println!("cargo:rustc-env=PROFILE={}", env::var("PROFILE").unwrap()); - - dotenv_build::output(dotenv_build::Config { - filename: Path::new(".env.build"), - recursive_search: false, - fail_if_missing_dotenv: true, - }) - .unwrap(); } diff --git a/crates/cli/src/env.rs b/crates/cli/src/env.rs index 5fc31faad..d80332e9e 100644 --- a/crates/cli/src/env.rs +++ b/crates/cli/src/env.rs @@ -17,7 +17,7 @@ pub(super) fn resolve_deno_runtime_env() { }) }; - deno_runtime::MAYBE_DENO_VERSION.get_or_init(|| env!("DENO_VERSION").to_string()); + deno_runtime::MAYBE_DENO_VERSION.get_or_init(|| deno_manifest::version().to_string()); resolve_boolish_env( "DENO_NO_DEPRECATION_WARNINGS", diff --git a/crates/cli/src/flags.rs b/crates/cli/src/flags.rs index 2770d940d..2a4a5e0b6 100644 --- a/crates/cli/src/flags.rs +++ b/crates/cli/src/flags.rs @@ -12,7 +12,7 @@ pub(super) fn get_cli() -> Command { .version(format!( "{}\ndeno {} ({}, {})", crate_version!(), - env!("DENO_VERSION"), + deno_manifest::version(), env!("PROFILE"), env!("TARGET") )) diff --git a/crates/deno_manifest/src/lib.rs b/crates/deno_manifest/src/lib.rs new file mode 100644 index 000000000..99cfdd50b --- /dev/null +++ b/crates/deno_manifest/src/lib.rs @@ -0,0 +1,3 @@ +pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION") +} diff --git a/crates/http_utils/src/utils.rs b/crates/http_utils/src/utils.rs index fb22b2596..2555d053d 100644 --- a/crates/http_utils/src/utils.rs +++ b/crates/http_utils/src/utils.rs @@ -1,5 +1,5 @@ -use http::{header, response, HeaderMap, HeaderValue, Response, StatusCode}; -use hyper::Body; +use http_v02::{header, response, HeaderMap, HeaderValue, Response, StatusCode}; +use hyper_v014::body::Body; pub fn get_upgrade_type(headers: &HeaderMap) -> Option { let connection_header_exists = headers @@ -38,7 +38,7 @@ pub fn emit_status_code( builder.body(body) } else { builder - .header(http::header::CONTENT_LENGTH, 0) + .header(http_v02::header::CONTENT_LENGTH, 0) .body(Body::empty()) } .unwrap() diff --git a/crates/node/lib.rs b/crates/node/lib.rs index a13591ccd..0254964fc 100644 --- a/crates/node/lib.rs +++ b/crates/node/lib.rs @@ -95,34 +95,34 @@ impl NodePermissions for AllowAllNodePermissions { } } -impl NodePermissions for deno_permissions::PermissionsContainer { - #[inline(always)] - fn check_net_url(&mut self, url: &Url, api_name: &str) -> Result<(), AnyError> { - deno_permissions::PermissionsContainer::check_net_url(self, url, api_name) - } - - #[inline(always)] - fn check_read_with_api_name( - &mut self, - path: &Path, - api_name: Option<&str>, - ) -> Result<(), AnyError> { - deno_permissions::PermissionsContainer::check_read_with_api_name(self, path, api_name) - } - - #[inline(always)] - fn check_write_with_api_name( - &mut self, - path: &Path, - api_name: Option<&str>, - ) -> Result<(), AnyError> { - deno_permissions::PermissionsContainer::check_write_with_api_name(self, path, api_name) - } - - fn check_sys(&mut self, kind: &str, api_name: &str) -> Result<(), AnyError> { - deno_permissions::PermissionsContainer::check_sys(self, kind, api_name) - } -} +// impl NodePermissions for deno_permissions::PermissionsContainer { +// #[inline(always)] +// fn check_net_url(&mut self, url: &Url, api_name: &str) -> Result<(), AnyError> { +// deno_permissions::PermissionsContainer::check_net_url(self, url, api_name) +// } + +// #[inline(always)] +// fn check_read_with_api_name( +// &mut self, +// path: &Path, +// api_name: Option<&str>, +// ) -> Result<(), AnyError> { +// deno_permissions::PermissionsContainer::check_read_with_api_name(self, path, api_name) +// } + +// #[inline(always)] +// fn check_write_with_api_name( +// &mut self, +// path: &Path, +// api_name: Option<&str>, +// ) -> Result<(), AnyError> { +// deno_permissions::PermissionsContainer::check_write_with_api_name(self, path, api_name) +// } + +// fn check_sys(&mut self, kind: &str, api_name: &str) -> Result<(), AnyError> { +// deno_permissions::PermissionsContainer::check_sys(self, kind, api_name) +// } +// } #[allow(clippy::disallowed_types)] pub type NpmResolverRc = deno_fs::sync::MaybeArc; @@ -288,6 +288,10 @@ deno_core::extension!(deno_node, ops::fs::op_node_fs_exists_sync

, ops::fs::op_node_cp_sync

, ops::fs::op_node_cp

, + ops::fs::op_node_lchown_sync, + ops::fs::op_node_lchown, + ops::fs::op_node_lutimes_sync, + ops::fs::op_node_lutimes, ops::fs::op_node_statfs, ops::winerror::op_node_sys_to_uv_error, ops::v8::op_v8_cached_data_version_tag, @@ -327,11 +331,12 @@ deno_core::extension!(deno_node, ops::http2::op_http2_accept, ops::http2::op_http2_listen, ops::http2::op_http2_send_response, - ops::os::op_node_os_get_priority

, - ops::os::op_node_os_set_priority

, - ops::os::op_node_os_username

, - ops::os::op_geteuid

, - ops::os::op_cpus

, + // ops::os::op_node_os_get_priority

, + // ops::os::op_node_os_set_priority

, + // ops::os::op_node_os_username

, + ops::os::op_geteuid, + ops::os::op_getegid, + // ops::os::op_cpus

, op_node_build_os, op_node_is_promise_rejected, op_npm_process_state, @@ -390,8 +395,10 @@ deno_core::extension!(deno_node, "_fs/_fs_fsync.ts", "_fs/_fs_ftruncate.ts", "_fs/_fs_futimes.ts", + "_fs/_fs_lchown.ts", "_fs/_fs_link.ts", "_fs/_fs_lstat.ts", + "_fs/_fs_lutimes.ts", "_fs/_fs_mkdir.ts", "_fs/_fs_mkdtemp.ts", "_fs/_fs_open.ts", diff --git a/crates/node/ops/fs.rs b/crates/node/ops/fs.rs index ff7819da8..a56731637 100644 --- a/crates/node/ops/fs.rs +++ b/crates/node/ops/fs.rs @@ -260,7 +260,7 @@ pub async fn op_node_lutimes( // Ok(()) } -#[op2(fast)] +#[op2] pub fn op_node_lchown_sync( _state: &mut OpState, #[string] _path: String, diff --git a/crates/node/ops/os/mod.rs b/crates/node/ops/os/mod.rs index 1b1a3d83b..afb1268f0 100644 --- a/crates/node/ops/os/mod.rs +++ b/crates/node/ops/os/mod.rs @@ -54,41 +54,37 @@ where } #[op2(fast)] -pub fn op_geteuid

(state: &mut OpState) -> Result -where - P: NodePermissions + 'static, -{ - { - let permissions = state.borrow_mut::

(); - permissions.check_sys("uid", "node:os.geteuid()")?; - } - - #[cfg(windows)] - let euid = 0; - #[cfg(unix)] - // SAFETY: Call to libc geteuid. - let euid = unsafe { libc::geteuid() }; - - Ok(euid) +pub fn op_geteuid(_state: &mut OpState) -> Result { + // { + // let permissions = state.borrow_mut::

(); + // permissions.check_sys("uid", "node:os.geteuid()")?; + // } + + // #[cfg(windows)] + // let euid = 0; + // #[cfg(unix)] + // // SAFETY: Call to libc geteuid. + // let euid = unsafe { libc::geteuid() }; + + // Ok(euid) + Ok(0) } #[op2(fast)] -pub fn op_getegid

(state: &mut OpState) -> Result -where - P: NodePermissions + 'static, -{ - { - let permissions = state.borrow_mut::

(); - permissions.check_sys("getegid", "node:os.getegid()")?; - } - - #[cfg(windows)] - let egid = 0; - #[cfg(unix)] - // SAFETY: Call to libc getegid. - let egid = unsafe { libc::getegid() }; - - Ok(egid) +pub fn op_getegid(_state: &mut OpState) -> Result { + // { + // let permissions = state.borrow_mut::

(); + // permissions.check_sys("getegid", "node:os.getegid()")?; + // } + + // #[cfg(windows)] + // let egid = 0; + // #[cfg(unix)] + // // SAFETY: Call to libc getegid. + // let egid = unsafe { libc::getegid() }; + + // Ok(egid) + Ok(0) } #[op2] @@ -105,16 +101,12 @@ where cpus::cpu_info().ok_or_else(|| type_error("Failed to get cpu info")) } -#[op2] -#[string] -pub fn op_homedir

(state: &mut OpState) -> Result, AnyError> -where - P: NodePermissions + 'static, -{ - { - let permissions = state.borrow_mut::

(); - permissions.check_sys("homedir", "node:os.homedir()")?; - } +#[op2(fast)] +pub fn op_homedir(_state: &mut OpState) { + // { + // let permissions = state.borrow_mut::

(); + // permissions.check_sys("homedir", "node:os.homedir()")?; + // } - Ok(home::home_dir().map(|path| path.to_string_lossy().to_string())) + // Ok(home::home_dir().map(|path| path.to_string_lossy().to_string())) } diff --git a/crates/node/polyfills/process.ts b/crates/node/polyfills/process.ts index ec8671122..03c757864 100644 --- a/crates/node/polyfills/process.ts +++ b/crates/node/polyfills/process.ts @@ -50,7 +50,6 @@ import { } from "ext:deno_node/_next_tick.ts"; import { isWindows } from "ext:deno_node/_util/os.ts"; import * as io from "ext:deno_io/12_io.js"; -import * as denoOs from "ext:runtime/30_os.js"; export let argv0 = ""; @@ -83,13 +82,13 @@ let ProcessExitCode: undefined | null | string | number; /** https://nodejs.org/api/process.html#process_process_exit_code */ export const exit = (code?: number | string) => { - if (code || code === 0) { - denoOs.setExitCode(code); - } else if (Number.isNaN(code)) { - denoOs.setExitCode(1); - } + // if (code || code === 0) { + // denoOs.setExitCode(code); + // } else if (Number.isNaN(code)) { + // denoOs.setExitCode(1); + // } - ProcessExitCode = denoOs.getExitCode(); + // ProcessExitCode = denoOs.getExitCode(); if (!process._exiting) { process._exiting = true; // FIXME(bartlomieju): this is wrong, we won't be using syscall to exit @@ -397,7 +396,7 @@ Object.defineProperty(Process.prototype, "argv0", { get() { return argv0; }, - set(_val) {}, + set(_val) { }, }); /** https://nodejs.org/api/process.html#process_process_chdir_directory */ @@ -462,7 +461,7 @@ Object.defineProperty(Process.prototype, "exitCode", { throw new ERR_OUT_OF_RANGE("code", "an integer", parsedCode); } - denoOs.setExitCode(parsedCode); + // denoOs.setExitCode(parsedCode); ProcessExitCode = code; }, }); diff --git a/crates/npm/byonm.rs b/crates/npm/byonm.rs index d0b9e0e3f..1068e5ea6 100644 --- a/crates/npm/byonm.rs +++ b/crates/npm/byonm.rs @@ -3,31 +3,32 @@ use std::borrow::Cow; use std::path::Path; use std::path::PathBuf; -use std::rc::Rc; use std::sync::Arc; use deno_ast::ModuleSpecifier; +use deno_config::package_json::PackageJsonDepValue; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::serde_json; use deno_fs::FileSystem; use deno_semver::package::PackageReq; +use sb_node::errors::PackageFolderResolveError; +use sb_node::errors::PackageFolderResolveErrorKind; +use sb_node::load_pkg_json; use sb_node::NodePermissions; -use sb_node::NodeResolutionMode; use sb_node::NpmResolver; use sb_node::PackageJson; -use crate::package_json::get_local_package_json_version_reqs; use crate::{NpmProcessState, NpmProcessStateKind}; use sb_core::util::fs::{canonicalize_path_maybe_not_exists_with_fs, specifier_to_file_path}; -use super::common::types_package_name; use super::CliNpmResolver; use super::InnerCliNpmResolverRef; pub struct CliNpmResolverByonmCreateOptions { pub fs: Arc, - pub root_node_modules_dir: PathBuf, + // todo(dsherret): investigate removing this + pub root_node_modules_dir: Option, } pub fn create_byonm_npm_resolver( @@ -42,7 +43,7 @@ pub fn create_byonm_npm_resolver( #[derive(Debug)] pub struct ByonmCliNpmResolver { fs: Arc, - root_node_modules_dir: PathBuf, + root_node_modules_dir: Option, } impl ByonmCliNpmResolver { @@ -51,14 +52,12 @@ impl ByonmCliNpmResolver { &self, dep_name: &str, referrer: &ModuleSpecifier, - ) -> Option> { + ) -> Option> { let referrer_path = referrer.to_file_path().ok()?; let mut current_folder = referrer_path.parent()?; loop { let pkg_json_path = current_folder.join("package.json"); - if let Ok(pkg_json) = - PackageJson::load_skip_read_permission(self.fs.as_ref(), pkg_json_path) - { + if let Ok(Some(pkg_json)) = load_pkg_json(self.fs.as_ref(), &pkg_json_path) { if let Some(deps) = &pkg_json.dependencies { if deps.contains_key(dep_name) { return Some(pkg_json); @@ -78,13 +77,78 @@ impl ByonmCliNpmResolver { } } } + + fn resolve_pkg_json_and_alias_for_req( + &self, + req: &PackageReq, + referrer: &ModuleSpecifier, + ) -> Result<(Arc, String), AnyError> { + fn resolve_alias_from_pkg_json(req: &PackageReq, pkg_json: &PackageJson) -> Option { + let deps = pkg_json.resolve_local_package_json_deps(); + for (key, value) in deps { + if let Ok(value) = value { + match value { + PackageJsonDepValue::Req(dep_req) => { + if dep_req.name == req.name + && dep_req.version_req.intersects(&req.version_req) + { + return Some(key); + } + } + PackageJsonDepValue::Workspace(_workspace) => { + if key == req.name && req.version_req.tag() == Some("workspace") { + return Some(key); + } + } + } + } + } + None + } + + // attempt to resolve the npm specifier from the referrer's package.json, + if let Ok(file_path) = specifier_to_file_path(referrer) { + let mut current_path = file_path.as_path(); + while let Some(dir_path) = current_path.parent() { + let package_json_path = dir_path.join("package.json"); + if let Some(pkg_json) = load_pkg_json(self.fs.as_ref(), &package_json_path)? { + if let Some(alias) = resolve_alias_from_pkg_json(req, pkg_json.as_ref()) { + return Ok((pkg_json, alias)); + } + } + current_path = dir_path; + } + } + + // otherwise, fall fallback to the project's package.json + if let Some(root_node_modules_dir) = &self.root_node_modules_dir { + let root_pkg_json_path = root_node_modules_dir.parent().unwrap().join("package.json"); + if let Some(pkg_json) = load_pkg_json(self.fs.as_ref(), &root_pkg_json_path)? { + if let Some(alias) = resolve_alias_from_pkg_json(req, pkg_json.as_ref()) { + return Ok((pkg_json, alias)); + } + } + } + + bail!( + concat!( + "Could not find a matching package for 'npm:{}' in a package.json file. ", + "You must specify this as a package.json dependency when the ", + "node_modules folder is not managed by Deno.", + ), + req, + ); + } } impl NpmResolver for ByonmCliNpmResolver { fn get_npm_process_state(&self) -> String { serde_json::to_string(&NpmProcessState { kind: NpmProcessStateKind::Byonm, - local_node_modules_path: Some(self.root_node_modules_dir.to_string_lossy().to_string()), + local_node_modules_path: self + .root_node_modules_dir + .as_ref() + .map(|p| p.to_string_lossy().to_string()), }) .unwrap() } @@ -93,57 +157,46 @@ impl NpmResolver for ByonmCliNpmResolver { &self, name: &str, referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result { + ) -> Result { fn inner( fs: &dyn FileSystem, name: &str, referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result { - let referrer_file = specifier_to_file_path(referrer)?; - let types_pkg_name = if mode.is_types() && !name.starts_with("@types/") { - Some(types_package_name(name)) - } else { - None - }; - let mut current_folder = referrer_file.parent().unwrap(); - loop { - let node_modules_folder = if current_folder.ends_with("node_modules") { - Cow::Borrowed(current_folder) - } else { - Cow::Owned(current_folder.join("node_modules")) - }; + ) -> Result { + let maybe_referrer_file = specifier_to_file_path(referrer).ok(); + let maybe_start_folder = maybe_referrer_file.as_ref().and_then(|f| f.parent()); + if let Some(start_folder) = maybe_start_folder { + for current_folder in start_folder.ancestors() { + let node_modules_folder = if current_folder.ends_with("node_modules") { + Cow::Borrowed(current_folder) + } else { + Cow::Owned(current_folder.join("node_modules")) + }; - // attempt to resolve the types package first, then fallback to the regular package - if let Some(types_pkg_name) = &types_pkg_name { - let sub_dir = join_package_name(&node_modules_folder, types_pkg_name); + let sub_dir = join_package_name(&node_modules_folder, name); if fs.is_dir_sync(&sub_dir) { return Ok(sub_dir); } } - - let sub_dir = join_package_name(&node_modules_folder, name); - if fs.is_dir_sync(&sub_dir) { - return Ok(sub_dir); - } - - if let Some(parent) = current_folder.parent() { - current_folder = parent; - } else { - break; - } } - bail!( - "could not find package '{}' from referrer '{}'.", - name, - referrer - ); + Err(PackageFolderResolveErrorKind::NotFoundPackage { + package_name: name.to_string(), + referrer: referrer.clone(), + referrer_extra: None, + } + .into()) } - let path = inner(&*self.fs, name, referrer, mode)?; - Ok(self.fs.realpath_sync(&path)?) + let path = inner(&*self.fs, name, referrer)?; + self.fs.realpath_sync(&path).map_err(|err| { + PackageFolderResolveErrorKind::Io { + package_name: name.to_string(), + referrer: referrer.clone(), + source: err.into_io_error(), + } + .into() + }) } fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { @@ -156,7 +209,7 @@ impl NpmResolver for ByonmCliNpmResolver { fn ensure_read_permission( &self, - permissions: &dyn NodePermissions, + permissions: &mut dyn NodePermissions, path: &Path, ) -> Result<(), AnyError> { if !path @@ -186,7 +239,7 @@ impl CliNpmResolver for ByonmCliNpmResolver { } fn root_node_modules_path(&self) -> Option<&PathBuf> { - Some(&self.root_node_modules_dir) + self.root_node_modules_dir.as_ref() } fn resolve_pkg_folder_from_deno_module_req( @@ -194,61 +247,28 @@ impl CliNpmResolver for ByonmCliNpmResolver { req: &PackageReq, referrer: &ModuleSpecifier, ) -> Result { - fn resolve_from_package_json( - req: &PackageReq, - fs: &dyn FileSystem, - path: PathBuf, - ) -> Result { - let package_json = PackageJson::load_skip_read_permission(fs, path)?; - let deps = get_local_package_json_version_reqs(&package_json); - for (key, value) in deps { - if let Ok(value) = value { - if value.name == req.name && value.version_req.intersects(&req.version_req) { - let package_path = package_json - .path - .parent() - .unwrap() - .join("node_modules") - .join(key); - return Ok(canonicalize_path_maybe_not_exists_with_fs( - &package_path, - fs, - )?); - } - } + // resolve the pkg json and alias + let (pkg_json, alias) = self.resolve_pkg_json_and_alias_for_req(req, referrer)?; + // now try node resolution + for ancestor in pkg_json.path.parent().unwrap().ancestors() { + let node_modules_folder = ancestor.join("node_modules"); + let sub_dir = join_package_name(&node_modules_folder, &alias); + if self.fs.is_dir_sync(&sub_dir) { + return Ok(canonicalize_path_maybe_not_exists_with_fs( + &sub_dir, + self.fs.as_ref(), + )?); } - bail!( - concat!( - "Could not find a matching package for 'npm:{}' in '{}'. ", - "You must specify this as a package.json dependency when the ", - "node_modules folder is not managed by Deno.", - ), - req, - package_json.path.display() - ); } - // attempt to resolve the npm specifier from the referrer's package.json, - // but otherwise fallback to the project's package.json - if let Ok(file_path) = specifier_to_file_path(referrer) { - let mut current_path = file_path.as_path(); - while let Some(dir_path) = current_path.parent() { - let package_json_path = dir_path.join("package.json"); - if self.fs.exists_sync(&package_json_path) { - return resolve_from_package_json(req, self.fs.as_ref(), package_json_path); - } - current_path = dir_path; - } - } - - resolve_from_package_json( - req, - self.fs.as_ref(), - self.root_node_modules_dir - .parent() - .unwrap() - .join("package.json"), - ) + bail!( + concat!( + "Could not find \"{}\" in a node_modules folder. ", + "Deno expects the node_modules/ directory to be up to date. ", + "Did you forget to run `npm install`?" + ), + alias, + ); } fn check_state_hash(&self) -> Option { diff --git a/crates/npm/cache_dir.rs b/crates/npm/cache_dir.rs index d0ccec796..57cf0d6f2 100644 --- a/crates/npm/cache_dir.rs +++ b/crates/npm/cache_dir.rs @@ -20,10 +20,13 @@ pub struct NpmCacheDir { root_dir: PathBuf, // cached url representation of the root directory root_dir_url: Url, + // A list of all registry that were discovered via `.npmrc` files + // turned into a safe directory names. + known_registries_dirnames: Vec, } impl NpmCacheDir { - pub fn new(root_dir: PathBuf) -> Self { + pub fn new(root_dir: PathBuf, known_registries_urls: Vec) -> Self { fn try_get_canonicalized_root_dir(root_dir: &Path) -> Result { if !root_dir.exists() { std::fs::create_dir_all(root_dir) @@ -35,12 +38,27 @@ impl NpmCacheDir { // this may fail on readonly file systems, so just ignore if so let root_dir = try_get_canonicalized_root_dir(&root_dir).unwrap_or(root_dir); let root_dir_url = Url::from_directory_path(&root_dir).unwrap(); + + let known_registries_dirnames: Vec<_> = known_registries_urls + .into_iter() + .map(|url| { + root_url_to_safe_local_dirname(&url) + .to_string_lossy() + .replace('\\', "/") + }) + .collect(); + Self { root_dir, root_dir_url, + known_registries_dirnames, } } + pub fn root_dir(&self) -> &Path { + &self.root_dir + } + pub fn root_dir_url(&self) -> &Url { &self.root_dir_url } @@ -51,18 +69,14 @@ impl NpmCacheDir { registry_url: &Url, ) -> PathBuf { if folder_id.copy_index == 0 { - self.package_folder_for_name_and_version(&folder_id.nv, registry_url) + self.package_folder_for_nv(&folder_id.nv, registry_url) } else { self.package_name_folder(&folder_id.nv.name, registry_url) .join(format!("{}_{}", folder_id.nv.version, folder_id.copy_index)) } } - pub fn package_folder_for_name_and_version( - &self, - package: &PackageNv, - registry_url: &Url, - ) -> PathBuf { + pub fn package_folder_for_nv(&self, package: &PackageNv, registry_url: &Url) -> PathBuf { self.package_name_folder(&package.name, registry_url) .join(package.version.to_string()) } @@ -83,7 +97,7 @@ impl NpmCacheDir { } } - pub fn registry_folder(&self, registry_url: &Url) -> PathBuf { + fn registry_folder(&self, registry_url: &Url) -> PathBuf { self.root_dir .join(root_url_to_safe_local_dirname(registry_url)) } @@ -91,23 +105,29 @@ impl NpmCacheDir { pub fn resolve_package_folder_id_from_specifier( &self, specifier: &ModuleSpecifier, - registry_url: &Url, ) -> Option { - let registry_root_dir = self - .root_dir_url - .join(&format!( - "{}/", - root_url_to_safe_local_dirname(registry_url) - .to_string_lossy() - .replace('\\', "/") - )) - // this not succeeding indicates a fatal issue, so unwrap - .unwrap(); - let mut relative_url = registry_root_dir.make_relative(specifier)?; - if relative_url.starts_with("../") { - return None; + let mut maybe_relative_url = None; + + // Iterate through known registries and try to get a match. + for registry_dirname in &self.known_registries_dirnames { + let registry_root_dir = self + .root_dir_url + .join(&format!("{}/", registry_dirname)) + // this not succeeding indicates a fatal issue, so unwrap + .unwrap(); + let Some(relative_url) = registry_root_dir.make_relative(specifier) else { + continue; + }; + if relative_url.starts_with("../") { + continue; + } + + maybe_relative_url = Some(relative_url); + break; } + let mut relative_url = maybe_relative_url?; + // base32 decode the url if it starts with an underscore // * Ex. _{base32(package_name)}/ if let Some(end_url) = relative_url.strip_prefix('_') { diff --git a/crates/npm/common.rs b/crates/npm/common.rs index a54d4e36d..a39fab554 100644 --- a/crates/npm/common.rs +++ b/crates/npm/common.rs @@ -1,23 +1,25 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -/// Gets the corresponding @types package for the provided package name. -pub fn types_package_name(package_name: &str) -> String { - debug_assert!(!package_name.starts_with("@types/")); - // Scoped packages will get two underscores for each slash - // https://github.com/DefinitelyTyped/DefinitelyTyped/tree/15f1ece08f7b498f4b9a2147c2a46e94416ca777#what-about-scoped-packages - format!("@types/{}", package_name.replace('/', "__")) -} +use deno_npm::npm_rc::RegistryConfig; +use reqwest::header; -#[cfg(test)] -mod test { - use super::types_package_name; +// TODO(bartlomieju): support more auth methods besides token and basic auth +pub fn maybe_auth_header_for_npm_registry( + registry_config: &RegistryConfig, +) -> Option<(header::HeaderName, header::HeaderValue)> { + if let Some(token) = registry_config.auth_token.as_ref() { + return Some(( + header::AUTHORIZATION, + header::HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), + )); + } - #[test] - fn test_types_package_name() { - assert_eq!(types_package_name("name"), "@types/name"); - assert_eq!( - types_package_name("@scoped/package"), - "@types/@scoped__package" - ); + if let Some(auth) = registry_config.auth.as_ref() { + return Some(( + header::AUTHORIZATION, + header::HeaderValue::from_str(&format!("Basic {}", auth)).unwrap(), + )); } + + None } diff --git a/crates/npm/managed/cache.rs b/crates/npm/managed/cache/mod.rs similarity index 66% rename from crates/npm/managed/cache.rs rename to crates/npm/managed/cache/mod.rs index f3adfba72..f08e327f4 100644 --- a/crates/npm/managed/cache.rs +++ b/crates/npm/managed/cache/mod.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::fs; +use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -9,29 +10,34 @@ use std::sync::Arc; use deno_ast::ModuleSpecifier; use deno_core::anyhow::bail; use deno_core::anyhow::Context; -use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; +use deno_core::serde_json; use deno_core::url::Url; -use deno_fs; -use deno_npm::registry::NpmPackageVersionDistInfo; +use deno_npm::npm_rc::ResolvedNpmRc; +use deno_npm::registry::NpmPackageInfo; use deno_npm::NpmPackageCacheFolderId; use deno_semver::package::PackageNv; +use sb_core::cache::CACHE_PERM; +use sb_core::util::fs::atomic_write_file_with_retries; use crate::cache_dir::NpmCacheDir; use sb_core::cache::CacheSetting; use sb_core::util::fs::hard_link_dir_recursive; -use sb_core::util::http_util::HttpClient; -use super::tarball::verify_and_extract_tarball; +mod registry_info; +mod tarball; +mod tarball_extract; + +pub use registry_info::RegistryInfoDownloader; +pub use tarball::TarballCache; /// Stores a single copy of npm packages in a cache. #[derive(Debug)] pub struct NpmCache { cache_dir: NpmCacheDir, cache_setting: CacheSetting, - fs: Arc, - http_client: Arc, + npmrc: Arc, /// ensures a package is only downloaded once per run previously_reloaded_packages: Mutex>, } @@ -40,15 +46,13 @@ impl NpmCache { pub fn new( cache_dir: NpmCacheDir, cache_setting: CacheSetting, - fs: Arc, - http_client: Arc, + npmrc: Arc, ) -> Self { Self { cache_dir, cache_setting, - fs, - http_client, previously_reloaded_packages: Default::default(), + npmrc, } } @@ -65,7 +69,7 @@ impl NpmCache { /// to ensure a package is only downloaded once per run of the CLI. This /// prevents downloads from re-occurring when someone has `--reload` and /// and imports a dynamic import that imports the same package again for example. - fn should_use_global_cache_for_package(&self, package: &PackageNv) -> bool { + pub fn should_use_cache_for_package(&self, package: &PackageNv) -> bool { self.cache_setting.should_use_for_npm_package(&package.name) || !self .previously_reloaded_packages @@ -73,75 +77,19 @@ impl NpmCache { .insert(package.clone()) } - pub async fn ensure_package( - &self, - package: &PackageNv, - dist: &NpmPackageVersionDistInfo, - registry_url: &Url, - ) -> Result<(), AnyError> { - self.ensure_package_inner(package, dist, registry_url) - .await - .with_context(|| format!("Failed caching npm package '{package}'.")) - } - - async fn ensure_package_inner( - &self, - package: &PackageNv, - dist: &NpmPackageVersionDistInfo, - registry_url: &Url, - ) -> Result<(), AnyError> { - let package_folder = self - .cache_dir - .package_folder_for_name_and_version(package, registry_url); - if self.should_use_global_cache_for_package(package) - && self.fs.exists_sync(&package_folder) - // if this file exists, then the package didn't successfully extract - // the first time, or another process is currently extracting the zip file - && !self.fs.exists_sync(&package_folder.join(NPM_PACKAGE_SYNC_LOCK_FILENAME)) - { - return Ok(()); - } else if self.cache_setting == CacheSetting::Only { - return Err(custom_error( - "NotCached", - format!( - "An npm specifier not found in cache: \"{}\", --cached-only is specified.", - &package.name - ), - )); - } - - if dist.tarball.is_empty() { - bail!("Tarball URL was empty."); - } - - let maybe_bytes = self - .http_client - .download_with_progress(&dist.tarball) - .await?; - match maybe_bytes { - Some(bytes) => verify_and_extract_tarball(package, &bytes, dist, &package_folder), - None => { - bail!("Could not find npm package tarball at: {}", dist.tarball); - } - } - } - /// Ensures a copy of the package exists in the global cache. /// /// This assumes that the original package folder being hard linked /// from exists before this is called. - pub fn ensure_copy_package( - &self, - folder_id: &NpmPackageCacheFolderId, - registry_url: &Url, - ) -> Result<(), AnyError> { + pub fn ensure_copy_package(&self, folder_id: &NpmPackageCacheFolderId) -> Result<(), AnyError> { + let registry_url = self.npmrc.get_registry_url(&folder_id.nv.name); assert_ne!(folder_id.copy_index, 0); let package_folder = self .cache_dir .package_folder_for_id(folder_id, registry_url); if package_folder.exists() - // if this file exists, then the package didn't successfully extract + // if this file exists, then the package didn't successfully initialize // the first time, or another process is currently extracting the zip file && !package_folder.join(NPM_PACKAGE_SYNC_LOCK_FILENAME).exists() && self.cache_setting.should_use_for_npm_package(&folder_id.nv.name) @@ -151,45 +99,76 @@ impl NpmCache { let original_package_folder = self .cache_dir - .package_folder_for_name_and_version(&folder_id.nv, registry_url); + .package_folder_for_nv(&folder_id.nv, registry_url); + + // it seems Windows does an "AccessDenied" error when moving a + // directory with hard links, so that's why this solution is done with_folder_sync_lock(&folder_id.nv, &package_folder, || { hard_link_dir_recursive(&original_package_folder, &package_folder) })?; Ok(()) } - pub fn package_folder_for_id( - &self, - id: &NpmPackageCacheFolderId, - registry_url: &Url, - ) -> PathBuf { + pub fn package_folder_for_id(&self, id: &NpmPackageCacheFolderId) -> PathBuf { + let registry_url = self.npmrc.get_registry_url(&id.nv.name); self.cache_dir.package_folder_for_id(id, registry_url) } - pub fn package_folder_for_name_and_version( + pub fn package_folder_for_nv(&self, package: &PackageNv) -> PathBuf { + let registry_url = self.npmrc.get_registry_url(&package.name); + self.package_folder_for_nv_and_url(package, registry_url) + } + + pub fn package_folder_for_nv_and_url( &self, package: &PackageNv, registry_url: &Url, ) -> PathBuf { - self.cache_dir - .package_folder_for_name_and_version(package, registry_url) + self.cache_dir.package_folder_for_nv(package, registry_url) } - pub fn package_name_folder(&self, name: &str, registry_url: &Url) -> PathBuf { + pub fn package_name_folder(&self, name: &str) -> PathBuf { + let registry_url = self.npmrc.get_registry_url(name); self.cache_dir.package_name_folder(name, registry_url) } - pub fn registry_folder(&self, registry_url: &Url) -> PathBuf { - self.cache_dir.registry_folder(registry_url) + pub fn root_folder(&self) -> PathBuf { + self.cache_dir.root_dir().to_owned() } pub fn resolve_package_folder_id_from_specifier( &self, specifier: &ModuleSpecifier, - registry_url: &Url, ) -> Option { self.cache_dir - .resolve_package_folder_id_from_specifier(specifier, registry_url) + .resolve_package_folder_id_from_specifier(specifier) + } + + pub fn load_package_info(&self, name: &str) -> Result, AnyError> { + let file_cache_path = self.get_registry_package_info_file_cache_path(name); + + let file_text = match fs::read_to_string(file_cache_path) { + Ok(file_text) => file_text, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(err.into()), + }; + Ok(serde_json::from_str(&file_text)?) + } + + pub fn save_package_info( + &self, + name: &str, + package_info: &NpmPackageInfo, + ) -> Result<(), AnyError> { + let file_cache_path = self.get_registry_package_info_file_cache_path(name); + let file_text = serde_json::to_string(&package_info)?; + atomic_write_file_with_retries(&file_cache_path, file_text, CACHE_PERM)?; + Ok(()) + } + + fn get_registry_package_info_file_cache_path(&self, name: &str) -> PathBuf { + let name_folder_path = self.package_name_folder(name); + name_folder_path.join("registry.json") } } @@ -200,7 +179,6 @@ pub fn with_folder_sync_lock( output_folder: &Path, action: impl FnOnce() -> Result<(), AnyError>, ) -> Result<(), AnyError> { - #[allow(clippy::suspicious_open_options)] fn inner( output_folder: &Path, action: impl FnOnce() -> Result<(), AnyError>, @@ -220,6 +198,7 @@ pub fn with_folder_sync_lock( match fs::OpenOptions::new() .write(true) .create(true) + .truncate(false) .open(&sync_lock_path) { Ok(_) => { diff --git a/crates/npm/managed/cache/registry_info.rs b/crates/npm/managed/cache/registry_info.rs new file mode 100644 index 000000000..0f647ba3d --- /dev/null +++ b/crates/npm/managed/cache/registry_info.rs @@ -0,0 +1,243 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashMap; +use std::sync::Arc; + +use deno_core::anyhow::anyhow; +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; +use deno_core::error::custom_error; +use deno_core::error::AnyError; +use deno_core::futures::future::LocalBoxFuture; +use deno_core::futures::FutureExt; +use deno_core::parking_lot::Mutex; +use deno_core::serde_json; +use deno_core::url::Url; +use deno_npm::npm_rc::ResolvedNpmRc; +use deno_npm::registry::NpmPackageInfo; + +use crate::common::maybe_auth_header_for_npm_registry; + +use sb_core::cache::CacheSetting; +use sb_core::util::http_util::HttpClientProvider; +use sb_core::util::sync::MultiRuntimeAsyncValueCreator; + +use super::NpmCache; + +// todo(dsherret): create seams and unit test this + +type LoadResult = Result>; +type LoadFuture = LocalBoxFuture<'static, LoadResult>; + +#[derive(Debug, Clone)] +enum FutureResult { + PackageNotExists, + SavedFsCache(Arc), + ErroredFsCache(Arc), +} + +#[derive(Debug, Clone)] +enum MemoryCacheItem { + /// The cache item hasn't loaded yet. + Pending(Arc>), + /// The item has loaded in the past and was stored in the file system cache. + /// There is no reason to request this package from the npm registry again + /// for the duration of execution. + FsCached, + /// An item is memory cached when it fails saving to the file system cache + /// or the package does not exist. + MemoryCached(Result>, Arc>), +} + +/// Downloads packuments from the npm registry. +/// +/// This is shared amongst all the workers. +#[derive(Debug)] +pub struct RegistryInfoDownloader { + cache: Arc, + http_client_provider: Arc, + npmrc: Arc, + memory_cache: Mutex>, +} + +impl RegistryInfoDownloader { + pub fn new( + cache: Arc, + http_client_provider: Arc, + npmrc: Arc, + ) -> Self { + Self { + cache, + http_client_provider, + npmrc, + memory_cache: Default::default(), + } + } + + pub async fn load_package_info( + self: &Arc, + name: &str, + ) -> Result>, AnyError> { + self.load_package_info_inner(name).await.with_context(|| { + format!( + "Error getting response at {} for package \"{}\"", + self.get_package_url(name), + name + ) + }) + } + + async fn load_package_info_inner( + self: &Arc, + name: &str, + ) -> Result>, AnyError> { + if *self.cache.cache_setting() == CacheSetting::Only { + return Err(custom_error( + "NotCached", + format!( + "An npm specifier not found in cache: \"{name}\", --cached-only is specified." + ), + )); + } + + let cache_item = { + let mut mem_cache = self.memory_cache.lock(); + if let Some(cache_item) = mem_cache.get(name) { + cache_item.clone() + } else { + let value_creator = MultiRuntimeAsyncValueCreator::new({ + let downloader = self.clone(); + let name = name.to_string(); + Box::new(move || downloader.create_load_future(&name)) + }); + let cache_item = MemoryCacheItem::Pending(Arc::new(value_creator)); + mem_cache.insert(name.to_string(), cache_item.clone()); + cache_item + } + }; + + match cache_item { + MemoryCacheItem::FsCached => { + // this struct previously loaded from the registry, so we can load it from the file system cache + self.load_file_cached_package_info(name) + .await + .map(|info| Some(Arc::new(info))) + } + MemoryCacheItem::MemoryCached(maybe_info) => { + maybe_info.clone().map_err(|e| anyhow!("{}", e)) + } + MemoryCacheItem::Pending(value_creator) => { + match value_creator.get().await { + Ok(FutureResult::SavedFsCache(info)) => { + // return back the future and mark this package as having + // been saved in the cache for next time it's requested + *self.memory_cache.lock().get_mut(name).unwrap() = + MemoryCacheItem::FsCached; + Ok(Some(info)) + } + Ok(FutureResult::ErroredFsCache(info)) => { + // since saving to the fs cache failed, keep the package information in memory + *self.memory_cache.lock().get_mut(name).unwrap() = + MemoryCacheItem::MemoryCached(Ok(Some(info.clone()))); + Ok(Some(info)) + } + Ok(FutureResult::PackageNotExists) => { + *self.memory_cache.lock().get_mut(name).unwrap() = + MemoryCacheItem::MemoryCached(Ok(None)); + Ok(None) + } + Err(err) => { + let return_err = anyhow!("{}", err); + *self.memory_cache.lock().get_mut(name).unwrap() = + MemoryCacheItem::MemoryCached(Err(err)); + Err(return_err) + } + } + } + } + } + + async fn load_file_cached_package_info(&self, name: &str) -> Result { + // this scenario failing should be exceptionally rare so let's + // deal with improving it only when anyone runs into an issue + let maybe_package_info = deno_core::unsync::spawn_blocking({ + let cache = self.cache.clone(); + let name = name.to_string(); + move || cache.load_package_info(&name) + }) + .await + .unwrap() + .with_context(|| { + format!( + "Previously saved '{}' from the npm cache, but now it fails to load.", + name + ) + })?; + match maybe_package_info { + Some(package_info) => Ok(package_info), + None => { + bail!("The package '{}' previously saved its registry information to the file system cache, but that file no longer exists.", name) + } + } + } + + fn create_load_future(self: &Arc, name: &str) -> LoadFuture { + let downloader = self.clone(); + let package_url = self.get_package_url(name); + let registry_config = self.npmrc.get_registry_config(name); + let maybe_auth_header = maybe_auth_header_for_npm_registry(registry_config); + let name = name.to_string(); + async move { + let maybe_bytes = downloader + .http_client_provider + .get_or_create()? + .download_with_progress(package_url, maybe_auth_header) + .await?; + match maybe_bytes { + Some(bytes) => { + let future_result = deno_core::unsync::spawn_blocking( + move || -> Result { + let package_info = serde_json::from_slice(&bytes)?; + match downloader.cache.save_package_info(&name, &package_info) { + Ok(()) => Ok(FutureResult::SavedFsCache(Arc::new(package_info))), + Err(err) => { + log::debug!( + "Error saving package {} to cache: {:#}", + name, + err + ); + Ok(FutureResult::ErroredFsCache(Arc::new(package_info))) + } + } + }, + ) + .await??; + Ok(future_result) + } + None => Ok(FutureResult::PackageNotExists), + } + } + .map(|r| r.map_err(Arc::new)) + .boxed_local() + } + + fn get_package_url(&self, name: &str) -> Url { + let registry_url = self.npmrc.get_registry_url(name); + // list of all characters used in npm packages: + // !, ', (, ), *, -, ., /, [0-9], @, [A-Za-z], _, ~ + const ASCII_SET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC + .remove(b'!') + .remove(b'\'') + .remove(b'(') + .remove(b')') + .remove(b'*') + .remove(b'-') + .remove(b'.') + .remove(b'/') + .remove(b'@') + .remove(b'_') + .remove(b'~'); + let name = percent_encoding::utf8_percent_encode(name, &ASCII_SET); + registry_url.join(&name.to_string()).unwrap() + } +} diff --git a/crates/npm/managed/cache/tarball.rs b/crates/npm/managed/cache/tarball.rs new file mode 100644 index 000000000..38179e33c --- /dev/null +++ b/crates/npm/managed/cache/tarball.rs @@ -0,0 +1,230 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashMap; +use std::sync::Arc; + +use deno_core::anyhow::anyhow; +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; +use deno_core::error::custom_error; +use deno_core::error::AnyError; +use deno_core::futures::future::LocalBoxFuture; +use deno_core::futures::FutureExt; +use deno_core::parking_lot::Mutex; +use deno_fs::FileSystem; +use deno_npm::npm_rc::ResolvedNpmRc; +use deno_npm::registry::NpmPackageVersionDistInfo; +use deno_semver::package::PackageNv; +use reqwest::StatusCode; +use reqwest::Url; + +use sb_core::cache::CacheSetting; +use sb_core::util::http_util::DownloadError; +use sb_core::util::http_util::HttpClientProvider; +use sb_core::util::sync::MultiRuntimeAsyncValueCreator; + +use super::tarball_extract::verify_and_extract_tarball; +use super::tarball_extract::TarballExtractionMode; +use super::NpmCache; + +use crate::common::maybe_auth_header_for_npm_registry; + +// todo(dsherret): create seams and unit test this + +type LoadResult = Result<(), Arc>; +type LoadFuture = LocalBoxFuture<'static, LoadResult>; + +#[derive(Debug, Clone)] +enum MemoryCacheItem { + /// The cache item hasn't finished yet. + Pending(Arc>), + /// The result errored. + Errored(Arc), + /// This package has already been cached. + Cached, +} + +/// Coordinates caching of tarballs being loaded from +/// the npm registry. +/// +/// This is shared amongst all the workers. +#[derive(Debug)] +pub struct TarballCache { + cache: Arc, + fs: Arc, + http_client_provider: Arc, + npmrc: Arc, + memory_cache: Mutex>, +} + +impl TarballCache { + pub fn new( + cache: Arc, + fs: Arc, + http_client_provider: Arc, + npmrc: Arc, + ) -> Self { + Self { + cache, + fs, + http_client_provider, + npmrc, + memory_cache: Default::default(), + } + } + + pub async fn ensure_package( + self: &Arc, + package: &PackageNv, + dist: &NpmPackageVersionDistInfo, + ) -> Result<(), AnyError> { + self.ensure_package_inner(package, dist) + .await + .with_context(|| format!("Failed caching npm package '{}'.", package)) + } + + async fn ensure_package_inner( + self: &Arc, + package_nv: &PackageNv, + dist: &NpmPackageVersionDistInfo, + ) -> Result<(), AnyError> { + let cache_item = { + let mut mem_cache = self.memory_cache.lock(); + if let Some(cache_item) = mem_cache.get(package_nv) { + cache_item.clone() + } else { + let value_creator = MultiRuntimeAsyncValueCreator::new({ + let tarball_cache = self.clone(); + let package_nv = package_nv.clone(); + let dist = dist.clone(); + Box::new(move || { + tarball_cache.create_setup_future(package_nv.clone(), dist.clone()) + }) + }); + let cache_item = MemoryCacheItem::Pending(Arc::new(value_creator)); + mem_cache.insert(package_nv.clone(), cache_item.clone()); + cache_item + } + }; + + match cache_item { + MemoryCacheItem::Cached => Ok(()), + MemoryCacheItem::Errored(err) => Err(anyhow!("{}", err)), + MemoryCacheItem::Pending(creator) => { + let result = creator.get().await; + match result { + Ok(_) => { + *self.memory_cache.lock().get_mut(package_nv).unwrap() = + MemoryCacheItem::Cached; + Ok(()) + } + Err(err) => { + let result_err = anyhow!("{}", err); + *self.memory_cache.lock().get_mut(package_nv).unwrap() = + MemoryCacheItem::Errored(err); + Err(result_err) + } + } + } + } + } + + fn create_setup_future( + self: &Arc, + package_nv: PackageNv, + dist: NpmPackageVersionDistInfo, + ) -> LoadFuture { + let tarball_cache = self.clone(); + async move { + let registry_url = tarball_cache.npmrc.get_registry_url(&package_nv.name); + let package_folder = + tarball_cache.cache.package_folder_for_nv_and_url(&package_nv, registry_url); + let should_use_cache = tarball_cache.cache.should_use_cache_for_package(&package_nv); + let package_folder_exists = tarball_cache.fs.exists_sync(&package_folder); + if should_use_cache && package_folder_exists { + return Ok(()); + } else if tarball_cache.cache.cache_setting() == &CacheSetting::Only { + return Err(custom_error( + "NotCached", + format!( + "An npm specifier not found in cache: \"{}\", --cached-only is specified.", + &package_nv.name + ) + ) + ); + } + + if dist.tarball.is_empty() { + bail!("Tarball URL was empty."); + } + + // IMPORTANT: npm registries may specify tarball URLs at different URLS than the + // registry, so we MUST get the auth for the tarball URL and not the registry URL. + let tarball_uri = Url::parse(&dist.tarball)?; + let maybe_registry_config = + tarball_cache.npmrc.tarball_config(&tarball_uri); + let maybe_auth_header = maybe_registry_config.and_then(|c| maybe_auth_header_for_npm_registry(c)); + + let result = tarball_cache.http_client_provider + .get_or_create()? + .download_with_progress(tarball_uri, maybe_auth_header) + .await; + let maybe_bytes = match result { + Ok(maybe_bytes) => maybe_bytes, + Err(DownloadError::BadResponse(err)) => { + if err.status_code == StatusCode::UNAUTHORIZED + && maybe_registry_config.is_none() + && tarball_cache.npmrc.get_registry_config(&package_nv.name).auth_token.is_some() + { + bail!( + concat!( + "No auth for tarball URI, but present for scoped registry.\n\n", + "Tarball URI: {}\n", + "Scope URI: {}\n\n", + "More info here: https://github.com/npm/cli/wiki/%22No-auth-for-URI,-but-auth-present-for-scoped-registry%22" + ), + dist.tarball, + registry_url, + ) + } + return Err(err.into()) + }, + Err(err) => return Err(err.into()), + }; + match maybe_bytes { + Some(bytes) => { + let extraction_mode = if should_use_cache || !package_folder_exists { + TarballExtractionMode::SiblingTempDir + } else { + // The user ran with `--reload`, so overwrite the package instead of + // deleting it since the package might get corrupted if a user kills + // their deno process while it's deleting a package directory + // + // We can't rename this folder and delete it because the folder + // may be in use by another process or may now contain hardlinks, + // which will cause windows to throw an "AccessDenied" error when + // renaming. So we settle for overwriting. + TarballExtractionMode::Overwrite + }; + let dist = dist.clone(); + let package_nv = package_nv.clone(); + deno_core::unsync::spawn_blocking(move || { + verify_and_extract_tarball( + &package_nv, + &bytes, + &dist, + &package_folder, + extraction_mode, + ) + }) + .await? + } + None => { + bail!("Could not find npm package tarball at: {}", dist.tarball); + } + } + } + .map(|r| r.map_err(Arc::new)) + .boxed_local() + } +} diff --git a/crates/npm/managed/cache/tarball_extract.rs b/crates/npm/managed/cache/tarball_extract.rs new file mode 100644 index 000000000..d019dad15 --- /dev/null +++ b/crates/npm/managed/cache/tarball_extract.rs @@ -0,0 +1,204 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashSet; +use std::fs; +use std::io::ErrorKind; +use std::path::Path; +use std::path::PathBuf; + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_npm::registry::NpmPackageVersionDistInfo; +use deno_npm::registry::NpmPackageVersionDistInfoIntegrity; +use deno_semver::package::PackageNv; +use flate2::read::GzDecoder; +use tar::Archive; +use tar::EntryType; + +use sb_core::util::path::get_atomic_dir_path; + +#[derive(Debug, Copy, Clone)] +pub enum TarballExtractionMode { + /// Overwrites the destination directory without deleting any files. + Overwrite, + /// Creates and writes to a sibling temporary directory. When done, moves + /// it to the final destination. + /// + /// This is more robust than `Overwrite` as it better handles multiple + /// processes writing to the directory at the same time. + SiblingTempDir, +} + +pub fn verify_and_extract_tarball( + package_nv: &PackageNv, + data: &[u8], + dist_info: &NpmPackageVersionDistInfo, + output_folder: &Path, + extraction_mode: TarballExtractionMode, +) -> Result<(), AnyError> { + verify_tarball_integrity(package_nv, data, &dist_info.integrity())?; + + match extraction_mode { + TarballExtractionMode::Overwrite => extract_tarball(data, output_folder), + TarballExtractionMode::SiblingTempDir => { + let temp_dir = get_atomic_dir_path(output_folder); + extract_tarball(data, &temp_dir)?; + rename_with_retries(&temp_dir, output_folder) + .map_err(AnyError::from) + .context("Failed moving extracted tarball to final destination.") + } + } +} + +fn rename_with_retries(temp_dir: &Path, output_folder: &Path) -> Result<(), std::io::Error> { + fn already_exists(err: &std::io::Error, output_folder: &Path) -> bool { + // Windows will do an "Access is denied" error + err.kind() == ErrorKind::AlreadyExists || output_folder.exists() + } + + let mut count = 0; + // renaming might be flaky if a lot of processes are trying + // to do this, so retry a few times + loop { + match fs::rename(temp_dir, output_folder) { + Ok(_) => return Ok(()), + Err(err) if already_exists(&err, output_folder) => { + // another process copied here, just cleanup + let _ = fs::remove_dir_all(temp_dir); + return Ok(()); + } + Err(err) => { + count += 1; + if count > 5 { + // too many retries, cleanup and return the error + let _ = fs::remove_dir_all(temp_dir); + return Err(err); + } + + // wait a bit before retrying... this should be very rare or only + // in error cases, so ok to sleep a bit + let sleep_ms = std::cmp::min(100, 20 * count); + std::thread::sleep(std::time::Duration::from_millis(sleep_ms)); + } + } + } +} + +fn verify_tarball_integrity( + package: &PackageNv, + data: &[u8], + npm_integrity: &NpmPackageVersionDistInfoIntegrity, +) -> Result<(), AnyError> { + use ring::digest::Context; + let (tarball_checksum, expected_checksum) = match npm_integrity { + NpmPackageVersionDistInfoIntegrity::Integrity { + algorithm, + base64_hash, + } => { + let algo = match *algorithm { + "sha512" => &ring::digest::SHA512, + "sha1" => &ring::digest::SHA1_FOR_LEGACY_USE_ONLY, + hash_kind => bail!( + "Not implemented hash function for {}: {}", + package, + hash_kind + ), + }; + let mut hash_ctx = Context::new(algo); + hash_ctx.update(data); + let digest = hash_ctx.finish(); + let tarball_checksum = BASE64_STANDARD.encode(digest.as_ref()); + (tarball_checksum, base64_hash) + } + NpmPackageVersionDistInfoIntegrity::LegacySha1Hex(hex) => { + let mut hash_ctx = Context::new(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY); + hash_ctx.update(data); + let digest = hash_ctx.finish(); + let tarball_checksum = faster_hex::hex_string(digest.as_ref()); + (tarball_checksum, hex) + } + NpmPackageVersionDistInfoIntegrity::UnknownIntegrity(integrity) => { + bail!( + "Not implemented integrity kind for {}: {}", + package, + integrity + ) + } + }; + + if tarball_checksum != *expected_checksum { + bail!( + "Tarball checksum did not match what was provided by npm registry for {}.\n\nExpected: {}\nActual: {}", + package, + expected_checksum, + tarball_checksum, + ) + } + Ok(()) +} + +fn extract_tarball(data: &[u8], output_folder: &Path) -> Result<(), AnyError> { + fs::create_dir_all(output_folder)?; + let output_folder = fs::canonicalize(output_folder)?; + let tar = GzDecoder::new(data); + let mut archive = Archive::new(tar); + archive.set_overwrite(true); + archive.set_preserve_permissions(true); + let mut created_dirs = HashSet::new(); + + for entry in archive.entries()? { + let mut entry = entry?; + let path = entry.path()?; + let entry_type = entry.header().entry_type(); + + // Some package tarballs contain "pax_global_header", these entries + // should be skipped. + if entry_type == EntryType::XGlobalHeader { + continue; + } + + // skip the first component which will be either "package" or the name of the package + let relative_path = path.components().skip(1).collect::(); + let absolute_path = output_folder.join(relative_path); + let dir_path = if entry_type == EntryType::Directory { + absolute_path.as_path() + } else { + absolute_path.parent().unwrap() + }; + if created_dirs.insert(dir_path.to_path_buf()) { + fs::create_dir_all(dir_path)?; + let canonicalized_dir = fs::canonicalize(dir_path)?; + if !canonicalized_dir.starts_with(&output_folder) { + bail!( + "Extracted directory '{}' of npm tarball was not in output directory.", + canonicalized_dir.display() + ) + } + } + + let entry_type = entry.header().entry_type(); + match entry_type { + EntryType::Regular => { + entry.unpack(&absolute_path)?; + } + EntryType::Symlink | EntryType::Link => { + // At the moment, npm doesn't seem to support uploading hardlinks or + // symlinks to the npm registry. If ever adding symlink or hardlink + // support, we will need to validate that the hardlink and symlink + // target are within the package directory. + log::warn!( + "Ignoring npm tarball entry type {:?} for '{}'", + entry_type, + absolute_path.display() + ) + } + _ => { + // ignore + } + } + } + Ok(()) +} diff --git a/crates/npm/managed/installer.rs b/crates/npm/managed/installer.rs deleted file mode 100644 index e36259ab0..000000000 --- a/crates/npm/managed/installer.rs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -use std::future::Future; -use std::sync::Arc; - -use deno_core::error::AnyError; -use deno_core::futures::stream::FuturesOrdered; -use deno_core::futures::StreamExt; -use deno_npm::registry::NpmRegistryApi; -use deno_npm::registry::NpmRegistryPackageInfoLoadError; -use deno_semver::package::PackageReq; - -use crate::package_json::PackageJsonDepsProvider; - -use sb_core::util::sync::AtomicFlag; - -use super::CliNpmRegistryApi; -use super::NpmResolution; - -#[derive(Debug)] -struct PackageJsonDepsInstallerInner { - deps_provider: Arc, - has_installed_flag: AtomicFlag, - npm_registry_api: Arc, - npm_resolution: Arc, -} - -impl PackageJsonDepsInstallerInner { - pub fn reqs_with_info_futures<'a>( - &self, - reqs: &'a [&'a PackageReq], - ) -> FuturesOrdered< - impl Future< - Output = Result< - (&'a PackageReq, Arc), - NpmRegistryPackageInfoLoadError, - >, - >, - > { - FuturesOrdered::from_iter(reqs.iter().map(|req| { - let api = self.npm_registry_api.clone(); - async move { - let info = api.package_info(&req.name).await?; - Ok::<_, NpmRegistryPackageInfoLoadError>((*req, info)) - } - })) - } -} - -/// Holds and controls installing dependencies from package.json. -#[derive(Debug, Default)] -pub struct PackageJsonDepsInstaller(Option); - -impl PackageJsonDepsInstaller { - pub fn new( - deps_provider: Arc, - npm_registry_api: Arc, - npm_resolution: Arc, - ) -> Self { - Self(Some(PackageJsonDepsInstallerInner { - deps_provider, - has_installed_flag: Default::default(), - npm_registry_api, - npm_resolution, - })) - } - - /// Creates an installer that never installs local packages during - /// resolution. A top level install will be a no-op. - pub fn no_op() -> Self { - Self(None) - } - - /// Installs the top level dependencies in the package.json file - /// without going through and resolving the descendant dependencies yet. - pub async fn ensure_top_level_install(&self) -> Result<(), AnyError> { - let inner = match &self.0 { - Some(inner) => inner, - None => return Ok(()), - }; - - if !inner.has_installed_flag.raise() { - return Ok(()); // already installed by something else - } - - let package_reqs = inner.deps_provider.reqs().unwrap_or_default(); - - // check if something needs resolving before bothering to load all - // the package information (which is slow) - if package_reqs.iter().all(|req| { - inner - .npm_resolution - .resolve_pkg_id_from_pkg_req(req) - .is_ok() - }) { - log::debug!("All package.json deps resolvable. Skipping top level install."); - return Ok(()); // everything is already resolvable - } - - let mut reqs_with_info_futures = inner.reqs_with_info_futures(&package_reqs); - - while let Some(result) = reqs_with_info_futures.next().await { - let (req, info) = result?; - let result = inner - .npm_resolution - .resolve_pkg_req_as_pending_with_info(req, &info); - if let Err(err) = result { - if inner.npm_registry_api.mark_force_reload() { - log::debug!("Failed to resolve package. Retrying. Error: {err:#}"); - // re-initialize - reqs_with_info_futures = inner.reqs_with_info_futures(&package_reqs); - } else { - return Err(err.into()); - } - } - } - - Ok(()) - } -} diff --git a/crates/npm/managed/mod.rs b/crates/npm/managed/mod.rs index 540d36454..a2eac523c 100644 --- a/crates/npm/managed/mod.rs +++ b/crates/npm/managed/mod.rs @@ -4,45 +4,48 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use cache::RegistryInfoDownloader; +use cache::TarballCache; use deno_ast::ModuleSpecifier; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::serde_json; -use deno_core::url::Url; +use deno_core::unsync::AtomicFlag; use deno_fs::FileSystem; -use deno_graph::NpmPackageReqResolution; +use deno_npm::npm_rc::ResolvedNpmRc; +use deno_npm::registry::NpmPackageInfo; use deno_npm::registry::NpmRegistryApi; +use deno_npm::resolution::NpmResolutionSnapshot; use deno_npm::resolution::PackageReqNotFoundError; use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; -use deno_npm::resolution::{NpmResolutionSnapshot, SnapshotFromLockfileParams}; use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; use deno_npm::NpmSystemInfo; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; +use package_json::PackageJsonInstallDepsProvider; +use resolution::AddPkgReqsResult; +use sb_core::util::http_util::HttpClientProvider; +use sb_node::errors::PackageFolderResolveError; +use sb_node::errors::PackageFolderResolveErrorKind; use sb_node::NodePermissions; -use sb_node::NodeResolutionMode; use sb_node::NpmResolver; use crate::cache_dir::NpmCacheDir; -use crate::package_json::PackageJsonDepsProvider; use crate::{CliNpmResolver, InnerCliNpmResolverRef, NpmProcessState, NpmProcessStateKind}; use deno_lockfile::Lockfile; use sb_core::cache::common::FastInsecureHasher; use sb_core::cache::CacheSetting; use sb_core::util::fs::{canonicalize_path_maybe_not_exists_with_fs, dir_size}; -use sb_core::util::http_util::HttpClient; use self::cache::NpmCache; -use self::installer::PackageJsonDepsInstaller; use self::registry::CliNpmRegistryApi; use self::resolution::NpmResolution; use self::resolvers::create_npm_fs_resolver; use self::resolvers::NpmPackageFsResolver; pub mod cache; -pub mod installer; pub mod package_json; pub mod registry; pub mod resolution; @@ -54,65 +57,17 @@ pub enum CliNpmResolverManagedSnapshotOption { Specified(Option), } -pub enum CliNpmResolverManagedPackageJsonInstallerOption { - ConditionalInstall(Arc), - NoInstall, -} - pub struct CliNpmResolverManagedCreateOptions { pub snapshot: CliNpmResolverManagedSnapshotOption, pub maybe_lockfile: Option>>, - pub fs: Arc, - pub http_client: Arc, + pub fs: Arc, + pub http_client_provider: Arc, pub npm_global_cache_dir: PathBuf, pub cache_setting: CacheSetting, pub maybe_node_modules_path: Option, pub npm_system_info: NpmSystemInfo, - pub package_json_installer: CliNpmResolverManagedPackageJsonInstallerOption, - pub npm_registry_url: Url, -} - -async fn resolve_snapshot( - api: &CliNpmRegistryApi, - snapshot: CliNpmResolverManagedSnapshotOption, -) -> Result, AnyError> { - match snapshot { - CliNpmResolverManagedSnapshotOption::ResolveFromLockfile(lockfile) => { - if !lockfile.lock().overwrite { - let snapshot = snapshot_from_lockfile(lockfile.clone(), api) - .await - .with_context(|| { - format!( - "failed reading lockfile '{}'", - lockfile.lock().filename.display() - ) - })?; - // clear the memory cache to reduce memory usage - api.clear_memory_cache(); - Ok(Some(snapshot)) - } else { - Ok(None) - } - } - CliNpmResolverManagedSnapshotOption::Specified(snapshot) => Ok(snapshot), - } -} - -async fn snapshot_from_lockfile( - lockfile: Arc>, - api: &dyn NpmRegistryApi, -) -> Result { - let incomplete_snapshot = { - let lock = lockfile.lock(); - deno_npm::resolution::incomplete_snapshot_from_lockfile(&lock)? - }; - let snapshot = deno_npm::resolution::snapshot_from_lockfile(SnapshotFromLockfileParams { - incomplete_snapshot, - api, - skip_integrity_check: true, // TODO - }) - .await?; - Ok(snapshot) + pub package_json_deps_provider: Arc, + pub npmrc: Arc, } pub async fn create_managed_npm_resolver( @@ -122,94 +77,149 @@ pub async fn create_managed_npm_resolver( let npm_api = create_api(&options, npm_cache.clone()); let snapshot = resolve_snapshot(&npm_api, options.snapshot).await?; Ok(create_inner( - npm_cache, - npm_api, - snapshot, - options.maybe_lockfile, options.fs, + options.http_client_provider, + options.maybe_lockfile, + npm_api, + npm_cache, + options.npmrc, + options.package_json_deps_provider, options.maybe_node_modules_path, - options.package_json_installer, - options.npm_registry_url, options.npm_system_info, + snapshot, )) } #[allow(clippy::too_many_arguments)] fn create_inner( - npm_cache: Arc, - npm_api: Arc, - snapshot: Option, - maybe_lockfile: Option>>, fs: Arc, + http_client_provider: Arc, + maybe_lockfile: Option>>, + npm_api: Arc, + npm_cache: Arc, + npm_rc: Arc, + package_json_deps_provider: Arc, node_modules_dir_path: Option, - package_json_installer: CliNpmResolverManagedPackageJsonInstallerOption, - npm_registry_url: Url, npm_system_info: NpmSystemInfo, + snapshot: Option, ) -> Arc { let resolution = Arc::new(NpmResolution::from_serialized( npm_api.clone(), snapshot, maybe_lockfile.clone(), )); - let npm_fs_resolver = create_npm_fs_resolver( + let tarball_cache = Arc::new(TarballCache::new( + npm_cache.clone(), + fs.clone(), + http_client_provider.clone(), + npm_rc.clone(), + )); + let fs_resolver = create_npm_fs_resolver( fs.clone(), npm_cache.clone(), - npm_registry_url, + &package_json_deps_provider, resolution.clone(), + tarball_cache.clone(), node_modules_dir_path, npm_system_info.clone(), ); - let package_json_deps_installer = match package_json_installer { - CliNpmResolverManagedPackageJsonInstallerOption::ConditionalInstall(provider) => Arc::new( - PackageJsonDepsInstaller::new(provider, npm_api.clone(), resolution.clone()), - ), - CliNpmResolverManagedPackageJsonInstallerOption::NoInstall => { - Arc::new(PackageJsonDepsInstaller::no_op()) - } - }; Arc::new(ManagedCliNpmResolver::new( - npm_api, fs, - resolution, - npm_fs_resolver, - npm_cache, + fs_resolver, maybe_lockfile, - package_json_deps_installer, + npm_api, + npm_cache, + package_json_deps_provider, + resolution, + tarball_cache, npm_system_info, )) } -pub fn create_cache(options: &CliNpmResolverManagedCreateOptions) -> Arc { +fn create_cache(options: &CliNpmResolverManagedCreateOptions) -> Arc { Arc::new(NpmCache::new( - NpmCacheDir::new(options.npm_global_cache_dir.clone()), + NpmCacheDir::new( + options.npm_global_cache_dir.clone(), + options.npmrc.get_all_known_registries_urls(), + ), options.cache_setting.clone(), - options.fs.clone(), - options.http_client.clone(), + options.npmrc.clone(), )) } -pub fn create_api( +fn create_api( options: &CliNpmResolverManagedCreateOptions, npm_cache: Arc, ) -> Arc { Arc::new(CliNpmRegistryApi::new( - options.npm_registry_url.clone(), npm_cache.clone(), - options.http_client.clone(), + Arc::new(RegistryInfoDownloader::new( + npm_cache, + options.http_client_provider.clone(), + options.npmrc.clone(), + )), )) } +async fn resolve_snapshot( + api: &CliNpmRegistryApi, + snapshot: CliNpmResolverManagedSnapshotOption, +) -> Result, AnyError> { + match snapshot { + CliNpmResolverManagedSnapshotOption::ResolveFromLockfile(lockfile) => { + let lockfile_guard = lockfile.lock(); + let overwrite = lockfile_guard.overwrite; + if !overwrite { + let filename = lockfile_guard.filename.clone(); + + drop(lockfile_guard); + let snapshot = snapshot_from_lockfile(lockfile.clone(), api) + .await + .with_context(|| format!("failed reading lockfile '{}'", filename.display()))?; + Ok(Some(snapshot)) + } else { + Ok(None) + } + } + CliNpmResolverManagedSnapshotOption::Specified(snapshot) => Ok(snapshot), + } +} + +async fn snapshot_from_lockfile( + lockfile: Arc>, + api: &dyn NpmRegistryApi, +) -> Result { + let (incomplete_snapshot, skip_integrity_check) = { + let lock = lockfile.lock(); + ( + deno_npm::resolution::incomplete_snapshot_from_lockfile(&lock)?, + lock.overwrite, + ) + }; + let snapshot = deno_npm::resolution::snapshot_from_lockfile( + deno_npm::resolution::SnapshotFromLockfileParams { + incomplete_snapshot, + api, + skip_integrity_check, + }, + ) + .await?; + Ok(snapshot) +} + /// An npm resolver where the resolution is managed by Deno rather than /// the user bringing their own node_modules (BYONM) on the file system. pub struct ManagedCliNpmResolver { - api: Arc, fs: Arc, fs_resolver: Arc, - global_npm_cache: Arc, - resolution: Arc, maybe_lockfile: Option>>, + npm_api: Arc, + npm_cache: Arc, + package_json_deps_provider: Arc, + resolution: Arc, + tarball_cache: Arc, npm_system_info: NpmSystemInfo, - package_json_deps_installer: Arc, + top_level_install_flag: AtomicFlag, } impl std::fmt::Debug for ManagedCliNpmResolver { @@ -223,39 +233,30 @@ impl std::fmt::Debug for ManagedCliNpmResolver { impl ManagedCliNpmResolver { #[allow(clippy::too_many_arguments)] pub fn new( - api: Arc, fs: Arc, - resolution: Arc, fs_resolver: Arc, - global_npm_cache: Arc, maybe_lockfile: Option>>, - package_json_deps_installer: Arc, + npm_api: Arc, + npm_cache: Arc, + package_json_deps_provider: Arc, + resolution: Arc, + tarball_cache: Arc, npm_system_info: NpmSystemInfo, ) -> Self { Self { - api, fs, fs_resolver, - global_npm_cache, - resolution, maybe_lockfile, - package_json_deps_installer, + npm_api, + npm_cache, + package_json_deps_provider, + resolution, + tarball_cache, npm_system_info, + top_level_install_flag: Default::default(), } } - pub fn npm_api(&self) -> Arc { - self.api.clone() - } - - pub fn npm_resolution(&self) -> Arc { - self.resolution.clone() - } - - pub fn npm_cache(&self) -> Arc { - self.global_npm_cache.clone() - } - pub fn resolve_pkg_folder_from_pkg_id( &self, pkg_id: &NpmPackageId, @@ -311,21 +312,45 @@ impl ManagedCliNpmResolver { } /// Adds package requirements to the resolver and ensures everything is setup. + /// This includes setting up the `node_modules` directory, if applicable. pub async fn add_package_reqs(&self, packages: &[PackageReq]) -> Result<(), AnyError> { + self.add_package_reqs_raw(packages) + .await + .dependencies_result + } + + pub async fn add_package_reqs_raw(&self, packages: &[PackageReq]) -> AddPkgReqsResult { if packages.is_empty() { - return Ok(()); + return AddPkgReqsResult { + dependencies_result: Ok(()), + results: vec![], + }; } - self.resolution.add_package_reqs(packages).await?; - self.fs_resolver.cache_packages().await?; + let mut result = self.resolution.add_package_reqs(packages).await; + + if result.dependencies_result.is_ok() { + if let Some(_lockfile) = self.maybe_lockfile.as_ref() { + result.dependencies_result = { + Ok(()) + // NOTE(Nyannyacha): If the edge runtime implements the frozen option for + // lockfile, the comment below should be uncommented. + + // let lockfile = lockfile.lock(); - // If there's a lock file, update it with all discovered npm packages - if let Some(lockfile_mutex) = &self.maybe_lockfile { - let mut lockfile = lockfile_mutex.lock(); - self.lock(&mut lockfile)?; + // if lockfile.has_content_changed { + // Err(anyhow!("The lockfile is out of date.")) + // } else { + // Ok(()) + // } + }; + } + } + if result.dependencies_result.is_ok() { + result.dependencies_result = self.cache_packages().await.map_err(AnyError::from); } - Ok(()) + result } /// Sets package requirements to the resolver, removing old requirements and adding new ones. @@ -347,49 +372,18 @@ impl ManagedCliNpmResolver { .serialized_valid_snapshot_for_system(system_info) } - pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> { - self.resolution.lock(lockfile) - } - pub async fn inject_synthetic_types_node_package(&self) -> Result<(), AnyError> { // add and ensure this isn't added to the lockfile - let package_reqs = vec![PackageReq::from_str("@types/node").unwrap()]; - self.resolution.add_package_reqs(&package_reqs).await?; - self.fs_resolver.cache_packages().await?; + self.add_package_reqs(&[PackageReq::from_str("@types/node").unwrap()]) + .await?; Ok(()) } - pub async fn resolve_pending(&self) -> Result<(), AnyError> { - self.resolution.resolve_pending().await?; - self.cache_packages().await - } - pub async fn cache_packages(&self) -> Result<(), AnyError> { self.fs_resolver.cache_packages().await } - /// Resolves a package requirement for deno graph. This should only be - /// called by deno_graph's NpmResolver or for resolving packages in - /// a package.json - pub fn resolve_npm_for_deno_graph(&self, pkg_req: &PackageReq) -> NpmPackageReqResolution { - let result = self.resolution.resolve_pkg_req_as_pending(pkg_req); - match result { - Ok(nv) => NpmPackageReqResolution::Ok(nv), - Err(err) => { - if self.api.mark_force_reload() { - log::debug!( - "Restarting npm specifier resolution to check for new registry information. Error: {:#}", - err - ); - NpmPackageReqResolution::ReloadRegistryInfo(err.into()) - } else { - NpmPackageReqResolution::Err(err.into()) - } - } - } - } - pub fn resolve_pkg_folder_from_deno_module(&self, nv: &PackageNv) -> Result { let pkg_id = self.resolution.resolve_pkg_id_from_deno_module(nv)?; self.resolve_pkg_folder_from_pkg_id(&pkg_id) @@ -402,27 +396,46 @@ impl ManagedCliNpmResolver { self.resolution.resolve_pkg_id_from_pkg_req(req) } - pub async fn ensure_top_level_package_json_install(&self) -> Result<(), AnyError> { - self.package_json_deps_installer - .ensure_top_level_install() - .await + /// Ensures that the top level `package.json` dependencies are installed. + /// This may set up the `node_modules` directory. + /// + /// Returns `true` if any changes (such as caching packages) were made. + /// If this returns `false`, `node_modules` has _not_ been set up. + pub async fn ensure_top_level_package_json_install(&self) -> Result { + if !self.top_level_install_flag.raise() { + return Ok(false); // already did this + } + let reqs = self.package_json_deps_provider.remote_pkg_reqs(); + if reqs.is_empty() { + return Ok(false); + } + + // check if something needs resolving before bothering to load all + // the package information (which is slow) + if reqs + .iter() + .all(|req| self.resolution.resolve_pkg_id_from_pkg_req(req).is_ok()) + { + log::debug!("All package.json deps resolvable. Skipping top level install."); + return Ok(false); // everything is already resolvable + } + + self.add_package_reqs(reqs).await.map(|_| true) } - pub async fn cache_package_info(&self, package_name: &str) -> Result<(), AnyError> { + pub async fn cache_package_info( + &self, + package_name: &str, + ) -> Result, AnyError> { // this will internally cache the package information - self.api + self.npm_api .package_info(package_name) .await - .map(|_| ()) .map_err(|err| err.into()) } - pub fn registry_base_url(&self) -> &ModuleSpecifier { - self.api.base_url() - } - - pub fn registry_folder_in_global_cache(&self, registry_url: &ModuleSpecifier) -> PathBuf { - self.global_npm_cache.registry_folder(registry_url) + pub fn global_cache_root_folder(&self) -> PathBuf { + self.npm_cache.root_folder() } } @@ -447,11 +460,19 @@ impl NpmResolver for ManagedCliNpmResolver { &self, name: &str, referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result { + ) -> Result { let path = self .fs_resolver - .resolve_package_folder_from_package(name, referrer, mode)?; + .resolve_package_folder_from_package(name, referrer)?; + let path = + canonicalize_path_maybe_not_exists_with_fs(&path, self.fs.as_ref()).map_err(|err| { + PackageFolderResolveErrorKind::Io { + package_name: name.to_string(), + referrer: referrer.clone(), + source: err, + } + })?; + log::debug!("Resolved {} from {} to {}", name, referrer, path.display()); Ok(path) } @@ -463,7 +484,7 @@ impl NpmResolver for ManagedCliNpmResolver { fn ensure_read_permission( &self, - permissions: &dyn NodePermissions, + permissions: &mut dyn NodePermissions, path: &Path, ) -> Result<(), AnyError> { self.fs_resolver.ensure_read_permission(permissions, path) @@ -478,26 +499,28 @@ impl CliNpmResolver for ManagedCliNpmResolver { fn clone_snapshotted(&self) -> Arc { // create a new snapshotted npm resolution and resolver let npm_resolution = Arc::new(NpmResolution::new( - self.api.clone(), + self.npm_api.clone(), self.resolution.snapshot(), self.maybe_lockfile.clone(), )); Arc::new(ManagedCliNpmResolver::new( - self.api.clone(), self.fs.clone(), - npm_resolution.clone(), create_npm_fs_resolver( self.fs.clone(), - self.global_npm_cache.clone(), - self.api.base_url().clone(), - npm_resolution, + self.npm_cache.clone(), + &self.package_json_deps_provider, + npm_resolution.clone(), + self.tarball_cache.clone(), self.root_node_modules_path().map(ToOwned::to_owned), self.npm_system_info.clone(), ), - self.global_npm_cache.clone(), self.maybe_lockfile.clone(), - self.package_json_deps_installer.clone(), + self.npm_api.clone(), + self.npm_cache.clone(), + self.package_json_deps_provider.clone(), + npm_resolution, + self.tarball_cache.clone(), self.npm_system_info.clone(), )) } @@ -528,7 +551,10 @@ impl CliNpmResolver for ManagedCliNpmResolver { .into_iter() .collect::>(); package_reqs.sort_by(|a, b| a.0.cmp(&b.0)); // determinism - let mut hasher = FastInsecureHasher::new(); + let mut hasher = FastInsecureHasher::new_without_deno_version(); + // ensure the cache gets busted when turning nodeModulesDir on or off + // as this could cause changes in resolution + hasher.write_hashable(self.fs_resolver.node_modules_path().is_some()); for (pkg_req, pkg_nv) in package_reqs { hasher.write_hashable(&pkg_req); hasher.write_hashable(&pkg_nv); diff --git a/crates/npm/managed/package_json.rs b/crates/npm/managed/package_json.rs index 08d09a2dd..8030aeb1a 100644 --- a/crates/npm/managed/package_json.rs +++ b/crates/npm/managed/package_json.rs @@ -1,95 +1,88 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_npm::registry::parse_dep_entry_name_and_raw_version; + +use std::path::PathBuf; +use std::sync::Arc; + +use deno_config::package_json::PackageJsonDepValue; +use deno_config::workspace::Workspace; use deno_semver::package::PackageReq; -use deno_semver::VersionReq; -use deno_semver::VersionReqSpecifierParseError; -use indexmap::IndexMap; -use sb_node::PackageJson; -use thiserror::Error; -#[derive(Debug, Error, Clone)] -pub enum PackageJsonDepValueParseError { - #[error(transparent)] - Specifier(#[from] VersionReqSpecifierParseError), - #[error("Not implemented scheme '{scheme}'")] - Unsupported { scheme: String }, +#[derive(Debug)] +pub struct InstallNpmWorkspacePkg { + pub alias: String, + pub pkg_dir: PathBuf, } -pub type PackageJsonDeps = IndexMap>; - +// todo(#24419): this is not correct, but it's good enough for now. +// We need deno_npm to be able to understand workspace packages and +// then have a way to properly lay them out on the file system #[derive(Debug, Default)] -pub struct PackageJsonDepsProvider(Option); - -impl PackageJsonDepsProvider { - pub fn new(deps: Option) -> Self { - Self(deps) - } +pub struct PackageJsonInstallDepsProvider { + remote_pkg_reqs: Vec, + workspace_pkgs: Vec, +} - pub fn deps(&self) -> Option<&PackageJsonDeps> { - self.0.as_ref() +impl PackageJsonInstallDepsProvider { + pub fn empty() -> Self { + Self::default() } - pub fn reqs(&self) -> Option> { - match &self.0 { - Some(deps) => { - let mut package_reqs = deps - .values() - .filter_map(|r| r.as_ref().ok()) - .collect::>(); - package_reqs.sort(); // deterministic resolution - Some(package_reqs) + pub fn from_workspace(workspace: &Arc) -> Self { + let mut workspace_pkgs = Vec::new(); + let mut remote_pkg_reqs = Vec::new(); + let workspace_npm_pkgs = workspace.npm_packages(); + for pkg_json in workspace.package_jsons() { + let deps = pkg_json.resolve_local_package_json_deps(); + let mut pkg_reqs = Vec::with_capacity(deps.len()); + for (alias, dep) in deps { + let Ok(dep) = dep else { + continue; + }; + match dep { + PackageJsonDepValue::Req(pkg_req) => { + if let Some(pkg) = workspace_npm_pkgs + .iter() + .find(|pkg| pkg.matches_req(&pkg_req)) + { + workspace_pkgs.push(InstallNpmWorkspacePkg { + alias, + pkg_dir: pkg.pkg_json.dir_path().to_path_buf(), + }); + } else { + pkg_reqs.push(pkg_req) + } + } + PackageJsonDepValue::Workspace(version_req) => { + if let Some(pkg) = workspace_npm_pkgs + .iter() + .find(|pkg| pkg.matches_name_and_version_req(&alias, &version_req)) + { + workspace_pkgs.push(InstallNpmWorkspacePkg { + alias, + pkg_dir: pkg.pkg_json.dir_path().to_path_buf(), + }); + } + } + } } - None => None, - } - } -} + // sort within each package + pkg_reqs.sort(); -/// Gets an application level package.json's npm package requirements. -/// -/// Note that this function is not general purpose. It is specifically for -/// parsing the application level package.json that the user has control -/// over. This is a design limitation to allow mapping these dependency -/// entries to npm specifiers which can then be used in the resolver. -pub fn get_local_package_json_version_reqs(package_json: &PackageJson) -> PackageJsonDeps { - fn parse_entry(key: &str, value: &str) -> Result { - if value.starts_with("workspace:") - || value.starts_with("file:") - || value.starts_with("git:") - || value.starts_with("http:") - || value.starts_with("https:") - { - return Err(PackageJsonDepValueParseError::Unsupported { - scheme: value.split(':').next().unwrap().to_string(), - }); + remote_pkg_reqs.extend(pkg_reqs); } - let (name, version_req) = parse_dep_entry_name_and_raw_version(key, value); - let result = VersionReq::parse_from_specifier(version_req); - match result { - Ok(version_req) => Ok(PackageReq { - name: name.to_string(), - version_req, - }), - Err(err) => Err(PackageJsonDepValueParseError::Specifier(err)), + remote_pkg_reqs.shrink_to_fit(); + workspace_pkgs.shrink_to_fit(); + Self { + remote_pkg_reqs, + workspace_pkgs, } } - fn insert_deps(deps: Option<&IndexMap>, result: &mut PackageJsonDeps) { - if let Some(deps) = deps { - for (key, value) in deps { - result - .entry(key.to_string()) - .or_insert_with(|| parse_entry(key, value)); - } - } + pub fn remote_pkg_reqs(&self) -> &Vec { + &self.remote_pkg_reqs } - let deps = package_json.dependencies.as_ref(); - let dev_deps = package_json.dev_dependencies.as_ref(); - let mut result = IndexMap::new(); - - // favors the deps over dev_deps - insert_deps(deps, &mut result); - insert_deps(dev_deps, &mut result); - - result + pub fn workspace_pkgs(&self) -> &Vec { + &self.workspace_pkgs + } } diff --git a/crates/npm/managed/registry.rs b/crates/npm/managed/registry.rs index b53baca16..988b7d635 100644 --- a/crates/npm/managed/registry.rs +++ b/crates/npm/managed/registry.rs @@ -2,69 +2,39 @@ use std::collections::HashMap; use std::collections::HashSet; -use std::fs; -use std::io::ErrorKind; -use std::path::PathBuf; use std::sync::Arc; use async_trait::async_trait; use deno_core::anyhow::anyhow; -use deno_core::anyhow::Context; -use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::futures::future::BoxFuture; use deno_core::futures::future::Shared; use deno_core::futures::FutureExt; use deno_core::parking_lot::Mutex; -use deno_core::serde_json; -use deno_core::url::Url; use deno_npm::registry::NpmPackageInfo; use deno_npm::registry::NpmRegistryApi; use deno_npm::registry::NpmRegistryPackageInfoLoadError; -use once_cell::sync::Lazy; use sb_core::cache::CacheSetting; -use sb_core::cache::CACHE_PERM; -use sb_core::util::fs::atomic_write_file; -use sb_core::util::http_util::HttpClient; use sb_core::util::sync::AtomicFlag; use super::cache::NpmCache; - -static NPM_REGISTRY_DEFAULT_URL: Lazy = Lazy::new(|| { - let env_var_name = "NPM_CONFIG_REGISTRY"; - if let Ok(registry_url) = std::env::var(env_var_name) { - // ensure there is a trailing slash for the directory - let registry_url = format!("{}/", registry_url.trim_end_matches('/')); - match Url::parse(®istry_url) { - Ok(url) => { - return url; - } - Err(err) => { - println!("Invalid {} environment variable: {:#}", env_var_name, err,); - } - } - } - - Url::parse("https://registry.npmjs.org").unwrap() -}); +use super::cache::RegistryInfoDownloader; #[derive(Debug)] pub struct CliNpmRegistryApi(Option>); impl CliNpmRegistryApi { - pub fn default_url() -> &'static Url { - &NPM_REGISTRY_DEFAULT_URL - } - - pub fn new(base_url: Url, cache: Arc, http_client: Arc) -> Self { + pub fn new( + cache: Arc, + registry_info_downloader: Arc, + ) -> Self { Self(Some(Arc::new(CliNpmRegistryApiInner { - base_url, cache, force_reload_flag: Default::default(), mem_cache: Default::default(), previously_reloaded_packages: Default::default(), - http_client, + registry_info_downloader, }))) } @@ -73,14 +43,6 @@ impl CliNpmRegistryApi { self.inner().clear_memory_cache(); } - pub fn get_cached_package_info(&self, name: &str) -> Option> { - self.inner().get_cached_package_info(name) - } - - pub fn base_url(&self) -> &Url { - &self.inner().base_url - } - fn inner(&self) -> &Arc { // this panicking indicates a bug in the code where this // wasn't initialized @@ -88,7 +50,7 @@ impl CliNpmRegistryApi { } } -#[async_trait] +#[async_trait(?Send)] impl NpmRegistryApi for CliNpmRegistryApi { async fn package_info( &self, @@ -104,20 +66,7 @@ impl NpmRegistryApi for CliNpmRegistryApi { } fn mark_force_reload(&self) -> bool { - // never force reload the registry information if reloading - // is disabled or if we're already reloading - if matches!( - self.inner().cache.cache_setting(), - CacheSetting::Only | CacheSetting::ReloadAll - ) { - return false; - } - if self.inner().force_reload_flag.raise() { - self.clear_memory_cache(); // clear the cache to force reloading - true - } else { - false - } + self.inner().mark_force_reload() } } @@ -131,12 +80,11 @@ enum CacheItem { #[derive(Debug)] struct CliNpmRegistryApiInner { - base_url: Url, cache: Arc, force_reload_flag: AtomicFlag, mem_cache: Mutex>, previously_reloaded_packages: Mutex>, - http_client: Arc, + registry_info_downloader: Arc, } impl CliNpmRegistryApiInner { @@ -152,30 +100,28 @@ impl CliNpmRegistryApiInner { } Some(CacheItem::Pending(future)) => (false, future.clone()), None => { - if (self.cache.cache_setting().should_use_for_npm_package(name) && !self.force_reload()) - // if this has been previously reloaded, then try loading from the - // file system cache - || !self.previously_reloaded_packages.lock().insert(name.to_string()) - { - // attempt to load from the file cache - if let Some(info) = self.load_file_cached_package_info(name) { - let result = Some(Arc::new(info)); - mem_cache.insert(name.to_string(), CacheItem::Resolved(result.clone())); - return Ok(result); - } - } - let future = { let api = self.clone(); let name = name.to_string(); async move { - api.load_package_info_from_registry(&name) - .await - .map(|info| info.map(Arc::new)) - .map_err(Arc::new) - } - .boxed() - .shared() + if (api.cache.cache_setting().should_use_for_npm_package(&name) && !api.force_reload_flag.is_raised()) + // if this has been previously reloaded, then try loading from the + // file system cache + || !api.previously_reloaded_packages.lock().insert(name.to_string()) + { + // attempt to load from the file cache + if let Some(info) = api.load_file_cached_package_info(&name).await { + let result = Some(Arc::new(info)); + return Ok(result); + } + } + api.registry_info_downloader + .load_package_info(&name) + .await + .map_err(Arc::new) + } + .boxed() + .shared() }; mem_cache.insert(name.to_string(), CacheItem::Pending(future.clone())); (true, future) @@ -203,12 +149,32 @@ impl CliNpmRegistryApiInner { } } - fn force_reload(&self) -> bool { - self.force_reload_flag.is_raised() + fn mark_force_reload(&self) -> bool { + // never force reload the registry information if reloading + // is disabled or if we're already reloading + if matches!( + self.cache.cache_setting(), + CacheSetting::Only | CacheSetting::ReloadAll + ) { + return false; + } + if self.force_reload_flag.raise() { + self.clear_memory_cache(); + true + } else { + false + } } - fn load_file_cached_package_info(&self, name: &str) -> Option { - match self.load_file_cached_package_info_result(name) { + async fn load_file_cached_package_info(&self, name: &str) -> Option { + let result = deno_core::unsync::spawn_blocking({ + let cache = self.cache.clone(); + let name = name.to_string(); + move || cache.load_package_info(&name) + }) + .await + .unwrap(); + match result { Ok(value) => value, Err(err) => { if cfg!(debug_assertions) { @@ -220,126 +186,7 @@ impl CliNpmRegistryApiInner { } } - fn load_file_cached_package_info_result( - &self, - name: &str, - ) -> Result, AnyError> { - let file_cache_path = self.get_package_file_cache_path(name); - let file_text = match fs::read_to_string(file_cache_path) { - Ok(file_text) => file_text, - Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), - Err(err) => return Err(err.into()), - }; - match serde_json::from_str(&file_text) { - Ok(package_info) => Ok(Some(package_info)), - Err(err) => { - // This scenario might mean we need to load more data from the - // npm registry than before. So, just debug log while in debug - // rather than panic. - log::debug!( - "error deserializing registry.json for '{}'. Reloading. {:?}", - name, - err - ); - Ok(None) - } - } - } - - fn save_package_info_to_file_cache(&self, name: &str, package_info: &NpmPackageInfo) { - if let Err(err) = self.save_package_info_to_file_cache_result(name, package_info) { - if cfg!(debug_assertions) { - panic!("error saving cached npm package info for {name}: {err:#}"); - } - } - } - - fn save_package_info_to_file_cache_result( - &self, - name: &str, - package_info: &NpmPackageInfo, - ) -> Result<(), AnyError> { - let file_cache_path = self.get_package_file_cache_path(name); - let file_text = serde_json::to_string(&package_info)?; - atomic_write_file(&file_cache_path, file_text, CACHE_PERM)?; - Ok(()) - } - - async fn load_package_info_from_registry( - &self, - name: &str, - ) -> Result, AnyError> { - self.load_package_info_from_registry_inner(name) - .await - .with_context(|| { - format!( - "Error getting response at {} for package \"{}\"", - self.get_package_url(name), - name - ) - }) - } - - async fn load_package_info_from_registry_inner( - &self, - name: &str, - ) -> Result, AnyError> { - if *self.cache.cache_setting() == CacheSetting::Only { - return Err(custom_error( - "NotCached", - format!( - "An npm specifier not found in cache: \"{name}\", --cached-only is specified." - ), - )); - } - - let package_url = self.get_package_url(name); - - let maybe_bytes = self.http_client.download_with_progress(package_url).await?; - match maybe_bytes { - Some(bytes) => { - let package_info = serde_json::from_slice(&bytes)?; - self.save_package_info_to_file_cache(name, &package_info); - Ok(Some(package_info)) - } - None => Ok(None), - } - } - - fn get_package_url(&self, name: &str) -> Url { - // list of all characters used in npm packages: - // !, ', (, ), *, -, ., /, [0-9], @, [A-Za-z], _, ~ - const ASCII_SET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC - .remove(b'!') - .remove(b'\'') - .remove(b'(') - .remove(b')') - .remove(b'*') - .remove(b'-') - .remove(b'.') - .remove(b'/') - .remove(b'@') - .remove(b'_') - .remove(b'~'); - let name = percent_encoding::utf8_percent_encode(name, &ASCII_SET); - self.base_url.join(&name.to_string()).unwrap() - } - - fn get_package_file_cache_path(&self, name: &str) -> PathBuf { - let name_folder_path = self.cache.package_name_folder(name, &self.base_url); - name_folder_path.join("registry.json") - } - fn clear_memory_cache(&self) { self.mem_cache.lock().clear(); } - - pub fn get_cached_package_info(&self, name: &str) -> Option> { - let mem_cache = self.mem_cache.lock(); - if let Some(CacheItem::Resolved(maybe_info)) = mem_cache.get(name) { - maybe_info.clone() - } else { - None - } - } } diff --git a/crates/npm/managed/resolution.rs b/crates/npm/managed/resolution.rs index bf0c977de..64791ef95 100644 --- a/crates/npm/managed/resolution.rs +++ b/crates/npm/managed/resolution.rs @@ -6,13 +6,10 @@ use std::sync::Arc; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; -use deno_core::parking_lot::RwLock; +use deno_lockfile::Lockfile; use deno_lockfile::NpmPackageDependencyLockfileInfo; use deno_lockfile::NpmPackageLockfileInfo; -use deno_npm::registry::NpmPackageInfo; -use deno_npm::registry::NpmPackageVersionDistInfoIntegrity; use deno_npm::registry::NpmRegistryApi; -use deno_npm::resolution::NpmPackageVersionResolutionError; use deno_npm::resolution::NpmPackagesPartitioned; use deno_npm::resolution::NpmResolutionError; use deno_npm::resolution::NpmResolutionSnapshot; @@ -32,8 +29,17 @@ use deno_semver::package::PackageReq; use deno_semver::VersionReq; use super::registry::CliNpmRegistryApi; -use deno_lockfile::Lockfile; -use sb_core::util::sync::TaskQueue; +use sb_core::util::sync::SyncReadAsyncWriteLock; + +pub struct AddPkgReqsResult { + /// Results from adding the individual packages. + /// + /// The indexes of the results correspond to the indexes of the provided + /// package requirements. + pub results: Vec>, + /// The final result of resolving and caching all the package requirements. + pub dependencies_result: Result<(), AnyError>, +} /// Handles updating and storing npm resolution in memory where the underlying /// snapshot can be updated concurrently. Additionally handles updating the lockfile @@ -42,8 +48,7 @@ use sb_core::util::sync::TaskQueue; /// This does not interact with the file system. pub struct NpmResolution { api: Arc, - snapshot: RwLock, - update_queue: TaskQueue, + snapshot: SyncReadAsyncWriteLock, maybe_lockfile: Option>>, } @@ -73,30 +78,37 @@ impl NpmResolution { ) -> Self { Self { api, - snapshot: RwLock::new(initial_snapshot), - update_queue: Default::default(), + snapshot: SyncReadAsyncWriteLock::new(initial_snapshot), maybe_lockfile, } } - pub async fn add_package_reqs(&self, package_reqs: &[PackageReq]) -> Result<(), AnyError> { + pub async fn add_package_reqs(&self, package_reqs: &[PackageReq]) -> AddPkgReqsResult { // only allow one thread in here at a time - let _permit = self.update_queue.acquire().await; - let snapshot = add_package_reqs_to_snapshot( + let snapshot_lock = self.snapshot.acquire().await; + let result = add_package_reqs_to_snapshot( &self.api, package_reqs, self.maybe_lockfile.clone(), - || self.snapshot.read().clone(), + || snapshot_lock.read().clone(), ) - .await?; - - *self.snapshot.write() = snapshot; - Ok(()) + .await; + + AddPkgReqsResult { + results: result.results, + dependencies_result: match result.dep_graph_result { + Ok(snapshot) => { + *snapshot_lock.write() = snapshot; + Ok(()) + } + Err(err) => Err(err.into()), + }, + } } pub async fn set_package_reqs(&self, package_reqs: &[PackageReq]) -> Result<(), AnyError> { // only allow one thread in here at a time - let _permit = self.update_queue.acquire().await; + let snapshot_lock = self.snapshot.acquire().await; let reqs_set = package_reqs.iter().collect::>(); let snapshot = add_package_reqs_to_snapshot( @@ -104,7 +116,7 @@ impl NpmResolution { package_reqs, self.maybe_lockfile.clone(), || { - let snapshot = self.snapshot.read().clone(); + let snapshot = snapshot_lock.read().clone(); let has_removed_package = !snapshot .package_reqs() .keys() @@ -117,26 +129,10 @@ impl NpmResolution { } }, ) - .await?; - - *self.snapshot.write() = snapshot; - - Ok(()) - } - - pub async fn resolve_pending(&self) -> Result<(), AnyError> { - // only allow one thread in here at a time - let _permit = self.update_queue.acquire().await; - - let snapshot = add_package_reqs_to_snapshot( - &self.api, - &Vec::new(), - self.maybe_lockfile.clone(), - || self.snapshot.read().clone(), - ) - .await?; + .await + .into_result()?; - *self.snapshot.write() = snapshot; + *snapshot_lock.write() = snapshot; Ok(()) } @@ -205,37 +201,6 @@ impl NpmResolution { .map(|pkg| pkg.id.clone()) } - /// Resolves a package requirement for deno graph. This should only be - /// called by deno_graph's NpmResolver or for resolving packages in - /// a package.json - pub fn resolve_pkg_req_as_pending( - &self, - pkg_req: &PackageReq, - ) -> Result { - // we should always have this because it should have been cached before here - let package_info = self.api.get_cached_package_info(&pkg_req.name).unwrap(); - self.resolve_pkg_req_as_pending_with_info(pkg_req, &package_info) - } - - /// Resolves a package requirement for deno graph. This should only be - /// called by deno_graph's NpmResolver or for resolving packages in - /// a package.json - pub fn resolve_pkg_req_as_pending_with_info( - &self, - pkg_req: &PackageReq, - package_info: &NpmPackageInfo, - ) -> Result { - debug_assert_eq!(pkg_req.name, package_info.name); - let mut snapshot = self.snapshot.write(); - let pending_resolver = get_npm_pending_resolver(&self.api); - let nv = pending_resolver.resolve_package_req_as_pending( - &mut snapshot, - pkg_req, - package_info, - )?; - Ok(nv) - } - pub fn package_reqs(&self) -> HashMap { self.snapshot.read().package_reqs().clone() } @@ -269,11 +234,6 @@ impl NpmResolution { .read() .as_valid_serialized_for_system(system_info) } - - pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> { - let snapshot = self.snapshot.read(); - populate_lockfile_from_snapshot(lockfile, &snapshot) - } } async fn add_package_reqs_to_snapshot( @@ -281,46 +241,49 @@ async fn add_package_reqs_to_snapshot( package_reqs: &[PackageReq], maybe_lockfile: Option>>, get_new_snapshot: impl Fn() -> NpmResolutionSnapshot, -) -> Result { +) -> deno_npm::resolution::AddPkgReqsResult { let snapshot = get_new_snapshot(); - let snapshot = if !snapshot.has_pending() - && package_reqs - .iter() - .all(|req| snapshot.package_reqs().contains_key(req)) + if package_reqs + .iter() + .all(|req| snapshot.package_reqs().contains_key(req)) { - log::debug!("Snapshot already up to date. Skipping pending resolution."); - snapshot - } else { - let pending_resolver = get_npm_pending_resolver(api); - let result = pending_resolver - .resolve_pending(snapshot, package_reqs) - .await; - api.clear_memory_cache(); - match result { - Ok(snapshot) => snapshot, - Err(NpmResolutionError::Resolution(err)) if api.mark_force_reload() => { - log::debug!("{err:#}"); - log::debug!("npm resolution failed. Trying again..."); - - // try again - let snapshot = get_new_snapshot(); - let result = pending_resolver - .resolve_pending(snapshot, package_reqs) - .await; - api.clear_memory_cache(); - // now surface the result after clearing the cache - result? - } - Err(err) => return Err(err.into()), + log::debug!("Snapshot already up to date. Skipping npm resolution."); + return deno_npm::resolution::AddPkgReqsResult { + results: package_reqs + .iter() + .map(|req| Ok(snapshot.package_reqs().get(req).unwrap().clone())) + .collect(), + dep_graph_result: Ok(snapshot), + }; + } + log::debug!( + /* this string is used in tests */ + "Running npm resolution." + ); + let pending_resolver = get_npm_pending_resolver(api); + let result = pending_resolver.add_pkg_reqs(snapshot, package_reqs).await; + api.clear_memory_cache(); + let result = match &result.dep_graph_result { + Err(NpmResolutionError::Resolution(err)) if api.mark_force_reload() => { + log::debug!("{err:#}"); + log::debug!("npm resolution failed. Trying again..."); + + // try again + let snapshot = get_new_snapshot(); + let result = pending_resolver.add_pkg_reqs(snapshot, package_reqs).await; + api.clear_memory_cache(); + result } + _ => result, }; - if let Some(lockfile_mutex) = maybe_lockfile { - let mut lockfile = lockfile_mutex.lock(); - populate_lockfile_from_snapshot(&mut lockfile, &snapshot)?; + if let Ok(snapshot) = &result.dep_graph_result { + if let Some(lockfile) = maybe_lockfile { + populate_lockfile_from_snapshot(&lockfile, snapshot); + } } - Ok(snapshot) + result } fn get_npm_pending_resolver( @@ -334,10 +297,8 @@ fn get_npm_pending_resolver( }) } -fn populate_lockfile_from_snapshot( - lockfile: &mut Lockfile, - snapshot: &NpmResolutionSnapshot, -) -> Result<(), AnyError> { +fn populate_lockfile_from_snapshot(lockfile: &Mutex, snapshot: &NpmResolutionSnapshot) { + let mut lockfile = lockfile.lock(); for (package_req, nv) in snapshot.package_reqs() { lockfile.insert_package_specifier( format!("npm:{}", package_req), @@ -352,25 +313,11 @@ fn populate_lockfile_from_snapshot( ); } for package in snapshot.all_packages_for_every_system() { - lockfile.check_or_insert_npm_package(npm_package_to_lockfile_info(package))?; + lockfile.insert_npm_package(npm_package_to_lockfile_info(package)); } - Ok(()) } fn npm_package_to_lockfile_info(pkg: &NpmResolutionPackage) -> NpmPackageLockfileInfo { - fn integrity_for_lockfile(integrity: NpmPackageVersionDistInfoIntegrity) -> String { - match integrity { - NpmPackageVersionDistInfoIntegrity::Integrity { - algorithm, - base64_hash, - } => format!("{}-{}", algorithm, base64_hash), - NpmPackageVersionDistInfoIntegrity::UnknownIntegrity(integrity) => { - integrity.to_string() - } - NpmPackageVersionDistInfoIntegrity::LegacySha1Hex(hex) => hex.to_string(), - } - } - let dependencies = pkg .dependencies .iter() @@ -381,9 +328,8 @@ fn npm_package_to_lockfile_info(pkg: &NpmResolutionPackage) -> NpmPackageLockfil .collect(); NpmPackageLockfileInfo { - display_id: pkg.id.nv.to_string(), serialized_id: pkg.id.as_serialized(), - integrity: integrity_for_lockfile(pkg.dist.integrity()), + integrity: pkg.dist.integrity().for_lockfile(), dependencies, } } diff --git a/crates/npm/managed/resolvers/common.rs b/crates/npm/managed/resolvers/common.rs index 21ed8d49c..ce8fdfc5a 100644 --- a/crates/npm/managed/resolvers/common.rs +++ b/crates/npm/managed/resolvers/common.rs @@ -9,21 +9,23 @@ use std::sync::Mutex; use async_trait::async_trait; use deno_ast::ModuleSpecifier; +use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures; -use deno_core::unsync::spawn; +use deno_core::futures::StreamExt; use deno_core::url::Url; use deno_fs::FileSystem; use deno_npm::NpmPackageCacheFolderId; use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; + +use sb_node::errors::PackageFolderResolveError; use sb_node::NodePermissions; -use sb_node::NodeResolutionMode; -use super::super::cache::NpmCache; +use crate::managed::cache::TarballCache; /// Part of the resolution that interacts with the file system. -#[async_trait] +#[async_trait(?Send)] pub trait NpmPackageFsResolver: Send + Sync { /// Specifier for the root directory. fn root_dir_url(&self) -> &Url; @@ -31,19 +33,22 @@ pub trait NpmPackageFsResolver: Send + Sync { /// The local node_modules folder if it is applicable to the implementation. fn node_modules_path(&self) -> Option<&PathBuf>; - fn package_folder(&self, package_id: &NpmPackageId) -> Result; + fn maybe_package_folder(&self, package_id: &NpmPackageId) -> Option; + + fn package_folder(&self, package_id: &NpmPackageId) -> Result { + self.maybe_package_folder(package_id).ok_or_else(|| { + deno_core::anyhow::anyhow!( + "Package folder not found for '{}'", + package_id.as_serialized() + ) + }) + } fn resolve_package_folder_from_package( &self, name: &str, referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result; - - fn resolve_package_folder_from_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Result, AnyError>; + ) -> Result; fn resolve_package_cache_folder_id_from_specifier( &self, @@ -54,7 +59,7 @@ pub trait NpmPackageFsResolver: Send + Sync { fn ensure_read_permission( &self, - permissions: &dyn NodePermissions, + permissions: &mut dyn NodePermissions, path: &Path, ) -> Result<(), AnyError>; } @@ -77,7 +82,7 @@ impl RegistryReadPermissionChecker { pub fn ensure_registry_read_permission( &self, - permissions: &dyn NodePermissions, + permissions: &mut dyn NodePermissions, path: &Path, ) -> Result<(), AnyError> { // allow reading if it's in the node_modules @@ -88,30 +93,31 @@ impl RegistryReadPermissionChecker { if is_path_in_node_modules { let mut cache = self.cache.lock().unwrap(); - let registry_path_canon = match cache.get(&self.registry_path) { - Some(canon) => canon.clone(), - None => { - let canon = self.fs.realpath_sync(&self.registry_path)?; - cache.insert(self.registry_path.to_path_buf(), canon.clone()); - canon - } - }; - - let path_canon = match cache.get(path) { - Some(canon) => canon.clone(), - None => { - let canon = self.fs.realpath_sync(path); - if let Err(e) = &canon { - if e.kind() == ErrorKind::NotFound { - return Ok(()); + let mut canonicalize = |path: &Path| -> Result, AnyError> { + match cache.get(path) { + Some(canon) => Ok(Some(canon.clone())), + None => match self.fs.realpath_sync(path) { + Ok(canon) => { + cache.insert(path.to_path_buf(), canon.clone()); + Ok(Some(canon)) } - } - - let canon = canon?; - cache.insert(path.to_path_buf(), canon.clone()); - canon + Err(e) => { + if e.kind() == ErrorKind::NotFound { + return Ok(None); + } + Err(AnyError::from(e)).with_context(|| { + format!("failed canonicalizing '{}'", path.display()) + }) + } + }, } }; + let Some(registry_path_canon) = canonicalize(&self.registry_path)? else { + return Ok(()); // not exists, allow reading + }; + let Some(path_canon) = canonicalize(path)? else { + return Ok(()); // not exists, allow reading + }; if path_canon.starts_with(registry_path_canon) { return Ok(()); @@ -125,24 +131,19 @@ impl RegistryReadPermissionChecker { /// Caches all the packages in parallel. pub async fn cache_packages( packages: Vec, - cache: &Arc, - registry_url: &Url, + tarball_cache: &Arc, ) -> Result<(), AnyError> { - let mut handles = Vec::with_capacity(packages.len()); + let mut futures_unordered = futures::stream::FuturesUnordered::new(); for package in packages { - let cache = cache.clone(); - let registry_url = registry_url.clone(); - let handle = spawn(async move { - cache - .ensure_package(&package.id.nv, &package.dist, ®istry_url) + futures_unordered.push(async move { + tarball_cache + .ensure_package(&package.id.nv, &package.dist) .await }); - handles.push(handle); } - let results = futures::future::join_all(handles).await; - for result in results { + while let Some(result) = futures_unordered.next().await { // surface the first error - result??; + result?; } Ok(()) } diff --git a/crates/npm/managed/resolvers/global.rs b/crates/npm/managed/resolvers/global.rs index 171518428..d3c52a7c4 100644 --- a/crates/npm/managed/resolvers/global.rs +++ b/crates/npm/managed/resolvers/global.rs @@ -8,20 +8,19 @@ use std::sync::Arc; use async_trait::async_trait; use deno_ast::ModuleSpecifier; -use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::url::Url; use deno_fs::FileSystem; -use deno_npm::resolution::PackageNotFoundFromReferrerError; use deno_npm::NpmPackageCacheFolderId; use deno_npm::NpmPackageId; -use deno_npm::NpmResolutionPackage; use deno_npm::NpmSystemInfo; + +use sb_node::errors::PackageFolderResolveError; +use sb_node::errors::PackageFolderResolveErrorKind; use sb_node::NodePermissions; -use sb_node::NodeResolutionMode; -use super::super::super::common::types_package_name; use super::super::cache::NpmCache; +use super::super::cache::TarballCache; use super::super::resolution::NpmResolution; use super::common::cache_packages; use super::common::NpmPackageFsResolver; @@ -31,44 +30,34 @@ use super::common::RegistryReadPermissionChecker; #[derive(Debug)] pub struct GlobalNpmPackageResolver { cache: Arc, + tarball_cache: Arc, resolution: Arc, - registry_url: Url, system_info: NpmSystemInfo, registry_read_permission_checker: RegistryReadPermissionChecker, } impl GlobalNpmPackageResolver { pub fn new( - fs: Arc, cache: Arc, - registry_url: Url, + fs: Arc, + tarball_cache: Arc, resolution: Arc, system_info: NpmSystemInfo, ) -> Self { Self { - cache: cache.clone(), - resolution, - registry_url: registry_url.clone(), - system_info, registry_read_permission_checker: RegistryReadPermissionChecker::new( fs, - cache.registry_folder(®istry_url), + cache.root_folder(), ), + cache, + tarball_cache, + resolution, + system_info, } } - - fn resolve_types_package( - &self, - package_name: &str, - referrer_pkg_id: &NpmPackageCacheFolderId, - ) -> Result> { - let types_name = types_package_name(package_name); - self.resolution - .resolve_package_from_package(&types_name, referrer_pkg_id) - } } -#[async_trait] +#[async_trait(?Send)] impl NpmPackageFsResolver for GlobalNpmPackageResolver { fn root_dir_url(&self) -> &Url { self.cache.root_dir_url() @@ -78,57 +67,65 @@ impl NpmPackageFsResolver for GlobalNpmPackageResolver { None } - fn package_folder(&self, id: &NpmPackageId) -> Result { + fn maybe_package_folder(&self, id: &NpmPackageId) -> Option { let folder_id = self .resolution - .resolve_pkg_cache_folder_id_from_pkg_id(id) - .unwrap(); - Ok(self - .cache - .package_folder_for_id(&folder_id, &self.registry_url)) + .resolve_pkg_cache_folder_id_from_pkg_id(id)?; + Some(self.cache.package_folder_for_id(&folder_id)) } fn resolve_package_folder_from_package( &self, name: &str, referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result { - let Some(referrer_pkg_id) = self + ) -> Result { + use deno_npm::resolution::PackageNotFoundFromReferrerError; + let Some(referrer_cache_folder_id) = self .cache - .resolve_package_folder_id_from_specifier(referrer, &self.registry_url) + .resolve_package_folder_id_from_specifier(referrer) else { - bail!("could not find npm package for '{}'", referrer); - }; - let pkg = if mode.is_types() && !name.starts_with("@types/") { - // attempt to resolve the types package first, then fallback to the regular package - match self.resolve_types_package(name, &referrer_pkg_id) { - Ok(pkg) => pkg, - Err(_) => self - .resolution - .resolve_package_from_package(name, &referrer_pkg_id)?, + return Err(PackageFolderResolveErrorKind::NotFoundReferrer { + referrer: referrer.clone(), + referrer_extra: None, } - } else { - self.resolution - .resolve_package_from_package(name, &referrer_pkg_id)? + .into()); }; - self.package_folder(&pkg.id) - } - - fn resolve_package_folder_from_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Result, AnyError> { - let Some(pkg_folder_id) = self - .cache - .resolve_package_folder_id_from_specifier(specifier, &self.registry_url) - else { - return Ok(None); - }; - Ok(Some( - self.cache - .package_folder_for_id(&pkg_folder_id, &self.registry_url), - )) + let resolve_result = self + .resolution + .resolve_package_from_package(name, &referrer_cache_folder_id); + match resolve_result { + Ok(pkg) => match self.maybe_package_folder(&pkg.id) { + Some(folder) => Ok(folder), + None => Err(PackageFolderResolveErrorKind::NotFoundPackage { + package_name: name.to_string(), + referrer: referrer.clone(), + referrer_extra: Some(format!( + "{} -> {}", + referrer_cache_folder_id, + pkg.id.as_serialized() + )), + } + .into()), + }, + Err(err) => match *err { + PackageNotFoundFromReferrerError::Referrer(cache_folder_id) => { + Err(PackageFolderResolveErrorKind::NotFoundReferrer { + referrer: referrer.clone(), + referrer_extra: Some(cache_folder_id.to_string()), + } + .into()) + } + PackageNotFoundFromReferrerError::Package { + name, + referrer: cache_folder_id_referrer, + } => Err(PackageFolderResolveErrorKind::NotFoundPackage { + package_name: name, + referrer: referrer.clone(), + referrer_extra: Some(cache_folder_id_referrer.to_string()), + } + .into()), + }, + } } fn resolve_package_cache_folder_id_from_specifier( @@ -137,7 +134,7 @@ impl NpmPackageFsResolver for GlobalNpmPackageResolver { ) -> Result, AnyError> { Ok(self .cache - .resolve_package_folder_id_from_specifier(specifier, &self.registry_url)) + .resolve_package_folder_id_from_specifier(specifier)) } async fn cache_packages(&self) -> Result<(), AnyError> { @@ -145,12 +142,12 @@ impl NpmPackageFsResolver for GlobalNpmPackageResolver { .resolution .all_system_packages_partitioned(&self.system_info); - cache_packages(package_partitions.packages, &self.cache, &self.registry_url).await?; + cache_packages(package_partitions.packages, &self.tarball_cache).await?; // create the copy package folders for copy in package_partitions.copy_packages { self.cache - .ensure_copy_package(©.get_package_cache_folder_id(), &self.registry_url)?; + .ensure_copy_package(©.get_package_cache_folder_id())?; } Ok(()) @@ -158,7 +155,7 @@ impl NpmPackageFsResolver for GlobalNpmPackageResolver { fn ensure_read_permission( &self, - permissions: &dyn NodePermissions, + permissions: &mut dyn NodePermissions, path: &Path, ) -> Result<(), AnyError> { self.registry_read_permission_checker diff --git a/crates/npm/managed/resolvers/local.rs b/crates/npm/managed/resolvers/local.rs index 571cc7551..af54747df 100644 --- a/crates/npm/managed/resolvers/local.rs +++ b/crates/npm/managed/resolvers/local.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use std::cmp::Ordering; +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::fs; @@ -11,15 +12,12 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; -use crate::cache_dir::{mixed_case_package_name_decode, mixed_case_package_name_encode}; use async_trait::async_trait; use deno_ast::ModuleSpecifier; -use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; -use deno_core::futures; -use deno_core::unsync::spawn; -use deno_core::unsync::JoinHandle; +use deno_core::futures::stream::FuturesUnordered; +use deno_core::futures::StreamExt; use deno_core::url::Url; use deno_fs; use deno_npm::resolution::NpmResolutionSnapshot; @@ -28,18 +26,25 @@ use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; use deno_npm::NpmSystemInfo; use deno_semver::package::PackageNv; -use sb_core::cache::CACHE_PERM; -use sb_core::util::fs::{ - atomic_write_file, canonicalize_path_maybe_not_exists_with_fs, copy_dir_recursive, - hard_link_dir_recursive, symlink_dir, LaxSingleProcessFsFlag, -}; -use sb_node::NodePermissions; -use sb_node::NodeResolutionMode; use serde::Deserialize; use serde::Serialize; -use super::super::super::common::types_package_name; +use crate::cache_dir::mixed_case_package_name_decode; +use crate::cache_dir::mixed_case_package_name_encode; +use crate::managed::PackageJsonInstallDepsProvider; + +use sb_core::cache::CACHE_PERM; +use sb_core::util::fs::atomic_write_file_with_retries; +use sb_core::util::fs::canonicalize_path_maybe_not_exists_with_fs; +use sb_core::util::fs::clone_dir_recursive; +use sb_core::util::fs::symlink_dir; +use sb_core::util::fs::LaxSingleProcessFsFlag; +use sb_node::errors::PackageFolderResolveError; +use sb_node::errors::PackageFolderResolveErrorKind; +use sb_node::NodePermissions; + use super::super::cache::NpmCache; +use super::super::cache::TarballCache; use super::super::resolution::NpmResolution; use super::common::NpmPackageFsResolver; use super::common::RegistryReadPermissionChecker; @@ -48,10 +53,11 @@ use super::common::RegistryReadPermissionChecker; /// and resolves packages from it. #[derive(Debug)] pub struct LocalNpmPackageResolver { - fs: Arc, cache: Arc, + fs: Arc, + pkg_json_deps_provider: Arc, resolution: Arc, - registry_url: Url, + tarball_cache: Arc, root_node_modules_path: PathBuf, root_node_modules_url: Url, system_info: NpmSystemInfo, @@ -59,26 +65,29 @@ pub struct LocalNpmPackageResolver { } impl LocalNpmPackageResolver { + #[allow(clippy::too_many_arguments)] pub fn new( - fs: Arc, cache: Arc, - registry_url: Url, - node_modules_folder: PathBuf, + fs: Arc, + pkg_json_deps_provider: Arc, resolution: Arc, + tarball_cache: Arc, + node_modules_folder: PathBuf, system_info: NpmSystemInfo, ) -> Self { Self { - fs: fs.clone(), cache, + fs: fs.clone(), + pkg_json_deps_provider, resolution, - registry_url, - root_node_modules_url: Url::from_directory_path(&node_modules_folder).unwrap(), - root_node_modules_path: node_modules_folder.clone(), - system_info, + tarball_cache, registry_read_permission_checker: RegistryReadPermissionChecker::new( fs, - node_modules_folder, + node_modules_folder.clone(), ), + root_node_modules_url: Url::from_directory_path(&node_modules_folder).unwrap(), + root_node_modules_path: node_modules_folder, + system_info, } } @@ -97,7 +106,7 @@ impl LocalNpmPackageResolver { fn resolve_folder_for_specifier( &self, specifier: &ModuleSpecifier, - ) -> Result, AnyError> { + ) -> Result, std::io::Error> { let Some(relative_url) = self.root_node_modules_url.make_relative(specifier) else { return Ok(None); }; @@ -110,13 +119,22 @@ impl LocalNpmPackageResolver { }; // Canonicalize the path so it's not pointing to the symlinked directory // in `node_modules` directory of the referrer. - canonicalize_path_maybe_not_exists_with_fs(&path, self.fs.as_ref()) - .map(Some) - .map_err(|err| err.into()) + canonicalize_path_maybe_not_exists_with_fs(&path, self.fs.as_ref()).map(Some) + } + + fn resolve_package_folder_from_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Result, AnyError> { + let Some(local_path) = self.resolve_folder_for_specifier(specifier)? else { + return Ok(None); + }; + let package_root_path = self.resolve_package_root(&local_path); + Ok(Some(package_root_path)) } } -#[async_trait] +#[async_trait(?Send)] impl NpmPackageFsResolver for LocalNpmPackageResolver { fn root_dir_url(&self) -> &Url { &self.root_node_modules_url @@ -126,74 +144,66 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver { Some(&self.root_node_modules_path) } - fn package_folder(&self, id: &NpmPackageId) -> Result { - match self.resolution.resolve_pkg_cache_folder_id_from_pkg_id(id) { - // package is stored at: - // node_modules/.deno//node_modules/ - Some(cache_folder_id) => Ok(self - .root_node_modules_path + fn maybe_package_folder(&self, id: &NpmPackageId) -> Option { + let cache_folder_id = self + .resolution + .resolve_pkg_cache_folder_id_from_pkg_id(id)?; + // package is stored at: + // node_modules/.deno//node_modules/ + Some( + self.root_node_modules_path .join(".deno") .join(get_package_folder_id_folder_name(&cache_folder_id)) .join("node_modules") - .join(&cache_folder_id.nv.name)), - None => bail!( - "Could not find package information for '{}'", - id.as_serialized() - ), - } + .join(&cache_folder_id.nv.name), + ) } fn resolve_package_folder_from_package( &self, name: &str, referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result { - let Some(local_path) = self.resolve_folder_for_specifier(referrer)? else { - bail!("could not find npm package for '{}'", referrer); + ) -> Result { + let maybe_local_path = self.resolve_folder_for_specifier(referrer).map_err(|err| { + PackageFolderResolveErrorKind::Io { + package_name: name.to_string(), + referrer: referrer.clone(), + source: err, + } + })?; + let Some(local_path) = maybe_local_path else { + return Err(PackageFolderResolveErrorKind::NotFoundReferrer { + referrer: referrer.clone(), + referrer_extra: None, + } + .into()); }; let package_root_path = self.resolve_package_root(&local_path); let mut current_folder = package_root_path.as_path(); - loop { - current_folder = current_folder.parent().unwrap(); + while let Some(parent_folder) = current_folder.parent() { + current_folder = parent_folder; let node_modules_folder = if current_folder.ends_with("node_modules") { Cow::Borrowed(current_folder) } else { Cow::Owned(current_folder.join("node_modules")) }; - // attempt to resolve the types package first, then fallback to the regular package - if mode.is_types() && !name.starts_with("@types/") { - let sub_dir = join_package_name(&node_modules_folder, &types_package_name(name)); - if self.fs.is_dir_sync(&sub_dir) { - return Ok(sub_dir); - } - } - let sub_dir = join_package_name(&node_modules_folder, name); if self.fs.is_dir_sync(&sub_dir) { return Ok(sub_dir); } if current_folder == self.root_node_modules_path { - bail!( - "could not find package '{}' from referrer '{}'.", - name, - referrer - ); + break; } } - } - fn resolve_package_folder_from_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Result, AnyError> { - let Some(local_path) = self.resolve_folder_for_specifier(specifier)? else { - return Ok(None); - }; - let package_root_path = self.resolve_package_root(&local_path); - Ok(Some(package_root_path)) + Err(PackageFolderResolveErrorKind::NotFoundPackage { + package_name: name.to_string(), + referrer: referrer.clone(), + referrer_extra: None, + } + .into()) } fn resolve_package_cache_folder_id_from_specifier( @@ -211,7 +221,8 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver { sync_resolution_with_fs( &self.resolution.snapshot(), &self.cache, - &self.registry_url, + &self.pkg_json_deps_provider, + &self.tarball_cache, &self.root_node_modules_path, &self.system_info, ) @@ -220,7 +231,7 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver { fn ensure_read_permission( &self, - permissions: &dyn NodePermissions, + permissions: &mut dyn NodePermissions, path: &Path, ) -> Result<(), AnyError> { self.registry_read_permission_checker @@ -229,14 +240,16 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver { } /// Creates a pnpm style folder structure. +#[allow(clippy::too_many_arguments)] async fn sync_resolution_with_fs( snapshot: &NpmResolutionSnapshot, cache: &Arc, - registry_url: &Url, + pkg_json_deps_provider: &PackageJsonInstallDepsProvider, + tarball_cache: &Arc, root_node_modules_dir_path: &Path, system_info: &NpmSystemInfo, ) -> Result<(), AnyError> { - if snapshot.is_empty() { + if snapshot.is_empty() && pkg_json_deps_provider.workspace_pkgs().is_empty() { return Ok(()); // don't create the directory } @@ -244,6 +257,9 @@ async fn sync_resolution_with_fs( let deno_node_modules_dir = deno_local_registry_dir.join("node_modules"); fs::create_dir_all(&deno_node_modules_dir) .with_context(|| format!("Creating '{}'", deno_local_registry_dir.display()))?; + let bin_node_modules_dir_path = root_node_modules_dir_path.join(".bin"); + fs::create_dir_all(&bin_node_modules_dir_path) + .with_context(|| format!("Creating '{}'", bin_node_modules_dir_path.display()))?; let single_process_lock = LaxSingleProcessFsFlag::lock( deno_local_registry_dir.join(".deno.lock"), @@ -260,10 +276,10 @@ async fn sync_resolution_with_fs( // Copy (hardlink in future) // to // node_modules/.deno//node_modules/ let package_partitions = snapshot.all_system_packages_partitioned(system_info); - let mut handles: Vec>> = - Vec::with_capacity(package_partitions.packages.len()); + let mut cache_futures = FuturesUnordered::new(); let mut newest_packages_by_name: HashMap<&String, &NpmResolutionPackage> = HashMap::with_capacity(package_partitions.packages.len()); + for package in &package_partitions.packages { if let Some(current_pkg) = newest_packages_by_name.get_mut(&package.id.nv.name) { if current_pkg.id.nv.cmp(&package.id.nv) == Ordering::Less { @@ -286,33 +302,35 @@ async fn sync_resolution_with_fs( // are forced to be recreated setup_cache.remove_dep(&package_folder_name); - let cache = cache.clone(); - let registry_url = registry_url.clone(); - let package = package.clone(); - let handle = spawn(async move { - cache - .ensure_package(&package.id.nv, &package.dist, ®istry_url) + let folder_path = folder_path.clone(); + cache_futures.push(async move { + tarball_cache + .ensure_package(&package.id.nv, &package.dist) .await?; + let sub_node_modules = folder_path.join("node_modules"); let package_path = join_package_name(&sub_node_modules, &package.id.nv.name); - fs::create_dir_all(&package_path) - .with_context(|| format!("Creating '{}'", folder_path.display()))?; - let cache_folder = - cache.package_folder_for_name_and_version(&package.id.nv, ®istry_url); - // for now copy, but in the future consider hard linking - copy_dir_recursive(&cache_folder, &package_path)?; - // write out a file that indicates this folder has been initialized - fs::write(initialized_file, "")?; - // finally stop showing the progress bar - Ok(()) + let cache_folder = cache.package_folder_for_nv(&package.id.nv); + + deno_core::unsync::spawn_blocking({ + let package_path = package_path.clone(); + move || { + clone_dir_recursive(&cache_folder, &package_path)?; + // write out a file that indicates this folder has been initialized + fs::write(initialized_file, "")?; + + Ok::<_, AnyError>(()) + } + }) + .await??; + + Ok::<_, AnyError>(()) }); - handles.push(handle); } } - let results = futures::future::join_all(handles).await; - for result in results { - result??; // surface the first error + while let Some(result) = cache_futures.next().await { + result?; // surface the first error } // 2. Create any "copy" packages, which are used for peer dependencies @@ -324,8 +342,7 @@ async fn sync_resolution_with_fs( if !initialized_file.exists() { let sub_node_modules = destination_path.join("node_modules"); let package_path = join_package_name(&sub_node_modules, &package.id.nv.name); - fs::create_dir_all(&package_path) - .with_context(|| format!("Creating '{}'", destination_path.display()))?; + let source_path = join_package_name( &deno_local_registry_dir .join(get_package_folder_id_folder_name( @@ -334,7 +351,8 @@ async fn sync_resolution_with_fs( .join("node_modules"), &package.id.nv.name, ); - hard_link_dir_recursive(&source_path, &package_path)?; + + clone_dir_recursive(&source_path, &package_path)?; // write out a file that indicates this folder has been initialized fs::write(initialized_file, "")?; } @@ -434,10 +452,13 @@ async fn sync_resolution_with_fs( Ok(()) } +// Uses BTreeMap to preserve the ordering of the elements in memory, to ensure +// the file generated from this datastructure is deterministic. +// See: https://github.com/denoland/deno/issues/24479 /// Represents a dependency at `node_modules/.deno//` struct SetupCacheDep<'a> { - previous: Option<&'a HashMap>, - current: &'a mut HashMap, + previous: Option<&'a BTreeMap>, + current: &'a mut BTreeMap, } impl<'a> SetupCacheDep<'a> { @@ -452,11 +473,14 @@ impl<'a> SetupCacheDep<'a> { } } +// Uses BTreeMap to preserve the ordering of the elements in memory, to ensure +// the file generated from this datastructure is deterministic. +// See: https://github.com/denoland/deno/issues/24479 #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] struct SetupCacheData { - root_symlinks: HashMap, - deno_symlinks: HashMap, - dep_symlinks: HashMap>, + root_symlinks: BTreeMap, + deno_symlinks: BTreeMap, + dep_symlinks: BTreeMap>, } /// It is very slow to try to re-setup the symlinks each time, so this will @@ -488,9 +512,9 @@ impl SetupCache { } } - bincode::serialize(&self.current) - .ok() - .and_then(|data| atomic_write_file(&self.file_path, data, CACHE_PERM).ok()); + bincode::serialize(&self.current).ok().and_then(|data| { + atomic_write_file_with_retries(&self.file_path, data, CACHE_PERM).ok() + }); true } @@ -607,6 +631,7 @@ fn symlink_package_dir(old_path: &Path, new_path: &Path) -> Result<(), AnyError> #[cfg(windows)] fn junction_or_symlink_dir(old_path: &Path, new_path: &Path) -> Result<(), AnyError> { + use deno_core::anyhow::bail; // Use junctions because they're supported on ntfs file systems without // needing to elevate privileges on Windows diff --git a/crates/npm/managed/resolvers/mod.rs b/crates/npm/managed/resolvers/mod.rs index 1629f5df0..653f1ce91 100644 --- a/crates/npm/managed/resolvers/mod.rs +++ b/crates/npm/managed/resolvers/mod.rs @@ -7,10 +7,12 @@ mod local; use std::path::PathBuf; use std::sync::Arc; -use deno_core::url::Url; use deno_fs::FileSystem; use deno_npm::NpmSystemInfo; +use crate::cache::TarballCache; +use crate::package_json::PackageJsonInstallDepsProvider; + pub use self::common::NpmPackageFsResolver; use self::global::GlobalNpmPackageResolver; @@ -19,27 +21,30 @@ use self::local::LocalNpmPackageResolver; use super::cache::NpmCache; use super::resolution::NpmResolution; +#[allow(clippy::too_many_arguments)] pub fn create_npm_fs_resolver( fs: Arc, - cache: Arc, - registry_url: Url, + npm_cache: Arc, + pkg_json_deps_provider: &Arc, resolution: Arc, + tarball_cache: Arc, maybe_node_modules_path: Option, system_info: NpmSystemInfo, ) -> Arc { match maybe_node_modules_path { Some(node_modules_folder) => Arc::new(LocalNpmPackageResolver::new( + npm_cache, fs, - cache, - registry_url, - node_modules_folder, + pkg_json_deps_provider.clone(), resolution, + tarball_cache, + node_modules_folder, system_info, )), None => Arc::new(GlobalNpmPackageResolver::new( + npm_cache, fs, - cache, - registry_url, + tarball_cache, resolution, system_info, )), diff --git a/crates/sb_core/cache/fetch_cacher.rs b/crates/npm_cache/fetch_cacher.rs similarity index 54% rename from crates/sb_core/cache/fetch_cacher.rs rename to crates/npm_cache/fetch_cacher.rs index 961e591fc..a10fff8e7 100644 --- a/crates/sb_core/cache/fetch_cacher.rs +++ b/crates/npm_cache/fetch_cacher.rs @@ -1,67 +1,55 @@ -use crate::cache::emit::EmitCache; -use crate::cache::fc_permissions::FcPermissions; -use crate::cache::module_info::{ModuleInfoCache, ModuleInfoCacheSourceHash}; -use crate::cache::parsed_source::ParsedSourceCache; -use crate::cache::{CacheSetting, GlobalHttpCache}; -use crate::file_fetcher::{FetchOptions, FileFetcher}; -use crate::util::errors::get_error_class_name; -use crate::util::fs::canonicalize_path_maybe_not_exists; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; + use deno_ast::{MediaType, ModuleSpecifier}; use deno_core::futures; use deno_core::futures::FutureExt; use deno_graph::source::{CacheInfo, LoadFuture, LoadOptions, LoadResponse, Loader}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -pub fn resolve_specifier_into_node_modules(specifier: &ModuleSpecifier) -> ModuleSpecifier { - specifier - .to_file_path() - .ok() - // this path might not exist at the time the graph is being created - // because the node_modules folder might not yet exist - .and_then(|path| canonicalize_path_maybe_not_exists(&path).ok()) - .and_then(|path| ModuleSpecifier::from_file_path(path).ok()) - .unwrap_or_else(|| specifier.clone()) -} +use sb_core::cache::cache_db::CacheDBHash; +use sb_core::cache::emit::EmitCache; +use sb_core::cache::fc_permissions::FcPermissions; +use sb_core::cache::module_info::ModuleInfoCache; +use sb_core::cache::{CacheSetting, GlobalHttpCache}; +use sb_core::util::errors::get_error_class_name; +use sb_npm::CliNpmResolver; + +use crate::file_fetcher::{FetchNoFollowOptions, FetchOptions, FileFetcher, FileOrRedirect}; /// A "wrapper" for the FileFetcher and DiskCache for the Deno CLI that provides /// a concise interface to the DENO_DIR when building module graphs. #[allow(dead_code)] pub struct FetchCacher { - module_info_cache: Arc, emit_cache: EmitCache, file_fetcher: Arc, file_header_overrides: HashMap>, global_http_cache: Arc, - parsed_source_cache: Arc, + npm_resolver: Arc, + module_info_cache: Arc, permissions: FcPermissions, cache_info_enabled: bool, - maybe_local_node_modules_url: Option, } impl FetchCacher { - #[allow(clippy::too_many_arguments)] pub fn new( - module_info_cache: Arc, emit_cache: EmitCache, file_fetcher: Arc, file_header_overrides: HashMap>, global_http_cache: Arc, - parsed_source_cache: Arc, + npm_resolver: Arc, + module_info_cache: Arc, permissions: FcPermissions, - maybe_local_node_modules_url: Option, ) -> Self { Self { - module_info_cache, emit_cache, file_fetcher, file_header_overrides, global_http_cache, - parsed_source_cache, + npm_resolver, + module_info_cache, permissions, cache_info_enabled: false, - maybe_local_node_modules_url, } } @@ -121,18 +109,15 @@ impl Loader for FetchCacher { fn load(&self, specifier: &ModuleSpecifier, options: LoadOptions) -> LoadFuture { use deno_graph::source::CacheSetting as LoaderCacheSetting; - let cache_setting = options.cache_setting; - - let path = specifier.path(); - if path.contains("/node_modules/") { + if specifier.scheme() == "file" && specifier.path().contains("/node_modules/") { // The specifier might be in a completely different symlinked tree than // what the node_modules url is in (ex. `/my-project-1/node_modules` // symlinked to `/my-project-2/node_modules`), so first we checked if the path // is in a node_modules dir to avoid needlessly canonicalizing, then now compare // against the canonicalized specifier. - let specifier = resolve_specifier_into_node_modules(specifier); - if specifier.as_str().starts_with(path) { + let specifier = sb_core::node::resolve_specifier_into_node_modules(specifier); + if self.npm_resolver.in_npm_package(&specifier) { return Box::pin(futures::future::ready(Ok(Some(LoadResponse::External { specifier, })))); @@ -145,56 +130,71 @@ impl Loader for FetchCacher { let specifier = specifier.clone(); async move { - let maybe_cache_setting = match cache_setting { - LoaderCacheSetting::Use => None, - LoaderCacheSetting::Reload => { - if matches!(file_fetcher.cache_setting(), CacheSetting::Only) { - return Err(deno_core::anyhow::anyhow!( - "Failed to resolve version constraint. Try running again without --cached-only" - )); + let maybe_cache_setting = match options.cache_setting { + LoaderCacheSetting::Use => None, + LoaderCacheSetting::Reload => { + if matches!(file_fetcher.cache_setting(), CacheSetting::Only) { + return Err(deno_core::anyhow::anyhow!( + "Could not resolve version constraint using only cached data. Try running again without --cached-only" + )); + } + Some(CacheSetting::ReloadAll) + } + LoaderCacheSetting::Only => Some(CacheSetting::Only), + }; + file_fetcher + .fetch_no_follow_with_options(FetchNoFollowOptions { + fetch_options: FetchOptions { + specifier: &specifier, + permissions: permissions.clone(), + maybe_accept: None, + maybe_cache_setting: maybe_cache_setting.as_ref(), + }, + maybe_checksum: options.maybe_checksum.as_ref(), + }) + .await + .map(|file_or_redirect| { + match file_or_redirect { + FileOrRedirect::File(file) => { + let maybe_headers = + match (file.maybe_headers, file_header_overrides.get(&specifier)) { + (Some(headers), Some(overrides)) => { + Some(headers.into_iter().chain(overrides.clone()).collect()) } - Some(CacheSetting::ReloadAll) + (Some(headers), None) => Some(headers), + (None, Some(overrides)) => Some(overrides.clone()), + (None, None) => None, + }; + Ok(Some(LoadResponse::Module { + specifier: file.specifier, + maybe_headers, + content: file.source, + })) + }, + FileOrRedirect::Redirect(redirect_specifier) => { + Ok(Some(LoadResponse::Redirect { + specifier: redirect_specifier, + })) + }, + } + }) + .unwrap_or_else(|err| { + if let Some(io_err) = err.downcast_ref::() { + if io_err.kind() == std::io::ErrorKind::NotFound { + return Ok(None); + } else { + return Err(err); } - LoaderCacheSetting::Only => Some(CacheSetting::Only), - }; - file_fetcher - .fetch_with_options(FetchOptions { - specifier: &specifier, - permissions, - maybe_accept: None, - maybe_cache_setting: maybe_cache_setting.as_ref(), - }) - .await - .map(|file| { - let maybe_headers = match (file.maybe_headers, file_header_overrides.get(&specifier)) { - (Some(headers), Some(overrides)) => Some(headers.into_iter().chain(overrides.clone()).collect()), - (Some(headers), None) => Some(headers), - (None, Some(overrides)) => Some(overrides.clone()), - (None, None) => None, - }; - Ok(Some(LoadResponse::Module { - specifier: file.specifier, - maybe_headers, - content: file.source.into(), - })) - }) - .unwrap_or_else(|err| { - if let Some(io_err) = err.downcast_ref::() { - if io_err.kind() == std::io::ErrorKind::NotFound { - return Ok(None); - } else { - return Err(err); - } - } - let error_class_name = get_error_class_name(&err); - match error_class_name { - "NotFound" => Ok(None), - "NotCached" if cache_setting == LoaderCacheSetting::Only => Ok(None), - _ => Err(err), - } - }) + } + let error_class_name = get_error_class_name(&err); + match error_class_name { + "NotFound" => Ok(None), + "NotCached" if options.cache_setting == LoaderCacheSetting::Only => Ok(None), + _ => Err(err), + } + }) } - .boxed() + .boxed_local() } fn cache_module_info( @@ -203,11 +203,11 @@ impl Loader for FetchCacher { source: &Arc<[u8]>, module_info: &deno_graph::ModuleInfo, ) { - let source_hash = ModuleInfoCacheSourceHash::from_source(source); + let source_hash = CacheDBHash::from_source(source); let result = self.module_info_cache.set_module_info( specifier, MediaType::from_specifier(specifier), - &source_hash, + source_hash, module_info, ); if let Err(err) = result { diff --git a/crates/npm_cache/file_fetcher.rs b/crates/npm_cache/file_fetcher.rs new file mode 100644 index 000000000..391d27567 --- /dev/null +++ b/crates/npm_cache/file_fetcher.rs @@ -0,0 +1,579 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use deno_ast::MediaType; +use deno_cache_dir::HttpCache; +use deno_core::error::custom_error; +use deno_core::error::generic_error; +use deno_core::error::uri_error; +use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; +use deno_core::url::Url; +use deno_core::ModuleSpecifier; +use deno_graph::source::LoaderChecksum; +use deno_web::BlobStore; + +use anyhow::{bail, Context}; +use log::debug; + +use std::borrow::Cow; +use std::collections::HashMap; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::SystemTime; + +use sb_core::auth_tokens::AuthTokens; +use sb_core::cache::fc_permissions::FcPermissions; +use sb_core::cache::CacheSetting; +use sb_core::util::http_util::{ + CacheSemantics, FetchOnceArgs, FetchOnceResult, HttpClientProvider, +}; + +pub const SUPPORTED_SCHEMES: [&str; 5] = ["data", "blob", "file", "http", "https"]; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct TextDecodedFile { + pub media_type: MediaType, + /// The _final_ specifier for the file. The requested specifier and the final + /// specifier maybe different for remote files that have been redirected. + pub specifier: ModuleSpecifier, + /// The source of the file. + pub source: Arc, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum FileOrRedirect { + File(File), + Redirect(ModuleSpecifier), +} + +/// A structure representing a source file. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct File { + /// The _final_ specifier for the file. The requested specifier and the final + /// specifier maybe different for remote files that have been redirected. + pub specifier: ModuleSpecifier, + pub maybe_headers: Option>, + /// The source of the file. + pub source: Arc<[u8]>, +} + +impl File { + pub fn resolve_media_type_and_charset(&self) -> (MediaType, Option<&str>) { + deno_graph::source::resolve_media_type_and_charset_from_headers( + &self.specifier, + self.maybe_headers.as_ref(), + ) + } + + /// Decodes the source bytes into a string handling any encoding rules + /// for local vs remote files and dealing with the charset. + pub fn into_text_decoded(self) -> Result { + // lots of borrow checker fighting here + let (media_type, maybe_charset) = + deno_graph::source::resolve_media_type_and_charset_from_headers( + &self.specifier, + self.maybe_headers.as_ref(), + ); + let specifier = self.specifier; + match deno_graph::source::decode_source(&specifier, self.source, maybe_charset) { + Ok(source) => Ok(TextDecodedFile { + media_type, + specifier, + source, + }), + Err(err) => Err(err).with_context(|| format!("Failed decoding \"{}\".", specifier)), + } + } +} + +#[derive(Debug, Clone, Default)] +struct MemoryFiles(Arc>>); + +impl MemoryFiles { + pub fn get(&self, specifier: &ModuleSpecifier) -> Option { + self.0.lock().get(specifier).cloned() + } + + pub fn insert(&self, specifier: ModuleSpecifier, file: File) -> Option { + self.0.lock().insert(specifier, file) + } + + pub fn clear(&self) { + self.0.lock().clear(); + } +} + +/// Fetch a source file from the local file system. +fn fetch_local(specifier: &ModuleSpecifier) -> Result { + let local = specifier + .to_file_path() + .map_err(|_| uri_error(format!("Invalid file path.\n Specifier: {specifier}")))?; + let bytes = fs::read(local)?; + + Ok(File { + specifier: specifier.clone(), + maybe_headers: None, + source: bytes.into(), + }) +} + +/// Return a validated scheme for a given module specifier. +fn get_validated_scheme(specifier: &ModuleSpecifier) -> Result { + let scheme = specifier.scheme(); + if !SUPPORTED_SCHEMES.contains(&scheme) { + Err(generic_error(format!( + "Unsupported scheme \"{scheme}\" for module \"{specifier}\". Supported schemes: {SUPPORTED_SCHEMES:#?}" + ))) + } else { + Ok(scheme.to_string()) + } +} + +pub struct FetchOptions<'a> { + pub specifier: &'a ModuleSpecifier, + pub permissions: FcPermissions, + pub maybe_accept: Option<&'a str>, + pub maybe_cache_setting: Option<&'a CacheSetting>, +} + +pub struct FetchNoFollowOptions<'a> { + pub fetch_options: FetchOptions<'a>, + /// This setting doesn't make sense to provide for `FetchOptions` + /// since the required checksum may change for a redirect. + pub maybe_checksum: Option<&'a LoaderChecksum>, +} + +/// A structure for resolving, fetching and caching source files. +#[derive(Debug)] +pub struct FileFetcher { + auth_tokens: AuthTokens, + allow_remote: bool, + memory_files: MemoryFiles, + cache_setting: CacheSetting, + http_cache: Arc, + http_client_provider: Arc, + blob_store: Arc, + download_log_level: log::Level, +} + +impl FileFetcher { + pub fn new( + http_cache: Arc, + cache_setting: CacheSetting, + allow_remote: bool, + http_client_provider: Arc, + blob_store: Arc, + ) -> Self { + Self { + auth_tokens: AuthTokens::new(env::var("DENO_AUTH_TOKENS").ok()), + allow_remote, + memory_files: Default::default(), + cache_setting, + http_cache, + http_client_provider, + blob_store, + download_log_level: log::Level::Info, + } + } + + pub fn cache_setting(&self) -> &CacheSetting { + &self.cache_setting + } + + /// Sets the log level to use when outputting the download message. + pub fn set_download_log_level(&mut self, level: log::Level) { + self.download_log_level = level; + } + + /// Fetch cached remote file. + /// + /// This is a recursive operation if source file has redirections. + pub fn fetch_cached( + &self, + specifier: &ModuleSpecifier, + redirect_limit: i64, + ) -> Result, AnyError> { + let mut specifier = Cow::Borrowed(specifier); + for _ in 0..=redirect_limit { + match self.fetch_cached_no_follow(&specifier, None)? { + Some(FileOrRedirect::File(file)) => { + return Ok(Some(file)); + } + Some(FileOrRedirect::Redirect(redirect_specifier)) => { + specifier = Cow::Owned(redirect_specifier); + } + None => { + return Ok(None); + } + } + } + Err(custom_error("Http", "Too many redirects.")) + } + + fn fetch_cached_no_follow( + &self, + specifier: &ModuleSpecifier, + maybe_checksum: Option<&LoaderChecksum>, + ) -> Result, AnyError> { + debug!( + "FileFetcher::fetch_cached_no_follow - specifier: {}", + specifier + ); + + let cache_key = self.http_cache.cache_item_key(specifier)?; // compute this once + let Some(headers) = self.http_cache.read_headers(&cache_key)? else { + return Ok(None); + }; + if let Some(redirect_to) = headers.get("location") { + let redirect = deno_core::resolve_import(redirect_to, specifier.as_str())?; + return Ok(Some(FileOrRedirect::Redirect(redirect))); + } + let result = self.http_cache.read_file_bytes( + &cache_key, + maybe_checksum + .as_ref() + .map(|c| deno_cache_dir::Checksum::new(c.as_str())), + deno_cache_dir::GlobalToLocalCopy::Allow, + ); + let bytes = match result { + Ok(Some(bytes)) => bytes, + Ok(None) => return Ok(None), + Err(err) => match err { + deno_cache_dir::CacheReadFileError::Io(err) => return Err(err.into()), + deno_cache_dir::CacheReadFileError::ChecksumIntegrity(err) => { + // convert to the equivalent deno_graph error so that it + // enhances it if this is passed to deno_graph + return Err(deno_graph::source::ChecksumIntegrityError { + actual: err.actual, + expected: err.expected, + } + .into()); + } + }, + }; + + Ok(Some(FileOrRedirect::File(File { + specifier: specifier.clone(), + maybe_headers: Some(headers), + source: Arc::from(bytes), + }))) + } + + /// Convert a data URL into a file, resulting in an error if the URL is + /// invalid. + fn fetch_data_url(&self, specifier: &ModuleSpecifier) -> Result { + debug!("FileFetcher::fetch_data_url() - specifier: {}", specifier); + let data_url = deno_graph::source::RawDataUrl::parse(specifier)?; + let (bytes, headers) = data_url.into_bytes_and_headers(); + Ok(File { + specifier: specifier.clone(), + maybe_headers: Some(headers), + source: Arc::from(bytes), + }) + } + + /// Get a blob URL. + async fn fetch_blob_url(&self, specifier: &ModuleSpecifier) -> Result { + debug!("FileFetcher::fetch_blob_url() - specifier: {}", specifier); + let blob = self + .blob_store + .get_object_url(specifier.clone()) + .ok_or_else(|| { + custom_error("NotFound", format!("Blob URL not found: \"{specifier}\".")) + })?; + + let bytes = blob.read_all().await?; + let headers = HashMap::from([("content-type".to_string(), blob.media_type.clone())]); + + Ok(File { + specifier: specifier.clone(), + maybe_headers: Some(headers), + source: Arc::from(bytes), + }) + } + + async fn fetch_remote_no_follow( + &self, + specifier: &ModuleSpecifier, + maybe_accept: Option<&str>, + cache_setting: &CacheSetting, + maybe_checksum: Option<&LoaderChecksum>, + ) -> Result { + debug!( + "FileFetcher::fetch_remote_no_follow - specifier: {}", + specifier + ); + + if self.should_use_cache(specifier, cache_setting) { + if let Some(file_or_redirect) = + self.fetch_cached_no_follow(specifier, maybe_checksum)? + { + return Ok(file_or_redirect); + } + } + + if *cache_setting == CacheSetting::Only { + return Err(custom_error( + "NotCached", + format!( + "Specifier not found in cache: \"{specifier}\", --cached-only is specified." + ), + )); + } + + log::log!(self.download_log_level, "{} {}", "Download", specifier); + + let maybe_etag = self + .http_cache + .cache_item_key(specifier) + .ok() + .and_then(|key| self.http_cache.read_headers(&key).ok().flatten()) + .and_then(|headers| headers.get("etag").cloned()); + let maybe_auth_token = self.auth_tokens.get(specifier); + + async fn handle_request_or_server_error( + retried: &mut bool, + specifier: &Url, + err_str: String, + ) -> Result<(), AnyError> { + // Retry once, and bail otherwise. + if !*retried { + *retried = true; + log::debug!("Import '{}' failed: {}. Retrying...", specifier, err_str); + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + Ok(()) + } else { + Err(generic_error(format!( + "Import '{}' failed: {}", + specifier, err_str + ))) + } + } + + let mut maybe_etag = maybe_etag; + let mut retried = false; // retry intermittent failures + let result = loop { + let result = match self + .http_client_provider + .get_or_create()? + .fetch_no_follow(FetchOnceArgs { + url: specifier.clone(), + maybe_accept: maybe_accept.map(ToOwned::to_owned), + maybe_etag: maybe_etag.clone(), + maybe_auth_token: maybe_auth_token.clone(), + }) + .await? + { + FetchOnceResult::NotModified => { + let file_or_redirect = + self.fetch_cached_no_follow(specifier, maybe_checksum)?; + match file_or_redirect { + Some(file_or_redirect) => Ok(file_or_redirect), + None => { + // Someone may have deleted the body from the cache since + // it's currently stored in a separate file from the headers, + // so delete the etag and try again + if maybe_etag.is_some() { + debug!("Cache body not found. Trying again without etag."); + maybe_etag = None; + continue; + } else { + // should never happen + bail!("Your deno cache directory is in an unrecoverable state. Please delete it and try again.") + } + } + } + } + FetchOnceResult::Redirect(redirect_url, headers) => { + self.http_cache.set(specifier, headers, &[])?; + Ok(FileOrRedirect::Redirect(redirect_url)) + } + FetchOnceResult::Code(bytes, headers) => { + self.http_cache.set(specifier, headers.clone(), &bytes)?; + if let Some(checksum) = &maybe_checksum { + checksum.check_source(&bytes)?; + } + Ok(FileOrRedirect::File(File { + specifier: specifier.clone(), + maybe_headers: Some(headers), + source: Arc::from(bytes), + })) + } + FetchOnceResult::RequestError(err) => { + handle_request_or_server_error(&mut retried, specifier, err).await?; + continue; + } + FetchOnceResult::ServerError(status) => { + handle_request_or_server_error(&mut retried, specifier, status.to_string()) + .await?; + continue; + } + }; + break result; + }; + + result + } + + /// Returns if the cache should be used for a given specifier. + fn should_use_cache(&self, specifier: &ModuleSpecifier, cache_setting: &CacheSetting) -> bool { + match cache_setting { + CacheSetting::ReloadAll => false, + CacheSetting::Use | CacheSetting::Only => true, + CacheSetting::RespectHeaders => { + let Ok(cache_key) = self.http_cache.cache_item_key(specifier) else { + return false; + }; + let Ok(Some(headers)) = self.http_cache.read_headers(&cache_key) else { + return false; + }; + let Ok(Some(download_time)) = self.http_cache.read_download_time(&cache_key) else { + return false; + }; + let cache_semantics = + CacheSemantics::new(headers, download_time, SystemTime::now()); + cache_semantics.should_use() + } + CacheSetting::ReloadSome(list) => { + let mut url = specifier.clone(); + url.set_fragment(None); + if list.iter().any(|x| x == url.as_str()) { + return false; + } + url.set_query(None); + let mut path = PathBuf::from(url.as_str()); + loop { + if list.contains(&path.to_str().unwrap().to_string()) { + return false; + } + if !path.pop() { + break; + } + } + true + } + } + } + + /// Fetch a source file and asynchronously return it. + pub async fn fetch( + &self, + specifier: &ModuleSpecifier, + permissions: FcPermissions, + ) -> Result { + self.fetch_with_options(FetchOptions { + specifier, + permissions, + maybe_accept: None, + maybe_cache_setting: None, + }) + .await + } + + pub async fn fetch_with_options(&self, options: FetchOptions<'_>) -> Result { + self.fetch_with_options_and_max_redirect(options, 10).await + } + + async fn fetch_with_options_and_max_redirect( + &self, + options: FetchOptions<'_>, + max_redirect: usize, + ) -> Result { + let mut specifier = Cow::Borrowed(options.specifier); + for _ in 0..=max_redirect { + match self + .fetch_no_follow_with_options(FetchNoFollowOptions { + fetch_options: FetchOptions { + specifier: &specifier, + permissions: options.permissions.clone(), + maybe_accept: options.maybe_accept, + maybe_cache_setting: options.maybe_cache_setting, + }, + maybe_checksum: None, + }) + .await? + { + FileOrRedirect::File(file) => { + return Ok(file); + } + FileOrRedirect::Redirect(redirect_specifier) => { + specifier = Cow::Owned(redirect_specifier); + } + } + } + + Err(custom_error("Http", "Too many redirects.")) + } + + /// Fetches without following redirects. + pub async fn fetch_no_follow_with_options( + &self, + options: FetchNoFollowOptions<'_>, + ) -> Result { + let maybe_checksum = options.maybe_checksum; + let mut options = options.fetch_options; + let specifier = options.specifier; + // note: this debug output is used by the tests + debug!( + "FileFetcher::fetch_no_follow_with_options - specifier: {}", + specifier + ); + let scheme = get_validated_scheme(specifier)?; + options.permissions.check_specifier(specifier)?; + if let Some(file) = self.memory_files.get(specifier) { + Ok(FileOrRedirect::File(file)) + } else if scheme == "file" { + // we do not in memory cache files, as this would prevent files on the + // disk changing effecting things like workers and dynamic imports. + fetch_local(specifier).map(FileOrRedirect::File) + } else if scheme == "data" { + self.fetch_data_url(specifier).map(FileOrRedirect::File) + } else if scheme == "blob" { + self.fetch_blob_url(specifier) + .await + .map(FileOrRedirect::File) + } else if !self.allow_remote { + Err(custom_error( + "NoRemote", + format!("A remote specifier was requested: \"{specifier}\", but --no-remote is specified."), + )) + } else { + self.fetch_remote_no_follow( + specifier, + options.maybe_accept, + options.maybe_cache_setting.unwrap_or(&self.cache_setting), + maybe_checksum, + ) + .await + } + } + + /// A synchronous way to retrieve a source file, where if the file has already + /// been cached in memory it will be returned, otherwise for local files will + /// be read from disk. + pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option { + let maybe_file = self.memory_files.get(specifier); + if maybe_file.is_none() { + let is_local = specifier.scheme() == "file"; + if is_local { + if let Ok(file) = fetch_local(specifier) { + return Some(file); + } + } + None + } else { + maybe_file + } + } + + /// Insert a temporary module for the file fetcher. + pub fn insert_memory_files(&self, file: File) -> Option { + self.memory_files.insert(file.specifier.clone(), file) + } + + pub fn clear_memory_files(&self) { + self.memory_files.clear(); + } +} diff --git a/crates/npm_cache/lib.rs b/crates/npm_cache/lib.rs new file mode 100644 index 000000000..d8bc79085 --- /dev/null +++ b/crates/npm_cache/lib.rs @@ -0,0 +1,5 @@ +pub mod fetch_cacher; +pub mod file_fetcher; + +pub use fetch_cacher::FetchCacher; +pub use file_fetcher::FileFetcher; diff --git a/crates/sb_core/cache/cache_db.rs b/crates/sb_core/cache/cache_db.rs index 9ba4e68d5..25d4a2241 100644 --- a/crates/sb_core/cache/cache_db.rs +++ b/crates/sb_core/cache/cache_db.rs @@ -10,9 +10,50 @@ use deno_webstorage::rusqlite::OptionalExtension; use deno_webstorage::rusqlite::Params; use once_cell::sync::OnceCell; use std::io::IsTerminal; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use super::common::FastInsecureHasher; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct CacheDBHash(u64); + +impl CacheDBHash { + pub fn new(hash: u64) -> Self { + Self(hash) + } + + pub fn from_source(source: impl std::hash::Hash) -> Self { + Self::new( + // always write in the deno version just in case + // the clearing on deno version change doesn't work + FastInsecureHasher::new_deno_versioned() + .write_hashable(source) + .finish(), + ) + } +} + +impl rusqlite::types::ToSql for CacheDBHash { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + // sqlite doesn't support u64, but it does support i64 so store + // this value "incorrectly" as i64 then convert back to u64 on read + rusqlite::types::Value::Integer(self.0 as i64), + )) + } +} + +impl rusqlite::types::FromSql for CacheDBHash { + fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { + match value { + rusqlite::types::ValueRef::Integer(i) => Ok(Self::new(i as u64)), + _ => Err(rusqlite::types::FromSqlError::InvalidType), + } + } +} + /// What should the cache should do on failure? #[derive(Default)] pub enum CacheFailure { @@ -40,21 +81,16 @@ pub struct CacheDBConfiguration { impl CacheDBConfiguration { fn create_combined_sql(&self) -> String { format!( - " - PRAGMA journal_mode=TRUNCATE; - PRAGMA synchronous=NORMAL; - PRAGMA temp_store=memory; - PRAGMA page_size=4096; - PRAGMA mmap_size=6000000; - PRAGMA optimize; - - CREATE TABLE IF NOT EXISTS info ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - - {} - ", + concat!( + "PRAGMA journal_mode=WAL;", + "PRAGMA synchronous=NORMAL;", + "PRAGMA temp_store=memory;", + "PRAGMA page_size=4096;", + "PRAGMA mmap_size=6000000;", + "PRAGMA optimize;", + "CREATE TABLE IF NOT EXISTS info (key TEXT PRIMARY KEY, value TEXT NOT NULL);", + "{}", + ), self.table_initializer ) } @@ -133,6 +169,34 @@ impl CacheDB { new } + /// Useful for testing: re-create this cache DB with a different current version. + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn recreate_with_version(mut self, version: &'static str) -> Self { + // By taking the lock, we know there are no initialization threads alive + drop(self.conn.lock()); + + let arc = std::mem::take(&mut self.conn); + let conn = match Arc::try_unwrap(arc) { + Err(_) => panic!("Failed to unwrap connection"), + Ok(conn) => match conn.into_inner().into_inner() { + Some(ConnectionState::Connected(conn)) => conn, + _ => panic!("Connection had failed and cannot be unwrapped"), + }, + }; + + Self::initialize_connection(self.config, &conn, version).unwrap(); + + let cell = OnceCell::new(); + _ = cell.set(ConnectionState::Connected(conn)); + Self { + conn: Arc::new(Mutex::new(cell)), + path: self.path.clone(), + config: self.config, + version, + } + } + fn spawn_eager_init_thread(&self) { let clone = self.clone(); debug_assert!(tokio::runtime::Handle::try_current().is_ok()); @@ -143,10 +207,7 @@ impl CacheDB { } /// Open the connection in memory or on disk. - fn actually_open_connection( - &self, - path: &Option, - ) -> Result { + fn actually_open_connection(&self, path: Option<&Path>) -> Result { match path { // This should never fail unless something is very wrong None => Connection::open_in_memory(), @@ -189,7 +250,7 @@ impl CacheDB { } /// Open and initialize a connection. - fn open_connection_and_init(&self, path: &Option) -> Result { + fn open_connection_and_init(&self, path: Option<&Path>) -> Result { let conn = self.actually_open_connection(path)?; Self::initialize_connection(self.config, &conn, self.version)?; Ok(conn) @@ -198,87 +259,9 @@ impl CacheDB { /// This function represents the policy for dealing with corrupted cache files. We try fairly aggressively /// to repair the situation, and if we can't, we prefer to log noisily and continue with in-memory caches. fn open_connection(&self) -> Result { - // Success on first try? We hope that this is the case. - let err = match self.open_connection_and_init(&self.path) { - Ok(conn) => return Ok(ConnectionState::Connected(conn)), - Err(err) => err, - }; - - if self.path.is_none() { - // If an in-memory DB fails, that's game over - log::error!("Failed to initialize in-memory cache database."); - return Err(err); - } - - let path = self.path.as_ref().unwrap(); - - // There are rare times in the tests when we can't initialize a cache DB the first time, but it succeeds the second time, so - // we don't log these at a debug level. - log::trace!( - "Could not initialize cache database '{}', retrying... ({err:?})", - path.to_string_lossy(), - ); - - // Try a second time - let err = match self.open_connection_and_init(&self.path) { - Ok(conn) => return Ok(ConnectionState::Connected(conn)), - Err(err) => err, - }; - - // Failed, try deleting it - let is_tty = std::io::stderr().is_terminal(); - log::log!( - if is_tty { - log::Level::Warn - } else { - log::Level::Trace - }, - "Could not initialize cache database '{}', deleting and retrying... ({err:?})", - path.to_string_lossy() - ); - if std::fs::remove_file(path).is_ok() { - // Try a third time if we successfully deleted it - let res = self.open_connection_and_init(&self.path); - if let Ok(conn) = res { - return Ok(ConnectionState::Connected(conn)); - }; - } - - match self.config.on_failure { - CacheFailure::InMemory => { - log::log!( - if is_tty { - log::Level::Error - } else { - log::Level::Trace - }, - "Failed to open cache file '{}', opening in-memory cache.", - path.to_string_lossy() - ); - Ok(ConnectionState::Connected( - self.open_connection_and_init(&None)?, - )) - } - CacheFailure::Blackhole => { - log::log!( - if is_tty { - log::Level::Error - } else { - log::Level::Trace - }, - "Failed to open cache file '{}', performance may be degraded.", - path.to_string_lossy() - ); - Ok(ConnectionState::Blackhole) - } - CacheFailure::Error => { - log::error!( - "Failed to open cache file '{}', expect further errors.", - path.to_string_lossy() - ); - Err(err) - } - } + open_connection(self.config, self.path.as_deref(), |maybe_path| { + self.open_connection_and_init(maybe_path) + }) } fn initialize<'a>( @@ -356,3 +339,101 @@ impl CacheDB { Ok(res) } } + +/// This function represents the policy for dealing with corrupted cache files. We try fairly aggressively +/// to repair the situation, and if we can't, we prefer to log noisily and continue with in-memory caches. +fn open_connection( + config: &CacheDBConfiguration, + path: Option<&Path>, + open_connection_and_init: impl Fn(Option<&Path>) -> Result, +) -> Result { + // Success on first try? We hope that this is the case. + let err = match open_connection_and_init(path) { + Ok(conn) => return Ok(ConnectionState::Connected(conn)), + Err(err) => err, + }; + + let Some(path) = path.as_ref() else { + // If an in-memory DB fails, that's game over + log::error!("Failed to initialize in-memory cache database."); + return Err(err); + }; + + // ensure the parent directory exists + if let Some(parent) = path.parent() { + match std::fs::create_dir_all(parent) { + Ok(_) => { + log::debug!("Created parent directory for cache db."); + } + Err(err) => { + log::debug!("Failed creating the cache db parent dir: {:#}", err); + } + } + } + + // There are rare times in the tests when we can't initialize a cache DB the first time, but it succeeds the second time, so + // we don't log these at a debug level. + log::trace!( + "Could not initialize cache database '{}', retrying... ({err:?})", + path.to_string_lossy(), + ); + + // Try a second time + let err = match open_connection_and_init(Some(path)) { + Ok(conn) => return Ok(ConnectionState::Connected(conn)), + Err(err) => err, + }; + + // Failed, try deleting it + let is_tty = std::io::stderr().is_terminal(); + log::log!( + if is_tty { + log::Level::Warn + } else { + log::Level::Trace + }, + "Could not initialize cache database '{}', deleting and retrying... ({err:?})", + path.to_string_lossy() + ); + if std::fs::remove_file(path).is_ok() { + // Try a third time if we successfully deleted it + let res = open_connection_and_init(Some(path)); + if let Ok(conn) = res { + return Ok(ConnectionState::Connected(conn)); + }; + } + + match config.on_failure { + CacheFailure::InMemory => { + log::log!( + if is_tty { + log::Level::Error + } else { + log::Level::Trace + }, + "Failed to open cache file '{}', opening in-memory cache.", + path.to_string_lossy() + ); + Ok(ConnectionState::Connected(open_connection_and_init(None)?)) + } + CacheFailure::Blackhole => { + log::log!( + if is_tty { + log::Level::Error + } else { + log::Level::Trace + }, + "Failed to open cache file '{}', performance may be degraded.", + path.to_string_lossy() + ); + Ok(ConnectionState::Blackhole) + } + CacheFailure::Error => { + log::error!( + "Failed to open cache file '{}', expect further errors.", + path.to_string_lossy() + ); + Err(err) + } + } +} diff --git a/crates/sb_core/cache/common.rs b/crates/sb_core/cache/common.rs index 89a299251..8b30bb924 100644 --- a/crates/sb_core/cache/common.rs +++ b/crates/sb_core/cache/common.rs @@ -1,18 +1,19 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::hash::Hasher; /// A very fast insecure hasher that uses the xxHash algorithm. -#[derive(Default)] pub struct FastInsecureHasher(twox_hash::XxHash64); impl FastInsecureHasher { - pub fn new() -> Self { - Self::default() + pub fn new_without_deno_version() -> Self { + Self(Default::default()) } - pub fn hash(hashable: impl std::hash::Hash) -> u64 { - Self::new().write_hashable(hashable).finish() + pub fn new_deno_versioned() -> Self { + let mut hasher = Self::new_without_deno_version(); + hasher.write_str(deno_manifest::version()); + hasher } pub fn write_str(&mut self, text: &str) -> &mut Self { diff --git a/crates/sb_core/cache/disk_cache.rs b/crates/sb_core/cache/disk_cache.rs index ce53da238..a18deb4b7 100644 --- a/crates/sb_core/cache/disk_cache.rs +++ b/crates/sb_core/cache/disk_cache.rs @@ -3,7 +3,7 @@ use super::CACHE_PERM; use crate::util::fs::atomic_write_file; -use super::http_cache::url_to_filename; +use deno_cache_dir::url_to_filename; use deno_core::url::Host; use deno_core::url::Url; use std::ffi::OsStr; diff --git a/crates/sb_core/cache/emit.rs b/crates/sb_core/cache/emit.rs index 170f8200c..37520db1f 100644 --- a/crates/sb_core/cache/emit.rs +++ b/crates/sb_core/cache/emit.rs @@ -6,7 +6,6 @@ use crate::cache::common::FastInsecureHasher; use crate::cache::disk_cache::DiskCache; use crate::util::versions_util::deno; use deno_ast::ModuleSpecifier; -use deno_ast::TranspileOptions; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::serde_json; @@ -15,8 +14,8 @@ use serde::Serialize; #[derive(Debug, Deserialize, Serialize)] struct EmitMetadata { - pub source_hash: String, - pub emit_hash: String, + pub source_hash: u64, + pub emit_hash: u64, } /// The cache that stores previously emitted files. @@ -24,14 +23,12 @@ struct EmitMetadata { pub struct EmitCache { disk_cache: DiskCache, cli_version: &'static str, - transpile_options: TranspileOptions, } impl EmitCache { - pub fn new(disk_cache: DiskCache, transpile_options: TranspileOptions) -> Self { + pub fn new(disk_cache: DiskCache) -> Self { Self { disk_cache, - transpile_options, cli_version: deno(), } } @@ -55,15 +52,13 @@ impl EmitCache { // load and verify the meta data file is for this source and CLI version let bytes = self.disk_cache.get(&meta_filename).ok()?; let meta: EmitMetadata = serde_json::from_slice(&bytes).ok()?; - if meta.source_hash != expected_source_hash.to_string() { + if meta.source_hash != expected_source_hash { return None; } // load and verify the emit is for the meta data let emit_bytes = self.disk_cache.get(&emit_filename).ok()?; - if meta.emit_hash - != compute_emit_hash(&emit_bytes, self.cli_version, &self.transpile_options) - { + if meta.emit_hash != compute_emit_hash(&emit_bytes, self.cli_version) { return None; } @@ -108,12 +103,8 @@ impl EmitCache { // save the metadata let metadata = EmitMetadata { - source_hash: source_hash.to_string(), - emit_hash: compute_emit_hash( - code.as_bytes(), - self.cli_version, - &self.transpile_options, - ), + source_hash: source_hash, + emit_hash: compute_emit_hash(code.as_bytes(), self.cli_version), }; self.disk_cache @@ -136,19 +127,13 @@ impl EmitCache { } } -fn compute_emit_hash( - bytes: &[u8], - cli_version: &str, - transpile_options: &TranspileOptions, -) -> String { +fn compute_emit_hash(bytes: &[u8], cli_version: &str) -> u64 { // it's ok to use an insecure hash here because // if someone can change the emit source then they // can also change the version hash - FastInsecureHasher::new() + FastInsecureHasher::new_without_deno_version() .write(bytes) // emit should not be re-used between cli versions .write(cli_version.as_bytes()) - .write_hashable(transpile_options) .finish() - .to_string() } diff --git a/crates/sb_core/cache/incremental.rs b/crates/sb_core/cache/incremental.rs index 69d7130e6..5a138fb84 100644 --- a/crates/sb_core/cache/incremental.rs +++ b/crates/sb_core/cache/incremental.rs @@ -6,23 +6,23 @@ use std::path::PathBuf; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; -use deno_core::serde_json; use deno_core::unsync::spawn; use deno_core::unsync::JoinHandle; use deno_webstorage::rusqlite::params; -use serde::Serialize; use super::cache_db::CacheDB; use super::cache_db::CacheDBConfiguration; +use super::cache_db::CacheDBHash; use super::cache_db::CacheFailure; -use super::common::FastInsecureHasher; pub static INCREMENTAL_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { - table_initializer: "CREATE TABLE IF NOT EXISTS incrementalcache ( - file_path TEXT PRIMARY KEY, - state_hash TEXT NOT NULL, - source_hash TEXT NOT NULL - );", + table_initializer: concat!( + "CREATE TABLE IF NOT EXISTS incrementalcache (", + "file_path TEXT PRIMARY KEY,", + "state_hash INTEGER NOT NULL,", + "source_hash INTEGER NOT NULL", + ");" + ), on_version_change: "DELETE FROM incrementalcache;", preheat_queries: &[], // If the cache fails, just ignore all caching attempts @@ -34,7 +34,7 @@ pub static INCREMENTAL_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { pub struct IncrementalCache(IncrementalCacheInner); impl IncrementalCache { - pub fn new( + pub fn new( db: CacheDB, state: &TState, initial_file_paths: &[PathBuf], @@ -56,23 +56,23 @@ impl IncrementalCache { } enum ReceiverMessage { - Update(PathBuf, u64), + Update(PathBuf, CacheDBHash), Exit, } struct IncrementalCacheInner { - previous_hashes: HashMap, + previous_hashes: HashMap, sender: tokio::sync::mpsc::UnboundedSender, handle: Mutex>>, } impl IncrementalCacheInner { - pub fn new( + pub fn new( db: CacheDB, state: &TState, initial_file_paths: &[PathBuf], ) -> Self { - let state_hash = FastInsecureHasher::hash(serde_json::to_string(state).unwrap()); + let state_hash = CacheDBHash::from_source(state); let sql_cache = SqlIncrementalCache::new(db, state_hash); Self::from_sql_incremental_cache(sql_cache, initial_file_paths) } @@ -111,13 +111,13 @@ impl IncrementalCacheInner { pub fn is_file_same(&self, file_path: &Path, file_text: &str) -> bool { match self.previous_hashes.get(file_path) { - Some(hash) => *hash == FastInsecureHasher::hash(file_text), + Some(hash) => *hash == CacheDBHash::from_source(file_text), None => false, } } pub fn update_file(&self, file_path: &Path, file_text: &str) { - let hash = FastInsecureHasher::hash(file_text); + let hash = CacheDBHash::from_source(file_text); if let Some(previous_hash) = self.previous_hashes.get(file_path) { if *previous_hash == hash { return; // do not bother updating the db file because nothing has changed @@ -144,15 +144,15 @@ struct SqlIncrementalCache { /// A hash of the state used to produce the formatting/linting other than /// the CLI version. This state is a hash of the configuration and ensures /// we format/lint a file when the configuration changes. - state_hash: u64, + state_hash: CacheDBHash, } impl SqlIncrementalCache { - pub fn new(conn: CacheDB, state_hash: u64) -> Self { + pub fn new(conn: CacheDB, state_hash: CacheDBHash) -> Self { Self { conn, state_hash } } - pub fn get_source_hash(&self, path: &Path) -> Option { + pub fn get_source_hash(&self, path: &Path) -> Option { match self.get_source_hash_result(path) { Ok(option) => option, Err(err) => { @@ -166,40 +166,36 @@ impl SqlIncrementalCache { } } - fn get_source_hash_result(&self, path: &Path) -> Result, AnyError> { + fn get_source_hash_result(&self, path: &Path) -> Result, AnyError> { let query = " - SELECT - source_hash - FROM - incrementalcache - WHERE - file_path=?1 - AND state_hash=?2 - LIMIT 1"; + SELECT + source_hash + FROM + incrementalcache + WHERE + file_path=?1 + AND state_hash=?2 + LIMIT 1"; let res = self.conn.query_row( query, - params![path.to_string_lossy(), self.state_hash.to_string()], + params![path.to_string_lossy(), self.state_hash], |row| { - let hash: String = row.get(0)?; - Ok(hash.parse::()?) + let hash: CacheDBHash = row.get(0)?; + Ok(hash) }, )?; Ok(res) } - pub fn set_source_hash(&self, path: &Path, source_hash: u64) -> Result<(), AnyError> { + pub fn set_source_hash(&self, path: &Path, source_hash: CacheDBHash) -> Result<(), AnyError> { let sql = " - INSERT OR REPLACE INTO - incrementalcache (file_path, state_hash, source_hash) - VALUES - (?1, ?2, ?3)"; + INSERT OR REPLACE INTO + incrementalcache (file_path, state_hash, source_hash) + VALUES + (?1, ?2, ?3)"; self.conn.execute( sql, - params![ - path.to_string_lossy(), - &self.state_hash.to_string(), - &source_hash, - ], + params![path.to_string_lossy(), self.state_hash, source_hash], )?; Ok(()) } diff --git a/crates/sb_core/cache/mod.rs b/crates/sb_core/cache/mod.rs index 759037b57..0200654be 100644 --- a/crates/sb_core/cache/mod.rs +++ b/crates/sb_core/cache/mod.rs @@ -6,16 +6,12 @@ pub mod deno_dir; pub mod disk_cache; pub mod emit; pub mod fc_permissions; -pub mod fetch_cacher; -pub mod http_cache; pub mod incremental; pub mod module_info; pub mod node; pub mod parsed_source; use crate::util::fs::atomic_write_file; -pub use deno_cache_dir::CachedUrlMetadata; -pub use deno_cache_dir::HttpCache; use std::path::Path; use std::time::SystemTime; diff --git a/crates/sb_core/cache/module_info.rs b/crates/sb_core/cache/module_info.rs index 6fd09aaa7..57c809a0d 100644 --- a/crates/sb_core/cache/module_info.rs +++ b/crates/sb_core/cache/module_info.rs @@ -2,19 +2,19 @@ use std::sync::Arc; -use crate::cache::common::FastInsecureHasher; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::serde_json; use deno_graph::ModuleInfo; -use deno_graph::ModuleParser; use deno_graph::ParserModuleAnalyzer; use deno_webstorage::rusqlite::params; use super::cache_db::CacheDB; use super::cache_db::CacheDBConfiguration; +use super::cache_db::CacheDBHash; use super::cache_db::CacheFailure; +use super::parsed_source::ParsedSourceCache; const SELECT_MODULE_INFO: &str = " SELECT @@ -28,33 +28,19 @@ WHERE LIMIT 1"; pub static MODULE_INFO_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { - table_initializer: "CREATE TABLE IF NOT EXISTS moduleinfocache ( - specifier TEXT PRIMARY KEY, - media_type TEXT NOT NULL, - source_hash TEXT NOT NULL, - module_info TEXT NOT NULL - );", + table_initializer: concat!( + "CREATE TABLE IF NOT EXISTS moduleinfocache (", + "specifier TEXT PRIMARY KEY,", + "media_type INTEGER NOT NULL,", + "source_hash INTEGER NOT NULL,", + "module_info TEXT NOT NULL", + ");" + ), on_version_change: "DELETE FROM moduleinfocache;", preheat_queries: &[SELECT_MODULE_INFO], on_failure: CacheFailure::InMemory, }; -pub struct ModuleInfoCacheSourceHash(String); - -impl ModuleInfoCacheSourceHash { - pub fn new(hash: u64) -> Self { - Self(hash.to_string()) - } - - pub fn from_source(source: &[u8]) -> Self { - Self::new(FastInsecureHasher::hash(source)) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - /// A cache of `deno_graph::ModuleInfo` objects. Using this leads to a considerable /// performance improvement because when it exists we can skip parsing a module for /// deno_graph. @@ -72,11 +58,20 @@ impl ModuleInfoCache { Self { conn } } + /// Useful for testing: re-create this cache DB with a different current version. + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn recreate_with_version(self, version: &'static str) -> Self { + Self { + conn: self.conn.recreate_with_version(version), + } + } + pub fn get_module_info( &self, specifier: &ModuleSpecifier, media_type: MediaType, - expected_source_hash: &ModuleInfoCacheSourceHash, + expected_source_hash: CacheDBHash, ) -> Result, AnyError> { let query = SELECT_MODULE_INFO; let res = self.conn.query_row( @@ -84,7 +79,7 @@ impl ModuleInfoCache { params![ &specifier.as_str(), serialize_media_type(media_type), - expected_source_hash.as_str(), + expected_source_hash, ], |row| { let module_info: String = row.get(0)?; @@ -99,7 +94,7 @@ impl ModuleInfoCache { &self, specifier: &ModuleSpecifier, media_type: MediaType, - source_hash: &ModuleInfoCacheSourceHash, + source_hash: CacheDBHash, module_info: &ModuleInfo, ) -> Result<(), AnyError> { let sql = " @@ -112,7 +107,7 @@ impl ModuleInfoCache { params![ specifier.as_str(), serialize_media_type(media_type), - source_hash.as_str(), + source_hash, &serde_json::to_string(&module_info)?, ], )?; @@ -121,32 +116,33 @@ impl ModuleInfoCache { pub fn as_module_analyzer<'a>( &'a self, - parser: &'a dyn ModuleParser, + parsed_source_cache: &'a Arc, ) -> ModuleInfoCacheModuleAnalyzer<'a> { ModuleInfoCacheModuleAnalyzer { module_info_cache: self, - parser, + parsed_source_cache, } } } pub struct ModuleInfoCacheModuleAnalyzer<'a> { module_info_cache: &'a ModuleInfoCache, - parser: &'a dyn ModuleParser, + parsed_source_cache: &'a Arc, } +#[async_trait::async_trait(?Send)] impl<'a> deno_graph::ModuleAnalyzer for ModuleInfoCacheModuleAnalyzer<'a> { - fn analyze( + async fn analyze( &self, specifier: &ModuleSpecifier, source: Arc, media_type: MediaType, ) -> Result { // attempt to load from the cache - let source_hash = ModuleInfoCacheSourceHash::from_source(source.as_bytes()); + let source_hash = CacheDBHash::from_source(&source); match self .module_info_cache - .get_module_info(specifier, media_type, &source_hash) + .get_module_info(specifier, media_type, source_hash) { Ok(Some(info)) => return Ok(info), Ok(None) => {} @@ -160,16 +156,23 @@ impl<'a> deno_graph::ModuleAnalyzer for ModuleInfoCacheModuleAnalyzer<'a> { } // otherwise, get the module info from the parsed source cache - let analyzer = ParserModuleAnalyzer::new(self.parser); - let module_info = analyzer.analyze(specifier, source, media_type)?; + let module_info = deno_core::unsync::spawn_blocking({ + let cache = self.parsed_source_cache.clone(); + let specifier = specifier.clone(); + move || { + let parser = cache.as_capturing_parser(); + let analyzer = ParserModuleAnalyzer::new(&parser); + analyzer.analyze_sync(&specifier, source, media_type) + } + }) + .await + .unwrap()?; // then attempt to cache it - if let Err(err) = self.module_info_cache.set_module_info( - specifier, - media_type, - &source_hash, - &module_info, - ) { + if let Err(err) = + self.module_info_cache + .set_module_info(specifier, media_type, source_hash, &module_info) + { log::debug!( "Error saving module cache info for {}. {:#}", specifier, @@ -181,26 +184,24 @@ impl<'a> deno_graph::ModuleAnalyzer for ModuleInfoCacheModuleAnalyzer<'a> { } } -// todo(dsherret): change this to be stored as an integer next time -// the cache version is bumped -fn serialize_media_type(media_type: MediaType) -> &'static str { +fn serialize_media_type(media_type: MediaType) -> i64 { use MediaType::*; match media_type { - JavaScript => "1", - Jsx => "2", - Mjs => "3", - Cjs => "4", - TypeScript => "5", - Mts => "6", - Cts => "7", - Dts => "8", - Dmts => "9", - Dcts => "10", - Tsx => "11", - Json => "12", - Wasm => "13", - TsBuildInfo => "14", - SourceMap => "15", - Unknown => "16", + JavaScript => 1, + Jsx => 2, + Mjs => 3, + Cjs => 4, + TypeScript => 5, + Mts => 6, + Cts => 7, + Dts => 8, + Dmts => 9, + Dcts => 10, + Tsx => 11, + Json => 12, + Wasm => 13, + TsBuildInfo => 14, + SourceMap => 15, + Unknown => 16, } } diff --git a/crates/sb_core/cache/node.rs b/crates/sb_core/cache/node.rs index 4798a476b..b6b060693 100644 --- a/crates/sb_core/cache/node.rs +++ b/crates/sb_core/cache/node.rs @@ -1,21 +1,23 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use crate::cache::common::FastInsecureHasher; +use crate::node::CliCjsAnalysis; use deno_core::error::AnyError; use deno_core::serde_json; use deno_webstorage::rusqlite::params; -use sb_node::analyze::CliCjsAnalysis; use super::cache_db::CacheDB; use super::cache_db::CacheDBConfiguration; +use super::cache_db::CacheDBHash; use super::cache_db::CacheFailure; pub static NODE_ANALYSIS_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { - table_initializer: "CREATE TABLE IF NOT EXISTS cjsanalysiscache ( - specifier TEXT PRIMARY KEY, - source_hash TEXT NOT NULL, - data TEXT NOT NULL - );", + table_initializer: concat!( + "CREATE TABLE IF NOT EXISTS cjsanalysiscache (", + "specifier TEXT PRIMARY KEY,", + "source_hash INTEGER NOT NULL,", + "data TEXT NOT NULL", + ");" + ), on_version_change: "DELETE FROM cjsanalysiscache;", preheat_queries: &[], on_failure: CacheFailure::InMemory, @@ -33,10 +35,6 @@ impl NodeAnalysisCache { } } - pub fn compute_source_hash(text: &str) -> String { - FastInsecureHasher::hash(text).to_string() - } - fn ensure_ok(res: Result) -> T { match res { Ok(x) => x, @@ -57,7 +55,7 @@ impl NodeAnalysisCache { pub fn get_cjs_analysis( &self, specifier: &str, - expected_source_hash: &str, + expected_source_hash: CacheDBHash, ) -> Option { Self::ensure_ok(self.inner.get_cjs_analysis(specifier, expected_source_hash)) } @@ -65,7 +63,7 @@ impl NodeAnalysisCache { pub fn set_cjs_analysis( &self, specifier: &str, - source_hash: &str, + source_hash: CacheDBHash, cjs_analysis: &CliCjsAnalysis, ) { Self::ensure_ok( @@ -88,7 +86,7 @@ impl NodeAnalysisCacheInner { pub fn get_cjs_analysis( &self, specifier: &str, - expected_source_hash: &str, + expected_source_hash: CacheDBHash, ) -> Result, AnyError> { let query = " SELECT @@ -101,7 +99,7 @@ impl NodeAnalysisCacheInner { LIMIT 1"; let res = self .conn - .query_row(query, params![specifier, &expected_source_hash], |row| { + .query_row(query, params![specifier, expected_source_hash], |row| { let analysis_info: String = row.get(0)?; Ok(serde_json::from_str(&analysis_info)?) })?; @@ -111,7 +109,7 @@ impl NodeAnalysisCacheInner { pub fn set_cjs_analysis( &self, specifier: &str, - source_hash: &str, + source_hash: CacheDBHash, cjs_analysis: &CliCjsAnalysis, ) -> Result<(), AnyError> { let sql = " @@ -123,7 +121,7 @@ impl NodeAnalysisCacheInner { sql, params![ specifier, - &source_hash.to_string(), + source_hash, &serde_json::to_string(&cjs_analysis)?, ], )?; diff --git a/crates/sb_core/cert.rs b/crates/sb_core/cert.rs index f4c5c6b01..2aefdace8 100644 --- a/crates/sb_core/cert.rs +++ b/crates/sb_core/cert.rs @@ -1,8 +1,9 @@ +use anyhow::Context; use deno_core::error::AnyError; use deno_tls::deno_native_certs::load_native_certs; use deno_tls::rustls::RootCertStore; -use deno_tls::{rustls, rustls_pemfile, webpki_roots, RootCertStoreProvider}; -use std::io::{BufReader, Cursor}; +use deno_tls::{rustls_pemfile, webpki_roots, RootCertStoreProvider}; +use std::io::{BufReader, Cursor, Read}; use std::path::PathBuf; use thiserror::Error; @@ -63,24 +64,18 @@ pub fn get_root_cert_store( for store in ca_stores.iter() { match store.as_str() { "mozilla" => { - root_cert_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map( - |ta| { - rustls::OwnedTrustAnchor::from_subject_spki_name_constraints( - ta.subject, - ta.spki, - ta.name_constraints, - ) - }, - )); + root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); } + "system" => { let roots = load_native_certs().expect("could not load platform certs"); for root in roots { root_cert_store - .add(&rustls::Certificate(root.0)) + .add((&*root.0).into()) .expect("Failed to add platform cert to root cert store"); } } + _ => { return Err(RootCertStoreLoadError::UnknownStore(store.clone())); } @@ -89,7 +84,7 @@ pub fn get_root_cert_store( let ca_data = maybe_ca_data.or_else(|| std::env::var("DENO_CERT").ok().map(CaData::File)); if let Some(ca_data) = ca_data { - let result = match ca_data { + let mut reader: BufReader> = match ca_data { CaData::File(ca_file) => { let ca_file = if let Some(root) = &maybe_root_path { root.join(&ca_file) @@ -98,21 +93,22 @@ pub fn get_root_cert_store( }; let certfile = std::fs::File::open(ca_file) .map_err(|err| RootCertStoreLoadError::CaFileOpenError(err.to_string()))?; - let mut reader = BufReader::new(certfile); - rustls_pemfile::certs(&mut reader) - } - CaData::Bytes(data) => { - let mut reader = BufReader::new(Cursor::new(data)); - rustls_pemfile::certs(&mut reader) + BufReader::new(Box::new(certfile) as _) } + + CaData::Bytes(data) => BufReader::new(Box::new(Cursor::new(data)) as _), }; - match result { - Ok(certs) => { - root_cert_store.add_parsable_certificates(&certs); - } - Err(e) => { - return Err(RootCertStoreLoadError::FailedAddPemFile(e.to_string())); + for cert in rustls_pemfile::certs(&mut reader) { + if let Err(err) = cert + .with_context(|| "failed to load the certificate") + .and_then(|it| { + root_cert_store + .add(it.clone()) + .with_context(|| "error adding a certificate to the store") + }) + { + return Err(RootCertStoreLoadError::FailedAddPemFile(err.to_string())); } } } diff --git a/crates/sb_core/emit.rs b/crates/sb_core/emit.rs index 2a6df9afb..1be55ebb5 100644 --- a/crates/sb_core/emit.rs +++ b/crates/sb_core/emit.rs @@ -15,26 +15,29 @@ use std::sync::Arc; pub struct Emitter { emit_cache: EmitCache, parsed_source_cache: Arc, - emit_options: deno_ast::EmitOptions, - transpile_options: TranspileOptions, - // cached hash of the emit options - emit_options_hash: u64, + transpile_and_emit_options: Arc<(deno_ast::TranspileOptions, deno_ast::EmitOptions)>, + // cached hash of the transpile and emit options + transpile_and_emit_options_hash: u64, } impl Emitter { pub fn new( emit_cache: EmitCache, parsed_source_cache: Arc, - emit_options: deno_ast::EmitOptions, transpile_options: TranspileOptions, + emit_options: deno_ast::EmitOptions, ) -> Self { - let emit_options_hash = FastInsecureHasher::hash(&emit_options); + let transpile_and_emit_options_hash = { + let mut hasher = FastInsecureHasher::new_without_deno_version(); + hasher.write_hashable(&transpile_options); + hasher.write_hashable(&emit_options); + hasher.finish() + }; Self { emit_cache, parsed_source_cache, - emit_options, - emit_options_hash, - transpile_options, + transpile_and_emit_options: Arc::new((transpile_options, emit_options)), + transpile_and_emit_options_hash, } } @@ -81,14 +84,17 @@ impl Emitter { media_type, )?; - let transpiled_source = - parsed_source.transpile(&self.transpile_options, &self.emit_options)?; + let transpiled_source = parsed_source.transpile( + &self.transpile_and_emit_options.0, + &self.transpile_and_emit_options.1, + )?; let source = transpiled_source.into_source(); + let source_text = String::from_utf8(source.source)?; debug_assert!(source.source_map.is_none()); self.emit_cache - .set_emit_code(specifier, source_hash, &source.text.clone()); - Ok(source.text.into()) + .set_emit_code(specifier, source_hash, source_text.as_str()); + Ok(source_text.into()) } } @@ -96,9 +102,9 @@ impl Emitter { /// options then generates a string hash which can be stored to /// determine if the cached emit is valid or not. fn get_source_hash(&self, source_text: &str) -> u64 { - FastInsecureHasher::new() + FastInsecureHasher::new_without_deno_version() .write_str(source_text) - .write_u64(self.emit_options_hash) + .write_u64(self.transpile_and_emit_options_hash) .finish() } } diff --git a/crates/sb_core/file_fetcher.rs b/crates/sb_core/file_fetcher.rs deleted file mode 100644 index 32c86f630..000000000 --- a/crates/sb_core/file_fetcher.rs +++ /dev/null @@ -1,664 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use crate::auth_tokens::{AuthToken, AuthTokens}; -use crate::cache::fc_permissions::FcPermissions; -use crate::cache::CacheSetting; -use crate::util::http_util::{ - resolve_redirect_from_response, CacheSemantics, HeadersMap, HttpClient, -}; -use crate::util::{http_util, text_encoding}; -use data_url::DataUrl; -use deno_ast::MediaType; -use deno_cache_dir::HttpCache; -use deno_core::error::custom_error; -use deno_core::error::generic_error; -use deno_core::error::uri_error; -use deno_core::error::AnyError; -use deno_core::futures; -use deno_core::futures::future::FutureExt; -use deno_core::parking_lot::Mutex; -use deno_core::url::Url; -use deno_core::ModuleSpecifier; -use deno_fetch::reqwest::header::HeaderValue; -use deno_fetch::reqwest::header::ACCEPT; -use deno_fetch::reqwest::header::AUTHORIZATION; -use deno_fetch::reqwest::header::IF_NONE_MATCH; -use deno_fetch::reqwest::StatusCode; -use deno_web::BlobStore; -use log::debug; -use std::collections::HashMap; -use std::env; -use std::fs; -use std::future::Future; -use std::path::PathBuf; -use std::pin::Pin; -use std::sync::Arc; -use std::time::SystemTime; - -pub const SUPPORTED_SCHEMES: [&str; 5] = ["data", "blob", "file", "http", "https"]; - -/// A structure representing a source file. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct File { - /// For remote files, if there was an `X-TypeScript-Type` header, the parsed - /// out value of that header. - pub maybe_types: Option, - /// The resolved media type for the file. - pub media_type: MediaType, - /// The source of the file as a string. - pub source: Arc, - /// The _final_ specifier for the file. The requested specifier and the final - /// specifier maybe different for remote files that have been redirected. - pub specifier: ModuleSpecifier, - - pub maybe_headers: Option>, -} - -/// Simple struct implementing in-process caching to prevent multiple -/// fs reads/net fetches for same file. -#[derive(Debug, Clone, Default)] -pub struct FileCache(Arc>>); - -impl FileCache { - pub fn get(&self, specifier: &ModuleSpecifier) -> Option { - let cache = self.0.lock(); - cache.get(specifier).cloned() - } - - pub fn insert(&self, specifier: ModuleSpecifier, file: File) -> Option { - let mut cache = self.0.lock(); - cache.insert(specifier, file) - } -} - -/// Fetch a source file from the local file system. -fn fetch_local(specifier: &ModuleSpecifier) -> Result { - let local = specifier - .to_file_path() - .map_err(|_| uri_error(format!("Invalid file path.\n Specifier: {specifier}")))?; - let bytes = fs::read(local)?; - let charset = text_encoding::detect_charset(&bytes).to_string(); - let source = get_source_from_bytes(bytes, Some(charset))?; - let media_type = MediaType::from_specifier(specifier); - - Ok(File { - maybe_types: None, - media_type, - source: source.into(), - specifier: specifier.clone(), - maybe_headers: None, - }) -} - -/// Returns the decoded body and content-type of a provided -/// data URL. -pub fn get_source_from_data_url(specifier: &ModuleSpecifier) -> Result<(String, String), AnyError> { - let data_url = DataUrl::process(specifier.as_str()).map_err(|e| uri_error(format!("{e:?}")))?; - let mime = data_url.mime_type(); - let charset = mime.get_parameter("charset").map(|v| v.to_string()); - let (bytes, _) = data_url - .decode_to_vec() - .map_err(|e| uri_error(format!("{e:?}")))?; - Ok((get_source_from_bytes(bytes, charset)?, format!("{mime}"))) -} - -/// Given a vector of bytes and optionally a charset, decode the bytes to a -/// string. -pub fn get_source_from_bytes( - bytes: Vec, - maybe_charset: Option, -) -> Result { - let source = if let Some(charset) = maybe_charset { - text_encoding::convert_to_utf8(&bytes, &charset)?.to_string() - } else { - String::from_utf8(bytes)? - }; - - Ok(source) -} - -/// Return a validated scheme for a given module specifier. -fn get_validated_scheme(specifier: &ModuleSpecifier) -> Result { - let scheme = specifier.scheme(); - if !SUPPORTED_SCHEMES.contains(&scheme) { - Err(generic_error(format!( - "Unsupported scheme \"{scheme}\" for module \"{specifier}\". Supported schemes: {SUPPORTED_SCHEMES:#?}" - ))) - } else { - Ok(scheme.to_string()) - } -} - -/// Resolve a media type and optionally the charset from a module specifier and -/// the value of a content type header. -pub fn map_content_type( - specifier: &ModuleSpecifier, - maybe_content_type: Option<&String>, -) -> (MediaType, Option) { - if let Some(content_type) = maybe_content_type { - let mut content_types = content_type.split(';'); - let content_type = content_types.next().unwrap(); - let media_type = MediaType::from_content_type(specifier, content_type); - let charset = content_types - .map(str::trim) - .find_map(|s| s.strip_prefix("charset=")) - .map(String::from); - - (media_type, charset) - } else { - (MediaType::from_specifier(specifier), None) - } -} - -pub struct FetchOptions<'a> { - pub specifier: &'a ModuleSpecifier, - pub permissions: FcPermissions, - pub maybe_accept: Option<&'a str>, - pub maybe_cache_setting: Option<&'a CacheSetting>, -} - -/// A structure for resolving, fetching and caching source files. -#[derive(Debug, Clone)] -pub struct FileFetcher { - auth_tokens: AuthTokens, - allow_remote: bool, - cache: Arc, - cache_setting: CacheSetting, - http_cache: Arc, - http_client: Arc, - blob_store: Arc, - download_log_level: log::Level, -} - -impl FileFetcher { - pub fn new( - http_cache: Arc, - cache_setting: CacheSetting, - allow_remote: bool, - http_client: Arc, - blob_store: Arc, - file_cache: Arc, - ) -> Self { - Self { - auth_tokens: AuthTokens::new(env::var("DENO_AUTH_TOKENS").ok()), - allow_remote, - cache: file_cache, - cache_setting, - http_cache, - http_client, - blob_store, - download_log_level: log::Level::Info, - } - } - - pub fn cache_setting(&self) -> &CacheSetting { - &self.cache_setting - } - - /// Sets the log level to use when outputting the download message. - pub fn set_download_log_level(&mut self, level: log::Level) { - self.download_log_level = level; - } - - /// Creates a `File` structure for a remote file. - fn build_remote_file( - &self, - specifier: &ModuleSpecifier, - bytes: Vec, - headers: &HashMap, - ) -> Result { - let maybe_content_type = headers.get("content-type"); - let (media_type, maybe_charset) = map_content_type(specifier, maybe_content_type); - let source = get_source_from_bytes(bytes, maybe_charset)?; - let maybe_types = match media_type { - MediaType::JavaScript | MediaType::Cjs | MediaType::Mjs | MediaType::Jsx => { - headers.get("x-typescript-types").cloned() - } - _ => None, - }; - - Ok(File { - maybe_types, - media_type, - source: source.into(), - specifier: specifier.clone(), - maybe_headers: Some(headers.clone()), - }) - } - - /// Fetch cached remote file. - /// - /// This is a recursive operation if source file has redirections. - pub fn fetch_cached( - &self, - specifier: &ModuleSpecifier, - redirect_limit: i64, - ) -> Result, AnyError> { - debug!("FileFetcher::fetch_cached - specifier: {}", specifier); - if redirect_limit < 0 { - return Err(custom_error("Http", "Too many redirects.")); - } - - let cache_key = self.http_cache.cache_item_key(specifier)?; // compute this once - let Some(metadata) = self.http_cache.read_metadata(&cache_key)? else { - return Ok(None); - }; - let headers = metadata.headers; - if let Some(redirect_to) = headers.get("location") { - let redirect = deno_core::resolve_import(redirect_to, specifier.as_str())?; - return self.fetch_cached(&redirect, redirect_limit - 1); - } - let Some(bytes) = self.http_cache.read_file_bytes(&cache_key)? else { - return Ok(None); - }; - let file = self.build_remote_file(specifier, bytes, &headers)?; - - Ok(Some(file)) - } - - /// Convert a data URL into a file, resulting in an error if the URL is - /// invalid. - fn fetch_data_url(&self, specifier: &ModuleSpecifier) -> Result { - debug!("FileFetcher::fetch_data_url() - specifier: {}", specifier); - let (source, content_type) = get_source_from_data_url(specifier)?; - let (media_type, _) = map_content_type(specifier, Some(&content_type)); - let mut headers = HashMap::new(); - headers.insert("content-type".to_string(), content_type); - Ok(File { - maybe_types: None, - media_type, - source: source.into(), - specifier: specifier.clone(), - maybe_headers: Some(headers), - }) - } - - /// Get a blob URL. - async fn fetch_blob_url(&self, specifier: &ModuleSpecifier) -> Result { - debug!("FileFetcher::fetch_blob_url() - specifier: {}", specifier); - let blob = self - .blob_store - .get_object_url(specifier.clone()) - .ok_or_else(|| { - custom_error("NotFound", format!("Blob URL not found: \"{specifier}\".")) - })?; - - let content_type = blob.media_type.clone(); - let bytes = blob.read_all().await?; - - let (media_type, maybe_charset) = map_content_type(specifier, Some(&content_type)); - let source = get_source_from_bytes(bytes, maybe_charset)?; - let mut headers = HashMap::new(); - headers.insert("content-type".to_string(), content_type); - - Ok(File { - maybe_types: None, - media_type, - source: source.into(), - specifier: specifier.clone(), - maybe_headers: Some(headers), - }) - } - - /// Asynchronously fetch remote source file specified by the URL following - /// redirects. - /// - /// **Note** this is a recursive method so it can't be "async", but needs to - /// return a `Pin>`. - fn fetch_remote( - &self, - specifier: &ModuleSpecifier, - mut permissions: FcPermissions, - redirect_limit: i64, - maybe_accept: Option, - cache_setting: &CacheSetting, - ) -> Pin> + Send>> { - debug!("FileFetcher::fetch_remote() - specifier: {}", specifier); - if redirect_limit < 0 { - return futures::future::err(custom_error("Http", "Too many redirects.")).boxed(); - } - - if let Err(err) = permissions.check_specifier(specifier) { - return futures::future::err(err).boxed(); - } - - if self.should_use_cache(specifier, cache_setting) { - match self.fetch_cached(specifier, redirect_limit) { - Ok(Some(file)) => { - return futures::future::ok(file).boxed(); - } - Ok(None) => {} - Err(err) => { - return futures::future::err(err).boxed(); - } - } - } - - if *cache_setting == CacheSetting::Only { - return futures::future::err(custom_error( - "NotCached", - format!( - "Specifier not found in cache: \"{specifier}\", --cached-only is specified." - ), - )) - .boxed(); - } - - log::log!( - self.download_log_level, - "{} {}", - format!("Download"), - specifier - ); - - let maybe_etag = self - .http_cache - .cache_item_key(specifier) - .ok() - .and_then(|key| self.http_cache.read_metadata(&key).ok().flatten()) - .and_then(|metadata| metadata.headers.get("etag").cloned()); - let maybe_auth_token = self.auth_tokens.get(specifier); - let specifier = specifier.clone(); - let client = self.http_client.clone(); - let file_fetcher = self.clone(); - let cache_setting = cache_setting.clone(); - // A single pass of fetch either yields code or yields a redirect, server - // error causes a single retry to avoid crashing hard on intermittent failures. - - async fn handle_request_or_server_error( - retried: &mut bool, - specifier: &Url, - err_str: String, - ) -> Result<(), AnyError> { - // Retry once, and bail otherwise. - if !*retried { - *retried = true; - log::debug!("Import '{}' failed: {}. Retrying...", specifier, err_str); - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - Ok(()) - } else { - Err(generic_error(format!( - "Import '{}' failed: {}", - specifier, err_str - ))) - } - } - - async move { - let mut retried = false; - loop { - let result = match fetch_once( - &client, - FetchOnceArgs { - url: specifier.clone(), - maybe_accept: maybe_accept.clone(), - maybe_etag: maybe_etag.clone(), - maybe_auth_token: maybe_auth_token.clone(), - }, - ) - .await? - { - FetchOnceResult::NotModified => { - let file = file_fetcher.fetch_cached(&specifier, 10)?.unwrap(); - Ok(file) - } - FetchOnceResult::Redirect(redirect_url, headers) => { - file_fetcher.http_cache.set(&specifier, headers, &[])?; - file_fetcher - .fetch_remote( - &redirect_url, - permissions, - redirect_limit - 1, - maybe_accept, - &cache_setting, - ) - .await - } - FetchOnceResult::Code(bytes, headers) => { - file_fetcher - .http_cache - .set(&specifier, headers.clone(), &bytes)?; - let file = file_fetcher.build_remote_file(&specifier, bytes, &headers)?; - Ok(file) - } - FetchOnceResult::RequestError(err) => { - handle_request_or_server_error(&mut retried, &specifier, err).await?; - continue; - } - FetchOnceResult::ServerError(status) => { - handle_request_or_server_error( - &mut retried, - &specifier, - status.to_string(), - ) - .await?; - continue; - } - }; - break result; - } - } - .boxed() - } - - /// Returns if the cache should be used for a given specifier. - fn should_use_cache(&self, specifier: &ModuleSpecifier, cache_setting: &CacheSetting) -> bool { - match cache_setting { - CacheSetting::ReloadAll => false, - CacheSetting::Use | CacheSetting::Only => true, - CacheSetting::RespectHeaders => { - let Ok(cache_key) = self.http_cache.cache_item_key(specifier) else { - return false; - }; - let Ok(Some(metadata)) = self.http_cache.read_metadata(&cache_key) else { - return false; - }; - let cache_semantics = - CacheSemantics::new(metadata.headers, metadata.time, SystemTime::now()); - cache_semantics.should_use() - } - CacheSetting::ReloadSome(list) => { - let mut url = specifier.clone(); - url.set_fragment(None); - if list.iter().any(|x| x == url.as_str()) { - return false; - } - url.set_query(None); - let mut path = PathBuf::from(url.as_str()); - loop { - if list.contains(&path.to_str().unwrap().to_string()) { - return false; - } - if !path.pop() { - break; - } - } - true - } - } - } - - /// Fetch a source file and asynchronously return it. - pub async fn fetch( - &self, - specifier: &ModuleSpecifier, - permissions: FcPermissions, - ) -> Result { - self.fetch_with_options(FetchOptions { - specifier, - permissions, - maybe_accept: None, - maybe_cache_setting: None, - }) - .await - } - - pub async fn fetch_with_options( - &self, - mut options: FetchOptions<'_>, - ) -> Result { - let specifier = options.specifier.clone(); - debug!("FileFetcher::fetch() - specifier: {}", specifier); - let scheme = get_validated_scheme(&specifier)?; - options.permissions.check_specifier(&specifier)?; - if let Some(file) = self.cache.get(&specifier) { - Ok(file) - } else if scheme == "file" { - // we do not in memory cache files, as this would prevent files on the - // disk changing effecting things like workers and dynamic imports. - fetch_local(&specifier) - } else if scheme == "data" { - self.fetch_data_url(&specifier) - } else if scheme == "blob" { - self.fetch_blob_url(&specifier).await - } else if !self.allow_remote { - Err(custom_error( - "NoRemote", - format!("A remote specifier was requested: \"{specifier}\", but --no-remote is specified."), - )) - } else { - let def_sett = self.cache_setting.clone(); - let cache_settings = options.maybe_cache_setting.unwrap_or(&def_sett); - let result = self - .fetch_remote( - &specifier, - options.permissions, - 10, - options.maybe_accept.map(String::from), - cache_settings, - ) - .await; - if let Ok(file) = &result { - self.cache.insert(specifier.clone(), file.clone()); - } - result - } - } - - /// A synchronous way to retrieve a source file, where if the file has already - /// been cached in memory it will be returned, otherwise for local files will - /// be read from disk. - pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option { - let maybe_file = self.cache.get(specifier); - if maybe_file.is_none() { - let is_local = specifier.scheme() == "file"; - if is_local { - if let Ok(file) = fetch_local(specifier) { - return Some(file); - } - } - None - } else { - maybe_file - } - } - - /// Insert a temporary module into the in memory cache for the file fetcher. - pub fn insert_cached(&self, file: File) -> Option { - self.cache.insert(file.specifier.clone(), file) - } -} - -#[derive(Debug, Eq, PartialEq)] -enum FetchOnceResult { - Code(Vec, HeadersMap), - NotModified, - Redirect(Url, HeadersMap), - RequestError(String), - ServerError(StatusCode), -} - -#[derive(Debug)] -struct FetchOnceArgs { - pub url: Url, - pub maybe_accept: Option, - pub maybe_etag: Option, - pub maybe_auth_token: Option, -} - -/// Asynchronously fetches the given HTTP URL one pass only. -/// If no redirect is present and no error occurs, -/// yields Code(ResultPayload). -/// If redirect occurs, does not follow and -/// yields Redirect(url). -async fn fetch_once( - http_client: &HttpClient, - args: FetchOnceArgs, -) -> Result { - let mut request = http_client.get_no_redirect(args.url.clone())?; - - if let Some(etag) = args.maybe_etag { - let if_none_match_val = HeaderValue::from_str(&etag)?; - request = request.header(IF_NONE_MATCH, if_none_match_val); - } - if let Some(auth_token) = args.maybe_auth_token { - let authorization_val = HeaderValue::from_str(&auth_token.to_string())?; - request = request.header(AUTHORIZATION, authorization_val); - } - if let Some(accept) = args.maybe_accept { - let accepts_val = HeaderValue::from_str(&accept)?; - request = request.header(ACCEPT, accepts_val); - } - let response = match request.send().await { - Ok(resp) => resp, - Err(err) => { - if err.is_connect() || err.is_timeout() { - return Ok(FetchOnceResult::RequestError(err.to_string())); - } - return Err(err.into()); - } - }; - - if response.status() == StatusCode::NOT_MODIFIED { - return Ok(FetchOnceResult::NotModified); - } - - let mut result_headers = HashMap::new(); - let response_headers = response.headers(); - - if let Some(warning) = response_headers.get("X-Deno-Warning") { - log::warn!("{}", warning.to_str().unwrap()); - } - - for key in response_headers.keys() { - let key_str = key.to_string(); - let values = response_headers.get_all(key); - let values_str = values - .iter() - .map(|e| e.to_str().unwrap().to_string()) - .collect::>() - .join(","); - result_headers.insert(key_str, values_str); - } - - if response.status().is_redirection() { - let new_url = resolve_redirect_from_response(&args.url, &response)?; - return Ok(FetchOnceResult::Redirect(new_url, result_headers)); - } - - let status = response.status(); - - if status.is_server_error() { - return Ok(FetchOnceResult::ServerError(status)); - } - - if status.is_client_error() { - let err = if response.status() == StatusCode::NOT_FOUND { - custom_error( - "NotFound", - format!("Import '{}' failed, not found.", args.url), - ) - } else { - generic_error(format!( - "Import '{}' failed: {}", - args.url, - response.status() - )) - }; - return Err(err); - } - - let body = http_util::get_response_body_with_progress(response).await?; - - Ok(FetchOnceResult::Code(body, result_headers)) -} diff --git a/crates/sb_core/http.rs b/crates/sb_core/http.rs index 2ebab050f..76e86ece9 100644 --- a/crates/sb_core/http.rs +++ b/crates/sb_core/http.rs @@ -9,7 +9,7 @@ use deno_http::{HttpRequestReader, HttpStreamReadResource}; use deno_websocket::ws_create_server_stream; use futures::ready; use futures::{future::BoxFuture, FutureExt}; -use hyper::upgrade::{OnUpgrade, Parts}; +use hyper_v014::upgrade::{OnUpgrade, Parts}; use log::error; use serde::Serialize; use tokio::io::{copy_bidirectional, AsyncReadExt, AsyncWriteExt, DuplexStream}; @@ -217,7 +217,7 @@ async fn op_http_upgrade_websocket2( _ => return Err(http_error("cannot upgrade because request body was used")), }; - let upgraded = hyper::upgrade::on(request).await?; + let upgraded = hyper_v014::upgrade::on(request).await?; let Parts { io, read_buf, .. } = upgraded.downcast::().unwrap(); let (mut rw, conn_sync) = io .into_inner() diff --git a/crates/sb_core/js/bootstrap.js b/crates/sb_core/js/bootstrap.js index 942f8a2a2..d1f2647da 100644 --- a/crates/sb_core/js/bootstrap.js +++ b/crates/sb_core/js/bootstrap.js @@ -344,8 +344,8 @@ function warnOnDeprecatedApi(apiName, stack, ...suggestions) { ObjectAssign(internals, { warnOnDeprecatedApi }); function runtimeStart(target) { -/* core.setMacrotaskCallback(timers.handleTimerMacrotask); - core.setMacrotaskCallback(promiseRejectMacrotaskCallback);*/ + /* core.setMacrotaskCallback(timers.handleTimerMacrotask); + core.setMacrotaskCallback(promiseRejectMacrotaskCallback);*/ core.setWasmStreamingCallback(fetch.handleWasmStreaming); ops.op_set_format_exception_callback(formatException); @@ -379,7 +379,7 @@ let bootstrapMockFnThrowError = false; const MOCK_FN = () => { if (bootstrapMockFnThrowError) { throw new TypeError("called MOCK_FN"); - } + } }; const MAKE_HARD_ERR_FN = msg => { @@ -483,7 +483,7 @@ globalThis.bootstrapSBEdge = (opts, extraCtx) => { }); /// DISABLE SHARED MEMORY AND INSTALL MEM CHECK TIMING - + // NOTE: We should not allow user workers to use shared memory. This is // because they are not counted in the external memory statistics of the // individual isolates. @@ -497,7 +497,7 @@ globalThis.bootstrapSBEdge = (opts, extraCtx) => { const wasmMemoryPrototypeGrow = wasmMemoryCtor.prototype.grow; function patchedWasmMemoryPrototypeGrow(delta) { - let mem = wasmMemoryPrototypeGrow.call(this, delta); + const mem = wasmMemoryPrototypeGrow.call(this, delta); ops.op_schedule_mem_check(); @@ -505,7 +505,7 @@ globalThis.bootstrapSBEdge = (opts, extraCtx) => { } wasmMemoryCtor.prototype.grow = patchedWasmMemoryPrototypeGrow; - + function patchedWasmMemoryCtor(maybeOpts) { if (typeof maybeOpts === "object" && maybeOpts["shared"] === true) { throw new TypeError("Creating a shared memory is not supported"); @@ -530,14 +530,14 @@ globalThis.bootstrapSBEdge = (opts, extraCtx) => { }), ), }); - + const apiNames = ObjectKeys(PATCH_DENO_API_LIST); for (const name of apiNames) { const value = PATCH_DENO_API_LIST[name]; if (value === false) { - delete Deno[name]; + delete Deno[name]; } else if (typeof value === 'function') { Deno[name] = value; } @@ -554,7 +554,12 @@ globalThis.bootstrapSBEdge = (opts, extraCtx) => { const nodeBootstrap = globalThis.nodeBootstrap; if (nodeBootstrap) { - nodeBootstrap(false, undefined); + nodeBootstrap({ + usesLocalNodeModulesDir: false, + argv: void 0, + nodeDebug: Deno.env.get("NODE_DEBUG") ?? "" + }); + delete globalThis.nodeBootstrap; } diff --git a/crates/sb_core/js/http.js b/crates/sb_core/js/http.js index 139d478de..347326a7d 100644 --- a/crates/sb_core/js/http.js +++ b/crates/sb_core/js/http.js @@ -55,7 +55,7 @@ function serveHttp(conn) { const [connRid, watcherRid] = ops.op_http_start(conn[internalRidSymbol]); const httpConn = new HttpConn(connRid, conn.remoteAddr, conn.localAddr); - + httpConn.nextRequest = async () => { const nextRequest = await HttpConnPrototypeNextRequest.call(httpConn); @@ -82,8 +82,8 @@ function serveHttp(conn) { return httpConn; } -async function serve(args1, args2) { - let options = { +function serve(args1, args2) { + const options = { port: 9999, hostname: "0.0.0.0", transport: "tcp", @@ -186,7 +186,7 @@ async function respond(requestEvent, httpConn, options) { } setTimeout(async () => { - let { + const { status, headers } = await ops.op_http_upgrade_raw2_fence(fenceRid); @@ -219,7 +219,7 @@ function closeHttpConn(httpConn) { httpConn.close(); } catch { // connection has already been closed - } + } } function getSupabaseTag(request) { @@ -228,7 +228,7 @@ function getSupabaseTag(request) { function applySupabaseTag(src, dest) { if ( - !ObjectPrototypeIsPrototypeOf(RequestPrototype, src) + !ObjectPrototypeIsPrototypeOf(RequestPrototype, src) || !ObjectPrototypeIsPrototypeOf(RequestPrototype, dest) ) { throw new TypeError("Only Request instance can apply the supabase tag"); @@ -240,7 +240,7 @@ function applySupabaseTag(src, dest) { internals.getSupabaseTag = getSupabaseTag; internals.RAW_UPGRADE_RESPONSE_SENTINEL = RAW_UPGRADE_RESPONSE_SENTINEL; -export { +export { serve, serveHttp, getSupabaseTag, diff --git a/crates/sb_core/lib.rs b/crates/sb_core/lib.rs index 5368b3368..ed9722f66 100644 --- a/crates/sb_core/lib.rs +++ b/crates/sb_core/lib.rs @@ -24,10 +24,11 @@ pub mod conn_sync; pub mod emit; pub mod errors_rt; pub mod external_memory; -pub mod file_fetcher; pub mod http; pub mod http_start; pub mod net; +pub mod node; +pub mod npm; pub mod permissions; pub mod runtime; pub mod transpiler; diff --git a/crates/sb_module_loader/node/cjs_code_anaylzer.rs b/crates/sb_core/node.rs similarity index 54% rename from crates/sb_module_loader/node/cjs_code_anaylzer.rs rename to crates/sb_core/node.rs index 87e775d3b..ee8670273 100644 --- a/crates/sb_module_loader/node/cjs_code_anaylzer.rs +++ b/crates/sb_core/node.rs @@ -1,14 +1,18 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use std::sync::Arc; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; -use deno_fs; -use sb_core::cache::node::NodeAnalysisCache; -use sb_core::util::fs::canonicalize_path_maybe_not_exists; +use sb_node::analyze::CjsAnalysis as ExtNodeCjsAnalysis; +use sb_node::analyze::CjsAnalysisExports; use sb_node::analyze::CjsCodeAnalyzer; use sb_node::analyze::NodeCodeTranslator; -use sb_node::analyze::{CjsAnalysis as ExtNodeCjsAnalysis, CjsAnalysisExports, CliCjsAnalysis}; +use serde::{Deserialize, Serialize}; + +use crate::{ + cache::{cache_db::CacheDBHash, node::NodeAnalysisCache}, + util::fs::canonicalize_path_maybe_not_exists, +}; pub type CliNodeCodeTranslator = NodeCodeTranslator; @@ -29,6 +33,17 @@ pub fn resolve_specifier_into_node_modules(specifier: &ModuleSpecifier) -> Modul .unwrap_or_else(|| specifier.clone()) } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CliCjsAnalysis { + /// The module was found to be an ES module. + Esm, + /// The module was CJS. + Cjs { + exports: Vec, + reexports: Vec, + }, +} + pub struct CliCjsCodeAnalyzer { cache: NodeAnalysisCache, fs: deno_fs::FileSystemRc, @@ -39,16 +54,13 @@ impl CliCjsCodeAnalyzer { Self { cache, fs } } - fn inner_cjs_analysis( + async fn inner_cjs_analysis( &self, specifier: &ModuleSpecifier, source: &str, ) -> Result { - let source_hash = NodeAnalysisCache::compute_source_hash(source); - if let Some(analysis) = self - .cache - .get_cjs_analysis(specifier.as_str(), &source_hash) - { + let source_hash = CacheDBHash::from_source(source); + if let Some(analysis) = self.cache.get_cjs_analysis(specifier.as_str(), source_hash) { return Ok(analysis); } @@ -60,43 +72,55 @@ impl CliCjsCodeAnalyzer { }); } - let parsed_source = deno_ast::parse_program(deno_ast::ParseParams { - specifier: specifier.clone(), - text_info: deno_ast::SourceTextInfo::new(source.into()), - media_type, - capture_tokens: true, - scope_analysis: false, - maybe_syntax: None, - })?; - let analysis = if parsed_source.is_script() { - let analysis = parsed_source.analyze_cjs(); - CliCjsAnalysis::Cjs { - exports: analysis.exports, - reexports: analysis.reexports, + let analysis = deno_core::unsync::spawn_blocking({ + let specifier = specifier.clone(); + let source: Arc = source.into(); + move || -> Result<_, deno_ast::ParseDiagnostic> { + let parsed_source = deno_ast::parse_program(deno_ast::ParseParams { + specifier, + text: source, + media_type, + capture_tokens: true, + scope_analysis: false, + maybe_syntax: None, + })?; + if parsed_source.is_script() { + let analysis = parsed_source.analyze_cjs(); + Ok(CliCjsAnalysis::Cjs { + exports: analysis.exports, + reexports: analysis.reexports, + }) + } else { + Ok(CliCjsAnalysis::Esm) + } } - } else { - CliCjsAnalysis::Esm - }; + }) + .await + .unwrap()?; + self.cache - .set_cjs_analysis(specifier.as_str(), &source_hash, &analysis); + .set_cjs_analysis(specifier.as_str(), source_hash, &analysis); Ok(analysis) } } +#[async_trait::async_trait(?Send)] impl CjsCodeAnalyzer for CliCjsCodeAnalyzer { - fn analyze_cjs( + async fn analyze_cjs( &self, specifier: &ModuleSpecifier, source: Option, ) -> Result { let source = match source { Some(source) => source, - None => self - .fs - .read_text_file_sync(&specifier.to_file_path().unwrap(), None)?, + None => { + self.fs + .read_text_file_lossy_async(specifier.to_file_path().unwrap(), None) + .await? + } }; - let analysis = self.inner_cjs_analysis(specifier, &source)?; + let analysis = self.inner_cjs_analysis(specifier, &source).await?; match analysis { CliCjsAnalysis::Esm => Ok(ExtNodeCjsAnalysis::Esm(source)), CliCjsAnalysis::Cjs { exports, reexports } => { diff --git a/crates/sb_core/npm.rs b/crates/sb_core/npm.rs new file mode 100644 index 000000000..9711f159e --- /dev/null +++ b/crates/sb_core/npm.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; + +use deno_core::url::Url; + +use deno_npm::npm_rc::ResolvedNpmRc; +use once_cell::sync::Lazy; + +pub fn npm_registry_url() -> &'static Url { + static NPM_REGISTRY_DEFAULT_URL: Lazy = Lazy::new(|| { + let env_var_name = "NPM_CONFIG_REGISTRY"; + if let Ok(registry_url) = std::env::var(env_var_name) { + // ensure there is a trailing slash for the directory + let registry_url = format!("{}/", registry_url.trim_end_matches('/')); + match Url::parse(®istry_url) { + Ok(url) => { + return url; + } + Err(err) => { + log::debug!("Invalid {} environment variable: {:#}", env_var_name, err,); + } + } + } + + Url::parse("https://registry.npmjs.org").unwrap() + }); + + &NPM_REGISTRY_DEFAULT_URL +} + +pub fn create_default_npmrc() -> Arc { + Arc::new(ResolvedNpmRc { + default_config: deno_npm::npm_rc::RegistryConfigWithUrl { + registry_url: npm_registry_url().clone(), + config: Default::default(), + }, + scopes: Default::default(), + registry_configs: Default::default(), + }) +} diff --git a/crates/sb_core/permissions.rs b/crates/sb_core/permissions.rs index 6b4656e44..b9edc9150 100644 --- a/crates/sb_core/permissions.rs +++ b/crates/sb_core/permissions.rs @@ -223,24 +223,24 @@ impl sb_node::NodePermissions for Permissions { Ok(()) } - fn check_read(&self, _path: &Path) -> Result<(), AnyError> { + fn check_read(&mut self, _path: &Path) -> Result<(), AnyError> { Ok(()) } fn check_read_with_api_name( - &self, + &mut self, _path: &Path, _api_name: Option<&str>, ) -> Result<(), AnyError> { Ok(()) } - fn check_sys(&self, _kind: &str, _api_name: &str) -> Result<(), AnyError> { + fn check_sys(&mut self, _kind: &str, _api_name: &str) -> Result<(), AnyError> { Ok(()) } fn check_write_with_api_name( - &self, + &mut self, _path: &Path, _api_name: Option<&str>, ) -> Result<(), AnyError> { diff --git a/crates/sb_core/transpiler.rs b/crates/sb_core/transpiler.rs index cbfe7ad8d..88766148c 100644 --- a/crates/sb_core/transpiler.rs +++ b/crates/sb_core/transpiler.rs @@ -1,4 +1,4 @@ -use deno_ast::{MediaType, ParseParams, SourceMapOption, SourceTextInfo}; +use deno_ast::{MediaType, ParseParams, SourceMapOption}; use deno_core::error::AnyError; use deno_core::{ModuleCodeString, ModuleName, SourceMapData}; use std::path::Path; @@ -26,7 +26,7 @@ pub fn maybe_transpile_source( let parsed = deno_ast::parse_module(ParseParams { specifier: deno_core::url::Url::parse(&name).unwrap(), - text_info: SourceTextInfo::from_string(source.as_str().to_owned()), + text: source.into(), media_type, capture_tokens: false, scope_analysis: false, @@ -51,9 +51,8 @@ pub fn maybe_transpile_source( )? .into_source(); - let maybe_source_map: Option = transpiled_source - .source_map - .map(|sm| sm.into_bytes().into()); + let maybe_source_map: Option = transpiled_source.source_map.map(|sm| sm.into()); + let source_text = String::from_utf8(transpiled_source.source)?; - Ok((transpiled_source.text.into(), maybe_source_map)) + Ok((source_text.into(), maybe_source_map)) } diff --git a/crates/sb_core/util/errors.rs b/crates/sb_core/util/errors.rs index f408f7edd..3a00d3cce 100644 --- a/crates/sb_core/util/errors.rs +++ b/crates/sb_core/util/errors.rs @@ -1,7 +1,7 @@ use deno_ast::ParseDiagnostic; use deno_core::error::AnyError; -use deno_graph::ResolutionError; use deno_graph::{ModuleError, ModuleGraphError}; +use deno_graph::{ModuleLoadError, ResolutionError}; use import_map::ImportMapError; fn get_import_map_error_class(_: &ImportMapError) -> &'static str { @@ -13,23 +13,57 @@ fn get_diagnostic_class(_: &ParseDiagnostic) -> &'static str { } fn get_module_graph_error_class(err: &ModuleGraphError) -> &'static str { + use deno_graph::JsrLoadError; + use deno_graph::NpmLoadError; + use deno_graph::WorkspaceLoadError; + match err { - ModuleGraphError::ModuleError(err) => match err { - ModuleError::LoadingErr(_, _, err) => get_error_class_name(err.as_ref()), - ModuleError::InvalidTypeAssertion { .. } => "SyntaxError", - ModuleError::ParseErr(_, diagnostic) => get_diagnostic_class(diagnostic), - ModuleError::UnsupportedMediaType { .. } - | ModuleError::UnsupportedImportAttributeType { .. } => "TypeError", - ModuleError::Missing(_, _) - | ModuleError::MissingDynamic(_, _) - | ModuleError::MissingWorkspaceMemberExports { .. } - | ModuleError::UnknownExport { .. } - | ModuleError::UnknownPackage { .. } - | ModuleError::UnknownPackageReq { .. } => "NotFound", - }, ModuleGraphError::ResolutionError(err) | ModuleGraphError::TypesResolutionError(err) => { get_resolution_error_class(err) } + + ModuleGraphError::ModuleError(err) => { + match err { + ModuleError::InvalidTypeAssertion { .. } => "SyntaxError", + ModuleError::ParseErr(_, diagnostic) => get_diagnostic_class(diagnostic), + ModuleError::UnsupportedMediaType { .. } + | ModuleError::UnsupportedImportAttributeType { .. } => "TypeError", + ModuleError::Missing(_, _) | ModuleError::MissingDynamic(_, _) => "NotFound", + + ModuleError::LoadingErr(_, _, err) => match err { + ModuleLoadError::Loader(err) => get_error_class_name(err.as_ref()), + ModuleLoadError::HttpsChecksumIntegrity(_) + | ModuleLoadError::TooManyRedirects => "Error", + ModuleLoadError::NodeUnknownBuiltinModule(_) => "NotFound", + ModuleLoadError::Decode(_) => "TypeError", + ModuleLoadError::Npm(err) => match err { + NpmLoadError::NotSupportedEnvironment + | NpmLoadError::PackageReqResolution(_) + | NpmLoadError::RegistryInfo(_) => "Error", + NpmLoadError::PackageReqReferenceParse(_) => "TypeError", + }, + ModuleLoadError::Jsr(err) => match err { + JsrLoadError::UnsupportedManifestChecksum + | JsrLoadError::PackageFormat(_) => "TypeError", + JsrLoadError::ContentLoadExternalSpecifier + | JsrLoadError::ContentLoad(_) + | JsrLoadError::ContentChecksumIntegrity(_) + | JsrLoadError::PackageManifestLoad(_, _) + | JsrLoadError::PackageVersionManifestChecksumIntegrity(..) + | JsrLoadError::PackageVersionManifestLoad(_, _) + | JsrLoadError::RedirectInPackage(_) => "Error", + JsrLoadError::PackageNotFound(_) + | JsrLoadError::PackageReqNotFound(_) + | JsrLoadError::PackageVersionNotFound(_) + | JsrLoadError::UnknownExport { .. } => "NotFound", + }, + ModuleLoadError::Workspace(err) => match err { + WorkspaceLoadError::MemberInvalidExportPath { .. } => "TypeError", + WorkspaceLoadError::MissingMemberExports { .. } => "NotFound", + }, + }, + } + } } } diff --git a/crates/sb_core/util/fs.rs b/crates/sb_core/util/fs.rs index ef20e585d..b0cf0673b 100644 --- a/crates/sb_core/util/fs.rs +++ b/crates/sb_core/util/fs.rs @@ -17,6 +17,34 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +/// Writes the file to the file system at a temporary path, then +/// renames it to the destination in a single sys call in order +/// to never leave the file system in a corrupted state. +/// +/// This also handles creating the directory if a NotFound error +/// occurs. +pub fn atomic_write_file_with_retries>( + file_path: &Path, + data: T, + mode: u32, +) -> std::io::Result<()> { + let mut count = 0; + loop { + match atomic_write_file(file_path, data.as_ref(), mode) { + Ok(()) => return Ok(()), + Err(err) => { + if count >= 5 { + // too many retries, return the error + return Err(err); + } + count += 1; + let sleep_ms = std::cmp::min(50, 10 * count); + std::thread::sleep(std::time::Duration::from_millis(sleep_ms)); + } + } + } +} + /// Writes the file to the file system at a temporary path, then /// renames it to the destination in a single sys call in order /// to never leave the file system in a corrupted state. @@ -180,6 +208,74 @@ pub fn resolve_from_cwd(path: &Path) -> Result { Ok(normalize_path(resolved_path)) } +mod clone_dir_imp { + + #[cfg(target_vendor = "apple")] + mod apple { + use super::super::copy_dir_recursive; + use deno_core::error::AnyError; + use std::os::unix::ffi::OsStrExt; + use std::path::Path; + fn clonefile(from: &Path, to: &Path) -> std::io::Result<()> { + let from = std::ffi::CString::new(from.as_os_str().as_bytes())?; + let to = std::ffi::CString::new(to.as_os_str().as_bytes())?; + // SAFETY: `from` and `to` are valid C strings. + let ret = unsafe { libc::clonefile(from.as_ptr(), to.as_ptr(), 0) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + } + + pub fn clone_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> { + if let Some(parent) = to.parent() { + std::fs::create_dir_all(parent)?; + } + // Try to clone the whole directory + if let Err(err) = clonefile(from, to) { + if err.kind() != std::io::ErrorKind::AlreadyExists { + log::warn!( + "Failed to clone dir {:?} to {:?} via clonefile: {}", + from, + to, + err + ); + } + // clonefile won't overwrite existing files, so if the dir exists + // we need to handle it recursively. + copy_dir_recursive(from, to)?; + } + + Ok(()) + } + } + + #[cfg(target_vendor = "apple")] + pub(super) use apple::clone_dir_recursive; + + #[cfg(not(target_vendor = "apple"))] + pub(super) fn clone_dir_recursive( + from: &std::path::Path, + to: &std::path::Path, + ) -> Result<(), deno_core::error::AnyError> { + if let Err(e) = super::hard_link_dir_recursive(from, to) { + log::debug!("Failed to hard link dir {:?} to {:?}: {}", from, to, e); + super::copy_dir_recursive(from, to)?; + } + + Ok(()) + } +} + +/// Clones a directory to another directory. The exact method +/// is not guaranteed - it may be a hardlink, copy, or other platform-specific +/// operation. +/// +/// Note: Does not handle symlinks. +pub fn clone_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> { + clone_dir_imp::clone_dir_recursive(from, to) +} + /// Copies a directory to another directory. /// /// Note: Does not handle symlinks. diff --git a/crates/sb_core/util/http_util.rs b/crates/sb_core/util/http_util.rs index 03d17fd62..40a1fea89 100644 --- a/crates/sb_core/util/http_util.rs +++ b/crates/sb_core/util/http_util.rs @@ -1,22 +1,33 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use crate::auth_tokens::AuthToken; use crate::util::versions_util::get_user_agent; use cache_control::Cachability; use cache_control::CacheControl; use chrono::DateTime; -use deno_core::anyhow::bail; use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; use deno_core::url::Url; use deno_fetch::create_http_client; use deno_fetch::reqwest; +use deno_fetch::reqwest::header::HeaderName; +use deno_fetch::reqwest::header::HeaderValue; +use deno_fetch::reqwest::header::ACCEPT; +use deno_fetch::reqwest::header::AUTHORIZATION; +use deno_fetch::reqwest::header::IF_NONE_MATCH; use deno_fetch::reqwest::header::LOCATION; use deno_fetch::reqwest::Response; +use deno_fetch::reqwest::StatusCode; use deno_fetch::CreateHttpClientOptions; use deno_tls::RootCertStoreProvider; use std::collections::HashMap; use std::sync::Arc; +use std::thread::ThreadId; use std::time::Duration; use std::time::SystemTime; +use thiserror::Error; /// Construct the next uri based on base uri and location header fragment /// See @@ -48,7 +59,7 @@ fn resolve_url_from_location(base_url: &Url, location: &str) -> Url { pub fn resolve_redirect_from_response( request_url: &Url, response: &Response, -) -> Result { +) -> Result { debug_assert!(response.status().is_redirection()); if let Some(location) = response.headers().get(LOCATION) { let location_string = location.to_str()?; @@ -56,9 +67,9 @@ pub fn resolve_redirect_from_response( let new_url = resolve_url_from_location(request_url, location_string); Ok(new_url) } else { - Err(generic_error(format!( - "Redirection from '{request_url}' did not provide location header" - ))) + Err(DownloadError::NoRedirectHeader { + request_url: request_url.clone(), + }) } } @@ -208,13 +219,34 @@ impl CacheSemantics { } } -pub struct HttpClient { +#[derive(Debug, Eq, PartialEq)] +pub enum FetchOnceResult { + Code(Vec, HeadersMap), + NotModified, + Redirect(Url, HeadersMap), + RequestError(String), + ServerError(StatusCode), +} + +#[derive(Debug)] +pub struct FetchOnceArgs { + pub url: Url, + pub maybe_accept: Option, + pub maybe_etag: Option, + pub maybe_auth_token: Option, +} + +pub struct HttpClientProvider { options: CreateHttpClientOptions, root_cert_store_provider: Option>, - cell: once_cell::sync::OnceCell, + // it's not safe to share a reqwest::Client across tokio runtimes, + // so we store these Clients keyed by thread id + // https://github.com/seanmonstar/reqwest/issues/1148#issuecomment-910868788 + #[allow(clippy::disallowed_types)] // reqwest::Client allowed here + clients_by_thread_id: Mutex>, } -impl std::fmt::Debug for HttpClient { +impl std::fmt::Debug for HttpClientProvider { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("HttpClient") .field("options", &self.options) @@ -222,7 +254,7 @@ impl std::fmt::Debug for HttpClient { } } -impl HttpClient { +impl HttpClientProvider { pub fn new( root_cert_store_provider: Option>, unsafely_ignore_certificate_errors: Option>, @@ -233,99 +265,254 @@ impl HttpClient { ..Default::default() }, root_cert_store_provider, - cell: Default::default(), + clients_by_thread_id: Default::default(), } } - #[cfg(test)] - pub fn from_client(client: reqwest::Client) -> Self { - let result = Self { - options: Default::default(), - root_cert_store_provider: Default::default(), - cell: Default::default(), - }; - result.cell.set(client).unwrap(); - result + pub fn get_or_create(&self) -> Result { + use std::collections::hash_map::Entry; + let thread_id = std::thread::current().id(); + let mut clients = self.clients_by_thread_id.lock(); + let entry = clients.entry(thread_id); + match entry { + Entry::Occupied(entry) => Ok(HttpClient::new(entry.get().clone())), + Entry::Vacant(entry) => { + let client = create_http_client( + get_user_agent(), + CreateHttpClientOptions { + root_cert_store: match &self.root_cert_store_provider { + Some(provider) => Some(provider.get_or_try_init()?.clone()), + None => None, + }, + ..self.options.clone() + }, + )?; + entry.insert(client.clone()); + Ok(HttpClient::new(client)) + } + } } +} - fn client(&self) -> Result<&reqwest::Client, AnyError> { - self.cell.get_or_try_init(|| { - create_http_client( - get_user_agent(), - CreateHttpClientOptions { - root_cert_store: match &self.root_cert_store_provider { - Some(provider) => Some(provider.get_or_try_init()?.clone()), - None => None, - }, - ..self.options.clone() - }, - ) - }) +#[derive(Debug, Error)] +#[error("Bad response: {:?}{}", .status_code, .response_text.as_ref().map(|s| format!("\n\n{}", s)).unwrap_or_else(String::new))] +pub struct BadResponseError { + pub status_code: StatusCode, + pub response_text: Option, +} + +#[derive(Debug, Error)] +pub enum DownloadError { + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error(transparent)] + ToStr(#[from] reqwest::header::ToStrError), + #[error("Redirection from '{}' did not provide location header", .request_url)] + NoRedirectHeader { request_url: Url }, + #[error("Too many redirects.")] + TooManyRedirects, + #[error(transparent)] + BadResponse(#[from] BadResponseError), +} + +#[derive(Debug)] +pub struct HttpClient { + #[allow(clippy::disallowed_types)] // reqwest::Client allowed here + client: reqwest::Client, + // don't allow sending this across threads because then + // it might be shared accidentally across tokio runtimes + // which will cause issues + // https://github.com/seanmonstar/reqwest/issues/1148#issuecomment-910868788 + _unsend_marker: deno_core::unsync::UnsendMarker, +} + +impl HttpClient { + // DO NOT make this public. You should always be creating one of these from + // the HttpClientProvider + #[allow(clippy::disallowed_types)] // reqwest::Client allowed here + fn new(client: reqwest::Client) -> Self { + Self { + client, + _unsend_marker: deno_core::unsync::UnsendMarker::default(), + } } - /// Do a GET request without following redirects. - pub fn get_no_redirect( - &self, - url: U, - ) -> Result { - Ok(self.client()?.get(url)) + // todo(dsherret): don't expose `reqwest::RequestBuilder` because it + // is `Sync` and could accidentally be shared with multiple tokio runtimes + pub fn get(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder { + self.client.get(url) + } + + pub fn post(&self, url: impl reqwest::IntoUrl) -> reqwest::RequestBuilder { + self.client.post(url) } - pub async fn download_text(&self, url: U) -> Result { + /// Asynchronously fetches the given HTTP URL one pass only. + /// If no redirect is present and no error occurs, + /// yields Code(ResultPayload). + /// If redirect occurs, does not follow and + /// yields Redirect(url). + pub async fn fetch_no_follow(&self, args: FetchOnceArgs) -> Result { + let mut request = self.client.get(args.url.clone()); + + if let Some(etag) = args.maybe_etag { + let if_none_match_val = HeaderValue::from_str(&etag)?; + request = request.header(IF_NONE_MATCH, if_none_match_val); + } + if let Some(auth_token) = args.maybe_auth_token { + let authorization_val = HeaderValue::from_str(&auth_token.to_string())?; + request = request.header(AUTHORIZATION, authorization_val); + } + if let Some(accept) = args.maybe_accept { + let accepts_val = HeaderValue::from_str(&accept)?; + request = request.header(ACCEPT, accepts_val); + } + let response = match request.send().await { + Ok(resp) => resp, + Err(err) => { + if err.is_connect() || err.is_timeout() { + return Ok(FetchOnceResult::RequestError(err.to_string())); + } + return Err(err.into()); + } + }; + + if response.status() == StatusCode::NOT_MODIFIED { + return Ok(FetchOnceResult::NotModified); + } + + let mut result_headers = HashMap::new(); + let response_headers = response.headers(); + + if let Some(warning) = response_headers.get("X-Deno-Warning") { + log::warn!("{}", warning.to_str().unwrap()); + } + + for key in response_headers.keys() { + let key_str = key.to_string(); + let values = response_headers.get_all(key); + let values_str = values + .iter() + .map(|e| e.to_str().unwrap().to_string()) + .collect::>() + .join(","); + result_headers.insert(key_str, values_str); + } + + if response.status().is_redirection() { + let new_url = resolve_redirect_from_response(&args.url, &response)?; + return Ok(FetchOnceResult::Redirect(new_url, result_headers)); + } + + let status = response.status(); + + if status.is_server_error() { + return Ok(FetchOnceResult::ServerError(status)); + } + + if status.is_client_error() { + let err = if response.status() == StatusCode::NOT_FOUND { + custom_error( + "NotFound", + format!("Import '{}' failed, not found.", args.url), + ) + } else { + generic_error(format!( + "Import '{}' failed: {}", + args.url, + response.status() + )) + }; + return Err(err); + } + + let body = get_response_body_with_progress(response).await?; + + Ok(FetchOnceResult::Code(body, result_headers)) + } + + pub async fn download_text(&self, url: impl reqwest::IntoUrl) -> Result { let bytes = self.download(url).await?; Ok(String::from_utf8(bytes)?) } - pub async fn download(&self, url: U) -> Result, AnyError> { - let maybe_bytes = self.inner_download(url).await?; + pub async fn download(&self, url: impl reqwest::IntoUrl) -> Result, AnyError> { + let maybe_bytes = self.download_inner(url, None).await?; match maybe_bytes { Some(bytes) => Ok(bytes), None => Err(custom_error("Http", "Not found.")), } } - pub async fn download_with_progress( + pub async fn download_with_progress( &self, - url: U, - ) -> Result>, AnyError> { - self.inner_download(url).await + url: impl reqwest::IntoUrl, + maybe_header: Option<(HeaderName, HeaderValue)>, + ) -> Result>, DownloadError> { + self.download_inner(url, maybe_header).await } - async fn inner_download( + pub async fn get_redirected_url( &self, - url: U, - ) -> Result>, AnyError> { - let response = self.get_redirected_response(url).await?; + url: impl reqwest::IntoUrl, + maybe_header: Option<(HeaderName, HeaderValue)>, + ) -> Result { + let response = self.get_redirected_response(url, maybe_header).await?; + Ok(response.url().clone()) + } + + async fn download_inner( + &self, + url: impl reqwest::IntoUrl, + maybe_header: Option<(HeaderName, HeaderValue)>, + ) -> Result>, DownloadError> { + let response = self.get_redirected_response(url, maybe_header).await?; if response.status() == 404 { return Ok(None); } else if !response.status().is_success() { let status = response.status(); let maybe_response_text = response.text().await.ok(); - bail!( - "Bad response: {:?}{}", - status, - match maybe_response_text { - Some(text) => format!("\n\n{text}"), - None => String::new(), - } - ); + return Err(DownloadError::BadResponse(BadResponseError { + status_code: status, + response_text: maybe_response_text + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()), + })); } - get_response_body_with_progress(response).await.map(Some) + get_response_body_with_progress(response) + .await + .map(Some) + .map_err(Into::into) } - pub async fn get_redirected_response( + async fn get_redirected_response( &self, - url: U, - ) -> Result { + url: impl reqwest::IntoUrl, + mut maybe_header: Option<(HeaderName, HeaderValue)>, + ) -> Result { let mut url = url.into_url()?; - let mut response = self.get_no_redirect(url.clone())?.send().await?; + let mut builder = self.get(url.clone()); + if let Some((header_name, header_value)) = maybe_header.as_ref() { + builder = builder.header(header_name, header_value); + } + let mut response = builder.send().await?; let status = response.status(); if status.is_redirection() { for _ in 0..5 { let new_url = resolve_redirect_from_response(&url, &response)?; - let new_response = self.get_no_redirect(new_url.clone())?.send().await?; + let mut builder = self.get(new_url.clone()); + + if new_url.origin() == url.origin() { + if let Some((header_name, header_value)) = maybe_header.as_ref() { + builder = builder.header(header_name, header_value); + } + } else { + maybe_header = None; + } + + let new_response = builder.send().await?; let status = new_response.status(); if status.is_redirection() { response = new_response; @@ -334,7 +521,7 @@ impl HttpClient { return Ok(new_response); } } - Err(custom_error("Http", "Too many redirects.")) + Err(DownloadError::TooManyRedirects) } else { Ok(response) } @@ -343,7 +530,7 @@ impl HttpClient { pub async fn get_response_body_with_progress( response: reqwest::Response, -) -> Result, AnyError> { +) -> Result, reqwest::Error> { let bytes = response.bytes().await?; Ok(bytes.into()) } diff --git a/crates/sb_core/util/path.rs b/crates/sb_core/util/path.rs index 505f9aeae..3020523de 100644 --- a/crates/sb_core/util/path.rs +++ b/crates/sb_core/util/path.rs @@ -1,5 +1,8 @@ use deno_ast::ModuleSpecifier; -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; /// Gets if the provided character is not supported on all /// kinds of file systems. @@ -60,3 +63,23 @@ pub fn find_lowest_path(paths: &Vec) -> Option { lowest_path.map(|(path, _)| path.to_string()) } + +pub fn get_atomic_dir_path(file_path: &Path) -> PathBuf { + let rand = gen_rand_path_component(); + let new_file_name = format!( + ".{}_{}", + file_path + .file_name() + .map(|f| f.to_string_lossy()) + .unwrap_or(Cow::Borrowed("")), + rand + ); + file_path.with_file_name(new_file_name) +} + +fn gen_rand_path_component() -> String { + (0..4).fold(String::new(), |mut output, _| { + output.push_str(&format!("{:02x}", rand::random::())); + output + }) +} diff --git a/crates/sb_core/util/sync/async_flag.rs b/crates/sb_core/util/sync/async_flag.rs new file mode 100644 index 000000000..01f990915 --- /dev/null +++ b/crates/sb_core/util/sync/async_flag.rs @@ -0,0 +1,20 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use tokio_util::sync::CancellationToken; + +#[derive(Debug, Default, Clone)] +pub struct AsyncFlag(CancellationToken); + +impl AsyncFlag { + pub fn raise(&self) { + self.0.cancel(); + } + + pub fn is_raised(&self) -> bool { + self.0.is_cancelled() + } + + pub fn wait_raised(&self) -> impl std::future::Future + '_ { + self.0.cancelled() + } +} diff --git a/crates/sb_core/util/sync/mod.rs b/crates/sb_core/util/sync/mod.rs new file mode 100644 index 000000000..f58437503 --- /dev/null +++ b/crates/sb_core/util/sync/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +mod async_flag; +mod sync_read_async_write_lock; +mod task_queue; +mod value_creator; + +pub use async_flag::AsyncFlag; +pub use sync_read_async_write_lock::SyncReadAsyncWriteLock; +pub use task_queue::TaskQueue; +pub use task_queue::TaskQueuePermit; +pub use value_creator::MultiRuntimeAsyncValueCreator; +// todo(dsherret): this being in the unsync module is slightly confusing, but it's Sync +pub use deno_core::unsync::AtomicFlag; diff --git a/crates/sb_core/util/sync/sync_read_async_write_lock.rs b/crates/sb_core/util/sync/sync_read_async_write_lock.rs new file mode 100644 index 000000000..74bd92b1f --- /dev/null +++ b/crates/sb_core/util/sync/sync_read_async_write_lock.rs @@ -0,0 +1,62 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::parking_lot::RwLock; +use deno_core::parking_lot::RwLockReadGuard; +use deno_core::parking_lot::RwLockWriteGuard; + +use super::TaskQueue; +use super::TaskQueuePermit; + +/// A lock that can be read synchronously at any time (including when +/// being written to), but must write asynchronously. +pub struct SyncReadAsyncWriteLockWriteGuard<'a, T: Send + Sync> { + _update_permit: TaskQueuePermit<'a>, + data: &'a RwLock, +} + +impl<'a, T: Send + Sync> SyncReadAsyncWriteLockWriteGuard<'a, T> { + pub fn read(&self) -> RwLockReadGuard<'_, T> { + self.data.read() + } + + /// Warning: Only `write()` with data you created within this + /// write this `SyncReadAsyncWriteLockWriteGuard`. + /// + /// ```rs + /// let mut data = lock.write().await; + /// + /// let mut data = data.read().clone(); + /// data.value = 2; + /// *data.write() = data; + /// ``` + pub fn write(&self) -> RwLockWriteGuard<'_, T> { + self.data.write() + } +} + +/// A lock that can only be +pub struct SyncReadAsyncWriteLock { + data: RwLock, + update_queue: TaskQueue, +} + +impl SyncReadAsyncWriteLock { + pub fn new(data: T) -> Self { + Self { + data: RwLock::new(data), + update_queue: TaskQueue::default(), + } + } + + pub fn read(&self) -> RwLockReadGuard<'_, T> { + self.data.read() + } + + pub async fn acquire(&self) -> SyncReadAsyncWriteLockWriteGuard<'_, T> { + let update_permit = self.update_queue.acquire().await; + SyncReadAsyncWriteLockWriteGuard { + _update_permit: update_permit, + data: &self.data, + } + } +} diff --git a/crates/sb_core/util/sync.rs b/crates/sb_core/util/sync/task_queue.rs similarity index 88% rename from crates/sb_core/util/sync.rs rename to crates/sb_core/util/sync/task_queue.rs index 1abce82ca..f80ba0694 100644 --- a/crates/sb_core/util/sync.rs +++ b/crates/sb_core/util/sync/task_queue.rs @@ -1,29 +1,13 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::collections::LinkedList; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; use std::sync::Arc; use deno_core::futures::task::AtomicWaker; use deno_core::futures::Future; use deno_core::parking_lot::Mutex; -/// Simplifies the use of an atomic boolean as a flag. -#[derive(Debug, Default)] -pub struct AtomicFlag(AtomicBool); - -impl AtomicFlag { - /// Raises the flag returning if the raise was successful. - pub fn raise(&self) -> bool { - !self.0.swap(true, Ordering::SeqCst) - } - - /// Gets if the flag is raised. - pub fn is_raised(&self) -> bool { - self.0.load(Ordering::SeqCst) - } -} +use super::AtomicFlag; #[derive(Debug, Default)] struct TaskQueueTaskItem { diff --git a/crates/sb_core/util/sync/value_creator.rs b/crates/sb_core/util/sync/value_creator.rs new file mode 100644 index 000000000..b2b953872 --- /dev/null +++ b/crates/sb_core/util/sync/value_creator.rs @@ -0,0 +1,206 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::sync::Arc; + +use deno_core::futures::future::BoxFuture; +use deno_core::futures::future::LocalBoxFuture; +use deno_core::futures::future::Shared; +use deno_core::futures::FutureExt; +use deno_core::parking_lot::Mutex; +use tokio::task::JoinError; + +type JoinResult = Result>; +type CreateFutureFn = Box LocalBoxFuture<'static, TResult> + Send + Sync>; + +#[derive(Debug)] +struct State { + retry_index: usize, + future: Option>>>, +} + +/// Attempts to create a shared value asynchronously on one tokio runtime while +/// many runtimes are requesting the value. +/// +/// This is only useful when the value needs to get created once across +/// many runtimes. +/// +/// This handles the case where the tokio runtime creating the value goes down +/// while another one is waiting on the value. +pub struct MultiRuntimeAsyncValueCreator { + create_future: CreateFutureFn, + state: Mutex>, +} + +impl std::fmt::Debug for MultiRuntimeAsyncValueCreator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MultiRuntimeAsyncValueCreator").finish() + } +} + +impl MultiRuntimeAsyncValueCreator { + pub fn new(create_future: CreateFutureFn) -> Self { + Self { + state: Mutex::new(State { + retry_index: 0, + future: None, + }), + create_future, + } + } + + pub async fn get(&self) -> TResult { + let (mut future, mut retry_index) = { + let mut state = self.state.lock(); + let future = match &state.future { + Some(future) => future.clone(), + None => { + let future = self.create_shared_future(); + state.future = Some(future.clone()); + future + } + }; + (future, state.retry_index) + }; + + loop { + let result = future.await; + + match result { + Ok(result) => return result, + Err(join_error) => { + if join_error.is_cancelled() { + let mut state = self.state.lock(); + + if state.retry_index == retry_index { + // we were the first one to retry, so create a new future + // that we'll run from the current runtime + state.retry_index += 1; + state.future = Some(self.create_shared_future()); + } + + retry_index = state.retry_index; + future = state.future.as_ref().unwrap().clone(); + + // just in case we're stuck in a loop + if retry_index > 1000 { + panic!("Something went wrong.") // should never happen + } + } else { + panic!("{}", join_error); + } + } + } + } + } + + fn create_shared_future(&self) -> Shared>> { + let future = (self.create_future)(); + deno_core::unsync::spawn(future) + .map(|result| result.map_err(Arc::new)) + .boxed() + .shared() + } +} + +#[cfg(test)] +mod test { + use deno_core::unsync::spawn; + + use super::*; + + #[tokio::test] + async fn single_runtime() { + let value_creator = + MultiRuntimeAsyncValueCreator::new(Box::new(|| async { 1 }.boxed_local())); + let value = value_creator.get().await; + assert_eq!(value, 1); + } + + #[test] + fn multi_runtimes() { + let value_creator = Arc::new(MultiRuntimeAsyncValueCreator::new(Box::new(|| { + async { + tokio::task::yield_now().await; + 1 + } + .boxed_local() + }))); + let handles = (0..3) + .map(|_| { + let value_creator = value_creator.clone(); + std::thread::spawn(|| { + create_runtime().block_on(async move { value_creator.get().await }) + }) + }) + .collect::>(); + for handle in handles { + assert_eq!(handle.join().unwrap(), 1); + } + } + + #[test] + fn multi_runtimes_first_never_finishes() { + let is_first_run = Arc::new(Mutex::new(true)); + let (tx, rx) = std::sync::mpsc::channel::<()>(); + let value_creator = Arc::new(MultiRuntimeAsyncValueCreator::new({ + let is_first_run = is_first_run.clone(); + Box::new(move || { + let is_first_run = is_first_run.clone(); + let tx = tx.clone(); + async move { + let is_first_run = { + let mut is_first_run = is_first_run.lock(); + let initial_value = *is_first_run; + *is_first_run = false; + tx.send(()).unwrap(); + initial_value + }; + if is_first_run { + tokio::time::sleep(std::time::Duration::from_millis(30_000)).await; + panic!("TIMED OUT"); // should not happen + } else { + tokio::task::yield_now().await; + } + 1 + } + .boxed_local() + }) + })); + std::thread::spawn({ + let value_creator = value_creator.clone(); + let is_first_run = is_first_run.clone(); + move || { + create_runtime().block_on(async { + let value_creator = value_creator.clone(); + // spawn a task that will never complete + spawn(async move { value_creator.get().await }); + // wait for the task to set is_first_run to false + while *is_first_run.lock() { + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + } + // now exit the runtime while the value_creator is still pending + }) + } + }); + let handle = { + let value_creator = value_creator.clone(); + std::thread::spawn(|| { + create_runtime().block_on(async move { + let value_creator = value_creator.clone(); + rx.recv().unwrap(); + // even though the other runtime shutdown, this get() should + // recover and still get the value + value_creator.get().await + }) + }) + }; + assert_eq!(handle.join().unwrap(), 1); + } + + fn create_runtime() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + } +} diff --git a/crates/sb_fs/file_system.rs b/crates/sb_fs/file_system.rs index aa98da9be..66fd87617 100644 --- a/crates/sb_fs/file_system.rs +++ b/crates/sb_fs/file_system.rs @@ -129,6 +129,20 @@ impl FileSystem for DenoCompileFileSystem { RealFs.chown_async(path, uid, gid).await } + fn lchown_sync(&self, path: &Path, uid: Option, gid: Option) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.lchown_sync(path, uid, gid) + } + async fn lchown_async( + &self, + path: PathBuf, + uid: Option, + gid: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.lchown_async(path, uid, gid).await + } + fn remove_sync(&self, path: &Path, recursive: bool) -> FsResult<()> { self.error_if_in_vfs(path)?; RealFs.remove_sync(path, recursive) @@ -328,4 +342,29 @@ impl FileSystem for DenoCompileFileSystem { .utime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) .await } + + fn lutime_sync( + &self, + path: &Path, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.lutime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + } + async fn lutime_async( + &self, + path: PathBuf, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs + .lutime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + .await + } } diff --git a/crates/sb_fs/lib.rs b/crates/sb_fs/lib.rs index 643acf332..18348f657 100644 --- a/crates/sb_fs/lib.rs +++ b/crates/sb_fs/lib.rs @@ -3,15 +3,13 @@ use anyhow::{bail, Context}; use deno_core::normalize_path; use deno_npm::NpmSystemInfo; use eszip::EszipV2; -use log::warn; +use indexmap::IndexMap; use sb_eszip_shared::{AsyncEszipDataRead, STATIC_FILES_ESZIP_KEY}; -use sb_npm::cache::NpmCache; -use sb_npm::registry::CliNpmRegistryApi; -use sb_npm::resolution::NpmResolution; use sb_npm::{CliNpmResolver, InnerCliNpmResolverRef}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; +use virtual_fs::VfsEntry; use url::Url; pub mod file_system; @@ -21,9 +19,6 @@ pub mod virtual_fs; pub struct VfsOpts { pub npm_resolver: Arc, - pub npm_registry_api: Arc, - pub npm_cache: Arc, - pub npm_resolution: Arc, } pub type EszipStaticFiles = HashMap; @@ -145,11 +140,48 @@ where let folder = npm_resolver.resolve_pkg_folder_from_pkg_id(&package.id)?; builder.add_dir_recursive(&folder)?; } - // overwrite the root directory's name to obscure the user's registry url - builder.set_root_dir_name("node_modules".to_string()); + + // Flatten all the registries folders into a single "node_modules/localhost" folder + // that will be used by denort when loading the npm cache. This avoids us exposing + // the user's private registry information and means we don't have to bother + // serializing all the different registry config into the binary. + builder.with_root_dir(|root_dir| { + root_dir.name = "node_modules".to_string(); + let mut new_entries = Vec::with_capacity(root_dir.entries.len()); + let mut localhost_entries = IndexMap::new(); + for entry in std::mem::take(&mut root_dir.entries) { + match entry { + VfsEntry::Dir(dir) => { + for entry in dir.entries { + log::debug!("Flattening {} into node_modules", entry.name()); + if let Some(existing) = + localhost_entries.insert(entry.name().to_string(), entry) + { + panic!( + "Unhandled scenario where a duplicate entry was found: {:?}", + existing + ); + } + } + } + VfsEntry::File(_) | VfsEntry::Symlink(_) => { + new_entries.push(entry); + } + } + } + new_entries.push(VfsEntry::Dir(VirtualDirectory { + name: "localhost".to_string(), + entries: localhost_entries.into_iter().map(|(_, v)| v).collect(), + })); + // needs to be sorted by name + new_entries.sort_by(|a, b| a.name().cmp(b.name())); + root_dir.entries = new_entries; + }); + Ok(builder) } } + _ => { unreachable!(); } diff --git a/crates/sb_fs/static_fs.rs b/crates/sb_fs/static_fs.rs index 137f66dc6..a5c9bb2ce 100644 --- a/crates/sb_fs/static_fs.rs +++ b/crates/sb_fs/static_fs.rs @@ -118,6 +118,19 @@ impl deno_fs::FileSystem for StaticFs { Err(FsError::NotSupported) } + fn lchown_sync(&self, _path: &Path, _uid: Option, _gid: Option) -> FsResult<()> { + Err(FsError::NotSupported) + } + + async fn lchown_async( + &self, + _path: PathBuf, + _uid: Option, + _gid: Option, + ) -> FsResult<()> { + Err(FsError::NotSupported) + } + fn remove_sync(&self, _path: &Path, _recursive: bool) -> FsResult<()> { Err(FsError::NotSupported) } @@ -286,6 +299,28 @@ impl deno_fs::FileSystem for StaticFs { Err(FsError::NotSupported) } + fn lutime_sync( + &self, + _path: &Path, + _atime_secs: i64, + _atime_nanos: u32, + _mtime_secs: i64, + _mtime_nanos: u32, + ) -> FsResult<()> { + Err(FsError::NotSupported) + } + + async fn lutime_async( + &self, + _path: PathBuf, + _atime_secs: i64, + _atime_nanos: u32, + _mtime_secs: i64, + _mtime_nanos: u32, + ) -> FsResult<()> { + Err(FsError::NotSupported) + } + fn read_file_sync( &self, path: &Path, diff --git a/crates/sb_fs/virtual_fs.rs b/crates/sb_fs/virtual_fs.rs index 3dac406a2..55ad253bd 100644 --- a/crates/sb_fs/virtual_fs.rs +++ b/crates/sb_fs/virtual_fs.rs @@ -1,4 +1,4 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::borrow::Cow; use std::collections::HashMap; @@ -64,9 +64,8 @@ impl<'scope> VfsBuilder<'scope> { root_dir: VirtualDirectory { name: root_path .file_stem() - .unwrap() - .to_string_lossy() - .into_owned(), + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or("root".to_string()), entries: Vec::new(), }, root_path, @@ -78,13 +77,16 @@ impl<'scope> VfsBuilder<'scope> { }) } - pub fn set_root_dir_name(&mut self, name: String) { - self.root_dir.name = name; + pub fn with_root_dir(&mut self, with_root: impl FnOnce(&mut VirtualDirectory) -> R) -> R { + with_root(&mut self.root_dir) } pub fn add_dir_recursive(&mut self, path: &Path) -> Result<(), AnyError> { - let path = canonicalize_path(path)?; - self.add_dir_recursive_internal(&path) + let target_path = canonicalize_path(path)?; + if path != target_path { + self.add_symlink(path, &target_path)?; + } + self.add_dir_recursive_internal(&target_path) } fn add_dir_recursive_internal(&mut self, path: &Path) -> Result<(), AnyError> { @@ -100,31 +102,38 @@ impl<'scope> VfsBuilder<'scope> { if file_type.is_dir() { self.add_dir_recursive_internal(&path)?; } else if file_type.is_file() { - let file_bytes = - std::fs::read(&path).with_context(|| format!("Reading {}", path.display()))?; - self.add_file(&path, file_bytes)?; + self.add_file_at_path_not_symlink(&path)?; } else if file_type.is_symlink() { - let target = canonicalize_path(&path) - .with_context(|| format!("Reading symlink {}", path.display()))?; - if let Err(StripRootError { .. }) = self.add_symlink(&path, &target) { - if target.is_file() { - // this may change behavior, so warn the user about it - log::warn!( - "Symlink target is outside '{}'. Inlining symlink at '{}' to '{}' as file.", - self.root_path.display(), - path.display(), - target.display(), - ); - // inline the symlink and make the target file - let file_bytes = std::fs::read(&target) - .with_context(|| format!("Reading {}", path.display()))?; - self.add_file(&path, file_bytes)?; - } else { + match canonicalize_path(&path) { + Ok(target) => { + if let Err(StripRootError { .. }) = self.add_symlink(&path, &target) { + if target.is_file() { + // this may change behavior, so warn the user about it + log::warn!( + "Symlink target is outside '{}'. Inlining symlink at '{}' to '{}' as file.", + self.root_path.display(), + path.display(), + target.display(), + ); + // inline the symlink and make the target file + let file_bytes = std::fs::read(&target) + .with_context(|| format!("Reading {}", path.display()))?; + self.add_file(&path, file_bytes)?; + } else { + log::warn!( + "Symlink target is outside '{}'. Excluding symlink at '{}' with target '{}'.", + self.root_path.display(), + path.display(), + target.display(), + ); + } + } + } + Err(err) => { log::warn!( - "Symlink target is outside '{}'. Excluding symlink at '{}' with target '{}'.", - self.root_path.display(), + "Failed resolving symlink. Ignoring.\n Path: {}\n Message: {:#}", path.display(), - target.display(), + err ); } } @@ -168,6 +177,20 @@ impl<'scope> VfsBuilder<'scope> { Ok(current_dir) } + pub fn add_file_at_path(&mut self, path: &Path) -> Result<(), AnyError> { + let target_path = canonicalize_path(path)?; + if target_path != path { + self.add_symlink(path, &target_path)?; + } + self.add_file_at_path_not_symlink(&target_path) + } + + pub fn add_file_at_path_not_symlink(&mut self, path: &Path) -> Result<(), AnyError> { + let file_bytes = + std::fs::read(path).with_context(|| format!("Reading {}", path.display()))?; + self.add_file(path, file_bytes) + } + fn add_file(&mut self, path: &Path, data: Vec) -> Result<(), AnyError> { log::debug!("Adding file '{}'", path.display()); let checksum = checksum::gen(&[&data]); @@ -183,23 +206,39 @@ impl<'scope> VfsBuilder<'scope> { let dir = self.add_dir(path.parent().unwrap())?; let name = path.file_name().unwrap().to_string_lossy(); let data_len = data.len(); - let insert_index = match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { - Err(insert_index) => insert_index, - Ok(_) => unreachable!(), - }; - - let len = data.len(); - let key = (add_content_callback_fn.lock().unwrap())(path, &name, data); - - dir.entries.insert( - insert_index, - VfsEntry::File(VirtualFile { - key, - name: name.to_string(), - offset, - len: len as u64, - }), - ); + match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { + Ok(_) => { + // already added, just ignore + } + Err(insert_index) => { + dir.entries.insert( + insert_index, + VfsEntry::File(VirtualFile { + name: name.to_string(), + offset, + len: data.len() as u64, + content: Some(data), + }), + ); + } + } + // let insert_index = match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { + // Err(insert_index) => insert_index, + // Ok(_) => unreachable!(), + // }; + + // let len = data.len(); + // let key = (add_content_callback_fn.lock().unwrap())(path, &name, data); + + // dir.entries.insert( + // insert_index, + // VfsEntry::File(VirtualFile { + // key, + // name: name.to_string(), + // offset, + // len: len as u64, + // }), + // ); // new file, update the list of files if self.current_offset == offset { @@ -217,6 +256,10 @@ impl<'scope> VfsBuilder<'scope> { target.display() ); let dest = self.path_relative_root(target)?; + if dest == self.path_relative_root(path)? { + // it's the same, ignore + return Ok(()); + } let dir = self.add_dir(path.parent().unwrap())?; let name = path.file_name().unwrap().to_string_lossy(); match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { diff --git a/crates/sb_graph/emitter.rs b/crates/sb_graph/emitter.rs index f15c23e44..4f55c9dcd 100644 --- a/crates/sb_graph/emitter.rs +++ b/crates/sb_graph/emitter.rs @@ -1,36 +1,36 @@ -use crate::graph_resolver::{CliGraphResolver, CliGraphResolverOptions}; use crate::jsx_util::{get_jsx_emit_opts, get_rt_from_jsx}; +use crate::resolver::{ + CjsResolutionStore, CliGraphResolver, CliGraphResolverOptions, CliNodeResolver, +}; use crate::DecoratorType; use deno_ast::{EmitOptions, SourceMapOption, TranspileOptions}; +use deno_cache_dir::HttpCache; +use deno_config::workspace::{PackageJsonDepResolution, WorkspaceResolver}; use deno_config::JsxImportSourceConfig; use deno_core::error::AnyError; +use deno_core::futures::FutureExt; use deno_core::parking_lot::Mutex; use deno_lockfile::Lockfile; use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use eszip::deno_graph::source::Loader; use import_map::ImportMap; +use npm_cache::file_fetcher::FileFetcher; +use npm_cache::FetchCacher; use sb_core::cache::caches::Caches; use sb_core::cache::deno_dir::{DenoDir, DenoDirProvider}; use sb_core::cache::emit::EmitCache; use sb_core::cache::fc_permissions::FcPermissions; -use sb_core::cache::fetch_cacher::FetchCacher; use sb_core::cache::module_info::ModuleInfoCache; use sb_core::cache::parsed_source::ParsedSourceCache; -use sb_core::cache::{CacheSetting, GlobalHttpCache, HttpCache, RealDenoCacheEnv}; +use sb_core::cache::{CacheSetting, GlobalHttpCache, RealDenoCacheEnv}; use sb_core::emit::Emitter; -use sb_core::file_fetcher::{FileCache, FileFetcher}; -use sb_core::util::http_util::HttpClient; -use sb_node::PackageJson; -use sb_npm::cache::NpmCache; -use sb_npm::installer::PackageJsonDepsInstaller; -use sb_npm::package_json::{ - get_local_package_json_version_reqs, PackageJsonDeps, PackageJsonDepsProvider, -}; -use sb_npm::registry::CliNpmRegistryApi; -use sb_npm::resolution::NpmResolution; +use sb_core::npm; +use sb_core::util::http_util::HttpClientProvider; +use sb_node::NodeResolver; + use sb_npm::{ create_managed_npm_resolver, CliNpmResolver, CliNpmResolverManagedCreateOptions, - CliNpmResolverManagedPackageJsonInstallerOption, CliNpmResolverManagedSnapshotOption, + CliNpmResolverManagedSnapshotOption, }; use std::collections::HashMap; use std::future::Future; @@ -84,18 +84,20 @@ pub struct EmitterFactory { deno_dir: DenoDir, pub npm_snapshot: Option, lockfile: Deferred>>>, - package_json_deps_provider: Deferred>, - package_json_deps_installer: Deferred>, - maybe_package_json_deps: Option, + http_client_provider: Deferred>, maybe_lockfile: Option, maybe_decorator: Option, + cjs_resolutions: Deferred>, + node_resolver: Deferred>, + cli_node_resolver: Deferred>, npm_resolver: Deferred>, + workspace_resolver: Deferred>, resolver: Deferred>, + file_fetcher: Deferred>, file_fetcher_cache_strategy: Option, jsx_import_source_config: Option, file_fetcher_allow_remote: bool, - pub maybe_import_map: Option>, - file_cache: Deferred>, + pub maybe_import_map: Option, module_info_cache: Deferred>, } @@ -114,17 +116,19 @@ impl EmitterFactory { deno_dir, npm_snapshot: None, lockfile: Default::default(), - package_json_deps_provider: Default::default(), - package_json_deps_installer: Default::default(), - maybe_package_json_deps: None, + http_client_provider: Default::default(), maybe_lockfile: None, maybe_decorator: None, + cjs_resolutions: Default::default(), + node_resolver: Default::default(), + cli_node_resolver: Default::default(), npm_resolver: Default::default(), + workspace_resolver: Default::default(), resolver: Default::default(), + file_fetcher: Default::default(), file_fetcher_cache_strategy: None, file_fetcher_allow_remote: true, maybe_import_map: None, - file_cache: Default::default(), jsx_import_source_config: None, } } @@ -138,19 +142,13 @@ impl EmitterFactory { } pub fn set_import_map(&mut self, import_map: Option) { - self.maybe_import_map = import_map - .map(|import_map| Some(Arc::new(import_map))) - .unwrap_or_else(|| None); + self.maybe_import_map = import_map; } pub fn set_decorator_type(&mut self, decorator_type: Option) { self.maybe_decorator = decorator_type; } - pub fn init_package_json_deps(&mut self, package: &PackageJson) { - self.maybe_package_json_deps = Some(get_local_package_json_version_reqs(package)); - } - pub fn deno_dir_provider(&self) -> Arc { Arc::new(DenoDirProvider::new(None)) } @@ -170,11 +168,8 @@ impl EmitterFactory { }) } - pub fn emit_cache(&self, transpile_options: TranspileOptions) -> Result { - Ok(EmitCache::new( - self.deno_dir.gen_cache.clone(), - transpile_options, - )) + pub fn emit_cache(&self) -> Result { + Ok(EmitCache::new(self.deno_dir.gen_cache.clone())) } pub fn parsed_source_cache(&self) -> Result, AnyError> { @@ -228,12 +223,11 @@ impl EmitterFactory { } pub fn emitter(&self) -> Result, AnyError> { - let transpile_options = self.transpile_options(); let emitter = Arc::new(Emitter::new( - self.emit_cache(transpile_options.clone())?, + self.emit_cache()?, self.parsed_source_cache()?, + self.transpile_options(), self.emit_options(), - transpile_options, )); Ok(emitter) @@ -243,63 +237,30 @@ impl EmitterFactory { GlobalHttpCache::new(self.deno_dir.deps_folder_path(), RealDenoCacheEnv) } - pub fn http_client(&self) -> Arc { - let root_cert_store = None; - let unsafely_ignore_certificate_errors = None; - - let http_client = HttpClient::new(root_cert_store, unsafely_ignore_certificate_errors); - - Arc::new(http_client) + pub fn http_client_provider(&self) -> &Arc { + self.http_client_provider + .get_or_init(|| Arc::new(HttpClientProvider::new(None, None))) } pub fn real_fs(&self) -> Arc { Arc::new(deno_fs::RealFs) } - pub async fn npm_cache(&self) -> Arc { - self.npm_resolver() - .await - .as_managed() - .unwrap() - .npm_cache() - .clone() - } - - pub async fn npm_api(&self) -> Arc { - self.npm_resolver() - .await - .as_managed() - .unwrap() - .npm_api() - .clone() - } - - pub async fn npm_resolution(&self) -> Arc { - self.npm_resolver() - .await - .as_managed() - .unwrap() - .npm_resolution() - .clone() - } - - pub fn file_cache(&self) -> &Arc { - self.file_cache.get_or_init(Default::default) - } - pub fn get_lock_file_deferred(&self) -> &Option>> { self.lockfile.get_or_init(|| { if let Some(lockfile_data) = self.maybe_lockfile.clone() { - Some(Arc::new(Mutex::new( - Lockfile::new(lockfile_data.path.clone(), lockfile_data.overwrite).unwrap(), - ))) + Some(Arc::new(Mutex::new(Lockfile::new_empty( + lockfile_data.path.clone(), + lockfile_data.overwrite, + )))) } else { let default_lockfile_path = std::env::current_dir() .map(|p| p.join(".supabase.lock")) .unwrap(); - Some(Arc::new(Mutex::new( - Lockfile::new(default_lockfile_path, true).unwrap(), - ))) + Some(Arc::new(Mutex::new(Lockfile::new_empty( + default_lockfile_path, + true, + )))) } }) } @@ -308,117 +269,125 @@ impl EmitterFactory { self.get_lock_file_deferred().as_ref().cloned() } - pub async fn npm_resolver(&self) -> &Arc { + pub fn cjs_resolutions(&self) -> &Arc { + self.cjs_resolutions.get_or_init(Default::default) + } + + pub async fn npm_resolver(&self) -> Result<&Arc, AnyError> { self.npm_resolver .get_or_try_init_async(async { create_managed_npm_resolver(CliNpmResolverManagedCreateOptions { snapshot: CliNpmResolverManagedSnapshotOption::Specified(None), maybe_lockfile: self.get_lock_file(), fs: self.real_fs(), - http_client: self.http_client(), + http_client_provider: self.http_client_provider().clone(), npm_global_cache_dir: self.deno_dir.npm_folder_path().clone(), cache_setting: CacheSetting::Use, maybe_node_modules_path: None, npm_system_info: Default::default(), - package_json_installer: - CliNpmResolverManagedPackageJsonInstallerOption::ConditionalInstall( - self.package_json_deps_provider().clone(), - ), - npm_registry_url: CliNpmRegistryApi::default_url().clone(), + package_json_deps_provider: Default::default(), + npmrc: npm::create_default_npmrc(), }) .await }) .await - .unwrap() } - pub fn package_json_deps_provider(&self) -> &Arc { - self.package_json_deps_provider.get_or_init(|| { - Arc::new(PackageJsonDepsProvider::new( - self.maybe_package_json_deps.clone(), - )) - }) + pub async fn node_resolver(&self) -> Result<&Arc, AnyError> { + self.node_resolver + .get_or_try_init_async( + async { + Ok(Arc::new(NodeResolver::new( + self.real_fs(), + self.npm_resolver().await?.clone().into_npm_resolver(), + ))) + } + .boxed_local(), + ) + .await } - pub async fn package_json_deps_installer(&self) -> &Arc { - self.package_json_deps_installer + pub async fn cli_node_resolver(&self) -> Result<&Arc, AnyError> { + self.cli_node_resolver .get_or_try_init_async(async { - Ok(Arc::new(PackageJsonDepsInstaller::new( - self.package_json_deps_provider().clone(), - self.npm_api().await.clone(), - self.npm_resolution().await.clone(), + Ok(Arc::new(CliNodeResolver::new( + self.cjs_resolutions().clone(), + self.real_fs(), + self.node_resolver().await?.clone(), + self.npm_resolver().await?.clone(), ))) }) .await - .unwrap() - } - - pub fn cli_graph_resolver_options(&self) -> CliGraphResolverOptions { - CliGraphResolverOptions { - maybe_import_map: self.maybe_import_map.clone(), - maybe_jsx_import_source_config: self.jsx_import_source_config.clone(), - no_npm: !self.file_fetcher_allow_remote, - ..Default::default() - } } - pub async fn set_jsx_import_source(&mut self, config: JsxImportSourceConfig) { - self.jsx_import_source_config = Some(config); + pub fn workspace_resolver(&self) -> Result<&Arc, AnyError> { + self.workspace_resolver.get_or_try_init(|| { + Ok(Arc::new(WorkspaceResolver::new_raw( + self.maybe_import_map.clone(), + vec![], + PackageJsonDepResolution::Disabled, + ))) + }) } pub async fn cli_graph_resolver(&self) -> &Arc { self.resolver .get_or_try_init_async(async { - Ok(Arc::new(CliGraphResolver::new( - self.npm_api().await.clone(), - self.package_json_deps_provider().clone(), - self.package_json_deps_installer().await.clone(), - self.cli_graph_resolver_options(), - if self.file_fetcher_allow_remote { - Some(self.npm_resolver().await.clone()) + Ok(Arc::new(CliGraphResolver::new(CliGraphResolverOptions { + node_resolver: Some(self.cli_node_resolver().await?.clone()), + npm_resolver: if self.file_fetcher_allow_remote { + Some(self.npm_resolver().await?.clone()) } else { None }, - ))) + + workspace_resolver: self.workspace_resolver()?.clone(), + bare_node_builtins_enabled: false, + maybe_jsx_import_source_config: self.jsx_import_source_config.clone(), + maybe_vendor_dir: None, + }))) }) .await .unwrap() } - pub fn file_fetcher(&self) -> FileFetcher { - let global_cache_struct = - GlobalHttpCache::new(self.deno_dir.deps_folder_path(), RealDenoCacheEnv); - let global_cache: Arc = Arc::new(global_cache_struct); - let http_client = self.http_client(); - let blob_store = Arc::new(deno_web::BlobStore::default()); - - FileFetcher::new( - global_cache.clone(), - self.file_fetcher_cache_strategy - .clone() - .unwrap_or(CacheSetting::ReloadAll), - self.file_fetcher_allow_remote, - http_client, - blob_store, - self.file_cache().clone(), - ) - } - - pub fn file_fetcher_loader(&self) -> Box { + pub async fn set_jsx_import_source(&mut self, config: JsxImportSourceConfig) { + self.jsx_import_source_config = Some(config); + } + + pub fn file_fetcher(&self) -> Result<&Arc, AnyError> { + self.file_fetcher.get_or_try_init(|| { + let global_cache_struct = + GlobalHttpCache::new(self.deno_dir.deps_folder_path(), RealDenoCacheEnv); + + let global_cache: Arc = Arc::new(global_cache_struct); + let http_client_provider = self.http_client_provider(); + let blob_store = Arc::new(deno_web::BlobStore::default()); + + Ok(Arc::new(FileFetcher::new( + global_cache.clone(), + self.file_fetcher_cache_strategy + .clone() + .unwrap_or(CacheSetting::ReloadAll), + self.file_fetcher_allow_remote, + http_client_provider.clone(), + blob_store, + ))) + }) + } + + pub async fn file_fetcher_loader(&self) -> Result, AnyError> { let global_cache_struct = GlobalHttpCache::new(self.deno_dir.deps_folder_path(), RealDenoCacheEnv); - let parsed_source = self.parsed_source_cache().unwrap(); - - Box::new(FetchCacher::new( - self.module_info_cache().unwrap().clone(), - self.emit_cache(self.transpile_options()).unwrap(), - Arc::new(self.file_fetcher()), + Ok(Box::new(FetchCacher::new( + self.emit_cache()?, + self.file_fetcher()?.clone(), HashMap::new(), Arc::new(global_cache_struct), - parsed_source, + self.npm_resolver().await?.clone(), + self.module_info_cache()?.clone(), FcPermissions::allow_all(), - None, // TODO: NPM - )) + ))) } } diff --git a/crates/sb_graph/graph_resolver.rs b/crates/sb_graph/graph_resolver.rs deleted file mode 100644 index aa61daea5..000000000 --- a/crates/sb_graph/graph_resolver.rs +++ /dev/null @@ -1,316 +0,0 @@ -use anyhow::anyhow; -use deno_config::JsxImportSourceConfig; -use deno_core::error::AnyError; -use deno_core::futures::future::LocalBoxFuture; -use deno_core::futures::FutureExt; -use deno_core::ModuleSpecifier; -use deno_npm::registry::NpmRegistryApi; -use deno_semver::package::PackageReq; -use eszip::deno_graph; -use eszip::deno_graph::source::{ - NpmResolver, ResolutionMode, ResolveError, Resolver, UnknownBuiltInNodeModuleError, - DEFAULT_JSX_IMPORT_SOURCE_MODULE, -}; -use eszip::deno_graph::NpmPackageReqResolution; -use import_map::ImportMap; -use log::debug; -use sb_core::util::sync::AtomicFlag; -use sb_node::is_builtin_node_module; -use sb_npm::installer::PackageJsonDepsInstaller; -use sb_npm::package_json::{PackageJsonDeps, PackageJsonDepsProvider}; -use sb_npm::registry::CliNpmRegistryApi; -use sb_npm::{CliNpmResolver, InnerCliNpmResolverRef}; -use std::path::PathBuf; -use std::sync::Arc; - -/// Result of checking if a specifier is mapped via -/// an import map or package.json. -pub enum MappedResolution { - None, - PackageJson(ModuleSpecifier), - ImportMap(ModuleSpecifier), -} - -impl MappedResolution { - pub fn into_specifier(self) -> Option { - match self { - MappedResolution::None => None, - MappedResolution::PackageJson(specifier) => Some(specifier), - MappedResolution::ImportMap(specifier) => Some(specifier), - } - } -} - -fn resolve_package_json_dep( - specifier: &str, - deps: &PackageJsonDeps, -) -> Result, AnyError> { - for (bare_specifier, req_result) in deps { - if specifier.starts_with(bare_specifier) { - let path = &specifier[bare_specifier.len()..]; - if path.is_empty() || path.starts_with('/') { - let req = req_result - .as_ref() - .map_err(|_err| anyhow!("Parsing version constraints in the application-level package.json is more strict at the moment."))?; - - return Ok(Some(ModuleSpecifier::parse(&format!("npm:{req}{path}"))?)); - } - } - } - - Ok(None) -} - -/// Resolver for specifiers that could be mapped via an -/// import map or package.json. -#[derive(Debug)] -pub struct MappedSpecifierResolver { - maybe_import_map: Option>, - package_json_deps_provider: Arc, -} - -impl MappedSpecifierResolver { - pub fn new( - maybe_import_map: Option>, - package_json_deps_provider: Arc, - ) -> Self { - Self { - maybe_import_map, - package_json_deps_provider, - } - } - - pub fn resolve( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - ) -> Result { - // attempt to resolve with the import map first - let maybe_import_map_err = match self - .maybe_import_map - .as_ref() - .map(|import_map| import_map.resolve(specifier, referrer)) - { - Some(Ok(value)) => return Ok(MappedResolution::ImportMap(value)), - Some(Err(err)) => Some(err), - None => None, - }; - - // then with package.json - if let Some(deps) = self.package_json_deps_provider.deps() { - if let Some(specifier) = resolve_package_json_dep(specifier, deps)? { - return Ok(MappedResolution::PackageJson(specifier)); - } - } - - // otherwise, surface the import map error or try resolving when has no import map - if let Some(err) = maybe_import_map_err { - Err(err.into()) - } else { - Ok(MappedResolution::None) - } - } -} - -/// A resolver that takes care of resolution, taking into account loaded -/// import map, JSX settings. -#[derive(Debug)] -pub struct CliGraphResolver { - mapped_specifier_resolver: MappedSpecifierResolver, - maybe_default_jsx_import_source: Option, - maybe_jsx_import_source_module: Option, - maybe_vendor_specifier: Option, - no_npm: bool, - npm_registry_api: Arc, - package_json_deps_installer: Arc, - found_package_json_dep_flag: Arc, - npm_resolver: Option>, -} - -#[derive(Default)] -pub struct CliGraphResolverOptions<'a> { - pub maybe_jsx_import_source_config: Option, - pub maybe_import_map: Option>, - pub maybe_vendor_dir: Option<&'a PathBuf>, - pub no_npm: bool, -} - -impl CliGraphResolver { - pub fn new( - npm_registry_api: Arc, - package_json_deps_provider: Arc, - package_json_deps_installer: Arc, - options: CliGraphResolverOptions, - npm_resolver: Option>, - ) -> Self { - Self { - mapped_specifier_resolver: MappedSpecifierResolver { - maybe_import_map: options.maybe_import_map, - package_json_deps_provider, - }, - maybe_default_jsx_import_source: options - .maybe_jsx_import_source_config - .as_ref() - .and_then(|c| c.default_specifier.clone()), - maybe_jsx_import_source_module: options - .maybe_jsx_import_source_config - .map(|c| c.module), - maybe_vendor_specifier: options - .maybe_vendor_dir - .and_then(|v| ModuleSpecifier::from_directory_path(v).ok()), - no_npm: options.no_npm, - npm_registry_api, - package_json_deps_installer, - found_package_json_dep_flag: Default::default(), - npm_resolver, - } - } - - pub fn set_jsx_import_source(&mut self, config: JsxImportSourceConfig) { - self.maybe_jsx_import_source_module = Some(config.module); - self.maybe_default_jsx_import_source = config.default_specifier; - } - - pub fn as_graph_resolver(&self) -> &dyn Resolver { - self - } - - pub fn as_graph_npm_resolver(&self) -> &dyn NpmResolver { - self - } - - pub fn found_package_json_dep(&self) -> bool { - self.found_package_json_dep_flag.is_raised() - } - - pub async fn force_top_level_package_json_install(&self) -> Result<(), AnyError> { - self.package_json_deps_installer - .ensure_top_level_install() - .await - } - - pub async fn top_level_package_json_install_if_necessary(&self) -> Result<(), AnyError> { - if self.found_package_json_dep_flag.is_raised() { - self.force_top_level_package_json_install().await?; - } - Ok(()) - } -} - -impl Resolver for CliGraphResolver { - fn default_jsx_import_source(&self) -> Option { - self.maybe_default_jsx_import_source.clone() - } - - fn jsx_import_source_module(&self) -> &str { - self.maybe_jsx_import_source_module - .as_deref() - .unwrap_or(DEFAULT_JSX_IMPORT_SOURCE_MODULE) - } - - fn resolve( - &self, - specifier: &str, - referrer_range: &deno_graph::Range, - _mode: ResolutionMode, - ) -> Result { - let referrer = &referrer_range.specifier; - let result = self - .mapped_specifier_resolver - .resolve(specifier, referrer) - .map_err(|err| err.into()) - .and_then(|resolution| match resolution { - MappedResolution::ImportMap(specifier) => Ok(specifier), - MappedResolution::PackageJson(specifier) => { - // found a specifier in the package.json, so mark that - // we need to do an "npm install" later - self.found_package_json_dep_flag.raise(); - Ok(specifier) - } - MappedResolution::None => { - deno_graph::resolve_import(specifier, &referrer_range.specifier) - .map_err(|err| err.into()) - } - }); - - // When the user is vendoring, don't allow them to import directly from the vendor/ directory - // as it might cause them confusion or duplicate dependencies. Additionally, this folder has - // special treatment in the language server so it will definitely cause issues/confusion there - // if they do this. - if let Some(vendor_specifier) = &self.maybe_vendor_specifier { - if let Ok(specifier) = &result { - if specifier.as_str().starts_with(vendor_specifier.as_str()) { - return Err(ResolveError::Other(anyhow!( - "Importing from the vendor directory is not permitted. Use a remote specifier instead or disable vendoring." - ))); - } - } - } - - result - } -} - -impl NpmResolver for CliGraphResolver { - fn resolve_builtin_node_module( - &self, - specifier: &ModuleSpecifier, - ) -> Result, UnknownBuiltInNodeModuleError> { - if specifier.scheme() != "node" { - return Ok(None); - } - - let module_name = specifier.path().to_string(); - if is_builtin_node_module(&module_name) { - Ok(Some(module_name)) - } else { - Err(UnknownBuiltInNodeModuleError { module_name }) - } - } - - fn on_resolve_bare_builtin_node_module(&self, module_name: &str, range: &deno_graph::Range) { - let deno_graph::Range { - start, specifier, .. - } = range.clone(); - let line = start.line + 1; - let column = start.character + 1; - debug!("Warning: Resolving \"{module_name}\" as \"node:{module_name}\" at {specifier}:{line}:{column}. If you want to use a built-in Node module, add a \"node:\" prefix.") - } - - fn load_and_cache_npm_package_info( - &self, - package_name: &str, - ) -> LocalBoxFuture<'static, Result<(), AnyError>> { - if self.no_npm { - // return it succeeded and error at the import site below - return Box::pin(deno_core::futures::future::ready(Ok(()))); - } - // this will internally cache the package information - let package_name = package_name.to_string(); - let api = self.npm_registry_api.clone(); - - async move { - api.package_info(&package_name) - .await - .map(|_| ()) - .map_err(|err| err.into()) - } - .boxed() - } - - fn resolve_npm(&self, package_req: &PackageReq) -> NpmPackageReqResolution { - match &self.npm_resolver { - Some(npm_resolver) => match npm_resolver.as_inner() { - InnerCliNpmResolverRef::Managed(npm_resolver) => { - npm_resolver.resolve_npm_for_deno_graph(package_req) - } - // if we are using byonm, then this should never be called because - // we don't use deno_graph's npm resolution in this case - InnerCliNpmResolverRef::Byonm(_) => unreachable!(), - }, - None => NpmPackageReqResolution::Err(anyhow!( - "npm specifiers were requested; but --no-npm is specified" - )), - } - } -} diff --git a/crates/sb_graph/graph_util.rs b/crates/sb_graph/graph_util.rs index 9270286d3..d2c8779fa 100644 --- a/crates/sb_graph/graph_util.rs +++ b/crates/sb_graph/graph_util.rs @@ -1,21 +1,24 @@ use crate::emitter::EmitterFactory; use crate::graph_fs::DenoGraphFsAdapter; -use crate::graph_resolver::CliGraphResolver; use crate::jsr::CliJsrUrlProvider; -use deno_ast::MediaType; +use crate::resolver::CliGraphResolver; +use anyhow::Context; use deno_core::error::{custom_error, AnyError}; use deno_core::parking_lot::Mutex; use deno_core::{FastString, ModuleSpecifier}; -use deno_graph::ModuleError; -use deno_graph::ResolutionError; +use deno_fs::FileSystem; +use deno_graph::source::{Loader, LoaderChecksum, ResolveError}; +use deno_graph::{ + GraphKind, JsrLoadError, ModuleError, ModuleGraph, ModuleLoadError, SpecifierError, +}; +use deno_graph::{ModuleGraphError, ResolutionError}; use deno_lockfile::Lockfile; use deno_semver::package::{PackageNv, PackageReq}; -use eszip::deno_graph::source::Loader; -use eszip::deno_graph::{GraphKind, ModuleGraph, ModuleGraphError}; -use eszip::{deno_graph, EszipV2}; +use eszip::EszipV2; +use import_map::ImportMapError; +use npm_cache::file_fetcher::File; use sb_core::cache::parsed_source::ParsedSourceCache; -use sb_core::errors_rt::get_error_class_name; -use sb_core::file_fetcher::File; +use sb_core::util::errors::get_error_class_name; use sb_npm::CliNpmResolver; use std::path::PathBuf; use std::sync::Arc; @@ -29,20 +32,103 @@ pub struct GraphValidOptions { /// Check if `roots` and their deps are available. Returns `Ok(())` if /// so. Returns `Err(_)` if there is a known module graph or resolution -/// error statically reachable from `roots` and not a dynamic import. -pub fn graph_valid_with_cli_options( +/// error statically reachable from `roots`. +/// +/// It is preferable to use this over using deno_graph's API directly +/// because it will have enhanced error message information specifically +/// for the CLI. +pub fn graph_valid( graph: &ModuleGraph, + fs: &Arc, roots: &[ModuleSpecifier], + options: GraphValidOptions, ) -> Result<(), AnyError> { - graph_valid( - graph, - roots, - GraphValidOptions { - is_vendoring: false, - follow_type_only: true, - check_js: false, - }, - ) + let mut errors = graph + .walk( + roots.iter(), + deno_graph::WalkOptions { + check_js: options.check_js, + follow_type_only: options.follow_type_only, + follow_dynamic: options.is_vendoring, + prefer_fast_check_graph: false, + }, + ) + .errors() + .flat_map(|error| { + let is_root = match &error { + ModuleGraphError::ResolutionError(_) + | ModuleGraphError::TypesResolutionError(_) => false, + ModuleGraphError::ModuleError(error) => roots.contains(error.specifier()), + }; + let mut message = match &error { + ModuleGraphError::ResolutionError(resolution_error) => { + enhanced_resolution_error_message(resolution_error) + } + ModuleGraphError::TypesResolutionError(resolution_error) => { + format!( + "Failed resolving types. {}", + enhanced_resolution_error_message(resolution_error) + ) + } + ModuleGraphError::ModuleError(error) => enhanced_lockfile_error_message(error) + .or_else(|| enhanced_sloppy_imports_error_message(fs, error)) + .unwrap_or_else(|| format!("{}", error)), + }; + + if let Some(range) = error.maybe_range() { + if !is_root && !range.specifier.as_str().contains("/$deno$eval") { + message.push_str("\n at "); + message.push_str(&format_range(range)); + } + } + + if graph.graph_kind() == GraphKind::TypesOnly + && matches!( + error, + ModuleGraphError::ModuleError(ModuleError::UnsupportedMediaType(..)) + ) + { + log::debug!("Ignoring: {}", message); + return None; + } + + if options.is_vendoring { + // warn about failing dynamic imports when vendoring, but don't fail completely + if matches!( + error, + ModuleGraphError::ModuleError(ModuleError::MissingDynamic(_, _)) + ) { + log::warn!("Ignoring: {}", message); + return None; + } + + // ignore invalid downgrades and invalid local imports when vendoring + match &error { + ModuleGraphError::ResolutionError(err) + | ModuleGraphError::TypesResolutionError(err) => { + if matches!( + err, + ResolutionError::InvalidDowngrade { .. } + | ResolutionError::InvalidLocalImport { .. } + ) { + return None; + } + } + ModuleGraphError::ModuleError(_) => {} + } + } + + Some(custom_error(get_error_class_name(&error.into()), message)) + }); + if let Some(error) = errors.next() { + Err(error) + } else { + // finally surface the npm resolution result + if let Err(err) = &graph.npm_dep_graph_result { + return Err(custom_error(get_error_class_name(err), format!("{}", err))); + } + Ok(()) + } } pub struct ModuleGraphBuilder { @@ -70,7 +156,7 @@ impl ModuleGraphBuilder { self.emitter_factory.get_lock_file() } - pub async fn npm_resolver(&self) -> &Arc { + pub async fn npm_resolver(&self) -> Result<&Arc, AnyError> { self.emitter_factory.npm_resolver().await } @@ -82,18 +168,19 @@ impl ModuleGraphBuilder { ) -> Result { let cli_resolver = self.resolver().await; let graph_resolver = cli_resolver.as_graph_resolver(); - let graph_npm_resolver = cli_resolver.as_graph_npm_resolver(); + let graph_npm_resolver = cli_resolver.create_graph_npm_resolver(); let psc = self.parsed_source_cache(); - let parser = psc.as_capturing_parser(); let analyzer = self .emitter_factory .module_info_cache() .unwrap() - .as_module_analyzer(&parser); + .as_module_analyzer(&psc); let mut graph = ModuleGraph::new(graph_kind); let fs = Arc::new(deno_fs::RealFs); let fs = DenoGraphFsAdapter(fs.as_ref()); + let lockfile = self.lockfile(); + let mut locker = lockfile.as_ref().map(|lockfile| LockfileLocker(&lockfile)); self.build_graph_with_npm_resolution( &mut graph, @@ -106,18 +193,19 @@ impl ModuleGraphBuilder { file_system: &fs, jsr_url_provider: &CliJsrUrlProvider, resolver: Some(graph_resolver), - npm_resolver: Some(graph_npm_resolver), + npm_resolver: Some(&graph_npm_resolver), module_analyzer: &analyzer, reporter: None, workspace_members: &[], passthrough_jsr_specifiers: false, + locker: locker.as_mut().map(|l| l as _), }, ) .await?; if graph.has_node_specifier && self.type_check { self.npm_resolver() - .await + .await? .as_managed() .unwrap() .inject_synthetic_types_node_package() @@ -131,13 +219,13 @@ impl ModuleGraphBuilder { &self, graph: &mut ModuleGraph, roots: Vec, - loader: &mut dyn deno_graph::source::Loader, + loader: &'a mut dyn deno_graph::source::Loader, options: deno_graph::BuildOptions<'a>, ) -> Result<(), AnyError> { - // TODO: Option here similar to: https://github.com/denoland/deno/blob/v1.37.1/cli/graph_util.rs#L323C5-L405C11 - // self.resolver.force_top_level_package_json_install().await?; TODO - // add the lockfile redirects to the graph if it's the first time executing - if graph.redirects.is_empty() { + // fill the graph with the information from the lockfile + let is_first_execution = graph.roots.is_empty(); + if is_first_execution { + // populate the information from the lockfile if let Some(lockfile) = &self.lockfile() { let lockfile = lockfile.lock(); for (from, to) in &lockfile.content.redirects { @@ -149,13 +237,6 @@ impl ModuleGraphBuilder { } } } - } - } - - // add the jsr specifiers to the graph if it's the first time executing - if graph.packages.is_empty() { - if let Some(lockfile) = &self.lockfile() { - let lockfile = lockfile.lock(); for (key, value) in &lockfile.content.packages.specifiers { if let Some(key) = key .strip_prefix("jsr:") @@ -172,46 +253,50 @@ impl ModuleGraphBuilder { } } + let initial_redirects_len = graph.redirects.len(); + let initial_package_deps_len = graph.packages.package_deps_sum(); + let initial_package_mappings_len = graph.packages.mappings().len(); + graph.build(roots, loader, options).await; - // add the redirects in the graph to the lockfile - if !graph.redirects.is_empty() { - if let Some(lockfile) = &self.lockfile() { - let graph_redirects = graph - .redirects - .iter() - .filter(|(from, _)| !matches!(from.scheme(), "npm" | "file" | "deno")); - let mut lockfile = lockfile.lock(); - for (from, to) in graph_redirects { - lockfile.insert_redirect(from.to_string(), to.to_string()); - } - } - } + let has_redirects_changed = graph.redirects.len() != initial_redirects_len; + let has_jsr_package_deps_changed = + graph.packages.package_deps_sum() != initial_package_deps_len; + let has_jsr_package_mappings_changed = + graph.packages.mappings().len() != initial_package_mappings_len; - // add the jsr specifiers in the graph to the lockfile - if !graph.packages.is_empty() { + if has_redirects_changed || has_jsr_package_deps_changed || has_jsr_package_mappings_changed + { if let Some(lockfile) = &self.lockfile() { - let mappings = graph.packages.mappings(); let mut lockfile = lockfile.lock(); - for (from, to) in mappings { - lockfile - .insert_package_specifier(format!("jsr:{}", from), format!("jsr:{}", to)); + // https redirects + if has_redirects_changed { + let graph_redirects = graph + .redirects + .iter() + .filter(|(from, _)| !matches!(from.scheme(), "npm" | "file" | "deno")); + for (from, to) in graph_redirects { + lockfile.insert_redirect(from.to_string(), to.to_string()); + } + } + // jsr package mappings + if has_jsr_package_mappings_changed { + for (from, to) in graph.packages.mappings() { + lockfile.insert_package_specifier( + format!("jsr:{}", from), + format!("jsr:{}", to), + ); + } + } + // jsr packages + if has_jsr_package_deps_changed { + for (name, deps) in graph.packages.packages_with_deps() { + lockfile.add_package_deps(&name.to_string(), deps.map(|s| s.to_string())); + } } } } - if let Some(npm_resolver) = self.npm_resolver().await.as_managed() { - // ensure that the top level package.json is installed if a - // specifier was matched in the package.json - if self.resolver().await.found_package_json_dep() { - npm_resolver.ensure_top_level_package_json_install().await?; - } - - // resolve the dependencies of any pending dependencies - // that were inserted by building the graph - npm_resolver.resolve_pending().await?; - } - Ok(()) } @@ -220,22 +305,23 @@ impl ModuleGraphBuilder { &self, roots: Vec, ) -> Result { - // - let mut cache = self.emitter_factory.file_fetcher_loader(); + let mut cache = self.emitter_factory.file_fetcher_loader().await?; let cli_resolver = self.resolver().await.clone(); let graph_resolver = cli_resolver.as_graph_resolver(); - let graph_npm_resolver = cli_resolver.as_graph_npm_resolver(); + let graph_npm_resolver = cli_resolver.create_graph_npm_resolver(); let psc = self.parsed_source_cache(); - let parser = psc.as_capturing_parser(); let analyzer = self .emitter_factory .module_info_cache() .unwrap() - .as_module_analyzer(&parser); + .as_module_analyzer(&psc); + let graph_kind = deno_graph::GraphKind::CodeOnly; let mut graph = ModuleGraph::new(graph_kind); let fs = Arc::new(deno_fs::RealFs); let fs = DenoGraphFsAdapter(fs.as_ref()); + let lockfile = self.lockfile(); + let mut locker = lockfile.as_ref().map(|lockfile| LockfileLocker(&lockfile)); self.build_graph_with_npm_resolution( &mut graph, @@ -248,85 +334,45 @@ impl ModuleGraphBuilder { file_system: &fs, jsr_url_provider: &CliJsrUrlProvider, resolver: Some(&*graph_resolver), - npm_resolver: Some(&*graph_npm_resolver), + npm_resolver: Some(&graph_npm_resolver), module_analyzer: &analyzer, reporter: None, workspace_members: &[], passthrough_jsr_specifiers: false, + locker: locker.as_mut().map(|l| l as _), }, ) .await?; + self.graph_valid(&graph)?; + Ok(graph) } -} -/// Check if `roots` and their deps are available. Returns `Ok(())` if -/// so. Returns `Err(_)` if there is a known module graph or resolution -/// error statically reachable from `roots`. -/// -/// It is preferable to use this over using deno_graph's API directly -/// because it will have enhanced error message information specifically -/// for the CLI. -pub fn graph_valid( - graph: &ModuleGraph, - roots: &[ModuleSpecifier], - options: GraphValidOptions, -) -> Result<(), AnyError> { - let mut errors = graph - .walk( + /// Check if `roots` and their deps are available. Returns `Ok(())` if + /// so. Returns `Err(_)` if there is a known module graph or resolution + /// error statically reachable from `roots` and not a dynamic import. + pub fn graph_valid(&self, graph: &ModuleGraph) -> Result<(), AnyError> { + self.graph_roots_valid(graph, &graph.roots.iter().cloned().collect::>()) + } + + pub fn graph_roots_valid( + &self, + graph: &ModuleGraph, + roots: &[ModuleSpecifier], + ) -> Result<(), AnyError> { + let fs: Arc = Arc::new(deno_fs::RealFs); + + graph_valid( + graph, + &fs, roots, - deno_graph::WalkOptions { - check_js: options.check_js, - follow_type_only: options.follow_type_only, - follow_dynamic: options.is_vendoring, - prefer_fast_check_graph: false, + GraphValidOptions { + is_vendoring: false, + follow_type_only: true, + check_js: false, }, ) - .errors() - .flat_map(|error| { - let _is_root = match &error { - ModuleGraphError::ResolutionError(_) => false, - ModuleGraphError::ModuleError(error) => roots.contains(error.specifier()), - _ => false, - }; - let message = if let ModuleGraphError::ResolutionError(_err) = &error { - format!("{error}") - } else { - format!("{error}") - }; - - if options.is_vendoring { - // warn about failing dynamic imports when vendoring, but don't fail completely - if matches!( - error, - ModuleGraphError::ModuleError(ModuleError::MissingDynamic(_, _)) - ) { - log::warn!("Ignoring: {:#}", message); - return None; - } - - // ignore invalid downgrades and invalid local imports when vendoring - if let ModuleGraphError::ResolutionError(err) = &error { - if matches!( - err, - ResolutionError::InvalidDowngrade { .. } - | ResolutionError::InvalidLocalImport { .. } - ) { - return None; - } - } - } - - Some(custom_error( - get_error_class_name(&error.into()).unwrap(), - message, - )) - }); - if let Some(error) = errors.next() { - Err(error) - } else { - Ok(()) } } @@ -338,33 +384,31 @@ pub async fn create_eszip_from_graph_raw( let emitter = emitter_factory.unwrap_or_else(|| Arc::new(EmitterFactory::new())); let parser_arc = emitter.clone().parsed_source_cache().unwrap(); let parser = parser_arc.as_capturing_parser(); + let transpile_options = emitter.transpile_options(); + let emit_options = emitter.emit_options(); - eszip::EszipV2::from_graph( + eszip::EszipV2::from_graph(eszip::FromGraphOptions { graph, - &parser, - emitter.transpile_options(), - emitter.emit_options(), - ) + parser, + transpile_options, + emit_options, + relative_file_base: None, + }) } pub async fn create_graph( file: PathBuf, emitter_factory: Arc, maybe_code: &Option, -) -> ModuleGraph { +) -> Result { let module_specifier = if let Some(code) = maybe_code { let specifier = ModuleSpecifier::parse("file:///src/index.ts").unwrap(); - emitter_factory.file_cache().insert( - specifier.clone(), - File { - maybe_types: None, - media_type: MediaType::TypeScript, - source: code.as_str().into(), - specifier: specifier.clone(), - maybe_headers: None, - }, - ); + emitter_factory.file_fetcher()?.insert_memory_files(File { + specifier: specifier.clone(), + maybe_headers: None, + source: code.as_bytes().into(), + }); specifier } else { @@ -376,7 +420,193 @@ pub async fn create_graph( }; let builder = ModuleGraphBuilder::new(emitter_factory, false); - let create_module_graph_task = builder.create_graph_and_maybe_check(vec![module_specifier]); - create_module_graph_task.await.unwrap() + + create_module_graph_task + .await + .context("failed to create the graph") +} + +/// Adds more explanatory information to a resolution error. +pub fn enhanced_resolution_error_message(error: &ResolutionError) -> String { + let message = format!("{error}"); + + // if let Some(specifier) = get_resolution_error_bare_node_specifier(error) { + // if !*DENO_DISABLE_PEDANTIC_NODE_WARNINGS { + // message.push_str(&format!( + // "\nIf you want to use a built-in Node module, add a \"node:\" prefix (ex. \"node:{specifier}\")." + // )); + // } + // } + + message +} + +fn enhanced_sloppy_imports_error_message( + _fs: &Arc, + error: &ModuleError, +) -> Option { + match error { + ModuleError::LoadingErr(_specifier, _, ModuleLoadError::Loader(_)) // ex. "Is a directory" error + | ModuleError::Missing(_specifier, _) => Some(format!("{}", error)), + _ => None, + } +} + +fn enhanced_lockfile_error_message(err: &ModuleError) -> Option { + match err { + ModuleError::LoadingErr( + specifier, + _, + ModuleLoadError::Jsr(JsrLoadError::ContentChecksumIntegrity( + checksum_err, + )), + ) => { + Some(format!( + concat!( + "Integrity check failed in package. The package may have been tampered with.\n\n", + " Specifier: {}\n", + " Actual: {}\n", + " Expected: {}\n\n", + "If you modified your global cache, run again with the --reload flag to restore ", + "its state. If you want to modify dependencies locally run again with the ", + "--vendor flag or specify `\"vendor\": true` in a deno.json then modify the contents ", + "of the vendor/ folder." + ), + specifier, + checksum_err.actual, + checksum_err.expected, + )) + } + ModuleError::LoadingErr( + _specifier, + _, + ModuleLoadError::Jsr( + JsrLoadError::PackageVersionManifestChecksumIntegrity( + package_nv, + checksum_err, + ), + ), + ) => { + Some(format!( + concat!( + "Integrity check failed for package. The source code is invalid, as it does not match the expected hash in the lock file.\n\n", + " Package: {}\n", + " Actual: {}\n", + " Expected: {}\n\n", + "This could be caused by:\n", + " * the lock file may be corrupt\n", + " * the source itself may be corrupt\n\n", + "Use the --lock-write flag to regenerate the lockfile or --reload to reload the source code from the server." + ), + package_nv, + checksum_err.actual, + checksum_err.expected, + )) + } + ModuleError::LoadingErr( + specifier, + _, + ModuleLoadError::HttpsChecksumIntegrity(checksum_err), + ) => { + Some(format!( + concat!( + "Integrity check failed for remote specifier. The source code is invalid, as it does not match the expected hash in the lock file.\n\n", + " Specifier: {}\n", + " Actual: {}\n", + " Expected: {}\n\n", + "This could be caused by:\n", + " * the lock file may be corrupt\n", + " * the source itself may be corrupt\n\n", + "Use the --lock-write flag to regenerate the lockfile or --reload to reload the source code from the server." + ), + specifier, + checksum_err.actual, + checksum_err.expected, + )) + } + _ => None, + } +} + +pub fn get_resolution_error_bare_node_specifier(error: &ResolutionError) -> Option<&str> { + get_resolution_error_bare_specifier(error) + .filter(|specifier| sb_node::is_builtin_node_module(specifier)) +} + +fn get_resolution_error_bare_specifier(error: &ResolutionError) -> Option<&str> { + if let ResolutionError::InvalidSpecifier { + error: SpecifierError::ImportPrefixMissing { specifier, .. }, + .. + } = error + { + Some(specifier.as_str()) + } else if let ResolutionError::ResolverError { error, .. } = error { + if let ResolveError::Other(error) = (*error).as_ref() { + if let Some(ImportMapError::UnmappedBareSpecifier(specifier, _)) = + error.downcast_ref::() + { + Some(specifier.as_str()) + } else { + None + } + } else { + None + } + } else { + None + } +} + +pub fn format_range(range: &deno_graph::Range) -> String { + format!( + "{}:{}:{}", + range.specifier.as_str(), + &(range.start.line + 1).to_string(), + &(range.start.character + 1).to_string() + ) +} + +struct LockfileLocker<'a>(&'a Arc>); + +impl<'a> deno_graph::source::Locker for LockfileLocker<'a> { + fn get_remote_checksum(&self, specifier: &deno_ast::ModuleSpecifier) -> Option { + self.0 + .lock() + .remote() + .get(specifier.as_str()) + .map(|s| LoaderChecksum::new(s.clone())) + } + + fn has_remote_checksum(&self, specifier: &deno_ast::ModuleSpecifier) -> bool { + self.0.lock().remote().contains_key(specifier.as_str()) + } + + fn set_remote_checksum( + &mut self, + specifier: &deno_ast::ModuleSpecifier, + checksum: LoaderChecksum, + ) { + self.0 + .lock() + .insert_remote(specifier.to_string(), checksum.into_string()) + } + + fn get_pkg_manifest_checksum(&self, package_nv: &PackageNv) -> Option { + self.0 + .lock() + .content + .packages + .jsr + .get(&package_nv.to_string()) + .map(|s| LoaderChecksum::new(s.integrity.clone())) + } + + fn set_pkg_manifest_checksum(&mut self, package_nv: &PackageNv, checksum: LoaderChecksum) { + // a value would only exist in here if two workers raced + // to insert the same package manifest checksum + self.0 + .lock() + .insert_package(package_nv.to_string(), checksum.into_string()); + } } diff --git a/crates/sb_graph/import_map.rs b/crates/sb_graph/import_map.rs index 4dfce6594..0d71a8920 100644 --- a/crates/sb_graph/import_map.rs +++ b/crates/sb_graph/import_map.rs @@ -27,7 +27,7 @@ pub fn load_import_map(maybe_path: Option) -> Result, .map_err(|_| anyhow!("invalid import map base url"))?; } - let result = parse_from_json(&base_url, json_str.as_str())?; + let result = parse_from_json(base_url, json_str.as_str())?; Ok(Some(result.import_map)) } else { Ok(None) diff --git a/crates/sb_graph/resolver.rs b/crates/sb_graph/resolver.rs new file mode 100644 index 000000000..fac6e861e --- /dev/null +++ b/crates/sb_graph/resolver.rs @@ -0,0 +1,758 @@ +use std::{ + borrow::Cow, + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use dashmap::DashSet; +use deno_ast::MediaType; +use deno_config::{ + package_json::PackageJsonDepValue, + workspace::{MappedResolution, MappedResolutionError, WorkspaceResolver}, + JsxImportSourceConfig, +}; +use deno_core::{error::AnyError, unsync::AtomicFlag, ModuleSourceCode, ModuleSpecifier}; +use deno_graph::{ + source::{ + ResolutionMode, ResolveError, Resolver, UnknownBuiltInNodeModuleError, + DEFAULT_JSX_IMPORT_SOURCE_MODULE, + }, + NpmLoadError, NpmResolvePkgReqsResult, +}; +use deno_npm::resolution::NpmResolutionError; +use deno_semver::{npm::NpmPackageReqReference, package::PackageReq}; +use sb_core::node::CliNodeCodeTranslator; +use sb_node::{ + errors::{ClosestPkgJsonError, UrlToNodeResolutionError}, + is_builtin_node_module, parse_npm_pkg_name, NodeModuleKind, NodeResolution, NodeResolutionMode, + NodeResolver, NpmResolver, PackageJson, +}; +use sb_npm::{byonm::ByonmCliNpmResolver, CliNpmResolver, InnerCliNpmResolverRef}; + +pub struct ModuleCodeStringSource { + pub code: ModuleSourceCode, + pub found_url: ModuleSpecifier, + pub media_type: MediaType, +} + +#[derive(Debug)] +pub struct CliNodeResolver { + cjs_resolutions: Arc, + fs: Arc, + node_resolver: Arc, + // todo(dsherret): remove this pub(crate) + pub(crate) npm_resolver: Arc, +} + +impl CliNodeResolver { + pub fn new( + cjs_resolutions: Arc, + fs: Arc, + node_resolver: Arc, + npm_resolver: Arc, + ) -> Self { + Self { + cjs_resolutions, + fs, + node_resolver, + npm_resolver, + } + } + + pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { + self.npm_resolver.in_npm_package(specifier) + } + + pub fn get_closest_package_json( + &self, + referrer: &ModuleSpecifier, + ) -> Result>, ClosestPkgJsonError> { + self.node_resolver.get_closest_package_json(referrer) + } + + pub fn resolve_if_in_npm_package( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + mode: NodeResolutionMode, + ) -> Option, AnyError>> { + if self.in_npm_package(referrer) { + // we're in an npm package, so use node resolution + Some(self.resolve(specifier, referrer, mode)) + } else { + None + } + } + + pub fn resolve( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + mode: NodeResolutionMode, + ) -> Result, AnyError> { + let referrer_kind = if self.cjs_resolutions.contains(referrer) { + NodeModuleKind::Cjs + } else { + NodeModuleKind::Esm + }; + + self.handle_node_resolve_result( + self.node_resolver + .resolve(specifier, referrer, referrer_kind, mode) + .map_err(AnyError::from), + ) + } + + pub fn resolve_req_reference( + &self, + req_ref: &NpmPackageReqReference, + referrer: &ModuleSpecifier, + mode: NodeResolutionMode, + ) -> Result { + self.resolve_req_with_sub_path(req_ref.req(), req_ref.sub_path(), referrer, mode) + } + + pub fn resolve_req_with_sub_path( + &self, + req: &PackageReq, + sub_path: Option<&str>, + referrer: &ModuleSpecifier, + mode: NodeResolutionMode, + ) -> Result { + let package_folder = self + .npm_resolver + .resolve_pkg_folder_from_deno_module_req(req, referrer)?; + let maybe_resolution = self.maybe_resolve_package_sub_path_from_deno_module( + &package_folder, + sub_path, + Some(referrer), + mode, + )?; + match maybe_resolution { + Some(resolution) => Ok(resolution), + None => { + if self.npm_resolver.as_byonm().is_some() { + let package_json_path = package_folder.join("package.json"); + if !self.fs.exists_sync(&package_json_path) { + return Err(anyhow!( + "Could not find '{}'. Deno expects the node_modules/ directory to be up to date. Did you forget to run `npm install`?", + package_json_path.display() + )); + } + } + Err(anyhow!( + "Failed resolving '{}{}' in '{}'.", + req, + sub_path.map(|s| format!("/{}", s)).unwrap_or_default(), + package_folder.display() + )) + } + } + } + + pub fn resolve_package_sub_path_from_deno_module( + &self, + package_folder: &Path, + sub_path: Option<&str>, + maybe_referrer: Option<&ModuleSpecifier>, + mode: NodeResolutionMode, + ) -> Result { + self.maybe_resolve_package_sub_path_from_deno_module( + package_folder, + sub_path, + maybe_referrer, + mode, + )? + .ok_or_else(|| { + anyhow!( + "Failed resolving '{}' in '{}'.", + sub_path + .map(|s| format!("/{}", s)) + .unwrap_or_else(|| ".".to_string()), + package_folder.display(), + ) + }) + } + + pub fn maybe_resolve_package_sub_path_from_deno_module( + &self, + package_folder: &Path, + sub_path: Option<&str>, + maybe_referrer: Option<&ModuleSpecifier>, + mode: NodeResolutionMode, + ) -> Result, AnyError> { + self.handle_node_resolve_result( + self.node_resolver + .resolve_package_subpath_from_deno_module( + package_folder, + sub_path, + maybe_referrer, + mode, + ) + .map_err(AnyError::from), + ) + } + + pub fn handle_if_in_node_modules( + &self, + specifier: ModuleSpecifier, + ) -> Result { + // skip canonicalizing if we definitely know it's unnecessary + if specifier.scheme() == "file" && specifier.path().contains("/node_modules/") { + // Specifiers in the node_modules directory are canonicalized + // so canoncalize then check if it's in the node_modules directory. + // If so, check if we need to store this specifier as being a CJS + // resolution. + let specifier = sb_core::node::resolve_specifier_into_node_modules(&specifier); + if self.in_npm_package(&specifier) { + let resolution = self.node_resolver.url_to_node_resolution(specifier)?; + if let NodeResolution::CommonJs(specifier) = &resolution { + self.cjs_resolutions.insert(specifier.clone()); + } + return Ok(resolution.into_url()); + } + } + + Ok(specifier) + } + + pub fn url_to_node_resolution( + &self, + specifier: ModuleSpecifier, + ) -> Result { + self.node_resolver.url_to_node_resolution(specifier) + } + + fn handle_node_resolve_result( + &self, + result: Result, AnyError>, + ) -> Result, AnyError> { + match result? { + Some(response) => { + if let NodeResolution::CommonJs(specifier) = &response { + // remember that this was a common js resolution + self.cjs_resolutions.insert(specifier.clone()); + } + Ok(Some(response)) + } + None => Ok(None), + } + } +} + +#[derive(Clone)] +pub struct NpmModuleLoader { + cjs_resolutions: Arc, + node_code_translator: Arc, + fs: Arc, + node_resolver: Arc, +} + +impl NpmModuleLoader { + pub fn new( + cjs_resolutions: Arc, + node_code_translator: Arc, + fs: Arc, + node_resolver: Arc, + ) -> Self { + Self { + cjs_resolutions, + node_code_translator, + fs, + node_resolver, + } + } + + pub async fn load_if_in_npm_package( + &self, + specifier: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, + ) -> Option> { + if self.node_resolver.in_npm_package(specifier) { + Some(self.load(specifier, maybe_referrer).await) + } else { + None + } + } + + pub async fn load( + &self, + specifier: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, + ) -> Result { + let file_path = specifier.to_file_path().unwrap(); + let code = self + .fs + .read_file_async(file_path.clone(), None) + .await + .map_err(AnyError::from) + .with_context(|| { + if file_path.is_dir() { + // directory imports are not allowed when importing from an + // ES module, so provide the user with a helpful error message + let dir_path = file_path; + let mut msg = "Directory import ".to_string(); + msg.push_str(&dir_path.to_string_lossy()); + if let Some(referrer) = &maybe_referrer { + msg.push_str(" is not supported resolving import from "); + msg.push_str(referrer.as_str()); + let entrypoint_name = ["index.mjs", "index.js", "index.cjs"] + .iter() + .find(|e| dir_path.join(e).is_file()); + if let Some(entrypoint_name) = entrypoint_name { + msg.push_str("\nDid you mean to import "); + msg.push_str(entrypoint_name); + msg.push_str(" within the directory?"); + } + } + msg + } else { + let mut msg = "Unable to load ".to_string(); + msg.push_str(&file_path.to_string_lossy()); + if let Some(referrer) = &maybe_referrer { + msg.push_str(" imported from "); + msg.push_str(referrer.as_str()); + } + msg + } + })?; + + let code = if self.cjs_resolutions.contains(specifier) { + // translate cjs to esm if it's cjs and inject node globals + let code = match String::from_utf8_lossy(&code) { + Cow::Owned(code) => code, + // SAFETY: `String::from_utf8_lossy` guarantees that the result is valid + // UTF-8 if `Cow::Borrowed` is returned. + Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(code) }, + }; + ModuleSourceCode::String( + self.node_code_translator + .translate_cjs_to_esm(specifier, Some(code)) + .await? + .into(), + ) + } else { + // esm and json code is untouched + ModuleSourceCode::Bytes(code.into_boxed_slice().into()) + }; + Ok(ModuleCodeStringSource { + code, + found_url: specifier.clone(), + media_type: MediaType::from_specifier(specifier), + }) + } +} + +/// Keeps track of what module specifiers were resolved as CJS. +#[derive(Debug, Default)] +pub struct CjsResolutionStore(DashSet); + +impl CjsResolutionStore { + pub fn contains(&self, specifier: &ModuleSpecifier) -> bool { + self.0.contains(specifier) + } + + pub fn insert(&self, specifier: ModuleSpecifier) { + self.0.insert(specifier); + } +} + +/// A resolver that takes care of resolution, taking into account loaded +/// import map, JSX settings. +#[derive(Debug)] +pub struct CliGraphResolver { + node_resolver: Option>, + npm_resolver: Option>, + workspace_resolver: Arc, + maybe_default_jsx_import_source: Option, + maybe_default_jsx_import_source_types: Option, + maybe_jsx_import_source_module: Option, + maybe_vendor_specifier: Option, + found_package_json_dep_flag: AtomicFlag, + bare_node_builtins_enabled: bool, +} + +pub struct CliGraphResolverOptions<'a> { + pub node_resolver: Option>, + pub npm_resolver: Option>, + pub workspace_resolver: Arc, + pub bare_node_builtins_enabled: bool, + pub maybe_jsx_import_source_config: Option, + pub maybe_vendor_dir: Option<&'a PathBuf>, +} + +impl CliGraphResolver { + pub fn new(options: CliGraphResolverOptions) -> Self { + Self { + node_resolver: options.node_resolver, + npm_resolver: options.npm_resolver, + workspace_resolver: options.workspace_resolver, + maybe_default_jsx_import_source: options + .maybe_jsx_import_source_config + .as_ref() + .and_then(|c| c.default_specifier.clone()), + maybe_default_jsx_import_source_types: options + .maybe_jsx_import_source_config + .as_ref() + .and_then(|c| c.default_types_specifier.clone()), + maybe_jsx_import_source_module: options + .maybe_jsx_import_source_config + .map(|c| c.module), + maybe_vendor_specifier: options + .maybe_vendor_dir + .and_then(|v| ModuleSpecifier::from_directory_path(v).ok()), + found_package_json_dep_flag: Default::default(), + bare_node_builtins_enabled: options.bare_node_builtins_enabled, + } + } + + pub fn as_graph_resolver(&self) -> &dyn Resolver { + self + } + + pub fn create_graph_npm_resolver(&self) -> WorkerCliNpmGraphResolver { + WorkerCliNpmGraphResolver { + npm_resolver: self.npm_resolver.as_ref(), + found_package_json_dep_flag: &self.found_package_json_dep_flag, + bare_node_builtins_enabled: self.bare_node_builtins_enabled, + } + } + + // todo(dsherret): if we returned structured errors from the NodeResolver we wouldn't need this + fn check_surface_byonm_node_error( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + original_err: AnyError, + resolver: &ByonmCliNpmResolver, + ) -> Result<(), AnyError> { + if let Ok((pkg_name, _, _)) = parse_npm_pkg_name(specifier, referrer) { + match resolver.resolve_package_folder_from_package(&pkg_name, referrer) { + Ok(_) => { + return Err(original_err); + } + Err(_) => { + if resolver + .find_ancestor_package_json_with_dep(&pkg_name, referrer) + .is_some() + { + return Err(anyhow!( + concat!( + "Could not resolve \"{}\", but found it in a package.json. ", + "Deno expects the node_modules/ directory to be up to date. ", + "Did you forget to run `npm install`?" + ), + specifier + )); + } + } + } + } + Ok(()) + } +} + +impl Resolver for CliGraphResolver { + fn default_jsx_import_source(&self) -> Option { + self.maybe_default_jsx_import_source.clone() + } + + fn default_jsx_import_source_types(&self) -> Option { + self.maybe_default_jsx_import_source_types.clone() + } + + fn jsx_import_source_module(&self) -> &str { + self.maybe_jsx_import_source_module + .as_deref() + .unwrap_or(DEFAULT_JSX_IMPORT_SOURCE_MODULE) + } + + fn resolve( + &self, + specifier: &str, + referrer_range: &deno_graph::Range, + mode: ResolutionMode, + ) -> Result { + fn to_node_mode(mode: ResolutionMode) -> NodeResolutionMode { + match mode { + ResolutionMode::Execution => NodeResolutionMode::Execution, + ResolutionMode::Types => NodeResolutionMode::Types, + } + } + + let referrer = &referrer_range.specifier; + let result: Result<_, ResolveError> = self + .workspace_resolver + .resolve(specifier, referrer) + .map_err(|err| match err { + MappedResolutionError::Specifier(err) => ResolveError::Specifier(err), + MappedResolutionError::ImportMap(err) => ResolveError::Other(err.into()), + }); + let result = match result { + Ok(resolution) => match resolution { + MappedResolution::Normal(specifier) | MappedResolution::ImportMap(specifier) => { + Ok(specifier) + } + // todo(dsherret): for byonm it should do resolution solely based on + // the referrer and not the package.json + MappedResolution::PackageJson { + dep_result, + alias, + sub_path, + .. + } => { + // found a specifier in the package.json, so mark that + // we need to do an "npm install" later + self.found_package_json_dep_flag.raise(); + + dep_result + .as_ref() + .map_err(|e| ResolveError::Other(e.clone().into())) + .and_then(|dep| match dep { + PackageJsonDepValue::Req(req) => ModuleSpecifier::parse(&format!( + "npm:{}{}", + req, + sub_path.map(|s| format!("/{}", s)).unwrap_or_default() + )) + .map_err(|e| ResolveError::Other(e.into())), + PackageJsonDepValue::Workspace(version_req) => self + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_pkg_json_dep( + alias, + version_req, + ) + .map_err(|e| ResolveError::Other(e.into())) + .and_then(|pkg_folder| { + Ok(self + .node_resolver + .as_ref() + .unwrap() + .resolve_package_sub_path_from_deno_module( + pkg_folder, + sub_path.as_deref(), + Some(referrer), + to_node_mode(mode), + )? + .into_url()) + }), + }) + } + }, + Err(err) => Err(err), + }; + + // check if it's an npm specifier that resolves to a workspace member + if let Some(node_resolver) = &self.node_resolver { + if let Ok(specifier) = &result { + if let Ok(req_ref) = NpmPackageReqReference::from_specifier(specifier) { + if let Some(pkg_folder) = self + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_npm_specifier(req_ref.req()) + { + return Ok(node_resolver + .resolve_package_sub_path_from_deno_module( + pkg_folder, + req_ref.sub_path(), + Some(referrer), + to_node_mode(mode), + )? + .into_url()); + } + } + } + } + + // When the user is vendoring, don't allow them to import directly from the vendor/ directory + // as it might cause them confusion or duplicate dependencies. Additionally, this folder has + // special treatment in the language server so it will definitely cause issues/confusion there + // if they do this. + if let Some(vendor_specifier) = &self.maybe_vendor_specifier { + if let Ok(specifier) = &result { + if specifier.as_str().starts_with(vendor_specifier.as_str()) { + return Err(ResolveError::Other(anyhow!("Importing from the vendor directory is not permitted. Use a remote specifier instead or disable vendoring."))); + } + } + } + + if let Some(resolver) = self.npm_resolver.as_ref().and_then(|r| r.as_byonm()) { + match &result { + Ok(specifier) => { + if let Ok(npm_req_ref) = NpmPackageReqReference::from_specifier(specifier) { + let node_resolver = self.node_resolver.as_ref().unwrap(); + return node_resolver + .resolve_req_reference(&npm_req_ref, referrer, to_node_mode(mode)) + .map(|res| res.into_url()) + .map_err(|err| err.into()); + } + } + Err(_) => { + if referrer.scheme() == "file" { + if let Some(node_resolver) = &self.node_resolver { + let node_result = + node_resolver.resolve(specifier, referrer, to_node_mode(mode)); + match node_result { + Ok(Some(res)) => { + return Ok(res.into_url()); + } + Ok(None) => { + self.check_surface_byonm_node_error( + specifier, + referrer, + anyhow!("Cannot find \"{}\"", specifier), + resolver, + ) + .map_err(ResolveError::Other)?; + } + Err(err) => { + self.check_surface_byonm_node_error( + specifier, referrer, err, resolver, + ) + .map_err(ResolveError::Other)?; + } + } + } + } + } + } + } + + if referrer.scheme() == "file" { + if let Some(node_resolver) = &self.node_resolver { + let node_result = node_resolver.resolve_if_in_npm_package( + specifier, + referrer, + to_node_mode(mode), + ); + if let Some(Ok(Some(res))) = node_result { + return Ok(res.into_url()); + } + } + } + + let specifier = result?; + match &self.node_resolver { + Some(node_resolver) => node_resolver + .handle_if_in_node_modules(specifier) + .map_err(|e| e.into()), + None => Ok(specifier), + } + } +} + +#[derive(Debug)] +pub struct WorkerCliNpmGraphResolver<'a> { + npm_resolver: Option<&'a Arc>, + found_package_json_dep_flag: &'a AtomicFlag, + bare_node_builtins_enabled: bool, +} + +#[async_trait(?Send)] +impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> { + fn resolve_builtin_node_module( + &self, + specifier: &ModuleSpecifier, + ) -> Result, UnknownBuiltInNodeModuleError> { + if specifier.scheme() != "node" { + return Ok(None); + } + + let module_name = specifier.path().to_string(); + if is_builtin_node_module(&module_name) { + Ok(Some(module_name)) + } else { + Err(UnknownBuiltInNodeModuleError { module_name }) + } + } + + fn on_resolve_bare_builtin_node_module(&self, _module_name: &str, _range: &deno_graph::Range) { + // let deno_graph::Range { + // start, specifier, .. + // } = range; + // let line = start.line + 1; + // let column = start.character + 1; + // if !*DENO_DISABLE_PEDANTIC_NODE_WARNINGS { + // log::warn!("Warning: Resolving \"{module_name}\" as \"node:{module_name}\" at {specifier}:{line}:{column}. If you want to use a built-in Node module, add a \"node:\" prefix.") + // } + } + + fn load_and_cache_npm_package_info(&self, package_name: &str) { + match self.npm_resolver { + Some(npm_resolver) if npm_resolver.as_managed().is_some() => { + let npm_resolver = npm_resolver.clone(); + let package_name = package_name.to_string(); + deno_core::unsync::spawn(async move { + if let Some(managed) = npm_resolver.as_managed() { + let _ignore = managed.cache_package_info(&package_name).await; + } + }); + } + _ => {} + } + } + + async fn resolve_pkg_reqs(&self, package_reqs: &[PackageReq]) -> NpmResolvePkgReqsResult { + match &self.npm_resolver { + Some(npm_resolver) => { + let npm_resolver = match npm_resolver.as_inner() { + InnerCliNpmResolverRef::Managed(npm_resolver) => npm_resolver, + // if we are using byonm, then this should never be called because + // we don't use deno_graph's npm resolution in this case + InnerCliNpmResolverRef::Byonm(_) => unreachable!(), + }; + + let top_level_result = if self.found_package_json_dep_flag.is_raised() { + npm_resolver + .ensure_top_level_package_json_install() + .await + .map(|_| ()) + } else { + Ok(()) + }; + + let result = npm_resolver.add_package_reqs_raw(package_reqs).await; + + NpmResolvePkgReqsResult { + results: result + .results + .into_iter() + .map(|r| { + r.map_err(|err| match err { + NpmResolutionError::Registry(e) => { + NpmLoadError::RegistryInfo(Arc::new(e.into())) + } + NpmResolutionError::Resolution(e) => { + NpmLoadError::PackageReqResolution(Arc::new(e.into())) + } + NpmResolutionError::DependencyEntry(e) => { + NpmLoadError::PackageReqResolution(Arc::new(e.into())) + } + }) + }) + .collect(), + dep_graph_result: match top_level_result { + Ok(()) => result.dependencies_result.map_err(Arc::new), + Err(err) => Err(Arc::new(err)), + }, + } + } + None => { + let err = Arc::new(anyhow!( + "npm specifiers were requested; but --no-npm is specified" + )); + NpmResolvePkgReqsResult { + results: package_reqs + .iter() + .map(|_| Err(NpmLoadError::RegistryInfo(err.clone()))) + .collect(), + dep_graph_result: Err(err), + } + } + } + } + + fn enables_bare_builtin_node_module(&self) -> bool { + self.bare_node_builtins_enabled + } +} diff --git a/crates/sb_module_loader/lib.rs b/crates/sb_module_loader/lib.rs index f676e27b6..15c5ffff1 100644 --- a/crates/sb_module_loader/lib.rs +++ b/crates/sb_module_loader/lib.rs @@ -2,17 +2,17 @@ use deno_core::{FastString, ModuleLoader}; use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use sb_fs::virtual_fs::FileBackedVfs; use sb_fs::EszipStaticFiles; -use sb_node::NpmResolver; +use sb_node::{NodeResolver, NpmResolver}; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; pub mod metadata; -pub mod node; pub mod standalone; pub mod util; pub struct RuntimeProviders { + pub node_resolver: Arc, pub npm_resolver: Arc, pub module_loader: Rc, pub vfs: Arc, diff --git a/crates/sb_module_loader/metadata.rs b/crates/sb_module_loader/metadata.rs index c303ca2ff..0c918b919 100644 --- a/crates/sb_module_loader/metadata.rs +++ b/crates/sb_module_loader/metadata.rs @@ -1,68 +1,8 @@ -use deno_semver::package::PackageReq; -use deno_semver::VersionReqSpecifierParseError; -use sb_npm::package_json::{PackageJsonDepValueParseError, PackageJsonDeps}; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; - -#[derive(Serialize, Deserialize)] -enum SerializablePackageJsonDepValueParseError { - Specifier(String), - Unsupported { scheme: String }, -} - -impl SerializablePackageJsonDepValueParseError { - pub fn from_err(err: PackageJsonDepValueParseError) -> Self { - match err { - PackageJsonDepValueParseError::Specifier(err) => { - Self::Specifier(err.source.to_string()) - } - PackageJsonDepValueParseError::Unsupported { scheme } => Self::Unsupported { scheme }, - } - } - - pub fn into_err(self) -> PackageJsonDepValueParseError { - match self { - SerializablePackageJsonDepValueParseError::Specifier(source) => { - PackageJsonDepValueParseError::Specifier(VersionReqSpecifierParseError { - source: monch::ParseErrorFailureError::new(source), - }) - } - SerializablePackageJsonDepValueParseError::Unsupported { scheme } => { - PackageJsonDepValueParseError::Unsupported { scheme } - } - } - } -} - -#[derive(Serialize, Deserialize)] -pub struct SerializablePackageJsonDeps( - BTreeMap>, -); - -impl SerializablePackageJsonDeps { - pub fn from_deps(deps: PackageJsonDeps) -> Self { - Self( - deps.into_iter() - .map(|(name, req)| { - let res = req.map_err(SerializablePackageJsonDepValueParseError::from_err); - (name, res) - }) - .collect(), - ) - } - - pub fn into_deps(self) -> PackageJsonDeps { - self.0 - .into_iter() - .map(|(name, res)| (name, res.map_err(|err| err.into_err()))) - .collect() - } -} #[derive(Deserialize, Serialize)] pub struct Metadata { pub ca_stores: Option>, pub ca_data: Option>, pub unsafely_ignore_certificate_errors: Option>, - pub package_json_deps: Option, } diff --git a/crates/sb_module_loader/node/cli_node_resolver.rs b/crates/sb_module_loader/node/cli_node_resolver.rs deleted file mode 100644 index e75321f18..000000000 --- a/crates/sb_module_loader/node/cli_node_resolver.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::node::node_module_loader::CjsResolutionStore; -use anyhow::Context; -use deno_core::error::{generic_error, AnyError}; -use deno_core::ModuleSpecifier; -use deno_semver::npm::NpmPackageReqReference; -use sb_node::{NodePermissions, NodeResolution, NodeResolutionMode, NodeResolver}; -use sb_npm::CliNpmResolver; -use std::path::Path; -use std::sync::Arc; - -pub struct CliNodeResolver { - cjs_resolutions: Arc, - node_resolver: Arc, - npm_resolver: Arc, -} - -impl CliNodeResolver { - pub fn new( - cjs_resolutions: Arc, - node_resolver: Arc, - npm_resolver: Arc, - ) -> Self { - Self { - cjs_resolutions, - node_resolver, - npm_resolver, - } - } - - pub fn in_npm_package(&self, referrer: &ModuleSpecifier) -> bool { - self.npm_resolver.in_npm_package(referrer) - } - - pub fn resolve_if_in_npm_package( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - permissions: &dyn NodePermissions, - ) -> Option> { - if self.in_npm_package(referrer) { - // we're in an npm package, so use node resolution - Some( - self.handle_node_resolve_result(self.node_resolver.resolve( - specifier, - referrer, - NodeResolutionMode::Execution, - permissions, - )) - .with_context(|| format!("Could not resolve '{specifier}' from '{referrer}'.")), - ) - } else { - None - } - } - - pub fn resolve_req_reference( - &self, - req_ref: &NpmPackageReqReference, - permissions: &dyn NodePermissions, - referrer: &ModuleSpecifier, - ) -> Result { - let package_folder = self - .npm_resolver - .resolve_pkg_folder_from_deno_module_req(req_ref.req(), referrer)?; - self.resolve_package_sub_path(&package_folder, req_ref.sub_path(), referrer, permissions) - .with_context(|| format!("Could not resolve '{}'.", req_ref)) - } - - pub fn resolve_package_sub_path( - &self, - package_folder: &Path, - sub_path: Option<&str>, - referrer: &ModuleSpecifier, - permissions: &dyn NodePermissions, - ) -> Result { - self.handle_node_resolve_result( - self.node_resolver.resolve_package_subpath_from_deno_module( - package_folder, - sub_path, - referrer, - NodeResolutionMode::Execution, - permissions, - ), - ) - } - - fn handle_node_resolve_result( - &self, - result: Result, AnyError>, - ) -> Result { - let response = match result? { - Some(response) => response, - None => return Err(generic_error("not found")), - }; - if let NodeResolution::CommonJs(specifier) = &response { - // remember that this was a common js resolution - self.cjs_resolutions.insert(specifier.clone()); - } - Ok(response.into_url()) - } -} diff --git a/crates/sb_module_loader/node/mod.rs b/crates/sb_module_loader/node/mod.rs deleted file mode 100644 index 21a5c525f..000000000 --- a/crates/sb_module_loader/node/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod cjs_code_anaylzer; -pub mod cli_node_resolver; -pub mod node_module_loader; diff --git a/crates/sb_module_loader/node/node_module_loader.rs b/crates/sb_module_loader/node/node_module_loader.rs deleted file mode 100644 index f7be055a8..000000000 --- a/crates/sb_module_loader/node/node_module_loader.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::node::cjs_code_anaylzer::CliNodeCodeTranslator; -use crate::node::cli_node_resolver::CliNodeResolver; -use anyhow::Context; -use deno_ast::MediaType; -use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; -use deno_core::{ModuleCodeString, ModuleSpecifier}; -use sb_node::NodePermissions; -use std::collections::HashSet; -use std::sync::Arc; - -pub struct NpmModuleLoader { - cjs_resolutions: Arc, - node_code_translator: Arc, - fs: Arc, - node_resolver: Arc, -} - -pub struct ModuleCodeSource { - pub code: ModuleCodeString, - pub found_url: ModuleSpecifier, - pub media_type: MediaType, -} - -impl NpmModuleLoader { - pub fn new( - cjs_resolutions: Arc, - node_code_translator: Arc, - fs: Arc, - node_resolver: Arc, - ) -> Self { - Self { - cjs_resolutions, - node_code_translator, - fs, - node_resolver, - } - } - - pub fn maybe_prepare_load(&self, specifier: &ModuleSpecifier) -> Option> { - if self.node_resolver.in_npm_package(specifier) { - // nothing to prepare - Some(Ok(())) - } else { - None - } - } - - pub fn load_sync_if_in_npm_package( - &self, - specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - permissions: &dyn NodePermissions, - ) -> Option> { - if self.node_resolver.in_npm_package(specifier) { - Some(self.load_sync(specifier, maybe_referrer, permissions)) - } else { - None - } - } - - fn load_sync( - &self, - specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - permissions: &dyn NodePermissions, - ) -> Result { - let file_path = specifier.to_file_path().unwrap(); - let code = self - .fs - .read_text_file_sync(&file_path, None) - .map_err(AnyError::from) - .with_context(|| { - if file_path.is_dir() { - // directory imports are not allowed when importing from an - // ES module, so provide the user with a helpful error message - let dir_path = file_path; - let mut msg = "Directory import ".to_string(); - msg.push_str(&dir_path.to_string_lossy()); - if let Some(referrer) = &maybe_referrer { - msg.push_str(" is not supported resolving import from "); - msg.push_str(referrer.as_str()); - let entrypoint_name = ["index.mjs", "index.js", "index.cjs"] - .iter() - .find(|e| dir_path.join(e).is_file()); - if let Some(entrypoint_name) = entrypoint_name { - msg.push_str("\nDid you mean to import "); - msg.push_str(entrypoint_name); - msg.push_str(" within the directory?"); - } - } - msg - } else { - let mut msg = "Unable to load ".to_string(); - msg.push_str(&file_path.to_string_lossy()); - if let Some(referrer) = &maybe_referrer { - msg.push_str(" imported from "); - msg.push_str(referrer.as_str()); - } - msg - } - })?; - - let code = if self.cjs_resolutions.contains(specifier) { - // translate cjs to esm if it's cjs and inject node globals - self.node_code_translator - .translate_cjs_to_esm(specifier, Some(code), permissions)? - } else { - // esm and json code is untouched - code - }; - Ok(ModuleCodeSource { - code: code.into(), - found_url: specifier.clone(), - media_type: MediaType::from_specifier(specifier), - }) - } -} - -#[derive(Default)] -pub struct CjsResolutionStore(Mutex>); -impl CjsResolutionStore { - pub fn contains(&self, specifier: &ModuleSpecifier) -> bool { - self.0.lock().contains(specifier) - } - - pub fn insert(&self, specifier: ModuleSpecifier) { - self.0.lock().insert(specifier); - } -} diff --git a/crates/sb_module_loader/standalone/mod.rs b/crates/sb_module_loader/standalone/mod.rs index 6a90619b4..4124be61d 100644 --- a/crates/sb_module_loader/standalone/mod.rs +++ b/crates/sb_module_loader/standalone/mod.rs @@ -1,13 +1,12 @@ use crate::metadata::Metadata; -use crate::node::cjs_code_anaylzer::CliCjsCodeAnalyzer; -use crate::node::cli_node_resolver::CliNodeResolver; -use crate::node::node_module_loader::{CjsResolutionStore, NpmModuleLoader}; use crate::standalone::standalone_module_loader::{EmbeddedModuleLoader, SharedModuleLoaderState}; use crate::RuntimeProviders; use anyhow::{bail, Context}; +use deno_config::workspace::{PackageJsonDepResolution, WorkspaceResolver}; use deno_core::error::AnyError; use deno_core::url::Url; use deno_core::{FastString, ModuleSpecifier}; +use deno_npm::npm_rc::ResolvedNpmRc; use deno_tls::rustls::RootCertStore; use deno_tls::RootCertStoreProvider; use futures_util::future::OptionFuture; @@ -17,20 +16,23 @@ use sb_core::cache::deno_dir::DenoDirProvider; use sb_core::cache::node::NodeAnalysisCache; use sb_core::cache::CacheSetting; use sb_core::cert::{get_root_cert_store, CaData}; -use sb_core::util::http_util::HttpClient; -use sb_eszip_shared::{AsyncEszipDataRead, SOURCE_CODE_ESZIP_KEY, VFS_ESZIP_KEY}; +use sb_core::node::CliCjsCodeAnalyzer; +use sb_core::util::http_util::HttpClientProvider; use sb_fs::file_system::DenoCompileFileSystem; use sb_fs::{extract_static_files_from_eszip, load_npm_vfs}; -use sb_graph::graph_resolver::MappedSpecifierResolver; +use sb_graph::resolver::{CjsResolutionStore, CliNodeResolver, NpmModuleLoader}; use sb_graph::{eszip_migrate, payload_to_eszip, EszipPayloadKind, LazyLoadableEszip}; +use sb_eszip_shared::{AsyncEszipDataRead, SOURCE_CODE_ESZIP_KEY, VFS_ESZIP_KEY}; use sb_node::analyze::NodeCodeTranslator; use sb_node::NodeResolver; use sb_npm::cache_dir::NpmCacheDir; -use sb_npm::package_json::PackageJsonDepsProvider; +use sb_npm::package_json::PackageJsonInstallDepsProvider; use sb_npm::{ create_managed_npm_resolver, CliNpmResolverManagedCreateOptions, - CliNpmResolverManagedPackageJsonInstallerOption, CliNpmResolverManagedSnapshotOption, + CliNpmResolverManagedSnapshotOption, }; +use standalone_module_loader::WorkspaceEszip; + use std::path::Path; use std::rc::Rc; use std::sync::Arc; @@ -75,18 +77,22 @@ where cell: Default::default(), }); - let http_client = Arc::new(HttpClient::new( + let http_client_provider = Arc::new(HttpClientProvider::new( Some(root_cert_store_provider.clone()), metadata.unsafely_ignore_certificate_errors.clone(), )); // use a dummy npm registry url let npm_registry_url = ModuleSpecifier::parse("https://localhost/").unwrap(); - let root_path = std::env::temp_dir() - .join(format!("sb-compile-{}", current_exe_name)) - .join("node_modules"); + let root_path = std::env::temp_dir().join(format!("sb-compile-{}", current_exe_name)); + + let root_dir_url = ModuleSpecifier::from_directory_path(&root_path).unwrap(); + let root_node_modules_path = root_path.join("node_modules"); - let npm_cache_dir = NpmCacheDir::new(root_path.clone()); + let npm_cache_dir = NpmCacheDir::new( + root_node_modules_path.clone(), + vec![npm_registry_url.clone()], + ); let npm_global_cache_dir = npm_cache_dir.get_cache_location(); let entry_module_source = OptionFuture::<_>::from( @@ -101,7 +107,7 @@ where let snapshot = eszip.take_npm_snapshot(); let static_files = extract_static_files_from_eszip(&eszip, base_dir_path).await; - let vfs_root_dir_path = npm_cache_dir.registry_folder(&npm_registry_url); + let vfs_root_dir_path = npm_cache_dir.root_dir().to_owned(); let (fs, vfs) = { let vfs_data = OptionFuture::<_>::from( @@ -125,25 +131,25 @@ where (Arc::new(fs) as Arc, fs_backed_vfs) }; - let package_json_deps_provider = Arc::new(PackageJsonDepsProvider::new( - metadata - .package_json_deps - .map(|serialized| serialized.into_deps()), - )); - + let package_json_deps_provider = Arc::new(PackageJsonInstallDepsProvider::empty()); let npm_resolver = create_managed_npm_resolver(CliNpmResolverManagedCreateOptions { snapshot: CliNpmResolverManagedSnapshotOption::Specified(snapshot.clone()), maybe_lockfile: None, fs: fs.clone(), - http_client, + http_client_provider, npm_global_cache_dir, cache_setting: CacheSetting::Use, maybe_node_modules_path: None, npm_system_info: Default::default(), - package_json_installer: CliNpmResolverManagedPackageJsonInstallerOption::ConditionalInstall( - package_json_deps_provider.clone(), - ), - npm_registry_url, + package_json_deps_provider, + npmrc: Arc::new(ResolvedNpmRc { + default_config: deno_npm::npm_rc::RegistryConfigWithUrl { + registry_url: npm_registry_url.clone(), + config: Default::default(), + }, + scopes: Default::default(), + registry_configs: Default::default(), + }), }) .await?; @@ -163,22 +169,23 @@ where npm_resolver.clone().into_npm_resolver(), )); - let maybe_import_map = maybe_import_map - .map(|import_map| Some(Arc::new(import_map))) - .unwrap_or_else(|| None); - let cli_node_resolver = Arc::new(CliNodeResolver::new( cjs_resolutions.clone(), + fs.clone(), node_resolver.clone(), npm_resolver.clone(), )); let module_loader_factory = StandaloneModuleLoaderFactory { shared: Arc::new(SharedModuleLoaderState { - eszip, - mapped_specifier_resolver: MappedSpecifierResolver::new( + eszip: WorkspaceEszip { + eszip, + root_dir_url, + }, + workspace_resolver: WorkspaceResolver::new_raw( maybe_import_map, - package_json_deps_provider.clone(), + vec![], + PackageJsonDepResolution::Disabled, ), node_resolver: cli_node_resolver.clone(), npm_module_loader: Arc::new(NpmModuleLoader::new( @@ -191,11 +198,12 @@ where }; Ok(RuntimeProviders { + node_resolver, + npm_resolver: npm_resolver.into_npm_resolver(), module_loader: Rc::new(EmbeddedModuleLoader { shared: module_loader_factory.shared.clone(), include_source_map, }), - npm_resolver: npm_resolver.into_npm_resolver(), vfs, module_code: entry_module_source, static_files, @@ -238,7 +246,9 @@ where maybe_import_map = Some(result.import_map); } } - } + + None + }; create_module_loader_for_eszip( eszip, @@ -247,7 +257,6 @@ where ca_stores: None, ca_data: None, unsafely_ignore_certificate_errors: None, - package_json_deps: None, }, maybe_import_map, include_source_map, diff --git a/crates/sb_module_loader/standalone/standalone_module_loader.rs b/crates/sb_module_loader/standalone/standalone_module_loader.rs index 006aa32cc..e363f7b22 100644 --- a/crates/sb_module_loader/standalone/standalone_module_loader.rs +++ b/crates/sb_module_loader/standalone/standalone_module_loader.rs @@ -1,8 +1,10 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use crate::node::node_module_loader::NpmModuleLoader; use base64::Engine; use deno_ast::MediaType; +use deno_config::package_json::PackageJsonDepValue; +use deno_config::workspace::MappedResolution; +use deno_config::workspace::WorkspaceResolver; use deno_core::error::generic_error; use deno_core::error::type_error; use deno_core::error::AnyError; @@ -13,18 +15,52 @@ use deno_core::{ModuleLoader, ModuleSourceCode}; use deno_core::{ModuleSpecifier, RequestedModuleType}; use deno_semver::npm::NpmPackageReqReference; use eszip::deno_graph; +use eszip::EszipRelativeFileBaseUrl; +use sb_graph::resolver::CliNodeResolver; +use sb_graph::resolver::NpmModuleLoader; +use sb_node::NodeResolutionMode; use sb_eszip_shared::AsyncEszipDataRead; use sb_graph::LazyLoadableEszip; use std::sync::Arc; use tracing::instrument; -use crate::node::cli_node_resolver::CliNodeResolver; use crate::util::arc_u8_to_arc_str; -use sb_graph::graph_resolver::MappedSpecifierResolver; + +pub struct WorkspaceEszipModule { + specifier: ModuleSpecifier, + inner: eszip::Module, +} + +pub struct WorkspaceEszip { + pub eszip: eszip::EszipV2, + pub root_dir_url: ModuleSpecifier, +} + +impl WorkspaceEszip { + pub fn get_module(&self, specifier: &ModuleSpecifier) -> Option { + if specifier.scheme() == "file" { + let specifier_key = + EszipRelativeFileBaseUrl::new(&self.root_dir_url).specifier_key(specifier); + let module = self.eszip.get_module(&specifier_key)?; + let specifier = self.root_dir_url.join(&module.specifier).unwrap(); + Some(WorkspaceEszipModule { + specifier, + inner: module, + }) + } else { + let module = self.eszip.get_module(specifier.as_str())?; + Some(WorkspaceEszipModule { + specifier: ModuleSpecifier::parse(&module.specifier).unwrap(), + inner: module, + }) + } + } +} pub struct SharedModuleLoaderState { + pub(crate) eszip: WorkspaceEszip, pub(crate) eszip: LazyLoadableEszip, - pub(crate) mapped_specifier_resolver: MappedSpecifierResolver, + pub(crate) workspace_resolver: WorkspaceResolver, pub(crate) npm_module_loader: Arc, pub(crate) node_resolver: Arc, } @@ -57,47 +93,88 @@ impl ModuleLoader for EmbeddedModuleLoader { .map_err(|err| type_error(format!("Referrer uses invalid specifier: {}", err)))? }; - let permissions = sb_node::allow_all(); - if let Some(result) = - self.shared - .node_resolver - .resolve_if_in_npm_package(specifier, &referrer, &*permissions) - { - return result; + if let Some(result) = self.shared.node_resolver.resolve_if_in_npm_package( + specifier, + &referrer, + NodeResolutionMode::Execution, + ) { + return match result? { + Some(res) => Ok(res.into_url()), + None => Err(generic_error("not found")), + }; } - let maybe_mapped = self - .shared - .mapped_specifier_resolver - .resolve(specifier, &referrer)? - .into_specifier(); - - // npm specifier - let specifier_text = maybe_mapped - .as_ref() - .map(|r| r.as_str()) - .unwrap_or(specifier); - - if let Ok(reference) = NpmPackageReqReference::from_str(specifier_text) { - return self.shared.node_resolver.resolve_req_reference( - &reference, - &*permissions, - &referrer, - ); - } + let mapped_resolution = self.shared.workspace_resolver.resolve(specifier, &referrer); - let specifier = match maybe_mapped { - Some(resolved) => resolved, - None => deno_core::resolve_import(specifier, referrer.as_str())?, - }; + match mapped_resolution { + Ok(MappedResolution::PackageJson { + dep_result, + sub_path, + alias, + .. + }) => match dep_result.as_ref().map_err(|e| AnyError::from(e.clone()))? { + PackageJsonDepValue::Req(req) => self + .shared + .node_resolver + .resolve_req_with_sub_path( + req, + sub_path.as_deref(), + &referrer, + NodeResolutionMode::Execution, + ) + .map(|res| res.into_url()), + PackageJsonDepValue::Workspace(version_req) => { + let pkg_folder = self + .shared + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_pkg_json_dep(alias, version_req)?; + Ok(self + .shared + .node_resolver + .resolve_package_sub_path_from_deno_module( + pkg_folder, + sub_path.as_deref(), + Some(&referrer), + NodeResolutionMode::Execution, + )? + .into_url()) + } + }, + Ok(MappedResolution::Normal(specifier)) + | Ok(MappedResolution::ImportMap(specifier)) => { + if let Ok(reference) = NpmPackageReqReference::from_specifier(&specifier) { + return self + .shared + .node_resolver + .resolve_req_reference(&reference, &referrer, NodeResolutionMode::Execution) + .map(|res| res.into_url()); + } - if specifier.scheme() == "jsr" { - if let Some(module) = self.shared.eszip.ensure_module(specifier.as_str()) { - return Ok(ModuleSpecifier::parse(&module.specifier).unwrap()); + if specifier.scheme() == "jsr" { + if let Some(module) = self.shared.eszip.get_module(&specifier) { + return Ok(module.specifier); + } + } + + self.shared + .node_resolver + .handle_if_in_node_modules(specifier) + } + Err(err) if err.is_unmapped_bare_specifier() && referrer.scheme() == "file" => { + // todo(dsherret): return a better error from node resolution so that + // we can more easily tell whether to surface it or not + let node_result = self.shared.node_resolver.resolve( + specifier, + &referrer, + NodeResolutionMode::Execution, + ); + if let Ok(Some(res)) = node_result { + return Ok(res.into_url()); + } + Err(err.into()) } + Err(err) => Err(err.into()), } - - Ok(specifier) } #[instrument(level = "debug", skip_all, fields(specifier = original_specifier.as_str()))] @@ -109,11 +186,9 @@ impl ModuleLoader for EmbeddedModuleLoader { _requested_module_type: RequestedModuleType, ) -> deno_core::ModuleLoadResponse { let include_source_map = self.include_source_map; - let permissions = sb_node::allow_all(); - if original_specifier.scheme() == "data" { let data_url_text = match deno_graph::source::RawDataUrl::parse(original_specifier) - .and_then(|url| url.decode().map_err(|err| err.into())) + .and_then(|url| url.decode()) { Ok(response) => response, Err(err) => { @@ -131,26 +206,28 @@ impl ModuleLoader for EmbeddedModuleLoader { ))); } - if let Some(result) = self.shared.npm_module_loader.load_sync_if_in_npm_package( - original_specifier, - maybe_referrer, - &*permissions, - ) { - return match result { - Ok(code_source) => deno_core::ModuleLoadResponse::Sync(Ok( - deno_core::ModuleSource::new_with_redirect( + if self.shared.node_resolver.in_npm_package(original_specifier) { + let npm_module_loader = self.shared.npm_module_loader.clone(); + let original_specifier = original_specifier.clone(); + let maybe_referrer = maybe_referrer.cloned(); + return deno_core::ModuleLoadResponse::Async( + async move { + let code_source = npm_module_loader + .load(&original_specifier, maybe_referrer.as_ref()) + .await?; + Ok(deno_core::ModuleSource::new_with_redirect( match code_source.media_type { MediaType::Json => ModuleType::Json, _ => ModuleType::JavaScript, }, - ModuleSourceCode::String(code_source.code), - original_specifier, + code_source.code, + &original_specifier, &code_source.found_url, None, - ), - )), - Err(err) => deno_core::ModuleLoadResponse::Sync(Err(err)), - }; + )) + } + .boxed_local(), + ); } let Some(module) = self.shared.eszip.ensure_module(original_specifier.as_str()) else { @@ -161,20 +238,15 @@ impl ModuleLoader for EmbeddedModuleLoader { }; let original_specifier = original_specifier.clone(); - let found_specifier = - ModuleSpecifier::parse(&module.specifier).expect("invalid url in eszip"); deno_core::ModuleLoadResponse::Async( async move { - let code = module - .source() - .await - .ok_or_else(|| type_error(format!("Module not found: {}", original_specifier))) - .and_then(|it| { - arc_u8_to_arc_str(it).map_err(|_| type_error("Module source is not utf-8")) - })?; - - let source_map = module.source_map().await; + let code = module.inner.source().await.ok_or_else(|| { + type_error(format!("Module not found: {}", original_specifier)) + })?; + let code = arc_u8_to_arc_str(code) + .map_err(|_| type_error("Module source is not utf-8"))?; + let source_map = module.inner.source_map().await; let maybe_code_with_source_map = 'scope: { if !include_source_map { break 'scope code; @@ -195,9 +267,8 @@ impl ModuleLoader for EmbeddedModuleLoader { Arc::from(src) }; - Ok(deno_core::ModuleSource::new_with_redirect( - match module.kind { + match module.inner.kind { eszip::ModuleKind::JavaScript => ModuleType::JavaScript, eszip::ModuleKind::Json => ModuleType::Json, eszip::ModuleKind::Jsonc => { @@ -209,7 +280,7 @@ impl ModuleLoader for EmbeddedModuleLoader { }, ModuleSourceCode::String(maybe_code_with_source_map.into()), &original_specifier, - &found_specifier, + &module.specifier, None, )) } diff --git a/crates/sb_workers/context.rs b/crates/sb_workers/context.rs index 62920ead0..4252494b9 100644 --- a/crates/sb_workers/context.rs +++ b/crates/sb_workers/context.rs @@ -3,7 +3,7 @@ use deno_config::JsxImportSourceConfig; use deno_core::FastString; use enum_as_inner::EnumAsInner; use event_worker::events::{UncaughtExceptionEvent, WorkerEventWithMetadata}; -use hyper::{Body, Request, Response}; +use hyper_v014::{Body, Request, Response}; use sb_core::util::sync::AtomicFlag; use sb_core::{MetricSource, SharedMetricSource}; use std::path::PathBuf; @@ -228,6 +228,6 @@ pub struct CreateUserWorkerResult { #[derive(Debug)] pub struct WorkerRequestMsg { pub req: Request, - pub res_tx: oneshot::Sender, hyper::Error>>, + pub res_tx: oneshot::Sender, hyper_v014::Error>>, pub conn_token: Option, } diff --git a/crates/sb_workers/lib.rs b/crates/sb_workers/lib.rs index 431b727e6..0e619ce97 100644 --- a/crates/sb_workers/lib.rs +++ b/crates/sb_workers/lib.rs @@ -19,10 +19,10 @@ use deno_core::{ use deno_http::{HttpRequestReader, HttpStreamReadResource}; use errors::WorkerError; use http_utils::utils::get_upgrade_type; -use hyper::body::HttpBody; -use hyper::header::{HeaderName, HeaderValue, CONTENT_LENGTH}; -use hyper::upgrade::OnUpgrade; -use hyper::{Body, Method, Request}; +use hyper_v014::body::HttpBody; +use hyper_v014::header::{HeaderName, HeaderValue, CONTENT_LENGTH}; +use hyper_v014::upgrade::OnUpgrade; +use hyper_v014::{Body, Method, Request}; use log::error; use sb_core::conn_sync::ConnWatcher; use sb_graph::{DecoratorType, EszipPayloadKind}; @@ -235,7 +235,7 @@ impl Resource for UserWorkerRequestBodyResource { fn write(self: Rc, buf: BufView) -> AsyncResult { Box::pin(async move { - let bytes: bytes::Bytes = buf.into(); + let bytes: bytes::Bytes = buf.to_vec().into(); let nwritten = bytes.len(); let body = RcRef::map(&self, |r| &r.body).borrow_mut().await; let body = (*body).as_ref(); From 29bd1fc8676b1be5b2d85421cda7f68bbbd80e4c Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Mon, 22 Jul 2024 04:44:58 +0000 Subject: [PATCH 07/20] stamp: clippy --- crates/npm/managed/mod.rs | 10 +++++----- crates/npm_cache/file_fetcher.rs | 7 +++---- crates/sb_core/cache/emit.rs | 2 +- crates/sb_graph/graph_util.rs | 4 ++-- crates/sb_module_loader/util.rs | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/npm/managed/mod.rs b/crates/npm/managed/mod.rs index a2eac523c..a1d8fa795 100644 --- a/crates/npm/managed/mod.rs +++ b/crates/npm/managed/mod.rs @@ -167,12 +167,12 @@ async fn resolve_snapshot( ) -> Result, AnyError> { match snapshot { CliNpmResolverManagedSnapshotOption::ResolveFromLockfile(lockfile) => { - let lockfile_guard = lockfile.lock(); - let overwrite = lockfile_guard.overwrite; - if !overwrite { - let filename = lockfile_guard.filename.clone(); + let (overwrite, filename) = { + let guard = lockfile.lock(); + (guard.overwrite, guard.filename.clone()) + }; - drop(lockfile_guard); + if !overwrite { let snapshot = snapshot_from_lockfile(lockfile.clone(), api) .await .with_context(|| format!("failed reading lockfile '{}'", filename.display()))?; diff --git a/crates/npm_cache/file_fetcher.rs b/crates/npm_cache/file_fetcher.rs index 391d27567..318f7817d 100644 --- a/crates/npm_cache/file_fetcher.rs +++ b/crates/npm_cache/file_fetcher.rs @@ -354,7 +354,8 @@ impl FileFetcher { let mut maybe_etag = maybe_etag; let mut retried = false; // retry intermittent failures - let result = loop { + + loop { let result = match self .http_client_provider .get_or_create()? @@ -412,9 +413,7 @@ impl FileFetcher { } }; break result; - }; - - result + } } /// Returns if the cache should be used for a given specifier. diff --git a/crates/sb_core/cache/emit.rs b/crates/sb_core/cache/emit.rs index 37520db1f..80e43d78c 100644 --- a/crates/sb_core/cache/emit.rs +++ b/crates/sb_core/cache/emit.rs @@ -103,7 +103,7 @@ impl EmitCache { // save the metadata let metadata = EmitMetadata { - source_hash: source_hash, + source_hash, emit_hash: compute_emit_hash(code.as_bytes(), self.cli_version), }; diff --git a/crates/sb_graph/graph_util.rs b/crates/sb_graph/graph_util.rs index d2c8779fa..ede01fc6a 100644 --- a/crates/sb_graph/graph_util.rs +++ b/crates/sb_graph/graph_util.rs @@ -180,7 +180,7 @@ impl ModuleGraphBuilder { let fs = Arc::new(deno_fs::RealFs); let fs = DenoGraphFsAdapter(fs.as_ref()); let lockfile = self.lockfile(); - let mut locker = lockfile.as_ref().map(|lockfile| LockfileLocker(&lockfile)); + let mut locker = lockfile.as_ref().map(LockfileLocker); self.build_graph_with_npm_resolution( &mut graph, @@ -321,7 +321,7 @@ impl ModuleGraphBuilder { let fs = Arc::new(deno_fs::RealFs); let fs = DenoGraphFsAdapter(fs.as_ref()); let lockfile = self.lockfile(); - let mut locker = lockfile.as_ref().map(|lockfile| LockfileLocker(&lockfile)); + let mut locker = lockfile.as_ref().map(LockfileLocker); self.build_graph_with_npm_resolution( &mut graph, diff --git a/crates/sb_module_loader/util.rs b/crates/sb_module_loader/util.rs index 172719bfb..95414c64f 100644 --- a/crates/sb_module_loader/util.rs +++ b/crates/sb_module_loader/util.rs @@ -6,5 +6,5 @@ pub fn arc_u8_to_arc_str(arc_u8: Arc<[u8]>) -> Result, std::str::Utf8Er // SAFETY: the string is valid UTF-8, and the layout Arc<[u8]> is the same as // Arc. This is proven by the From> impl for Arc<[u8]> from the // standard library. - Ok(unsafe { std::mem::transmute(arc_u8) }) + Ok(unsafe { std::mem::transmute::, std::sync::Arc>(arc_u8) }) } From f1cab2c7ecacfede8158c8f86053e49b9e0f04c6 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Mon, 22 Jul 2024 05:01:40 +0000 Subject: [PATCH 08/20] stamp: fmt --- crates/base/src/rt_worker/worker_ctx.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/base/src/rt_worker/worker_ctx.rs b/crates/base/src/rt_worker/worker_ctx.rs index 7aa0934c6..68286074c 100644 --- a/crates/base/src/rt_worker/worker_ctx.rs +++ b/crates/base/src/rt_worker/worker_ctx.rs @@ -14,9 +14,9 @@ use event_worker::events::{ BootEvent, ShutdownEvent, WorkerEventWithMetadata, WorkerEvents, WorkerMemoryUsed, }; use futures_util::pin_mut; -use http_v02::StatusCode; use http_utils::io::Upgraded2; use http_utils::utils::{emit_status_code, get_upgrade_type}; +use http_v02::StatusCode; use hyper_v014::client::conn::http1; use hyper_v014::upgrade::OnUpgrade; use hyper_v014::{Body, Request, Response}; From 0e1ffdf0e995652bffca52d965c8fb4adfa03c34 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 2 Jul 2024 03:46:57 +0000 Subject: [PATCH 09/20] fix(node): bring back `worker_threads` module but make it a mock --- crates/node/lib.rs | 2 +- crates/node/polyfills/01_require.js | 4 +- crates/node/polyfills/02_init.js | 15 +- crates/node/polyfills/worker_threads.ts | 524 ++++++++++++------------ crates/sb_core/js/bootstrap.js | 1 + 5 files changed, 264 insertions(+), 282 deletions(-) diff --git a/crates/node/lib.rs b/crates/node/lib.rs index 0254964fc..becc3e815 100644 --- a/crates/node/lib.rs +++ b/crates/node/lib.rs @@ -605,7 +605,7 @@ deno_core::extension!(deno_node, "node:util/types" = "util/types.ts", "node:v8" = "v8.ts", "node:vm" = "vm.ts", - /*"node:worker_threads" = "worker_threads.ts",*/ + "node:worker_threads" = "worker_threads.ts", "node:zlib" = "zlib.ts", ], options = { diff --git a/crates/node/polyfills/01_require.js b/crates/node/polyfills/01_require.js index a0eddc773..3821972d4 100644 --- a/crates/node/polyfills/01_require.js +++ b/crates/node/polyfills/01_require.js @@ -150,7 +150,7 @@ import utilTypes from "node:util/types"; import util from "node:util"; import v8 from "node:v8"; import vm from "node:vm"; -// import workerThreads from "node:worker_threads"; +import workerThreads from "node:worker_threads"; import wasi from "ext:deno_node/wasi.ts"; import zlib from "node:zlib"; @@ -255,7 +255,7 @@ function setupBuiltinModules() { v8, vm, wasi, - // worker_threads: workerThreads, Disabled + worker_threads: workerThreads, zlib, }; for (const [name, moduleExports] of ObjectEntries(nodeModules)) { diff --git a/crates/node/polyfills/02_init.js b/crates/node/polyfills/02_init.js index 3e8e83996..5329c6770 100644 --- a/crates/node/polyfills/02_init.js +++ b/crates/node/polyfills/02_init.js @@ -37,16 +37,11 @@ function initialize(args) { Deno.version, nodeDebug, ); - - // @andreespirela, no we shouldn't be using these worker threads - /* - internals.__initWorkerThreads( - runningOnMainThread, - workerId, - maybeWorkerMetadata, - ); - */ - + internals.__initWorkerThreads( + runningOnMainThread, + workerId, + maybeWorkerMetadata, + ); internals.__setupChildProcessIpcChannel(); // `Deno[Deno.internal].requireImpl` will be unreachable after this line. delete internals.requireImpl; diff --git a/crates/node/polyfills/worker_threads.ts b/crates/node/polyfills/worker_threads.ts index c3ab4bb55..81dfa8e36 100644 --- a/crates/node/polyfills/worker_threads.ts +++ b/crates/node/polyfills/worker_threads.ts @@ -2,34 +2,11 @@ // Copyright Joyent and Node contributors. All rights reserved. MIT license. import { core, internals, primordials } from "ext:core/mod.js"; -import { - op_create_worker, - op_host_post_message, - op_host_recv_ctrl, - op_host_recv_message, - op_host_terminate_worker, - op_message_port_recv_message_sync, - op_worker_threads_filename, -} from "ext:core/ops"; -import { - deserializeJsMessageData, - MessageChannel, - MessagePort, - MessagePortIdSymbol, - MessagePortPrototype, - MessagePortReceiveMessageOnPortSymbol, - nodeWorkerThreadCloseCb, - refMessagePort, - serializeJsMessageData, - unrefPollForMessages, -} from "ext:deno_web/13_message_port.js"; -import * as webidl from "ext:deno_webidl/00_webidl.js"; +import { unrefPollForMessages } from "ext:deno_web/13_message_port.js"; import { notImplemented } from "ext:deno_node/_utils.ts"; import { EventEmitter } from "node:events"; -import { BroadcastChannel } from "ext:deno_broadcast_channel/01_broadcast_channel.js"; import process from "node:process"; -const { JSONParse, JSONStringify, ObjectPrototypeIsPrototypeOf } = primordials; const { Error, ObjectHasOwn, @@ -37,20 +14,17 @@ const { SafeSet, Symbol, SymbolFor, - SymbolIterator, - StringPrototypeTrim, SafeWeakMap, SafeMap, - TypeError, } = primordials; -const debugWorkerThreads = false; -function debugWT(...args) { - if (debugWorkerThreads) { - // deno-lint-ignore prefer-primordials - console.log(...args); - } -} +// const debugWorkerThreads = false; +// function debugWT(...args) { +// if (debugWorkerThreads) { +// // deno-lint-ignore prefer-primordials +// console.log(...args); +// } +// } export interface WorkerOptions { // only for typings @@ -100,67 +74,68 @@ class NodeWorker extends EventEmitter { stackSizeMb: 4, }; - constructor(specifier: URL | string, options?: WorkerOptions) { + constructor(_specifier: URL | string, _options?: WorkerOptions) { super(); - - if ( - typeof specifier === "object" && - !(specifier.protocol === "data:" || specifier.protocol === "file:") - ) { - throw new TypeError( - "node:worker_threads support only 'file:' and 'data:' URLs", - ); - } - if (options?.eval) { - specifier = `data:text/javascript,${specifier}`; - } else if ( - !(typeof specifier === "object" && specifier.protocol === "data:") - ) { - // deno-lint-ignore prefer-primordials - specifier = specifier.toString(); - specifier = op_worker_threads_filename(specifier); - } - - // TODO(bartlomieu): this doesn't match the Node.js behavior, it should be - // `[worker {threadId}] {name}` or empty string. - let name = StringPrototypeTrim(options?.name ?? ""); - if (options?.eval) { - name = "[worker eval]"; - } - this.#name = name; - - // One of the most common usages will be to pass `process.env` here, - // but because `process.env` is a Proxy in Deno, we need to get a plain - // object out of it - otherwise we'll run in `DataCloneError`s. - // See https://github.com/denoland/deno/issues/23522. - let env_ = undefined; - if (options?.env) { - env_ = JSONParse(JSONStringify(options?.env)); - } - const serializedWorkerMetadata = serializeJsMessageData({ - workerData: options?.workerData, - environmentData: environmentData, - env: env_, - }, options?.transferList ?? []); - const id = op_create_worker( - { - // deno-lint-ignore prefer-primordials - specifier: specifier.toString(), - hasSourceCode: false, - sourceCode: "", - permissions: null, - name: this.#name, - workerType: "module", - closeOnIdle: true, - }, - serializedWorkerMetadata, - ); - this.#id = id; - this.threadId = id; - this.#pollControl(); - this.#pollMessages(); - // https://nodejs.org/api/worker_threads.html#event-online - this.emit("online"); + notImplemented("Worker.prototype.constructor"); + + // if ( + // typeof specifier === "object" && + // !(specifier.protocol === "data:" || specifier.protocol === "file:") + // ) { + // throw new TypeError( + // "node:worker_threads support only 'file:' and 'data:' URLs", + // ); + // } + // if (options?.eval) { + // specifier = `data:text/javascript,${specifier}`; + // } else if ( + // !(typeof specifier === "object" && specifier.protocol === "data:") + // ) { + // // deno-lint-ignore prefer-primordials + // specifier = specifier.toString(); + // specifier = op_worker_threads_filename(specifier); + // } + + // // TODO(bartlomieu): this doesn't match the Node.js behavior, it should be + // // `[worker {threadId}] {name}` or empty string. + // let name = StringPrototypeTrim(options?.name ?? ""); + // if (options?.eval) { + // name = "[worker eval]"; + // } + // this.#name = name; + + // // One of the most common usages will be to pass `process.env` here, + // // but because `process.env` is a Proxy in Deno, we need to get a plain + // // object out of it - otherwise we'll run in `DataCloneError`s. + // // See https://github.com/denoland/deno/issues/23522. + // let env_ = undefined; + // if (options?.env) { + // env_ = JSONParse(JSONStringify(options?.env)); + // } + // const serializedWorkerMetadata = serializeJsMessageData({ + // workerData: options?.workerData, + // environmentData: environmentData, + // env: env_, + // }, options?.transferList ?? []); + // const id = op_create_worker( + // { + // // deno-lint-ignore prefer-primordials + // specifier: specifier.toString(), + // hasSourceCode: false, + // sourceCode: "", + // permissions: null, + // name: this.#name, + // workerType: "module", + // closeOnIdle: true, + // }, + // serializedWorkerMetadata, + // ); + // this.#id = id; + // this.threadId = id; + // this.#pollControl(); + // this.#pollMessages(); + // // https://nodejs.org/api/worker_threads.html#event-online + // this.emit("online"); } [privateWorkerRef](ref) { @@ -187,104 +162,105 @@ class NodeWorker extends EventEmitter { } } - #handleError(err) { - this.emit("error", err); + #handleError(_err) { + // this.emit("error", err); } #pollControl = async () => { - while (this.#status === "RUNNING") { - this.#controlPromise = op_host_recv_ctrl(this.#id); - if (this.#refCount < 1) { - core.unrefOpPromise(this.#controlPromise); - } - const { 0: type, 1: data } = await this.#controlPromise; - - // If terminate was called then we ignore all messages - if (this.#status === "TERMINATED") { - return; - } - - switch (type) { - case 1: { // TerminalError - this.#status = "CLOSED"; - } /* falls through */ - case 2: { // Error - this.#handleError(data); - break; - } - case 3: { // Close - debugWT(`Host got "close" message from worker: ${this.#name}`); - this.#status = "CLOSED"; - return; - } - default: { - throw new Error(`Unknown worker event: "${type}"`); - } - } - } + // while (this.#status === "RUNNING") { + // this.#controlPromise = op_host_recv_ctrl(this.#id); + // if (this.#refCount < 1) { + // core.unrefOpPromise(this.#controlPromise); + // } + // const { 0: type, 1: data } = await this.#controlPromise; + + // // If terminate was called then we ignore all messages + // if (this.#status === "TERMINATED") { + // return; + // } + + // switch (type) { + // case 1: { // TerminalError + // this.#status = "CLOSED"; + // } /* falls through */ + // case 2: { // Error + // this.#handleError(data); + // break; + // } + // case 3: { // Close + // debugWT(`Host got "close" message from worker: ${this.#name}`); + // this.#status = "CLOSED"; + // return; + // } + // default: { + // throw new Error(`Unknown worker event: "${type}"`); + // } + // } + // } }; #pollMessages = async () => { - while (this.#status !== "TERMINATED") { - this.#messagePromise = op_host_recv_message(this.#id); - if (this.#refCount < 1) { - core.unrefOpPromise(this.#messagePromise); - } - const data = await this.#messagePromise; - if (this.#status === "TERMINATED" || data === null) { - return; - } - let message, _transferables; - try { - const v = deserializeJsMessageData(data); - message = v[0]; - _transferables = v[1]; - } catch (err) { - this.emit("messageerror", err); - return; - } - this.emit("message", message); - } + // while (this.#status !== "TERMINATED") { + // this.#messagePromise = op_host_recv_message(this.#id); + // if (this.#refCount < 1) { + // core.unrefOpPromise(this.#messagePromise); + // } + // const data = await this.#messagePromise; + // if (this.#status === "TERMINATED" || data === null) { + // return; + // } + // let message, _transferables; + // try { + // const v = deserializeJsMessageData(data); + // message = v[0]; + // _transferables = v[1]; + // } catch (err) { + // this.emit("messageerror", err); + // return; + // } + // this.emit("message", message); + // } }; postMessage(message, transferOrOptions = {}) { - const prefix = "Failed to execute 'postMessage' on 'MessagePort'"; - webidl.requiredArguments(arguments.length, 1, prefix); - message = webidl.converters.any(message); - let options; - if ( - webidl.type(transferOrOptions) === "Object" && - transferOrOptions !== undefined && - transferOrOptions[SymbolIterator] !== undefined - ) { - const transfer = webidl.converters["sequence"]( - transferOrOptions, - prefix, - "Argument 2", - ); - options = { transfer }; - } else { - options = webidl.converters.StructuredSerializeOptions( - transferOrOptions, - prefix, - "Argument 2", - ); - } - const { transfer } = options; - const data = serializeJsMessageData(message, transfer); - if (this.#status === "RUNNING") { - op_host_post_message(this.#id, data); - } + notImplemented("Worker.prototype.postMessage"); + // const prefix = "Failed to execute 'postMessage' on 'MessagePort'"; + // webidl.requiredArguments(arguments.length, 1, prefix); + // message = webidl.converters.any(message); + // let options; + // if ( + // webidl.type(transferOrOptions) === "Object" && + // transferOrOptions !== undefined && + // transferOrOptions[SymbolIterator] !== undefined + // ) { + // const transfer = webidl.converters["sequence"]( + // transferOrOptions, + // prefix, + // "Argument 2", + // ); + // options = { transfer }; + // } else { + // options = webidl.converters.StructuredSerializeOptions( + // transferOrOptions, + // prefix, + // "Argument 2", + // ); + // } + // const { transfer } = options; + // const data = serializeJsMessageData(message, transfer); + // if (this.#status === "RUNNING") { + // op_host_post_message(this.#id, data); + // } } // https://nodejs.org/api/worker_threads.html#workerterminate terminate() { - if (this.#status !== "TERMINATED") { - this.#status = "TERMINATED"; - op_host_terminate_worker(this.#id); - } - this.emit("exit", 0); - return PromiseResolve(0); + notImplemented("Worker.prototype.terminate"); + // if (this.#status !== "TERMINATED") { + // this.#status = "TERMINATED"; + // op_host_terminate_worker(this.#id); + // } + // this.emit("exit", 1); } ref() { @@ -452,18 +428,33 @@ export function moveMessagePortToContext() { * @param { MessagePort } port * @returns {object | undefined} */ -export function receiveMessageOnPort(port: MessagePort): object | undefined { - if (!(ObjectPrototypeIsPrototypeOf(MessagePortPrototype, port))) { - const err = new TypeError( - 'The "port" argument must be a MessagePort instance', - ); - err["code"] = "ERR_INVALID_ARG_TYPE"; - throw err; +export function receiveMessageOnPort(_port: MessagePort) { + notImplemented("receiveMessageOnPort"); + // if (!(ObjectPrototypeIsPrototypeOf(MessagePortPrototype, port))) { + // const err = new TypeError( + // 'The "port" argument must be a MessagePort instance', + // ); + // err["code"] = "ERR_INVALID_ARG_TYPE"; + // throw err; + // } + // port[MessagePortReceiveMessageOnPortSymbol] = true; + // const data = op_message_port_recv_message_sync(port[MessagePortIdSymbol]); + // if (data === null) return undefined; + // return { message: deserializeJsMessageData(data)[0] }; +} + +class MessagePort extends EventTarget { + constructor() { + super(); + notImplemented("MessagePort.prototype.constructor"); + } +} + +class BroadcastChannel extends EventTarget { + constructor(_name) { + super(); + notImplemented("BroadcastChannel.prototype.constructor"); } - port[MessagePortReceiveMessageOnPortSymbol] = true; - const data = op_message_port_recv_message_sync(port[MessagePortIdSymbol]); - if (data === null) return undefined; - return { message: deserializeJsMessageData(data)[0] }; } class NodeMessageChannel { @@ -471,94 +462,89 @@ class NodeMessageChannel { port2: MessagePort; constructor() { - const { port1, port2 } = new MessageChannel(); - this.port1 = webMessagePortToNodeMessagePort(port1); - this.port2 = webMessagePortToNodeMessagePort(port2); + notImplemented("MessageChannel.prototype.constructor"); + // const { port1, port2 } = new MessageChannel(); + // this.port1 = webMessagePortToNodeMessagePort(port1); + // this.port2 = webMessagePortToNodeMessagePort(port2); } } -const listeners = new SafeWeakMap< - // deno-lint-ignore no-explicit-any - (...args: any[]) => void, - // deno-lint-ignore no-explicit-any - (ev: any) => any ->(); -function webMessagePortToNodeMessagePort(port: MessagePort) { - port.on = port.addListener = function (this: MessagePort, name, listener) { - // deno-lint-ignore no-explicit-any - const _listener = (ev: any) => { - patchMessagePortIfFound(ev.data); - listener(ev.data); - }; - if (name == "message") { - if (port.onmessage === null) { - port.onmessage = _listener; - } else { - port.addEventListener("message", _listener); - } - } else if (name == "messageerror") { - if (port.onmessageerror === null) { - port.onmessageerror = _listener; - } else { - port.addEventListener("messageerror", _listener); - } - } else if (name == "close") { - port.addEventListener("close", _listener); - } else { - throw new Error(`Unknown event: "${name}"`); - } - listeners.set(listener, _listener); - return this; - }; - port.off = port.removeListener = function ( - this: MessagePort, - name, - listener, - ) { - if (name == "message") { - port.removeEventListener("message", listeners.get(listener)!); - } else if (name == "messageerror") { - port.removeEventListener("messageerror", listeners.get(listener)!); - } else if (name == "close") { - port.removeEventListener("close", listeners.get(listener)!); - } else { - throw new Error(`Unknown event: "${name}"`); - } - listeners.delete(listener); - return this; - }; - port[nodeWorkerThreadCloseCb] = () => { - port.dispatchEvent(new Event("close")); - }; - port.unref = () => { - port[refMessagePort](false); - }; - port.ref = () => { - port[refMessagePort](true); - }; - return port; -} +// const listeners = new SafeWeakMap< +// // deno-lint-ignore no-explicit-any +// (...args: any[]) => void, +// // deno-lint-ignore no-explicit-any +// (ev: any) => any +// >(); +// function webMessagePortToNodeMessagePort(_port: MessagePort) { +// port.on = port.addListener = function (this: MessagePort, name, listener) { +// // deno-lint-ignore no-explicit-any +// const _listener = (ev: any) => listener(ev.data); +// if (name == "message") { +// if (port.onmessage === null) { +// port.onmessage = _listener; +// } else { +// port.addEventListener("message", _listener); +// } +// } else if (name == "messageerror") { +// if (port.onmessageerror === null) { +// port.onmessageerror = _listener; +// } else { +// port.addEventListener("messageerror", _listener); +// } +// } else if (name == "close") { +// port.addEventListener("close", _listener); +// } else { +// throw new Error(`Unknown event: "${name}"`); +// } +// listeners.set(listener, _listener); +// return this; +// }; +// port.off = port.removeListener = function ( +// this: MessagePort, +// name, +// listener, +// ) { +// if (name == "message") { +// port.removeEventListener("message", listeners.get(listener)!); +// } else if (name == "messageerror") { +// port.removeEventListener("messageerror", listeners.get(listener)!); +// } else if (name == "close") { +// port.removeEventListener("close", listeners.get(listener)!); +// } else { +// throw new Error(`Unknown event: "${name}"`); +// } +// listeners.delete(listener); +// return this; +// }; +// port[nodeWorkerThreadCloseCb] = () => { +// port.dispatchEvent(new Event("close")); +// }; +// port.unref = () => { +// port[refMessagePort](false); +// }; +// port.ref = () => { +// port[refMessagePort](true); +// }; +// return port; +// } // TODO(@marvinhagemeister): Recursively iterating over all message // properties seems slow. // Maybe there is a way we can patch the prototype of MessagePort _only_ // inside worker_threads? For now correctness is more important than perf. // deno-lint-ignore no-explicit-any -function patchMessagePortIfFound(data: any, seen = new SafeSet()) { - if (data === null || typeof data !== "object" || seen.has(data)) { - return; - } - seen.add(data); - - if (ObjectPrototypeIsPrototypeOf(MessagePortPrototype, data)) { - webMessagePortToNodeMessagePort(data); - } else { - for (const obj in data as Record) { - if (ObjectHasOwn(data, obj)) { - patchMessagePortIfFound(data[obj], seen); - } - } - } +function patchMessagePortIfFound(_data: any) { + // if (ObjectPrototypeIsPrototypeOf(MessagePortPrototype, data)) { + // data = webMessagePortToNodeMessagePort(data); + // } else { + // for (const obj in data as Record) { + // if (ObjectPrototypeIsPrototypeOf(MessagePortPrototype, data[obj])) { + // data[obj] = webMessagePortToNodeMessagePort(data[obj] as MessagePort); + // break; + // } + // } + // } + // return data; } export { diff --git a/crates/sb_core/js/bootstrap.js b/crates/sb_core/js/bootstrap.js index d1f2647da..79b47652c 100644 --- a/crates/sb_core/js/bootstrap.js +++ b/crates/sb_core/js/bootstrap.js @@ -555,6 +555,7 @@ globalThis.bootstrapSBEdge = (opts, extraCtx) => { const nodeBootstrap = globalThis.nodeBootstrap; if (nodeBootstrap) { nodeBootstrap({ + runningOnMainThread: true, usesLocalNodeModulesDir: false, argv: void 0, nodeDebug: Deno.env.get("NODE_DEBUG") ?? "" From 7a11fa7eed5959dcd4d14b9ed7af04d20fa0cf85 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 2 Jul 2024 03:47:20 +0000 Subject: [PATCH 10/20] chore: add integration tests for `fastify` package --- .../base/test_cases/fastify-latest/index.ts | 10 ++++++ crates/base/test_cases/fastify-v4/index.ts | 10 ++++++ crates/base/tests/integration_tests.rs | 36 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 crates/base/test_cases/fastify-latest/index.ts create mode 100644 crates/base/test_cases/fastify-v4/index.ts diff --git a/crates/base/test_cases/fastify-latest/index.ts b/crates/base/test_cases/fastify-latest/index.ts new file mode 100644 index 000000000..94647a4f5 --- /dev/null +++ b/crates/base/test_cases/fastify-latest/index.ts @@ -0,0 +1,10 @@ +import Fastify from "npm:fastify"; + +const servicePath = import.meta.dirname.split("/").at(-1); +const fastify = Fastify(); + +fastify.get(`/${servicePath}`, () => { + return "meow"; +}); + +await fastify.listen(); diff --git a/crates/base/test_cases/fastify-v4/index.ts b/crates/base/test_cases/fastify-v4/index.ts new file mode 100644 index 000000000..f23385def --- /dev/null +++ b/crates/base/test_cases/fastify-v4/index.ts @@ -0,0 +1,10 @@ +import Fastify from "npm:fastify@4"; + +const servicePath = import.meta.dirname.split("/").at(-1); +const fastify = Fastify(); + +fastify.get(`/${servicePath}`, () => { + return "meow"; +}); + +await fastify.listen(); diff --git a/crates/base/tests/integration_tests.rs b/crates/base/tests/integration_tests.rs index ead528c20..2229f3b24 100644 --- a/crates/base/tests/integration_tests.rs +++ b/crates/base/tests/integration_tests.rs @@ -2214,6 +2214,42 @@ async fn test_allow_net_fetch_google_com() { .await; } +#[tokio::test] +#[serial] +async fn test_fastify_v4_package() { + integration_test!( + "./test_cases/main", + NON_SECURE_PORT, + "fastify-v4", + None, + None, + None, + None, + (|resp| async { + assert_eq!(resp.unwrap().text().await.unwrap(), "meow"); + }), + TerminationToken::new() + ); +} + +#[tokio::test] +#[serial] +async fn test_fastify_latest_package() { + integration_test!( + "./test_cases/main", + NON_SECURE_PORT, + "fastify-latest", + None, + None, + None, + None, + (|resp| async { + assert_eq!(resp.unwrap().text().await.unwrap(), "meow"); + }), + TerminationToken::new() + ); +} + trait AsyncReadWrite: AsyncRead + AsyncWrite + Send + Unpin {} impl AsyncReadWrite for T where T: AsyncRead + AsyncWrite + Send + Unpin {} From 451bcca0796982787b98668065c7a58fd345ff73 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 2 Jul 2024 03:47:41 +0000 Subject: [PATCH 11/20] chore: add an example for `fastify` package --- examples/fastify-v4/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 examples/fastify-v4/index.ts diff --git a/examples/fastify-v4/index.ts b/examples/fastify-v4/index.ts new file mode 100644 index 000000000..029930038 --- /dev/null +++ b/examples/fastify-v4/index.ts @@ -0,0 +1,12 @@ +import Fastify from "npm:fastify@4"; + +const servicePath = import.meta.dirname.split("/").at(-1); +const fastify = Fastify(); + +// Declare a route +fastify.get(`/${servicePath}`, function handler(_request, _reply) { + return "Hello World!"; +}); + +// Run the server! +await fastify.listen(); \ No newline at end of file From 58463039e7872b3e922608a0decadab4d5b2755c Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 23 Jul 2024 00:01:58 +0000 Subject: [PATCH 12/20] stamp: polishing --- crates/base/src/macros/test_macros.rs | 4 ++-- crates/base/src/rt_worker/worker.rs | 2 +- crates/base/tests/integration_tests.rs | 6 +++--- crates/sb_env/lib.rs | 4 +--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/base/src/macros/test_macros.rs b/crates/base/src/macros/test_macros.rs index 1a8459bd4..746a423ca 100644 --- a/crates/base/src/macros/test_macros.rs +++ b/crates/base/src/macros/test_macros.rs @@ -110,13 +110,13 @@ macro_rules! integration_test_with_server_flag { if tokio::time::timeout( // XXX(Nyannyacha): Should we apply variable timeout? - core::time::Duration::from_secs(10), + core::time::Duration::from_secs(30), wait_fut ) .await .is_err() { - panic!("failed to terminate server within 10 seconds"); + panic!("failed to terminate server within 30 seconds"); } }; diff --git a/crates/base/src/rt_worker/worker.rs b/crates/base/src/rt_worker/worker.rs index fa48f84cd..9c00d6eee 100644 --- a/crates/base/src/rt_worker/worker.rs +++ b/crates/base/src/rt_worker/worker.rs @@ -304,7 +304,7 @@ impl Worker { Err(err) => { let _ = booter_signal - .send(Err(anyhow!("worker boot error {}", err.to_string()))); + .send(Err(anyhow!("worker boot error: {err}"))); method_cloner.handle_error(err) } }; diff --git a/crates/base/tests/integration_tests.rs b/crates/base/tests/integration_tests.rs index 2229f3b24..2ce9bf4c9 100644 --- a/crates/base/tests/integration_tests.rs +++ b/crates/base/tests/integration_tests.rs @@ -214,7 +214,7 @@ async fn test_not_trigger_pku_sigsegv_due_to_jit_compilation_non_cli() { .await .unwrap(); - let (res_tx, res_rx) = oneshot::channel::, hyper_v014::Error>>(); + let (res_tx, res_rx) = oneshot::channel::, hyper::Error>>(); let req = Request::builder() .uri("/slow_resp") @@ -1228,7 +1228,7 @@ async fn test_oak_file_upload( mime: Option<&str>, resp_callback: F, ) where - F: FnOnce(Result) -> R, + F: FnOnce(Result) -> R, R: Future, { let client = Client::builder().build().unwrap(); @@ -1485,7 +1485,7 @@ async fn test_decorators(ty: Option) { .text() .await .unwrap() - .starts_with("{\"msg\":\"InvalidWorkerCreation: worker boot error Uncaught SyntaxError: Invalid or unexpected token"),); + .starts_with("{\"msg\":\"InvalidWorkerCreation: worker boot error: Uncaught SyntaxError: Invalid or unexpected token"),); } else { assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.text().await.unwrap().as_str(), "meow?"); diff --git a/crates/sb_env/lib.rs b/crates/sb_env/lib.rs index 8e50292cd..890d00193 100644 --- a/crates/sb_env/lib.rs +++ b/crates/sb_env/lib.rs @@ -59,6 +59,4 @@ fn op_get_env(state: &mut OpState, #[string] key: String) -> Result Result<(), AnyError> { - Err(not_supported()) -} +fn op_delete_env(_state: &mut OpState, #[string] _key: String) {} From 815b10eaf304492739473a512b89cf5c47ed373b Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Wed, 31 Jul 2024 01:53:22 +0000 Subject: [PATCH 13/20] stamp: reflect upstream changes --- crates/sb_core/permissions.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/sb_core/permissions.rs b/crates/sb_core/permissions.rs index b9edc9150..079491d15 100644 --- a/crates/sb_core/permissions.rs +++ b/crates/sb_core/permissions.rs @@ -2,7 +2,6 @@ use deno_core::error::{custom_error, generic_error, AnyError}; use deno_core::url::Url; use deno_fs::OpenOptions; use deno_permissions::NetDescriptor; -use fqdn::fqdn; use std::borrow::Cow; use std::path::Path; @@ -67,11 +66,8 @@ impl deno_fetch::FetchPermissions for Permissions { } if let Some(allow_net) = &self.allow_net { - let hostname = url - .host_str() - .ok_or(generic_error("empty host"))? - .to_string(); - let descriptor = NetDescriptor(fqdn!(&hostname), url.port()); + let hostname = url.host_str().ok_or(generic_error("empty host"))?.parse()?; + let descriptor = NetDescriptor(hostname, url.port()); if !allow_net.contains(&descriptor) { return Err(custom_error( "PermissionDenied", @@ -132,11 +128,8 @@ impl deno_websocket::WebSocketPermissions for Permissions { )); } if let Some(allow_net) = &self.allow_net { - let hostname = url - .host_str() - .ok_or(generic_error("empty host"))? - .to_string(); - let descriptor = NetDescriptor(fqdn!(&hostname), url.port()); + let hostname = url.host_str().ok_or(generic_error("empty host"))?.parse()?; + let descriptor = NetDescriptor(hostname, url.port()); if !allow_net.contains(&descriptor) { return Err(custom_error( "PermissionDenied", From efdd5106c8e55ca02cbadd82402455ad3c0e9d4c Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 13 Aug 2024 04:18:56 +0000 Subject: [PATCH 14/20] stamp: sync with main branch --- crates/base/src/deno_runtime.rs | 245 ++++++++++++------ crates/base/src/macros/test_macros.rs | 8 +- .../implementation/default_handler.rs | 2 +- crates/base/src/rt_worker/worker.rs | 6 +- crates/base/src/rt_worker/worker_ctx.rs | 9 +- crates/base/tests/integration_tests.rs | 2 +- crates/sb_fs/lib.rs | 6 +- crates/sb_fs/virtual_fs.rs | 25 +- crates/sb_graph/eszip_migrate.rs | 1 + crates/sb_graph/eszip_parse.rs | 78 +++++- crates/sb_graph/lib.rs | 112 ++++---- crates/sb_module_loader/standalone/mod.rs | 27 +- .../standalone/standalone_module_loader.rs | 13 +- 13 files changed, 324 insertions(+), 210 deletions(-) diff --git a/crates/base/src/deno_runtime.rs b/crates/base/src/deno_runtime.rs index 4ccc23072..2673ce531 100644 --- a/crates/base/src/deno_runtime.rs +++ b/crates/base/src/deno_runtime.rs @@ -33,14 +33,17 @@ use std::borrow::Cow; use std::collections::HashMap; use std::ffi::c_void; use std::fmt; +use std::future::Future; use std::marker::PhantomData; use std::str::FromStr; use std::sync::{Arc, RwLock}; use std::task::Poll; +use std::thread::ThreadId; use std::time::Duration; use tokio::sync::mpsc; use tokio::time::interval; use tokio_util::sync::CancellationToken; +use tracing::debug; use crate::snapshot; use event_worker::events::{EventMetadata, WorkerEventWithMetadata}; @@ -190,7 +193,8 @@ pub struct DenoRuntime { pub env_vars: HashMap, // TODO: does this need to be pub? pub conf: WorkerRuntimeOpts, - pub(crate) is_termination_requested: Arc, + pub(crate) termination_request_token: CancellationToken, + pub(crate) is_terminated: Arc, pub(crate) is_found_inspector_session: Arc, @@ -286,7 +290,7 @@ where }; } - let mut maybe_arc_import_map = None; + let mut maybe_import_map = None; let only_module_code = maybe_module_code.is_some() && maybe_eszip.is_none() && !is_some_entry_point; @@ -311,10 +315,8 @@ where .await; } - let maybe_import_map = load_import_map(import_map_path.clone())?; - - emitter_factory.set_import_map(maybe_import_map); - maybe_arc_import_map.clone_from(&emitter_factory.maybe_import_map); + emitter_factory.set_import_map(load_import_map(import_map_path.clone())?); + maybe_import_map.clone_from(&emitter_factory.maybe_import_map); let arc_emitter_factory = Arc::new(emitter_factory); let main_module_url_file_path = main_module_url.clone().to_file_path().unwrap(); @@ -395,7 +397,7 @@ where let rt_provider = create_module_loader_for_standalone_from_eszip_kind( eszip, base_dir_path.clone(), - maybe_arc_import_map, + maybe_import_map, import_map_path, maybe_inspector.is_some(), ) @@ -654,7 +656,8 @@ where env_vars, conf, - is_termination_requested: Arc::default(), + termination_request_token: CancellationToken::new(), + is_terminated: Arc::default(), is_found_inspector_session: Arc::default(), @@ -687,20 +690,17 @@ where } } - let send_cpu_metrics_fn = |metric: CPUUsageMetrics| { - if let Some(cpu_metric_tx) = maybe_cpu_usage_metrics_tx.as_ref() { - let _ = cpu_metric_tx.send(metric); - } - }; + let _terminate_guard = scopeguard::guard(self.is_terminated.clone(), |v| { + v.raise(); + }); // NOTE: This is unnecessary on the LIFO task scheduler that can't steal // the task from the other threads. let current_thread_id = std::thread::current().id(); - let mut current_cpu_time_ns; let mut accumulated_cpu_time_ns = 0i64; let inspector = self.inspector(); - let mod_result_rx = unsafe { + let mut mod_result_rx = unsafe { self.js_runtime.v8_isolate().enter(); if inspector.is_some() { @@ -723,7 +723,7 @@ where this.wait_for_inspector_session(); } - if this.is_termination_requested.is_raised() { + if this.termination_request_token.is_cancelled() { this.js_runtime.v8_isolate().exit(); is_terminated.raise(); return (Ok(()), 0i64); @@ -734,30 +734,86 @@ where it.v8_isolate().exit(); }); - send_cpu_metrics_fn(CPUUsageMetrics::Enter(current_thread_id)); + with_cpu_metrics_guard( + current_thread_id, + &maybe_cpu_usage_metrics_tx, + &mut accumulated_cpu_time_ns, + || js_runtime.mod_evaluate(self.main_module_id), + ) + }; + + macro_rules! get_accumulated_cpu_time_ms { + () => { + accumulated_cpu_time_ns / 1_000_000 + }; + } - current_cpu_time_ns = get_current_cpu_time_ns().unwrap(); + { + let event_loop_fut = self.run_event_loop( + name.as_deref(), + current_thread_id, + &maybe_cpu_usage_metrics_tx, + &mut accumulated_cpu_time_ns, + ); - let top_level_await_fut = js_runtime.mod_evaluate(self.main_module_id); - let cpu_time_after_eval_ns = get_current_cpu_time_ns().unwrap(); - let diff_cpu_time_ns = cpu_time_after_eval_ns - current_cpu_time_ns; + let mod_result = tokio::select! { + // Not using biased mode leads to non-determinism for relatively simple + // programs. + biased; - accumulated_cpu_time_ns += diff_cpu_time_ns; + maybe_mod_result = &mut mod_result_rx => { + debug!("received module evaluate {:#?}", maybe_mod_result); + maybe_mod_result - send_cpu_metrics_fn(CPUUsageMetrics::Leave(CPUUsage { - accumulated: accumulated_cpu_time_ns, - diff: diff_cpu_time_ns, - })); + } - top_level_await_fut - }; + event_loop_result = event_loop_fut => { + if let Err(err) = event_loop_result { + Err(anyhow!("event loop error while evaluating the module: {}", err)) + } else { + mod_result_rx.await + } + } + }; + + if let Err(err) = mod_result { + return (Err(err), get_accumulated_cpu_time_ms!()); + } + } + + if let Err(err) = self + .run_event_loop( + name.as_deref(), + current_thread_id, + &maybe_cpu_usage_metrics_tx, + &mut accumulated_cpu_time_ns, + ) + .await + { + return ( + Err(anyhow!("event loop error: {}", err)), + get_accumulated_cpu_time_ms!(), + ); + } + + (Ok(()), get_accumulated_cpu_time_ms!()) + } - let is_termination_requested = self.is_termination_requested.clone(); + fn run_event_loop<'l>( + &'l mut self, + name: Option<&'l str>, + current_thread_id: ThreadId, + maybe_cpu_usage_metrics_tx: &'l Option>, + accumulated_cpu_time_ns: &'l mut i64, + ) -> impl Future> + 'l { + let has_inspector = self.inspector().is_some(); let is_user_worker = self.conf.is_user_worker(); let global_waker = self.waker.clone(); - let mem_check = is_user_worker.then(|| self.mem_check.clone()); + let termination_request_token = self.termination_request_token.clone(); - let poll_result = poll_fn(|cx| unsafe { + let mem_check_state = is_user_worker.then(|| self.mem_check.clone()); + + poll_fn(move |cx| { // INVARIANT: Only can steal current task by other threads when LIFO // task scheduler heuristic disabled. Turning off the heuristic is // unstable now, so it's not considered. @@ -770,16 +826,16 @@ where global_waker.register(waker); - let mut js_runtime = scopeguard::guard(&mut self.js_runtime, |it| { - it.v8_isolate().exit(); - }); + let mut this = self.get_v8_tls_guard(); - js_runtime.v8_isolate().enter(); - send_cpu_metrics_fn(CPUUsageMetrics::Enter(thread_id)); - - current_cpu_time_ns = get_current_cpu_time_ns().unwrap(); + let js_runtime = &mut this.js_runtime; + let cpu_metrics_guard = get_cpu_metrics_guard( + thread_id, + maybe_cpu_usage_metrics_tx, + accumulated_cpu_time_ns, + ); - let wait_for_inspector = if inspector.is_some() { + let wait_for_inspector = if has_inspector { let inspector = js_runtime.inspector(); let inspector_ref = inspector.borrow(); inspector_ref.has_active_sessions() || inspector_ref.has_blocking_sessions() @@ -807,29 +863,17 @@ where &mut std::task::Context::from_waker(waker.as_ref()), PollEventLoopOptions { wait_for_inspector, - pump_v8_message_loop: true, + ..Default::default() }, ) } else { Poll::Pending }; - let cpu_time_after_poll_ns = get_current_cpu_time_ns().unwrap(); - let diff_cpu_time_ns = if need_pool_event_loop { - cpu_time_after_poll_ns - current_cpu_time_ns - } else { - 0 - }; - - accumulated_cpu_time_ns += diff_cpu_time_ns; - - send_cpu_metrics_fn(CPUUsageMetrics::Leave(CPUUsage { - accumulated: accumulated_cpu_time_ns, - diff: diff_cpu_time_ns, - })); + drop(cpu_metrics_guard); if is_user_worker { - let mem_state = mem_check.as_ref().unwrap(); + let mem_state = mem_check_state.as_ref().unwrap(); let total_malloced_bytes = mem_state.check(js_runtime.v8_isolate().as_mut()); mem_state.waker.register(waker); @@ -838,7 +882,7 @@ where "name: {:?}, thread_id: {:?}, accumulated_cpu_time: {}ms, malloced: {}", name.as_ref(), thread_id, - accumulated_cpu_time_ns / 1_000_000, + *accumulated_cpu_time_ns / 1_000_000, bytes_to_display(total_malloced_bytes as u64) ); } @@ -850,29 +894,13 @@ where // terminate the runtime. if need_pool_event_loop && poll_result.is_pending() - && is_termination_requested.is_raised() + && termination_request_token.is_cancelled() { return Poll::Ready(Ok(())); } poll_result }) - .await; - - let result = match poll_result { - Err(err) => Err(anyhow!("event loop error: {}", err)), - Ok(_) => match mod_result_rx.await { - Err(e) => { - error!("{}", e.to_string()); - Err(e) - } - Ok(_) => Ok(()), - }, - }; - - self.is_terminated.raise(); - - (result, accumulated_cpu_time_ns / 1_000_000) } pub fn inspector(&self) -> Option { @@ -921,12 +949,77 @@ where } } } + + fn get_v8_tls_guard<'l>( + &'l mut self, + ) -> scopeguard::ScopeGuard< + &'l mut DenoRuntime, + impl FnOnce(&'l mut DenoRuntime) + 'l, + > { + let mut guard = scopeguard::guard(self, |v| unsafe { + v.js_runtime.v8_isolate().exit(); + }); + + unsafe { + guard.js_runtime.v8_isolate().enter(); + } + + guard + } } fn get_current_cpu_time_ns() -> Result { get_thread_time().context("can't get current thread time") } +fn with_cpu_metrics_guard<'l, F, R>( + thread_id: ThreadId, + maybe_cpu_usage_metrics_tx: &'l Option>, + accumulated_cpu_time_ns: &'l mut i64, + work_fn: F, +) -> R +where + F: FnOnce() -> R, +{ + let _cpu_metrics_guard = get_cpu_metrics_guard( + thread_id, + maybe_cpu_usage_metrics_tx, + accumulated_cpu_time_ns, + ); + + work_fn() +} + +fn get_cpu_metrics_guard<'l>( + thread_id: ThreadId, + maybe_cpu_usage_metrics_tx: &'l Option>, + accumulated_cpu_time_ns: &'l mut i64, +) -> scopeguard::ScopeGuard<(), impl FnOnce(()) + 'l> { + let send_cpu_metrics_fn = move |metric: CPUUsageMetrics| { + if let Some(cpu_metric_tx) = maybe_cpu_usage_metrics_tx.as_ref() { + let _ = cpu_metric_tx.send(metric); + } + }; + + send_cpu_metrics_fn(CPUUsageMetrics::Enter(thread_id)); + + let current_cpu_time_ns = get_current_cpu_time_ns().unwrap(); + + scopeguard::guard((), move |_| { + debug_assert_eq!(thread_id, std::thread::current().id()); + + let cpu_time_after_drop_ns = get_current_cpu_time_ns().unwrap_or(current_cpu_time_ns); + let diff_cpu_time_ns = cpu_time_after_drop_ns - current_cpu_time_ns; + + *accumulated_cpu_time_ns += diff_cpu_time_ns; + + send_cpu_metrics_fn(CPUUsageMetrics::Leave(CPUUsage { + accumulated: *accumulated_cpu_time_ns, + diff: diff_cpu_time_ns, + })); + }) +} + fn set_v8_flags() { let v8_flags = std::env::var("V8_FLAGS").unwrap_or("".to_string()); let mut vec = vec![""]; @@ -1760,10 +1853,10 @@ mod test { let wait_fut = async move { let (result, _) = user_rt.run(duplex_stream_rx, None, None).await; - assert_eq!( - result.unwrap_err().to_string(), - "event loop error: Uncaught Error: execution terminated" - ); + assert!(result + .unwrap_err() + .to_string() + .ends_with("Error: execution terminated")); callback_rx.recv().await.unwrap(); diff --git a/crates/base/src/macros/test_macros.rs b/crates/base/src/macros/test_macros.rs index 746a423ca..df56a1d9d 100644 --- a/crates/base/src/macros/test_macros.rs +++ b/crates/base/src/macros/test_macros.rs @@ -104,8 +104,12 @@ macro_rules! integration_test_with_server_flag { (@term_cleanup $(#[manual] $_:expr)?, $__:ident, $___:ident) => {}; (@term_cleanup $_:expr, $token:ident, $join_fut:ident) => { let wait_fut = async move { - $token.cancel_and_wait().await; - $join_fut.await.unwrap(); + let (_, ret) = tokio::join!( + $token.cancel_and_wait(), + $join_fut + ); + + ret.unwrap(); }; if tokio::time::timeout( diff --git a/crates/base/src/rt_worker/implementation/default_handler.rs b/crates/base/src/rt_worker/implementation/default_handler.rs index cf4809a09..a16999a58 100644 --- a/crates/base/src/rt_worker/implementation/default_handler.rs +++ b/crates/base/src/rt_worker/implementation/default_handler.rs @@ -58,7 +58,7 @@ impl WorkerHandler for Worker { // NOTE(Nyannyacha): If a supervisor unconditionally // requests the isolate to terminate, it might not come to // this branch, so it might be removed in the future. - if created_rt.is_termination_requested.is_raised() { + if created_rt.termination_request_token.is_cancelled() { static EVENT_RECV_DEADLINE_DUR: Duration = Duration::from_secs(5); if let Ok(Ok(ev)) = diff --git a/crates/base/src/rt_worker/worker.rs b/crates/base/src/rt_worker/worker.rs index 9c00d6eee..a6e1e8dc2 100644 --- a/crates/base/src/rt_worker/worker.rs +++ b/crates/base/src/rt_worker/worker.rs @@ -182,8 +182,8 @@ impl Worker { pending().boxed() } else if let Some(token) = termination_token.clone() { let is_terminated = new_runtime.is_terminated.clone(); - let is_termination_requested = - new_runtime.is_termination_requested.clone(); + let termination_request_token = + new_runtime.termination_request_token.clone(); let (waker, thread_safe_handle) = { let js_runtime = &mut new_runtime.js_runtime; @@ -196,7 +196,7 @@ impl Worker { base_rt::SUPERVISOR_RT .spawn(async move { token.inbound.cancelled().await; - is_termination_requested.raise(); + termination_request_token.cancel(); let data_ptr_mut = Box::into_raw(Box::new(supervisor::IsolateInterruptData { diff --git a/crates/base/src/rt_worker/worker_ctx.rs b/crates/base/src/rt_worker/worker_ctx.rs index 68286074c..ca3cf30d5 100644 --- a/crates/base/src/rt_worker/worker_ctx.rs +++ b/crates/base/src/rt_worker/worker_ctx.rs @@ -271,7 +271,7 @@ pub fn create_supervisor( // we assert supervisor is only run for user workers let conf = worker_runtime.conf.as_user_worker().unwrap().clone(); let mem_check_state = worker_runtime.mem_check_state(); - let is_termination_requested = worker_runtime.is_termination_requested.clone(); + let termination_request_token = worker_runtime.termination_request_token.clone(); let giveup_process_requests_token = cancel.clone(); let supervise_cancel_token = CancellationToken::new(); @@ -381,12 +381,13 @@ pub fn create_supervisor( let wait_inspector_disconnect_fut = async move { let ls = tokio::task::LocalSet::new(); ls.run_until(async move { - if is_terminated.is_raised() || is_termination_requested.is_raised() + if is_terminated.is_raised() + || termination_request_token.is_cancelled() { return; } - is_termination_requested.raise(); + termination_request_token.cancel(); if is_found.is_raised() { return; @@ -452,7 +453,7 @@ pub fn create_supervisor( .await .unwrap(); } else { - is_termination_requested.raise(); + termination_request_token.cancel(); } // NOTE: If we issue a hard CPU time limit, It's OK because it is diff --git a/crates/base/tests/integration_tests.rs b/crates/base/tests/integration_tests.rs index 2ce9bf4c9..f768e4cbf 100644 --- a/crates/base/tests/integration_tests.rs +++ b/crates/base/tests/integration_tests.rs @@ -1175,7 +1175,7 @@ async fn req_failure_case_intentional_peer_reset_secure() { async fn req_failure_case_op_cancel_from_server_due_to_cpu_resource_limit() { test_oak_file_upload( Cow::Borrowed("./test_cases/main_small_cpu_time"), - 48 * MB, + 120 * MB, None, |resp| async { let res = resp.unwrap(); diff --git a/crates/sb_fs/lib.rs b/crates/sb_fs/lib.rs index 18348f657..2c36dd8bc 100644 --- a/crates/sb_fs/lib.rs +++ b/crates/sb_fs/lib.rs @@ -4,13 +4,14 @@ use deno_core::normalize_path; use deno_npm::NpmSystemInfo; use eszip::EszipV2; use indexmap::IndexMap; +use log::warn; use sb_eszip_shared::{AsyncEszipDataRead, STATIC_FILES_ESZIP_KEY}; use sb_npm::{CliNpmResolver, InnerCliNpmResolverRef}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; -use virtual_fs::VfsEntry; use url::Url; +use virtual_fs::VfsEntry; pub mod file_system; mod rt; @@ -133,8 +134,7 @@ where } else { // DO NOT include the user's registry url as it may contain credentials, // but also don't make this dependent on the registry url - let registry_url = npm_resolver.registry_base_url(); - let root_path = npm_resolver.registry_folder_in_global_cache(registry_url); + let root_path = npm_resolver.global_cache_root_folder(); let mut builder = VfsBuilder::new(root_path, add_content_callback_fn)?; for package in npm_resolver.all_system_packages(&NpmSystemInfo::default()) { let folder = npm_resolver.resolve_pkg_folder_from_pkg_id(&package.id)?; diff --git a/crates/sb_fs/virtual_fs.rs b/crates/sb_fs/virtual_fs.rs index 55ad253bd..dfb0fc244 100644 --- a/crates/sb_fs/virtual_fs.rs +++ b/crates/sb_fs/virtual_fs.rs @@ -206,39 +206,26 @@ impl<'scope> VfsBuilder<'scope> { let dir = self.add_dir(path.parent().unwrap())?; let name = path.file_name().unwrap().to_string_lossy(); let data_len = data.len(); + match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { Ok(_) => { // already added, just ignore } Err(insert_index) => { + let len = data.len(); + let key = (add_content_callback_fn.lock().unwrap())(path, &name, data); + dir.entries.insert( insert_index, VfsEntry::File(VirtualFile { + key, name: name.to_string(), offset, - len: data.len() as u64, - content: Some(data), + len: len as u64, }), ); } } - // let insert_index = match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { - // Err(insert_index) => insert_index, - // Ok(_) => unreachable!(), - // }; - - // let len = data.len(); - // let key = (add_content_callback_fn.lock().unwrap())(path, &name, data); - - // dir.entries.insert( - // insert_index, - // VfsEntry::File(VirtualFile { - // key, - // name: name.to_string(), - // offset, - // len: len as u64, - // }), - // ); // new file, update the list of files if self.current_offset == offset { diff --git a/crates/sb_graph/eszip_migrate.rs b/crates/sb_graph/eszip_migrate.rs index d91faa40f..e1468dbda 100644 --- a/crates/sb_graph/eszip_migrate.rs +++ b/crates/sb_graph/eszip_migrate.rs @@ -82,6 +82,7 @@ mod v0 { EszipV2 { modules: EszipV2Modules::default(), npm_snapshot: v0_eszip.npm_snapshot.take(), + options: v0_eszip.options, }, None, ); diff --git a/crates/sb_graph/eszip_parse.rs b/crates/sb_graph/eszip_parse.rs index 26867ae55..2d3cf20e6 100644 --- a/crates/sb_graph/eszip_parse.rs +++ b/crates/sb_graph/eszip_parse.rs @@ -1,4 +1,4 @@ -// Below is roughly originated from eszip@0.60.0/src/v2.rs +// Below is roughly originated from eszip@0.72.2/src/v2.rs use std::{ collections::HashMap, @@ -7,18 +7,19 @@ use std::{ use eszip::{ v2::{ - read_npm_section, EszipNpmPackageIndex, EszipV2Module, EszipV2Modules, EszipV2SourceSlot, - HashedSection, + read_npm_section, Checksum, EszipNpmPackageIndex, EszipV2Module, EszipV2Modules, + EszipV2SourceSlot, Options, Section, }, EszipV2, ModuleKind, ParseError, }; use futures::{io::BufReader, AsyncRead, AsyncReadExt}; use hashlink::LinkedHashMap; -const ESZIP_V2_1_MAGIC: &[u8; 8] = b"ESZIP2.1"; +const ESZIP_V2_MAGIC: &[u8; 8] = b"ESZIP_V2"; +const ESZIP_V2_2_MAGIC: &[u8; 8] = b"ESZIP2.2"; pub async fn parse_v2_header( - reader: &mut BufReader, + mut reader: &mut BufReader, ) -> Result { let mut magic = [0u8; 8]; reader.read_exact(&mut magic).await?; @@ -27,9 +28,57 @@ pub async fn parse_v2_header( return Err(ParseError::InvalidV2); } - let is_v3 = magic == *ESZIP_V2_1_MAGIC; - let header = HashedSection::read(reader).await?; - if !header.hash_valid() { + let supports_npm = magic != *ESZIP_V2_MAGIC; + let supports_options = magic == *ESZIP_V2_2_MAGIC; + let mut options = Options::default_for_version(&magic); + + if supports_options { + let mut pre_options = options; + // First read options without checksum, then reread and validate if necessary + pre_options.checksum = Some(Checksum::NoChecksum); + pre_options.checksum_size = None; + let options_header = Section::read(&mut reader, pre_options).await?; + if options_header.content_len() % 2 != 0 { + return Err(ParseError::InvalidV22OptionsHeader(String::from( + "options are expected to be byte tuples", + ))); + } + + for option in options_header.content().chunks(2) { + let (option, value) = (option[0], option[1]); + match option { + 0 => { + options.checksum = Checksum::from_u8(value); + } + 1 => { + options.checksum_size = Some(value); + } + _ => {} // Ignore unknown options for forward compatibility + } + } + if options.checksum_size().is_none() { + return Err(ParseError::InvalidV22OptionsHeader(String::from( + "checksum size must be known", + ))); + } + + if let Some(1..) = options.checksum_size() { + // If the eszip has some checksum configured, the options header is also checksumed. Reread + // it again with the checksum and validate it + let options_header_with_checksum = Section::read_with_size( + options_header.content().chain(&mut reader), + options, + options_header.content_len(), + ) + .await?; + if !options_header_with_checksum.is_checksum_valid() { + return Err(ParseError::InvalidV22OptionsHeaderHash); + } + } + } + + let header = Section::read(&mut reader, options).await?; + if !header.is_checksum_valid() { return Err(ParseError::InvalidV2HeaderHash); } @@ -43,16 +92,16 @@ pub async fn parse_v2_header( // error. macro_rules! read { ($n:expr, $err:expr) => {{ - if read + $n > header.len() { + if read + $n > header.content_len() { return Err(ParseError::InvalidV2Header($err)); } let start = read; read += $n; - &header.bytes()[start..read] + &header.content()[start..read] }}; } - while read < header.len() { + while read < header.content_len() { let specifier_len = u32::from_be_bytes(read!(4, "specifier len").try_into().unwrap()) as usize; let specifier = String::from_utf8(read!(specifier_len, "specifier").to_vec()) @@ -107,7 +156,7 @@ pub async fn parse_v2_header( .map_err(|_| ParseError::InvalidV2Specifier(read))?; modules.insert(specifier, EszipV2Module::Redirect { target }); } - 2 if is_v3 => { + 2 if supports_npm => { // npm specifier let pkg_id = u32::from_be_bytes(read!(4, "npm package id").try_into().unwrap()); npm_specifiers.insert(specifier, EszipNpmPackageIndex(pkg_id)); @@ -116,8 +165,8 @@ pub async fn parse_v2_header( }; } - let npm_snapshot = if is_v3 { - read_npm_section(reader, npm_specifiers).await? + let npm_snapshot = if supports_npm { + read_npm_section(reader, options, npm_specifiers).await? } else { None }; @@ -125,5 +174,6 @@ pub async fn parse_v2_header( Ok(EszipV2 { modules: EszipV2Modules(Arc::new(Mutex::new(modules))), npm_snapshot, + options, }) } diff --git a/crates/sb_graph/lib.rs b/crates/sb_graph/lib.rs index c182272c1..12ea02db2 100644 --- a/crates/sb_graph/lib.rs +++ b/crates/sb_graph/lib.rs @@ -8,7 +8,7 @@ use deno_core::url::Url; use deno_core::{FastString, JsBuffer, ModuleSpecifier}; use deno_fs::{FileSystem, RealFs}; use deno_npm::NpmSystemInfo; -use eszip::v2::{EszipV2Module, EszipV2Modules, EszipV2SourceSlot}; +use eszip::v2::{EszipV2Module, EszipV2Modules, EszipV2SourceSlot, Options, Section}; use eszip::{EszipV2, Module, ModuleKind, ParseError}; use futures::future::OptionFuture; use futures::{AsyncReadExt, AsyncSeekExt}; @@ -22,7 +22,6 @@ use sb_fs::{build_vfs, VfsOpts}; use sb_npm::InnerCliNpmResolverRef; use scopeguard::ScopeGuard; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use std::borrow::Cow; use std::collections::HashMap; use std::fs; @@ -38,11 +37,11 @@ pub mod emitter; pub mod errors; pub mod eszip_migrate; pub mod graph_fs; -pub mod graph_resolver; pub mod graph_util; pub mod import_map; pub mod jsr; pub mod jsx_util; +pub mod resolver; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -114,6 +113,7 @@ impl Clone for LazyLoadableEszip { eszip: EszipV2 { modules: self.eszip.modules.clone(), npm_snapshot: None, + options: self.eszip.options, }, maybe_data_section: self.maybe_data_section.clone(), } @@ -216,6 +216,7 @@ pub enum EszipDataSectionMetadata { pub struct EszipDataSection { inner: Arc>>>, modules: EszipV2Modules, + options: Options, initial_offset: u64, sources_len: Arc>>, locs_by_specifier: Arc>>>, @@ -223,10 +224,16 @@ pub struct EszipDataSection { } impl EszipDataSection { - pub fn new(inner: Cursor>, initial_offset: u64, modules: EszipV2Modules) -> Self { + pub fn new( + inner: Cursor>, + initial_offset: u64, + modules: EszipV2Modules, + options: Options, + ) -> Self { Self { inner: Arc::new(Mutex::new(inner)), modules, + options, initial_offset, sources_len: Arc::default(), locs_by_specifier: Arc::default(), @@ -320,25 +327,17 @@ impl EszipDataSection { Self::wake_source_slot(modules, specifier, || EszipV2SourceSlot::Taken); }); - let mut source_bytes = vec![0u8; loc.source_length]; - io.read_exact(&mut source_bytes).await?; + let source_bytes = + Section::read_with_size(&mut io, self.options, loc.source_length).await?; - let expected_hash = &mut [0u8; 32]; - io.read_exact(expected_hash).await?; - - let mut hasher = Sha256::new(); - hasher.update(&source_bytes); - - let actual_hash = hasher.finalize(); - - if &*actual_hash != expected_hash { + if !source_bytes.is_checksum_valid() { return Err(ParseError::InvalidV2SourceHash(specifier.to_string())) .context("invalid source hash"); } let _ = ScopeGuard::into_inner(wake_guard); - Some(source_bytes) + Some(source_bytes.into_content()) }; if let Some(bytes) = source_bytes { @@ -383,25 +382,17 @@ impl EszipDataSection { Self::wake_source_map_slot(modules, specifier, || EszipV2SourceSlot::Taken); }); - let mut source_map_bytes = vec![0u8; loc.source_map_length]; - io.read_exact(&mut source_map_bytes).await?; - - let expected_hash = &mut [0u8; 32]; - io.read_exact(expected_hash).await?; + let source_map_bytes = + Section::read_with_size(&mut io, self.options, loc.source_map_length).await?; - let mut hasher = Sha256::new(); - hasher.update(&source_map_bytes); - - let actual_hash = hasher.finalize(); - - if &*actual_hash != expected_hash { + if !source_map_bytes.is_checksum_valid() { return Err(ParseError::InvalidV2SourceHash(specifier.to_string())) .context("invalid source hash"); } let _ = ScopeGuard::into_inner(wake_guard); - Some(source_map_bytes) + Some(source_map_bytes.into_content()) }; if let Some(bytes) = source_map_bytes { @@ -414,10 +405,15 @@ impl EszipDataSection { } pub async fn read_data_section_all(self: Arc) -> Result<(), ParseError> { - // NOTE: Below codes is roughly originated from eszip@0.60.0/src/v2.rs + // NOTE: Below codes is roughly originated from eszip@0.72.2/src/v2.rs let this = Arc::into_inner(self).unwrap(); let modules = this.modules; + let checksum_size = this + .options + .checksum_size() + .expect("checksum size must be known") as usize; + let mut loaded_locs = Arc::into_inner(this.loaded_locs_by_specifier) .unwrap() .into_inner(); @@ -483,7 +479,7 @@ impl EszipDataSection { .ok_or(ParseError::InvalidV2SourceOffset(read))?; if !need_load { - read += length + 32; + read += length + checksum_size; io.seek(SeekFrom::Current((length + 32) as i64)) .await @@ -492,25 +488,16 @@ impl EszipDataSection { continue; } - let mut source_bytes = vec![0u8; length]; - io.read_exact(&mut source_bytes).await?; - - let expected_hash = &mut [0u8; 32]; - io.read_exact(expected_hash).await?; + let source_bytes = Section::read_with_size(&mut io, this.options, length).await?; - let mut hasher = Sha256::new(); - hasher.update(&source_bytes); - - let actual_hash = hasher.finalize(); - - if &*actual_hash != expected_hash { + if !source_bytes.is_checksum_valid() { return Err(ParseError::InvalidV2SourceHash(specifier)); } - read += length + 32; + read += source_bytes.total_len(); Self::wake_source_slot(&modules, &specifier, move || { - EszipV2SourceSlot::Ready(Arc::from(source_bytes)) + EszipV2SourceSlot::Ready(Arc::from(source_bytes.into_content())) }); } @@ -523,34 +510,25 @@ impl EszipDataSection { .ok_or(ParseError::InvalidV2SourceOffset(read))?; if !need_load { - read += length + 32; + read += length + checksum_size; - io.seek(SeekFrom::Current((length + 32) as i64)) + io.seek(SeekFrom::Current((length + checksum_size) as i64)) .await .unwrap(); continue; } - let mut source_map_bytes = vec![0u8; length]; - io.read_exact(&mut source_map_bytes).await?; - - let expected_hash = &mut [0u8; 32]; - io.read_exact(expected_hash).await?; + let source_map_bytes = Section::read_with_size(&mut io, this.options, length).await?; - let mut hasher = Sha256::new(); - hasher.update(&source_map_bytes); - - let actual_hash = hasher.finalize(); - - if &*actual_hash != expected_hash { + if !source_map_bytes.is_checksum_valid() { return Err(ParseError::InvalidV2SourceHash(specifier)); } - read += length + 32; + read += source_map_bytes.total_len(); Self::wake_source_map_slot(&modules, &specifier, move || { - EszipV2SourceSlot::Ready(Arc::from(source_map_bytes)) + EszipV2SourceSlot::Ready(Arc::from(source_map_bytes.into_content())) }); } @@ -633,8 +611,12 @@ pub async fn payload_to_eszip(eszip_payload_kind: EszipPayloadKind) -> LazyLoada let eszip = eszip_parse::parse_v2_header(&mut bufreader).await.unwrap(); let initial_offset = bufreader.stream_position().await.unwrap(); - let data_section = - EszipDataSection::new(io.into_inner(), initial_offset, eszip.modules.clone()); + let data_section = EszipDataSection::new( + io.into_inner(), + initial_offset, + eszip.modules.clone(), + eszip.options, + ); LazyLoadableEszip::new(eszip, Some(Arc::new(data_section))) } @@ -656,7 +638,7 @@ where emitter_factory.clone(), &maybe_module_code, ) - .await; + .await?; let mut eszip = create_eszip_from_graph_raw(graph, Some(emitter_factory.clone())).await?; @@ -679,8 +661,7 @@ where )?; let bin_code: Arc<[u8]> = emit_source.as_bytes().into(); - let npm_res = emitter_factory.npm_resolution().await; - let resolver = emitter_factory.npm_resolver().await; + let resolver = emitter_factory.npm_resolver().await.cloned()?; let (npm_vfs, _npm_files) = match resolver.clone().as_inner() { InnerCliNpmResolverRef::Managed(managed) => { @@ -690,9 +671,6 @@ where let (root_dir, files) = build_vfs( VfsOpts { npm_resolver: resolver.clone(), - npm_registry_api: emitter_factory.npm_api().await.clone(), - npm_cache: emitter_factory.npm_cache().await.clone(), - npm_resolution: emitter_factory.npm_resolution().await.clone(), }, |_path, _key, content| { let key = format!("vfs://{}", count); @@ -705,7 +683,7 @@ where .into_dir_and_files(); let snapshot = - npm_res.serialized_valid_snapshot_for_system(&NpmSystemInfo::default()); + managed.serialized_valid_snapshot_for_system(&NpmSystemInfo::default()); eszip.add_npm_snapshot(snapshot); diff --git a/crates/sb_module_loader/standalone/mod.rs b/crates/sb_module_loader/standalone/mod.rs index 4124be61d..d9a2e83a6 100644 --- a/crates/sb_module_loader/standalone/mod.rs +++ b/crates/sb_module_loader/standalone/mod.rs @@ -18,11 +18,11 @@ use sb_core::cache::CacheSetting; use sb_core::cert::{get_root_cert_store, CaData}; use sb_core::node::CliCjsCodeAnalyzer; use sb_core::util::http_util::HttpClientProvider; +use sb_eszip_shared::{AsyncEszipDataRead, SOURCE_CODE_ESZIP_KEY, VFS_ESZIP_KEY}; use sb_fs::file_system::DenoCompileFileSystem; use sb_fs::{extract_static_files_from_eszip, load_npm_vfs}; use sb_graph::resolver::{CjsResolutionStore, CliNodeResolver, NpmModuleLoader}; use sb_graph::{eszip_migrate, payload_to_eszip, EszipPayloadKind, LazyLoadableEszip}; -use sb_eszip_shared::{AsyncEszipDataRead, SOURCE_CODE_ESZIP_KEY, VFS_ESZIP_KEY}; use sb_node::analyze::NodeCodeTranslator; use sb_node::NodeResolver; use sb_npm::cache_dir::NpmCacheDir; @@ -215,14 +215,13 @@ where pub async fn create_module_loader_for_standalone_from_eszip_kind

( eszip_payload_kind: EszipPayloadKind, base_dir_path: P, - maybe_import_map_arc: Option>, + maybe_import_map: Option, maybe_import_map_path: Option, include_source_map: bool, ) -> Result where P: AsRef, { - let mut maybe_import_map = None; let eszip = match eszip_migrate::try_migrate_if_needed( payload_to_eszip(eszip_payload_kind).await, ) @@ -234,16 +233,18 @@ where } }; - if let Some(import_map) = maybe_import_map_arc { - let clone_import_map = (*import_map).clone(); - maybe_import_map = Some(clone_import_map); - } else if let Some(import_map_path) = maybe_import_map_path { - let import_map_url = Url::parse(import_map_path.as_str())?; - if let Some(import_map_module) = eszip.ensure_import_map(import_map_url.as_str()) { - if let Some(source) = import_map_module.source().await { - let source = std::str::from_utf8(&source)?.to_string(); - let result = parse_from_json(&import_map_url, &source)?; - maybe_import_map = Some(result.import_map); + let maybe_import_map = 'scope: { + if maybe_import_map.is_some() { + break 'scope maybe_import_map; + } else if let Some(import_map_path) = maybe_import_map_path { + let import_map_url = Url::parse(import_map_path.as_str())?; + if let Some(import_map_module) = eszip.ensure_import_map(import_map_url.as_str()) { + if let Some(source) = import_map_module.source().await { + let source = std::str::from_utf8(&source)?.to_string(); + let result = parse_from_json(import_map_url, &source)?; + + break 'scope Some(result.import_map); + } } } diff --git a/crates/sb_module_loader/standalone/standalone_module_loader.rs b/crates/sb_module_loader/standalone/standalone_module_loader.rs index e363f7b22..b64f2dd7d 100644 --- a/crates/sb_module_loader/standalone/standalone_module_loader.rs +++ b/crates/sb_module_loader/standalone/standalone_module_loader.rs @@ -16,11 +16,11 @@ use deno_core::{ModuleSpecifier, RequestedModuleType}; use deno_semver::npm::NpmPackageReqReference; use eszip::deno_graph; use eszip::EszipRelativeFileBaseUrl; +use sb_eszip_shared::AsyncEszipDataRead; use sb_graph::resolver::CliNodeResolver; use sb_graph::resolver::NpmModuleLoader; -use sb_node::NodeResolutionMode; -use sb_eszip_shared::AsyncEszipDataRead; use sb_graph::LazyLoadableEszip; +use sb_node::NodeResolutionMode; use std::sync::Arc; use tracing::instrument; @@ -32,7 +32,7 @@ pub struct WorkspaceEszipModule { } pub struct WorkspaceEszip { - pub eszip: eszip::EszipV2, + pub eszip: LazyLoadableEszip, pub root_dir_url: ModuleSpecifier, } @@ -41,14 +41,14 @@ impl WorkspaceEszip { if specifier.scheme() == "file" { let specifier_key = EszipRelativeFileBaseUrl::new(&self.root_dir_url).specifier_key(specifier); - let module = self.eszip.get_module(&specifier_key)?; + let module = self.eszip.ensure_module(&specifier_key)?; let specifier = self.root_dir_url.join(&module.specifier).unwrap(); Some(WorkspaceEszipModule { specifier, inner: module, }) } else { - let module = self.eszip.get_module(specifier.as_str())?; + let module = self.eszip.ensure_module(specifier.as_str())?; Some(WorkspaceEszipModule { specifier: ModuleSpecifier::parse(&module.specifier).unwrap(), inner: module, @@ -59,7 +59,6 @@ impl WorkspaceEszip { pub struct SharedModuleLoaderState { pub(crate) eszip: WorkspaceEszip, - pub(crate) eszip: LazyLoadableEszip, pub(crate) workspace_resolver: WorkspaceResolver, pub(crate) npm_module_loader: Arc, pub(crate) node_resolver: Arc, @@ -230,7 +229,7 @@ impl ModuleLoader for EmbeddedModuleLoader { ); } - let Some(module) = self.shared.eszip.ensure_module(original_specifier.as_str()) else { + let Some(module) = self.shared.eszip.get_module(original_specifier) else { return deno_core::ModuleLoadResponse::Sync(Err(type_error(format!( "Module not found: {}", original_specifier From 0fd557624e77743460c23fd1876cd70f05f214b9 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 13 Aug 2024 21:58:40 +0000 Subject: [PATCH 15/20] feat: expose eszip v2 checksum option into cli --- crates/base/src/deno_runtime.rs | 6 ++++-- crates/cli/src/flags.rs | 24 +++++++++++++++++++++++- crates/cli/src/main.rs | 7 ++++++- crates/sb_graph/lib.rs | 7 +++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/crates/base/src/deno_runtime.rs b/crates/base/src/deno_runtime.rs index 2673ce531..f055dd93a 100644 --- a/crates/base/src/deno_runtime.rs +++ b/crates/base/src/deno_runtime.rs @@ -331,6 +331,8 @@ where arc_emitter_factory, maybe_code, import_map_path.clone(), + // here we don't want to add extra cost, so we won't use a checksum + None, ) .await?; @@ -1279,7 +1281,7 @@ mod test { .unwrap(); let path_buf = PathBuf::from("./test_cases/eszip-source-test.ts"); let emitter_factory = Arc::new(EmitterFactory::new()); - let bin_eszip = generate_binary_eszip(path_buf, emitter_factory.clone(), None, None) + let bin_eszip = generate_binary_eszip(path_buf, emitter_factory.clone(), None, None, None) .await .unwrap(); fs::remove_file("./test_cases/eszip-source-test.ts").unwrap(); @@ -1344,7 +1346,7 @@ mod test { let file = PathBuf::from("./test_cases/eszip-silly-test/index.ts"); let service_path = PathBuf::from("./test_cases/eszip-silly-test"); let emitter_factory = Arc::new(EmitterFactory::new()); - let binary_eszip = generate_binary_eszip(file, emitter_factory.clone(), None, None) + let binary_eszip = generate_binary_eszip(file, emitter_factory.clone(), None, None, None) .await .unwrap(); diff --git a/crates/cli/src/flags.rs b/crates/cli/src/flags.rs index 2a4a5e0b6..02ba957a8 100644 --- a/crates/cli/src/flags.rs +++ b/crates/cli/src/flags.rs @@ -3,8 +3,24 @@ use std::{net::SocketAddr, path::PathBuf}; use clap::{ arg, builder::{BoolishValueParser, FalseyValueParser, TypedValueParser}, - crate_version, value_parser, ArgAction, ArgGroup, Command, + crate_version, value_parser, ArgAction, ArgGroup, Command, ValueEnum, }; +use sb_graph::Checksum; + +#[derive(ValueEnum, Default, Clone, Copy)] +#[repr(u8)] +pub(super) enum EszipV2ChecksumKind { + #[default] + NoChecksum = 0, + Sha256 = 1, + XxHash3 = 2, +} + +impl From for Option { + fn from(value: EszipV2ChecksumKind) -> Self { + Checksum::from_u8(value as u8) + } +} pub(super) fn get_cli() -> Command { Command::new(env!("CARGO_BIN_NAME")) @@ -218,6 +234,12 @@ fn get_bundle_command() -> Command { .help("Type of decorator to use when bundling. If not specified, the decorator feature is disabled.") .value_parser(["tc39", "typescript", "typescript_with_metadata"]), ) + .arg( + arg!(--"checksum" ) + .env("EDGE_RUNTIME_BUNDLE_CHECKSUM") + .help("Hash function to use when checksum the contents") + .value_parser(value_parser!(EszipV2ChecksumKind)) + ) } fn get_unbundle_command() -> Command { diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2c5895c5a..6684c4e8d 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -13,7 +13,7 @@ use base::{DecoratorType, InspectorOption}; use clap::ArgMatches; use deno_core::url::Url; use env::resolve_deno_runtime_env; -use flags::get_cli; +use flags::{get_cli, EszipV2ChecksumKind}; use log::warn; use sb_graph::emitter::EmitterFactory; use sb_graph::import_map::load_import_map; @@ -271,6 +271,10 @@ fn main() -> Result<(), anyhow::Error> { .to_string(), ); } + let maybe_checksum_kind = sub_matches + .get_one::("checksum") + .copied() + .and_then(EszipV2ChecksumKind::into); emitter_factory.set_decorator_type(maybe_decorator); emitter_factory.set_import_map(maybe_import_map.clone()); @@ -280,6 +284,7 @@ fn main() -> Result<(), anyhow::Error> { Arc::new(emitter_factory), None, maybe_import_map_url, + maybe_checksum_kind, ) .await?; diff --git a/crates/sb_graph/lib.rs b/crates/sb_graph/lib.rs index 12ea02db2..d7cb33728 100644 --- a/crates/sb_graph/lib.rs +++ b/crates/sb_graph/lib.rs @@ -43,6 +43,8 @@ pub mod jsr; pub mod jsx_util; pub mod resolver; +pub use eszip::v2::Checksum; + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DecoratorType { @@ -628,6 +630,7 @@ pub async fn generate_binary_eszip

( emitter_factory: Arc, maybe_module_code: Option, maybe_import_map_url: Option, + maybe_checksum: Option, ) -> Result where P: AsRef, @@ -641,6 +644,9 @@ where .await?; let mut eszip = create_eszip_from_graph_raw(graph, Some(emitter_factory.clone())).await?; + if let Some(checksum) = maybe_checksum { + eszip.set_checksum(checksum); + } let source_code: Arc = if let Some(code) = maybe_module_code { code.as_str().into() @@ -903,6 +909,7 @@ mod test { Arc::new(EmitterFactory::new()), None, None, + None, ) .await; From f04aeb982206adffeb685b32ce8412afb919982c Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 13 Aug 2024 22:04:14 +0000 Subject: [PATCH 16/20] stamp: polishing --- crates/base/src/deno_runtime.rs | 70 ++++++++++++++++----------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/crates/base/src/deno_runtime.rs b/crates/base/src/deno_runtime.rs index f055dd93a..aca907cb8 100644 --- a/crates/base/src/deno_runtime.rs +++ b/crates/base/src/deno_runtime.rs @@ -1327,8 +1327,8 @@ mod test { "", ModuleCodeString::from( r#" - globalThis.isTenEven; - "# + globalThis.isTenEven; + "# .to_string(), ), ) @@ -1391,8 +1391,8 @@ mod test { "", ModuleCodeString::from( r#" - globalThis.isTenEven; - "# + globalThis.isTenEven; + "# .to_string(), ), ) @@ -1456,8 +1456,8 @@ mod test { "", ModuleCodeString::from( r#" - Deno.readTextFileSync("./test_cases/readFile/hello_world.json"); - "# + Deno.readTextFileSync("./test_cases/readFile/hello_world.json"); + "# .to_string(), ), ) @@ -1498,8 +1498,8 @@ mod test { "", ModuleCodeString::from( r#" - globalThis.hello; - "# + globalThis.hello; + "# .to_string(), ), ) @@ -1581,21 +1581,21 @@ mod test { "", ModuleCodeString::from( r#" - // Should not be able to set - const data = { - gid: Deno.gid(), - uid: Deno.uid(), - hostname: Deno.hostname(), - loadavg: Deno.loadavg(), - osUptime: Deno.osUptime(), - osRelease: Deno.osRelease(), - systemMemoryInfo: Deno.systemMemoryInfo(), - consoleSize: Deno.consoleSize(), - version: [Deno.version.deno, Deno.version.v8, Deno.version.typescript], - networkInterfaces: Deno.networkInterfaces() - }; - data; - "# + // Should not be able to set + const data = { + gid: Deno.gid(), + uid: Deno.uid(), + hostname: Deno.hostname(), + loadavg: Deno.loadavg(), + osUptime: Deno.osUptime(), + osRelease: Deno.osRelease(), + systemMemoryInfo: Deno.systemMemoryInfo(), + consoleSize: Deno.consoleSize(), + version: [Deno.version.deno, Deno.version.v8, Deno.version.typescript], + networkInterfaces: Deno.networkInterfaces() + }; + data; + "# .to_string(), ), ) @@ -1675,9 +1675,9 @@ mod test { "", ModuleCodeString::from( r#" - let cmd = new Deno.Command("", {}); - cmd.outputSync(); - "# + let cmd = new Deno.Command("", {}); + cmd.outputSync(); + "# .to_string(), ), ); @@ -1708,9 +1708,9 @@ mod test { "", ModuleCodeString::from( r#" - // Should not be able to set - Deno.env.set("Supa_Test", "Supa_Value"); - "# + // Should not be able to set + Deno.env.set("Supa_Test", "Supa_Value"); + "# .to_string(), ), ) @@ -1726,9 +1726,9 @@ mod test { "", ModuleCodeString::from( r#" - // Should not be able to set - Deno.env.get("Supa_Test"); - "# + // Should not be able to set + Deno.env.get("Supa_Test"); + "# .to_string(), ), ) @@ -1745,9 +1745,9 @@ mod test { "", ModuleCodeString::from( r#" - // Should not be able to set - Deno.env.get("Supa_Test"); - "# + // Should not be able to set + Deno.env.get("Supa_Test"); + "# .to_string(), ), ) From 3ae0711097bf3369c902fa4a8fad0001f7820827 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Tue, 13 Aug 2024 23:04:59 +0000 Subject: [PATCH 17/20] chore: make clippy happy --- crates/base/src/deno_runtime.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/base/src/deno_runtime.rs b/crates/base/src/deno_runtime.rs index aca907cb8..4bdfd0acd 100644 --- a/crates/base/src/deno_runtime.rs +++ b/crates/base/src/deno_runtime.rs @@ -804,7 +804,7 @@ where fn run_event_loop<'l>( &'l mut self, name: Option<&'l str>, - current_thread_id: ThreadId, + #[allow(unused_variables)] current_thread_id: ThreadId, maybe_cpu_usage_metrics_tx: &'l Option>, accumulated_cpu_time_ns: &'l mut i64, ) -> impl Future> + 'l { From 4949c5c324f6071e80d5ae8c2547b66dd058fd18 Mon Sep 17 00:00:00 2001 From: Nyannyacha Date: Wed, 14 Aug 2024 03:02:59 +0000 Subject: [PATCH 18/20] fix(eszip): bump up supabase eszip version to v1.1 and add migration --- .../npm-supabase-js/v1_1_no_checksum.eszip | Bin 0 -> 6484325 bytes .../npm-supabase-js/v1_1_xx_hash3.eszip | Bin 0 -> 6490173 bytes crates/sb_eszip_shared/lib.rs | 2 +- crates/sb_graph/eszip_migrate.rs | 184 +++++++++++++++++- 4 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 crates/base/test_cases/eszip-migration/npm-supabase-js/v1_1_no_checksum.eszip create mode 100644 crates/base/test_cases/eszip-migration/npm-supabase-js/v1_1_xx_hash3.eszip diff --git a/crates/base/test_cases/eszip-migration/npm-supabase-js/v1_1_no_checksum.eszip b/crates/base/test_cases/eszip-migration/npm-supabase-js/v1_1_no_checksum.eszip new file mode 100644 index 0000000000000000000000000000000000000000..b27393cf8cafdded7b3f3bb7c04855ccc272fa5c GIT binary patch literal 6484325 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

^A6IP#2p({aqV-FL;U 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 z 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

bh%C;^ZInjXvnIlMU|v=}s| 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

PL7v5M@WSP{T z{5-m-&C*^Q3BxC=&4JWwoAaUGh7*)7!oEn8?FQidgsE67r6orL!jl)a_0kKqPAw!y~mg{OabdNH`Cs0|7Z#k z*q}Wdc0qS;fXbq`STuCF%QKNgBopVc&X1#Z_+{4lZnN02)wrS|J0Kg^G3;n^O52_G zCbmP7DcFR96B9^cQhB$Owb9HY$XH|itik>T9di1D7I1(8q5I;$AEm1X8WqL0Bw0_2~xA5?P2Sb((* zx}@%tH+;#4$S-S17Qo`AVOh@5Pqi^vLiq#hKn8-<#zql*4SD6007SJdpw8dyqnDus zJjh5B0QnU33zxsAhET<-23hU`PlZHcKx}Mm z(pcCT%5f4@9R}v9?EfjDe5=HLXF~n##>n$U@=*)Hj_R=G5%ITYM>*y<4MZoE+uC6| zXJbsq*P3W|4>?BV0UxcQD|QrRWlTL)SCFKWc8i`xS2n@yPZu7iS90D@krh>{|GLiq zEja&f@ACcP>Wae!_z|u+ruZDn?RtdZflnN8Oya?07Y=&Wwqz*2d_>SPovmx^t$&M2&!^yQgv}E) z2=bFQa;JSOKL?1&1+IY`2Mhjj{PRdt;ER4E{Qe9jpD`$Edufzqfq;_{yFm!AE3poK zW*ejH8R#xIawn{Yu>sIb^<}ZY)8Qqm$4!u0TKtY%U+_J@kBiMC*BcN z)|FFJaLOU^q&T6ZoOrqUy@1W-hsY7*RB<<>23v5F62g;f3c_W{RyaY}@1q;iRNji? z5QTrp1S=6tX9{dH-qFV;fUaCg3C@pKcORnrqJ!KN)F|P3=GM^SMPH- zDHFR#(u<#w$HE136<Vf-PhKcxX^7N923b%`-)#^U}Y zhs9P%Cr1_}YJwQY76+uKHAqbzA#s~PhS~rNQIRb_=8P;PY5~yMqh?1W3ryfkmMO8& zf)s?nKm6((%nX-mXdn`e#8a>-;PHv@@F&-EpoIwzt5i^rMo4L~dKlH%Ovq^XCI~1vEX9P1T__{`l__7Z|5P@jpHs3{W<{B=&M$=RPh6}T3=`#%1aE{^1 zJPb?hLs}&yCT#1#FuaRCyZOXqilv;-~*NU zchduQg_w&Jf$oT61hwbO9ifsxF_Sd-a@8Bb1gub!KvOCUuix9?MzbE|cecH^!7`ka zE|jdGP>5H^ethM-_cmD0T&oyVH@9%mNKt0A z-iPk){{{chd!BFTrr@*p6#Ov4C5>>eju2Spe{}S3lDq{$tX$euAn#<8Ss%eDZGLR^ z0%YODzbek40IN;sRIB zw<}YI$Y=gZ$L*y({j6n>-r~7`xz|kW1yRfnFwOBv0I#^fn{?U8;{YEVi41K!pMvH z6Mqw=PkJza30opx5E}rh(RY?kMdWLKqEyq@BBej^tAmQe2`u?hSe17DJBwJw&;+4U z!yZdITin7{qyzh%rHk+g9Mf#W}J<#+c!mgPSi0?VpF3~dfe7kWiuR!xfd1$PV+2b&DX5GKV#;_Ux-D^Md*nCuj z9`FcZTRy`FdxFKzuOYCv=!(tBfPR5FxZF~Jm(3Yz-V8ltI92}J!n^*{Kf6QS^tm?R zBw${fRE|-z1l-o3;wy>I8qNkLYhm$`FPA4f#guSoR|JEPu5Iu(lTajfmOM74d3XpJ z5GnBcKD-CE?}@@6UQ`^rsmW_M#V~xM>V>CmPm@Ckl|B~S)IDTQK3ur|UGp+eu?Aj) z@w%SsEyTQ>Q~5eL=#kC_5vJj&^!c8gEf&jC#{T<-Tgo4$m(oQM7K!HG-v7dd+vray zPuZ$ipm}~lekCzL8_t1omoU~8^EK$E23RkjH=&NFWaG%1yD~m7$>AFxDV{YfN!-Jw zre*h0U9PTZ_@0jcF($H2e1IKx=luIj_X{=C$g-eAb*FuYx5&_7`vNO_%0Dbkd5Ayp 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