diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a823829419..4e7a8d7618 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,9 +1,8 @@ FROM mcr.microsoft.com/vscode/devcontainers/rust:1-bullseye -# Install cmake and protobuf-compiler +# Install cmake RUN apt-get update \ && apt-get install -y cmake \ - && apt-get install -y protobuf-compiler \ && rm -rf /var/lib/apt/lists/* # Install Deno diff --git a/.dprint.json b/.dprint.json index 6ee3db3c5d..3b2376f786 100644 --- a/.dprint.json +++ b/.dprint.json @@ -65,13 +65,17 @@ "tests/wpt/runner/expectation.json", "tests/wpt/runner/manifest.json", "tests/wpt/suite", - "third_party" + "third_party", + "tests/specs/run/shebang_with_json_imports_tsc", + "tests/specs/run/shebang_with_json_imports_swc", + "tests/specs/run/ext_flag_takes_precedence_over_extension", + "tests/specs/run/error_syntax_empty_trailing_line/error_syntax_empty_trailing_line.mjs" ], "plugins": [ - "https://plugins.dprint.dev/typescript-0.93.0.wasm", - "https://plugins.dprint.dev/json-0.19.3.wasm", + "https://plugins.dprint.dev/typescript-0.93.2.wasm", + "https://plugins.dprint.dev/json-0.19.4.wasm", "https://plugins.dprint.dev/markdown-0.17.8.wasm", - "https://plugins.dprint.dev/toml-0.6.2.wasm", + "https://plugins.dprint.dev/toml-0.6.3.wasm", "https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0", "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.0.wasm" ] diff --git a/.github/workflows/cargo_publish.yml b/.github/workflows/cargo_publish.yml index 45f075b579..3af97f4662 100644 --- a/.github/workflows/cargo_publish.yml +++ b/.github/workflows/cargo_publish.yml @@ -2,10 +2,15 @@ name: cargo_publish on: workflow_dispatch +# Ensures only one publish is running at a time +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + jobs: build: name: cargo publish - runs-on: ubuntu-20.04-xl + runs-on: ubuntu-24.04-xl timeout-minutes: 90 env: @@ -28,16 +33,10 @@ jobs: - uses: dsherret/rust-toolchain-file@v1 - name: Install deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: deno-version: v1.x - - name: Install protoc - uses: arduino/setup-protoc@v3 - with: - version: '21.12' - repo-token: '${{ secrets.GITHUB_TOKEN }}' - - name: Publish env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/ci.generate.ts b/.github/workflows/ci.generate.ts index e9fcacc823..5ed02d3cde 100755 --- a/.github/workflows/ci.generate.ts +++ b/.github/workflows/ci.generate.ts @@ -5,16 +5,16 @@ import { stringify } from "jsr:@std/yaml@^0.221/stringify"; // Bump this number when you want to purge the cache. // Note: the tools/release/01_bump_crate_versions.ts script will update this version // automatically via regex, so ensure that this line maintains this format. -const cacheVersion = 15; +const cacheVersion = 25; -const ubuntuX86Runner = "ubuntu-22.04"; -const ubuntuX86XlRunner = "ubuntu-22.04-xl"; +const ubuntuX86Runner = "ubuntu-24.04"; +const ubuntuX86XlRunner = "ubuntu-24.04-xl"; const ubuntuARMRunner = "ubicloud-standard-16-arm"; const windowsX86Runner = "windows-2022"; const windowsX86XlRunner = "windows-2022-xl"; const macosX86Runner = "macos-13"; const macosArmRunner = "macos-14"; -const macosArmXlRunner = "macos-14-xlarge"; +const selfHostedMacosArmRunner = "self-hosted"; const Runners = { linuxX86: { @@ -39,15 +39,10 @@ const Runners = { runner: macosX86Runner, }, macosArm: { - os: "macos", - arch: "aarch64", - runner: macosArmRunner, - }, - macosArmXl: { os: "macos", arch: "aarch64", runner: - `\${{ github.repository == 'denoland/deno' && '${macosArmXlRunner}' || '${macosArmRunner}' }}`, + `\${{ github.repository == 'denoland/deno' && startsWith(github.ref, 'refs/tags/') && '${selfHostedMacosArmRunner}' || '${macosArmRunner}' }}`, }, windowsX86: { os: "windows", @@ -66,7 +61,7 @@ const prCacheKeyPrefix = `${cacheVersion}-cargo-target-\${{ matrix.os }}-\${{ matrix.arch }}-\${{ matrix.profile }}-\${{ matrix.job }}-`; // Note that you may need to add more version to the `apt-get remove` line below if you change this -const llvmVersion = 18; +const llvmVersion = 19; const installPkgsCommand = `sudo apt-get install --no-install-recommends clang-${llvmVersion} lld-${llvmVersion} clang-tools-${llvmVersion} clang-format-${llvmVersion} clang-tidy-${llvmVersion}`; const sysRootStep = { @@ -78,7 +73,7 @@ export DEBIAN_FRONTEND=noninteractive sudo apt-get -qq remove --purge -y man-db > /dev/null 2> /dev/null # Remove older clang before we install sudo apt-get -qq remove \ - 'clang-12*' 'clang-13*' 'clang-14*' 'clang-15*' 'clang-16*' 'llvm-12*' 'llvm-13*' 'llvm-14*' 'llvm-15*' 'llvm-16*' 'lld-12*' 'lld-13*' 'lld-14*' 'lld-15*' 'lld-16*' > /dev/null 2> /dev/null + 'clang-12*' 'clang-13*' 'clang-14*' 'clang-15*' 'clang-16*' 'clang-17*' 'clang-18*' 'llvm-12*' 'llvm-13*' 'llvm-14*' 'llvm-15*' 'llvm-16*' 'lld-12*' 'lld-13*' 'lld-14*' 'lld-15*' 'lld-16*' 'lld-17*' 'lld-18*' > /dev/null 2> /dev/null # Install clang-XXX, lld-XXX, and debootstrap. echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-${llvmVersion} main" | @@ -93,7 +88,7 @@ ${installPkgsCommand} || echo 'Failed. Trying again.' && sudo apt-get clean && s (yes '' | sudo update-alternatives --force --all) > /dev/null 2> /dev/null || true echo "Decompressing sysroot..." -wget -q https://github.com/denoland/deno_sysroot_build/releases/download/sysroot-20240528/sysroot-\`uname -m\`.tar.xz -O /tmp/sysroot.tar.xz +wget -q https://github.com/denoland/deno_sysroot_build/releases/download/sysroot-20241030/sysroot-\`uname -m\`.tar.xz -O /tmp/sysroot.tar.xz cd / xzcat /tmp/sysroot.tar.xz | sudo tar -x sudo mount --rbind /dev /sysroot/dev @@ -198,14 +193,9 @@ const installNodeStep = { uses: "actions/setup-node@v4", with: { "node-version": 18 }, }; -const installProtocStep = { - name: "Install protoc", - uses: "arduino/setup-protoc@v3", - with: { "version": "21.12", "repo-token": "${{ secrets.GITHUB_TOKEN }}" }, -}; const installDenoStep = { name: "Install Deno", - uses: "denoland/setup-deno@v1", + uses: "denoland/setup-deno@v2", with: { "deno-version": "v1.x" }, }; @@ -385,7 +375,7 @@ const ci = { job: "test", profile: "debug", }, { - ...Runners.macosArmXl, + ...Runners.macosArm, job: "test", profile: "release", skip_pr: true, @@ -501,7 +491,6 @@ const ci = { if: "matrix.job == 'bench' || matrix.job == 'test'", ...installNodeStep, }, - installProtocStep, { if: [ "matrix.profile == 'release' &&", @@ -765,8 +754,10 @@ const ci = { run: [ "cd target/release", "zip -r deno-${{ matrix.arch }}-unknown-linux-gnu.zip deno", + "shasum -a 256 deno-${{ matrix.arch }}-unknown-linux-gnu.zip > deno-${{ matrix.arch }}-unknown-linux-gnu.zip.sha256sum", "strip denort", "zip -r denort-${{ matrix.arch }}-unknown-linux-gnu.zip denort", + "shasum -a 256 denort-${{ matrix.arch }}-unknown-linux-gnu.zip > denort-${{ matrix.arch }}-unknown-linux-gnu.zip.sha256sum", "./deno types > lib.deno.d.ts", ].join("\n"), }, @@ -791,8 +782,10 @@ const ci = { "--entitlements-xml-file=cli/entitlements.plist", "cd target/release", "zip -r deno-${{ matrix.arch }}-apple-darwin.zip deno", + "shasum -a 256 deno-${{ matrix.arch }}-apple-darwin.zip > deno-${{ matrix.arch }}-apple-darwin.zip.sha256sum", "strip denort", "zip -r denort-${{ matrix.arch }}-apple-darwin.zip denort", + "shasum -a 256 denort-${{ matrix.arch }}-apple-darwin.zip > denort-${{ matrix.arch }}-apple-darwin.zip.sha256sum", ] .join("\n"), }, @@ -807,7 +800,9 @@ const ci = { shell: "pwsh", run: [ "Compress-Archive -CompressionLevel Optimal -Force -Path target/release/deno.exe -DestinationPath target/release/deno-${{ matrix.arch }}-pc-windows-msvc.zip", + "Get-FileHash target/release/deno-${{ matrix.arch }}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/deno-${{ matrix.arch }}-pc-windows-msvc.zip.sha256sum", "Compress-Archive -CompressionLevel Optimal -Force -Path target/release/denort.exe -DestinationPath target/release/denort-${{ matrix.arch }}-pc-windows-msvc.zip", + "Get-FileHash target/release/denort-${{ matrix.arch }}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/denort-${{ matrix.arch }}-pc-windows-msvc.zip.sha256sum", ].join("\n"), }, { @@ -820,6 +815,7 @@ const ci = { ].join("\n"), run: [ 'gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.zip gs://dl.deno.land/canary/$(git rev-parse HEAD)/', + 'gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.sha256sum gs://dl.deno.land/canary/$(git rev-parse HEAD)/', "echo ${{ github.sha }} > canary-latest.txt", 'gsutil -h "Cache-Control: no-cache" cp canary-latest.txt gs://dl.deno.land/canary-$(rustc -vV | sed -n "s|host: ||p")-latest.txt', ].join("\n"), @@ -1001,8 +997,10 @@ const ci = { "github.repository == 'denoland/deno' &&", "startsWith(github.ref, 'refs/tags/')", ].join("\n"), - run: + run: [ 'gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.zip gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/', + 'gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.sha256sum gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/', + ].join("\n"), }, { name: "Upload release to dl.deno.land (windows)", @@ -1016,8 +1014,10 @@ const ci = { env: { CLOUDSDK_PYTHON: "${{env.pythonLocation}}\\python.exe", }, - run: + run: [ 'gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.zip gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/', + 'gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.sha256sum gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/', + ].join("\n"), }, { name: "Create release notes", @@ -1047,15 +1047,25 @@ const ci = { with: { files: [ "target/release/deno-x86_64-pc-windows-msvc.zip", + "target/release/deno-x86_64-pc-windows-msvc.zip.sha256sum", "target/release/denort-x86_64-pc-windows-msvc.zip", + "target/release/denort-x86_64-pc-windows-msvc.zip.sha256sum", "target/release/deno-x86_64-unknown-linux-gnu.zip", + "target/release/deno-x86_64-unknown-linux-gnu.zip.sha256sum", "target/release/denort-x86_64-unknown-linux-gnu.zip", + "target/release/denort-x86_64-unknown-linux-gnu.zip.sha256sum", "target/release/deno-x86_64-apple-darwin.zip", + "target/release/deno-x86_64-apple-darwin.zip.sha256sum", "target/release/denort-x86_64-apple-darwin.zip", + "target/release/denort-x86_64-apple-darwin.zip.sha256sum", "target/release/deno-aarch64-unknown-linux-gnu.zip", + "target/release/deno-aarch64-unknown-linux-gnu.zip.sha256sum", "target/release/denort-aarch64-unknown-linux-gnu.zip", + "target/release/denort-aarch64-unknown-linux-gnu.zip.sha256sum", "target/release/deno-aarch64-apple-darwin.zip", + "target/release/deno-aarch64-apple-darwin.zip.sha256sum", "target/release/denort-aarch64-apple-darwin.zip", + "target/release/denort-aarch64-apple-darwin.zip.sha256sum", "target/release/deno_src.tar.gz", "target/release/lib.deno.d.ts", ].join("\n"), @@ -1074,6 +1084,7 @@ const ci = { "./target", "!./target/*/gn_out", "!./target/*/*.zip", + "!./target/*/*.sha256sum", "!./target/*/*.tar.gz", ].join("\n"), key: prCacheKeyPrefix + "${{ github.sha }}", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 035a4244fc..39a3afe769 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,18 +62,18 @@ jobs: profile: debug - os: macos arch: x86_64 - runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'')) && ''ubuntu-22.04'' || ''macos-13'' }}' + runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'')) && ''ubuntu-24.04'' || ''macos-13'' }}' job: test profile: release skip: '${{ !contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'') }}' - os: macos arch: aarch64 - runner: macos-14 + runner: '${{ github.repository == ''denoland/deno'' && startsWith(github.ref, ''refs/tags/'') && ''self-hosted'' || ''macos-14'' }}' job: test profile: debug - os: macos arch: aarch64 - runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'')) && ''ubuntu-22.04'' || github.repository == ''denoland/deno'' && ''macos-14-xlarge'' || ''macos-14'' }}' + runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'')) && ''ubuntu-24.04'' || github.repository == ''denoland/deno'' && startsWith(github.ref, ''refs/tags/'') && ''self-hosted'' || ''macos-14'' }}' job: test profile: release skip: '${{ !contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'') }}' @@ -84,33 +84,33 @@ jobs: profile: debug - os: windows arch: x86_64 - runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'')) && ''ubuntu-22.04'' || github.repository == ''denoland/deno'' && ''windows-2022-xl'' || ''windows-2022'' }}' + runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'')) && ''ubuntu-24.04'' || github.repository == ''denoland/deno'' && ''windows-2022-xl'' || ''windows-2022'' }}' job: test profile: release skip: '${{ !contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'') }}' - os: linux arch: x86_64 - runner: '${{ github.repository == ''denoland/deno'' && ''ubuntu-22.04-xl'' || ''ubuntu-22.04'' }}' + runner: '${{ github.repository == ''denoland/deno'' && ''ubuntu-24.04-xl'' || ''ubuntu-24.04'' }}' job: test profile: release use_sysroot: true wpt: '${{ !startsWith(github.ref, ''refs/tags/'') }}' - os: linux arch: x86_64 - runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'' && !contains(github.event.pull_request.labels.*.name, ''ci-bench''))) && ''ubuntu-22.04'' || github.repository == ''denoland/deno'' && ''ubuntu-22.04-xl'' || ''ubuntu-22.04'' }}' + runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'' && !contains(github.event.pull_request.labels.*.name, ''ci-bench''))) && ''ubuntu-24.04'' || github.repository == ''denoland/deno'' && ''ubuntu-24.04-xl'' || ''ubuntu-24.04'' }}' job: bench profile: release use_sysroot: true skip: '${{ !contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'' && !contains(github.event.pull_request.labels.*.name, ''ci-bench'')) }}' - os: linux arch: x86_64 - runner: ubuntu-22.04 + runner: ubuntu-24.04 job: test profile: debug use_sysroot: true - os: linux arch: x86_64 - runner: ubuntu-22.04 + runner: ubuntu-24.04 job: lint profile: debug - os: linux @@ -178,7 +178,7 @@ jobs: if: '!(matrix.skip)' - if: '!(matrix.skip) && (matrix.job == ''lint'' || matrix.job == ''test'' || matrix.job == ''bench'')' name: Install Deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: deno-version: v1.x - name: Install Python @@ -199,12 +199,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 18 - - name: Install protoc - uses: arduino/setup-protoc@v3 - with: - version: '21.12' - repo-token: '${{ secrets.GITHUB_TOKEN }}' - if: '!(matrix.skip)' - if: |- !(matrix.skip) && (matrix.profile == 'release' && matrix.job == 'test' && @@ -258,22 +252,22 @@ jobs: # to complete. sudo apt-get -qq remove --purge -y man-db > /dev/null 2> /dev/null # Remove older clang before we install - sudo apt-get -qq remove 'clang-12*' 'clang-13*' 'clang-14*' 'clang-15*' 'clang-16*' 'llvm-12*' 'llvm-13*' 'llvm-14*' 'llvm-15*' 'llvm-16*' 'lld-12*' 'lld-13*' 'lld-14*' 'lld-15*' 'lld-16*' > /dev/null 2> /dev/null + sudo apt-get -qq remove 'clang-12*' 'clang-13*' 'clang-14*' 'clang-15*' 'clang-16*' 'clang-17*' 'clang-18*' 'llvm-12*' 'llvm-13*' 'llvm-14*' 'llvm-15*' 'llvm-16*' 'lld-12*' 'lld-13*' 'lld-14*' 'lld-15*' 'lld-16*' 'lld-17*' 'lld-18*' > /dev/null 2> /dev/null # Install clang-XXX, lld-XXX, and debootstrap. - echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" | - sudo dd of=/etc/apt/sources.list.d/llvm-toolchain-jammy-18.list + echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-19 main" | + sudo dd of=/etc/apt/sources.list.d/llvm-toolchain-jammy-19.list curl https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor | sudo dd of=/etc/apt/trusted.gpg.d/llvm-snapshot.gpg sudo apt-get update # this was unreliable sometimes, so try again if it fails - sudo apt-get install --no-install-recommends clang-18 lld-18 clang-tools-18 clang-format-18 clang-tidy-18 || echo 'Failed. Trying again.' && sudo apt-get clean && sudo apt-get update && sudo apt-get install --no-install-recommends clang-18 lld-18 clang-tools-18 clang-format-18 clang-tidy-18 + sudo apt-get install --no-install-recommends clang-19 lld-19 clang-tools-19 clang-format-19 clang-tidy-19 || echo 'Failed. Trying again.' && sudo apt-get clean && sudo apt-get update && sudo apt-get install --no-install-recommends clang-19 lld-19 clang-tools-19 clang-format-19 clang-tidy-19 # Fix alternatives (yes '' | sudo update-alternatives --force --all) > /dev/null 2> /dev/null || true echo "Decompressing sysroot..." - wget -q https://github.com/denoland/deno_sysroot_build/releases/download/sysroot-20240528/sysroot-`uname -m`.tar.xz -O /tmp/sysroot.tar.xz + wget -q https://github.com/denoland/deno_sysroot_build/releases/download/sysroot-20241030/sysroot-`uname -m`.tar.xz -O /tmp/sysroot.tar.xz cd / xzcat /tmp/sysroot.tar.xz | sudo tar -x sudo mount --rbind /dev /sysroot/dev @@ -305,8 +299,8 @@ jobs: CARGO_PROFILE_RELEASE_LTO=false RUSTFLAGS<<__1 -C linker-plugin-lto=true - -C linker=clang-18 - -C link-arg=-fuse-ld=lld-18 + -C linker=clang-19 + -C link-arg=-fuse-ld=lld-19 -C link-arg=-ldl -C link-arg=-Wl,--allow-shlib-undefined -C link-arg=-Wl,--thinlto-cache-dir=$(pwd)/target/release/lto-cache @@ -316,8 +310,8 @@ jobs: __1 RUSTDOCFLAGS<<__1 -C linker-plugin-lto=true - -C linker=clang-18 - -C link-arg=-fuse-ld=lld-18 + -C linker=clang-19 + -C link-arg=-fuse-ld=lld-19 -C link-arg=-ldl -C link-arg=-Wl,--allow-shlib-undefined -C link-arg=-Wl,--thinlto-cache-dir=$(pwd)/target/release/lto-cache @@ -325,7 +319,7 @@ jobs: --cfg tokio_unstable $RUSTFLAGS __1 - CC=/usr/bin/clang-18 + CC=/usr/bin/clang-19 CFLAGS=-flto=thin $CFLAGS " > $GITHUB_ENV - name: Remove macOS cURL --ipv4 flag @@ -367,8 +361,8 @@ jobs: path: |- ~/.cargo/registry/index ~/.cargo/registry/cache - key: '15-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles(''Cargo.lock'') }}' - restore-keys: '15-cargo-home-${{ matrix.os }}-${{ matrix.arch }}' + key: '25-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles(''Cargo.lock'') }}' + restore-keys: '25-cargo-home-${{ matrix.os }}-${{ matrix.arch }}' if: '!(matrix.skip)' - name: Restore cache build output (PR) uses: actions/cache/restore@v4 @@ -381,7 +375,7 @@ jobs: !./target/*/*.zip !./target/*/*.tar.gz key: never_saved - restore-keys: '15-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-' + restore-keys: '25-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-' - name: Apply and update mtime cache if: '!(matrix.skip) && (!startsWith(github.ref, ''refs/tags/''))' uses: ./.github/mtime_cache @@ -449,8 +443,10 @@ jobs: run: |- cd target/release zip -r deno-${{ matrix.arch }}-unknown-linux-gnu.zip deno + shasum -a 256 deno-${{ matrix.arch }}-unknown-linux-gnu.zip > deno-${{ matrix.arch }}-unknown-linux-gnu.zip.sha256sum strip denort zip -r denort-${{ matrix.arch }}-unknown-linux-gnu.zip denort + shasum -a 256 denort-${{ matrix.arch }}-unknown-linux-gnu.zip > denort-${{ matrix.arch }}-unknown-linux-gnu.zip.sha256sum ./deno types > lib.deno.d.ts - name: Pre-release (mac) if: |- @@ -466,8 +462,10 @@ jobs: rcodesign sign target/release/deno --code-signature-flags=runtime --p12-password="$APPLE_CODESIGN_PASSWORD" --p12-file=<(echo $APPLE_CODESIGN_KEY | base64 -d) --entitlements-xml-file=cli/entitlements.plist cd target/release zip -r deno-${{ matrix.arch }}-apple-darwin.zip deno + shasum -a 256 deno-${{ matrix.arch }}-apple-darwin.zip > deno-${{ matrix.arch }}-apple-darwin.zip.sha256sum strip denort zip -r denort-${{ matrix.arch }}-apple-darwin.zip denort + shasum -a 256 denort-${{ matrix.arch }}-apple-darwin.zip > denort-${{ matrix.arch }}-apple-darwin.zip.sha256sum - name: Pre-release (windows) if: |- !(matrix.skip) && (matrix.os == 'windows' && @@ -477,7 +475,9 @@ jobs: shell: pwsh run: |- Compress-Archive -CompressionLevel Optimal -Force -Path target/release/deno.exe -DestinationPath target/release/deno-${{ matrix.arch }}-pc-windows-msvc.zip + Get-FileHash target/release/deno-${{ matrix.arch }}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/deno-${{ matrix.arch }}-pc-windows-msvc.zip.sha256sum Compress-Archive -CompressionLevel Optimal -Force -Path target/release/denort.exe -DestinationPath target/release/denort-${{ matrix.arch }}-pc-windows-msvc.zip + Get-FileHash target/release/denort-${{ matrix.arch }}-pc-windows-msvc.zip -Algorithm SHA256 | Format-List > target/release/denort-${{ matrix.arch }}-pc-windows-msvc.zip.sha256sum - name: Upload canary to dl.deno.land if: |- !(matrix.skip) && (matrix.job == 'test' && @@ -486,6 +486,7 @@ jobs: github.ref == 'refs/heads/main') run: |- gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.zip gs://dl.deno.land/canary/$(git rev-parse HEAD)/ + gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.sha256sum gs://dl.deno.land/canary/$(git rev-parse HEAD)/ echo ${{ github.sha }} > canary-latest.txt gsutil -h "Cache-Control: no-cache" cp canary-latest.txt gs://dl.deno.land/canary-$(rustc -vV | sed -n "s|host: ||p")-latest.txt - name: Autobahn testsuite @@ -615,7 +616,9 @@ jobs: matrix.profile == 'release' && github.repository == 'denoland/deno' && startsWith(github.ref, 'refs/tags/')) - run: 'gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.zip gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/' + run: |- + gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.zip gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/ + gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.sha256sum gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/ - name: Upload release to dl.deno.land (windows) if: |- !(matrix.skip) && (matrix.os == 'windows' && @@ -625,7 +628,9 @@ jobs: startsWith(github.ref, 'refs/tags/')) env: CLOUDSDK_PYTHON: '${{env.pythonLocation}}\python.exe' - run: 'gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.zip gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/' + run: |- + gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.zip gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/ + gsutil -h "Cache-Control: public, max-age=3600" cp ./target/release/*.sha256sum gs://dl.deno.land/release/${GITHUB_REF#refs/*/}/ - name: Create release notes if: |- !(matrix.skip) && (matrix.job == 'test' && @@ -647,15 +652,25 @@ jobs: with: files: |- target/release/deno-x86_64-pc-windows-msvc.zip + target/release/deno-x86_64-pc-windows-msvc.zip.sha256sum target/release/denort-x86_64-pc-windows-msvc.zip + target/release/denort-x86_64-pc-windows-msvc.zip.sha256sum target/release/deno-x86_64-unknown-linux-gnu.zip + target/release/deno-x86_64-unknown-linux-gnu.zip.sha256sum target/release/denort-x86_64-unknown-linux-gnu.zip + target/release/denort-x86_64-unknown-linux-gnu.zip.sha256sum target/release/deno-x86_64-apple-darwin.zip + target/release/deno-x86_64-apple-darwin.zip.sha256sum target/release/denort-x86_64-apple-darwin.zip + target/release/denort-x86_64-apple-darwin.zip.sha256sum target/release/deno-aarch64-unknown-linux-gnu.zip + target/release/deno-aarch64-unknown-linux-gnu.zip.sha256sum target/release/denort-aarch64-unknown-linux-gnu.zip + target/release/denort-aarch64-unknown-linux-gnu.zip.sha256sum target/release/deno-aarch64-apple-darwin.zip + target/release/deno-aarch64-apple-darwin.zip.sha256sum target/release/denort-aarch64-apple-darwin.zip + target/release/denort-aarch64-apple-darwin.zip.sha256sum target/release/deno_src.tar.gz target/release/lib.deno.d.ts body_path: target/release/release-notes.md @@ -668,11 +683,12 @@ jobs: ./target !./target/*/gn_out !./target/*/*.zip + !./target/*/*.sha256sum !./target/*/*.tar.gz - key: '15-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-${{ github.sha }}' + key: '25-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-${{ github.sha }}' publish-canary: name: publish canary - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: - build if: github.repository == 'denoland/deno' && github.ref == 'refs/heads/main' diff --git a/.github/workflows/post_publish.yml b/.github/workflows/post_publish.yml index c0db0906df..dd80b3637a 100644 --- a/.github/workflows/post_publish.yml +++ b/.github/workflows/post_publish.yml @@ -7,7 +7,7 @@ on: jobs: update-dl-version: name: update dl.deno.land version - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: github.repository == 'denoland/deno' steps: - name: Authenticate with Google Cloud diff --git a/.github/workflows/promote_to_release.yml b/.github/workflows/promote_to_release.yml index 3dc15dc730..79fefa6d6c 100644 --- a/.github/workflows/promote_to_release.yml +++ b/.github/workflows/promote_to_release.yml @@ -40,7 +40,7 @@ jobs: project_id: denoland - name: Install deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: deno-version: v1.x diff --git a/.github/workflows/start_release.yml b/.github/workflows/start_release.yml index 392551afbe..40a44bb61a 100644 --- a/.github/workflows/start_release.yml +++ b/.github/workflows/start_release.yml @@ -16,7 +16,7 @@ on: jobs: build: name: start release - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 30 env: @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v4 - name: Install deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: deno-version: v1.x diff --git a/.github/workflows/version_bump.yml b/.github/workflows/version_bump.yml index 733abbb024..9038fe0d22 100644 --- a/.github/workflows/version_bump.yml +++ b/.github/workflows/version_bump.yml @@ -16,7 +16,7 @@ on: jobs: build: name: version bump - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 90 env: @@ -39,7 +39,7 @@ jobs: - uses: dsherret/rust-toolchain-file@v1 - name: Install deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: deno-version: v1.x diff --git a/.github/workflows/wpt_epoch.yml b/.github/workflows/wpt_epoch.yml index c3c6277b93..1d86ed5557 100644 --- a/.github/workflows/wpt_epoch.yml +++ b/.github/workflows/wpt_epoch.yml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: deno-version: [v1.x, canary] - os: [ubuntu-22.04-xl] + os: [ubuntu-24.04-xl] steps: - name: Clone repository @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup Deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: deno-version: ${{ matrix.deno-version }} diff --git a/Cargo.lock b/Cargo.lock index 6ef9cd2e62..87265c02d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,19 +128,6 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" -[[package]] -name = "ammonia" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e6d1c7838db705c9b756557ee27c384ce695a1c51a6fe528784cb1c6840170" -dependencies = [ - "html5ever", - "maplit", - "once_cell", - "tendril", - "url", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -360,6 +347,53 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -450,25 +484,22 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.4" +version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.10.5", - "lazy_static", - "lazycell", + "itertools 0.13.0", "log", - "prettyplease 0.2.17", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash 1.1.0", "shlex", "syn 2.0.72", - "which 4.4.2", ] [[package]] @@ -631,12 +662,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.10" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" +checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -688,7 +720,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.3", + "libloading 0.8.5", ] [[package]] @@ -757,11 +789,13 @@ dependencies = [ "fastwebsockets", "file_test_runner", "flaky_test", + "hickory-client", + "hickory-server", "http 1.1.0", "http-body-util", "hyper 1.4.1", "hyper-util", - "nix 0.26.2", + "nix", "once_cell", "os_pipe", "pretty_assertions", @@ -770,8 +804,6 @@ dependencies = [ "serde", "test_server", "tokio", - "trust-dns-client", - "trust-dns-server", "url", "uuid", "zeromq", @@ -840,9 +872,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "comrak" -version = "0.26.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ab67843c57df5a4ee29d610740828dbc928cc64ecf0f2a1d5cd0e98e107a9" +checksum = "c93ab3577cca16b4a1d80a88c2e0cd8b6e969e51696f0bbb0d1dcb0157109832" dependencies = [ "caseless", "derive_builder", @@ -1040,7 +1072,7 @@ dependencies = [ "cpufeatures", "curve25519-dalek-derive", "digest", - "fiat-crypto", + "fiat-crypto 0.2.7", "rustc_version 0.4.0", "subtle", "zeroize", @@ -1064,7 +1096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ "bitflags 2.6.0", - "libloading 0.8.3", + "libloading 0.8.5", "winapi", ] @@ -1110,7 +1142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1146,11 +1178,10 @@ dependencies = [ [[package]] name = "deno" -version = "2.0.0-rc.7" +version = "2.0.6" dependencies = [ "anstream", "async-trait", - "base32", "base64 0.21.7", "bincode", "bytes", @@ -1174,11 +1205,14 @@ dependencies = [ "deno_lockfile", "deno_npm", "deno_package_json", + "deno_path_util", + "deno_resolver", "deno_runtime", "deno_semver", "deno_task_shell", "deno_terminal 0.2.0", "deno_tower_lsp", + "dhat", "dissimilar", "dotenvy", "dprint-plugin-json", @@ -1186,7 +1220,6 @@ dependencies = [ "dprint-plugin-markdown", "dprint-plugin-typescript", "env_logger", - "eszip", "fancy-regex", "faster-hex", "flate2", @@ -1198,7 +1231,7 @@ dependencies = [ "http-body-util", "hyper-util", "import_map", - "indexmap", + "indexmap 2.3.0", "jsonc-parser", "junction", "lazy-regex", @@ -1211,8 +1244,7 @@ dependencies = [ "markup_fmt", "memmem", "monch", - "napi_sym", - "nix 0.26.2", + "nix", "node_resolver", "notify", "once_cell", @@ -1220,7 +1252,7 @@ dependencies = [ "p256", "pathdiff", "percent-encoding", - "phf 0.11.2", + "phf", "pretty_assertions", "pretty_yaml", "quick-junit", @@ -1253,7 +1285,6 @@ dependencies = [ "which 4.4.2", "winapi", "winres", - "yoke", "zeromq", "zip", "zstd", @@ -1272,9 +1303,9 @@ dependencies = [ [[package]] name = "deno_ast" -version = "0.42.0" +version = "0.43.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b08d11d9e4086b00d3428650e31153cf5896586411763cb88a6423ce5b18791" +checksum = "48d00b724e06d2081a141ec1155756a0b465d413d8e2a7515221f61d482eb2ee" dependencies = [ "base64 0.21.7", "deno_media_type", @@ -1316,7 +1347,7 @@ dependencies = [ [[package]] name = "deno_bench_util" -version = "0.162.0" +version = "0.171.0" dependencies = [ "bencher", "deno_core", @@ -1325,34 +1356,38 @@ dependencies = [ [[package]] name = "deno_broadcast_channel" -version = "0.162.0" +version = "0.171.0" dependencies = [ "async-trait", "deno_core", + "thiserror", "tokio", "uuid", ] [[package]] name = "deno_cache" -version = "0.100.0" +version = "0.109.0" dependencies = [ "async-trait", "deno_core", "rusqlite", "serde", "sha2", + "thiserror", "tokio", ] [[package]] name = "deno_cache_dir" -version = "0.11.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df43311cb7703fa3242c282823a850e4c8d0c06b9527d8209b55bd695452ea5" +checksum = "08c1f52170cd7715f8006da54cde1444863a0d6fbd9c11d037a737db2dec8e22" dependencies = [ + "base32", "deno_media_type", - "indexmap", + "deno_path_util", + "indexmap 2.3.0", "log", "once_cell", "parking_lot", @@ -1365,31 +1400,33 @@ dependencies = [ [[package]] name = "deno_canvas" -version = "0.37.0" +version = "0.46.0" dependencies = [ "deno_core", "deno_webgpu", "image", "serde", + "thiserror", ] [[package]] name = "deno_config" -version = "0.35.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105864a9e0a7fbc22f1106784b2d263f402f157be1c3e1a9905f53d182700c9f" +checksum = "966825073480a6ac7e01977a3879d13edc8d6ea2d65ea164b37156a5fb206e9a" dependencies = [ "anyhow", "deno_package_json", + "deno_path_util", "deno_semver", "glob", "ignore", "import_map", - "indexmap", + "indexmap 2.3.0", "jsonc-parser", "log", "percent-encoding", - "phf 0.11.2", + "phf", "serde", "serde_json", "thiserror", @@ -1398,16 +1435,16 @@ dependencies = [ [[package]] name = "deno_console" -version = "0.168.0" +version = "0.177.0" dependencies = [ "deno_core", ] [[package]] name = "deno_core" -version = "0.311.0" +version = "0.319.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e09bd55da542fa1fde753aff617c355b5d782e763ab2a19e4371a56d7844cac" +checksum = "e9dbb841f9850534320d8927dce53ca8d64bafbab5576c2a98f03f9e08534215" dependencies = [ "anyhow", "bincode", @@ -1420,7 +1457,7 @@ dependencies = [ "deno_unsync", "futures", "libc", - "memoffset 0.9.1", + "memoffset", "parking_lot", "percent-encoding", "pin-project", @@ -1437,25 +1474,26 @@ dependencies = [ [[package]] name = "deno_core_icudata" -version = "0.0.73" +version = "0.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13951ea98c0a4c372f162d669193b4c9d991512de9f2381dd161027f34b26b1" +checksum = "fe4dccb6147bb3f3ba0c7a48e993bfeb999d2c2e47a81badee80e2b370c8d695" [[package]] name = "deno_cron" -version = "0.48.0" +version = "0.57.0" dependencies = [ "anyhow", "async-trait", "chrono", "deno_core", "saffron", + "thiserror", "tokio", ] [[package]] name = "deno_crypto" -version = "0.182.0" +version = "0.191.0" dependencies = [ "aes", "aes-gcm", @@ -1467,6 +1505,7 @@ dependencies = [ "curve25519-dalek", "deno_core", "deno_web", + "ed448-goldilocks", "elliptic-curve", "num-traits", "once_cell", @@ -1483,17 +1522,18 @@ dependencies = [ "sha2", "signature", "spki", + "thiserror", + "tokio", "uuid", "x25519-dalek", ] [[package]] name = "deno_doc" -version = "0.150.0" +version = "0.156.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762829006b555837691b7016828eb1f93acf0a4ff344357b946898ea5b5610d" +checksum = "2585b98d6ad76dae30bf2d7b6d71b8363cae041158b8780d14a2f4fe17590a61" dependencies = [ - "ammonia", "anyhow", "cfg-if", "comrak", @@ -1503,7 +1543,7 @@ dependencies = [ "handlebars", "html-escape", "import_map", - "indexmap", + "indexmap 2.3.0", "itoa", "lazy_static", "regex", @@ -1515,7 +1555,7 @@ dependencies = [ [[package]] name = "deno_fetch" -version = "0.192.0" +version = "0.201.0" dependencies = [ "base64 0.21.7", "bytes", @@ -1536,6 +1576,7 @@ dependencies = [ "rustls-webpki", "serde", "serde_json", + "thiserror", "tokio", "tokio-rustls", "tokio-socks", @@ -1547,7 +1588,7 @@ dependencies = [ [[package]] name = "deno_ffi" -version = "0.155.0" +version = "0.164.0" dependencies = [ "deno_core", "deno_permissions", @@ -1556,37 +1597,42 @@ dependencies = [ "libffi", "libffi-sys", "log", + "num-bigint", "serde", "serde-value", "serde_json", + "thiserror", + "tokio", "winapi", ] [[package]] name = "deno_fs" -version = "0.78.0" +version = "0.87.0" dependencies = [ "async-trait", "base32", "deno_core", "deno_io", + "deno_path_util", "deno_permissions", "filetime", "junction", "libc", - "nix 0.26.2", + "nix", "rand", "rayon", "serde", + "thiserror", "winapi", "windows-sys 0.52.0", ] [[package]] name = "deno_graph" -version = "0.82.3" +version = "0.84.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938ed2efa1dd9fdcceeebc169b2b7910506b8dacc992cfdcffd84aa6a3eb8db0" +checksum = "cd4f4a14aa069087be41c2998077b0453f0191747898f96e6343f700abfc2c18" dependencies = [ "anyhow", "async-trait", @@ -1597,7 +1643,7 @@ dependencies = [ "encoding_rs", "futures", "import_map", - "indexmap", + "indexmap 2.3.0", "log", "monch", "once_cell", @@ -1613,7 +1659,7 @@ dependencies = [ [[package]] name = "deno_http" -version = "0.166.0" +version = "0.175.0" dependencies = [ "async-compression", "async-trait", @@ -1638,7 +1684,7 @@ dependencies = [ "mime", "once_cell", "percent-encoding", - "phf 0.11.2", + "phf", "pin-project", "rand", "ring", @@ -1652,7 +1698,7 @@ dependencies = [ [[package]] name = "deno_io" -version = "0.78.0" +version = "0.87.0" dependencies = [ "async-trait", "deno_core", @@ -1673,7 +1719,7 @@ dependencies = [ [[package]] name = "deno_kv" -version = "0.76.0" +version = "0.85.0" dependencies = [ "anyhow", "async-trait", @@ -1682,6 +1728,7 @@ dependencies = [ "chrono", "deno_core", "deno_fetch", + "deno_path_util", "deno_permissions", "deno_tls", "denokv_proto", @@ -1697,14 +1744,15 @@ dependencies = [ "rand", "rusqlite", "serde", + "thiserror", "url", ] [[package]] name = "deno_lint" -version = "0.67.0" +version = "0.68.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871b60e32bfb6c110cbb9b0688dbf048f81e5d347fe4ce5a42239263de9dd938" +checksum = "bb994e6d1b18223df0a756c7948143b35682941d615edffef60d5b38822f38ac" dependencies = [ "anyhow", "deno_ast", @@ -1712,7 +1760,7 @@ dependencies = [ "if_chain", "log", "once_cell", - "phf 0.11.2", + "phf", "regex", "serde", "serde_json", @@ -1732,9 +1780,9 @@ dependencies = [ [[package]] name = "deno_media_type" -version = "0.1.4" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8978229b82552bf8457a0125aa20863f023619cfc21ebb007b1e571d68fd85b" +checksum = "7fcf552fbdedbe81c89705349d7d2485c7051382b000dfddbdbf7fc25931cf83" dependencies = [ "data-url", "serde", @@ -1743,11 +1791,17 @@ dependencies = [ [[package]] name = "deno_napi" -version = "0.99.0" +version = "0.108.0" dependencies = [ "deno_core", "deno_permissions", + "libc", "libloading 0.7.4", + "libuv-sys-lite", + "log", + "napi_sym", + "thiserror", + "windows-sys 0.52.0", ] [[package]] @@ -1765,23 +1819,24 @@ dependencies = [ [[package]] name = "deno_net" -version = "0.160.0" +version = "0.169.0" dependencies = [ "deno_core", "deno_permissions", "deno_tls", + "hickory-proto", + "hickory-resolver", "pin-project", "rustls-tokio-stream", "serde", "socket2", + "thiserror", "tokio", - "trust-dns-proto", - "trust-dns-resolver", ] [[package]] name = "deno_node" -version = "0.105.0" +version = "0.114.0" dependencies = [ "aead-gcm-stream", "aes", @@ -1800,6 +1855,7 @@ dependencies = [ "deno_media_type", "deno_net", "deno_package_json", + "deno_path_util", "deno_permissions", "deno_whoami", "der", @@ -1819,7 +1875,7 @@ dependencies = [ "hyper 1.4.1", "hyper-util", "idna 0.3.0", - "indexmap", + "indexmap 2.3.0", "ipnetwork", "k256", "lazy-regex", @@ -1870,9 +1926,9 @@ dependencies = [ [[package]] name = "deno_npm" -version = "0.25.2" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1809e2d77d8a06bc2800dc10c1d4acb664197e518e289a86e336411c1feba785" +checksum = "e6b4dc4a9f1cff63d5638e7d93042f24f46300d1cc77b86f3caaa699a7ddccf7" dependencies = [ "anyhow", "async-trait", @@ -1889,9 +1945,9 @@ dependencies = [ [[package]] name = "deno_ops" -version = "0.187.0" +version = "0.195.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e040fd4def8a67538fe38c9955fd970efc9f44284bd69d44f8992a456afd665d" +checksum = "797f348c38c07a5398bf790b280077c698e13fb49252f61ca6f6c5c616060292" dependencies = [ "proc-macro-rules", "proc-macro2", @@ -1904,23 +1960,35 @@ dependencies = [ [[package]] name = "deno_package_json" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38cf6ea5cc98ea7ad58b0e84593773bea03fc0431071a296017bed4151e3dc1d" +checksum = "6cbc4c4d3eb0960b58e8f43f9fc2d3f620fcac9a03cd85203e08db5b04e83c1f" dependencies = [ "deno_semver", - "indexmap", + "indexmap 2.3.0", "serde", "serde_json", "thiserror", "url", ] +[[package]] +name = "deno_path_util" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff25f6e08e7a0214bbacdd6f7195c7f1ebcd850c87a624e4ff06326b68b42d99" +dependencies = [ + "percent-encoding", + "thiserror", + "url", +] + [[package]] name = "deno_permissions" -version = "0.28.0" +version = "0.37.0" dependencies = [ "deno_core", + "deno_path_util", "deno_terminal 0.2.0", "fqdn", "libc", @@ -1928,14 +1996,35 @@ dependencies = [ "once_cell", "percent-encoding", "serde", + "thiserror", "which 4.4.2", "winapi", ] [[package]] -name = "deno_runtime" -version = "0.177.0" +name = "deno_resolver" +version = "0.9.0" dependencies = [ + "anyhow", + "base32", + "dashmap", + "deno_config", + "deno_media_type", + "deno_package_json", + "deno_path_util", + "deno_semver", + "node_resolver", + "test_server", + "thiserror", + "url", +] + +[[package]] +name = "deno_runtime" +version = "0.186.0" +dependencies = [ + "async-trait", + "color-print", "deno_ast", "deno_broadcast_channel", "deno_cache", @@ -1953,6 +2042,7 @@ dependencies = [ "deno_napi", "deno_net", "deno_node", + "deno_path_util", "deno_permissions", "deno_terminal 0.2.0", "deno_tls", @@ -1974,19 +2064,27 @@ dependencies = [ "libc", "log", "netif", - "nix 0.26.2", + "nix", "node_resolver", "notify", "ntapi", "once_cell", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", "percent-encoding", + "pin-project", "regex", "rustyline", + "same-file", "serde", "signal-hook", "signal-hook-registry", "tempfile", "test_server", + "thiserror", "tokio", "tokio-metrics", "twox-hash", @@ -1998,9 +2096,9 @@ dependencies = [ [[package]] name = "deno_semver" -version = "0.5.13" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6657fecb9ac6a7a71f552c95e8cc492466a75f5660224577e2226bcf30db9768" +checksum = "c957c6a57c38b7dde2315df0da0ec228911e56a74f185b108a488d0401841a67" dependencies = [ "monch", "once_cell", @@ -2011,9 +2109,9 @@ dependencies = [ [[package]] name = "deno_task_shell" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6413ffc1654cad015edb5c4ab574069acdc929a6efafed23bc947901bcff1a" +checksum = "4f444918f7102c1a5a143e9d57809e499fb4d365070519bf2e8bdb16d586af2a" dependencies = [ "anyhow", "futures", @@ -2048,7 +2146,7 @@ dependencies = [ [[package]] name = "deno_tls" -version = "0.155.0" +version = "0.164.0" dependencies = [ "deno_core", "deno_native_certs", @@ -2057,6 +2155,7 @@ dependencies = [ "rustls-tokio-stream", "rustls-webpki", "serde", + "thiserror", "tokio", "webpki-roots", ] @@ -2096,18 +2195,19 @@ dependencies = [ [[package]] name = "deno_url" -version = "0.168.0" +version = "0.177.0" dependencies = [ "deno_bench_util", "deno_console", "deno_core", "deno_webidl", + "thiserror", "urlpattern", ] [[package]] name = "deno_web" -version = "0.199.0" +version = "0.208.0" dependencies = [ "async-trait", "base64-simd 0.8.0", @@ -2122,17 +2222,19 @@ dependencies = [ "flate2", "futures", "serde", + "thiserror", "tokio", "uuid", ] [[package]] name = "deno_webgpu" -version = "0.135.0" +version = "0.144.0" dependencies = [ "deno_core", "raw-window-handle", "serde", + "thiserror", "tokio", "wgpu-core", "wgpu-types", @@ -2140,7 +2242,7 @@ dependencies = [ [[package]] name = "deno_webidl" -version = "0.168.0" +version = "0.177.0" dependencies = [ "deno_bench_util", "deno_core", @@ -2148,7 +2250,7 @@ dependencies = [ [[package]] name = "deno_websocket" -version = "0.173.0" +version = "0.182.0" dependencies = [ "bytes", "deno_core", @@ -2164,16 +2266,18 @@ dependencies = [ "once_cell", "rustls-tokio-stream", "serde", + "thiserror", "tokio", ] [[package]] name = "deno_webstorage" -version = "0.163.0" +version = "0.172.0" dependencies = [ "deno_core", "deno_web", "rusqlite", + "thiserror", ] [[package]] @@ -2188,9 +2292,9 @@ dependencies = [ [[package]] name = "denokv_proto" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f629c77d2bf59e2e2a07fd7b14bfffae352a3813fbdcb801e72205741fb7625c" +checksum = "f7ba1f99ed11a9c11e868a8521b1f71a7e1aba785d7f42ea9ecbdc01146c89ec" dependencies = [ "anyhow", "async-trait", @@ -2204,9 +2308,9 @@ dependencies = [ [[package]] name = "denokv_remote" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d57717b5123e8d1ec5f52973a67f98e3621274d362d18b245038967b402082df" +checksum = "08ed833073189e8f6d03155fe3b05a024e75e29d8a28a4c2e9ec3b5c925e727b" dependencies = [ "anyhow", "async-stream", @@ -2229,9 +2333,9 @@ dependencies = [ [[package]] name = "denokv_sqlite" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c4f5719e2bf698ec4f39fe29d91b62ff06a4b4a37ee481ffb8658d140fed986" +checksum = "9b790f01d1302d53a0c3cbd27de88a06b3abd64ec8ab8673924e490541c7c713" dependencies = [ "anyhow", "async-stream", @@ -2359,6 +2463,22 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" +[[package]] +name = "dhat" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" +dependencies = [ + "backtrace", + "lazy_static", + "mintex", + "parking_lot", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "thousands", +] + [[package]] name = "diff" version = "0.1.13" @@ -2461,8 +2581,8 @@ checksum = "f3ab0dd2bedc109d25f0d21afb09b7d329f6c6fa83b095daf31d2d967e091548" dependencies = [ "anyhow", "bumpalo", - "hashbrown", - "indexmap", + "hashbrown 0.14.5", + "indexmap 2.3.0", "rustc-hash 1.1.0", "serde", "unicode-width", @@ -2480,9 +2600,9 @@ dependencies = [ [[package]] name = "dprint-plugin-json" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19f4a9f2f548b2098b8ec597d7bb40af133b6e9a3187c1d3c4caa101b8c93c3" +checksum = "57f91e594559b450b7c5d6a0ba9f3f9fe951c1ea371168f7c95973da3fdbd85a" dependencies = [ "anyhow", "dprint-core", @@ -2494,9 +2614,9 @@ dependencies = [ [[package]] name = "dprint-plugin-jupyter" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c67b0e54b552a4775c221b44ed33be918c400bd8041d1f044f947fbb01025cc0" +checksum = "d0d20684e37b3824e2bc917cfcb14e2cdf88398eef507335d839cbd78172bfee" dependencies = [ "anyhow", "dprint-core", @@ -2522,9 +2642,9 @@ dependencies = [ [[package]] name = "dprint-plugin-typescript" -version = "0.93.0" +version = "0.93.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9308d98b923b7c0335c2ee1560199e3f2321b1be82803107b4ba4ed5dac46cc" +checksum = "3ff29fd136541e59d51946f0d2d353fefc886776f61a799ebfb5838b06cef13b" dependencies = [ "anyhow", "deno_ast", @@ -2552,15 +2672,6 @@ dependencies = [ "text_lines", ] -[[package]] -name = "drain" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d105028bd2b5dfcb33318fd79a445001ead36004dd8dffef1bdd7e493d8bc1e" -dependencies = [ - "tokio", -] - [[package]] name = "dsa" version = "0.6.3" @@ -2658,6 +2769,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed448-goldilocks" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06924531e9e90130842b012e447f85bdaf9161bc8a0f8092be8cb70b01ebe092" +dependencies = [ + "fiat-crypto 0.1.20", + "hex", + "subtle", + "zeroize", +] + [[package]] name = "editpe" version = "0.1.0" @@ -2665,7 +2788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48cede2bb1b07dd598d269f973792c43e0cd92686d3b452bd6e01d7a8eb01211" dependencies = [ "debug-ignore", - "indexmap", + "indexmap 2.3.0", "log", "thiserror", "zerocopy", @@ -2796,29 +2919,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8" -[[package]] -name = "eszip" -version = "0.78.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0546f00d41dbc6e90b50e922759c02559a897e59b683369c3a13519cd5108b6" -dependencies = [ - "anyhow", - "async-trait", - "base64 0.21.7", - "deno_ast", - "deno_graph", - "deno_npm", - "deno_semver", - "futures", - "hashlink 0.8.4", - "indexmap", - "serde", - "serde_json", - "sha2", - "thiserror", - "url", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -2920,6 +3020,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + [[package]] name = "fiat-crypto" version = "0.2.7" @@ -3103,16 +3209,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.30" @@ -3329,7 +3425,7 @@ checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ "bitflags 2.6.0", "gpu-descriptor-types", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -3373,7 +3469,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", @@ -3392,7 +3488,7 @@ dependencies = [ "futures-sink", "futures-util", "http 1.1.0", - "indexmap", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", @@ -3405,15 +3501,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "serde", ] [[package]] name = "handlebars" -version = "5.1.2" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +checksum = "ce25b617d1375ef96eeb920ae717e3da34a02fc979fe632c75128350f9e1f74a" dependencies = [ "heck 0.5.0", "log", @@ -3424,6 +3520,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -3434,22 +3536,13 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown", -] - [[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -3482,6 +3575,92 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hickory-client" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab9683b08d8f8957a857b0236455d80e1886eaa8c6178af556aa7871fb61b55" +dependencies = [ + "cfg-if", + "data-encoding", + "futures-channel", + "futures-util", + "hickory-proto", + "once_cell", + "radix_trie", + "rand", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hickory-proto" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "serde", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "serde", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hickory-server" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be0e43c556b9b3fdb6c7c71a9a32153a2275d02419e3de809e520bfcfe40c37" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "enum-as-inner", + "futures-util", + "hickory-proto", + "serde", + "thiserror", + "time", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -3526,10 +3705,10 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9de2bdef6354361892492bab5e316b2d78a0ee9971db4d36da9b1eb0e11999" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "new_debug_unreachable", "once_cell", - "phf 0.11.2", + "phf", "rustc-hash 1.1.0", "triomphe", ] @@ -3543,20 +3722,6 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "html5ever" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "http" version = "0.2.12" @@ -3694,6 +3859,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.7" @@ -3782,7 +3960,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "351a787decc56f38d65d16d32687265045d6d6a4531b4a0e1b649def3590354e" dependencies = [ - "indexmap", + "indexmap 2.3.0", "log", "percent-encoding", "serde", @@ -3791,6 +3969,16 @@ dependencies = [ "url", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.3.0" @@ -3798,7 +3986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", "serde", ] @@ -3957,9 +4145,9 @@ dependencies = [ [[package]] name = "jsonc-parser" -version = "0.23.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7725c320caac8c21d8228c1d055af27a995d371f78cc763073d3e068323641b5" +checksum = "b558af6b49fd918e970471374e7a798b2c9bbcda624a210ffa3901ee5614bc8e" dependencies = [ "serde_json", ] @@ -3974,6 +4162,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "jupyter-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd71aa17c4fa65e6d7536ab2728881a41f8feb2ee5841c2240516c3c3d65d8b3" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "thiserror", + "uuid", +] + [[package]] name = "k256" version = "0.13.3" @@ -4004,7 +4205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.3", + "libloading 0.8.5", "pkg-config", ] @@ -4066,12 +4267,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.153" @@ -4109,9 +4304,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets 0.52.4", @@ -4135,9 +4330,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "6b694a822684ddb75df4d657029161431bcb4a85c1856952f845b76912bc6fec" dependencies = [ "cc", "pkg-config", @@ -4146,9 +4341,9 @@ dependencies = [ [[package]] name = "libsui" -version = "0.3.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e39af24eff8df7c8b9980ef56a1a1f4d2e77b34b2d5c0529f108c53ae96a7a" +checksum = "89795977654ad6250d6c0915411b622bac22f9efb4f852af94b2e00964cab832" dependencies = [ "editpe", "libc", @@ -4158,10 +4353,20 @@ dependencies = [ ] [[package]] -name = "libz-sys" -version = "1.1.16" +name = "libuv-sys-lite" +version = "1.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "ca8dfd1a173826d193e3b955e07c22765829890f62c677a59c4a410cb4f47c01" +dependencies = [ + "bindgen", + "libloading 0.8.5", +] + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" dependencies = [ "cc", "cmake", @@ -4229,12 +4434,6 @@ dependencies = [ "serde_repr", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "malloc_buf" version = "0.0.6" @@ -4246,9 +4445,9 @@ dependencies = [ [[package]] name = "malva" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484beda6e5d775ed06a8ec0fce79e51d39f49d834ed2a29da3f437079321804f" +checksum = "1c67b97ed99f56b86fa3c010843441f1fcdb71884bab96b8551bb3d1e7c6d529" dependencies = [ "aho-corasick", "itertools 0.13.0", @@ -4257,31 +4456,11 @@ dependencies = [ "tiny_pretty", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "markup5ever" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" -dependencies = [ - "log", - "phf 0.10.1", - "phf_codegen", - "string_cache", - "string_cache_codegen", - "tendril", -] - [[package]] name = "markup_fmt" -version = "0.13.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dab5ae899659fbe5c8835b2c8ca8d3e357974a3e454138925b404004973361f" +checksum = "ebae65c91eab3d42231232bf48107f351e5a8d511454927218c53aeb68bbdb6f" dependencies = [ "aho-corasick", "css_dataset", @@ -4296,6 +4475,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -4336,15 +4521,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -4391,6 +4567,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mintex" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bec4598fddb13cc7b528819e697852653252b760f1228b7642679bf2ff2cd07" + [[package]] name = "mio" version = "0.8.11" @@ -4426,7 +4608,7 @@ dependencies = [ "bitflags 2.6.0", "codespan-reporting", "hexf-parse", - "indexmap", + "indexmap 2.3.0", "log", "num-traits", "rustc-hash 1.1.0", @@ -4454,7 +4636,7 @@ dependencies = [ [[package]] name = "napi_sym" -version = "0.98.0" +version = "0.107.0" dependencies = [ "quote", "serde", @@ -4496,20 +4678,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "nix" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", - "pin-utils", - "static_assertions", -] - [[package]] name = "nix" version = "0.27.1" @@ -4523,12 +4691,13 @@ dependencies = [ [[package]] name = "node_resolver" -version = "0.7.0" +version = "0.16.0" dependencies = [ "anyhow", "async-trait", "deno_media_type", "deno_package_json", + "deno_path_util", "futures", "lazy-regex", "once_cell", @@ -4743,6 +4912,93 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "opentelemetry" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3cebff57f7dbd1255b44d8bddc2cebeb0ea677dbaa2e25a3070a91b318f660" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry-http" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80" +dependencies = [ + "async-trait", + "bytes", + "http 1.1.0", + "opentelemetry", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" +dependencies = [ + "async-trait", + "futures-core", + "http 1.1.0", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "serde_json", + "thiserror", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" +dependencies = [ + "hex", + "opentelemetry", + "opentelemetry_sdk", + "prost", + "serde", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1b6902ff63b32ef6c489e8048c5e253e2e4a803ea3ea7e783914536eb15c52" + +[[package]] +name = "opentelemetry_sdk" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b742c1cae4693792cc564e58d75a2a0ba29421a34a85b50da92efa89ecb2bc" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror", + "tracing", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -4968,16 +5224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", + "indexmap 2.3.0", ] [[package]] @@ -4987,27 +5234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", - "phf_shared 0.11.2", -] - -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand", + "phf_shared", ] [[package]] @@ -5016,7 +5243,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared 0.11.2", + "phf_shared", "rand", ] @@ -5026,22 +5253,13 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.72", ] -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher", -] - [[package]] name = "phf_shared" version = "0.11.2" @@ -5164,12 +5382,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "pretty_assertions" version = "1.4.0" @@ -5191,16 +5403,6 @@ dependencies = [ "yaml_parser", ] -[[package]] -name = "prettyplease" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" -dependencies = [ - "proc-macro2", - "syn 1.0.109", -] - [[package]] name = "prettyplease" version = "0.2.17" @@ -5284,9 +5486,9 @@ checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" [[package]] name = "prost" -version = "0.11.9" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" dependencies = [ "bytes", "prost-derive", @@ -5294,44 +5496,43 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.11.9" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" dependencies = [ "bytes", - "heck 0.4.1", - "itertools 0.10.5", - "lazy_static", + "heck 0.5.0", + "itertools 0.13.0", "log", "multimap", + "once_cell", "petgraph", - "prettyplease 0.1.25", + "prettyplease", "prost", "prost-types", "regex", - "syn 1.0.109", + "syn 2.0.72", "tempfile", - "which 4.4.2", ] [[package]] name = "prost-derive" -version = "0.11.9" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.72", ] [[package]] name = "prost-types" -version = "0.11.9" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" dependencies = [ "prost", ] @@ -5389,7 +5590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1a341ae463320e9f8f34adda49c8a85d81d4e8f34cce4397fb0350481552224" dependencies = [ "chrono", - "indexmap", + "indexmap 2.3.0", "quick-xml", "strip-ansi-escapes", "thiserror", @@ -5751,8 +5952,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" dependencies = [ "countme", - "hashbrown", - "memoffset 0.9.1", + "hashbrown 0.14.5", + "memoffset", "rustc-hash 1.1.0", "text-size", ] @@ -5779,9 +5980,9 @@ dependencies = [ [[package]] name = "runtimelib" -version = "0.14.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d817764e3971867351e6103955b17d808f5330e9ef63aaaaab55bf8c664c1" +checksum = "fe23ba9967355bbb1be2fb9a8e51bd239ffdf9c791fad5a9b765122ee2bde2e4" dependencies = [ "anyhow", "base64 0.22.1", @@ -5791,6 +5992,7 @@ dependencies = [ "dirs", "futures", "glob", + "jupyter-serde", "rand", "ring", "serde", @@ -5803,14 +6005,14 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.32.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +checksum = "1cdbe9230a57259b37f7257d0aff38b8c9dbda3513edba2105e59b130189d82f" dependencies = [ "bitflags 2.6.0", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.9.1", + "hashlink", "libsqlite3-sys", "smallvec", ] @@ -5960,7 +6162,7 @@ dependencies = [ "libc", "log", "memchr", - "nix 0.27.1", + "nix", "radix_trie", "unicode-segmentation", "unicode-width", @@ -6156,7 +6358,7 @@ version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ - "indexmap", + "indexmap 2.3.0", "itoa", "memchr", "ryu", @@ -6174,15 +6376,6 @@ dependencies = [ "syn 2.0.72", ] -[[package]] -name = "serde_spanned" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -6197,9 +6390,9 @@ dependencies = [ [[package]] name = "serde_v8" -version = "0.220.0" +version = "0.228.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e7a65d91d79acc82aa229aeb084f4a39bda269069bc1520df40f679495388e4" +checksum = "bfe23e75c9a167f4e9d67a90d9fcaa622d1eec9aecad526c270e99a92f6915ff" dependencies = [ "num-bigint", "serde", @@ -6500,32 +6693,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "string_cache" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" -dependencies = [ - "new_debug_unreachable", - "once_cell", - "parking_lot", - "phf_shared 0.10.0", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro2", - "quote", -] - [[package]] name = "string_enum" version = "0.4.4" @@ -6588,7 +6755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc8bd3075d1c6964010333fae9ddcd91ad422a4f8eb8b3206a9b2b6afb4209e" dependencies = [ "bumpalo", - "hashbrown", + "hashbrown 0.14.5", "ptr_meta", "rustc-hash 1.1.0", "triomphe", @@ -6614,7 +6781,7 @@ checksum = "c77c112c218a09635d99a45802a81b4f341d6c28c81076aa2c29ba3bcd9151a9" dependencies = [ "anyhow", "crc", - "indexmap", + "indexmap 2.3.0", "is-macro", "once_cell", "parking_lot", @@ -6684,7 +6851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4740e53eaf68b101203c1df0937d5161a29f3c13bceed0836ddfe245b72dd000" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.3.0", "serde", "serde_json", "swc_cached", @@ -6712,7 +6879,7 @@ dependencies = [ "bitflags 2.6.0", "is-macro", "num-bigint", - "phf 0.11.2", + "phf", "scoped-tls", "serde", "string_enum", @@ -6776,7 +6943,7 @@ dependencies = [ "new_debug_unreachable", "num-bigint", "num-traits", - "phf 0.11.2", + "phf", "serde", "smallvec", "smartstring", @@ -6796,9 +6963,9 @@ checksum = "65f21494e75d0bd8ef42010b47cabab9caaed8f2207570e809f6f4eb51a710d1" dependencies = [ "better_scoped_tls", "bitflags 2.6.0", - "indexmap", + "indexmap 2.3.0", "once_cell", - "phf 0.11.2", + "phf", "rustc-hash 1.1.0", "serde", "smallvec", @@ -6844,7 +7011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98d8447ea20ef76958a8240feef95743702485a84331e6df5bdbe7e383c87838" dependencies = [ "dashmap", - "indexmap", + "indexmap 2.3.0", "once_cell", "petgraph", "rustc-hash 1.1.0", @@ -6889,7 +7056,7 @@ checksum = "76c76d8b9792ce51401d38da0fa62158d61f6d80d16d68fe5b03ce4bf5fba383" dependencies = [ "base64 0.21.7", "dashmap", - "indexmap", + "indexmap 2.3.0", "once_cell", "serde", "sha1", @@ -6929,7 +7096,7 @@ version = "0.134.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029eec7dd485923a75b5a45befd04510288870250270292fc2c1b3a9e7547408" dependencies = [ - "indexmap", + "indexmap 2.3.0", "num_cpus", "once_cell", "rustc-hash 1.1.0", @@ -6974,7 +7141,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357e2c97bb51431d65080f25b436bc4e2fc1a7f64a643bc21a8353e478dc799f" dependencies = [ - "indexmap", + "indexmap 2.3.0", "petgraph", "rustc-hash 1.1.0", "swc_common", @@ -7127,17 +7294,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -7169,6 +7325,7 @@ dependencies = [ name = "test_napi" version = "0.1.0" dependencies = [ + "libuv-sys-lite", "napi-build", "napi-sys", "test_server", @@ -7185,6 +7342,7 @@ dependencies = [ "console_static_text", "deno_unsync", "denokv_proto", + "faster-hex", "fastwebsockets", "flate2", "futures", @@ -7199,7 +7357,7 @@ dependencies = [ "libc", "lsp-types", "monch", - "nix 0.26.2", + "nix", "once_cell", "os_pipe", "parking_lot", @@ -7240,24 +7398,30 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", "syn 2.0.72", ] +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + [[package]] name = "thread_local" version = "1.1.8" @@ -7390,9 +7554,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -7410,7 +7574,7 @@ dependencies = [ "futures-io", "futures-sink", "futures-util", - "hashbrown", + "hashbrown 0.14.5", "pin-project-lite", "slab", "tokio", @@ -7427,37 +7591,33 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.7.8" +name = "tonic" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.5.40", + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2 0.4.4", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.1", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", ] [[package]] @@ -7468,11 +7628,16 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "indexmap 1.9.3", "pin-project", "pin-project-lite", + "rand", + "slab", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -7549,95 +7714,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "trust-dns-client" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14135e72c7e6d4c9b6902d4437881a8598f0145dbb2e3f86f92dbad845b61e63" -dependencies = [ - "cfg-if", - "data-encoding", - "futures-channel", - "futures-util", - "once_cell", - "radix_trie", - "rand", - "thiserror", - "tokio", - "tracing", - "trust-dns-proto", -] - -[[package]] -name = "trust-dns-proto" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.4.0", - "ipnet", - "once_cell", - "rand", - "serde", - "smallvec", - "thiserror", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "trust-dns-resolver" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a3e6c3aff1718b3c73e395d1f35202ba2ffa847c6a62eea0db8fb4cfe30be6" -dependencies = [ - "cfg-if", - "futures-util", - "ipconfig", - "lru-cache", - "once_cell", - "parking_lot", - "rand", - "resolv-conf", - "serde", - "smallvec", - "thiserror", - "tokio", - "tracing", - "trust-dns-proto", -] - -[[package]] -name = "trust-dns-server" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c540f73c2b2ec2f6c54eabd0900e7aafb747a820224b742f556e8faabb461bc7" -dependencies = [ - "async-trait", - "bytes", - "cfg-if", - "drain", - "enum-as-inner", - "futures-executor", - "futures-util", - "serde", - "thiserror", - "time", - "tokio", - "toml 0.7.8", - "tracing", - "trust-dns-proto", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -7851,9 +7927,9 @@ dependencies = [ [[package]] name = "v8" -version = "0.106.0" +version = "130.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a381badc47c6f15acb5fe0b5b40234162349ed9d4e4fd7c83a7f5547c0fc69c5" +checksum = "c23b5c2caff00209b03a716609b275acae94b02dd3b63c4648e7232a84a8402f" dependencies = [ "bindgen", "bitflags 2.6.0", @@ -7874,7 +7950,7 @@ checksum = "97599c400fc79925922b58303e98fcb8fa88f573379a08ddb652e72cbd2e70f6" dependencies = [ "bitflags 2.6.0", "encoding_rs", - "indexmap", + "indexmap 2.3.0", "num-bigint", "serde", "thiserror", @@ -8083,7 +8159,7 @@ dependencies = [ "cfg_aliases", "codespan-reporting", "document-features", - "indexmap", + "indexmap 2.3.0", "log", "naga", "once_cell", @@ -8122,7 +8198,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.3", + "libloading 0.8.5", "log", "metal", "naga", @@ -8387,15 +8463,6 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.6.15" @@ -8431,7 +8498,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" dependencies = [ - "toml 0.5.11", + "toml", ] [[package]] @@ -8508,7 +8575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a6a39b6b5ba0d02c910d05d7fbc366a4befb8901ea107dcde9c1c97acb8a366" dependencies = [ "rowan", - "winnow 0.6.15", + "winnow", ] [[package]] @@ -8605,9 +8672,9 @@ dependencies = [ [[package]] name = "zeromq" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0560d00172817b7f7c2265060783519c475702ae290b154115ca75e976d4d0" +checksum = "6a4528179201f6eecf211961a7d3276faa61554c82651ecc66387f68fc3004bd" dependencies = [ "async-trait", "asynchronous-codec", @@ -8641,7 +8708,7 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap", + "indexmap 2.3.0", "memchr", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 81cbf8b4c6..4a78e7e466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ resolver = "2" members = [ "bench_util", "cli", - "cli/napi/sym", "ext/broadcast_channel", "ext/cache", "ext/canvas", @@ -19,15 +18,17 @@ members = [ "ext/io", "ext/kv", "ext/napi", + "ext/napi/sym", "ext/net", "ext/node", - "ext/node_resolver", "ext/url", "ext/web", "ext/webgpu", "ext/webidl", "ext/websocket", "ext/webstorage", + "resolvers/deno", + "resolvers/node", "runtime", "runtime/permissions", "tests", @@ -44,48 +45,54 @@ license = "MIT" repository = "https://github.com/denoland/deno" [workspace.dependencies] -deno_ast = { version = "=0.42.0", features = ["transpiling"] } -deno_core = { version = "0.311.0" } +deno_ast = { version = "=0.43.3", features = ["transpiling"] } +deno_core = { version = "0.319.0" } -deno_bench_util = { version = "0.162.0", path = "./bench_util" } +deno_bench_util = { version = "0.171.0", path = "./bench_util" } +deno_config = { version = "=0.38.2", features = ["workspace", "sync"] } deno_lockfile = "=0.23.1" -deno_media_type = { version = "0.1.4", features = ["module_specifier"] } -deno_permissions = { version = "0.28.0", path = "./runtime/permissions" } -deno_runtime = { version = "0.177.0", path = "./runtime" } -deno_semver = "=0.5.13" +deno_media_type = { version = "0.2.0", features = ["module_specifier"] } +deno_npm = "=0.25.4" +deno_path_util = "=0.2.1" +deno_permissions = { version = "0.37.0", path = "./runtime/permissions" } +deno_runtime = { version = "0.186.0", path = "./runtime" } +deno_semver = "=0.5.16" deno_terminal = "0.2.0" -napi_sym = { version = "0.98.0", path = "./cli/napi/sym" } +napi_sym = { version = "0.107.0", path = "./ext/napi/sym" } test_util = { package = "test_server", path = "./tests/util/server" } -denokv_proto = "0.8.1" -denokv_remote = "0.8.1" +denokv_proto = "0.8.4" +denokv_remote = "0.8.4" # denokv_sqlite brings in bundled sqlite if we don't disable the default features -denokv_sqlite = { default-features = false, version = "0.8.2" } +denokv_sqlite = { default-features = false, version = "0.8.4" } # exts -deno_broadcast_channel = { version = "0.162.0", path = "./ext/broadcast_channel" } -deno_cache = { version = "0.100.0", path = "./ext/cache" } -deno_canvas = { version = "0.37.0", path = "./ext/canvas" } -deno_console = { version = "0.168.0", path = "./ext/console" } -deno_cron = { version = "0.48.0", path = "./ext/cron" } -deno_crypto = { version = "0.182.0", path = "./ext/crypto" } -deno_fetch = { version = "0.192.0", path = "./ext/fetch" } -deno_ffi = { version = "0.155.0", path = "./ext/ffi" } -deno_fs = { version = "0.78.0", path = "./ext/fs" } -deno_http = { version = "0.166.0", path = "./ext/http" } -deno_io = { version = "0.78.0", path = "./ext/io" } -deno_kv = { version = "0.76.0", path = "./ext/kv" } -deno_napi = { version = "0.99.0", path = "./ext/napi" } -deno_net = { version = "0.160.0", path = "./ext/net" } -deno_node = { version = "0.105.0", path = "./ext/node" } -deno_tls = { version = "0.155.0", path = "./ext/tls" } -deno_url = { version = "0.168.0", path = "./ext/url" } -deno_web = { version = "0.199.0", path = "./ext/web" } -deno_webgpu = { version = "0.135.0", path = "./ext/webgpu" } -deno_webidl = { version = "0.168.0", path = "./ext/webidl" } -deno_websocket = { version = "0.173.0", path = "./ext/websocket" } -deno_webstorage = { version = "0.163.0", path = "./ext/webstorage" } -node_resolver = { version = "0.7.0", path = "./ext/node_resolver" } +deno_broadcast_channel = { version = "0.171.0", path = "./ext/broadcast_channel" } +deno_cache = { version = "0.109.0", path = "./ext/cache" } +deno_canvas = { version = "0.46.0", path = "./ext/canvas" } +deno_console = { version = "0.177.0", path = "./ext/console" } +deno_cron = { version = "0.57.0", path = "./ext/cron" } +deno_crypto = { version = "0.191.0", path = "./ext/crypto" } +deno_fetch = { version = "0.201.0", path = "./ext/fetch" } +deno_ffi = { version = "0.164.0", path = "./ext/ffi" } +deno_fs = { version = "0.87.0", path = "./ext/fs" } +deno_http = { version = "0.175.0", path = "./ext/http" } +deno_io = { version = "0.87.0", path = "./ext/io" } +deno_kv = { version = "0.85.0", path = "./ext/kv" } +deno_napi = { version = "0.108.0", path = "./ext/napi" } +deno_net = { version = "0.169.0", path = "./ext/net" } +deno_node = { version = "0.114.0", path = "./ext/node" } +deno_tls = { version = "0.164.0", path = "./ext/tls" } +deno_url = { version = "0.177.0", path = "./ext/url" } +deno_web = { version = "0.208.0", path = "./ext/web" } +deno_webgpu = { version = "0.144.0", path = "./ext/webgpu" } +deno_webidl = { version = "0.177.0", path = "./ext/webidl" } +deno_websocket = { version = "0.182.0", path = "./ext/websocket" } +deno_webstorage = { version = "0.172.0", path = "./ext/webstorage" } + +# resolvers +deno_resolver = { version = "0.9.0", path = "./resolvers/deno" } +node_resolver = { version = "0.16.0", path = "./resolvers/node" } aes = "=0.8.3" anyhow = "1.0.57" @@ -100,11 +107,13 @@ cbc = { version = "=0.1.2", features = ["alloc"] } # Note: Do not use the "clock" feature of chrono, as it links us to CoreFoundation on macOS. # Instead use util::time::utc_now() chrono = { version = "0.4", default-features = false, features = ["std", "serde"] } +color-print = "0.3.5" console_static_text = "=0.8.1" +dashmap = "5.5.3" data-encoding = "2.3.3" data-url = "=0.3.0" -deno_cache_dir = "=0.11.1" -deno_package_json = { version = "=0.1.1", default-features = false } +deno_cache_dir = "=0.13.2" +deno_package_json = { version = "0.1.2", default-features = false } dlopen2 = "0.6.1" ecb = "=0.1.2" elliptic-curve = { version = "0.13.4", features = ["alloc", "arithmetic", "ecdh", "std", "pem", "jwk"] } @@ -129,11 +138,11 @@ hyper-util = { version = "=0.1.7", features = ["tokio", "client", "client-legacy hyper_v014 = { package = "hyper", version = "0.14.26", features = ["runtime", "http1"] } indexmap = { version = "2", features = ["serde"] } ipnet = "2.3" -jsonc-parser = { version = "=0.23.0", features = ["serde"] } +jsonc-parser = { version = "=0.26.2", features = ["serde"] } lazy-regex = "3" libc = "0.2.126" -libz-sys = { version = "1.1", default-features = false } -log = "0.4.20" +libz-sys = { version = "1.1.20", default-features = false } +log = { version = "0.4.20", features = ["kv"] } lsp-types = "=0.97.0" # used by tower-lsp and "proposed" feature is unstable in patch releases memmem = "0.1.1" monch = "=0.5.0" @@ -149,8 +158,8 @@ percent-encoding = "2.3.0" phf = { version = "0.11", features = ["macros"] } pin-project = "1.0.11" # don't pin because they yank crates from cargo pretty_assertions = "=1.4.0" -prost = "0.11" -prost-build = "0.11" +prost = "0.13" +prost-build = "0.13" rand = "=0.8.5" regex = "^1.7.0" reqwest = { version = "=0.12.5", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli", "socks", "json", "http2"] } # pinned because of https://github.com/seanmonstar/reqwest/pull/1955 @@ -179,7 +188,7 @@ tar = "=0.4.40" tempfile = "3.4.0" termcolor = "1.1.3" thiserror = "1.0.61" -tokio = { version = "=1.36.0", features = ["full"] } +tokio = { version = "1.36.0", features = ["full"] } tokio-metrics = { version = "0.3.0", features = ["rt"] } tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring", "tls12"] } tokio-socks = "0.5.1" @@ -196,9 +205,15 @@ webpki-root-certs = "0.26.5" webpki-roots = "0.26" which = "4.2.5" yoke = { version = "0.7.4", features = ["derive"] } -zeromq = { version = "=0.4.0", default-features = false, features = ["tcp-transport", "tokio-runtime"] } +zeromq = { version = "=0.4.1", default-features = false, features = ["tcp-transport", "tokio-runtime"] } zstd = "=0.12.4" +opentelemetry = "0.27.0" +opentelemetry-http = "0.27.0" +opentelemetry-otlp = { version = "0.27.0", features = ["logs", "http-proto", "http-json"] } +opentelemetry-semantic-conventions = { version = "0.27.0", features = ["semconv_experimental"] } +opentelemetry_sdk = "0.27.0" + # crypto hkdf = "0.12.3" rsa = { version = "0.9.3", default-features = false, features = ["std", "pem", "hazmat"] } # hazmat needed for PrehashSigner in ext/node @@ -213,15 +228,14 @@ quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } # unix -nix = "=0.26.2" +nix = "=0.27.1" # windows deps junction = "=0.2.0" winapi = "=0.3.9" -windows-sys = { version = "0.52.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_WindowsProgramming", "Wdk", "Wdk_System", "Wdk_System_SystemInformation", "Win32_System_Pipes", "Wdk_Storage_FileSystem", "Win32_System_Registry"] } +windows-sys = { version = "0.52.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_WindowsProgramming", "Wdk", "Wdk_System", "Wdk_System_SystemInformation", "Win32_Security", "Win32_System_Pipes", "Wdk_Storage_FileSystem", "Win32_System_Registry", "Win32_System_Kernel"] } winres = "=0.1.12" -# NB: the `bench` and `release` profiles must remain EXACTLY the same. [profile.release] codegen-units = 1 incremental = true @@ -239,13 +253,6 @@ inherits = "release" codegen-units = 128 lto = "thin" -# NB: the `bench` and `release` profiles must remain EXACTLY the same. -[profile.bench] -codegen-units = 1 -incremental = true -lto = true -opt-level = 'z' # Optimize for size - # Key generation is too slow on `debug` [profile.dev.package.num-bigint-dig] opt-level = 3 @@ -254,80 +261,6 @@ opt-level = 3 [profile.dev.package.v8] opt-level = 1 -# Optimize these packages for performance. -# NB: the `bench` and `release` profiles must remain EXACTLY the same. -[profile.bench.package.async-compression] -opt-level = 3 -[profile.bench.package.base64-simd] -opt-level = 3 -[profile.bench.package.brotli] -opt-level = 3 -[profile.bench.package.brotli-decompressor] -opt-level = 3 -[profile.bench.package.bytes] -opt-level = 3 -[profile.bench.package.deno_bench_util] -opt-level = 3 -[profile.bench.package.deno_broadcast_channel] -opt-level = 3 -[profile.bench.package.deno_core] -opt-level = 3 -[profile.bench.package.deno_crypto] -opt-level = 3 -[profile.bench.package.deno_fetch] -opt-level = 3 -[profile.bench.package.deno_ffi] -opt-level = 3 -[profile.bench.package.deno_http] -opt-level = 3 -[profile.bench.package.deno_napi] -opt-level = 3 -[profile.bench.package.deno_net] -opt-level = 3 -[profile.bench.package.deno_node] -opt-level = 3 -[profile.bench.package.deno_runtime] -opt-level = 3 -[profile.bench.package.deno_tls] -opt-level = 3 -[profile.bench.package.deno_url] -opt-level = 3 -[profile.bench.package.deno_web] -opt-level = 3 -[profile.bench.package.deno_websocket] -opt-level = 3 -[profile.bench.package.fastwebsockets] -opt-level = 3 -[profile.bench.package.flate2] -opt-level = 3 -[profile.bench.package.futures-util] -opt-level = 3 -[profile.bench.package.hyper] -opt-level = 3 -[profile.bench.package.miniz_oxide] -opt-level = 3 -[profile.bench.package.num-bigint-dig] -opt-level = 3 -[profile.bench.package.rand] -opt-level = 3 -[profile.bench.package.serde] -opt-level = 3 -[profile.bench.package.serde_v8] -opt-level = 3 -[profile.bench.package.test_napi] -opt-level = 3 -[profile.bench.package.tokio] -opt-level = 3 -[profile.bench.package.url] -opt-level = 3 -[profile.bench.package.v8] -opt-level = 3 -[profile.bench.package.zstd] -opt-level = 3 -[profile.bench.package.zstd-sys] -opt-level = 3 - -# NB: the `bench` and `release` profiles must remain EXACTLY the same. [profile.release.package.async-compression] opt-level = 3 [profile.release.package.base64-simd] @@ -386,6 +319,8 @@ opt-level = 3 opt-level = 3 [profile.release.package.serde_v8] opt-level = 3 +[profile.release.package.libsui] +opt-level = 3 [profile.release.package.test_napi] opt-level = 3 [profile.release.package.tokio] diff --git a/README.md b/README.md index 102319f4f2..19d4fa8a12 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,12 @@ brew install deno choco install deno ``` +[WinGet](https://winstall.app/apps/DenoLand.Deno) (Windows): + +```powershell +winget install --id=DenoLand.Deno +``` + ### Build and install from source Complete instructions for building Deno from source can be found in the manual diff --git a/Releases.md b/Releases.md index a6e4d84e7b..5ce25815bd 100644 --- a/Releases.md +++ b/Releases.md @@ -6,6 +6,538 @@ https://github.com/denoland/deno/releases We also have one-line install commands at: https://github.com/denoland/deno_install +### 2.0.6 / 2024.11.10 + +- feat(ext/http): abort event when request is cancelled (#26781) +- feat(ext/http): abort signal when request is cancelled (#26761) +- feat(lsp): auto-import completions from byonm dependencies (#26680) +- fix(ext/cache): don't panic when creating cache (#26780) +- fix(ext/node): better inspector support (#26471) +- fix(fmt): don't use self-closing tags in HTML (#26754) +- fix(install): cache jsr deps from all workspace config files (#26779) +- fix(node:zlib): gzip & gzipSync should accept ArrayBuffer (#26762) +- fix: performance.timeOrigin (#26787) + +### 2.0.5 / 2024.11.05 + +- fix(add): better error message when adding package that only has pre-release + versions (#26724) +- fix(add): only add npm deps to package.json if it's at least as close as + deno.json (#26683) +- fix(cli): set `npm_config_user_agent` when running npm packages or tasks + (#26639) +- fix(coverage): exclude comment lines from coverage reports (#25939) +- fix(ext/node): add `findSourceMap` to the default export of `node:module` + (#26720) +- fix(ext/node): convert errors from `fs.readFile/fs.readFileSync` to node + format (#26632) +- fix(ext/node): resolve exports even if parent module filename isn't present + (#26553) +- fix(ext/node): return `this` from `http.Server.ref/unref()` (#26647) +- fix(fmt): do not panic for jsx ignore container followed by jsx text (#26723) +- fix(fmt): fix several HTML and components issues (#26654) +- fix(fmt): ignore file directive for YAML files (#26717) +- fix(install): handle invalid function error, and fallback to junctions + regardless of the error (#26730) +- fix(lsp): include unstable features from editor settings (#26655) +- fix(lsp): scope attribution for lazily loaded assets (#26699) +- fix(node): Implement `os.userInfo` properly, add missing `toPrimitive` + (#24702) +- fix(serve): support serve hmr (#26078) +- fix(types): missing `import` permission on `PermissionOptionsObject` (#26627) +- fix(workspace): support wildcard packages (#26568) +- fix: clamp smi in fast calls by default (#26506) +- fix: improved support for cjs and cts modules (#26558) +- fix: op_run_microtasks crash (#26718) +- fix: panic_hook hangs without procfs (#26732) +- fix: remove permission check in op_require_node_module_paths (#26645) +- fix: surface package.json location on dep parse failure (#26665) +- perf(lsp): don't walk coverage directory (#26715) + +### 2.0.4 / 2024.10.29 + +- Revert "fix(ext/node): fix dns.lookup result ordering (#26264)" (#26621) +- Revert "fix(ext/node): use primordials in `ext/node/polyfills/https.ts` + (#26323)" (#26613) +- feat(lsp): "typescript.preferences.preferTypeOnlyAutoImports" setting (#26546) +- fix(check): expose more globals from @types/node (#26603) +- fix(check): ignore resolving `jsxImportSource` when jsx is not used in graph + (#26548) +- fix(cli): Make --watcher CLEAR_SCREEN clear scrollback buffer as well as + visible screen (#25997) +- fix(compile): regression handling redirects (#26586) +- fix(ext/napi): export dynamic symbols list for {Free,Open}BSD (#26605) +- fix(ext/node): add path to `fs.stat` and `fs.statSync` error (#26037) +- fix(ext/node): compatibility with {Free,Open}BSD (#26604) +- fix(ext/node): use primordials in + ext\node\polyfills\internal\crypto\_randomInt.ts (#26534) +- fix(install): cache json exports of JSR packages (#26552) +- fix(install): regression - do not panic when config file contains \r\n + newlines (#26547) +- fix(lsp): make missing import action fix infallible (#26539) +- fix(npm): match npm bearer token generation (#26544) +- fix(upgrade): stop running `deno lsp` processes on windows before attempting + to replace executable (#26542) +- fix(watch): don't panic on invalid file specifiers (#26577) +- fix: do not panic when failing to write to http cache (#26591) +- fix: provide hints in terminal errors for Node.js globals (#26610) +- fix: report exceptions from nextTick (#26579) +- fix: support watch flag to enable watching other files than the main module on + serve subcommand (#26622) +- perf: pass transpiled module to deno_core as known string (#26555) + +### 2.0.3 / 2024.10.25 + +- feat(lsp): interactive inlay hints (#26382) +- fix: support node-api in denort (#26389) +- fix(check): support `--frozen` on deno check (#26479) +- fix(cli): increase size of blocking task threadpool on windows (#26465) +- fix(config): schemas for lint rule and tag autocompletion (#26515) +- fix(ext/console): ignore casing for named colors in css parsing (#26466) +- fix(ext/ffi): return u64/i64 as bigints from nonblocking ffi calls (#26486) +- fix(ext/node): cancel pending ipc writes on channel close (#26504) +- fix(ext/node): map `ERROR_INVALID_NAME` to `ENOENT` on windows (#26475) +- fix(ext/node): only set our end of child process pipe to nonblocking mode + (#26495) +- fix(ext/node): properly map reparse point error in readlink (#26375) +- fix(ext/node): refactor http.ServerResponse into function class (#26210) +- fix(ext/node): stub HTTPParser internal binding (#26401) +- fix(ext/node): use primordials in `ext/node/polyfills/https.ts` (#26323) +- fix(fmt): --ext flag requires to pass files (#26525) +- fix(fmt): upgrade formatters (#26469) +- fix(help): missing package specifier (#26380) +- fix(info): resolve workspace member mappings (#26350) +- fix(install): better json editing (#26450) +- fix(install): cache all exports of JSR packages listed in `deno.json` (#26501) +- fix(install): cache type only module deps in `deno install` (#26497) +- fix(install): don't cache json exports of JSR packages (for now) (#26530) +- fix(install): update lockfile when using package.json (#26458) +- fix(lsp): import-map-remap quickfix for type imports (#26454) +- fix(node/util): support array formats in `styleText` (#26507) +- fix(node:tls): set TLSSocket.alpnProtocol for client connections (#26476) +- fix(npm): ensure scoped package name is encoded in URLs (#26390) +- fix(npm): support version ranges with && or comma (#26453) +- fix: `.npmrc` settings not being passed to install/add command (#26473) +- fix: add 'fmt-component' to unstable features in schema file (#26526) +- fix: share inotify fd across watchers (#26200) +- fix: unpin tokio version (#26457) +- perf(compile): pass module source data from binary directly to v8 (#26494) +- perf: avoid multiple calls to runMicrotask (#26378) + +### 2.0.2 / 2024.10.17 + +- fix(cli): set napi object property properly (#26344) +- fix(ext/node): add null check for kStreamBaseField (#26368) +- fix(install): don't attempt to cache specifiers that point to directories + (#26369) +- fix(jupyter): fix panics for overslow subtraction (#26371) +- fix(jupyter): update to the new logo (#26353) +- fix(net): don't try to set nodelay on upgrade streams (#26342) +- fix(node/fs): copyFile with `COPYFILE_EXCL` should not throw if the + destination doesn't exist (#26360) +- fix(node/http): normalize header names in `ServerResponse` (#26339) +- fix(runtime): send ws ping frames from inspector server (#26352) +- fix: don't warn on ignored signals on windows (#26332) + +### 2.0.1 / 2024.10.16 + +- feat(lsp): "deno/didRefreshDenoConfigurationTree" notifications (#26215) +- feat(unstable): `--unstable-detect-cjs` for respecting explicit + `"type": "commonjs"` (#26149) +- fix(add): create deno.json when running `deno add jsr:` (#26275) +- fix(add): exact version should not have range `^` specifier (#26302) +- fix(child_process): map node `--no-warnings` flag to `--quiet` (#26288) +- fix(cli): add prefix to install commands in help (#26318) +- fix(cli): consolidate pkg parser for install & remove (#26298) +- fix(cli): named export takes precedence over default export in doc testing + (#26112) +- fix(cli): improve deno info output for npm packages (#25906) +- fix(console/ext/repl): support using parseFloat() (#25900) +- fix(ext/console): apply coloring for console.table (#26280) +- fix(ext/napi): pass user context to napi_threadsafe_fn finalizers (#26229) +- fix(ext/node): allow writing to tty columns (#26201) +- fix(ext/node): compute pem length (upper bound) for key exports (#26231) +- fix(ext/node): fix dns.lookup result ordering (#26264) +- fix(ext/node): handle http2 server ending stream (#26235) +- fix(ext/node): implement TCP.setNoDelay (#26263) +- fix(ext/node): timingSafeEqual account for AB byteOffset (#26292) +- fix(ext/node): use primordials in `ext/node/polyfills/internal/buffer.mjs` + (#24993) +- fix(ext/webgpu): allow GL backend on Windows (#26206) +- fix(install): duplicate dependencies in `package.json` (#26128) +- fix(install): handle pkg with dep on self when pkg part of peer dep resolution + (#26277) +- fix(install): retry downloads of registry info / tarballs (#26278) +- fix(install): support installing npm package with alias (#26246) +- fix(jupyter): copy kernels icons to the kernel directory (#26084) +- fix(jupyter): keep running event loop when waiting for messages (#26049) +- fix(lsp): relative completions for bare import-mapped specifiers (#26137) +- fix(node): make `process.stdout.isTTY` writable (#26130) +- fix(node/util): export `styleText` from `node:util` (#26194) +- fix(npm): support `--allow-scripts` on `deno run` (and `deno add`, + `deno test`, etc) (#26075) +- fix(repl): importing json files (#26053) +- fix(repl): remove check flags (#26140) +- fix(unstable/worker): ensure import permissions are passed (#26101) +- fix: add hint for missing `document` global in terminal error (#26218) +- fix: do not panic on wsl share file paths on windows (#26081) +- fix: do not panic running remote cjs module (#26259) +- fix: do not panic when using methods on classes and interfaces in deno doc + html output (#26100) +- fix: improve suggestions and hints when using CommonJS modules (#26287) +- fix: node-api function call should use preamble (#26297) +- fix: panic in `prepare_stack_trace_callback` when global interceptor throws + (#26241) +- fix: use syntect for deno doc html generation (#26322) +- perf(http): avoid clone getting request method and url (#26250) +- perf(http): cache webidl.converters lookups in ext/fetch/23_response.js + (#26256) +- perf(http): make heap allocation for path conditional (#26289) +- perf: use fast calls for microtask ops (#26236) + +### 2.0.0 / 2024.10.09 + +Read announcement blog post at: https://deno.com/blog/v2 + +- BREAKING: `DENO_FUTURE=1` by default, or welcome to Deno 2.0 (#25213) +- BREAKING: disallow `new Deno.FsFile()` (#25478) +- BREAKING: drop support for Deno.run.{clearEnv,gid,uid} (#25371) +- BREAKING: improve types for `Deno.serve` (#25369) +- BREAKING: improved error code accuracy (#25383) +- BREAKING: make supported compilerOptions an allow list (#25432) +- BREAKING: move `width` and `height` options to `UnsafeWindowSurface` + constructor (#24200) +- BREAKING: remove --allow-hrtime (#25367) +- BREAKING: remove "emit" and "map" from deno info output (#25468) +- BREAKING: remove `--allow-none` flag (#25337) +- BREAKING: remove `--jobs` flag (#25336) +- BREAKING: remove `--trace-ops` (#25344) +- BREAKING: remove `--ts` flag (#25338) +- BREAKING: remove `--unstable` flag (#25522) +- BREAKING: remove `deno bundle` (#25339) +- BREAKING: remove `deno vendor` (#25343) +- BREAKING: remove `Deno.[Tls]Listener.prototype.rid` (#25556) +- BREAKING: remove `Deno.{Conn,TlsConn,TcpConn,UnixConn}.prototype.rid` (#25446) +- BREAKING: remove `Deno.{Reader,Writer}[Sync]` and `Deno.Closer` (#25524) +- BREAKING: remove `Deno.Buffer` (#25441) +- BREAKING: remove `Deno.close()` (#25347) +- BREAKING: remove `Deno.ConnectTlsOptions.{certChain,certFile,privateKey}` and + `Deno.ListenTlsOptions.certChain,certFile,keyFile}` (#25525) +- BREAKING: remove `Deno.copy()` (#25345) +- BREAKING: remove `Deno.customInspect` (#25348) +- BREAKING: remove `Deno.fdatasync[Sync]()` (#25520) +- BREAKING: remove `Deno.File` (#25447) +- BREAKING: remove `Deno.flock[Sync]()` (#25350) +- BREAKING: remove `Deno.FsFile.prototype.rid` (#25499) +- BREAKING: remove `Deno.fstat[Sync]()` (#25351) +- BREAKING: remove `Deno.FsWatcher.prototype.rid` (#25444) +- BREAKING: remove `Deno.fsync[Sync]()` (#25448) +- BREAKING: remove `Deno.ftruncate[Sync]()` (#25412) +- BREAKING: remove `Deno.funlock[Sync]()` (#25442) +- BREAKING: remove `Deno.futime[Sync]()` (#25252) +- BREAKING: remove `Deno.iter[Sync]()` (#25346) +- BREAKING: remove `Deno.read[Sync]()` (#25409) +- BREAKING: remove `Deno.readAll[Sync]()` (#25386) +- BREAKING: remove `Deno.seek[Sync]()` (#25449) +- BREAKING: remove `Deno.Seeker[Sync]` (#25551) +- BREAKING: remove `Deno.shutdown()` (#25253) +- BREAKING: remove `Deno.write[Sync]()` (#25408) +- BREAKING: remove `Deno.writeAll[Sync]()` (#25407) +- BREAKING: remove deprecated `UnsafeFnPointer` constructor type with untyped + `Deno.PointerObject` parameter (#25577) +- BREAKING: remove deprecated files config (#25535) +- BREAKING: Remove obsoleted Temporal APIs part 2 (#25505) +- BREAKING: remove remaining web types for compatibility (#25334) +- BREAKING: remove support for remote import maps in deno.json (#25836) +- BREAKING: rename "deps" remote cache folder to "remote" (#25969) +- BREAKING: soft-remove `Deno.isatty()` (#25410) +- BREAKING: soft-remove `Deno.run()` (#25403) +- BREAKING: soft-remove `Deno.serveHttp()` (#25451) +- BREAKING: undeprecate `Deno.FsWatcher.prototype.return()` (#25623) +- feat: add `--allow-import` flag (#25469) +- feat: Add a hint on error about 'Relative import path ... not prefixed with + ...' (#25430) +- feat: Add better error messages for unstable APIs (#25519) +- feat: Add suggestion for packages using Node-API addons (#25975) +- feat: Allow importing .cjs files (#25426) +- feat: default to TS for file extension and support ext flag in more scenarios + (#25472) +- feat: deprecate import assertions (#25281) +- feat: Don't warn about --allow-script when using esbuild (#25894) +- feat: hide several --unstable-* flags (#25378) +- feat: improve lockfile v4 to store normalized version constraints and be more + terse (#25247) +- feat: improve warnings for deprecations and lifecycle script for npm packages + (#25694) +- feat: include version number in all --json based outputs (#25335) +- feat: lockfile v4 by default (#25165) +- feat: make 'globalThis.location' a configurable property (#25812) +- feat: print `Listening on` messages on stderr instead of stdout (#25491) +- feat: remove `--lock-write` flag (#25214) +- feat: require jsr prefix for `deno install` and `deno add` (#25698) +- feat: require(esm) (#25501) +- feat: Show hints when using `window` global (#25805) +- feat: stabilize `Deno.createHttpClient()` (#25569) +- feat: suggest `deno install --entrypoint` instead of `deno cache` (#25228) +- feat: support DENO_LOG env var instead of RUST_LOG (#25356) +- feat: TypeScript 5.6 and `npm:@types/node@22` (#25614) +- feat: Update no-window lint rule (#25486) +- feat: update warning message for --allow-run with no list (#25693) +- feat: warn when using `--allow-run` with no allow list (#25215) +- feat(add): Add npm packages to package.json if present (#25477) +- feat(add): strip package subpath when adding a package (#25419) +- feat(add/install): Flag to add dev dependency to package.json (#25495) +- feat(byonm): support `deno run npm:` when package is not in + package.json (#25981) +- feat(check): turn on noImplicitOverride (#25695) +- feat(check): turn on useUnknownInCatchVariables (#25465) +- feat(cli): evaluate code snippets in JSDoc and markdown (#25220) +- feat(cli): give access to `process` global everywhere (#25291) +- feat(cli): use NotCapable error for permission errors (#25431) +- feat(config): Node modules option for 2.0 (#25299) +- feat(ext/crypto): import and export p521 keys (#25789) +- feat(ext/crypto): X448 support (#26043) +- feat(ext/kv): configurable limit params (#25174) +- feat(ext/node): add abort helpers, process & streams fix (#25262) +- feat(ext/node): add rootCertificates to node:tls (#25707) +- feat(ext/node): buffer.transcode() (#25972) +- feat(ext/node): export 'promises' symbol from 'node:timers' (#25589) +- feat(ext/node): export missing constants from 'zlib' module (#25584) +- feat(ext/node): export missing symbols from domain, puncode, repl, tls + (#25585) +- feat(ext/node): export more symbols from streams and timers/promises (#25582) +- feat(ext/node): expose ES modules for _ modules (#25588) +- feat(flags): allow double commas to escape values in path based flags (#25453) +- feat(flags): support user provided args in repl subcommand (#25605) +- feat(fmt): better error on malfored HTML files (#25853) +- feat(fmt): stabilize CSS, HTML and YAML formatters (#25753) +- feat(fmt): support vto and njk extensions (#25831) +- feat(fmt): upgrade markup_fmt (#25768) +- feat(install): deno install with entrypoint (#25411) +- feat(install): warn repeatedly about not-run lifecycle scripts on explicit + installs (#25878) +- feat(lint): add `no-process-global` lint rule (#25709) +- feat(lsp): add a message when someone runs 'deno lsp' manually (#26051) +- feat(lsp): auto-import types with 'import type' (#25662) +- feat(lsp): html/css/yaml file formatting (#25353) +- feat(lsp): quick fix for @deno-types="npm:@types/*" (#25954) +- feat(lsp): turn on useUnknownInCatchVariables (#25474) +- feat(lsp): unstable setting as list (#25552) +- feat(permissions): `Deno.mainModule` doesn't require permissions (#25667) +- feat(permissions): allow importing from cdn.jsdelivr.net by default (#26013) +- feat(serve): Support second parameter in deno serve (#25606) +- feat(tools/doc): display subitems in symbol overviews where applicable + (#25885) +- feat(uninstall): alias to 'deno remove' if -g flag missing (#25461) +- feat(upgrade): better error message on failure (#25503) +- feat(upgrade): print info links for Deno 2 RC releases (#25225) +- feat(upgrade): support LTS release channel (#25123) +- fix: add link to env var docs (#25557) +- fix: add suggestion how to fix importing CJS module (#21764) +- fix: add test ensuring als works across dynamic import (#25593) +- fix: better error for Deno.UnsafeWindowSurface, correct HttpClient name, + cleanup unused code (#25833) +- fix: cjs resolution cases (#25739) +- fix: consistent with deno_config and treat `"experimentalDecorators"` as + deprecated (#25735) +- fix: delete old Deno 1.x headers file when loading cache (#25283) +- fix: do not panic running invalid file specifier (#25530) +- fix: don't include extensionless files in file collection for lint & fmt by + default (#25721) +- fix: don't prompt when using `Deno.permissions.request` with `--no-prompt` + (#25811) +- fix: eagerly error for specifier with empty version constraint (#25944) +- fix: enable `Win32_Security` feature in `windows-sys` (#26007) +- fix: error on unsupported compiler options (#25714) +- fix: error out if a valid flag is passed before a subcommand (#25830) +- fix: fix jupyter display function type (#25326) +- fix: Float16Array type (#25506) +- fix: handle showing warnings while the progress bar is shown (#25187) +- fix: Hide 'deno cache' from help output (#25960) +- fix: invalid ipv6 hostname on `deno serve` (#25482) +- fix: linux canonicalization checks (#24641) +- fix: lock down allow-run permissions more (#25370) +- fix: make some warnings more standard (#25324) +- fix: no cmd prefix in help output go links (#25459) +- fix: only enable byonm if workspace root has pkg json (#25379) +- fix: panic when require(esm) (#25769) +- fix: precompile preserve SVG camelCase attributes (#25945) +- fix: reland async context (#25140) +- fix: remove --allow-run warning when using deno without args or subcommand + (#25684) +- fix: remove entrypoint hack for Deno 2.0 (#25332) +- fix: remove recently added deno.json node_modules aliasing (#25542) +- fix: remove the typo in the help message (#25962) +- fix: removed unstable-htttp from deno help (#25216) +- fix: replace `npm install` hint with `deno install` hint (#25244) +- fix: trim space around DENO_AUTH_TOKENS (#25147) +- fix: update deno_doc (#25290) +- fix: Update deno_npm to fix `deno install` with crossws (#25837) +- fix: update hint for `deno add ` (#25455) +- fix: update malva in deno to support astro css comments (#25553) +- fix: update nodeModulesDir config JSON schema (#25653) +- fix: update patchver to 0.2 (#25952) +- fix: update sui to 0.4 (#25942) +- fix: upgrade deno_ast 0.42 (#25313) +- fix: upgrade deno_core to 0.307.0 (#25287) +- fix(add/install): default to "latest" tag for npm packages in + `deno add npm:pkg` (#25858) +- fix(bench): Fix table column alignments and NO_COLOR=1 (#25190) +- fix(BREAKING): make dns record types have consistent naming (#25357) +- fix(byonm): resolve npm deps of jsr deps (#25399) +- fix(check): ignore noImplicitOverrides in remote modules (#25854) +- fix(check): move is cjs check from resolving to loading (#25597) +- fix(check): properly surface dependency errors in types file of js file + (#25860) +- fix(cli): `deno task` exit with status 0 (#25637) +- fix(cli): Default to auto with --node-modules-dir flag (#25772) +- fix(cli): handle edge cases around `export`s in doc tests and default export + (#25720) +- fix(cli): Map error kind to `PermissionDenied` when symlinking fails due to + permissions (#25398) +- fix(cli): Only set allow net flag for deno serve if not already allowed all + (#25743) +- fix(cli): Warn on not-run lifecycle scripts with global cache (#25786) +- fix(cli/tools): correct `deno init --serve` template behavior (#25318) +- fix(compile): support 'deno compile' in RC and LTS releases (#25875) +- fix(config): validate export names (#25436) +- fix(coverage): ignore urls from doc testing (#25736) +- fix(doc): surface graph errors as warnings (#25888) +- fix(dts): stabilize `fetch` declaration for use with `Deno.HttpClient` + (#25683) +- fix(ext/console): more precision in console.time (#25723) +- fix(ext/console): prevent duplicate error printing when the cause is assigned + (#25327) +- fix(ext/crypto): ensure EC public keys are exported uncompressed (#25766) +- fix(ext/crypto): fix identity test for x25519 derive bits (#26011) +- fix(ext/crypto): reject empty usages in SubtleCrypto#importKey (#25759) +- fix(ext/crypto): support md4 digest algorithm (#25656) +- fix(ext/crypto): throw DataError for invalid EC key import (#25181) +- fix(ext/fetch): fix lowercase http_proxy classified as https (#25686) +- fix(ext/fetch): percent decode userinfo when parsing proxies (#25229) +- fix(ext/http): do not set localhost to hostname unnecessarily (#24777) +- fix(ext/http): gracefully handle Response.error responses (#25712) +- fix(ext/node): add `FileHandle#writeFile` (#25555) +- fix(ext/node): add `vm.constants` (#25630) +- fix(ext/node): Add missing `node:path` exports (#25567) +- fix(ext/node): Add missing node:fs and node:constants exports (#25568) +- fix(ext/node): add stubs for `node:trace_events` (#25628) +- fix(ext/node): attach console stream properties (#25617) +- fix(ext/node): avoid showing `UNKNOWN` error from TCP handle (#25550) +- fix(ext/node): close upgraded socket when the underlying http connection is + closed (#25387) +- fix(ext/node): delay accept() call 2 ticks in net.Server#listen (#25481) +- fix(ext/node): don't throw error for unsupported signal binding on windows + (#25699) +- fix(ext/node): emit `online` event after worker thread is initialized (#25243) +- fix(ext/node): export `process.allowedNodeEnvironmentFlags` (#25629) +- fix(ext/node): export JWK public key (#25239) +- fix(ext/node): export request and response clases from `http2` module (#25592) +- fix(ext/node): fix `Cipheriv#update(string, undefined)` (#25571) +- fix(ext/node): fix Decipheriv when autoPadding disabled (#25598) +- fix(ext/node): fix process.stdin.pause() (#25864) +- fix(ext/node): Fix vm sandbox object panic (#24985) +- fix(ext/node): http2session ready state (#25143) +- fix(ext/node): Implement detached option in `child_process` (#25218) +- fix(ext/node): import EC JWK keys (#25266) +- fix(ext/node): import JWK octet key pairs (#25180) +- fix(ext/node): import RSA JWK keys (#25267) +- fix(ext/node): register `node:wasi` built-in (#25134) +- fix(ext/node): remove unimplemented promiseHook stubs (#25979) +- fix(ext/node): report freemem() on Linux in bytes (#25511) +- fix(ext/node): Rewrite `node:v8` serialize/deserialize (#25439) +- fix(ext/node): session close during stream setup (#25170) +- fix(ext/node): Stream should be instance of EventEmitter (#25527) +- fix(ext/node): stub `inspector/promises` (#25635) +- fix(ext/node): stub `process.cpuUsage()` (#25462) +- fix(ext/node): stub cpu_info() for OpenBSD (#25807) +- fix(ext/node): support x509 certificates in `createPublicKey` (#25731) +- fix(ext/node): throw when loading `cpu-features` module (#25257) +- fix(ext/node): update aead-gcm-stream to 0.3 (#25261) +- fix(ext/node): use primordials in `ext/node/polyfills/console.ts` (#25572) +- fix(ext/node): use primordials in ext/node/polyfills/wasi.ts (#25608) +- fix(ext/node): validate input lengths in `Cipheriv` and `Decipheriv` (#25570) +- fix(ext/web): don't ignore capture in EventTarget.removeEventListener (#25788) +- fix(ext/webgpu): allow to build on unsupported platforms (#25202) +- fix(ext/webgpu): sync category comment (#25580) +- fix(ext/webstorage): make `getOwnPropertyDescriptor` with symbol return + `undefined` (#13348) +- fix(flags): --allow-all should conflict with lower permissions (#25909) +- fix(flags): don't treat empty run command as task subcommand (#25708) +- fix(flags): move some content from docs.deno.com into help output (#25951) +- fix(flags): properly error out for urls (#25770) +- fix(flags): require global flag for permission flags in install subcommand + (#25391) +- fix(fmt): --check was broken for CSS, YAML and HTML (#25848) +- fix(fmt): fix incorrect quotes in components (#25249) +- fix(fmt): fix tabs in YAML (#25536) +- fix(fmt/markdown): fix regression with multi-line footnotes and inline math + (#25222) +- fix(info): error instead of panic for npm specifiers when using byonm (#25947) +- fix(info): move "version" field to top of json output (#25890) +- fix(inspector): Fix panic when re-entering runtime ops (#25537) +- fix(install): compare versions directly to decide whether to create a child + node_modules dir for a workspace member (#26001) +- fix(install): Make sure target node_modules exists when symlinking (#25494) +- fix(install): recommend using `deno install -g` when using a single http url + (#25388) +- fix(install): store tags associated with package in node_modules dir (#26000) +- fix(install): surface package.json dependency errors (#26023) +- fix(install): Use relative symlinks in deno install (#25164) +- fix(installl): make bin entries executable even if not put in + `node_modules/.bin` (#25873) +- fix(jupyter): allow unstable flags (#25483) +- fix(lint): correctly handle old jsx in linter (#25902) +- fix(lint): support linting jsr pkg without version field (#25230) +- fix(lockfile): use loose deserialization for version constraints (#25660) +- fix(lsp): encode url parts before parsing as uri (#25509) +- fix(lsp): exclude missing import quick fixes with bad resolutions (#26025) +- fix(lsp): panic on url_to_uri() (#25238) +- fix(lsp): properly resolve jsxImportSource for caching (#25688) +- fix(lsp): update diagnostics on npm install (#25352) +- fix(napi): Don't run microtasks in napi_resolve_deferred (#25246) +- fix(napi): Fix worker threads importing already-loaded NAPI addon (#25245) +- fix(no-slow-types): better `override` handling (#25989) +- fix(node): Don't error out if we fail to statically analyze CJS re-export + (#25748) +- fix(node): fix worker_threads issues blocking Angular support (#26024) +- fix(node): implement libuv APIs needed to support `npm:sqlite3` (#25893) +- fix(node): Include "node" condition during CJS re-export analysis (#25785) +- fix(node): Pass NPM_PROCESS_STATE to subprocesses via temp file instead of env + var (#25896) +- fix(node/byonm): do not accidentally resolve bare node built-ins (#25543) +- fix(node/cluster): improve stubs to make log4js work (#25146) +- fix(npm): better error handling for remote npm deps (#25670) +- fix(npm): root package has peer dependency on itself (#26022) +- fix(permissions): disallow any `LD_` or `DYLD_` prefixed env var without full + --allow-run permissions (#25271) +- fix(permissions): disallow launching subprocess with LD_PRELOAD env var + without full run permissions (#25221) +- fix(publish): ensure provenance is spec compliant (#25200) +- fix(regression): do not expose resolved path in Deno.Command permission denied + error (#25434) +- fix(runtime): don't error `child.output()` on consumed stream (#25657) +- fix(runtime): use more null proto objects again (#25040) +- fix(runtime/web_worker): populate `SnapshotOptions` for `WebWorker` when + instantiated without snapshot (#25280) +- fix(task): correct name for scoped npm package binaries (#25390) +- fix(task): support tasks with colons in name in `deno run` (#25233) +- fix(task): use current executable for deno even when not named deno (#26019) +- fix(types): simplify mtls related types (#25658) +- fix(upgrade): more informative information on invalid version (#25319) +- fix(windows): Deno.Command - align binary resolution with linux and mac + (#25429) +- fix(workspace): handle when config has members when specified via --config + (#25988) +- perf: fast path for cached dyn imports (#25636) +- perf: Use -O3 for sui in release builds (#26010) +- perf(cache): single cache file for remote modules (#24983) +- perf(cache): single cache file for typescript emit (#24994) +- perf(ext/fetch): improve decompression throughput by upgrading `tower_http` + (#25806) +- perf(ext/node): reduce some allocations in require (#25197) +- perf(ext/web): optimize performance.measure() (#25774) + ### 1.46.3 / 2024.09.04 - feat(upgrade): print info links for Deno 2 RC releases (#25225) diff --git a/bench_util/Cargo.toml b/bench_util/Cargo.toml index 5a04ab5355..d6eefc3a5d 100644 --- a/bench_util/Cargo.toml +++ b/bench_util/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_bench_util" -version = "0.162.0" +version = "0.171.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f5a8624e1d..374f3dae5e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno" -version = "2.0.0-rc.7" +version = "2.0.6" authors.workspace = true default-run = "deno" edition.workspace = true @@ -38,6 +38,11 @@ path = "./bench/lsp_bench_standalone.rs" [features] default = ["upgrade", "__vendored_zlib_ng"] +# A feature that enables heap profiling with dhat on Linux. +# 1. Compile with `cargo build --profile=release-with-debug --features=dhat-heap` +# 2. Run the executable. It will output a dhat-heap.json file. +# 3. Open the json file in https://nnethercote.github.io/dh_view/dh_view.html +dhat-heap = ["dhat"] # A feature that enables the upgrade subcommand and the background check for # available updates (of deno binary). This is typically disabled for (Linux) # distribution packages. @@ -64,27 +69,26 @@ winres.workspace = true [dependencies] deno_ast = { workspace = true, features = ["bundler", "cjs", "codegen", "proposal", "react", "sourcemap", "transforms", "typescript", "view", "visit"] } -deno_cache_dir = { workspace = true } -deno_config = { version = "=0.35.0", features = ["workspace", "sync"] } +deno_cache_dir.workspace = true +deno_config.workspace = true deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] } -deno_doc = { version = "0.150.0", features = ["html", "syntect"] } -deno_graph = { version = "=0.82.3" } -deno_lint = { version = "=0.67.0", features = ["docs"] } +deno_doc = { version = "0.156.0", default-features = false, features = ["rust", "html", "syntect"] } +deno_graph = { version = "=0.84.1" } +deno_lint = { version = "=0.68.0", features = ["docs"] } deno_lockfile.workspace = true -deno_npm = "=0.25.2" +deno_npm.workspace = true deno_package_json.workspace = true +deno_path_util.workspace = true +deno_resolver.workspace = true deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] } deno_semver.workspace = true -deno_task_shell = "=0.17.0" +deno_task_shell = "=0.18.1" deno_terminal.workspace = true -eszip = "=0.78.0" -libsui = "0.3.1" -napi_sym.workspace = true +libsui = "0.5.0" node_resolver.workspace = true anstream = "0.6.14" async-trait.workspace = true -base32.workspace = true base64.workspace = true bincode = "=1.3.3" bytes.workspace = true @@ -93,16 +97,17 @@ chrono = { workspace = true, features = ["now"] } clap = { version = "=4.5.16", features = ["env", "string", "wrap_help", "error-context"] } clap_complete = "=4.5.24" clap_complete_fig = "=4.5.2" -color-print = "0.3.5" +color-print.workspace = true console_static_text.workspace = true -dashmap = "5.5.3" +dashmap.workspace = true data-encoding.workspace = true +dhat = { version = "0.3.3", optional = true } dissimilar = "=1.0.4" dotenvy = "0.15.7" -dprint-plugin-json = "=0.19.3" -dprint-plugin-jupyter = "=0.1.3" +dprint-plugin-json = "=0.19.4" +dprint-plugin-jupyter = "=0.1.5" dprint-plugin-markdown = "=0.17.8" -dprint-plugin-typescript = "=0.93.0" +dprint-plugin-typescript = "=0.93.2" env_logger = "=0.10.0" fancy-regex = "=0.10.0" faster-hex.workspace = true @@ -116,15 +121,15 @@ http-body-util.workspace = true hyper-util.workspace = true import_map = { version = "=0.20.1", features = ["ext"] } indexmap.workspace = true -jsonc-parser.workspace = true -jupyter_runtime = { package = "runtimelib", version = "=0.14.0" } +jsonc-parser = { workspace = true, features = ["cst", "serde"] } +jupyter_runtime = { package = "runtimelib", version = "=0.19.0", features = ["tokio-runtime"] } lazy-regex.workspace = true libc.workspace = true libz-sys.workspace = true log = { workspace = true, features = ["serde"] } lsp-types.workspace = true -malva = "=0.10.1" -markup_fmt = "=0.13.1" +malva = "=0.11.0" +markup_fmt = "=0.15.0" memmem.workspace = true monch.workspace = true notify.workspace = true @@ -161,7 +166,6 @@ typed-arena = "=2.0.2" uuid = { workspace = true, features = ["serde"] } walkdir = "=2.3.2" which.workspace = true -yoke.workspace = true zeromq.workspace = true zip = { version = "2.1.6", default-features = false, features = ["deflate-flate2"] } zstd.workspace = true diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 9938a09552..720d8db3b0 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -29,13 +29,14 @@ use deno_config::glob::PathOrPatternSet; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; -use deno_core::normalize_path; use deno_core::resolve_url_or_path; use deno_core::url::Url; use deno_graph::GraphKind; -use deno_runtime::deno_permissions::parse_sys_kind; +use deno_path_util::normalize_path; +use deno_path_util::url_to_file_path; use deno_runtime::deno_permissions::PermissionsOptions; -use deno_runtime::fs_util::specifier_to_file_path; +use deno_runtime::deno_permissions::SysDescriptor; +use deno_runtime::ops::otel::OtelConfig; use log::debug; use log::Level; use serde::Deserialize; @@ -575,11 +576,20 @@ fn parse_packages_allowed_scripts(s: &str) -> Result { pub struct UnstableConfig { // TODO(bartlomieju): remove in Deno 2.5 pub legacy_flag_enabled: bool, // --unstable - pub bare_node_builtins: bool, // --unstable-bare-node-builts + pub bare_node_builtins: bool, pub sloppy_imports: bool, pub features: Vec, // --unstabe-kv --unstable-cron } +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub struct InternalFlags { + /// Used when the language server is configured with an + /// explicit cache option. + pub cache_path: Option, + /// Only reads to the lockfile instead of writing to it. + pub lockfile_skip_write: bool, +} + #[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct Flags { /// Vector of CLI arguments - these are user script arguments, all Deno @@ -591,9 +601,6 @@ pub struct Flags { pub ca_stores: Option>, pub ca_data: Option, pub cache_blocklist: Vec, - /// This is not exposed as an option in the CLI, it is used internally when - /// the language server is configured with an explicit cache option. - pub cache_path: Option, pub cached_only: bool, pub type_check_mode: TypeCheckMode, pub config_flag: ConfigFlag, @@ -602,6 +609,8 @@ pub struct Flags { pub enable_op_summary_metrics: bool, pub enable_testing_features: bool, pub ext: Option, + /// Flags that aren't exposed in the CLI, but are used internally. + pub internal: InternalFlags, pub ignore: Vec, pub import_map_path: Option, pub env_file: Option, @@ -688,9 +697,10 @@ impl PermissionFlags { } let builtin_allowed_import_hosts = [ + "jsr.io:443", "deno.land:443", "esm.sh:443", - "jsr.io:443", + "cdn.jsdelivr.net:443", "raw.githubusercontent.com:443", "gist.githubusercontent.com:443", ]; @@ -958,6 +968,24 @@ impl Flags { args } + pub fn otel_config(&self) -> Option { + if self + .unstable_config + .features + .contains(&String::from("otel")) + { + Some(OtelConfig { + runtime_name: Cow::Borrowed("deno"), + runtime_version: Cow::Borrowed(crate::version::DENO_VERSION_INFO.deno), + deterministic: std::env::var("DENO_UNSTABLE_OTEL_DETERMINISTIC") + .is_ok(), + ..Default::default() + }) + } else { + None + } + } + /// Extract the paths the config file should be discovered from. /// /// Returns `None` if the config file should not be auto-discovered. @@ -1002,7 +1030,7 @@ impl Flags { if module_specifier.scheme() == "file" || module_specifier.scheme() == "npm" { - if let Ok(p) = specifier_to_file_path(&module_specifier) { + if let Ok(p) = url_to_file_path(&module_specifier) { Some(vec![p.parent().unwrap().to_path_buf()]) } else { Some(vec![current_dir.to_path_buf()]) @@ -1168,15 +1196,14 @@ static DENO_HELP: &str = cstr!( Dependency management: add Add dependencies - deno add @std/assert | deno add npm:express - install Install script as an executable - uninstall Uninstall a script previously installed with deno install + deno add jsr:@std/assert | deno add npm:express + install Installs dependencies either in the local project or globally to a bin directory + uninstall Uninstalls a dependency or an executable script in the installation root's bin directory remove Remove dependencies from the configuration file Tooling: bench Run benchmarks deno bench bench.ts - cache Cache the dependencies check Type-check the dependencies clean Remove the cache directory compile Compile the script into a self contained executable @@ -1258,28 +1285,7 @@ pub fn flags_from_vec(args: Vec) -> clap::error::Result { .get_arguments() .any(|arg| arg.get_id().as_str() == "unstable") { - subcommand = subcommand - .mut_arg("unstable", |arg| { - let new_help = arg - .get_help() - .unwrap() - .to_string() - .split_once("\n") - .unwrap() - .0 - .to_string(); - arg.help_heading(UNSTABLE_HEADING).help(new_help) - }) - .mut_args(|arg| { - // long_help here is being used as a metadata, see unstable args definition - if arg.get_help_heading() == Some(UNSTABLE_HEADING) - && arg.get_long_help().is_some() - { - arg.hide(false) - } else { - arg - } - }); + subcommand = enable_unstable(subcommand); } help_parse(&mut flags, subcommand); @@ -1355,7 +1361,7 @@ pub fn flags_from_vec(args: Vec) -> clap::error::Result { } match subcommand.as_str() { - "add" => add_parse(&mut flags, &mut m), + "add" => add_parse(&mut flags, &mut m)?, "remove" => remove_parse(&mut flags, &mut m), "bench" => bench_parse(&mut flags, &mut m)?, "bundle" => bundle_parse(&mut flags, &mut m), @@ -1414,6 +1420,31 @@ pub fn flags_from_vec(args: Vec) -> clap::error::Result { Ok(flags) } +fn enable_unstable(command: Command) -> Command { + command + .mut_arg("unstable", |arg| { + let new_help = arg + .get_help() + .unwrap() + .to_string() + .split_once("\n") + .unwrap() + .0 + .to_string(); + arg.help_heading(UNSTABLE_HEADING).help(new_help) + }) + .mut_args(|arg| { + // long_help here is being used as a metadata, see unstable args definition + if arg.get_help_heading() == Some(UNSTABLE_HEADING) + && arg.get_long_help().is_some() + { + arg.hide(false) + } else { + arg + } + }) +} + macro_rules! heading { ($($name:ident = $title:expr),+; $total:literal) => { $(const $name: &str = $title;)+ @@ -1516,7 +1547,7 @@ pub fn clap_root() -> Command { ); run_args(Command::new("deno"), true) - .args(unstable_args(UnstableArgsConfig::ResolutionAndRuntime)) + .with_unstable_args(UnstableArgsConfig::ResolutionAndRuntime) .next_line_help(false) .bin_name("deno") .styles( @@ -1618,7 +1649,7 @@ fn command( ) -> Command { Command::new(name) .about(about) - .args(unstable_args(unstable_args_config)) + .with_unstable_args(unstable_args_config) } fn help_subcommand(app: &Command) -> Command { @@ -1646,10 +1677,10 @@ fn add_subcommand() -> Command { "add", cstr!( "Add dependencies to your configuration file. - deno add @std/path + deno add jsr:@std/path You can add multiple dependencies at once: - deno add @std/path @std/assert" + deno add jsr:@std/path jsr:@std/assert" ), UnstableArgsConfig::None, ) @@ -1663,6 +1694,7 @@ You can add multiple dependencies at once: .action(ArgAction::Append), ) .arg(add_dev_arg()) + .arg(allow_scripts_arg()) }) } @@ -1705,7 +1737,7 @@ If you specify a directory instead of a file, the path is expanded to all contai UnstableArgsConfig::ResolutionAndRuntime, ) .defer(|cmd| { - runtime_args(cmd, true, false) + runtime_args(cmd, true, false, true) .arg(check_arg(true)) .arg( Arg::new("json") @@ -1842,6 +1874,7 @@ Unless --reload is specified, this command will not re-download already cached d .required_unless_present("help") .value_hint(ValueHint::FilePath), ) + .arg(frozen_lockfile_arg()) .arg(allow_import_arg()) } ) @@ -1852,11 +1885,15 @@ fn compile_subcommand() -> Command { "compile", cstr!("Compiles the given script into a self contained executable. - deno compile -A jsr:@std/http/file-server + deno compile --allow-read --allow-net jsr:@std/http/file-server deno compile --output file_server jsr:@std/http/file-server Any flags specified which affect runtime behavior will be applied to the resulting binary. +This allows distribution of a Deno application to systems that do not have Deno installed. +Under the hood, it bundles a slimmed down version of the Deno runtime along with your +JavaScript or TypeScript code. + Cross-compiling to different target architectures is supported using the --target flag. On the first invocation with deno will download the proper binary and cache it in $DENO_DIR. @@ -1865,7 +1902,7 @@ On the first invocation with deno will download the proper binary and cache it i UnstableArgsConfig::ResolutionAndRuntime, ) .defer(|cmd| { - runtime_args(cmd, true, false) + runtime_args(cmd, true, false, true) .arg(check_arg(true)) .arg( Arg::new("include") @@ -2186,7 +2223,7 @@ This command has implicit access to all permissions. UnstableArgsConfig::ResolutionAndRuntime, ) .defer(|cmd| { - runtime_args(cmd, false, true) + runtime_args(cmd, false, true, true) .arg(check_arg(false)) .arg(executable_ext_arg()) .arg( @@ -2223,6 +2260,9 @@ Supported file types which are behind corresponding unstable flags (see formatti Format stdin and write to stdout: cat file.ts | deno fmt - +Check if the files are formatted: + deno fmt --check + Ignore formatting code by preceding it with an ignore comment: // deno-fmt-ignore @@ -2252,7 +2292,7 @@ Ignore formatting a file by adding an ignore comment at the top of the file: "sass", "less", "html", "svelte", "vue", "astro", "yml", "yaml", "ipynb", ]) - .help_heading(FMT_HEADING), + .help_heading(FMT_HEADING).requires("files"), ) .arg( Arg::new("ignore") @@ -2373,7 +2413,7 @@ Ignore formatting a file by adding an ignore comment at the top of the file: } fn init_subcommand() -> Command { - command("init", "Initialize a new project", UnstableArgsConfig::None).defer( + command("init", "scaffolds a basic Deno project with a script, test, and configuration file", UnstableArgsConfig::None).defer( |cmd| { cmd .arg(Arg::new("dir").value_hint(ValueHint::DirPath)) @@ -2418,7 +2458,7 @@ The following information is shown: .arg( location_arg() .conflicts_with("file") - .help("Show files used for origin bound APIs like the Web Storage API when running a script with '--location='") + .help(cstr!("Show files used for origin bound APIs like the Web Storage API when running a script with --location=<>")) ) .arg(no_check_arg().hide(true)) // TODO(lucacasonato): remove for 2.0 .arg(no_config_arg()) @@ -2449,7 +2489,7 @@ in the package cache. If no dependency is specified, installs all dependencies l If the --entrypoint flag is passed, installs the dependencies of the specified entrypoint(s). deno install - deno install @std/bytes + deno install jsr:@std/bytes deno install npm:chalk deno install --entrypoint entry1.ts entry2.ts @@ -2460,7 +2500,7 @@ If the --global flag is set, installs a script as an executable in the deno install --global --allow-net --allow-read jsr:@std/http/file-server deno install -g https://examples.deno.land/color-logging.ts -To change the executable name, use -n/--name: +To change the executable name, use -n/--name: deno install -g --allow-net --allow-read -n serve jsr:@std/http/file-server The executable name is inferred by default: @@ -2482,7 +2522,7 @@ The installation root is determined, in order of precedence: These must be added to the path manually if required."), UnstableArgsConfig::ResolutionAndRuntime) .visible_alias("i") .defer(|cmd| { - permission_args(runtime_args(cmd, false, true), Some("global")) + permission_args(runtime_args(cmd, false, true, false), Some("global")) .arg(check_arg(true)) .arg(allow_scripts_arg()) .arg( @@ -2742,9 +2782,19 @@ To ignore linting on an entire file, you can add an ignore comment at the top of } fn repl_subcommand() -> Command { - command("repl", "Read Eval Print Loop", UnstableArgsConfig::ResolutionAndRuntime) - .defer(|cmd| runtime_args(cmd, true, true) - .arg(check_arg(false)) + command("repl", cstr!( + "Starts a read-eval-print-loop, which lets you interactively build up program state in the global context. +It is especially useful for quick prototyping and checking snippets of code. + +TypeScript is supported, however it is not type-checked, only transpiled." + ), UnstableArgsConfig::ResolutionAndRuntime) + .defer(|cmd| { + let cmd = compile_args_without_check_args(cmd); + let cmd = inspect_args(cmd); + let cmd = permission_args(cmd, None); + let cmd = runtime_misc_args(cmd); + + cmd .arg( Arg::new("eval-file") .long("eval-file") @@ -2763,7 +2813,7 @@ fn repl_subcommand() -> Command { .after_help(cstr!("Environment variables: DENO_REPL_HISTORY Set REPL history file path. History file is disabled when the value is empty. [default: $DENO_DIR/deno_history.txt]")) - ) + }) .arg(env_file_arg()) .arg( Arg::new("args") @@ -2775,7 +2825,7 @@ fn repl_subcommand() -> Command { } fn run_args(command: Command, top_level: bool) -> Command { - runtime_args(command, true, true) + runtime_args(command, true, true, true) .arg(check_arg(false)) .arg(watch_arg(true)) .arg(hmr_arg(true)) @@ -2825,19 +2875,17 @@ fn serve_subcommand() -> Command { The serve command uses the default exports of the main module to determine which servers to start. -See https://docs.deno.com/runtime/manual/tools/serve for more detailed information. - Start a server defined in server.ts: deno serve server.ts Start a server defined in server.ts, watching for changes and running on port 5050: deno serve --watch --port 5050 server.ts -Read more: https://docs.deno.com/go/serve"), UnstableArgsConfig::ResolutionAndRuntime), true, true) +Read more: https://docs.deno.com/go/serve"), UnstableArgsConfig::ResolutionAndRuntime), true, true, true) .arg( Arg::new("port") .long("port") - .help("The TCP port to serve on, defaulting to 8000. Pass 0 to pick a random free port") + .help(cstr!("The TCP port to serve on. Pass 0 to pick a random free port [default: 8000]")) .value_parser(value_parser!(u16)), ) .arg( @@ -2888,6 +2936,7 @@ List all available tasks: .help("Specify the directory to run the task in") .value_hint(ValueHint::DirPath), ) + .arg(node_modules_dir_arg()) }) } @@ -2906,7 +2955,7 @@ or **/__tests__/**: UnstableArgsConfig::ResolutionAndRuntime ) .defer(|cmd| - runtime_args(cmd, true, true) + runtime_args(cmd, true, true, true) .arg(check_arg(true)) .arg( Arg::new("ignore") @@ -3047,11 +3096,13 @@ fn parallel_arg(descr: &str) -> Arg { fn types_subcommand() -> Command { command( "types", - "Print runtime TypeScript declarations. + cstr!( + "Print runtime TypeScript declarations. deno types > lib.deno.d.ts -The declaration file could be saved and used for typing information.", +The declaration file could be saved and used for typing information." + ), UnstableArgsConfig::None, ) } @@ -3161,7 +3212,7 @@ See the Deno 1.x to 2.x Migration Guide for migration instructions: https://docs } fn publish_subcommand() -> Command { - command("publish", "Publish the current working directory's package or workspace", UnstableArgsConfig::ResolutionOnly) + command("publish", "Publish the current working directory's package or workspace to JSR", UnstableArgsConfig::ResolutionOnly) .defer(|cmd| { cmd .arg( @@ -3238,7 +3289,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { -W, --allow-write[=<...] Allow file system write access. Optionally specify allowed paths. --allow-write | --allow-write="/etc,/var/log.txt" -I, --allow-import[=<...] Allow importing from remote hosts. Optionally specify allowed IP addresses and host names, with ports as necessary. - Default value: deno.land:443,jsr.io:443,esm.sh:443,raw.githubusercontent.com:443,user.githubusercontent.com:443 + Default value: deno.land:443,jsr.io:443,esm.sh:443,cdn.jsdelivr.net:443,raw.githubusercontent.com:443,user.githubusercontent.com:443 --allow-import | --allow-import="example.com,github.com" -N, --allow-net[=<...] Allow network access. Optionally specify allowed IP addresses and host names, with ports as necessary. --allow-net | --allow-net="localhost:8080,deno.land" @@ -3258,7 +3309,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { --deny-net | --deny-net="localhost:8080,deno.land" --deny-env[=<...] Deny access to environment variables. Optionally specify inacessible environment variables. --deny-env | --deny-env="PORT,HOME,PATH" - -S, --deny-sys[=<...] Deny access to OS information. Optionally deny specific APIs by function name. + --deny-sys[=<...] Deny access to OS information. Optionally deny specific APIs by function name. --deny-sys | --deny-sys="systemMemoryInfo,osRelease" --deny-run[=<...] Deny running subprocesses. Optionally specify denied runnable program names. --deny-run | --deny-run="whoami,ps" @@ -3355,8 +3406,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { .value_name("IP_OR_HOSTNAME") .help("Allow network access. Optionally specify allowed IP addresses and host names, with ports as necessary") .value_parser(flags_net::validator) - .hide(true) - ; + .hide(true); if let Some(requires) = requires { arg = arg.requires(requires) } @@ -3448,7 +3498,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { .require_equals(true) .value_name("API_NAME") .help("Allow access to OS information. Optionally allow specific APIs by function name") - .value_parser(|key: &str| parse_sys_kind(key).map(ToString::to_string)) + .value_parser(|key: &str| SysDescriptor::parse(key.to_string()).map(|s| s.into_string())) .hide(true) ; if let Some(requires) = requires { @@ -3466,7 +3516,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { .require_equals(true) .value_name("API_NAME") .help("Deny access to OS information. Optionally deny specific APIs by function name") - .value_parser(|key: &str| parse_sys_kind(key).map(ToString::to_string)) + .value_parser(|key: &str| SysDescriptor::parse(key.to_string()).map(|s| s.into_string())) .hide(true) ; if let Some(requires) = requires { @@ -3550,8 +3600,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { .long("allow-hrtime") .action(ArgAction::SetTrue) .help("REMOVED in Deno 2.0") - .hide(true) - ; + .hide(true); if let Some(requires) = requires { arg = arg.requires(requires) } @@ -3564,8 +3613,7 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command { .long("deny-hrtime") .action(ArgAction::SetTrue) .help("REMOVED in Deno 2.0") - .hide(true) - ; + .hide(true); if let Some(requires) = requires { arg = arg.requires(requires) } @@ -3619,6 +3667,7 @@ fn runtime_args( app: Command, include_perms: bool, include_inspector: bool, + include_allow_scripts: bool, ) -> Command { let app = compile_args(app); let app = if include_perms { @@ -3631,6 +3680,15 @@ fn runtime_args( } else { app }; + let app = if include_allow_scripts { + app.arg(allow_scripts_arg()) + } else { + app + }; + runtime_misc_args(app) +} + +fn runtime_misc_args(app: Command) -> Command { app .arg(frozen_lockfile_arg()) .arg(cached_only_arg()) @@ -3650,7 +3708,7 @@ fn allow_import_arg() -> Arg { .require_equals(true) .value_name("IP_OR_HOSTNAME") .help(cstr!( - "Allow importing from remote hosts. Optionally specify allowed IP addresses and host names, with ports as necessary. Default value: deno.land:443,jsr.io:443,esm.sh:443,raw.githubusercontent.com:443,user.githubusercontent.com:443" + "Allow importing from remote hosts. Optionally specify allowed IP addresses and host names, with ports as necessary. Default value: deno.land:443,jsr.io:443,esm.sh:443,cdn.jsdelivr.net:443,raw.githubusercontent.com:443,user.githubusercontent.com:443" )) .value_parser(flags_net::validator) } @@ -3801,7 +3859,9 @@ fn location_arg() -> Arg { url.set_password(None).unwrap(); Ok(url) }) - .help("Value of 'globalThis.location' used by some web APIs") + .help(cstr!( + "Value of globalThis.location used by some web APIs" + )) .value_hint(ValueHint::Url) } @@ -4110,23 +4170,29 @@ enum UnstableArgsConfig { ResolutionAndRuntime, } -struct UnstableArgsIter { - idx: usize, - cfg: UnstableArgsConfig, +trait CommandExt { + fn with_unstable_args(self, cfg: UnstableArgsConfig) -> Self; } -impl Iterator for UnstableArgsIter { - type Item = Arg; +impl CommandExt for Command { + fn with_unstable_args(self, cfg: UnstableArgsConfig) -> Self { + let mut next_display_order = { + let mut value = 1000; + move || { + value += 1; + value + } + }; - fn next(&mut self) -> Option { - let arg = if self.idx == 0 { + let mut cmd = self.arg( Arg::new("unstable") - .long("unstable") - .help(cstr!("Enable all unstable features and APIs. Instead of using this flag, consider enabling individual unstable features + .long("unstable") + .help(cstr!("Enable all unstable features and APIs. Instead of using this flag, consider enabling individual unstable features To view the list of individual unstable feature flags, run this command again with --help=unstable")) - .action(ArgAction::SetTrue) - .hide(matches!(self.cfg, UnstableArgsConfig::None)) - } else if self.idx == 1 { + .action(ArgAction::SetTrue) + .hide(matches!(cfg, UnstableArgsConfig::None)) + .display_order(next_display_order()) + ).arg( Arg::new("unstable-bare-node-builtins") .long("unstable-bare-node-builtins") .help("Enable unstable bare node builtins feature") @@ -4134,20 +4200,36 @@ impl Iterator for UnstableArgsIter { .value_parser(FalseyValueParser::new()) .action(ArgAction::SetTrue) .hide(true) - .long_help(match self.cfg { + .long_help(match cfg { UnstableArgsConfig::None => None, UnstableArgsConfig::ResolutionOnly | UnstableArgsConfig::ResolutionAndRuntime => Some("true"), }) .help_heading(UNSTABLE_HEADING) - } else if self.idx == 2 { + .display_order(next_display_order()), + ).arg( + Arg::new("unstable-detect-cjs") + .long("unstable-detect-cjs") + .help("Reads the package.json type field in a project to treat .js files as .cjs") + .value_parser(FalseyValueParser::new()) + .action(ArgAction::SetTrue) + .hide(true) + .long_help(match cfg { + UnstableArgsConfig::None => None, + UnstableArgsConfig::ResolutionOnly + | UnstableArgsConfig::ResolutionAndRuntime => Some("true"), + }) + .help_heading(UNSTABLE_HEADING) + .display_order(next_display_order()) + ).arg( Arg::new("unstable-byonm") .long("unstable-byonm") .value_parser(FalseyValueParser::new()) .action(ArgAction::SetTrue) .hide(true) .help_heading(UNSTABLE_HEADING) - } else if self.idx == 3 { + .display_order(next_display_order()), + ).arg( Arg::new("unstable-sloppy-imports") .long("unstable-sloppy-imports") .help("Enable unstable resolving of specifiers by extension probing, .js to .ts, and directory probing") @@ -4155,40 +4237,39 @@ impl Iterator for UnstableArgsIter { .value_parser(FalseyValueParser::new()) .action(ArgAction::SetTrue) .hide(true) - .long_help(match self.cfg { + .long_help(match cfg { UnstableArgsConfig::None => None, UnstableArgsConfig::ResolutionOnly | UnstableArgsConfig::ResolutionAndRuntime => Some("true") }) .help_heading(UNSTABLE_HEADING) - } else if self.idx > 3 { - let granular_flag = crate::UNSTABLE_GRANULAR_FLAGS.get(self.idx - 4)?; - Arg::new(format!("unstable-{}", granular_flag.name)) - .long(format!("unstable-{}", granular_flag.name)) - .help(granular_flag.help_text) - .action(ArgAction::SetTrue) - .hide(true) - .help_heading(UNSTABLE_HEADING) - // we don't render long help, so using it here as a sort of metadata - .long_help(if granular_flag.show_in_help { - match self.cfg { - UnstableArgsConfig::None | UnstableArgsConfig::ResolutionOnly => { - None - } - UnstableArgsConfig::ResolutionAndRuntime => Some("true"), - } - } else { - None - }) - } else { - return None; - }; - self.idx += 1; - Some(arg.display_order(self.idx + 1000)) - } -} + .display_order(next_display_order()) + ); -fn unstable_args(cfg: UnstableArgsConfig) -> impl IntoIterator { - UnstableArgsIter { idx: 0, cfg } + for granular_flag in crate::UNSTABLE_GRANULAR_FLAGS.iter() { + cmd = cmd.arg( + Arg::new(format!("unstable-{}", granular_flag.name)) + .long(format!("unstable-{}", granular_flag.name)) + .help(granular_flag.help_text) + .action(ArgAction::SetTrue) + .hide(true) + .help_heading(UNSTABLE_HEADING) + // we don't render long help, so using it here as a sort of metadata + .long_help(if granular_flag.show_in_help { + match cfg { + UnstableArgsConfig::None | UnstableArgsConfig::ResolutionOnly => { + None + } + UnstableArgsConfig::ResolutionAndRuntime => Some("true"), + } + } else { + None + }) + .display_order(next_display_order()), + ); + } + + cmd + } } fn allow_scripts_arg_parse( @@ -4210,8 +4291,13 @@ fn allow_scripts_arg_parse( Ok(()) } -fn add_parse(flags: &mut Flags, matches: &mut ArgMatches) { +fn add_parse( + flags: &mut Flags, + matches: &mut ArgMatches, +) -> clap::error::Result<()> { + allow_scripts_arg_parse(flags, matches)?; flags.subcommand = DenoSubcommand::Add(add_parse_inner(matches, None)); + Ok(()) } fn add_parse_inner( @@ -4237,7 +4323,7 @@ fn bench_parse( ) -> clap::error::Result<()> { flags.type_check_mode = TypeCheckMode::Local; - runtime_args_parse(flags, matches, true, false)?; + runtime_args_parse(flags, matches, true, false, true)?; ext_arg_parse(flags, matches); // NOTE: `deno bench` always uses `--no-prompt`, tests shouldn't ever do @@ -4305,6 +4391,7 @@ fn check_parse( flags.type_check_mode = TypeCheckMode::Local; compile_args_without_check_parse(flags, matches)?; unstable_args_parse(flags, matches, UnstableArgsConfig::ResolutionAndRuntime); + frozen_lockfile_arg_parse(flags, matches); let files = matches.remove_many::("file").unwrap().collect(); if matches.get_flag("all") || matches.get_flag("remote") { flags.type_check_mode = TypeCheckMode::All; @@ -4327,7 +4414,7 @@ fn compile_parse( matches: &mut ArgMatches, ) -> clap::error::Result<()> { flags.type_check_mode = TypeCheckMode::Local; - runtime_args_parse(flags, matches, true, false)?; + runtime_args_parse(flags, matches, true, false, true)?; let mut script = matches.remove_many::("script_arg").unwrap(); let source_file = script.next().unwrap(); @@ -4502,7 +4589,7 @@ fn eval_parse( flags: &mut Flags, matches: &mut ArgMatches, ) -> clap::error::Result<()> { - runtime_args_parse(flags, matches, false, true)?; + runtime_args_parse(flags, matches, false, true, false)?; unstable_args_parse(flags, matches, UnstableArgsConfig::ResolutionAndRuntime); flags.allow_all(); @@ -4595,7 +4682,7 @@ fn install_parse( flags: &mut Flags, matches: &mut ArgMatches, ) -> clap::error::Result<()> { - runtime_args_parse(flags, matches, true, true)?; + runtime_args_parse(flags, matches, true, true, false)?; let global = matches.get_flag("global"); if global { @@ -4655,7 +4742,7 @@ fn json_reference_parse( app.build(); fn serialize_command( - command: &mut Command, + mut command: Command, top_level: bool, ) -> deno_core::serde_json::Value { let args = command @@ -4663,7 +4750,7 @@ fn json_reference_parse( .filter(|arg| { !arg.is_hide_set() && if top_level { - true + arg.is_global_set() } else { !arg.is_global_set() } @@ -4672,40 +4759,49 @@ fn json_reference_parse( let name = arg.get_id().as_str(); let short = arg.get_short(); let long = arg.get_long(); - let aliases = arg.get_visible_aliases(); let required = arg.is_required_set(); - let help = arg.get_help().map(|help| help.to_string()); + let help = arg.get_help().map(|help| help.ansi().to_string()); + let help_heading = arg + .get_help_heading() + .map(|help_heading| help_heading.to_string()); let usage = arg.to_string(); json!({ "name": name, "short": short, "long": long, - "aliases": aliases, "required": required, "help": help, + "help_heading": help_heading, "usage": usage, }) }) .collect::>(); let name = command.get_name().to_string(); - let about = command.get_about().map(|about| about.to_string()); - let visible_aliases = command - .get_visible_aliases() - .map(|s| s.to_string()) - .collect::>(); - let usage = command.render_usage().to_string(); + let about = command.get_about().map(|about| about.ansi().to_string()); + let usage = command.render_usage().ansi().to_string(); let subcommands = command - .get_subcommands_mut() - .map(|command| serialize_command(command, false)) + .get_subcommands() + .map(|command| { + serialize_command( + if command + .get_arguments() + .any(|arg| arg.get_id().as_str() == "unstable") + { + enable_unstable(command.clone()) + } else { + command.clone() + }, + false, + ) + }) .collect::>(); json!({ "name": name, "about": about, - "visible_aliases": visible_aliases, "args": args, "subcommands": subcommands, "usage": usage, @@ -4713,7 +4809,7 @@ fn json_reference_parse( } flags.subcommand = DenoSubcommand::JSONReference(JSONReferenceFlags { - json: serialize_command(&mut app, true), + json: serialize_command(app, true), }) } @@ -4812,8 +4908,18 @@ fn repl_parse( flags: &mut Flags, matches: &mut ArgMatches, ) -> clap::error::Result<()> { - runtime_args_parse(flags, matches, true, true)?; - unsafely_ignore_certificate_errors_parse(flags, matches); + unstable_args_parse(flags, matches, UnstableArgsConfig::ResolutionAndRuntime); + compile_args_without_check_parse(flags, matches)?; + cached_only_arg_parse(flags, matches); + frozen_lockfile_arg_parse(flags, matches); + permission_args_parse(flags, matches)?; + inspect_arg_parse(flags, matches); + location_arg_parse(flags, matches); + v8_flags_arg_parse(flags, matches); + seed_arg_parse(flags, matches); + enable_testing_features_arg_parse(flags, matches); + env_file_arg_parse(flags, matches); + strace_ops_parse(flags, matches); let eval_files = matches .remove_many::("eval-file") @@ -4845,7 +4951,7 @@ fn run_parse( mut app: Command, bare: bool, ) -> clap::error::Result<()> { - runtime_args_parse(flags, matches, true, true)?; + runtime_args_parse(flags, matches, true, true, true)?; ext_arg_parse(flags, matches); flags.code_cache_enabled = !matches.get_flag("no-code-cache"); @@ -4886,7 +4992,7 @@ fn serve_parse( let worker_count = parallel_arg_parse(matches).map(|v| v.get()); - runtime_args_parse(flags, matches, true, true)?; + runtime_args_parse(flags, matches, true, true, true)?; // If the user didn't pass --allow-net, add this port to the network // allowlist. If the host is 0.0.0.0, we add :{port} and allow the same network perms // as if it was passed to --allow-net directly. @@ -4941,6 +5047,7 @@ fn task_parse(flags: &mut Flags, matches: &mut ArgMatches) { .unwrap_or(ConfigFlag::Discover); unstable_args_parse(flags, matches, UnstableArgsConfig::ResolutionAndRuntime); + node_modules_arg_parse(flags, matches); let mut task_flags = TaskFlags { cwd: matches.remove_one::("cwd"), @@ -4980,7 +5087,7 @@ fn test_parse( matches: &mut ArgMatches, ) -> clap::error::Result<()> { flags.type_check_mode = TypeCheckMode::Local; - runtime_args_parse(flags, matches, true, true)?; + runtime_args_parse(flags, matches, true, true, true)?; ext_arg_parse(flags, matches); // NOTE: `deno test` always uses `--no-prompt`, tests shouldn't ever do @@ -5345,6 +5452,7 @@ fn runtime_args_parse( matches: &mut ArgMatches, include_perms: bool, include_inspector: bool, + include_allow_scripts: bool, ) -> clap::error::Result<()> { unstable_args_parse(flags, matches, UnstableArgsConfig::ResolutionAndRuntime); compile_args_parse(flags, matches)?; @@ -5356,6 +5464,9 @@ fn runtime_args_parse( if include_inspector { inspect_arg_parse(flags, matches); } + if include_allow_scripts { + allow_scripts_arg_parse(flags, matches)?; + } location_arg_parse(flags, matches); v8_flags_arg_parse(flags, matches); seed_arg_parse(flags, matches); @@ -6707,6 +6818,32 @@ mod tests { ..Flags::default() } ); + + let r = flags_from_vec(svec!["deno", "fmt", "--ext", "html"]); + assert!(r.is_err()); + let r = flags_from_vec(svec!["deno", "fmt", "--ext", "html", "./**"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Fmt(FmtFlags { + check: false, + files: FileFlags { + include: vec!["./**".to_string()], + ignore: vec![], + }, + use_tabs: None, + line_width: None, + indent_width: None, + single_quote: None, + prose_wrap: None, + no_semicolons: None, + unstable_component: false, + watch: Default::default(), + }), + ext: Some("html".to_string()), + ..Flags::default() + } + ); } #[test] @@ -7355,7 +7492,7 @@ mod tests { #[test] fn repl_with_flags() { #[rustfmt::skip] - let r = flags_from_vec(svec!["deno", "repl", "-A", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--reload", "--lock", "lock.json", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--v8-flags=--help", "--seed", "1", "--inspect=127.0.0.1:9229", "--unsafely-ignore-certificate-errors", "--env=.example.env"]); + let r = flags_from_vec(svec!["deno", "repl", "-A", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--reload", "--lock", "lock.json", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--v8-flags=--help", "--seed", "1", "--inspect=127.0.0.1:9229", "--unsafely-ignore-certificate-errors", "--env=.example.env"]); assert_eq!( r.unwrap(), Flags { @@ -7403,7 +7540,6 @@ mod tests { allow_write: Some(vec![]), ..Default::default() }, - type_check_mode: TypeCheckMode::None, ..Flags::default() } ); @@ -7425,7 +7561,6 @@ mod tests { eval: None, is_default_command: false, }), - type_check_mode: TypeCheckMode::None, ..Flags::default() } ); @@ -8827,8 +8962,12 @@ mod tests { #[test] fn test_no_colon_in_value_name() { - let app = - runtime_args(Command::new("test_inspect_completion_value"), true, true); + let app = runtime_args( + Command::new("test_inspect_completion_value"), + true, + true, + false, + ); let inspect_args = app .get_arguments() .filter(|arg| arg.get_id() == "inspect") diff --git a/cli/args/flags_net.rs b/cli/args/flags_net.rs index 88ffcf0e46..abfcf28382 100644 --- a/cli/args/flags_net.rs +++ b/cli/args/flags_net.rs @@ -51,7 +51,7 @@ pub fn parse(paths: Vec) -> clap::error::Result> { } } else { NetDescriptor::parse(&host_and_port).map_err(|e| { - clap::Error::raw(clap::error::ErrorKind::InvalidValue, format!("{e:?}")) + clap::Error::raw(clap::error::ErrorKind::InvalidValue, e.to_string()) })?; out.push(host_and_port) } diff --git a/cli/args/lockfile.rs b/cli/args/lockfile.rs index 59ec7f0ef5..1805d26426 100644 --- a/cli/args/lockfile.rs +++ b/cli/args/lockfile.rs @@ -24,11 +24,20 @@ use crate::args::InstallKind; use deno_lockfile::Lockfile; +#[derive(Debug)] +pub struct CliLockfileReadFromPathOptions { + pub file_path: PathBuf, + pub frozen: bool, + /// Causes the lockfile to only be read from, but not written to. + pub skip_write: bool, +} + #[derive(Debug)] pub struct CliLockfile { lockfile: Mutex, pub filename: PathBuf, - pub frozen: bool, + frozen: bool, + skip_write: bool, } pub struct Guard<'a, T> { @@ -50,15 +59,6 @@ impl<'a, T> std::ops::DerefMut for Guard<'a, T> { } impl CliLockfile { - pub fn new(lockfile: Lockfile, frozen: bool) -> Self { - let filename = lockfile.filename.clone(); - Self { - lockfile: Mutex::new(lockfile), - filename, - frozen, - } - } - /// Get the inner deno_lockfile::Lockfile. pub fn lock(&self) -> Guard { Guard { @@ -78,6 +78,10 @@ impl CliLockfile { } pub fn write_if_changed(&self) -> Result<(), AnyError> { + if self.skip_write { + return Ok(()); + } + self.error_if_changed()?; let mut lockfile = self.lockfile.lock(); let Some(bytes) = lockfile.resolve_write_bytes() else { @@ -142,7 +146,7 @@ impl CliLockfile { return Ok(None); } - let filename = match flags.lock { + let file_path = match flags.lock { Some(ref lock) => PathBuf::from(lock), None => match workspace.resolve_lockfile_path()? { Some(path) => path, @@ -160,7 +164,11 @@ impl CliLockfile { .unwrap_or(false) }); - let lockfile = Self::read_from_path(filename, frozen)?; + let lockfile = Self::read_from_path(CliLockfileReadFromPathOptions { + file_path, + frozen, + skip_write: flags.internal.lockfile_skip_write, + })?; // initialize the lockfile with the workspace's configuration let root_url = workspace.root_dir(); @@ -212,25 +220,29 @@ impl CliLockfile { } pub fn read_from_path( - file_path: PathBuf, - frozen: bool, + opts: CliLockfileReadFromPathOptions, ) -> Result { - match std::fs::read_to_string(&file_path) { - Ok(text) => Ok(CliLockfile::new( - Lockfile::new(deno_lockfile::NewLockfileOptions { - file_path, - content: &text, - overwrite: false, - })?, - frozen, - )), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok( - CliLockfile::new(Lockfile::new_empty(file_path, false), frozen), - ), - Err(err) => Err(err).with_context(|| { - format!("Failed reading lockfile '{}'", file_path.display()) - }), - } + let lockfile = match std::fs::read_to_string(&opts.file_path) { + Ok(text) => Lockfile::new(deno_lockfile::NewLockfileOptions { + file_path: opts.file_path, + content: &text, + overwrite: false, + })?, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + Lockfile::new_empty(opts.file_path, false) + } + Err(err) => { + return Err(err).with_context(|| { + format!("Failed reading lockfile '{}'", opts.file_path.display()) + }); + } + }; + Ok(CliLockfile { + filename: lockfile.filename.clone(), + lockfile: Mutex::new(lockfile), + frozen: opts.frozen, + skip_write: opts.skip_write, + }) } pub fn error_if_changed(&self) -> Result<(), AnyError> { diff --git a/cli/args/mod.rs b/cli/args/mod.rs index c3a4c29377..50a37b3346 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -7,6 +7,7 @@ mod import_map; mod lockfile; mod package_json; +use deno_ast::MediaType; use deno_ast::SourceMapOption; use deno_config::deno_json::NodeModulesDirMode; use deno_config::workspace::CreateResolverOptions; @@ -20,20 +21,20 @@ use deno_config::workspace::WorkspaceDiscoverOptions; use deno_config::workspace::WorkspaceDiscoverStart; use deno_config::workspace::WorkspaceLintConfig; use deno_config::workspace::WorkspaceResolver; -use deno_core::normalize_path; use deno_core::resolve_url_or_path; use deno_graph::GraphKind; use deno_npm::npm_rc::NpmRc; use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use deno_npm::NpmSystemInfo; +use deno_path_util::normalize_path; +use deno_runtime::ops::otel::OtelConfig; use deno_semver::npm::NpmPackageReqReference; use import_map::resolve_import_map_value_from_specifier; pub use deno_config::deno_json::BenchConfig; pub use deno_config::deno_json::ConfigFile; pub use deno_config::deno_json::FmtOptionsConfig; -pub use deno_config::deno_json::JsxImportSourceConfig; pub use deno_config::deno_json::LintRulesConfig; pub use deno_config::deno_json::ProseWrap; pub use deno_config::deno_json::TsConfig; @@ -44,7 +45,9 @@ pub use deno_config::glob::FilePatterns; pub use deno_json::check_warn_tsconfig; pub use flags::*; pub use lockfile::CliLockfile; +pub use lockfile::CliLockfileReadFromPathOptions; pub use package_json::NpmInstallDepsProvider; +pub use package_json::PackageJsonDepValueParseWithLocationError; use deno_ast::ModuleSpecifier; use deno_core::anyhow::bail; @@ -199,6 +202,8 @@ pub fn ts_config_to_transpile_and_emit_options( precompile_jsx_dynamic_props: None, transform_jsx, var_decl_imports: false, + // todo(dsherret): support verbatim_module_syntax here properly + verbatim_module_syntax: false, }, deno_ast::EmitOptions { inline_sources: options.inline_sources, @@ -577,6 +582,7 @@ fn discover_npmrc( let resolved = npmrc .as_resolved(npm_registry_url()) .context("Failed to resolve .npmrc options")?; + log::debug!(".npmrc found at: '{}'", path.display()); Ok(Arc::new(resolved)) } @@ -817,18 +823,14 @@ impl CliOptions { }; let msg = format!("DANGER: TLS certificate validation is disabled {}", domains); - #[allow(clippy::print_stderr)] { - // use eprintln instead of log::warn so this always gets shown - eprintln!("{}", colors::yellow(msg)); + log::error!("{}", colors::yellow(msg)); } } - warn_insecure_allow_run_flags(&flags); - let maybe_lockfile = maybe_lockfile.filter(|_| !force_global_cache); let deno_dir_provider = - Arc::new(DenoDirProvider::new(flags.cache_path.clone())); + Arc::new(DenoDirProvider::new(flags.internal.cache_path.clone())); let maybe_node_modules_folder = resolve_node_modules_folder( &initial_cwd, &flags, @@ -964,6 +966,9 @@ impl CliOptions { match self.sub_command() { DenoSubcommand::Cache(_) => GraphKind::All, DenoSubcommand::Check(_) => GraphKind::TypesOnly, + DenoSubcommand::Install(InstallFlags { + kind: InstallKind::Local(_), + }) => GraphKind::All, _ => self.type_check_mode().as_graph_kind(), } } @@ -1123,6 +1128,10 @@ impl CliOptions { } } + pub fn otel_config(&self) -> Option { + self.flags.otel_config() + } + pub fn env_file_name(&self) -> Option<&String> { self.flags.env_file.as_ref() } @@ -1131,25 +1140,34 @@ impl CliOptions { self .main_module_cell .get_or_init(|| { - let main_module = match &self.flags.subcommand { + Ok(match &self.flags.subcommand { DenoSubcommand::Compile(compile_flags) => { resolve_url_or_path(&compile_flags.source_file, self.initial_cwd())? } DenoSubcommand::Eval(_) => { - resolve_url_or_path("./$deno$eval.ts", self.initial_cwd())? + resolve_url_or_path("./$deno$eval.mts", self.initial_cwd())? } DenoSubcommand::Repl(_) => { - resolve_url_or_path("./$deno$repl.ts", self.initial_cwd())? + resolve_url_or_path("./$deno$repl.mts", self.initial_cwd())? } DenoSubcommand::Run(run_flags) => { if run_flags.is_stdin() { - resolve_url_or_path("./$deno$stdin.ts", self.initial_cwd())? - } else if NpmPackageReqReference::from_str(&run_flags.script) - .is_ok() - { - ModuleSpecifier::parse(&run_flags.script)? + resolve_url_or_path("./$deno$stdin.mts", self.initial_cwd())? } else { - resolve_url_or_path(&run_flags.script, self.initial_cwd())? + let url = + resolve_url_or_path(&run_flags.script, self.initial_cwd())?; + if self.is_node_main() + && url.scheme() == "file" + && MediaType::from_specifier(&url) == MediaType::Unknown + { + try_resolve_node_binary_main_entrypoint( + &run_flags.script, + self.initial_cwd(), + )? + .unwrap_or(url) + } else { + url + } } } DenoSubcommand::Serve(run_flags) => { @@ -1158,9 +1176,7 @@ impl CliOptions { _ => { bail!("No main module.") } - }; - - Ok(main_module) + }) }) .as_ref() .map_err(|err| deno_core::anyhow::anyhow!("{}", err)) @@ -1209,7 +1225,7 @@ impl CliOptions { // This is triggered via a secret environment variable which is used // for functionality like child_process.fork. Users should NOT depend // on this functionality. - pub fn is_npm_main(&self) -> bool { + pub fn is_node_main(&self) -> bool { NPM_PROCESS_STATE.is_some() } @@ -1453,6 +1469,12 @@ impl CliOptions { watch: Some(WatchFlagsWithPaths { hmr, .. }), .. }) = &self.flags.subcommand + { + *hmr + } else if let DenoSubcommand::Serve(ServeFlags { + watch: Some(WatchFlagsWithPaths { hmr, .. }), + .. + }) = &self.flags.subcommand { *hmr } else { @@ -1581,6 +1603,13 @@ impl CliOptions { || self.workspace().has_unstable("bare-node-builtins") } + pub fn detect_cjs(&self) -> bool { + // only enabled when there's a package.json in order to not have a + // perf penalty for non-npm Deno projects of searching for the closest + // package.json beside each module + self.workspace().package_jsons().next().is_some() || self.is_node_main() + } + fn byonm_enabled(&self) -> bool { // check if enabled via unstable self.node_modules_dir().ok().flatten() == Some(NodeModulesDirMode::Manual) @@ -1591,6 +1620,15 @@ impl CliOptions { } pub fn use_byonm(&self) -> bool { + if matches!( + self.sub_command(), + DenoSubcommand::Install(_) + | DenoSubcommand::Add(_) + | DenoSubcommand::Remove(_) + ) { + // For `deno install/add/remove` we want to force the managed resolver so it can set up `node_modules/` directory. + return false; + } if self.node_modules_dir().ok().flatten().is_none() && self.maybe_node_modules_folder.is_some() && self @@ -1625,21 +1663,16 @@ impl CliOptions { }); if !from_config_file.is_empty() { - // collect unstable granular flags - let mut all_valid_unstable_flags: Vec<&str> = - crate::UNSTABLE_GRANULAR_FLAGS - .iter() - .map(|granular_flag| granular_flag.name) - .collect(); - - let mut another_unstable_flags = Vec::from([ - "sloppy-imports", - "byonm", - "bare-node-builtins", - "fmt-component", - ]); - // add more unstable flags to the same vector holding granular flags - all_valid_unstable_flags.append(&mut another_unstable_flags); + let all_valid_unstable_flags: Vec<&str> = crate::UNSTABLE_GRANULAR_FLAGS + .iter() + .map(|granular_flag| granular_flag.name) + .chain([ + "sloppy-imports", + "byonm", + "bare-node-builtins", + "fmt-component", + ]) + .collect(); // check and warn if the unstable flag of config file isn't supported, by // iterating through the vector holding the unstable flags @@ -1672,6 +1705,10 @@ impl CliOptions { if let DenoSubcommand::Run(RunFlags { watch: Some(WatchFlagsWithPaths { paths, .. }), .. + }) + | DenoSubcommand::Serve(ServeFlags { + watch: Some(WatchFlagsWithPaths { paths, .. }), + .. }) = &self.flags.subcommand { full_paths.extend(paths.iter().map(|path| self.initial_cwd.join(path))); @@ -1714,27 +1751,6 @@ impl CliOptions { } } -/// Warns for specific uses of `--allow-run`. This function is not -/// intended to catch every single possible insecure use of `--allow-run`, -/// but is just an attempt to discourage some common pitfalls. -fn warn_insecure_allow_run_flags(flags: &Flags) { - let permissions = &flags.permissions; - if permissions.allow_all { - return; - } - let Some(allow_run_list) = permissions.allow_run.as_ref() else { - return; - }; - - // discourage using --allow-run without an allow list - if allow_run_list.is_empty() { - log::warn!( - "{} --allow-run without an allow list is susceptible to exploits. Prefer specifying an allow list (https://docs.deno.com/runtime/fundamentals/security/#running-subprocesses)", - colors::yellow("Warning") - ); - } -} - /// Resolves the path to use for a local node_modules folder. fn resolve_node_modules_folder( cwd: &Path, @@ -1792,6 +1808,36 @@ fn resolve_node_modules_folder( Ok(Some(canonicalize_path_maybe_not_exists(&path)?)) } +fn try_resolve_node_binary_main_entrypoint( + specifier: &str, + initial_cwd: &Path, +) -> Result, AnyError> { + // node allows running files at paths without a `.js` extension + // or at directories with an index.js file + let path = deno_core::normalize_path(initial_cwd.join(specifier)); + if path.is_dir() { + let index_file = path.join("index.js"); + Ok(if index_file.is_file() { + Some(deno_path_util::url_from_file_path(&index_file)?) + } else { + None + }) + } else { + let path = path.with_extension( + path + .extension() + .and_then(|s| s.to_str()) + .map(|s| format!("{}.js", s)) + .unwrap_or("js".to_string()), + ); + if path.is_file() { + Ok(Some(deno_path_util::url_from_file_path(&path)?)) + } else { + Ok(None) + } + } +} + fn resolve_import_map_specifier( maybe_import_map_path: Option<&str>, maybe_config_file: Option<&ConfigFile>, diff --git a/cli/args/package_json.rs b/cli/args/package_json.rs index 2529e54fdf..7dc75550c3 100644 --- a/cli/args/package_json.rs +++ b/cli/args/package_json.rs @@ -5,9 +5,12 @@ use std::sync::Arc; use deno_config::workspace::Workspace; use deno_core::serde_json; +use deno_core::url::Url; use deno_package_json::PackageJsonDepValue; +use deno_package_json::PackageJsonDepValueParseError; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; +use thiserror::Error; #[derive(Debug)] pub struct InstallNpmRemotePkg { @@ -22,10 +25,20 @@ pub struct InstallNpmWorkspacePkg { pub target_dir: PathBuf, } +#[derive(Debug, Error, Clone)] +#[error("Failed to install '{}'\n at {}", alias, location)] +pub struct PackageJsonDepValueParseWithLocationError { + pub location: Url, + pub alias: String, + #[source] + pub source: PackageJsonDepValueParseError, +} + #[derive(Debug, Default)] pub struct NpmInstallDepsProvider { remote_pkgs: Vec, workspace_pkgs: Vec, + pkg_json_dep_errors: Vec, } impl NpmInstallDepsProvider { @@ -37,6 +50,7 @@ impl NpmInstallDepsProvider { // todo(dsherret): estimate capacity? let mut workspace_pkgs = Vec::new(); let mut remote_pkgs = Vec::new(); + let mut pkg_json_dep_errors = Vec::new(); let workspace_npm_pkgs = workspace.npm_packages(); for (_, folder) in workspace.config_folders() { @@ -83,8 +97,18 @@ impl NpmInstallDepsProvider { let deps = pkg_json.resolve_local_package_json_deps(); let mut pkg_pkgs = Vec::with_capacity(deps.len()); for (alias, dep) in deps { - let Ok(dep) = dep else { - continue; + let dep = match dep { + Ok(dep) => dep, + Err(err) => { + pkg_json_dep_errors.push( + PackageJsonDepValueParseWithLocationError { + location: pkg_json.specifier(), + alias, + source: err, + }, + ); + continue; + } }; match dep { PackageJsonDepValue::Req(pkg_req) => { @@ -131,14 +155,21 @@ impl NpmInstallDepsProvider { Self { remote_pkgs, workspace_pkgs, + pkg_json_dep_errors, } } - pub fn remote_pkgs(&self) -> &Vec { + pub fn remote_pkgs(&self) -> &[InstallNpmRemotePkg] { &self.remote_pkgs } - pub fn workspace_pkgs(&self) -> &Vec { + pub fn workspace_pkgs(&self) -> &[InstallNpmWorkspacePkg] { &self.workspace_pkgs } + + pub fn pkg_json_dep_errors( + &self, + ) -> &[PackageJsonDepValueParseWithLocationError] { + &self.pkg_json_dep_errors + } } diff --git a/cli/bench/encode_into.js b/cli/bench/encode_into.js index 11f5a56d90..ab5e11b04d 100644 --- a/cli/bench/encode_into.js +++ b/cli/bench/encode_into.js @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-console +// deno-lint-ignore-file no-console no-process-globals let [total, count] = typeof Deno !== "undefined" ? Deno.args diff --git a/cli/bench/getrandom.js b/cli/bench/getrandom.js index 3c3ec4aa19..fe99bbcbdf 100644 --- a/cli/bench/getrandom.js +++ b/cli/bench/getrandom.js @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-console +// deno-lint-ignore-file no-console no-process-globals let [total, count] = typeof Deno !== "undefined" ? Deno.args diff --git a/cli/bench/http.rs b/cli/bench/http.rs deleted file mode 100644 index f739b76ba8..0000000000 --- a/cli/bench/http.rs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -use std::collections::HashMap; -use std::net::TcpStream; -use std::path::Path; -use std::process::Command; -use std::sync::atomic::AtomicU16; -use std::sync::atomic::Ordering; -use std::time::Duration; -use std::time::Instant; - -use super::Result; - -pub use test_util::parse_wrk_output; -pub use test_util::WrkOutput as HttpBenchmarkResult; -// Some of the benchmarks in this file have been renamed. In case the history -// somehow gets messed up: -// "node_http" was once called "node" -// "deno_tcp" was once called "deno" -// "deno_http" was once called "deno_net_http" - -const DURATION: &str = "10s"; - -pub fn benchmark( - target_path: &Path, -) -> Result> { - let deno_exe = test_util::deno_exe_path(); - let deno_exe = deno_exe.to_string(); - - let hyper_hello_exe = target_path.join("test_server"); - let hyper_hello_exe = hyper_hello_exe.to_str().unwrap(); - - let mut res = HashMap::new(); - let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let http_dir = manifest_dir.join("bench").join("http"); - for entry in std::fs::read_dir(&http_dir)? { - let entry = entry?; - let pathbuf = entry.path(); - let path = pathbuf.to_str().unwrap(); - if path.ends_with(".lua") { - continue; - } - let file_stem = pathbuf.file_stem().unwrap().to_str().unwrap(); - - let lua_script = http_dir.join(format!("{file_stem}.lua")); - let mut maybe_lua = None; - if lua_script.exists() { - maybe_lua = Some(lua_script.to_str().unwrap()); - } - - let port = get_port(); - // deno run -A --unstable-net - res.insert( - file_stem.to_string(), - run( - &[ - deno_exe.as_str(), - "run", - "--allow-all", - "--unstable-net", - "--enable-testing-features-do-not-use", - path, - &server_addr(port), - ], - port, - None, - None, - maybe_lua, - )?, - ); - } - - res.insert("hyper".to_string(), hyper_http(hyper_hello_exe)?); - - Ok(res) -} - -fn run( - server_cmd: &[&str], - port: u16, - env: Option>, - origin_cmd: Option<&[&str]>, - lua_script: Option<&str>, -) -> Result { - // Wait for port 4544 to become available. - // TODO Need to use SO_REUSEPORT with tokio::net::TcpListener. - std::thread::sleep(Duration::from_secs(5)); - - let mut origin = None; - if let Some(cmd) = origin_cmd { - let mut com = Command::new(cmd[0]); - com.args(&cmd[1..]); - if let Some(env) = env.clone() { - com.envs(env); - } - origin = Some(com.spawn()?); - }; - - println!("{}", server_cmd.join(" ")); - let mut server = { - let mut com = Command::new(server_cmd[0]); - com.args(&server_cmd[1..]); - if let Some(env) = env { - com.envs(env); - } - com.spawn()? - }; - - // Wait for server to wake up. - let now = Instant::now(); - let addr = format!("127.0.0.1:{port}"); - while now.elapsed().as_secs() < 30 { - if TcpStream::connect(&addr).is_ok() { - break; - } - std::thread::sleep(Duration::from_millis(10)); - } - TcpStream::connect(&addr).expect("Failed to connect to server in time"); - println!("Server took {} ms to start", now.elapsed().as_millis()); - - let wrk = test_util::prebuilt_tool_path("wrk"); - assert!(wrk.is_file()); - - let addr = format!("http://{addr}/"); - let wrk = wrk.to_string(); - let mut wrk_cmd = vec![wrk.as_str(), "-d", DURATION, "--latency", &addr]; - - if let Some(lua_script) = lua_script { - wrk_cmd.push("-s"); - wrk_cmd.push(lua_script); - } - - println!("{}", wrk_cmd.join(" ")); - let output = test_util::run_collect(&wrk_cmd, None, None, None, true).0; - - std::thread::sleep(Duration::from_secs(1)); // wait to capture failure. TODO racy. - - println!("{output}"); - assert!( - server.try_wait()?.map(|s| s.success()).unwrap_or(true), - "server ended with error" - ); - - server.kill()?; - if let Some(mut origin) = origin { - origin.kill()?; - } - - Ok(parse_wrk_output(&output)) -} - -static NEXT_PORT: AtomicU16 = AtomicU16::new(4544); -pub(crate) fn get_port() -> u16 { - let p = NEXT_PORT.load(Ordering::SeqCst); - NEXT_PORT.store(p.wrapping_add(1), Ordering::SeqCst); - p -} - -fn server_addr(port: u16) -> String { - format!("0.0.0.0:{port}") -} - -fn hyper_http(exe: &str) -> Result { - let port = get_port(); - println!("http_benchmark testing RUST hyper"); - run(&[exe, &port.to_string()], port, None, None, None) -} diff --git a/cli/bench/http/deno_flash_hono_router.js b/cli/bench/http/deno_flash_hono_router.js deleted file mode 100644 index baced0cece..0000000000 --- a/cli/bench/http/deno_flash_hono_router.js +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { Hono } from "https://deno.land/x/hono@v2.0.9/mod.ts"; - -const addr = Deno.args[0] || "127.0.0.1:4500"; -const [hostname, port] = addr.split(":"); - -const app = new Hono(); -app.get("/", (c) => c.text("Hello, World!")); - -Deno.serve({ port: Number(port), hostname }, app.fetch); diff --git a/cli/bench/http/deno_flash_send_file.js b/cli/bench/http/deno_flash_send_file.js deleted file mode 100644 index bf8541f8b0..0000000000 --- a/cli/bench/http/deno_flash_send_file.js +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -const addr = Deno.args[0] || "127.0.0.1:4500"; -const [hostname, port] = addr.split(":"); -const { serve } = Deno; - -const path = new URL("../testdata/128k.bin", import.meta.url).pathname; - -function handler() { - const file = Deno.openSync(path); - return new Response(file.readable); -} - -serve({ hostname, port: Number(port) }, handler); diff --git a/cli/bench/http/deno_http_read_headers.lua b/cli/bench/http/deno_http_read_headers.lua deleted file mode 100644 index 64f1923ff3..0000000000 --- a/cli/bench/http/deno_http_read_headers.lua +++ /dev/null @@ -1,5 +0,0 @@ -wrk.headers["foo"] = "bar" -wrk.headers["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36" -wrk.headers["Viewport-Width"] = "1920" -wrk.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" -wrk.headers["Accept-Language"] = "en,la;q=0.9" \ No newline at end of file diff --git a/cli/bench/http/deno_http_serve.js b/cli/bench/http/deno_http_serve.js deleted file mode 100644 index 639982ce60..0000000000 --- a/cli/bench/http/deno_http_serve.js +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -const addr = Deno.args[0] ?? "127.0.0.1:4500"; -const [hostname, port] = addr.split(":"); -const { serve } = Deno; - -function handler() { - return new Response("Hello World"); -} - -serve({ hostname, port: Number(port), reusePort: true }, handler); diff --git a/cli/bench/http/deno_post_bin.lua b/cli/bench/http/deno_post_bin.lua deleted file mode 100644 index c8f5d3e3f7..0000000000 --- a/cli/bench/http/deno_post_bin.lua +++ /dev/null @@ -1,5 +0,0 @@ -wrk.method = "POST" -wrk.headers["Content-Type"] = "application/octet-stream" - -file = io.open("./cli/bench/testdata/128k.bin", "rb") -wrk.body = file:read("*a") \ No newline at end of file diff --git a/cli/bench/http/deno_post_json.lua b/cli/bench/http/deno_post_json.lua deleted file mode 100644 index cc6c4e226d..0000000000 --- a/cli/bench/http/deno_post_json.lua +++ /dev/null @@ -1,3 +0,0 @@ -wrk.method = "POST" -wrk.headers["Content-Type"] = "application/json" -wrk.body = '{"hello":"deno"}' \ No newline at end of file diff --git a/cli/bench/http/deno_reactdom_ssr_flash.jsx b/cli/bench/http/deno_reactdom_ssr_flash.jsx deleted file mode 100644 index eaabf89121..0000000000 --- a/cli/bench/http/deno_reactdom_ssr_flash.jsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -import { renderToReadableStream } from "https://esm.run/react-dom/server"; -import * as React from "https://esm.run/react"; -const { serve } = Deno; -const addr = Deno.args[0] || "127.0.0.1:4500"; -const [hostname, port] = addr.split(":"); - -const App = () => ( - - -

Hello World

- - -); - -const headers = { - headers: { - "Content-Type": "text/html", - }, -}; - -serve({ hostname, port: Number(port) }, async () => { - return new Response(await renderToReadableStream(), headers); -}); diff --git a/cli/bench/http/deno_tcp.ts b/cli/bench/http/deno_tcp.ts deleted file mode 100644 index b795910737..0000000000 --- a/cli/bench/http/deno_tcp.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// Used for benchmarking Deno's networking. -// TODO(bartlomieju): Replace this with a real HTTP server once -// https://github.com/denoland/deno/issues/726 is completed. -// Note: this is a keep-alive server. -// deno-lint-ignore-file no-console -const addr = Deno.args[0] || "127.0.0.1:4500"; -const [hostname, port] = addr.split(":"); -const listener = Deno.listen({ hostname, port: Number(port) }); -const response = new TextEncoder().encode( - "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n", -); -async function handle(conn: Deno.Conn): Promise { - const buffer = new Uint8Array(1024); - try { - while (true) { - await conn.read(buffer); - await conn.write(response); - } - } catch (e) { - if ( - !(e instanceof Deno.errors.BrokenPipe) && - !(e instanceof Deno.errors.ConnectionReset) - ) { - throw e; - } - } - conn.close(); -} - -console.log("Listening on", addr); -for await (const conn of listener) { - handle(conn); -} diff --git a/cli/bench/lsp.rs b/cli/bench/lsp.rs index b088865c6b..7baaffca7e 100644 --- a/cli/bench/lsp.rs +++ b/cli/bench/lsp.rs @@ -150,7 +150,11 @@ fn bench_big_file_edits(deno_exe: &Path) -> Duration { .deno_exe(deno_exe) .build(); client.initialize_default(); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.change_configuration(json!({ "deno": { "enable": true } })); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.write_notification( "textDocument/didOpen", @@ -206,6 +210,8 @@ fn bench_code_lens(deno_exe: &Path) -> Duration { .deno_exe(deno_exe) .build(); client.initialize_default(); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.change_configuration(json!({ "deno": { "enable": true, "codeLens": { @@ -214,6 +220,8 @@ fn bench_code_lens(deno_exe: &Path) -> Duration { "test": true, }, } })); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.write_notification( "textDocument/didOpen", @@ -257,7 +265,11 @@ fn bench_find_replace(deno_exe: &Path) -> Duration { .deno_exe(deno_exe) .build(); client.initialize_default(); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.change_configuration(json!({ "deno": { "enable": true } })); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); for i in 0..10 { client.write_notification( @@ -341,7 +353,11 @@ fn bench_startup_shutdown(deno_exe: &Path) -> Duration { .deno_exe(deno_exe) .build(); client.initialize_default(); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.change_configuration(json!({ "deno": { "enable": true } })); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.write_notification( "textDocument/didOpen", diff --git a/cli/bench/lsp_bench_standalone.rs b/cli/bench/lsp_bench_standalone.rs index 9c4f264ec9..3c946cfbe3 100644 --- a/cli/bench/lsp_bench_standalone.rs +++ b/cli/bench/lsp_bench_standalone.rs @@ -13,7 +13,11 @@ use test_util::lsp::LspClientBuilder; fn incremental_change_wait(bench: &mut Bencher) { let mut client = LspClientBuilder::new().use_diagnostic_sync(false).build(); client.initialize_default(); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.change_configuration(json!({ "deno": { "enable": true } })); + let (method, _): (String, Option) = client.read_notification(); + assert_eq!(method, "deno/didRefreshDenoConfigurationTree"); client.write_notification( "textDocument/didOpen", diff --git a/cli/bench/main.rs b/cli/bench/main.rs index 72fa7e9636..c3c42d2488 100644 --- a/cli/bench/main.rs +++ b/cli/bench/main.rs @@ -17,7 +17,6 @@ use std::process::Stdio; use std::time::SystemTime; use test_util::PathRef; -mod http; mod lsp; fn read_json(filename: &Path) -> Result { @@ -345,9 +344,11 @@ struct BenchResult { binary_size: HashMap, bundle_size: HashMap, cargo_deps: usize, + // TODO(bartlomieju): remove max_latency: HashMap, max_memory: HashMap, lsp_exec_time: HashMap, + // TODO(bartlomieju): remove req_per_sec: HashMap, syscall_count: HashMap, thread_count: HashMap, @@ -362,7 +363,6 @@ async fn main() -> Result<()> { "binary_size", "cargo_deps", "lsp", - "http", "strace", "mem_usage", ]; @@ -427,21 +427,6 @@ async fn main() -> Result<()> { new_data.lsp_exec_time = lsp_exec_times; } - if benchmarks.contains(&"http") && cfg!(not(target_os = "windows")) { - let stats = http::benchmark(target_dir.as_path())?; - let req_per_sec = stats - .iter() - .map(|(name, result)| (name.clone(), result.requests as i64)) - .collect(); - new_data.req_per_sec = req_per_sec; - let max_latency = stats - .iter() - .map(|(name, result)| (name.clone(), result.latency)) - .collect(); - - new_data.max_latency = max_latency; - } - if cfg!(target_os = "linux") && benchmarks.contains(&"strace") { use std::io::Read; diff --git a/cli/bench/op_now.js b/cli/bench/op_now.js index bcc3ea3c56..7c1427c809 100644 --- a/cli/bench/op_now.js +++ b/cli/bench/op_now.js @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-console +// deno-lint-ignore-file no-console no-process-globals const queueMicrotask = globalThis.queueMicrotask || process.nextTick; let [total, count] = typeof Deno !== "undefined" diff --git a/cli/bench/secure_curves.js b/cli/bench/secure_curves.js index 02d248b23f..912b75cccd 100644 --- a/cli/bench/secure_curves.js +++ b/cli/bench/secure_curves.js @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-console +// deno-lint-ignore-file no-console no-process-globals let [total, count] = typeof Deno !== "undefined" ? Deno.args diff --git a/cli/bench/tty.js b/cli/bench/tty.js index 248a901137..e494e76af7 100644 --- a/cli/bench/tty.js +++ b/cli/bench/tty.js @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-console +// deno-lint-ignore-file no-console no-process-globals const queueMicrotask = globalThis.queueMicrotask || process.nextTick; let [total, count] = typeof Deno !== "undefined" diff --git a/cli/bench/url_parse.js b/cli/bench/url_parse.js index 367cf73f46..9cb0045f64 100644 --- a/cli/bench/url_parse.js +++ b/cli/bench/url_parse.js @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-console +// deno-lint-ignore-file no-console no-process-globals const queueMicrotask = globalThis.queueMicrotask || process.nextTick; let [total, count] = typeof Deno !== "undefined" diff --git a/cli/bench/write_file.js b/cli/bench/write_file.js index 104a23a8db..747503ce2a 100644 --- a/cli/bench/write_file.js +++ b/cli/bench/write_file.js @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// deno-lint-ignore-file no-console +// deno-lint-ignore-file no-console no-process-globals const queueMicrotask = globalThis.queueMicrotask || process.nextTick; let [total, count] = typeof Deno !== "undefined" diff --git a/cli/build.rs b/cli/build.rs index c3b6f8b045..2678a8dbb0 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -365,6 +365,9 @@ fn main() { return; } + deno_napi::print_linker_flags("deno"); + deno_napi::print_linker_flags("denort"); + // Host snapshots won't work when cross compiling. let target = env::var("TARGET").unwrap(); let host = env::var("HOST").unwrap(); @@ -374,56 +377,6 @@ fn main() { panic!("Cross compiling with snapshot is not supported."); } - let symbols_file_name = match env::consts::OS { - "android" | "freebsd" | "openbsd" => { - "generated_symbol_exports_list_linux.def".to_string() - } - os => format!("generated_symbol_exports_list_{}.def", os), - }; - let symbols_path = std::path::Path::new("napi") - .join(symbols_file_name) - .canonicalize() - .expect( - "Missing symbols list! Generate using tools/napi/generate_symbols_lists.js", - ); - - #[cfg(target_os = "windows")] - println!( - "cargo:rustc-link-arg-bin=deno=/DEF:{}", - symbols_path.display() - ); - - #[cfg(target_os = "macos")] - println!( - "cargo:rustc-link-arg-bin=deno=-Wl,-exported_symbols_list,{}", - symbols_path.display() - ); - - #[cfg(target_os = "linux")] - { - // If a custom compiler is set, the glibc version is not reliable. - // Here, we assume that if a custom compiler is used, that it will be modern enough to support a dynamic symbol list. - if env::var("CC").is_err() - && glibc_version::get_version() - .map(|ver| ver.major <= 2 && ver.minor < 35) - .unwrap_or(false) - { - println!("cargo:warning=Compiling with all symbols exported, this will result in a larger binary. Please use glibc 2.35 or later for an optimised build."); - println!("cargo:rustc-link-arg-bin=deno=-rdynamic"); - } else { - println!( - "cargo:rustc-link-arg-bin=deno=-Wl,--export-dynamic-symbol-list={}", - symbols_path.display() - ); - } - } - - #[cfg(target_os = "android")] - println!( - "cargo:rustc-link-arg-bin=deno=-Wl,--export-dynamic-symbol-list={}", - symbols_path.display() - ); - // To debug snapshot issues uncomment: // op_fetch_asset::trace_serializer(); diff --git a/cli/cache/cache_db.rs b/cli/cache/cache_db.rs index b24078f29b..329ed2d970 100644 --- a/cli/cache/cache_db.rs +++ b/cli/cache/cache_db.rs @@ -57,7 +57,7 @@ impl rusqlite::types::FromSql for CacheDBHash { } /// What should the cache should do on failure? -#[derive(Default)] +#[derive(Debug, Default)] pub enum CacheFailure { /// Return errors if failure mode otherwise unspecified. #[default] @@ -69,6 +69,7 @@ pub enum CacheFailure { } /// Configuration SQL and other parameters for a [`CacheDB`]. +#[derive(Debug)] pub struct CacheDBConfiguration { /// SQL to run for a new database. pub table_initializer: &'static str, @@ -98,6 +99,7 @@ impl CacheDBConfiguration { } } +#[derive(Debug)] enum ConnectionState { Connected(Connection), Blackhole, @@ -106,7 +108,7 @@ enum ConnectionState { /// A cache database that eagerly initializes itself off-thread, preventing initialization operations /// from blocking the main thread. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct CacheDB { // TODO(mmastrac): We can probably simplify our thread-safe implementation here conn: Arc>>, diff --git a/cli/cache/deno_dir.rs b/cli/cache/deno_dir.rs index 88d8a31c04..7b7059c224 100644 --- a/cli/cache/deno_dir.rs +++ b/cli/cache/deno_dir.rs @@ -126,9 +126,9 @@ impl DenoDir { self.root.join("registries") } - /// Path to the dependencies cache folder. - pub fn deps_folder_path(&self) -> PathBuf { - self.root.join("deps") + /// Path to the remote cache folder. + pub fn remote_folder_path(&self) -> PathBuf { + self.root.join("remote") } /// Path to the origin data cache folder. diff --git a/cli/cache/emit.rs b/cli/cache/emit.rs index 6807f06c10..3c9eecfcbd 100644 --- a/cli/cache/emit.rs +++ b/cli/cache/emit.rs @@ -10,6 +10,7 @@ use deno_core::unsync::sync::AtomicFlag; use super::DiskCache; /// The cache that stores previously emitted files. +#[derive(Debug)] pub struct EmitCache { disk_cache: DiskCache, emit_failed_flag: AtomicFlag, @@ -39,7 +40,7 @@ impl EmitCache { &self, specifier: &ModuleSpecifier, expected_source_hash: u64, - ) -> Option> { + ) -> Option { let emit_filename = self.get_emit_filename(specifier)?; let bytes = self.disk_cache.get(&emit_filename).ok()?; self @@ -91,6 +92,7 @@ impl EmitCache { const LAST_LINE_PREFIX: &str = "\n// denoCacheMetadata="; +#[derive(Debug)] struct EmitFileSerializer { cli_version: &'static str, } @@ -100,7 +102,7 @@ impl EmitFileSerializer { &self, mut bytes: Vec, expected_source_hash: u64, - ) -> Option> { + ) -> Option { let last_newline_index = bytes.iter().rposition(|&b| b == b'\n')?; let (content, last_line) = bytes.split_at(last_newline_index); let hashes = last_line.strip_prefix(LAST_LINE_PREFIX.as_bytes())?; @@ -120,7 +122,7 @@ impl EmitFileSerializer { // everything looks good, truncate and return it bytes.truncate(content.len()); - Some(bytes) + String::from_utf8(bytes).ok() } pub fn serialize(&self, code: &[u8], source_hash: u64) -> Vec { @@ -170,8 +172,6 @@ mod test { }, emit_failed_flag: Default::default(), }; - let to_string = - |bytes: Vec| -> String { String::from_utf8(bytes).unwrap() }; let specifier1 = ModuleSpecifier::from_file_path(temp_dir.path().join("file1.ts")) @@ -188,13 +188,10 @@ mod test { assert_eq!(cache.get_emit_code(&specifier1, 5), None); // providing the correct source hash assert_eq!( - cache.get_emit_code(&specifier1, 10).map(to_string), + cache.get_emit_code(&specifier1, 10), Some(emit_code1.clone()), ); - assert_eq!( - cache.get_emit_code(&specifier2, 2).map(to_string), - Some(emit_code2) - ); + assert_eq!(cache.get_emit_code(&specifier2, 2), Some(emit_code2)); // try changing the cli version (should not load previous ones) let cache = EmitCache { @@ -215,18 +212,12 @@ mod test { }, emit_failed_flag: Default::default(), }; - assert_eq!( - cache.get_emit_code(&specifier1, 5).map(to_string), - Some(emit_code1) - ); + assert_eq!(cache.get_emit_code(&specifier1, 5), Some(emit_code1)); // adding when already exists should not cause issue let emit_code3 = "asdf".to_string(); cache.set_emit_code(&specifier1, 20, emit_code3.as_bytes()); assert_eq!(cache.get_emit_code(&specifier1, 5), None); - assert_eq!( - cache.get_emit_code(&specifier1, 20).map(to_string), - Some(emit_code3) - ); + assert_eq!(cache.get_emit_code(&specifier1, 20), Some(emit_code3)); } } diff --git a/cli/cache/mod.rs b/cli/cache/mod.rs index 2d88137165..50fc135ddf 100644 --- a/cli/cache/mod.rs +++ b/cli/cache/mod.rs @@ -8,9 +8,9 @@ use crate::file_fetcher::FetchOptions; use crate::file_fetcher::FetchPermissionsOptionRef; use crate::file_fetcher::FileFetcher; use crate::file_fetcher::FileOrRedirect; -use crate::npm::CliNpmResolver; use crate::util::fs::atomic_write_file_with_retries; -use crate::util::path::specifier_has_extension; +use crate::util::fs::atomic_write_file_with_retries_and_fs; +use crate::util::fs::AtomicWriteFileFsAdapter; use deno_ast::MediaType; use deno_core::futures; @@ -20,7 +20,9 @@ use deno_graph::source::CacheInfo; use deno_graph::source::LoadFuture; use deno_graph::source::LoadResponse; use deno_graph::source::Loader; +use deno_runtime::deno_fs; use deno_runtime::deno_permissions::PermissionsContainer; +use node_resolver::InNpmPackageChecker; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -77,8 +79,12 @@ impl deno_cache_dir::DenoCacheEnv for RealDenoCacheEnv { atomic_write_file_with_retries(path, bytes, CACHE_PERM) } - fn remove_file(&self, path: &Path) -> std::io::Result<()> { - std::fs::remove_file(path) + fn canonicalize_path(&self, path: &Path) -> std::io::Result { + crate::util::fs::canonicalize_path(path) + } + + fn create_dir_all(&self, path: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(path) } fn modified(&self, path: &Path) -> std::io::Result> { @@ -100,6 +106,66 @@ impl deno_cache_dir::DenoCacheEnv for RealDenoCacheEnv { } } +#[derive(Debug, Clone)] +pub struct DenoCacheEnvFsAdapter<'a>( + pub &'a dyn deno_runtime::deno_fs::FileSystem, +); + +impl<'a> deno_cache_dir::DenoCacheEnv for DenoCacheEnvFsAdapter<'a> { + fn read_file_bytes(&self, path: &Path) -> std::io::Result> { + self + .0 + .read_file_sync(path, None) + .map_err(|err| err.into_io_error()) + } + + fn atomic_write_file( + &self, + path: &Path, + bytes: &[u8], + ) -> std::io::Result<()> { + atomic_write_file_with_retries_and_fs( + &AtomicWriteFileFsAdapter { + fs: self.0, + write_mode: CACHE_PERM, + }, + path, + bytes, + ) + } + + fn canonicalize_path(&self, path: &Path) -> std::io::Result { + self.0.realpath_sync(path).map_err(|e| e.into_io_error()) + } + + fn create_dir_all(&self, path: &Path) -> std::io::Result<()> { + self + .0 + .mkdir_sync(path, true, None) + .map_err(|e| e.into_io_error()) + } + + fn modified(&self, path: &Path) -> std::io::Result> { + self + .0 + .stat_sync(path) + .map(|stat| { + stat + .mtime + .map(|ts| SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(ts)) + }) + .map_err(|e| e.into_io_error()) + } + + fn is_file(&self, path: &Path) -> bool { + self.0.is_file_sync(path) + } + + fn time_now(&self) -> SystemTime { + SystemTime::now() + } +} + pub type GlobalHttpCache = deno_cache_dir::GlobalHttpCache; pub type LocalHttpCache = deno_cache_dir::LocalHttpCache; pub type LocalLspHttpCache = @@ -116,28 +182,31 @@ pub struct FetchCacherOptions { /// A "wrapper" for the FileFetcher and DiskCache for the Deno CLI that provides /// a concise interface to the DENO_DIR when building module graphs. pub struct FetchCacher { - file_fetcher: Arc, pub file_header_overrides: HashMap>, + file_fetcher: Arc, + fs: Arc, global_http_cache: Arc, - npm_resolver: Arc, + in_npm_pkg_checker: Arc, module_info_cache: Arc, permissions: PermissionsContainer, - cache_info_enabled: bool, is_deno_publish: bool, + cache_info_enabled: bool, } impl FetchCacher { pub fn new( file_fetcher: Arc, + fs: Arc, global_http_cache: Arc, - npm_resolver: Arc, + in_npm_pkg_checker: Arc, module_info_cache: Arc, options: FetchCacherOptions, ) -> Self { Self { file_fetcher, + fs, global_http_cache, - npm_resolver, + in_npm_pkg_checker, module_info_cache, file_header_overrides: options.file_header_overrides, permissions: options.permissions, @@ -192,28 +261,21 @@ impl Loader for FetchCacher { ) -> LoadFuture { use deno_graph::source::CacheSetting as LoaderCacheSetting; - if specifier.scheme() == "file" { - if 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 = - crate::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 }, - )))); - } - } - - // make local CJS modules external to the graph - if specifier_has_extension(specifier, "cjs") { + 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 = crate::node::resolve_specifier_into_node_modules( + specifier, + self.fs.as_ref(), + ); + if self.in_npm_pkg_checker.in_npm_package(&specifier) { return Box::pin(futures::future::ready(Ok(Some( - LoadResponse::External { - specifier: specifier.clone(), - }, + LoadResponse::External { specifier }, )))); } } @@ -259,6 +321,7 @@ impl Loader for FetchCacher { } else { FetchPermissionsOptionRef::DynamicContainer(&permissions) }, + maybe_auth: None, maybe_accept: None, maybe_cache_setting: maybe_cache_setting.as_ref(), }, diff --git a/cli/cache/module_info.rs b/cli/cache/module_info.rs index 4dbb01c37b..060a6f4f0c 100644 --- a/cli/cache/module_info.rs +++ b/cli/cache/module_info.rs @@ -44,18 +44,32 @@ pub static MODULE_INFO_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { /// 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. +#[derive(Debug)] pub struct ModuleInfoCache { conn: CacheDB, + parsed_source_cache: Arc, } impl ModuleInfoCache { #[cfg(test)] - pub fn new_in_memory(version: &'static str) -> Self { - Self::new(CacheDB::in_memory(&MODULE_INFO_CACHE_DB, version)) + pub fn new_in_memory( + version: &'static str, + parsed_source_cache: Arc, + ) -> Self { + Self::new( + CacheDB::in_memory(&MODULE_INFO_CACHE_DB, version), + parsed_source_cache, + ) } - pub fn new(conn: CacheDB) -> Self { - Self { conn } + pub fn new( + conn: CacheDB, + parsed_source_cache: Arc, + ) -> Self { + Self { + conn, + parsed_source_cache, + } } /// Useful for testing: re-create this cache DB with a different current version. @@ -63,6 +77,7 @@ impl ModuleInfoCache { pub(crate) fn recreate_with_version(self, version: &'static str) -> Self { Self { conn: self.conn.recreate_with_version(version), + parsed_source_cache: self.parsed_source_cache, } } @@ -113,13 +128,10 @@ impl ModuleInfoCache { Ok(()) } - pub fn as_module_analyzer<'a>( - &'a self, - parsed_source_cache: &'a Arc, - ) -> ModuleInfoCacheModuleAnalyzer<'a> { + pub fn as_module_analyzer(&self) -> ModuleInfoCacheModuleAnalyzer { ModuleInfoCacheModuleAnalyzer { module_info_cache: self, - parsed_source_cache, + parsed_source_cache: &self.parsed_source_cache, } } } @@ -129,6 +141,84 @@ pub struct ModuleInfoCacheModuleAnalyzer<'a> { parsed_source_cache: &'a Arc, } +impl<'a> ModuleInfoCacheModuleAnalyzer<'a> { + fn load_cached_module_info( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + source_hash: CacheDBHash, + ) -> Option { + match self.module_info_cache.get_module_info( + specifier, + media_type, + source_hash, + ) { + Ok(Some(info)) => Some(info), + Ok(None) => None, + Err(err) => { + log::debug!( + "Error loading module cache info for {}. {:#}", + specifier, + err + ); + None + } + } + } + + fn save_module_info_to_cache( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + source_hash: CacheDBHash, + module_info: &ModuleInfo, + ) { + 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, + err + ); + } + } + + pub fn analyze_sync( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + source: &Arc, + ) -> Result { + // attempt to load from the cache + let source_hash = CacheDBHash::from_source(source); + if let Some(info) = + self.load_cached_module_info(specifier, media_type, source_hash) + { + return Ok(info); + } + + // otherwise, get the module info from the parsed source cache + let parser = self.parsed_source_cache.as_capturing_parser(); + let analyzer = ParserModuleAnalyzer::new(&parser); + let module_info = + analyzer.analyze_sync(specifier, source.clone(), media_type)?; + + // then attempt to cache it + self.save_module_info_to_cache( + specifier, + media_type, + source_hash, + &module_info, + ); + + Ok(module_info) + } +} + #[async_trait::async_trait(?Send)] impl<'a> deno_graph::ModuleAnalyzer for ModuleInfoCacheModuleAnalyzer<'a> { async fn analyze( @@ -139,20 +229,10 @@ impl<'a> deno_graph::ModuleAnalyzer for ModuleInfoCacheModuleAnalyzer<'a> { ) -> Result { // attempt to load from the cache let source_hash = CacheDBHash::from_source(&source); - match self.module_info_cache.get_module_info( - specifier, - media_type, - source_hash, - ) { - Ok(Some(info)) => return Ok(info), - Ok(None) => {} - Err(err) => { - log::debug!( - "Error loading module cache info for {}. {:#}", - specifier, - err - ); - } + if let Some(info) = + self.load_cached_module_info(specifier, media_type, source_hash) + { + return Ok(info); } // otherwise, get the module info from the parsed source cache @@ -169,18 +249,12 @@ impl<'a> deno_graph::ModuleAnalyzer for ModuleInfoCacheModuleAnalyzer<'a> { .unwrap()?; // then attempt to cache it - if let Err(err) = self.module_info_cache.set_module_info( + self.save_module_info_to_cache( specifier, media_type, source_hash, &module_info, - ) { - log::debug!( - "Error saving module cache info for {}. {:#}", - specifier, - err - ); - } + ); Ok(module_info) } @@ -202,7 +276,7 @@ fn serialize_media_type(media_type: MediaType) -> i64 { Tsx => 11, Json => 12, Wasm => 13, - TsBuildInfo => 14, + Css => 14, SourceMap => 15, Unknown => 16, } @@ -217,7 +291,7 @@ mod test { #[test] pub fn module_info_cache_general_use() { - let cache = ModuleInfoCache::new_in_memory("1.0.0"); + let cache = ModuleInfoCache::new_in_memory("1.0.0", Default::default()); let specifier1 = ModuleSpecifier::parse("https://localhost/mod.ts").unwrap(); let specifier2 = diff --git a/cli/cache/parsed_source.rs b/cli/cache/parsed_source.rs index e956361f46..7e819ae998 100644 --- a/cli/cache/parsed_source.rs +++ b/cli/cache/parsed_source.rs @@ -7,9 +7,9 @@ use deno_ast::MediaType; use deno_ast::ModuleSpecifier; use deno_ast::ParsedSource; use deno_core::parking_lot::Mutex; -use deno_graph::CapturingModuleParser; -use deno_graph::DefaultModuleParser; -use deno_graph::ModuleParser; +use deno_graph::CapturingEsParser; +use deno_graph::DefaultEsParser; +use deno_graph::EsParser; use deno_graph::ParseOptions; use deno_graph::ParsedSourceStore; @@ -46,7 +46,7 @@ impl<'a> LazyGraphSourceParser<'a> { } } -#[derive(Default)] +#[derive(Debug, Default)] pub struct ParsedSourceCache { sources: Mutex>, } @@ -57,12 +57,11 @@ impl ParsedSourceCache { module: &deno_graph::JsModule, ) -> Result { let parser = self.as_capturing_parser(); - // this will conditionally parse because it's using a CapturingModuleParser - parser.parse_module(ParseOptions { + // this will conditionally parse because it's using a CapturingEsParser + parser.parse_program(ParseOptions { specifier: &module.specifier, source: module.source.clone(), media_type: module.media_type, - // don't bother enabling because this method is currently only used for vendoring scope_analysis: false, }) } @@ -86,10 +85,9 @@ impl ParsedSourceCache { specifier, source, media_type, - // don't bother enabling because this method is currently only used for emitting scope_analysis: false, }; - DefaultModuleParser.parse_module(options) + DefaultEsParser.parse_program(options) } /// Frees the parsed source from memory. @@ -99,8 +97,8 @@ impl ParsedSourceCache { /// Creates a parser that will reuse a ParsedSource from the store /// if it exists, or else parse. - pub fn as_capturing_parser(&self) -> CapturingModuleParser { - CapturingModuleParser::new(None, self) + pub fn as_capturing_parser(&self) -> CapturingEsParser { + CapturingEsParser::new(None, self) } } diff --git a/cli/clippy.toml b/cli/clippy.toml index e20c56c47a..f1c25acfb8 100644 --- a/cli/clippy.toml +++ b/cli/clippy.toml @@ -1,5 +1,6 @@ disallowed-methods = [ { path = "reqwest::Client::new", reason = "create an HttpClient via an HttpClientProvider instead" }, + { path = "std::process::exit", reason = "use deno_runtime::exit instead" }, ] disallowed-types = [ { path = "reqwest::Client", reason = "use crate::http_util::HttpClient instead" }, diff --git a/cli/emit.rs b/cli/emit.rs index b3f4a4477a..3cd23b7abb 100644 --- a/cli/emit.rs +++ b/cli/emit.rs @@ -3,24 +3,28 @@ use crate::cache::EmitCache; use crate::cache::FastInsecureHasher; use crate::cache::ParsedSourceCache; +use crate::resolver::CjsTracker; +use deno_ast::ModuleKind; use deno_ast::SourceMapOption; use deno_ast::SourceRange; use deno_ast::SourceRanged; use deno_ast::SourceRangedForSpanned; +use deno_ast::TranspileModuleOptions; use deno_ast::TranspileResult; use deno_core::error::AnyError; use deno_core::futures::stream::FuturesUnordered; use deno_core::futures::FutureExt; use deno_core::futures::StreamExt; -use deno_core::ModuleCodeBytes; use deno_core::ModuleSpecifier; use deno_graph::MediaType; use deno_graph::Module; use deno_graph::ModuleGraph; use std::sync::Arc; +#[derive(Debug)] pub struct Emitter { + cjs_tracker: Arc, emit_cache: Arc, parsed_source_cache: Arc, transpile_and_emit_options: @@ -31,6 +35,7 @@ pub struct Emitter { impl Emitter { pub fn new( + cjs_tracker: Arc, emit_cache: Arc, parsed_source_cache: Arc, transpile_options: deno_ast::TranspileOptions, @@ -43,6 +48,7 @@ impl Emitter { hasher.finish() }; Self { + cjs_tracker, emit_cache, parsed_source_cache, transpile_and_emit_options: Arc::new((transpile_options, emit_options)), @@ -60,20 +66,19 @@ impl Emitter { continue; }; - let is_emittable = matches!( - module.media_type, - MediaType::TypeScript - | MediaType::Mts - | MediaType::Cts - | MediaType::Jsx - | MediaType::Tsx - ); - if is_emittable { + if module.media_type.is_emittable() { futures.push( self .emit_parsed_source( &module.specifier, module.media_type, + ModuleKind::from_is_cjs( + self.cjs_tracker.is_cjs_with_known_is_script( + &module.specifier, + module.media_type, + module.is_script, + )?, + ), &module.source, ) .boxed_local(), @@ -92,9 +97,10 @@ impl Emitter { pub fn maybe_cached_emit( &self, specifier: &ModuleSpecifier, + module_kind: deno_ast::ModuleKind, source: &str, - ) -> Option> { - let source_hash = self.get_source_hash(source); + ) -> Option { + let source_hash = self.get_source_hash(module_kind, source); self.emit_cache.get_emit_code(specifier, source_hash) } @@ -102,25 +108,27 @@ impl Emitter { &self, specifier: &ModuleSpecifier, media_type: MediaType, + module_kind: deno_ast::ModuleKind, source: &Arc, - ) -> Result { + ) -> Result { // Note: keep this in sync with the sync version below let helper = EmitParsedSourceHelper(self); - match helper.pre_emit_parsed_source(specifier, source) { + match helper.pre_emit_parsed_source(specifier, module_kind, source) { PreEmitResult::Cached(emitted_text) => Ok(emitted_text), PreEmitResult::NotCached { source_hash } => { let parsed_source_cache = self.parsed_source_cache.clone(); let transpile_and_emit_options = self.transpile_and_emit_options.clone(); - let transpile_result = deno_core::unsync::spawn_blocking({ + let transpiled_source = deno_core::unsync::spawn_blocking({ let specifier = specifier.clone(); let source = source.clone(); move || -> Result<_, AnyError> { EmitParsedSourceHelper::transpile( &parsed_source_cache, &specifier, - source.clone(), media_type, + module_kind, + source.clone(), &transpile_and_emit_options.0, &transpile_and_emit_options.1, ) @@ -128,11 +136,12 @@ impl Emitter { }) .await .unwrap()?; - Ok(helper.post_emit_parsed_source( + helper.post_emit_parsed_source( specifier, - transpile_result, + &transpiled_source, source_hash, - )) + ); + Ok(transpiled_source) } } } @@ -141,26 +150,29 @@ impl Emitter { &self, specifier: &ModuleSpecifier, media_type: MediaType, + module_kind: deno_ast::ModuleKind, source: &Arc, - ) -> Result { + ) -> Result { // Note: keep this in sync with the async version above let helper = EmitParsedSourceHelper(self); - match helper.pre_emit_parsed_source(specifier, source) { + match helper.pre_emit_parsed_source(specifier, module_kind, source) { PreEmitResult::Cached(emitted_text) => Ok(emitted_text), PreEmitResult::NotCached { source_hash } => { - let transpile_result = EmitParsedSourceHelper::transpile( + let transpiled_source = EmitParsedSourceHelper::transpile( &self.parsed_source_cache, specifier, - source.clone(), media_type, + module_kind, + source.clone(), &self.transpile_and_emit_options.0, &self.transpile_and_emit_options.1, )?; - Ok(helper.post_emit_parsed_source( + helper.post_emit_parsed_source( specifier, - transpile_result, + &transpiled_source, source_hash, - )) + ); + Ok(transpiled_source) } } } @@ -190,10 +202,20 @@ impl Emitter { // this statement is probably wrong) let mut options = self.transpile_and_emit_options.1.clone(); options.source_map = SourceMapOption::None; + let is_cjs = self.cjs_tracker.is_cjs_with_known_is_script( + specifier, + media_type, + parsed_source.compute_is_script(), + )?; let transpiled_source = parsed_source - .transpile(&self.transpile_and_emit_options.0, &options)? - .into_source() - .into_string()?; + .transpile( + &self.transpile_and_emit_options.0, + &deno_ast::TranspileModuleOptions { + module_kind: Some(ModuleKind::from_is_cjs(is_cjs)), + }, + &options, + )? + .into_source(); Ok(transpiled_source.text) } MediaType::JavaScript @@ -204,7 +226,7 @@ impl Emitter { | MediaType::Dcts | MediaType::Json | MediaType::Wasm - | MediaType::TsBuildInfo + | MediaType::Css | MediaType::SourceMap | MediaType::Unknown => { // clear this specifier from the parsed source cache as it's now out of date @@ -217,16 +239,17 @@ impl Emitter { /// A hashing function that takes the source code and uses the global emit /// 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 { + fn get_source_hash(&self, module_kind: ModuleKind, source_text: &str) -> u64 { FastInsecureHasher::new_without_deno_version() // stored in the transpile_and_emit_options_hash .write_str(source_text) .write_u64(self.transpile_and_emit_options_hash) + .write_hashable(module_kind) .finish() } } enum PreEmitResult { - Cached(ModuleCodeBytes), + Cached(String), NotCached { source_hash: u64 }, } @@ -237,14 +260,15 @@ impl<'a> EmitParsedSourceHelper<'a> { pub fn pre_emit_parsed_source( &self, specifier: &ModuleSpecifier, + module_kind: deno_ast::ModuleKind, source: &Arc, ) -> PreEmitResult { - let source_hash = self.0.get_source_hash(source); + let source_hash = self.0.get_source_hash(module_kind, source); if let Some(emit_code) = self.0.emit_cache.get_emit_code(specifier, source_hash) { - PreEmitResult::Cached(emit_code.into_boxed_slice().into()) + PreEmitResult::Cached(emit_code) } else { PreEmitResult::NotCached { source_hash } } @@ -253,25 +277,24 @@ impl<'a> EmitParsedSourceHelper<'a> { pub fn transpile( parsed_source_cache: &ParsedSourceCache, specifier: &ModuleSpecifier, - source: Arc, media_type: MediaType, + module_kind: deno_ast::ModuleKind, + source: Arc, transpile_options: &deno_ast::TranspileOptions, emit_options: &deno_ast::EmitOptions, - ) -> Result { + ) -> Result { // nothing else needs the parsed source at this point, so remove from // the cache in order to not transpile owned let parsed_source = parsed_source_cache .remove_or_parse_module(specifier, source, media_type)?; ensure_no_import_assertion(&parsed_source)?; - Ok(parsed_source.transpile(transpile_options, emit_options)?) - } - - pub fn post_emit_parsed_source( - &self, - specifier: &ModuleSpecifier, - transpile_result: TranspileResult, - source_hash: u64, - ) -> ModuleCodeBytes { + let transpile_result = parsed_source.transpile( + transpile_options, + &TranspileModuleOptions { + module_kind: Some(module_kind), + }, + emit_options, + )?; let transpiled_source = match transpile_result { TranspileResult::Owned(source) => source, TranspileResult::Cloned(source) => { @@ -280,12 +303,20 @@ impl<'a> EmitParsedSourceHelper<'a> { } }; debug_assert!(transpiled_source.source_map.is_none()); + Ok(transpiled_source.text) + } + + pub fn post_emit_parsed_source( + &self, + specifier: &ModuleSpecifier, + transpiled_source: &str, + source_hash: u64, + ) { self.0.emit_cache.set_emit_code( specifier, source_hash, - &transpiled_source.source, + transpiled_source.as_bytes(), ); - transpiled_source.source.into_boxed_slice().into() } } @@ -317,7 +348,7 @@ fn ensure_no_import_assertion( deno_core::anyhow::anyhow!("{}", msg) } - let Some(module) = parsed_source.program_ref().as_module() else { + let deno_ast::ProgramRef::Module(module) = parsed_source.program_ref() else { return Ok(()); }; diff --git a/cli/errors.rs b/cli/errors.rs index 25b3fc3324..b1808f7339 100644 --- a/cli/errors.rs +++ b/cli/errors.rs @@ -88,6 +88,10 @@ fn get_resolution_error_class(err: &ResolutionError) -> &'static str { } } +fn get_try_from_int_error_class(_: &std::num::TryFromIntError) -> &'static str { + "TypeError" +} + pub fn get_error_class_name(e: &AnyError) -> &'static str { deno_runtime::errors::get_error_class_name(e) .or_else(|| { @@ -106,5 +110,9 @@ pub fn get_error_class_name(e: &AnyError) -> &'static str { e.downcast_ref::() .map(get_resolution_error_class) }) + .or_else(|| { + e.downcast_ref::() + .map(get_try_from_int_error_class) + }) .unwrap_or("Error") } diff --git a/cli/factory.rs b/cli/factory.rs index 8aea635d2d..7949a83a55 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -11,6 +11,7 @@ use crate::args::StorageKeyResolver; use crate::args::TsConfigType; use crate::cache::Caches; use crate::cache::CodeCache; +use crate::cache::DenoCacheEnvFsAdapter; use crate::cache::DenoDir; use crate::cache::DenoDirProvider; use crate::cache::EmitCache; @@ -32,22 +33,30 @@ use crate::module_loader::ModuleLoadPreparer; use crate::node::CliCjsCodeAnalyzer; use crate::node::CliNodeCodeTranslator; use crate::npm::create_cli_npm_resolver; +use crate::npm::create_in_npm_pkg_checker; +use crate::npm::CliByonmNpmResolverCreateOptions; +use crate::npm::CliManagedInNpmPkgCheckerCreateOptions; +use crate::npm::CliManagedNpmResolverCreateOptions; use crate::npm::CliNpmResolver; -use crate::npm::CliNpmResolverByonmCreateOptions; use crate::npm::CliNpmResolverCreateOptions; -use crate::npm::CliNpmResolverManagedCreateOptions; use crate::npm::CliNpmResolverManagedSnapshotOption; -use crate::resolver::CjsResolutionStore; -use crate::resolver::CliGraphResolver; -use crate::resolver::CliGraphResolverOptions; -use crate::resolver::CliNodeResolver; +use crate::npm::CreateInNpmPkgCheckerOptions; +use crate::resolver::CjsTracker; +use crate::resolver::CliDenoResolver; +use crate::resolver::CliDenoResolverFs; +use crate::resolver::CliNpmReqResolver; +use crate::resolver::CliResolver; +use crate::resolver::CliResolverOptions; +use crate::resolver::CliSloppyImportsResolver; +use crate::resolver::IsCjsResolverOptions; use crate::resolver::NpmModuleLoader; -use crate::resolver::SloppyImportsResolver; +use crate::resolver::SloppyImportsCachedFs; use crate::standalone::DenoCompileBinaryWriter; use crate::tools::check::TypeChecker; use crate::tools::coverage::CoverageCollector; use crate::tools::lint::LintRuleProvider; use crate::tools::run::hmr::HmrRunner; +use crate::tsc::TypeCheckingCjsTracker; use crate::util::file_watcher::WatcherCommunicator; use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::progress_bar::ProgressBar; @@ -56,15 +65,20 @@ use crate::worker::CliMainWorkerFactory; use crate::worker::CliMainWorkerOptions; use std::path::PathBuf; +use deno_cache_dir::npm::NpmCacheDir; use deno_config::workspace::PackageJsonDepResolution; use deno_config::workspace::WorkspaceResolver; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::FeatureChecker; +use deno_resolver::npm::NpmReqResolverOptions; +use deno_resolver::DenoResolverOptions; +use deno_resolver::NodeAndNpmReqResolver; use deno_runtime::deno_fs; use deno_runtime::deno_node::DenoFsNodeResolverEnv; use deno_runtime::deno_node::NodeResolver; +use deno_runtime::deno_node::PackageJsonResolver; use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_tls::rustls::RootCertStore; @@ -74,6 +88,7 @@ use deno_runtime::inspector_server::InspectorServer; use deno_runtime::permissions::RuntimePermissionDescriptorParser; use log::warn; use node_resolver::analyze::NodeCodeTranslator; +use node_resolver::InNpmPackageChecker; use once_cell::sync::OnceCell; use std::future::Future; use std::sync::Arc; @@ -115,7 +130,7 @@ impl RootCertStoreProvider for CliRootCertStoreProvider { } } -struct Deferred(once_cell::unsync::OnceCell); +pub struct Deferred(once_cell::unsync::OnceCell); impl Default for Deferred { fn default() -> Self { @@ -161,38 +176,42 @@ impl Deferred { #[derive(Default)] struct CliFactoryServices { - cli_options: Deferred>, + blob_store: Deferred>, caches: Deferred>, + cjs_tracker: Deferred>, + cli_options: Deferred>, + code_cache: Deferred>, + deno_resolver: Deferred>, + emit_cache: Deferred>, + emitter: Deferred>, + feature_checker: Deferred>, file_fetcher: Deferred>, + fs: Deferred>, global_http_cache: Deferred>, http_cache: Deferred>, http_client_provider: Deferred>, - emit_cache: Deferred>, - emitter: Deferred>, - fs: Deferred>, + in_npm_pkg_checker: Deferred>, main_graph_container: Deferred>, - maybe_inspector_server: Deferred>>, - root_cert_store_provider: Deferred>, - blob_store: Deferred>, - module_info_cache: Deferred>, - parsed_source_cache: Deferred>, - resolver: Deferred>, maybe_file_watcher_reporter: Deferred>, + maybe_inspector_server: Deferred>>, module_graph_builder: Deferred>, module_graph_creator: Deferred>, + module_info_cache: Deferred>, module_load_preparer: Deferred>, node_code_translator: Deferred>, node_resolver: Deferred>, + npm_cache_dir: Deferred>, + npm_req_resolver: Deferred>, npm_resolver: Deferred>, + parsed_source_cache: Deferred>, permission_desc_parser: Deferred>, + pkg_json_resolver: Deferred>, + resolver: Deferred>, + root_cert_store_provider: Deferred>, root_permissions_container: Deferred, - sloppy_imports_resolver: Deferred>>, + sloppy_imports_resolver: Deferred>>, text_only_progress_bar: Deferred, type_checker: Deferred>, - cjs_resolutions: Deferred>, - cli_node_resolver: Deferred>, - feature_checker: Deferred>, - code_cache: Deferred>, workspace_resolver: Deferred>, } @@ -299,7 +318,7 @@ impl CliFactory { pub fn global_http_cache(&self) -> Result<&Arc, AnyError> { self.services.global_http_cache.get_or_try_init(|| { Ok(Arc::new(GlobalHttpCache::new( - self.deno_dir()?.deps_folder_path(), + self.deno_dir()?.remote_folder_path(), crate::cache::RealDenoCacheEnv, ))) }) @@ -349,72 +368,127 @@ impl CliFactory { self.services.fs.get_or_init(|| Arc::new(deno_fs::RealFs)) } + pub fn in_npm_pkg_checker( + &self, + ) -> Result<&Arc, AnyError> { + self.services.in_npm_pkg_checker.get_or_try_init(|| { + let cli_options = self.cli_options()?; + let options = if cli_options.use_byonm() { + CreateInNpmPkgCheckerOptions::Byonm + } else { + CreateInNpmPkgCheckerOptions::Managed( + CliManagedInNpmPkgCheckerCreateOptions { + root_cache_dir_url: self.npm_cache_dir()?.root_dir_url(), + maybe_node_modules_path: cli_options + .node_modules_dir_path() + .map(|p| p.as_path()), + }, + ) + }; + Ok(create_in_npm_pkg_checker(options)) + }) + } + + pub fn npm_cache_dir(&self) -> Result<&Arc, AnyError> { + self.services.npm_cache_dir.get_or_try_init(|| { + let fs = self.fs(); + let global_path = self.deno_dir()?.npm_folder_path(); + let cli_options = self.cli_options()?; + Ok(Arc::new(NpmCacheDir::new( + &DenoCacheEnvFsAdapter(fs.as_ref()), + global_path, + cli_options.npmrc().get_all_known_registries_urls(), + ))) + }) + } + pub async fn npm_resolver( &self, ) -> Result<&Arc, AnyError> { self .services .npm_resolver - .get_or_try_init_async(async { - let fs = self.fs(); - let cli_options = self.cli_options()?; - // For `deno install` we want to force the managed resolver so it can set up `node_modules/` directory. - create_cli_npm_resolver(if cli_options.use_byonm() && !matches!(cli_options.sub_command(), DenoSubcommand::Install(_) | DenoSubcommand::Add(_) | DenoSubcommand::Remove(_)) { - CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions { - fs: fs.clone(), - root_node_modules_dir: Some(match cli_options.node_modules_dir_path() { - Some(node_modules_path) => node_modules_path.to_path_buf(), - // path needs to be canonicalized for node resolution - // (node_modules_dir_path above is already canonicalized) - None => canonicalize_path_maybe_not_exists(cli_options.initial_cwd())? - .join("node_modules"), - }), - }) - } else { - CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions { - snapshot: match cli_options.resolve_npm_resolution_snapshot()? { - Some(snapshot) => { - CliNpmResolverManagedSnapshotOption::Specified(Some(snapshot)) - } - None => match cli_options.maybe_lockfile() { - Some(lockfile) => { - CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( - lockfile.clone(), - ) - } - None => CliNpmResolverManagedSnapshotOption::Specified(None), + .get_or_try_init_async( + async { + let fs = self.fs(); + let cli_options = self.cli_options()?; + create_cli_npm_resolver(if cli_options.use_byonm() { + CliNpmResolverCreateOptions::Byonm( + CliByonmNpmResolverCreateOptions { + fs: CliDenoResolverFs(fs.clone()), + pkg_json_resolver: self.pkg_json_resolver().clone(), + root_node_modules_dir: Some( + match cli_options.node_modules_dir_path() { + Some(node_modules_path) => node_modules_path.to_path_buf(), + // path needs to be canonicalized for node resolution + // (node_modules_dir_path above is already canonicalized) + None => canonicalize_path_maybe_not_exists( + cli_options.initial_cwd(), + )? + .join("node_modules"), + }, + ), }, - }, - maybe_lockfile: cli_options.maybe_lockfile().cloned(), - fs: fs.clone(), - http_client_provider: self.http_client_provider().clone(), - npm_global_cache_dir: self.deno_dir()?.npm_folder_path(), - cache_setting: cli_options.cache_setting(), - text_only_progress_bar: self.text_only_progress_bar().clone(), - maybe_node_modules_path: cli_options.node_modules_dir_path().cloned(), - npm_install_deps_provider: Arc::new(NpmInstallDepsProvider::from_workspace(cli_options.workspace())), - npm_system_info: cli_options.npm_system_info(), - npmrc: cli_options.npmrc().clone(), - lifecycle_scripts: cli_options.lifecycle_scripts_config(), + ) + } else { + CliNpmResolverCreateOptions::Managed( + CliManagedNpmResolverCreateOptions { + snapshot: match cli_options.resolve_npm_resolution_snapshot()? { + Some(snapshot) => { + CliNpmResolverManagedSnapshotOption::Specified(Some( + snapshot, + )) + } + None => match cli_options.maybe_lockfile() { + Some(lockfile) => { + CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( + lockfile.clone(), + ) + } + None => { + CliNpmResolverManagedSnapshotOption::Specified(None) + } + }, + }, + maybe_lockfile: cli_options.maybe_lockfile().cloned(), + fs: fs.clone(), + http_client_provider: self.http_client_provider().clone(), + npm_cache_dir: self.npm_cache_dir()?.clone(), + cache_setting: cli_options.cache_setting(), + text_only_progress_bar: self.text_only_progress_bar().clone(), + maybe_node_modules_path: cli_options + .node_modules_dir_path() + .cloned(), + npm_install_deps_provider: Arc::new( + NpmInstallDepsProvider::from_workspace( + cli_options.workspace(), + ), + ), + npm_system_info: cli_options.npm_system_info(), + npmrc: cli_options.npmrc().clone(), + lifecycle_scripts: cli_options.lifecycle_scripts_config(), + }, + ) }) - }).await - }.boxed_local()) + .await + } + .boxed_local(), + ) .await } pub fn sloppy_imports_resolver( &self, - ) -> Result>, AnyError> { + ) -> Result>, AnyError> { self .services .sloppy_imports_resolver .get_or_try_init(|| { - Ok( - self - .cli_options()? - .unstable_sloppy_imports() - .then(|| Arc::new(SloppyImportsResolver::new(self.fs().clone()))), - ) + Ok(self.cli_options()?.unstable_sloppy_imports().then(|| { + Arc::new(CliSloppyImportsResolver::new(SloppyImportsCachedFs::new( + self.fs().clone(), + ))) + })) }) .map(|maybe| maybe.as_ref()) } @@ -454,28 +528,47 @@ impl CliFactory { .await } - pub async fn resolver(&self) -> Result<&Arc, AnyError> { + pub async fn deno_resolver(&self) -> Result<&Arc, AnyError> { + self + .services + .deno_resolver + .get_or_try_init_async(async { + let cli_options = self.cli_options()?; + Ok(Arc::new(CliDenoResolver::new(DenoResolverOptions { + in_npm_pkg_checker: self.in_npm_pkg_checker()?.clone(), + node_and_req_resolver: if cli_options.no_npm() { + None + } else { + Some(NodeAndNpmReqResolver { + node_resolver: self.node_resolver().await?.clone(), + npm_req_resolver: self.npm_req_resolver().await?.clone(), + }) + }, + sloppy_imports_resolver: self.sloppy_imports_resolver()?.cloned(), + workspace_resolver: self.workspace_resolver().await?.clone(), + is_byonm: cli_options.use_byonm(), + maybe_vendor_dir: cli_options.vendor_dir_path(), + }))) + }) + .await + } + + pub async fn resolver(&self) -> Result<&Arc, AnyError> { self .services .resolver .get_or_try_init_async( async { let cli_options = self.cli_options()?; - Ok(Arc::new(CliGraphResolver::new(CliGraphResolverOptions { - sloppy_imports_resolver: self.sloppy_imports_resolver()?.cloned(), - node_resolver: Some(self.cli_node_resolver().await?.clone()), + Ok(Arc::new(CliResolver::new(CliResolverOptions { npm_resolver: if cli_options.no_npm() { None } else { Some(self.npm_resolver().await?.clone()) }, - workspace_resolver: self.workspace_resolver().await?.clone(), bare_node_builtins_enabled: cli_options .unstable_bare_node_builtins(), - maybe_jsx_import_source_config: cli_options - .workspace() - .to_maybe_jsx_import_source_config()?, - maybe_vendor_dir: cli_options.vendor_dir_path(), + deno_resolver: self.deno_resolver().await?.clone(), }))) } .boxed_local(), @@ -504,6 +597,7 @@ impl CliFactory { self.services.module_info_cache.get_or_try_init(|| { Ok(Arc::new(ModuleInfoCache::new( self.caches()?.dep_analysis_db(), + self.parsed_source_cache().clone(), ))) }) } @@ -532,6 +626,7 @@ impl CliFactory { ts_config_result.ts_config, )?; Ok(Arc::new(Emitter::new( + self.cjs_tracker()?.clone(), self.emit_cache()?.clone(), self.parsed_source_cache().clone(), transpile_options, @@ -555,7 +650,13 @@ impl CliFactory { async { Ok(Arc::new(NodeResolver::new( DenoFsNodeResolverEnv::new(self.fs().clone()), - self.npm_resolver().await?.clone().into_npm_resolver(), + self.in_npm_pkg_checker()?.clone(), + self + .npm_resolver() + .await? + .clone() + .into_npm_pkg_folder_resolver(), + self.pkg_json_resolver().clone(), ))) } .boxed_local(), @@ -573,23 +674,57 @@ impl CliFactory { let caches = self.caches()?; let node_analysis_cache = NodeAnalysisCache::new(caches.node_analysis_db()); - let node_resolver = self.cli_node_resolver().await?.clone(); + let node_resolver = self.node_resolver().await?.clone(); let cjs_esm_analyzer = CliCjsCodeAnalyzer::new( node_analysis_cache, + self.cjs_tracker()?.clone(), self.fs().clone(), - node_resolver, + Some(self.parsed_source_cache().clone()), ); Ok(Arc::new(NodeCodeTranslator::new( cjs_esm_analyzer, DenoFsNodeResolverEnv::new(self.fs().clone()), - self.node_resolver().await?.clone(), - self.npm_resolver().await?.clone().into_npm_resolver(), + self.in_npm_pkg_checker()?.clone(), + node_resolver, + self + .npm_resolver() + .await? + .clone() + .into_npm_pkg_folder_resolver(), + self.pkg_json_resolver().clone(), ))) }) .await } + pub async fn npm_req_resolver( + &self, + ) -> Result<&Arc, AnyError> { + self + .services + .npm_req_resolver + .get_or_try_init_async(async { + let npm_resolver = self.npm_resolver().await?; + Ok(Arc::new(CliNpmReqResolver::new(NpmReqResolverOptions { + byonm_resolver: (npm_resolver.clone()).into_maybe_byonm(), + fs: CliDenoResolverFs(self.fs().clone()), + in_npm_pkg_checker: self.in_npm_pkg_checker()?.clone(), + node_resolver: self.node_resolver().await?.clone(), + npm_req_resolver: npm_resolver.clone().into_npm_req_resolver(), + }))) + }) + .await + } + + pub fn pkg_json_resolver(&self) -> &Arc { + self.services.pkg_json_resolver.get_or_init(|| { + Arc::new(PackageJsonResolver::new(DenoFsNodeResolverEnv::new( + self.fs().clone(), + ))) + }) + } + pub async fn type_checker(&self) -> Result<&Arc, AnyError> { self .services @@ -598,6 +733,10 @@ impl CliFactory { let cli_options = self.cli_options()?; Ok(Arc::new(TypeChecker::new( self.caches()?.clone(), + Arc::new(TypeCheckingCjsTracker::new( + self.cjs_tracker()?.clone(), + self.module_info_cache()?.clone(), + )), cli_options.clone(), self.module_graph_builder().await?.clone(), self.node_resolver().await?.clone(), @@ -616,17 +755,19 @@ impl CliFactory { .get_or_try_init_async(async { let cli_options = self.cli_options()?; Ok(Arc::new(ModuleGraphBuilder::new( - cli_options.clone(), self.caches()?.clone(), + self.cjs_tracker()?.clone(), + cli_options.clone(), + self.file_fetcher()?.clone(), self.fs().clone(), - self.resolver().await?.clone(), - self.npm_resolver().await?.clone(), - self.module_info_cache()?.clone(), - self.parsed_source_cache().clone(), + self.global_http_cache()?.clone(), + self.in_npm_pkg_checker()?.clone(), cli_options.maybe_lockfile().cloned(), self.maybe_file_watcher_reporter().clone(), - self.file_fetcher()?.clone(), - self.global_http_cache()?.clone(), + self.module_info_cache()?.clone(), + self.npm_resolver().await?.clone(), + self.parsed_source_cache().clone(), + self.resolver().await?.clone(), self.root_permissions_container()?.clone(), ))) }) @@ -698,25 +839,18 @@ impl CliFactory { .await } - pub fn cjs_resolutions(&self) -> &Arc { - self.services.cjs_resolutions.get_or_init(Default::default) - } - - pub async fn cli_node_resolver( - &self, - ) -> Result<&Arc, AnyError> { - self - .services - .cli_node_resolver - .get_or_try_init_async(async { - Ok(Arc::new(CliNodeResolver::new( - self.cjs_resolutions().clone(), - self.fs().clone(), - self.node_resolver().await?.clone(), - self.npm_resolver().await?.clone(), - ))) - }) - .await + pub fn cjs_tracker(&self) -> Result<&Arc, AnyError> { + self.services.cjs_tracker.get_or_try_init(|| { + let options = self.cli_options()?; + Ok(Arc::new(CjsTracker::new( + self.in_npm_pkg_checker()?.clone(), + self.pkg_json_resolver().clone(), + IsCjsResolverOptions { + detect_cjs: options.detect_cjs(), + is_node_main: options.is_node_main(), + }, + ))) + }) } pub fn permission_desc_parser( @@ -749,7 +883,9 @@ impl CliFactory { ) -> Result { let cli_options = self.cli_options()?; Ok(DenoCompileBinaryWriter::new( + self.cjs_tracker()?, self.deno_dir()?, + self.emitter()?, self.file_fetcher()?, self.http_client_provider(), self.npm_resolver().await?.as_ref(), @@ -778,15 +914,20 @@ impl CliFactory { &self, ) -> Result { let cli_options = self.cli_options()?; + let fs = self.fs(); let node_resolver = self.node_resolver().await?; let npm_resolver = self.npm_resolver().await?; - let fs = self.fs(); - let cli_node_resolver = self.cli_node_resolver().await?; + let cli_npm_resolver = self.npm_resolver().await?.clone(); + let in_npm_pkg_checker = self.in_npm_pkg_checker()?; let maybe_file_watcher_communicator = if cli_options.has_hmr() { Some(self.watcher_communicator.clone().unwrap()) } else { None }; + let node_code_translator = self.node_code_translator().await?; + let cjs_tracker = self.cjs_tracker()?.clone(); + let pkg_json_resolver = self.pkg_json_resolver().clone(); + let npm_req_resolver = self.npm_req_resolver().await?; Ok(CliMainWorkerFactory::new( self.blob_store().clone(), @@ -796,38 +937,44 @@ impl CliFactory { None }, self.feature_checker()?.clone(), - self.fs().clone(), + fs.clone(), maybe_file_watcher_communicator, self.maybe_inspector_server()?.clone(), cli_options.maybe_lockfile().cloned(), Box::new(CliModuleLoaderFactory::new( cli_options, + cjs_tracker, if cli_options.code_cache_enabled() { Some(self.code_cache()?.clone()) } else { None }, self.emitter()?.clone(), + fs.clone(), + in_npm_pkg_checker.clone(), self.main_module_graph_container().await?.clone(), self.module_load_preparer().await?.clone(), - cli_node_resolver.clone(), + node_code_translator.clone(), + node_resolver.clone(), + npm_req_resolver.clone(), + cli_npm_resolver.clone(), NpmModuleLoader::new( - self.cjs_resolutions().clone(), - self.node_code_translator().await?.clone(), + self.cjs_tracker()?.clone(), fs.clone(), - cli_node_resolver.clone(), + node_code_translator.clone(), ), self.parsed_source_cache().clone(), self.resolver().await?.clone(), )), node_resolver.clone(), npm_resolver.clone(), - self.permission_desc_parser()?.clone(), + pkg_json_resolver, self.root_cert_store_provider().clone(), self.root_permissions_container()?.clone(), StorageKeyResolver::from_options(cli_options), cli_options.sub_command().clone(), self.create_cli_main_worker_options()?, + self.cli_options()?.otel_config(), )) } @@ -876,7 +1023,6 @@ impl CliFactory { inspect_wait: cli_options.inspect_wait().is_some(), strace_ops: cli_options.strace_ops().clone(), is_inspecting: cli_options.is_inspecting(), - is_npm_main: cli_options.is_npm_main(), location: cli_options.location_flag().clone(), // if the user ran a binary command, we'll need to set process.argv[0] // to be the name of the binary command instead of deno diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index ca11449393..95d778f0bb 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -21,9 +21,10 @@ use deno_core::url::Url; use deno_core::ModuleSpecifier; use deno_graph::source::LoaderChecksum; +use deno_path_util::url_to_file_path; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_web::BlobStore; -use deno_runtime::fs_util::specifier_to_file_path; +use http::header; use log::debug; use std::borrow::Cow; use std::collections::HashMap; @@ -136,7 +137,7 @@ impl MemoryFiles { /// Fetch a source file from the local file system. fn fetch_local(specifier: &ModuleSpecifier) -> Result { - let local = specifier_to_file_path(specifier).map_err(|_| { + let local = url_to_file_path(specifier).map_err(|_| { uri_error(format!("Invalid file path.\n Specifier: {specifier}")) })?; // If it doesnt have a extension, we want to treat it as typescript by default @@ -181,6 +182,7 @@ pub enum FetchPermissionsOptionRef<'a> { pub struct FetchOptions<'a> { pub specifier: &'a ModuleSpecifier, pub permissions: FetchPermissionsOptionRef<'a>, + pub maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, pub maybe_accept: Option<&'a str>, pub maybe_cache_setting: Option<&'a CacheSetting>, } @@ -333,7 +335,7 @@ impl FileFetcher { ) })?; - let bytes = blob.read_all().await?; + let bytes = blob.read_all().await; let headers = HashMap::from([("content-type".to_string(), blob.media_type.clone())]); @@ -350,6 +352,7 @@ impl FileFetcher { maybe_accept: Option<&str>, cache_setting: &CacheSetting, maybe_checksum: Option<&LoaderChecksum>, + maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, ) -> Result { debug!( "FileFetcher::fetch_remote_no_follow - specifier: {}", @@ -442,6 +445,7 @@ impl FileFetcher { .as_ref() .map(|(_, etag)| etag.clone()), maybe_auth_token: maybe_auth_token.clone(), + maybe_auth: maybe_auth.clone(), maybe_progress_guard: maybe_progress_guard.as_ref(), }) .await? @@ -538,7 +542,18 @@ impl FileFetcher { specifier: &ModuleSpecifier, ) -> Result { self - .fetch_inner(specifier, FetchPermissionsOptionRef::AllowAll) + .fetch_inner(specifier, None, FetchPermissionsOptionRef::AllowAll) + .await + } + + #[inline(always)] + pub async fn fetch_bypass_permissions_with_maybe_auth( + &self, + specifier: &ModuleSpecifier, + maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, + ) -> Result { + self + .fetch_inner(specifier, maybe_auth, FetchPermissionsOptionRef::AllowAll) .await } @@ -552,6 +567,7 @@ impl FileFetcher { self .fetch_inner( specifier, + None, FetchPermissionsOptionRef::StaticContainer(permissions), ) .await @@ -560,12 +576,14 @@ impl FileFetcher { async fn fetch_inner( &self, specifier: &ModuleSpecifier, + maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, permissions: FetchPermissionsOptionRef<'_>, ) -> Result { self .fetch_with_options(FetchOptions { specifier, permissions, + maybe_auth, maybe_accept: None, maybe_cache_setting: None, }) @@ -585,12 +603,14 @@ impl FileFetcher { max_redirect: usize, ) -> Result { let mut specifier = Cow::Borrowed(options.specifier); + let mut maybe_auth = options.maybe_auth.clone(); for _ in 0..=max_redirect { match self .fetch_no_follow_with_options(FetchNoFollowOptions { fetch_options: FetchOptions { specifier: &specifier, permissions: options.permissions, + maybe_auth: maybe_auth.clone(), maybe_accept: options.maybe_accept, maybe_cache_setting: options.maybe_cache_setting, }, @@ -602,6 +622,10 @@ impl FileFetcher { return Ok(file); } FileOrRedirect::Redirect(redirect_specifier) => { + // If we were redirected to another origin, don't send the auth header anymore. + if redirect_specifier.origin() != specifier.origin() { + maybe_auth = None; + } specifier = Cow::Owned(redirect_specifier); } } @@ -666,6 +690,7 @@ impl FileFetcher { options.maybe_accept, options.maybe_cache_setting.unwrap_or(&self.cache_setting), maybe_checksum, + options.maybe_auth, ) .await } @@ -726,7 +751,7 @@ mod tests { maybe_temp_dir: Option, ) -> (FileFetcher, TempDir, Arc) { let temp_dir = maybe_temp_dir.unwrap_or_default(); - let location = temp_dir.path().join("deps").to_path_buf(); + let location = temp_dir.path().join("remote").to_path_buf(); let blob_store: Arc = Default::default(); let file_fetcher = FileFetcher::new( Arc::new(GlobalHttpCache::new(location, RealDenoCacheEnv)), @@ -756,6 +781,7 @@ mod tests { FetchOptions { specifier, permissions: FetchPermissionsOptionRef::AllowAll, + maybe_auth: None, maybe_accept: None, maybe_cache_setting: Some(&file_fetcher.cache_setting), }, @@ -964,7 +990,7 @@ mod tests { // This creates a totally new instance, simulating another Deno process // invocation and indicates to "cache bust". - let location = temp_dir.path().join("deps").to_path_buf(); + let location = temp_dir.path().join("remote").to_path_buf(); let file_fetcher = FileFetcher::new( Arc::new(GlobalHttpCache::new( location, @@ -990,7 +1016,7 @@ mod tests { async fn test_fetch_uses_cache() { let _http_server_guard = test_util::http_server(); let temp_dir = TempDir::new(); - let location = temp_dir.path().join("deps").to_path_buf(); + let location = temp_dir.path().join("remote").to_path_buf(); let specifier = resolve_url("http://localhost:4545/subdir/mismatch_ext.ts").unwrap(); @@ -1156,7 +1182,7 @@ mod tests { async fn test_fetch_uses_cache_with_redirects() { let _http_server_guard = test_util::http_server(); let temp_dir = TempDir::new(); - let location = temp_dir.path().join("deps").to_path_buf(); + let location = temp_dir.path().join("remote").to_path_buf(); let specifier = resolve_url("http://localhost:4548/subdir/mismatch_ext.ts").unwrap(); let redirected_specifier = @@ -1255,6 +1281,7 @@ mod tests { FetchOptions { specifier: &specifier, permissions: FetchPermissionsOptionRef::AllowAll, + maybe_auth: None, maybe_accept: None, maybe_cache_setting: Some(&file_fetcher.cache_setting), }, @@ -1268,6 +1295,7 @@ mod tests { FetchOptions { specifier: &specifier, permissions: FetchPermissionsOptionRef::AllowAll, + maybe_auth: None, maybe_accept: None, maybe_cache_setting: Some(&file_fetcher.cache_setting), }, @@ -1324,7 +1352,7 @@ mod tests { async fn test_fetch_no_remote() { let _http_server_guard = test_util::http_server(); let temp_dir = TempDir::new(); - let location = temp_dir.path().join("deps").to_path_buf(); + let location = temp_dir.path().join("remote").to_path_buf(); let file_fetcher = FileFetcher::new( Arc::new(GlobalHttpCache::new( location, @@ -1350,7 +1378,7 @@ mod tests { async fn test_fetch_cache_only() { let _http_server_guard = test_util::http_server(); let temp_dir = TempDir::new(); - let location = temp_dir.path().join("deps").to_path_buf(); + let location = temp_dir.path().join("remote").to_path_buf(); let file_fetcher_01 = FileFetcher::new( Arc::new(GlobalHttpCache::new(location.clone(), RealDenoCacheEnv)), CacheSetting::Only, diff --git a/cli/graph_util.rs b/cli/graph_util.rs index 17f7d6739f..6ed0506dd7 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -13,14 +13,19 @@ use crate::colors; use crate::errors::get_error_class_name; use crate::file_fetcher::FileFetcher; use crate::npm::CliNpmResolver; -use crate::resolver::CliGraphResolver; -use crate::resolver::SloppyImportsResolver; +use crate::resolver::CjsTracker; +use crate::resolver::CliResolver; +use crate::resolver::CliSloppyImportsResolver; +use crate::resolver::SloppyImportsCachedFs; use crate::tools::check; use crate::tools::check::TypeChecker; use crate::util::file_watcher::WatcherCommunicator; use crate::util::fs::canonicalize_path; +use deno_config::deno_json::JsxImportSourceConfig; use deno_config::workspace::JsrPackageConfig; +use deno_core::anyhow::bail; use deno_graph::source::LoaderChecksum; +use deno_graph::source::ResolutionMode; use deno_graph::FillFromLockfileOptions; use deno_graph::JsrLoadError; use deno_graph::ModuleLoadError; @@ -31,7 +36,6 @@ use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::ModuleSpecifier; use deno_graph::source::Loader; -use deno_graph::source::ResolutionMode; use deno_graph::source::ResolveError; use deno_graph::GraphKind; use deno_graph::ModuleError; @@ -39,13 +43,15 @@ use deno_graph::ModuleGraph; use deno_graph::ModuleGraphError; use deno_graph::ResolutionError; use deno_graph::SpecifierError; +use deno_path_util::url_to_file_path; +use deno_resolver::sloppy_imports::SloppyImportsResolutionMode; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node; use deno_runtime::deno_permissions::PermissionsContainer; -use deno_runtime::fs_util::specifier_to_file_path; use deno_semver::jsr::JsrDepPackageReq; use deno_semver::package::PackageNv; use import_map::ImportMapError; +use node_resolver::InNpmPackageChecker; use std::collections::HashSet; use std::error::Error; use std::ops::Deref; @@ -182,7 +188,7 @@ pub fn graph_exit_integrity_errors(graph: &ModuleGraph) { fn exit_for_integrity_error(err: &ModuleError) { if let Some(err_message) = enhanced_integrity_error_message(err) { log::error!("{} {}", colors::red("error:"), err_message); - std::process::exit(10); + deno_runtime::exit(10); } } @@ -375,48 +381,54 @@ pub struct BuildFastCheckGraphOptions<'a> { } pub struct ModuleGraphBuilder { - options: Arc, caches: Arc, + cjs_tracker: Arc, + cli_options: Arc, + file_fetcher: Arc, fs: Arc, - resolver: Arc, - npm_resolver: Arc, - module_info_cache: Arc, - parsed_source_cache: Arc, + global_http_cache: Arc, + in_npm_pkg_checker: Arc, lockfile: Option>, maybe_file_watcher_reporter: Option, - file_fetcher: Arc, - global_http_cache: Arc, + module_info_cache: Arc, + npm_resolver: Arc, + parsed_source_cache: Arc, + resolver: Arc, root_permissions_container: PermissionsContainer, } impl ModuleGraphBuilder { #[allow(clippy::too_many_arguments)] pub fn new( - options: Arc, caches: Arc, + cjs_tracker: Arc, + cli_options: Arc, + file_fetcher: Arc, fs: Arc, - resolver: Arc, - npm_resolver: Arc, - module_info_cache: Arc, - parsed_source_cache: Arc, + global_http_cache: Arc, + in_npm_pkg_checker: Arc, lockfile: Option>, maybe_file_watcher_reporter: Option, - file_fetcher: Arc, - global_http_cache: Arc, + module_info_cache: Arc, + npm_resolver: Arc, + parsed_source_cache: Arc, + resolver: Arc, root_permissions_container: PermissionsContainer, ) -> Self { Self { - options, caches, + cjs_tracker, + cli_options, + file_fetcher, fs, - resolver, - npm_resolver, - module_info_cache, - parsed_source_cache, + global_http_cache, + in_npm_pkg_checker, lockfile, maybe_file_watcher_reporter, - file_fetcher, - global_http_cache, + module_info_cache, + npm_resolver, + parsed_source_cache, + resolver, root_permissions_container, } } @@ -502,19 +514,17 @@ impl ModuleGraphBuilder { } let maybe_imports = if options.graph_kind.include_types() { - self.options.to_compiler_option_types()? + self.cli_options.to_compiler_option_types()? } else { Vec::new() }; - let analyzer = self - .module_info_cache - .as_module_analyzer(&self.parsed_source_cache); + let analyzer = self.module_info_cache.as_module_analyzer(); let mut loader = match options.loader { Some(loader) => MutLoaderRef::Borrowed(loader), None => MutLoaderRef::Owned(self.create_graph_loader()), }; let cli_resolver = &self.resolver; - let graph_resolver = cli_resolver.as_graph_resolver(); + let graph_resolver = self.create_graph_resolver()?; let graph_npm_resolver = cli_resolver.create_graph_npm_resolver(); let maybe_file_watcher_reporter = self .maybe_file_watcher_reporter @@ -539,7 +549,7 @@ impl ModuleGraphBuilder { npm_resolver: Some(&graph_npm_resolver), module_analyzer: &analyzer, reporter: maybe_file_watcher_reporter, - resolver: Some(graph_resolver), + resolver: Some(&graph_resolver), locker: locker.as_mut().map(|l| l as _), }, ) @@ -556,7 +566,7 @@ impl ModuleGraphBuilder { // ensure an "npm install" is done if the user has explicitly // opted into using a node_modules directory if self - .options + .cli_options .node_modules_dir()? .map(|m| m.uses_node_modules_dir()) .unwrap_or(false) @@ -592,6 +602,12 @@ impl ModuleGraphBuilder { let initial_package_deps_len = graph.packages.package_deps_sum(); let initial_package_mappings_len = graph.packages.mappings().len(); + if roots.iter().any(|r| r.scheme() == "npm") + && self.npm_resolver.as_byonm().is_some() + { + bail!("Resolving npm specifier entrypoints this way is currently not supported with \"nodeModules\": \"manual\". In the meantime, try with --node-modules-dir=auto instead"); + } + graph.build(roots, loader, options).await; let has_redirects_changed = graph.redirects.len() != initial_redirects_len; @@ -656,16 +672,16 @@ impl ModuleGraphBuilder { }; let parser = self.parsed_source_cache.as_capturing_parser(); let cli_resolver = &self.resolver; - let graph_resolver = cli_resolver.as_graph_resolver(); + let graph_resolver = self.create_graph_resolver()?; let graph_npm_resolver = cli_resolver.create_graph_npm_resolver(); graph.build_fast_check_type_graph( deno_graph::BuildFastCheckTypeGraphOptions { - jsr_url_provider: &CliJsrUrlProvider, + es_parser: Some(&parser), fast_check_cache: fast_check_cache.as_ref().map(|c| c as _), fast_check_dts: false, - module_parser: Some(&parser), - resolver: Some(graph_resolver), + jsr_url_provider: &CliJsrUrlProvider, + resolver: Some(&graph_resolver), npm_resolver: Some(&graph_npm_resolver), workspace_fast_check: options.workspace_fast_check, }, @@ -684,14 +700,15 @@ impl ModuleGraphBuilder { ) -> cache::FetchCacher { cache::FetchCacher::new( self.file_fetcher.clone(), + self.fs.clone(), self.global_http_cache.clone(), - self.npm_resolver.clone(), + self.in_npm_pkg_checker.clone(), self.module_info_cache.clone(), cache::FetchCacherOptions { - file_header_overrides: self.options.resolve_file_header_overrides(), + file_header_overrides: self.cli_options.resolve_file_header_overrides(), permissions, is_deno_publish: matches!( - self.options.sub_command(), + self.cli_options.sub_command(), crate::args::DenoSubcommand::Publish { .. } ), }, @@ -718,16 +735,28 @@ impl ModuleGraphBuilder { &self.fs, roots, GraphValidOptions { - kind: if self.options.type_check_mode().is_true() { + kind: if self.cli_options.type_check_mode().is_true() { GraphKind::All } else { GraphKind::CodeOnly }, - check_js: self.options.check_js(), + check_js: self.cli_options.check_js(), exit_integrity_errors: true, }, ) } + + fn create_graph_resolver(&self) -> Result { + let jsx_import_source_config = self + .cli_options + .workspace() + .to_maybe_jsx_import_source_config()?; + Ok(CliGraphResolver { + cjs_tracker: &self.cjs_tracker, + resolver: &self.resolver, + jsx_import_source_config, + }) + } } /// Adds more explanatory information to a resolution error. @@ -765,8 +794,8 @@ fn enhanced_sloppy_imports_error_message( match error { ModuleError::LoadingErr(specifier, _, ModuleLoadError::Loader(_)) // ex. "Is a directory" error | ModuleError::Missing(specifier, _) => { - let additional_message = SloppyImportsResolver::new(fs.clone()) - .resolve(specifier, ResolutionMode::Execution)? + let additional_message = CliSloppyImportsResolver::new(SloppyImportsCachedFs::new(fs.clone())) + .resolve(specifier, SloppyImportsResolutionMode::Execution)? .as_suggestion_message(); Some(format!( "{} {} or run with --unstable-sloppy-imports", @@ -948,7 +977,7 @@ pub fn has_graph_root_local_dependent_changed( }, ); while let Some((s, _)) = dependent_specifiers.next() { - if let Ok(path) = specifier_to_file_path(s) { + if let Ok(path) = url_to_file_path(s) { if let Ok(path) = canonicalize_path(&path) { if canonicalized_changed_paths.contains(&path) { return true; @@ -990,7 +1019,11 @@ impl deno_graph::source::Reporter for FileWatcherReporter { ) { let mut file_paths = self.file_paths.lock(); if specifier.scheme() == "file" { - file_paths.push(specifier.to_file_path().unwrap()); + // Don't trust that the path is a valid path at this point: + // https://github.com/denoland/deno/issues/26209. + if let Ok(file_path) = specifier.to_file_path() { + file_paths.push(file_path); + } } if modules_done == modules_total { @@ -1128,6 +1161,53 @@ fn format_deno_graph_error(err: &dyn Error) -> String { message } +#[derive(Debug)] +struct CliGraphResolver<'a> { + cjs_tracker: &'a CjsTracker, + resolver: &'a CliResolver, + jsx_import_source_config: Option, +} + +impl<'a> deno_graph::source::Resolver for CliGraphResolver<'a> { + fn default_jsx_import_source(&self) -> Option { + self + .jsx_import_source_config + .as_ref() + .and_then(|c| c.default_specifier.clone()) + } + + fn default_jsx_import_source_types(&self) -> Option { + self + .jsx_import_source_config + .as_ref() + .and_then(|c| c.default_types_specifier.clone()) + } + + fn jsx_import_source_module(&self) -> &str { + self + .jsx_import_source_config + .as_ref() + .map(|c| c.module.as_str()) + .unwrap_or(deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE) + } + + fn resolve( + &self, + raw_specifier: &str, + referrer_range: &deno_graph::Range, + mode: ResolutionMode, + ) -> Result { + self.resolver.resolve( + raw_specifier, + referrer_range, + self + .cjs_tracker + .get_referrer_kind(&referrer_range.specifier), + mode, + ) + } +} + #[cfg(test)] mod test { use std::sync::Arc; diff --git a/cli/http_util.rs b/cli/http_util.rs index cf244c525a..4b17936d68 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -19,6 +19,7 @@ use deno_runtime::deno_fetch; use deno_runtime::deno_fetch::create_http_client; use deno_runtime::deno_fetch::CreateHttpClientOptions; use deno_runtime::deno_tls::RootCertStoreProvider; +use http::header; use http::header::HeaderName; use http::header::HeaderValue; use http::header::ACCEPT; @@ -204,6 +205,7 @@ pub struct FetchOnceArgs<'a> { pub maybe_accept: Option, pub maybe_etag: Option, pub maybe_auth_token: Option, + pub maybe_auth: Option<(header::HeaderName, header::HeaderValue)>, pub maybe_progress_guard: Option<&'a UpdateGuard>, } @@ -382,6 +384,8 @@ impl HttpClient { request .headers_mut() .insert(AUTHORIZATION, authorization_val); + } else if let Some((header, value)) = args.maybe_auth { + request.headers_mut().insert(header, value); } if let Some(accept) = args.maybe_accept { let accepts_val = HeaderValue::from_str(&accept)?; @@ -470,15 +474,23 @@ impl HttpClient { } } - pub async fn download_with_progress( + pub async fn download_with_progress_and_retries( &self, url: Url, maybe_header: Option<(HeaderName, HeaderValue)>, progress_guard: &UpdateGuard, ) -> Result>, DownloadError> { - self - .download_inner(url, maybe_header, Some(progress_guard)) - .await + crate::util::retry::retry( + || { + self.download_inner( + url.clone(), + maybe_header.clone(), + Some(progress_guard), + ) + }, + |e| matches!(e, DownloadError::BadResponse(_) | DownloadError::Fetch(_)), + ) + .await } pub async fn get_redirected_url( @@ -784,6 +796,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, headers)) = result { @@ -810,6 +823,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, headers)) = result { @@ -837,6 +851,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, headers)) = result { @@ -858,6 +873,7 @@ mod test { maybe_etag: Some("33a64df551425fcc55e".to_string()), maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; assert_eq!(res.unwrap(), FetchOnceResult::NotModified); @@ -877,6 +893,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, headers)) = result { @@ -906,6 +923,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, _)) = result { @@ -931,6 +949,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Redirect(url, _)) = result { @@ -966,6 +985,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, headers)) = result { @@ -1013,6 +1033,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; @@ -1075,6 +1096,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; @@ -1128,6 +1150,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, headers)) = result { @@ -1169,6 +1192,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, headers)) = result { @@ -1191,6 +1215,7 @@ mod test { maybe_etag: Some("33a64df551425fcc55e".to_string()), maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; assert_eq!(res.unwrap(), FetchOnceResult::NotModified); @@ -1225,6 +1250,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; if let Ok(FetchOnceResult::Code(body, headers)) = result { @@ -1254,6 +1280,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; assert!(result.is_err()); @@ -1275,6 +1302,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; @@ -1298,6 +1326,7 @@ mod test { maybe_etag: None, maybe_auth_token: None, maybe_progress_guard: None, + maybe_auth: None, }) .await; diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index fee9c86cbe..044b1573b9 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -2,6 +2,7 @@ use super::diagnostics::DenoDiagnostic; use super::diagnostics::DiagnosticSource; +use super::documents::Document; use super::documents::Documents; use super::language_server; use super::resolver::LspResolver; @@ -9,13 +10,17 @@ use super::tsc; use super::urls::url_to_uri; use crate::args::jsr_url; +use crate::lsp::logging::lsp_warn; +use crate::lsp::search::PackageSearchApi; use crate::tools::lint::CliLinter; +use crate::util::path::relative_specifier; +use deno_config::workspace::MappedResolution; +use deno_graph::source::ResolutionMode; use deno_lint::diagnostic::LintDiagnosticRange; use deno_ast::SourceRange; use deno_ast::SourceRangedForSpanned; use deno_ast::SourceTextInfo; -use deno_core::anyhow::anyhow; use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::serde::Deserialize; @@ -23,8 +28,8 @@ use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::ModuleSpecifier; +use deno_path_util::url_to_file_path; use deno_runtime::deno_node::PathClean; -use deno_runtime::fs_util::specifier_to_file_path; use deno_semver::jsr::JsrPackageNvReference; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; @@ -34,13 +39,15 @@ use deno_semver::package::PackageReq; use deno_semver::package::PackageReqReference; use deno_semver::Version; use import_map::ImportMap; -use node_resolver::NpmResolver; +use node_resolver::NodeModuleKind; use once_cell::sync::Lazy; use regex::Regex; +use std::borrow::Cow; use std::cmp::Ordering; use std::collections::HashMap; use std::collections::HashSet; use std::path::Path; +use text_lines::LineAndColumnIndex; use tower_lsp::lsp_types as lsp; use tower_lsp::lsp_types::Position; use tower_lsp::lsp_types::Range; @@ -225,6 +232,8 @@ pub struct TsResponseImportMapper<'a> { documents: &'a Documents, maybe_import_map: Option<&'a ImportMap>, resolver: &'a LspResolver, + tsc_specifier_map: &'a tsc::TscSpecifierMap, + file_referrer: ModuleSpecifier, } impl<'a> TsResponseImportMapper<'a> { @@ -232,11 +241,15 @@ impl<'a> TsResponseImportMapper<'a> { documents: &'a Documents, maybe_import_map: Option<&'a ImportMap>, resolver: &'a LspResolver, + tsc_specifier_map: &'a tsc::TscSpecifierMap, + file_referrer: &ModuleSpecifier, ) -> Self { Self { documents, maybe_import_map, resolver, + tsc_specifier_map, + file_referrer: file_referrer.clone(), } } @@ -257,8 +270,6 @@ impl<'a> TsResponseImportMapper<'a> { } } - let file_referrer = self.documents.get_file_referrer(referrer); - if let Some(jsr_path) = specifier.as_str().strip_prefix(jsr_url().as_str()) { let mut segments = jsr_path.split('/'); @@ -273,7 +284,7 @@ impl<'a> TsResponseImportMapper<'a> { let export = self.resolver.jsr_lookup_export_for_path( &nv, &path, - file_referrer.as_deref(), + Some(&self.file_referrer), )?; let sub_path = (export != ".").then_some(export); let mut req = None; @@ -299,7 +310,7 @@ impl<'a> TsResponseImportMapper<'a> { req = req.or_else(|| { self .resolver - .jsr_lookup_req_for_nv(&nv, file_referrer.as_deref()) + .jsr_lookup_req_for_nv(&nv, Some(&self.file_referrer)) }); let spec_str = if let Some(req) = req { let req_ref = PackageReqReference { req, sub_path }; @@ -329,9 +340,13 @@ impl<'a> TsResponseImportMapper<'a> { if let Some(npm_resolver) = self .resolver - .maybe_managed_npm_resolver(file_referrer.as_deref()) + .maybe_managed_npm_resolver(Some(&self.file_referrer)) { - if npm_resolver.in_npm_package(specifier) { + let in_npm_pkg = self + .resolver + .in_npm_pkg_checker(Some(&self.file_referrer)) + .in_npm_package(specifier); + if in_npm_pkg { if let Ok(Some(pkg_id)) = npm_resolver.resolve_pkg_id_from_specifier(specifier) { @@ -378,6 +393,11 @@ impl<'a> TsResponseImportMapper<'a> { } } } + } else if let Some(dep_name) = self + .resolver + .file_url_to_package_json_dep(specifier, Some(&self.file_referrer)) + { + return Some(dep_name); } // check if the import map has this specifier @@ -401,7 +421,7 @@ impl<'a> TsResponseImportMapper<'a> { .flatten()?; let root_folder = package_json.path.parent()?; - let specifier_path = specifier_to_file_path(specifier).ok()?; + let specifier_path = url_to_file_path(specifier).ok()?; let mut search_paths = vec![specifier_path.clone()]; // TypeScript will provide a .js extension for quick fixes, so do // a search for the .d.ts file instead @@ -447,24 +467,65 @@ impl<'a> TsResponseImportMapper<'a> { &self, specifier: &str, referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, ) -> Option { - if let Ok(specifier) = referrer.join(specifier) { - if let Some(specifier) = self.check_specifier(&specifier, referrer) { - return Some(specifier); - } - } - let specifier = specifier.strip_suffix(".js").unwrap_or(specifier); - for ext in SUPPORTED_EXTENSIONS { - let specifier_with_ext = format!("{specifier}{ext}"); - if self - .documents - .contains_import(&specifier_with_ext, referrer) + let specifier_stem = specifier.strip_suffix(".js").unwrap_or(specifier); + let specifiers = std::iter::once(Cow::Borrowed(specifier)).chain( + SUPPORTED_EXTENSIONS + .iter() + .map(|ext| Cow::Owned(format!("{specifier_stem}{ext}"))), + ); + for specifier in specifiers { + if let Some(specifier) = self + .resolver + .as_cli_resolver(Some(&self.file_referrer)) + .resolve( + &specifier, + &deno_graph::Range { + specifier: referrer.clone(), + start: deno_graph::Position::zeroed(), + end: deno_graph::Position::zeroed(), + }, + referrer_kind, + ResolutionMode::Types, + ) + .ok() + .and_then(|s| self.tsc_specifier_map.normalize(s.as_str()).ok()) + .filter(|s| self.documents.exists(s, Some(&self.file_referrer))) { - return Some(specifier_with_ext); + if let Some(specifier) = self + .check_specifier(&specifier, referrer) + .or_else(|| relative_specifier(referrer, &specifier)) + .filter(|s| !s.contains("/node_modules/")) + { + return Some(specifier); + } } } None } + + pub fn is_valid_import( + &self, + specifier_text: &str, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + ) -> bool { + self + .resolver + .as_cli_resolver(Some(&self.file_referrer)) + .resolve( + specifier_text, + &deno_graph::Range { + specifier: referrer.clone(), + start: deno_graph::Position::zeroed(), + end: deno_graph::Position::zeroed(), + }, + referrer_kind, + deno_graph::source::ResolutionMode::Types, + ) + .is_ok() + } } fn try_reverse_map_package_json_exports( @@ -529,9 +590,11 @@ fn try_reverse_map_package_json_exports( /// like an import and rewrite the import specifier to include the extension pub fn fix_ts_import_changes( referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, changes: &[tsc::FileTextChanges], - import_mapper: &TsResponseImportMapper, + language_server: &language_server::Inner, ) -> Result, AnyError> { + let import_mapper = language_server.get_ts_response_import_mapper(referrer); let mut r = Vec::new(); for change in changes { let mut text_changes = Vec::new(); @@ -544,8 +607,8 @@ pub fn fix_ts_import_changes( if let Some(captures) = IMPORT_SPECIFIER_RE.captures(line) { let specifier = captures.iter().skip(1).find_map(|s| s).unwrap().as_str(); - if let Some(new_specifier) = - import_mapper.check_unresolved_specifier(specifier, referrer) + if let Some(new_specifier) = import_mapper + .check_unresolved_specifier(specifier, referrer, referrer_kind) { line.replace(specifier, &new_specifier) } else { @@ -573,66 +636,64 @@ pub fn fix_ts_import_changes( /// Fix tsc import code actions so that the module specifier is correct for /// resolution by Deno (includes the extension). -fn fix_ts_import_action( +fn fix_ts_import_action<'a>( referrer: &ModuleSpecifier, - action: &tsc::CodeFixAction, - import_mapper: &TsResponseImportMapper, -) -> Result { - if matches!( + referrer_kind: NodeModuleKind, + action: &'a tsc::CodeFixAction, + language_server: &language_server::Inner, +) -> Option> { + if !matches!( action.fix_name.as_str(), "import" | "fixMissingFunctionDeclaration" ) { - let change = action + return Some(Cow::Borrowed(action)); + } + let specifier = (|| { + let text_change = action.changes.first()?.text_changes.first()?; + let captures = IMPORT_SPECIFIER_RE.captures(&text_change.new_text)?; + Some(captures.get(1)?.as_str()) + })(); + let Some(specifier) = specifier else { + return Some(Cow::Borrowed(action)); + }; + let import_mapper = language_server.get_ts_response_import_mapper(referrer); + if let Some(new_specifier) = + import_mapper.check_unresolved_specifier(specifier, referrer, referrer_kind) + { + let description = action.description.replace(specifier, &new_specifier); + let changes = action .changes - .first() - .ok_or_else(|| anyhow!("Unexpected action changes."))?; - let text_change = change - .text_changes - .first() - .ok_or_else(|| anyhow!("Missing text change."))?; - if let Some(captures) = IMPORT_SPECIFIER_RE.captures(&text_change.new_text) - { - let specifier = captures - .get(1) - .ok_or_else(|| anyhow!("Missing capture."))? - .as_str(); - if let Some(new_specifier) = - import_mapper.check_unresolved_specifier(specifier, referrer) - { - let description = action.description.replace(specifier, &new_specifier); - let changes = action - .changes + .iter() + .map(|c| { + let text_changes = c + .text_changes .iter() - .map(|c| { - let text_changes = c - .text_changes - .iter() - .map(|tc| tsc::TextChange { - span: tc.span.clone(), - new_text: tc.new_text.replace(specifier, &new_specifier), - }) - .collect(); - tsc::FileTextChanges { - file_name: c.file_name.clone(), - text_changes, - is_new_file: c.is_new_file, - } + .map(|tc| tsc::TextChange { + span: tc.span.clone(), + new_text: tc.new_text.replace(specifier, &new_specifier), }) .collect(); + tsc::FileTextChanges { + file_name: c.file_name.clone(), + text_changes, + is_new_file: c.is_new_file, + } + }) + .collect(); - return Ok(tsc::CodeFixAction { - description, - changes, - commands: None, - fix_name: action.fix_name.clone(), - fix_id: None, - fix_all_description: None, - }); - } - } + Some(Cow::Owned(tsc::CodeFixAction { + description, + changes, + commands: None, + fix_name: action.fix_name.clone(), + fix_id: None, + fix_all_description: None, + })) + } else if !import_mapper.is_valid_import(specifier, referrer, referrer_kind) { + None + } else { + Some(Cow::Borrowed(action)) } - - Ok(action.clone()) } /// Determines if two TypeScript diagnostic codes are effectively equivalent. @@ -693,8 +754,14 @@ pub fn ts_changes_to_edit( ) -> Result, AnyError> { let mut text_document_edits = Vec::new(); for change in changes { - let text_document_edit = change.to_text_document_edit(language_server)?; - text_document_edits.push(text_document_edit); + let edit = match change.to_text_document_edit(language_server) { + Ok(e) => e, + Err(err) => { + lsp_warn!("Couldn't covert text document edit: {:#}", err); + continue; + } + }; + text_document_edits.push(edit); } Ok(Some(lsp::WorkspaceEdit { changes: None, @@ -703,7 +770,7 @@ pub fn ts_changes_to_edit( })) } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeActionData { pub specifier: ModuleSpecifier, @@ -956,6 +1023,7 @@ impl CodeActionCollection { pub fn add_ts_fix_action( &mut self, specifier: &ModuleSpecifier, + specifier_kind: NodeModuleKind, action: &tsc::CodeFixAction, diagnostic: &lsp::Diagnostic, language_server: &language_server::Inner, @@ -973,11 +1041,11 @@ impl CodeActionCollection { "The action returned from TypeScript is unsupported.", )); } - let action = fix_ts_import_action( - specifier, - action, - &language_server.get_ts_response_import_mapper(specifier), - )?; + let Some(action) = + fix_ts_import_action(specifier, specifier_kind, action, language_server) + else { + return Ok(()); + }; let edit = ts_changes_to_edit(&action.changes, language_server)?; let code_action = lsp::CodeAction { title: action.description.clone(), @@ -997,7 +1065,7 @@ impl CodeActionCollection { }); self .actions - .push(CodeActionKind::Tsc(code_action, action.clone())); + .push(CodeActionKind::Tsc(code_action, action.as_ref().clone())); if let Some(fix_id) = &action.fix_id { if let Some(CodeActionKind::Tsc(existing_fix_all, existing_action)) = @@ -1024,10 +1092,12 @@ impl CodeActionCollection { specifier: &ModuleSpecifier, diagnostic: &lsp::Diagnostic, ) { - let data = Some(json!({ - "specifier": specifier, - "fixId": action.fix_id, - })); + let data = action.fix_id.as_ref().map(|fix_id| { + json!(CodeActionData { + specifier: specifier.clone(), + fix_id: fix_id.clone(), + }) + }); let title = if let Some(description) = &action.fix_all_description { description.clone() } else { @@ -1151,6 +1221,192 @@ impl CodeActionCollection { ..Default::default() })); } + + pub async fn add_source_actions( + &mut self, + document: &Document, + range: &lsp::Range, + language_server: &language_server::Inner, + ) { + fn import_start_from_specifier( + document: &Document, + import: &deno_graph::Import, + ) -> Option { + // find the top level statement that contains the specifier + let parsed_source = document.maybe_parsed_source()?.as_ref().ok()?; + let text_info = parsed_source.text_info_lazy(); + let specifier_range = SourceRange::new( + text_info.loc_to_source_pos(LineAndColumnIndex { + line_index: import.specifier_range.start.line, + column_index: import.specifier_range.start.character, + }), + text_info.loc_to_source_pos(LineAndColumnIndex { + line_index: import.specifier_range.end.line, + column_index: import.specifier_range.end.character, + }), + ); + + parsed_source + .program_ref() + .body() + .find(|i| i.range().contains(&specifier_range)) + .map(|i| text_info.line_and_column_index(i.range().start)) + } + + async fn deno_types_for_npm_action( + document: &Document, + range: &lsp::Range, + language_server: &language_server::Inner, + ) -> Option { + let (dep_key, dependency, _) = + document.get_maybe_dependency(&range.end)?; + if dependency.maybe_deno_types_specifier.is_some() { + return None; + } + if dependency.maybe_code.maybe_specifier().is_none() + && dependency.maybe_type.maybe_specifier().is_none() + { + // We're using byonm and the package is not cached. + return None; + } + let position = deno_graph::Position::new( + range.end.line as usize, + range.end.character as usize, + ); + let import_start = dependency.imports.iter().find_map(|i| { + if json!(i.kind) != json!("es") && json!(i.kind) != json!("tsType") { + return None; + } + if !i.specifier_range.includes(&position) { + return None; + } + + import_start_from_specifier(document, i) + })?; + let referrer = document.specifier(); + let referrer_kind = language_server + .is_cjs_resolver + .get_doc_module_kind(document); + let file_referrer = document.file_referrer(); + let config_data = language_server + .config + .tree + .data_for_specifier(file_referrer?)?; + let workspace_resolver = config_data.resolver.clone(); + let npm_ref = if let Ok(resolution) = + workspace_resolver.resolve(&dep_key, document.specifier()) + { + let specifier = match resolution { + MappedResolution::Normal { specifier, .. } + | MappedResolution::ImportMap { specifier, .. } => specifier, + _ => { + return None; + } + }; + NpmPackageReqReference::from_specifier(&specifier).ok()? + } else { + // Only resolve bare package.json deps for byonm. + if !config_data.byonm { + return None; + } + if !language_server.resolver.is_bare_package_json_dep( + &dep_key, + referrer, + referrer_kind, + ) { + return None; + } + NpmPackageReqReference::from_str(&format!("npm:{}", &dep_key)).ok()? + }; + let package_name = &npm_ref.req().name; + if package_name.starts_with("@types/") { + return None; + } + let managed_npm_resolver = language_server + .resolver + .maybe_managed_npm_resolver(file_referrer); + if let Some(npm_resolver) = managed_npm_resolver { + if !npm_resolver.is_pkg_req_folder_cached(npm_ref.req()) { + return None; + } + } + if language_server + .resolver + .npm_to_file_url(&npm_ref, referrer, referrer_kind, file_referrer) + .is_some() + { + // The package import has types. + return None; + } + let types_package_name = format!("@types/{package_name}"); + let types_package_version = language_server + .npm_search_api + .versions(&types_package_name) + .await + .ok() + .and_then(|versions| versions.first().cloned())?; + let types_specifier_text = + if let Some(npm_resolver) = managed_npm_resolver { + let mut specifier_text = if let Some(req) = + npm_resolver.top_package_req_for_name(&types_package_name) + { + format!("npm:{req}") + } else { + format!("npm:{}@^{}", &types_package_name, types_package_version) + }; + let specifier = ModuleSpecifier::parse(&specifier_text).ok()?; + if let Some(file_referrer) = file_referrer { + if let Some(text) = language_server + .get_ts_response_import_mapper(file_referrer) + .check_specifier(&specifier, referrer) + { + specifier_text = text; + } + } + specifier_text + } else { + types_package_name.clone() + }; + let uri = language_server + .url_map + .specifier_to_uri(referrer, file_referrer) + .ok()?; + let position = lsp::Position { + line: import_start.line_index as u32, + character: import_start.column_index as u32, + }; + let new_text = format!( + "{}// @deno-types=\"{}\"\n", + if position.character == 0 { "" } else { "\n" }, + &types_specifier_text + ); + let text_edit = lsp::TextEdit { + range: lsp::Range { + start: position, + end: position, + }, + new_text, + }; + Some(lsp::CodeAction { + title: format!( + "Add @deno-types directive for \"{}\"", + &types_specifier_text + ), + kind: Some(lsp::CodeActionKind::QUICKFIX), + diagnostics: None, + edit: Some(lsp::WorkspaceEdit { + changes: Some([(uri, vec![text_edit])].into_iter().collect()), + ..Default::default() + }), + ..Default::default() + }) + } + if let Some(action) = + deno_types_for_npm_action(document, range, language_server).await + { + self.actions.push(CodeActionKind::Deno(action)); + } + } } /// Prepend the whitespace characters found at the start of line_content to content. diff --git a/cli/lsp/cache.rs b/cli/lsp/cache.rs index a32087842d..fbf9ea6f1b 100644 --- a/cli/lsp/cache.rs +++ b/cli/lsp/cache.rs @@ -10,7 +10,7 @@ use crate::lsp::logging::lsp_warn; use deno_core::url::Url; use deno_core::ModuleSpecifier; -use deno_runtime::fs_util::specifier_to_file_path; +use deno_path_util::url_to_file_path; use std::collections::BTreeMap; use std::fs; use std::path::Path; @@ -24,7 +24,7 @@ pub fn calculate_fs_version( ) -> Option { match specifier.scheme() { "npm" | "node" | "data" | "blob" => None, - "file" => specifier_to_file_path(specifier) + "file" => url_to_file_path(specifier) .ok() .and_then(|path| calculate_fs_version_at_path(&path)), _ => calculate_fs_version_in_cache(cache, specifier, file_referrer), @@ -82,7 +82,7 @@ impl Default for LspCache { impl LspCache { pub fn new(global_cache_url: Option) -> Self { let global_cache_path = global_cache_url.and_then(|s| { - specifier_to_file_path(&s) + url_to_file_path(&s) .inspect(|p| { lsp_log!("Resolved global cache path: \"{}\"", p.to_string_lossy()); }) @@ -94,7 +94,7 @@ impl LspCache { let deno_dir = DenoDir::new(global_cache_path) .expect("should be infallible with absolute custom root"); let global = Arc::new(GlobalHttpCache::new( - deno_dir.deps_folder_path(), + deno_dir.remote_folder_path(), crate::cache::RealDenoCacheEnv, )); Self { @@ -165,7 +165,7 @@ impl LspCache { &self, specifier: &ModuleSpecifier, ) -> Option { - let path = specifier_to_file_path(specifier).ok()?; + let path = url_to_file_path(specifier).ok()?; let vendor = self .vendors_by_scope .iter() @@ -176,7 +176,7 @@ impl LspCache { } pub fn is_valid_file_referrer(&self, specifier: &ModuleSpecifier) -> bool { - if let Ok(path) = specifier_to_file_path(specifier) { + if let Ok(path) = url_to_file_path(specifier) { if !path.starts_with(&self.deno_dir().root) { return true; } diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index e93d3b7c20..5cdb1224d8 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -147,11 +147,11 @@ pub fn server_capabilities( moniker_provider: None, experimental: Some(json!({ "denoConfigTasks": true, - "testingApi":true, + "testingApi": true, + "didRefreshDenoConfigurationTreeNotifications": true, })), inlay_hint_provider: Some(OneOf::Left(true)), position_encoding: None, - // TODO(nayeemrmn): Support pull-based diagnostics. diagnostic_provider: None, inline_value_provider: None, inline_completion_provider: None, diff --git a/cli/lsp/client.rs b/cli/lsp/client.rs index b3f0d64fa6..65865d5b32 100644 --- a/cli/lsp/client.rs +++ b/cli/lsp/client.rs @@ -92,6 +92,19 @@ impl Client { }); } + pub fn send_did_refresh_deno_configuration_tree_notification( + &self, + params: lsp_custom::DidRefreshDenoConfigurationTreeNotificationParams, + ) { + // do on a task in case the caller currently is in the lsp lock + let client = self.0.clone(); + spawn(async move { + client + .send_did_refresh_deno_configuration_tree_notification(params) + .await; + }); + } + pub fn send_did_change_deno_configuration_notification( &self, params: lsp_custom::DidChangeDenoConfigurationNotificationParams, @@ -169,6 +182,10 @@ trait ClientTrait: Send + Sync { params: lsp_custom::DiagnosticBatchNotificationParams, ); async fn send_test_notification(&self, params: TestingNotification); + async fn send_did_refresh_deno_configuration_tree_notification( + &self, + params: lsp_custom::DidRefreshDenoConfigurationTreeNotificationParams, + ); async fn send_did_change_deno_configuration_notification( &self, params: lsp_custom::DidChangeDenoConfigurationNotificationParams, @@ -249,6 +266,18 @@ impl ClientTrait for TowerClient { } } + async fn send_did_refresh_deno_configuration_tree_notification( + &self, + params: lsp_custom::DidRefreshDenoConfigurationTreeNotificationParams, + ) { + self + .0 + .send_notification::( + params, + ) + .await + } + async fn send_did_change_deno_configuration_notification( &self, params: lsp_custom::DidChangeDenoConfigurationNotificationParams, @@ -366,6 +395,12 @@ impl ClientTrait for ReplClient { async fn send_test_notification(&self, _params: TestingNotification) {} + async fn send_did_refresh_deno_configuration_tree_notification( + &self, + _params: lsp_custom::DidRefreshDenoConfigurationTreeNotificationParams, + ) { + } + async fn send_did_change_deno_configuration_notification( &self, _params: lsp_custom::DidChangeDenoConfigurationNotificationParams, diff --git a/cli/lsp/code_lens.rs b/cli/lsp/code_lens.rs index e117888fba..a57ca3ac9f 100644 --- a/cli/lsp/code_lens.rs +++ b/cli/lsp/code_lens.rs @@ -421,7 +421,7 @@ pub fn collect_test( ) -> Result, AnyError> { let mut collector = DenoTestCollector::new(specifier.clone(), parsed_source.clone()); - parsed_source.module().visit_with(&mut collector); + parsed_source.program().visit_with(&mut collector); Ok(collector.take()) } @@ -581,7 +581,7 @@ mod tests { .unwrap(); let mut collector = DenoTestCollector::new(specifier, parsed_module.clone()); - parsed_module.module().visit_with(&mut collector); + parsed_module.program().visit_with(&mut collector); assert_eq!( collector.take(), vec![ diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index b3d6fbbd04..3ee8ae93e4 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -9,6 +9,7 @@ use super::jsr::CliJsrSearchApi; use super::lsp_custom; use super::npm::CliNpmSearchApi; use super::registries::ModuleRegistry; +use super::resolver::LspIsCjsResolver; use super::resolver::LspResolver; use super::search::PackageSearchApi; use super::tsc; @@ -29,12 +30,13 @@ use deno_core::serde::Serialize; use deno_core::serde_json::json; use deno_core::url::Position; use deno_core::ModuleSpecifier; -use deno_runtime::fs_util::specifier_to_file_path; +use deno_path_util::url_to_file_path; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::package::PackageNv; use import_map::ImportMap; use indexmap::IndexSet; use lsp_types::CompletionList; +use node_resolver::NodeModuleKind; use once_cell::sync::Lazy; use regex::Regex; use tower_lsp::lsp_types as lsp; @@ -159,15 +161,17 @@ pub async fn get_import_completions( jsr_search_api: &CliJsrSearchApi, npm_search_api: &CliNpmSearchApi, documents: &Documents, + is_cjs_resolver: &LspIsCjsResolver, resolver: &LspResolver, maybe_import_map: Option<&ImportMap>, ) -> Option { let document = documents.get(specifier)?; + let specifier_kind = is_cjs_resolver.get_doc_module_kind(&document); let file_referrer = document.file_referrer(); let (text, _, range) = document.get_maybe_dependency(position)?; let range = to_narrow_lsp_range(document.text_info(), &range); let resolved = resolver - .as_graph_resolver(file_referrer) + .as_cli_resolver(file_referrer) .resolve( &text, &Range { @@ -175,6 +179,7 @@ pub async fn get_import_completions( start: deno_graph::Position::zeroed(), end: deno_graph::Position::zeroed(), }, + specifier_kind, ResolutionMode::Execution, ) .ok(); @@ -200,15 +205,11 @@ pub async fn get_import_completions( { // completions for import map specifiers Some(lsp::CompletionResponse::List(completion_list)) - } else if text.starts_with("./") - || text.starts_with("../") - || text.starts_with('/') + } else if let Some(completion_list) = + get_local_completions(specifier, specifier_kind, &text, &range, resolver) { // completions for local relative modules - Some(lsp::CompletionResponse::List(CompletionList { - is_incomplete: false, - items: get_local_completions(specifier, &text, &range, resolver)?, - })) + Some(lsp::CompletionResponse::List(completion_list)) } else if !text.is_empty() { // completion of modules from a module registry or cache check_auto_config_registry( @@ -359,84 +360,86 @@ fn get_import_map_completions( /// Return local completions that are relative to the base specifier. fn get_local_completions( - base: &ModuleSpecifier, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, text: &str, range: &lsp::Range, resolver: &LspResolver, -) -> Option> { - if base.scheme() != "file" { +) -> Option { + if referrer.scheme() != "file" { return None; } - let parent = base.join(text).ok()?.join(".").ok()?; + let parent = &text[..text.char_indices().rfind(|(_, c)| *c == '/')?.0 + 1]; let resolved_parent = resolver - .as_graph_resolver(Some(base)) + .as_cli_resolver(Some(referrer)) .resolve( - parent.as_str(), + parent, &Range { - specifier: base.clone(), + specifier: referrer.clone(), start: deno_graph::Position::zeroed(), end: deno_graph::Position::zeroed(), }, + referrer_kind, ResolutionMode::Execution, ) .ok()?; - let resolved_parent_path = specifier_to_file_path(&resolved_parent).ok()?; - let raw_parent = - &text[..text.char_indices().rfind(|(_, c)| *c == '/')?.0 + 1]; + let resolved_parent_path = url_to_file_path(&resolved_parent).ok()?; if resolved_parent_path.is_dir() { let cwd = std::env::current_dir().ok()?; - let items = std::fs::read_dir(resolved_parent_path).ok()?; - Some( - items - .filter_map(|de| { - let de = de.ok()?; - let label = de.path().file_name()?.to_string_lossy().to_string(); - let entry_specifier = resolve_path(de.path().to_str()?, &cwd).ok()?; - if entry_specifier == *base { - return None; - } - let full_text = format!("{raw_parent}{label}"); - let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: *range, - new_text: full_text.clone(), - })); - let filter_text = Some(full_text); - match de.file_type() { - Ok(file_type) if file_type.is_dir() => Some(lsp::CompletionItem { - label, - kind: Some(lsp::CompletionItemKind::FOLDER), - detail: Some("(local)".to_string()), - filter_text, - sort_text: Some("1".to_string()), - text_edit, - commit_characters: Some( - IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), - ), - ..Default::default() - }), - Ok(file_type) if file_type.is_file() => { - if is_importable_ext(&de.path()) { - Some(lsp::CompletionItem { - label, - kind: Some(lsp::CompletionItemKind::FILE), - detail: Some("(local)".to_string()), - filter_text, - sort_text: Some("1".to_string()), - text_edit, - commit_characters: Some( - IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), - ), - ..Default::default() - }) - } else { - None - } + let entries = std::fs::read_dir(resolved_parent_path).ok()?; + let items = entries + .filter_map(|de| { + let de = de.ok()?; + let label = de.path().file_name()?.to_string_lossy().to_string(); + let entry_specifier = resolve_path(de.path().to_str()?, &cwd).ok()?; + if entry_specifier == *referrer { + return None; + } + let full_text = format!("{parent}{label}"); + let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: full_text.clone(), + })); + let filter_text = Some(full_text); + match de.file_type() { + Ok(file_type) if file_type.is_dir() => Some(lsp::CompletionItem { + label, + kind: Some(lsp::CompletionItemKind::FOLDER), + detail: Some("(local)".to_string()), + filter_text, + sort_text: Some("1".to_string()), + text_edit, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), + ..Default::default() + }), + Ok(file_type) if file_type.is_file() => { + if is_importable_ext(&de.path()) { + Some(lsp::CompletionItem { + label, + kind: Some(lsp::CompletionItemKind::FILE), + detail: Some("(local)".to_string()), + filter_text, + sort_text: Some("1".to_string()), + text_edit, + commit_characters: Some( + IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(), + ), + ..Default::default() + }) + } else { + None } - _ => None, } - }) - .collect(), - ) + _ => None, + } + }) + .collect(); + Some(CompletionList { + is_incomplete: false, + items, + }) } else { None } @@ -909,6 +912,7 @@ mod tests { ModuleSpecifier::from_file_path(file_c).expect("could not create"); let actual = get_local_completions( &specifier, + NodeModuleKind::Esm, "./", &lsp::Range { start: lsp::Position { @@ -921,11 +925,11 @@ mod tests { }, }, &Default::default(), - ); - assert!(actual.is_some()); - let actual = actual.unwrap(); - assert_eq!(actual.len(), 3); - for item in actual { + ) + .unwrap(); + assert!(!actual.is_incomplete); + assert_eq!(actual.items.len(), 3); + for item in actual.items { match item.text_edit { Some(lsp::CompletionTextEdit::Edit(text_edit)) => { assert!(["./b", "./f.mjs", "./g.json"] diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index e55e7b734d..ea77e36bcf 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -4,6 +4,7 @@ use deno_ast::MediaType; use deno_config::deno_json::DenoJsonCache; use deno_config::deno_json::FmtConfig; use deno_config::deno_json::FmtOptionsConfig; +use deno_config::deno_json::JsxImportSourceConfig; use deno_config::deno_json::LintConfig; use deno_config::deno_json::NodeModulesDirMode; use deno_config::deno_json::TestConfig; @@ -36,11 +37,12 @@ use deno_core::ModuleSpecifier; use deno_lint::linter::LintConfig as DenoLintConfig; use deno_npm::npm_rc::ResolvedNpmRc; use deno_package_json::PackageJsonCache; +use deno_path_util::url_to_file_path; use deno_runtime::deno_node::PackageJson; -use deno_runtime::fs_util::specifier_to_file_path; use indexmap::IndexSet; use lsp_types::ClientCapabilities; use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::collections::HashMap; use std::ops::Deref; use std::ops::DerefMut; @@ -50,16 +52,20 @@ use std::sync::Arc; use tower_lsp::lsp_types as lsp; use super::logging::lsp_log; +use super::lsp_custom; +use super::urls::url_to_uri; use crate::args::discover_npmrc_from_workspace; use crate::args::has_flag_env_var; use crate::args::CliLockfile; +use crate::args::CliLockfileReadFromPathOptions; use crate::args::ConfigFile; use crate::args::LintFlags; use crate::args::LintOptions; use crate::cache::FastInsecureHasher; use crate::file_fetcher::FileFetcher; use crate::lsp::logging::lsp_warn; -use crate::resolver::SloppyImportsResolver; +use crate::resolver::CliSloppyImportsResolver; +use crate::resolver::SloppyImportsCachedFs; use crate::tools::lint::CliLinter; use crate::tools::lint::CliLinterOptions; use crate::tools::lint::LintRuleProvider; @@ -435,6 +441,8 @@ pub struct LanguagePreferences { pub use_aliases_for_renames: bool, #[serde(default)] pub quote_style: QuoteStyle, + #[serde(default)] + pub prefer_type_only_auto_imports: bool, } impl Default for LanguagePreferences { @@ -445,6 +453,7 @@ impl Default for LanguagePreferences { auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, quote_style: Default::default(), + prefer_type_only_auto_imports: false, } } } @@ -801,7 +810,7 @@ impl Settings { /// Returns `None` if the value should be deferred to the presence of a /// `deno.json` file. pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> Option { - let Ok(path) = specifier_to_file_path(specifier) else { + let Ok(path) = url_to_file_path(specifier) else { // Non-file URLs are not disabled by these settings. return Some(true); }; @@ -810,7 +819,7 @@ impl Settings { let mut disable_paths = vec![]; let mut enable_paths = None; if let Some(folder_uri) = folder_uri { - if let Ok(folder_path) = specifier_to_file_path(folder_uri) { + if let Ok(folder_path) = url_to_file_path(folder_uri) { disable_paths = settings .disable_paths .iter() @@ -847,12 +856,12 @@ impl Settings { &self, specifier: &ModuleSpecifier, ) -> (&WorkspaceSettings, Option<&ModuleSpecifier>) { - let Ok(path) = specifier_to_file_path(specifier) else { + let Ok(path) = url_to_file_path(specifier) else { return (&self.unscoped, self.first_folder.as_ref()); }; for (folder_uri, settings) in self.by_workspace_folder.iter().rev() { if let Some(settings) = settings { - let Ok(folder_path) = specifier_to_file_path(folder_uri) else { + let Ok(folder_path) = url_to_file_path(folder_uri) else { continue; }; if path.starts_with(folder_path) { @@ -977,7 +986,7 @@ impl Config { | MediaType::Tsx => Some(&workspace_settings.typescript), MediaType::Json | MediaType::Wasm - | MediaType::TsBuildInfo + | MediaType::Css | MediaType::SourceMap | MediaType::Unknown => None, } @@ -1181,8 +1190,9 @@ pub struct ConfigData { pub lockfile: Option>, pub npmrc: Option>, pub resolver: Arc, - pub sloppy_imports_resolver: Option>, + pub sloppy_imports_resolver: Option>, pub import_map_from_settings: Option, + pub unstable: BTreeSet, watched_files: HashMap, } @@ -1580,13 +1590,22 @@ impl ConfigData { .join("\n") ); } + let unstable = member_dir + .workspace + .unstable_features() + .iter() + .chain(settings.unstable.as_deref()) + .cloned() + .collect::>(); let unstable_sloppy_imports = std::env::var("DENO_UNSTABLE_SLOPPY_IMPORTS") .is_ok() - || member_dir.workspace.has_unstable("sloppy-imports"); + || unstable.contains("sloppy-imports"); let sloppy_imports_resolver = unstable_sloppy_imports.then(|| { - Arc::new(SloppyImportsResolver::new_without_stat_cache(Arc::new( - deno_runtime::deno_fs::RealFs, - ))) + Arc::new(CliSloppyImportsResolver::new( + SloppyImportsCachedFs::new_without_stat_cache(Arc::new( + deno_runtime::deno_fs::RealFs, + )), + )) }); let resolver = Arc::new(resolver); let lint_rule_provider = LintRuleProvider::new( @@ -1621,6 +1640,7 @@ impl ConfigData { lockfile, npmrc, import_map_from_settings, + unstable, watched_files, } } @@ -1635,6 +1655,17 @@ impl ConfigData { self.member_dir.maybe_pkg_json() } + pub fn maybe_jsx_import_source_config( + &self, + ) -> Option { + self + .member_dir + .workspace + .to_maybe_jsx_import_source_config() + .ok() + .flatten() + } + pub fn scope_contains_specifier(&self, specifier: &ModuleSpecifier) -> bool { specifier.as_str().starts_with(self.scope.as_str()) || self @@ -1712,14 +1743,14 @@ impl ConfigTree { .unwrap_or_else(|| Arc::new(FmtConfig::new_with_base(PathBuf::from("/")))) } - /// Returns (scope_uri, type). + /// Returns (scope_url, type). pub fn watched_file_type( &self, specifier: &ModuleSpecifier, ) -> Option<(&ModuleSpecifier, ConfigWatchedFileType)> { - for (scope_uri, data) in self.scopes.iter() { + for (scope_url, data) in self.scopes.iter() { if let Some(typ) = data.watched_files.get(specifier) { - return Some((scope_uri, *typ)); + return Some((scope_url, *typ)); } } None @@ -1743,6 +1774,46 @@ impl ConfigTree { .any(|data| data.watched_files.contains_key(specifier)) } + pub fn to_did_refresh_params( + &self, + ) -> lsp_custom::DidRefreshDenoConfigurationTreeNotificationParams { + let data = self + .scopes + .values() + .filter_map(|data| { + let workspace_root_scope_uri = + Some(data.member_dir.workspace.root_dir()) + .filter(|s| *s != data.member_dir.dir_url()) + .and_then(|s| url_to_uri(s).ok()); + Some(lsp_custom::DenoConfigurationData { + scope_uri: url_to_uri(&data.scope).ok()?, + deno_json: data.maybe_deno_json().and_then(|c| { + if workspace_root_scope_uri.is_some() + && Some(&c.specifier) + == data + .member_dir + .workspace + .root_deno_json() + .map(|c| &c.specifier) + { + return None; + } + Some(lsp::TextDocumentIdentifier { + uri: url_to_uri(&c.specifier).ok()?, + }) + }), + package_json: data.maybe_pkg_json().and_then(|p| { + Some(lsp::TextDocumentIdentifier { + uri: url_to_uri(&p.specifier()).ok()?, + }) + }), + workspace_root_scope_uri, + }) + }) + .collect(); + lsp_custom::DidRefreshDenoConfigurationTreeNotificationParams { data } + } + pub async fn refresh( &mut self, settings: &Settings, @@ -1767,7 +1838,7 @@ impl ConfigTree { let config_file_path = (|| { let config_setting = ws_settings.config.as_ref()?; let config_uri = folder_uri.join(config_setting).ok()?; - specifier_to_file_path(&config_uri).ok() + url_to_file_path(&config_uri).ok() })(); if config_file_path.is_some() || ws_settings.import_map.is_some() { scopes.insert( @@ -1844,7 +1915,7 @@ impl ConfigTree { let scope = config_file.specifier.join(".").unwrap(); let json_text = serde_json::to_string(&config_file.json).unwrap(); let test_fs = deno_runtime::deno_fs::InMemoryFs::default(); - let config_path = specifier_to_file_path(&config_file.specifier).unwrap(); + let config_path = url_to_file_path(&config_file.specifier).unwrap(); test_fs.setup_text_files(vec![( config_path.to_string_lossy().to_string(), json_text, @@ -1928,7 +1999,11 @@ fn resolve_lockfile_from_path( lockfile_path: PathBuf, frozen: bool, ) -> Option { - match CliLockfile::read_from_path(lockfile_path, frozen) { + match CliLockfile::read_from_path(CliLockfileReadFromPathOptions { + file_path: lockfile_path, + frozen, + skip_write: false, + }) { Ok(value) => { if value.filename.exists() { if let Ok(specifier) = ModuleSpecifier::from_file_path(&value.filename) @@ -2201,6 +2276,7 @@ mod tests { auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, quote_style: QuoteStyle::Auto, + prefer_type_only_auto_imports: false, }, suggest: CompletionSettings { complete_function_calls: false, @@ -2246,6 +2322,7 @@ mod tests { auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, quote_style: QuoteStyle::Auto, + prefer_type_only_auto_imports: false, }, suggest: CompletionSettings { complete_function_calls: false, diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 1aebaf56fc..e4fb82e58d 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -19,8 +19,8 @@ use super::urls::LspUrlMap; use crate::graph_util; use crate::graph_util::enhanced_resolution_error_message; use crate::lsp::lsp_custom::DiagnosticBatchNotificationParams; -use crate::resolver::SloppyImportsResolution; -use crate::resolver::SloppyImportsResolver; +use crate::resolver::CliSloppyImportsResolver; +use crate::resolver::SloppyImportsCachedFs; use crate::tools::lint::CliLinter; use crate::tools::lint::CliLinterOptions; use crate::tools::lint::LintRuleProvider; @@ -40,11 +40,12 @@ use deno_core::unsync::spawn_blocking; use deno_core::unsync::JoinHandle; use deno_core::url::Url; use deno_core::ModuleSpecifier; -use deno_graph::source::ResolutionMode; use deno_graph::source::ResolveError; use deno_graph::Resolution; use deno_graph::ResolutionError; use deno_graph::SpecifierError; +use deno_resolver::sloppy_imports::SloppyImportsResolution; +use deno_resolver::sloppy_imports::SloppyImportsResolutionMode; use deno_runtime::deno_fs; use deno_runtime::deno_node; use deno_runtime::tokio_util::create_basic_runtime; @@ -1263,7 +1264,9 @@ impl DenoDiagnostic { Self::NotInstalledJsr(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("JSR package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))), Self::NotInstalledNpm(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("NPM package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))), Self::NoLocal(specifier) => { - let maybe_sloppy_resolution = SloppyImportsResolver::new(Arc::new(deno_fs::RealFs)).resolve(specifier, ResolutionMode::Execution); + let maybe_sloppy_resolution = CliSloppyImportsResolver::new( + SloppyImportsCachedFs::new(Arc::new(deno_fs::RealFs)) + ).resolve(specifier, SloppyImportsResolutionMode::Execution); let data = maybe_sloppy_resolution.as_ref().map(|res| { json!({ "specifier": specifier, @@ -1496,7 +1499,11 @@ fn diagnose_dependency( .data_for_specifier(referrer_doc.file_referrer().unwrap_or(referrer)) .and_then(|d| d.resolver.maybe_import_map()); if let Some(import_map) = import_map { - if let Resolution::Ok(resolved) = &dependency.maybe_code { + let resolved = dependency + .maybe_code + .ok() + .or_else(|| dependency.maybe_type.ok()); + if let Some(resolved) = resolved { if let Some(to) = import_map.lookup(&resolved.specifier, referrer) { if dependency_key != to { diagnostics.push( @@ -1514,17 +1521,19 @@ fn diagnose_dependency( let import_ranges: Vec<_> = dependency .imports .iter() - .map(|i| documents::to_lsp_range(&i.range)) + .map(|i| documents::to_lsp_range(&i.specifier_range)) .collect(); // TODO(nayeemrmn): This is a crude way of detecting `@deno-types` which has // a different specifier and therefore needs a separate call to // `diagnose_resolution()`. It would be much cleaner if that were modelled as // a separate dependency: https://github.com/denoland/deno_graph/issues/247. let is_types_deno_types = !dependency.maybe_type.is_none() - && !dependency - .imports - .iter() - .any(|i| dependency.maybe_type.includes(&i.range.start).is_some()); + && !dependency.imports.iter().any(|i| { + dependency + .maybe_type + .includes(&i.specifier_range.start) + .is_some() + }); diagnostics.extend( diagnose_resolution( @@ -1698,6 +1707,7 @@ mod tests { documents: Arc::new(documents), assets: Default::default(), config: Arc::new(config), + is_cjs_resolver: Default::default(), resolver, }, ) diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index e96b8831bb..b62fa85533 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -3,7 +3,9 @@ use super::cache::calculate_fs_version; use super::cache::LspCache; use super::config::Config; +use super::resolver::LspIsCjsResolver; use super::resolver::LspResolver; +use super::resolver::SingleReferrerGraphResolver; use super::testing::TestCollector; use super::testing::TestModule; use super::text::LineIndex; @@ -26,13 +28,14 @@ use deno_core::parking_lot::Mutex; use deno_core::ModuleSpecifier; use deno_graph::source::ResolutionMode; use deno_graph::Resolution; +use deno_path_util::url_to_file_path; use deno_runtime::deno_node; -use deno_runtime::fs_util::specifier_to_file_path; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; use indexmap::IndexMap; use indexmap::IndexSet; +use node_resolver::NodeModuleKind; use std::borrow::Cow; use std::collections::BTreeMap; use std::collections::BTreeSet; @@ -272,7 +275,7 @@ fn get_maybe_test_module_fut( parsed_source.specifier().clone(), parsed_source.text_info_lazy().clone(), ); - parsed_source.module().visit_with(&mut collector); + parsed_source.program().visit_with(&mut collector); Arc::new(collector.take()) }) .map(Result::ok) @@ -293,6 +296,8 @@ pub struct Document { /// Contains the last-known-good set of dependencies from parsing the module. config: Arc, dependencies: Arc>, + /// If this is maybe a CJS script and maybe not an ES module. + is_script: Option, // TODO(nayeemrmn): This is unused, use it for scope attribution for remote // modules. file_referrer: Option, @@ -323,6 +328,7 @@ impl Document { maybe_lsp_version: Option, maybe_language_id: Option, maybe_headers: Option>, + is_cjs_resolver: &LspIsCjsResolver, resolver: Arc, config: Arc, cache: &Arc, @@ -332,12 +338,8 @@ impl Document { .filter(|s| cache.is_valid_file_referrer(s)) .cloned() .or(file_referrer); - let media_type = resolve_media_type( - &specifier, - maybe_headers.as_ref(), - maybe_language_id, - &resolver, - ); + let media_type = + resolve_media_type(&specifier, maybe_headers.as_ref(), maybe_language_id); let (maybe_parsed_source, maybe_module) = if media_type_is_diagnosable(media_type) { parse_and_analyze_module( @@ -346,6 +348,7 @@ impl Document { maybe_headers.as_ref(), media_type, file_referrer.as_ref(), + is_cjs_resolver, &resolver, ) } else { @@ -371,6 +374,7 @@ impl Document { file_referrer.as_ref(), ), file_referrer, + is_script: maybe_module.as_ref().map(|m| m.is_script), maybe_types_dependency, line_index, maybe_language_id, @@ -392,6 +396,7 @@ impl Document { fn with_new_config( &self, + is_cjs_resolver: &LspIsCjsResolver, resolver: Arc, config: Arc, ) -> Arc { @@ -399,11 +404,11 @@ impl Document { &self.specifier, self.maybe_headers.as_ref(), self.maybe_language_id, - &resolver, ); let dependencies; let maybe_types_dependency; let maybe_parsed_source; + let is_script; let maybe_test_module_fut; if media_type != self.media_type { let parsed_source_result = @@ -413,6 +418,7 @@ impl Document { &parsed_source_result, self.maybe_headers.as_ref(), self.file_referrer.as_ref(), + is_cjs_resolver, &resolver, ) .ok(); @@ -420,6 +426,7 @@ impl Document { .as_ref() .map(|m| Arc::new(m.dependencies.clone())) .unwrap_or_default(); + is_script = maybe_module.as_ref().map(|m| m.is_script); maybe_types_dependency = maybe_module .as_ref() .and_then(|m| Some(Arc::new(m.maybe_types_dependency.clone()?))); @@ -427,10 +434,19 @@ impl Document { maybe_test_module_fut = get_maybe_test_module_fut(maybe_parsed_source.as_ref(), &config); } else { - let graph_resolver = - resolver.as_graph_resolver(self.file_referrer.as_ref()); + let cli_resolver = resolver.as_cli_resolver(self.file_referrer.as_ref()); let npm_resolver = resolver.create_graph_npm_resolver(self.file_referrer.as_ref()); + let config_data = resolver.as_config_data(self.file_referrer.as_ref()); + let jsx_import_source_config = + config_data.and_then(|d| d.maybe_jsx_import_source_config()); + let resolver = SingleReferrerGraphResolver { + valid_referrer: &self.specifier, + referrer_kind: is_cjs_resolver + .get_lsp_referrer_kind(&self.specifier, self.is_script), + cli_resolver, + jsx_import_source_config: jsx_import_source_config.as_ref(), + }; dependencies = Arc::new( self .dependencies @@ -441,7 +457,7 @@ impl Document { d.with_new_resolver( s, &CliJsrUrlProvider, - Some(graph_resolver), + Some(&resolver), Some(&npm_resolver), ), ) @@ -451,10 +467,11 @@ impl Document { maybe_types_dependency = self.maybe_types_dependency.as_ref().map(|d| { Arc::new(d.with_new_resolver( &CliJsrUrlProvider, - Some(graph_resolver), + Some(&resolver), Some(&npm_resolver), )) }); + is_script = self.is_script; maybe_parsed_source = self.maybe_parsed_source().cloned(); maybe_test_module_fut = self .maybe_test_module_fut @@ -466,6 +483,7 @@ impl Document { // updated properties dependencies, file_referrer: self.file_referrer.clone(), + is_script, maybe_types_dependency, maybe_navigation_tree: Mutex::new(None), // maintain - this should all be copies/clones @@ -490,6 +508,7 @@ impl Document { fn with_change( &self, + is_cjs_resolver: &LspIsCjsResolver, version: i32, changes: Vec, ) -> Result, AnyError> { @@ -523,6 +542,7 @@ impl Document { self.maybe_headers.as_ref(), media_type, self.file_referrer.as_ref(), + is_cjs_resolver, self.resolver.as_ref(), ) } else { @@ -546,6 +566,7 @@ impl Document { get_maybe_test_module_fut(maybe_parsed_source.as_ref(), &self.config); Ok(Arc::new(Self { config: self.config.clone(), + is_script: maybe_module.as_ref().map(|m| m.is_script), specifier: self.specifier.clone(), file_referrer: self.file_referrer.clone(), maybe_fs_version: self.maybe_fs_version.clone(), @@ -580,6 +601,7 @@ impl Document { ), maybe_language_id: self.maybe_language_id, dependencies: self.dependencies.clone(), + is_script: self.is_script, maybe_types_dependency: self.maybe_types_dependency.clone(), text: self.text.clone(), text_info_cell: once_cell::sync::OnceCell::new(), @@ -607,6 +629,7 @@ impl Document { ), maybe_language_id: self.maybe_language_id, dependencies: self.dependencies.clone(), + is_script: self.is_script, maybe_types_dependency: self.maybe_types_dependency.clone(), text: self.text.clone(), text_info_cell: once_cell::sync::OnceCell::new(), @@ -655,6 +678,13 @@ impl Document { }) } + /// If this is maybe a CJS script and maybe not an ES module. + /// + /// Use `LspIsCjsResolver` to determine for sure. + pub fn is_script(&self) -> Option { + self.is_script + } + pub fn line_index(&self) -> Arc { self.line_index.clone() } @@ -764,14 +794,7 @@ fn resolve_media_type( specifier: &ModuleSpecifier, maybe_headers: Option<&HashMap>, maybe_language_id: Option, - resolver: &LspResolver, ) -> MediaType { - if resolver.in_node_modules(specifier) { - if let Some(media_type) = resolver.node_media_type(specifier) { - return media_type; - } - } - if let Some(language_id) = maybe_language_id { return MediaType::from_specifier_and_content_type( specifier, @@ -809,6 +832,7 @@ impl FileSystemDocuments { pub fn get( &self, specifier: &ModuleSpecifier, + is_cjs_resolver: &LspIsCjsResolver, resolver: &Arc, config: &Arc, cache: &Arc, @@ -832,7 +856,14 @@ impl FileSystemDocuments { }; if dirty { // attempt to update the file on the file system - self.refresh_document(specifier, resolver, config, cache, file_referrer) + self.refresh_document( + specifier, + is_cjs_resolver, + resolver, + config, + cache, + file_referrer, + ) } else { old_doc } @@ -843,13 +874,14 @@ impl FileSystemDocuments { fn refresh_document( &self, specifier: &ModuleSpecifier, + is_cjs_resolver: &LspIsCjsResolver, resolver: &Arc, config: &Arc, cache: &Arc, file_referrer: Option<&ModuleSpecifier>, ) -> Option> { let doc = if specifier.scheme() == "file" { - let path = specifier_to_file_path(specifier).ok()?; + let path = url_to_file_path(specifier).ok()?; let bytes = fs::read(path).ok()?; let content = deno_graph::source::decode_owned_source(specifier, bytes, None).ok()?; @@ -859,6 +891,7 @@ impl FileSystemDocuments { None, None, None, + is_cjs_resolver, resolver.clone(), config.clone(), cache, @@ -875,6 +908,7 @@ impl FileSystemDocuments { None, None, None, + is_cjs_resolver, resolver.clone(), config.clone(), cache, @@ -902,6 +936,7 @@ impl FileSystemDocuments { None, None, maybe_headers, + is_cjs_resolver, resolver.clone(), config.clone(), cache, @@ -942,6 +977,11 @@ pub struct Documents { /// The DENO_DIR that the documents looks for non-file based modules. cache: Arc, config: Arc, + /// Resolver for detecting if a document is CJS or ESM. + is_cjs_resolver: Arc, + /// A resolver that takes into account currently loaded import map and JSX + /// settings. + resolver: Arc, /// A flag that indicates that stated data is potentially invalid and needs to /// be recalculated before being considered valid. dirty: bool, @@ -949,9 +989,6 @@ pub struct Documents { open_docs: HashMap>, /// Documents stored on the file system. file_system_docs: Arc, - /// A resolver that takes into account currently loaded import map and JSX - /// settings. - resolver: Arc, /// The npm package requirements found in npm specifiers. npm_reqs_by_scope: Arc, BTreeSet>>, @@ -982,6 +1019,7 @@ impl Documents { // the cache for remote modules here in order to get the // x-typescript-types? None, + &self.is_cjs_resolver, self.resolver.clone(), self.config.clone(), &self.cache, @@ -1016,7 +1054,7 @@ impl Documents { )) })?; self.dirty = true; - let doc = doc.with_change(version, changes)?; + let doc = doc.with_change(&self.is_cjs_resolver, version, changes)?; self.open_docs.insert(doc.specifier().clone(), doc.clone()); Ok(doc) } @@ -1071,34 +1109,6 @@ impl Documents { self.cache.is_valid_file_referrer(specifier) } - /// Return `true` if the provided specifier can be resolved to a document, - /// otherwise `false`. - pub fn contains_import( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - ) -> bool { - let file_referrer = self.get_file_referrer(referrer); - let maybe_specifier = self - .resolver - .as_graph_resolver(file_referrer.as_deref()) - .resolve( - specifier, - &deno_graph::Range { - specifier: referrer.clone(), - start: deno_graph::Position::zeroed(), - end: deno_graph::Position::zeroed(), - }, - ResolutionMode::Types, - ) - .ok(); - if let Some(import_specifier) = maybe_specifier { - self.exists(&import_specifier, file_referrer.as_deref()) - } else { - false - } - } - pub fn resolve_document_specifier( &self, specifier: &ModuleSpecifier, @@ -1136,7 +1146,7 @@ impl Documents { return true; } if specifier.scheme() == "file" { - return specifier_to_file_path(&specifier) + return url_to_file_path(&specifier) .map(|p| p.is_file()) .unwrap_or(false); } @@ -1173,6 +1183,7 @@ impl Documents { if let Some(old_doc) = old_doc { self.file_system_docs.get( specifier, + &self.is_cjs_resolver, &self.resolver, &self.config, &self.cache, @@ -1197,6 +1208,7 @@ impl Documents { } else { self.file_system_docs.get( &specifier, + &self.is_cjs_resolver, &self.resolver, &self.config, &self.cache, @@ -1255,12 +1267,15 @@ impl Documents { referrer: &ModuleSpecifier, file_referrer: Option<&ModuleSpecifier>, ) -> Vec> { - let document = self.get(referrer); - let file_referrer = document + let referrer_doc = self.get(referrer); + let file_referrer = referrer_doc .as_ref() .and_then(|d| d.file_referrer()) .or(file_referrer); - let dependencies = document.as_ref().map(|d| d.dependencies()); + let dependencies = referrer_doc.as_ref().map(|d| d.dependencies()); + let referrer_kind = self + .is_cjs_resolver + .get_maybe_doc_module_kind(referrer, referrer_doc.as_deref()); let mut results = Vec::new(); for raw_specifier in raw_specifiers { if raw_specifier.starts_with("asset:") { @@ -1277,31 +1292,35 @@ impl Documents { results.push(self.resolve_dependency( specifier, referrer, + referrer_kind, file_referrer, )); } else if let Some(specifier) = dep.maybe_code.maybe_specifier() { results.push(self.resolve_dependency( specifier, referrer, + referrer_kind, file_referrer, )); } else { results.push(None); } } else if let Ok(specifier) = - self.resolver.as_graph_resolver(file_referrer).resolve( + self.resolver.as_cli_resolver(file_referrer).resolve( raw_specifier, &deno_graph::Range { specifier: referrer.clone(), start: deno_graph::Position::zeroed(), end: deno_graph::Position::zeroed(), }, + referrer_kind, ResolutionMode::Types, ) { results.push(self.resolve_dependency( &specifier, referrer, + referrer_kind, file_referrer, )); } else { @@ -1320,12 +1339,16 @@ impl Documents { ) { self.config = Arc::new(config.clone()); self.cache = Arc::new(cache.clone()); + self.is_cjs_resolver = Arc::new(LspIsCjsResolver::new(cache)); self.resolver = resolver.clone(); + + node_resolver::PackageJsonThreadLocalCache::clear(); + { let fs_docs = &self.file_system_docs; // Clean up non-existent documents. fs_docs.docs.retain(|specifier, _| { - let Ok(path) = specifier_to_file_path(specifier) else { + let Ok(path) = url_to_file_path(specifier) else { // Remove non-file schemed docs (deps). They may not be dependencies // anymore after updating resolvers. return false; @@ -1340,14 +1363,21 @@ impl Documents { if !config.specifier_enabled(doc.specifier()) { continue; } - *doc = doc.with_new_config(self.resolver.clone(), self.config.clone()); + *doc = doc.with_new_config( + &self.is_cjs_resolver, + self.resolver.clone(), + self.config.clone(), + ); } for mut doc in self.file_system_docs.docs.iter_mut() { if !config.specifier_enabled(doc.specifier()) { continue; } - *doc.value_mut() = - doc.with_new_config(self.resolver.clone(), self.config.clone()); + *doc.value_mut() = doc.with_new_config( + &self.is_cjs_resolver, + self.resolver.clone(), + self.config.clone(), + ); } self.open_docs = open_docs; let mut preload_count = 0; @@ -1364,6 +1394,7 @@ impl Documents { { fs_docs.refresh_document( specifier, + &self.is_cjs_resolver, &self.resolver, &self.config, &self.cache, @@ -1449,6 +1480,7 @@ impl Documents { &self, specifier: &ModuleSpecifier, referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, file_referrer: Option<&ModuleSpecifier>, ) -> Option<(ModuleSpecifier, MediaType)> { if let Some(module_name) = specifier.as_str().strip_prefix("node:") { @@ -1462,10 +1494,12 @@ impl Documents { let mut specifier = specifier.clone(); let mut media_type = None; if let Ok(npm_ref) = NpmPackageReqReference::from_specifier(&specifier) { - let (s, mt) = - self - .resolver - .npm_to_file_url(&npm_ref, referrer, file_referrer)?; + let (s, mt) = self.resolver.npm_to_file_url( + &npm_ref, + referrer, + referrer_kind, + file_referrer, + )?; specifier = s; media_type = Some(mt); } @@ -1475,7 +1509,8 @@ impl Documents { return Some((specifier, media_type)); }; if let Some(types) = doc.maybe_types_dependency().maybe_specifier() { - self.resolve_dependency(types, &specifier, file_referrer) + let specifier_kind = self.is_cjs_resolver.get_doc_module_kind(&doc); + self.resolve_dependency(types, &specifier, specifier_kind, file_referrer) } else { Some((doc.specifier().clone(), doc.media_type())) } @@ -1543,6 +1578,7 @@ fn parse_and_analyze_module( maybe_headers: Option<&HashMap>, media_type: MediaType, file_referrer: Option<&ModuleSpecifier>, + is_cjs_resolver: &LspIsCjsResolver, resolver: &LspResolver, ) -> (Option, Option) { let parsed_source_result = parse_source(specifier.clone(), text, media_type); @@ -1551,6 +1587,7 @@ fn parse_and_analyze_module( &parsed_source_result, maybe_headers, file_referrer, + is_cjs_resolver, resolver, ); (Some(parsed_source_result), Some(module_result)) @@ -1561,7 +1598,7 @@ fn parse_source( text: Arc, media_type: MediaType, ) -> ParsedSourceResult { - deno_ast::parse_module(deno_ast::ParseParams { + deno_ast::parse_program(deno_ast::ParseParams { specifier, text, media_type, @@ -1576,11 +1613,26 @@ fn analyze_module( parsed_source_result: &ParsedSourceResult, maybe_headers: Option<&HashMap>, file_referrer: Option<&ModuleSpecifier>, + is_cjs_resolver: &LspIsCjsResolver, resolver: &LspResolver, ) -> ModuleResult { match parsed_source_result { Ok(parsed_source) => { let npm_resolver = resolver.create_graph_npm_resolver(file_referrer); + let cli_resolver = resolver.as_cli_resolver(file_referrer); + let config_data = resolver.as_config_data(file_referrer); + let valid_referrer = specifier.clone(); + let jsx_import_source_config = + config_data.and_then(|d| d.maybe_jsx_import_source_config()); + let resolver = SingleReferrerGraphResolver { + valid_referrer: &valid_referrer, + referrer_kind: is_cjs_resolver.get_lsp_referrer_kind( + &specifier, + Some(parsed_source.compute_is_script()), + ), + cli_resolver, + jsx_import_source_config: jsx_import_source_config.as_ref(), + }; Ok(deno_graph::parse_module_from_ast( deno_graph::ParseModuleFromAstOptions { graph_kind: deno_graph::GraphKind::TypesOnly, @@ -1591,7 +1643,7 @@ fn analyze_module( // dynamic imports like import(`./dir/${something}`) in the LSP file_system: &deno_graph::source::NullFileSystem, jsr_url_provider: &CliJsrUrlProvider, - maybe_resolver: Some(resolver.as_graph_resolver(file_referrer)), + maybe_resolver: Some(&resolver), maybe_npm_resolver: Some(&npm_resolver), }, )) diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 216b14de62..b2bd72416a 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -15,13 +15,14 @@ use deno_core::url::Url; use deno_core::ModuleSpecifier; use deno_graph::GraphKind; use deno_graph::Resolution; +use deno_path_util::url_to_file_path; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::RootCertStoreProvider; -use deno_runtime::fs_util::specifier_to_file_path; use deno_semver::jsr::JsrPackageReqReference; use indexmap::Equivalent; use indexmap::IndexSet; use log::error; +use node_resolver::NodeModuleKind; use serde::Deserialize; use serde_json::from_value; use std::collections::BTreeMap; @@ -77,6 +78,7 @@ use super::parent_process_checker; use super::performance::Performance; use super::refactor; use super::registries::ModuleRegistry; +use super::resolver::LspIsCjsResolver; use super::resolver::LspResolver; use super::testing; use super::text; @@ -96,6 +98,7 @@ use crate::args::CaData; use crate::args::CacheSetting; use crate::args::CliOptions; use crate::args::Flags; +use crate::args::InternalFlags; use crate::args::UnstableFmtOptions; use crate::factory::CliFactory; use crate::file_fetcher::FileFetcher; @@ -143,6 +146,7 @@ pub struct StateSnapshot { pub project_version: usize, pub assets: AssetsSnapshot, pub config: Arc, + pub is_cjs_resolver: Arc, pub documents: Arc, pub resolver: Arc, } @@ -202,16 +206,17 @@ pub struct Inner { pub documents: Documents, http_client_provider: Arc, initial_cwd: PathBuf, + pub is_cjs_resolver: Arc, jsr_search_api: CliJsrSearchApi, /// Handles module registries, which allow discovery of modules module_registry: ModuleRegistry, /// A lazily create "server" for handling test run requests. maybe_testing_server: Option, - npm_search_api: CliNpmSearchApi, + pub npm_search_api: CliNpmSearchApi, project_version: usize, /// A collection of measurements which instrument that performance of the LSP. performance: Arc, - resolver: Arc, + pub resolver: Arc, task_queue: LanguageServerTaskQueue, /// A memoized version of fixable diagnostic codes retrieved from TypeScript. ts_fixable_diagnostics: Vec, @@ -479,6 +484,7 @@ impl Inner { let initial_cwd = std::env::current_dir().unwrap_or_else(|_| { panic!("Could not resolve current working directory") }); + let is_cjs_resolver = Arc::new(LspIsCjsResolver::new(&cache)); Self { assets, @@ -490,6 +496,7 @@ impl Inner { documents, http_client_provider, initial_cwd: initial_cwd.clone(), + is_cjs_resolver, jsr_search_api, project_version: 0, task_queue: Default::default(), @@ -600,6 +607,7 @@ impl Inner { project_version: self.project_version, assets: self.assets.snapshot(), config: Arc::new(self.config.clone()), + is_cjs_resolver: self.is_cjs_resolver.clone(), documents: Arc::new(self.documents.clone()), resolver: self.resolver.snapshot(), }) @@ -621,12 +629,13 @@ impl Inner { } }); self.cache = LspCache::new(global_cache_url); + self.is_cjs_resolver = Arc::new(LspIsCjsResolver::new(&self.cache)); let deno_dir = self.cache.deno_dir(); let workspace_settings = self.config.workspace_settings(); let maybe_root_path = self .config .root_uri() - .and_then(|uri| specifier_to_file_path(uri).ok()); + .and_then(|uri| url_to_file_path(uri).ok()); let root_cert_store = get_root_cert_store( maybe_root_path, workspace_settings.certificate_stores.clone(), @@ -802,7 +811,7 @@ impl Inner { let mut roots = config .workspace_folders .iter() - .filter_map(|p| specifier_to_file_path(&p.0).ok()) + .filter_map(|p| url_to_file_path(&p.0).ok()) .collect::>(); roots.sort(); let roots = roots @@ -862,7 +871,10 @@ impl Inner { // We ignore these directories by default because there is a // high likelihood they aren't relevant. Someone can opt-into // them by specifying one of them as an enabled path. - if matches!(dir_name.as_str(), "vendor" | "node_modules" | ".git") { + if matches!( + dir_name.as_str(), + "vendor" | "coverage" | "node_modules" | ".git" + ) { continue; } // ignore cargo target directories for anyone using Deno with Rust @@ -903,7 +915,7 @@ impl Inner { | MediaType::Tsx => {} MediaType::Wasm | MediaType::SourceMap - | MediaType::TsBuildInfo + | MediaType::Css | MediaType::Unknown => { if path.extension().and_then(|s| s.to_str()) != Some("jsonc") { continue; @@ -962,6 +974,11 @@ impl Inner { .tree .refresh(&self.config.settings, &self.workspace_files, &file_fetcher) .await; + self + .client + .send_did_refresh_deno_configuration_tree_notification( + self.config.tree.to_did_refresh_params(), + ); for config_file in self.config.tree.config_files() { (|| { let compiler_options = config_file.to_compiler_options().ok()?.options; @@ -973,7 +990,7 @@ impl Inner { spawn(async move { let specifier = { let inner = ls.inner.read().await; - let resolver = inner.resolver.as_graph_resolver(Some(&referrer)); + let resolver = inner.resolver.as_cli_resolver(Some(&referrer)); let Ok(specifier) = resolver.resolve( &specifier, &deno_graph::Range { @@ -981,6 +998,7 @@ impl Inner { start: deno_graph::Position::zeroed(), end: deno_graph::Position::zeroed(), }, + NodeModuleKind::Esm, deno_graph::source::ResolutionMode::Types, ) else { return; @@ -1124,7 +1142,7 @@ impl Inner { { return; } - match specifier_to_file_path(&specifier) { + match url_to_file_path(&specifier) { Ok(path) if is_importable_ext(&path) => {} _ => return, } @@ -1362,7 +1380,7 @@ impl Inner { { specifier = uri_to_url(¶ms.text_document.uri); } - let file_path = specifier_to_file_path(&specifier).map_err(|err| { + let file_path = url_to_file_path(&specifier).map_err(|err| { error!("{:#}", err); LspError::invalid_request() })?; @@ -1378,14 +1396,10 @@ impl Inner { .clone(); fmt_options.use_tabs = Some(!params.options.insert_spaces); fmt_options.indent_width = Some(params.options.tab_size as u8); - let maybe_workspace = self - .config - .tree - .data_for_specifier(&specifier) - .map(|d| &d.member_dir.workspace); + let config_data = self.config.tree.data_for_specifier(&specifier); let unstable_options = UnstableFmtOptions { - component: maybe_workspace - .map(|w| w.has_unstable("fmt-component")) + component: config_data + .map(|d| d.unstable.contains("fmt-component")) .unwrap_or(false), }; let document = document.clone(); @@ -1612,11 +1626,15 @@ impl Inner { None => false, }) .collect(); + let mut code_actions = CodeActionCollection::default(); if !fixable_diagnostics.is_empty() { - let mut code_actions = CodeActionCollection::default(); let file_diagnostics = self .diagnostics_server .get_ts_diagnostics(&specifier, asset_or_doc.document_lsp_version()); + let specifier_kind = asset_or_doc + .document() + .map(|d| self.is_cjs_resolver.get_doc_module_kind(d)) + .unwrap_or(NodeModuleKind::Esm); let mut includes_no_cache = false; for diagnostic in &fixable_diagnostics { match diagnostic.source.as_deref() { @@ -1655,7 +1673,13 @@ impl Inner { .await; for action in actions { code_actions - .add_ts_fix_action(&specifier, &action, diagnostic, self) + .add_ts_fix_action( + &specifier, + specifier_kind, + &action, + diagnostic, + self, + ) .map_err(|err| { error!("Unable to convert fix: {:#}", err); LspError::internal_error() @@ -1721,9 +1745,14 @@ impl Inner { .add_cache_all_action(&specifier, no_cache_diagnostics.to_owned()); } } - code_actions.set_preferred_fixes(); - all_actions.extend(code_actions.get_response()); } + if let Some(document) = asset_or_doc.document() { + code_actions + .add_source_actions(document, ¶ms.range, self) + .await; + } + code_actions.set_preferred_fixes(); + all_actions.extend(code_actions.get_response()); // Refactor let only = params @@ -1796,10 +1825,9 @@ impl Inner { error!("Unable to decode code action data: {:#}", err); LspError::invalid_params("The CodeAction's data is invalid.") })?; - let scope = self - .get_asset_or_document(&code_action_data.specifier) - .ok() - .and_then(|d| d.scope().cloned()); + let maybe_asset_or_doc = + self.get_asset_or_document(&code_action_data.specifier).ok(); + let scope = maybe_asset_or_doc.as_ref().and_then(|d| d.scope().cloned()); let combined_code_actions = self .ts_server .get_combined_code_fix( @@ -1826,8 +1854,13 @@ impl Inner { let changes = if code_action_data.fix_id == "fixMissingImport" { fix_ts_import_changes( &code_action_data.specifier, + maybe_asset_or_doc + .as_ref() + .and_then(|d| d.document()) + .map(|d| self.is_cjs_resolver.get_doc_module_kind(d)) + .unwrap_or(NodeModuleKind::Esm), &combined_code_actions.changes, - &self.get_ts_response_import_mapper(&code_action_data.specifier), + self, ) .map_err(|err| { error!("Unable to remap changes: {:#}", err); @@ -1879,8 +1912,12 @@ impl Inner { if kind_suffix == ".rewrite.function.returnType" { refactor_edit_info.edits = fix_ts_import_changes( &action_data.specifier, + asset_or_doc + .document() + .map(|d| self.is_cjs_resolver.get_doc_module_kind(d)) + .unwrap_or(NodeModuleKind::Esm), &refactor_edit_info.edits, - &self.get_ts_response_import_mapper(&action_data.specifier), + self, ) .map_err(|err| { error!("Unable to remap changes: {:#}", err); @@ -1911,7 +1948,9 @@ impl Inner { // todo(dsherret): this should probably just take the resolver itself // as the import map is an implementation detail .and_then(|d| d.resolver.maybe_import_map()), - self.resolver.as_ref(), + &self.resolver, + &self.ts_server.specifier_map, + file_referrer, ) } @@ -2226,6 +2265,7 @@ impl Inner { &self.jsr_search_api, &self.npm_search_api, &self.documents, + &self.is_cjs_resolver, self.resolver.as_ref(), self .config @@ -2273,7 +2313,11 @@ impl Inner { .into(), scope.cloned(), ) - .await; + .await + .unwrap_or_else(|err| { + error!("Unable to get completion info from TypeScript: {:#}", err); + None + }); if let Some(completions) = maybe_completion_info { response = Some( @@ -2508,7 +2552,7 @@ impl Inner { let maybe_root_path_owned = self .config .root_uri() - .and_then(|uri| specifier_to_file_path(uri).ok()); + .and_then(|uri| url_to_file_path(uri).ok()); let mut resolved_items = Vec::::new(); for item in incoming_calls.iter() { if let Some(resolved) = item.try_resolve_call_hierarchy_incoming_call( @@ -2554,7 +2598,7 @@ impl Inner { let maybe_root_path_owned = self .config .root_uri() - .and_then(|uri| specifier_to_file_path(uri).ok()); + .and_then(|uri| url_to_file_path(uri).ok()); let mut resolved_items = Vec::::new(); for item in outgoing_calls.iter() { if let Some(resolved) = item.try_resolve_call_hierarchy_outgoing_call( @@ -2603,7 +2647,7 @@ impl Inner { let maybe_root_path_owned = self .config .root_uri() - .and_then(|uri| specifier_to_file_path(uri).ok()); + .and_then(|uri| url_to_file_path(uri).ok()); let mut resolved_items = Vec::::new(); match one_or_many { tsc::OneOrMany::One(item) => { @@ -3600,7 +3644,10 @@ impl Inner { }; let cli_options = CliOptions::new( Arc::new(Flags { - cache_path: Some(self.cache.deno_dir().root.clone()), + internal: InternalFlags { + cache_path: Some(self.cache.deno_dir().root.clone()), + ..Default::default() + }, ca_stores: workspace_settings.certificate_stores.clone(), ca_data: workspace_settings.tls_certificate.clone().map(CaData::File), unsafely_ignore_certificate_errors: workspace_settings @@ -3797,7 +3844,7 @@ impl Inner { let maybe_inlay_hints = maybe_inlay_hints.map(|hints| { hints .iter() - .map(|hint| hint.to_lsp(line_index.clone())) + .map(|hint| hint.to_lsp(line_index.clone(), self)) .collect() }); self.performance.measure(mark); @@ -3933,7 +3980,9 @@ mod tests { fn test_walk_workspace() { let temp_dir = TempDir::new(); temp_dir.create_dir_all("root1/vendor/"); + temp_dir.create_dir_all("root1/coverage/"); temp_dir.write("root1/vendor/mod.ts", ""); // no, vendor + temp_dir.write("root1/coverage/mod.ts", ""); // no, coverage temp_dir.create_dir_all("root1/node_modules/"); temp_dir.write("root1/node_modules/mod.ts", ""); // no, node_modules diff --git a/cli/lsp/lsp_custom.rs b/cli/lsp/lsp_custom.rs index 5f485db7ac..b570b6d0e2 100644 --- a/cli/lsp/lsp_custom.rs +++ b/cli/lsp/lsp_custom.rs @@ -46,6 +46,30 @@ pub struct DiagnosticBatchNotificationParams { pub messages_len: usize, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DenoConfigurationData { + pub scope_uri: lsp::Uri, + pub workspace_root_scope_uri: Option, + pub deno_json: Option, + pub package_json: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DidRefreshDenoConfigurationTreeNotificationParams { + pub data: Vec, +} + +pub enum DidRefreshDenoConfigurationTreeNotification {} + +impl lsp::notification::Notification + for DidRefreshDenoConfigurationTreeNotification +{ + type Params = DidRefreshDenoConfigurationTreeNotificationParams; + const METHOD: &'static str = "deno/didRefreshDenoConfigurationTree"; +} + #[derive(Debug, Eq, Hash, PartialEq, Copy, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum DenoConfigurationChangeType { @@ -88,13 +112,15 @@ pub struct DidChangeDenoConfigurationNotificationParams { pub changes: Vec, } +// TODO(nayeemrmn): This is being replaced by +// `DidRefreshDenoConfigurationTreeNotification` for Deno > v2.0.0. Remove it +// soon. pub enum DidChangeDenoConfigurationNotification {} impl lsp::notification::Notification for DidChangeDenoConfigurationNotification { type Params = DidChangeDenoConfigurationNotificationParams; - const METHOD: &'static str = "deno/didChangeDenoConfiguration"; } @@ -102,7 +128,6 @@ pub enum DidUpgradeCheckNotification {} impl lsp::notification::Notification for DidUpgradeCheckNotification { type Params = DidUpgradeCheckNotificationParams; - const METHOD: &'static str = "deno/didUpgradeCheck"; } @@ -125,6 +150,5 @@ pub enum DiagnosticBatchNotification {} impl lsp::notification::Notification for DiagnosticBatchNotification { type Params = DiagnosticBatchNotificationParams; - const METHOD: &'static str = "deno/internalTestDiagnosticBatch"; } diff --git a/cli/lsp/npm.rs b/cli/lsp/npm.rs index 8bdeb7e7d8..2decfc3429 100644 --- a/cli/lsp/npm.rs +++ b/cli/lsp/npm.rs @@ -4,6 +4,7 @@ use dashmap::DashMap; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::serde_json; +use deno_npm::npm_rc::NpmRc; use deno_semver::package::PackageNv; use deno_semver::Version; use serde::Deserialize; @@ -25,7 +26,10 @@ pub struct CliNpmSearchApi { impl CliNpmSearchApi { pub fn new(file_fetcher: Arc) -> Self { - let resolver = NpmFetchResolver::new(file_fetcher.clone()); + let resolver = NpmFetchResolver::new( + file_fetcher.clone(), + Arc::new(NpmRc::default().as_resolved(npm_registry_url()).unwrap()), + ); Self { file_fetcher, resolver, diff --git a/cli/lsp/parent_process_checker.rs b/cli/lsp/parent_process_checker.rs index e5b2b2f231..b8a42cd1a4 100644 --- a/cli/lsp/parent_process_checker.rs +++ b/cli/lsp/parent_process_checker.rs @@ -11,7 +11,7 @@ pub fn start(parent_process_id: u32) { std::thread::sleep(Duration::from_secs(10)); if !is_process_active(parent_process_id) { - std::process::exit(1); + deno_runtime::exit(1); } }); } diff --git a/cli/lsp/registries.rs b/cli/lsp/registries.rs index 5f7ce00823..ade353e683 100644 --- a/cli/lsp/registries.rs +++ b/cli/lsp/registries.rs @@ -482,6 +482,7 @@ impl ModuleRegistry { .fetch_with_options(FetchOptions { specifier: &specifier, permissions: FetchPermissionsOptionRef::AllowAll, + maybe_auth: None, maybe_accept: Some("application/vnd.deno.reg.v2+json, application/vnd.deno.reg.v1+json;q=0.9, application/json;q=0.8"), maybe_cache_setting: None, }) diff --git a/cli/lsp/repl.rs b/cli/lsp/repl.rs index fa5809045e..b4aaa8cd0d 100644 --- a/cli/lsp/repl.rs +++ b/cli/lsp/repl.rs @@ -263,7 +263,7 @@ impl ReplLanguageServer { } fn get_document_uri(&self) -> Uri { - uri_parse_unencoded(self.cwd_uri.join("$deno$repl.ts").unwrap().as_str()) + uri_parse_unencoded(self.cwd_uri.join("$deno$repl.mts").unwrap().as_str()) .unwrap() } } diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index 2844eb6f98..399b896381 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -2,27 +2,35 @@ use dashmap::DashMap; use deno_ast::MediaType; +use deno_cache_dir::npm::NpmCacheDir; use deno_cache_dir::HttpCache; +use deno_config::deno_json::JsxImportSourceConfig; use deno_config::workspace::PackageJsonDepResolution; use deno_config::workspace::WorkspaceResolver; use deno_core::url::Url; -use deno_graph::source::Resolver; +use deno_graph::source::ResolutionMode; use deno_graph::GraphImport; use deno_graph::ModuleSpecifier; +use deno_graph::Range; use deno_npm::NpmSystemInfo; +use deno_path_util::url_from_directory_path; +use deno_path_util::url_to_file_path; +use deno_resolver::npm::NpmReqResolverOptions; +use deno_resolver::DenoResolverOptions; +use deno_resolver::NodeAndNpmReqResolver; use deno_runtime::deno_fs; use deno_runtime::deno_node::NodeResolver; use deno_runtime::deno_node::PackageJson; -use deno_runtime::fs_util::specifier_to_file_path; +use deno_runtime::deno_node::PackageJsonResolver; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use indexmap::IndexMap; use node_resolver::errors::ClosestPkgJsonError; -use node_resolver::NodeResolution; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; use node_resolver::NodeResolutionMode; -use node_resolver::NpmResolver; use std::borrow::Cow; use std::collections::BTreeMap; use std::collections::BTreeSet; @@ -31,51 +39,69 @@ use std::collections::HashSet; use std::sync::Arc; use super::cache::LspCache; +use super::documents::Document; use super::jsr::JsrCacheResolver; use crate::args::create_default_npmrc; use crate::args::CacheSetting; use crate::args::CliLockfile; use crate::args::NpmInstallDepsProvider; +use crate::cache::DenoCacheEnvFsAdapter; +use crate::factory::Deferred; use crate::graph_util::CliJsrUrlProvider; use crate::http_util::HttpClientProvider; use crate::lsp::config::Config; use crate::lsp::config::ConfigData; use crate::lsp::logging::lsp_warn; use crate::npm::create_cli_npm_resolver_for_lsp; +use crate::npm::CliByonmNpmResolverCreateOptions; +use crate::npm::CliManagedInNpmPkgCheckerCreateOptions; +use crate::npm::CliManagedNpmResolverCreateOptions; use crate::npm::CliNpmResolver; -use crate::npm::CliNpmResolverByonmCreateOptions; use crate::npm::CliNpmResolverCreateOptions; -use crate::npm::CliNpmResolverManagedCreateOptions; use crate::npm::CliNpmResolverManagedSnapshotOption; +use crate::npm::CreateInNpmPkgCheckerOptions; use crate::npm::ManagedCliNpmResolver; -use crate::resolver::CjsResolutionStore; -use crate::resolver::CliGraphResolver; -use crate::resolver::CliGraphResolverOptions; -use crate::resolver::CliNodeResolver; +use crate::resolver::CliDenoResolver; +use crate::resolver::CliDenoResolverFs; +use crate::resolver::CliNpmReqResolver; +use crate::resolver::CliResolver; +use crate::resolver::CliResolverOptions; +use crate::resolver::IsCjsResolver; use crate::resolver::WorkerCliNpmGraphResolver; +use crate::tsc::into_specifier_and_media_type; +use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; #[derive(Debug, Clone)] struct LspScopeResolver { - graph_resolver: Arc, + resolver: Arc, + in_npm_pkg_checker: Arc, jsr_resolver: Option>, npm_resolver: Option>, - node_resolver: Option>, + node_resolver: Option>, + npm_pkg_req_resolver: Option>, + pkg_json_resolver: Arc, redirect_resolver: Option>, graph_imports: Arc>, + package_json_deps_by_resolution: Arc>, config_data: Option>, } impl Default for LspScopeResolver { fn default() -> Self { + let factory = ResolverFactory::new(None); Self { - graph_resolver: create_graph_resolver(None, None, None), + resolver: factory.cli_resolver().clone(), + in_npm_pkg_checker: factory.in_npm_pkg_checker().clone(), jsr_resolver: None, npm_resolver: None, node_resolver: None, + npm_pkg_req_resolver: None, + pkg_json_resolver: factory.pkg_json_resolver().clone(), redirect_resolver: None, graph_imports: Default::default(), + package_json_deps_by_resolution: Default::default(), config_data: None, } } @@ -87,22 +113,16 @@ impl LspScopeResolver { cache: &LspCache, http_client_provider: Option<&Arc>, ) -> Self { - let mut npm_resolver = None; - let mut node_resolver = None; - if let Some(http_client) = http_client_provider { - npm_resolver = create_npm_resolver( - config_data.map(|d| d.as_ref()), - cache, - http_client, - ) - .await; - node_resolver = create_node_resolver(npm_resolver.as_ref()); + let mut factory = ResolverFactory::new(config_data); + if let Some(http_client_provider) = http_client_provider { + factory.init_npm_resolver(http_client_provider, cache).await; } - let graph_resolver = create_graph_resolver( - config_data.map(|d| d.as_ref()), - npm_resolver.as_ref(), - node_resolver.as_ref(), - ); + let in_npm_pkg_checker = factory.in_npm_pkg_checker().clone(); + let npm_resolver = factory.npm_resolver().cloned(); + let node_resolver = factory.node_resolver().cloned(); + let npm_pkg_req_resolver = factory.npm_pkg_req_resolver().cloned(); + let cli_resolver = factory.cli_resolver().clone(); + let pkg_json_resolver = factory.pkg_json_resolver().clone(); let jsr_resolver = Some(Arc::new(JsrCacheResolver::new( cache.for_specifier(config_data.map(|d| d.scope.as_ref())), config_data.map(|d| d.as_ref()), @@ -111,7 +131,9 @@ impl LspScopeResolver { cache.for_specifier(config_data.map(|d| d.scope.as_ref())), config_data.and_then(|d| d.lockfile.clone()), ))); - let npm_graph_resolver = graph_resolver.create_graph_npm_resolver(); + let npm_graph_resolver = cli_resolver.create_graph_npm_resolver(); + let maybe_jsx_import_source_config = + config_data.and_then(|d| d.maybe_jsx_import_source_config()); let graph_imports = config_data .and_then(|d| d.member_dir.workspace.to_compiler_option_types().ok()) .map(|imports| { @@ -119,11 +141,18 @@ impl LspScopeResolver { imports .into_iter() .map(|(referrer, imports)| { + let resolver = SingleReferrerGraphResolver { + valid_referrer: &referrer, + referrer_kind: NodeModuleKind::Esm, + cli_resolver: &cli_resolver, + jsx_import_source_config: maybe_jsx_import_source_config + .as_ref(), + }; let graph_import = GraphImport::new( &referrer, imports, &CliJsrUrlProvider, - Some(graph_resolver.as_ref()), + Some(&resolver), Some(&npm_graph_resolver), ); (referrer, graph_import) @@ -132,33 +161,70 @@ impl LspScopeResolver { ) }) .unwrap_or_default(); + let package_json_deps_by_resolution = (|| { + let npm_pkg_req_resolver = npm_pkg_req_resolver.as_ref()?; + let package_json = config_data?.maybe_pkg_json()?; + let referrer = package_json.specifier(); + let dependencies = package_json.dependencies.as_ref()?; + let result = dependencies + .iter() + .flat_map(|(name, _)| { + let req_ref = + NpmPackageReqReference::from_str(&format!("npm:{name}")).ok()?; + let specifier = into_specifier_and_media_type(Some( + npm_pkg_req_resolver + .resolve_req_reference( + &req_ref, + &referrer, + // todo(dsherret): this is wrong because it doesn't consider CJS referrers + NodeModuleKind::Esm, + NodeResolutionMode::Types, + ) + .ok()?, + )) + .0; + Some((specifier, name.clone())) + }) + .collect(); + Some(result) + })(); + let package_json_deps_by_resolution = + Arc::new(package_json_deps_by_resolution.unwrap_or_default()); Self { - graph_resolver, + resolver: cli_resolver, + in_npm_pkg_checker, jsr_resolver, + npm_pkg_req_resolver, npm_resolver, node_resolver, + pkg_json_resolver, redirect_resolver, graph_imports, + package_json_deps_by_resolution, config_data: config_data.cloned(), } } fn snapshot(&self) -> Arc { + let mut factory = ResolverFactory::new(self.config_data.as_ref()); let npm_resolver = self.npm_resolver.as_ref().map(|r| r.clone_snapshotted()); - let node_resolver = create_node_resolver(npm_resolver.as_ref()); - let graph_resolver = create_graph_resolver( - self.config_data.as_deref(), - npm_resolver.as_ref(), - node_resolver.as_ref(), - ); + if let Some(npm_resolver) = &npm_resolver { + factory.set_npm_resolver(npm_resolver.clone()); + } Arc::new(Self { - graph_resolver, + resolver: factory.cli_resolver().clone(), + in_npm_pkg_checker: factory.in_npm_pkg_checker().clone(), jsr_resolver: self.jsr_resolver.clone(), - npm_resolver, - node_resolver, + npm_pkg_req_resolver: factory.npm_pkg_req_resolver().cloned(), + npm_resolver: factory.npm_resolver().cloned(), + node_resolver: factory.node_resolver().cloned(), redirect_resolver: self.redirect_resolver.clone(), + pkg_json_resolver: factory.pkg_json_resolver().clone(), graph_imports: self.graph_imports.clone(), + package_json_deps_by_resolution: self + .package_json_deps_by_resolution + .clone(), config_data: self.config_data.clone(), }) } @@ -244,12 +310,12 @@ impl LspResolver { } } - pub fn as_graph_resolver( + pub fn as_cli_resolver( &self, file_referrer: Option<&ModuleSpecifier>, - ) -> &dyn Resolver { + ) -> &CliResolver { let resolver = self.get_scope_resolver(file_referrer); - resolver.graph_resolver.as_ref() + resolver.resolver.as_ref() } pub fn create_graph_npm_resolver( @@ -257,7 +323,23 @@ impl LspResolver { file_referrer: Option<&ModuleSpecifier>, ) -> WorkerCliNpmGraphResolver { let resolver = self.get_scope_resolver(file_referrer); - resolver.graph_resolver.create_graph_npm_resolver() + resolver.resolver.create_graph_npm_resolver() + } + + pub fn as_config_data( + &self, + file_referrer: Option<&ModuleSpecifier>, + ) -> Option<&Arc> { + let resolver = self.get_scope_resolver(file_referrer); + resolver.config_data.as_ref() + } + + pub fn in_npm_pkg_checker( + &self, + file_referrer: Option<&ModuleSpecifier>, + ) -> &Arc { + let resolver = self.get_scope_resolver(file_referrer); + &resolver.in_npm_pkg_checker } pub fn maybe_managed_npm_resolver( @@ -323,15 +405,33 @@ impl LspResolver { &self, req_ref: &NpmPackageReqReference, referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, file_referrer: Option<&ModuleSpecifier>, ) -> Option<(ModuleSpecifier, MediaType)> { let resolver = self.get_scope_resolver(file_referrer); - let node_resolver = resolver.node_resolver.as_ref()?; - Some(NodeResolution::into_specifier_and_media_type( - node_resolver - .resolve_req_reference(req_ref, referrer, NodeResolutionMode::Types) - .ok(), - )) + let npm_pkg_req_resolver = resolver.npm_pkg_req_resolver.as_ref()?; + Some(into_specifier_and_media_type(Some( + npm_pkg_req_resolver + .resolve_req_reference( + req_ref, + referrer, + referrer_kind, + NodeResolutionMode::Types, + ) + .ok()?, + ))) + } + + pub fn file_url_to_package_json_dep( + &self, + specifier: &ModuleSpecifier, + file_referrer: Option<&ModuleSpecifier>, + ) -> Option { + let resolver = self.get_scope_resolver(file_referrer); + resolver + .package_json_deps_by_resolution + .get(specifier) + .cloned() } pub fn in_node_modules(&self, specifier: &ModuleSpecifier) -> bool { @@ -345,14 +445,10 @@ impl LspResolver { .contains("/node_modules/") } - let global_npm_resolver = self - .get_scope_resolver(Some(specifier)) - .npm_resolver - .as_ref() - .and_then(|npm_resolver| npm_resolver.as_managed()) - .filter(|r| r.root_node_modules_path().is_none()); - if let Some(npm_resolver) = &global_npm_resolver { - if npm_resolver.in_npm_package(specifier) { + if let Some(node_resolver) = + &self.get_scope_resolver(Some(specifier)).node_resolver + { + if node_resolver.in_npm_package(specifier) { return true; } } @@ -360,16 +456,27 @@ impl LspResolver { has_node_modules_dir(specifier) } - pub fn node_media_type( + pub fn is_bare_package_json_dep( &self, - specifier: &ModuleSpecifier, - ) -> Option { - let resolver = self.get_scope_resolver(Some(specifier)); - let node_resolver = resolver.node_resolver.as_ref()?; - let resolution = node_resolver - .url_to_node_resolution(specifier.clone()) - .ok()?; - Some(NodeResolution::into_specifier_and_media_type(Some(resolution)).1) + specifier_text: &str, + referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, + ) -> bool { + let resolver = self.get_scope_resolver(Some(referrer)); + let Some(npm_pkg_req_resolver) = resolver.npm_pkg_req_resolver.as_ref() + else { + return false; + }; + npm_pkg_req_resolver + .resolve_if_for_npm_pkg( + specifier_text, + referrer, + referrer_kind, + NodeResolutionMode::Types, + ) + .ok() + .flatten() + .is_some() } pub fn get_closest_package_json( @@ -377,10 +484,9 @@ impl LspResolver { referrer: &ModuleSpecifier, ) -> Result>, ClosestPkgJsonError> { let resolver = self.get_scope_resolver(Some(referrer)); - let Some(node_resolver) = resolver.node_resolver.as_ref() else { - return Ok(None); - }; - node_resolver.get_closest_package_json(referrer) + resolver + .pkg_json_resolver + .get_closest_package_json(referrer) } pub fn resolve_redirects( @@ -432,113 +538,206 @@ impl LspResolver { } } -async fn create_npm_resolver( - config_data: Option<&ConfigData>, - cache: &LspCache, - http_client_provider: &Arc, -) -> Option> { - let enable_byonm = config_data.map(|d| d.byonm).unwrap_or(false); - let options = if enable_byonm { - CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions { - fs: Arc::new(deno_fs::RealFs), - root_node_modules_dir: config_data.and_then(|config_data| { - config_data.node_modules_dir.clone().or_else(|| { - specifier_to_file_path(&config_data.scope) - .ok() - .map(|p| p.join("node_modules/")) - }) - }), - }) - } else { - CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions { - http_client_provider: http_client_provider.clone(), - snapshot: match config_data.and_then(|d| d.lockfile.as_ref()) { - Some(lockfile) => { - CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( - lockfile.clone(), - ) - } - None => CliNpmResolverManagedSnapshotOption::Specified(None), - }, - // Don't provide the lockfile. We don't want these resolvers - // updating it. Only the cache request should update the lockfile. - maybe_lockfile: None, - fs: Arc::new(deno_fs::RealFs), - npm_global_cache_dir: cache.deno_dir().npm_folder_path(), - // Use an "only" cache setting in order to make the - // user do an explicit "cache" command and prevent - // the cache from being filled with lots of packages while - // the user is typing. - cache_setting: CacheSetting::Only, - text_only_progress_bar: ProgressBar::new(ProgressBarStyle::TextOnly), - maybe_node_modules_path: config_data - .and_then(|d| d.node_modules_dir.clone()), - // only used for top level install, so we can ignore this - npm_install_deps_provider: Arc::new(NpmInstallDepsProvider::empty()), - npmrc: config_data +#[derive(Default)] +struct ResolverFactoryServices { + cli_resolver: Deferred>, + in_npm_pkg_checker: Deferred>, + node_resolver: Deferred>>, + npm_pkg_req_resolver: Deferred>>, + npm_resolver: Option>, +} + +struct ResolverFactory<'a> { + config_data: Option<&'a Arc>, + fs: Arc, + pkg_json_resolver: Arc, + services: ResolverFactoryServices, +} + +impl<'a> ResolverFactory<'a> { + pub fn new(config_data: Option<&'a Arc>) -> Self { + let fs = Arc::new(deno_fs::RealFs); + let pkg_json_resolver = Arc::new(PackageJsonResolver::new( + deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), + )); + Self { + config_data, + fs, + pkg_json_resolver, + services: Default::default(), + } + } + + async fn init_npm_resolver( + &mut self, + http_client_provider: &Arc, + cache: &LspCache, + ) { + let enable_byonm = self.config_data.map(|d| d.byonm).unwrap_or(false); + let options = if enable_byonm { + CliNpmResolverCreateOptions::Byonm(CliByonmNpmResolverCreateOptions { + fs: CliDenoResolverFs(Arc::new(deno_fs::RealFs)), + pkg_json_resolver: self.pkg_json_resolver.clone(), + root_node_modules_dir: self.config_data.and_then(|config_data| { + config_data.node_modules_dir.clone().or_else(|| { + url_to_file_path(&config_data.scope) + .ok() + .map(|p| p.join("node_modules/")) + }) + }), + }) + } else { + let npmrc = self + .config_data .and_then(|d| d.npmrc.clone()) - .unwrap_or_else(create_default_npmrc), - npm_system_info: NpmSystemInfo::default(), - lifecycle_scripts: Default::default(), + .unwrap_or_else(create_default_npmrc); + let npm_cache_dir = Arc::new(NpmCacheDir::new( + &DenoCacheEnvFsAdapter(self.fs.as_ref()), + cache.deno_dir().npm_folder_path(), + npmrc.get_all_known_registries_urls(), + )); + CliNpmResolverCreateOptions::Managed(CliManagedNpmResolverCreateOptions { + http_client_provider: http_client_provider.clone(), + snapshot: match self.config_data.and_then(|d| d.lockfile.as_ref()) { + Some(lockfile) => { + CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( + lockfile.clone(), + ) + } + None => CliNpmResolverManagedSnapshotOption::Specified(None), + }, + // Don't provide the lockfile. We don't want these resolvers + // updating it. Only the cache request should update the lockfile. + maybe_lockfile: None, + fs: Arc::new(deno_fs::RealFs), + npm_cache_dir, + // Use an "only" cache setting in order to make the + // user do an explicit "cache" command and prevent + // the cache from being filled with lots of packages while + // the user is typing. + cache_setting: CacheSetting::Only, + text_only_progress_bar: ProgressBar::new(ProgressBarStyle::TextOnly), + maybe_node_modules_path: self + .config_data + .and_then(|d| d.node_modules_dir.clone()), + // only used for top level install, so we can ignore this + npm_install_deps_provider: Arc::new(NpmInstallDepsProvider::empty()), + npmrc, + npm_system_info: NpmSystemInfo::default(), + lifecycle_scripts: Default::default(), + }) + }; + self.set_npm_resolver(create_cli_npm_resolver_for_lsp(options).await); + } + + pub fn set_npm_resolver(&mut self, npm_resolver: Arc) { + self.services.npm_resolver = Some(npm_resolver); + } + + pub fn npm_resolver(&self) -> Option<&Arc> { + self.services.npm_resolver.as_ref() + } + + pub fn cli_resolver(&self) -> &Arc { + self.services.cli_resolver.get_or_init(|| { + let npm_req_resolver = self.npm_pkg_req_resolver().cloned(); + let deno_resolver = Arc::new(CliDenoResolver::new(DenoResolverOptions { + in_npm_pkg_checker: self.in_npm_pkg_checker().clone(), + node_and_req_resolver: match (self.node_resolver(), npm_req_resolver) { + (Some(node_resolver), Some(npm_req_resolver)) => { + Some(NodeAndNpmReqResolver { + node_resolver: node_resolver.clone(), + npm_req_resolver, + }) + } + _ => None, + }, + sloppy_imports_resolver: self + .config_data + .and_then(|d| d.sloppy_imports_resolver.clone()), + workspace_resolver: self + .config_data + .map(|d| d.resolver.clone()) + .unwrap_or_else(|| { + Arc::new(WorkspaceResolver::new_raw( + // this is fine because this is only used before initialization + Arc::new(ModuleSpecifier::parse("file:///").unwrap()), + None, + Vec::new(), + Vec::new(), + PackageJsonDepResolution::Disabled, + )) + }), + is_byonm: self.config_data.map(|d| d.byonm).unwrap_or(false), + maybe_vendor_dir: self.config_data.and_then(|d| d.vendor_dir.as_ref()), + })); + Arc::new(CliResolver::new(CliResolverOptions { + deno_resolver, + npm_resolver: self.npm_resolver().cloned(), + bare_node_builtins_enabled: self + .config_data + .is_some_and(|d| d.unstable.contains("bare-node-builtins")), + })) }) - }; - Some(create_cli_npm_resolver_for_lsp(options).await) -} + } -fn create_node_resolver( - npm_resolver: Option<&Arc>, -) -> Option> { - use once_cell::sync::Lazy; + pub fn pkg_json_resolver(&self) -> &Arc { + &self.pkg_json_resolver + } - // it's not ideal to share this across all scopes and to - // never clear it, but it's fine for the time being - static CJS_RESOLUTIONS: Lazy> = - Lazy::new(Default::default); + pub fn in_npm_pkg_checker(&self) -> &Arc { + self.services.in_npm_pkg_checker.get_or_init(|| { + crate::npm::create_in_npm_pkg_checker( + match self.services.npm_resolver.as_ref().map(|r| r.as_inner()) { + Some(crate::npm::InnerCliNpmResolverRef::Byonm(_)) | None => { + CreateInNpmPkgCheckerOptions::Byonm + } + Some(crate::npm::InnerCliNpmResolverRef::Managed(m)) => { + CreateInNpmPkgCheckerOptions::Managed( + CliManagedInNpmPkgCheckerCreateOptions { + root_cache_dir_url: m.global_cache_root_url(), + maybe_node_modules_path: m.maybe_node_modules_path(), + }, + ) + } + }, + ) + }) + } - let npm_resolver = npm_resolver?; - let fs = Arc::new(deno_fs::RealFs); - let node_resolver_inner = Arc::new(NodeResolver::new( - deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), - npm_resolver.clone().into_npm_resolver(), - )); - Some(Arc::new(CliNodeResolver::new( - CJS_RESOLUTIONS.clone(), - fs, - node_resolver_inner, - npm_resolver.clone(), - ))) -} + pub fn node_resolver(&self) -> Option<&Arc> { + self + .services + .node_resolver + .get_or_init(|| { + let npm_resolver = self.services.npm_resolver.as_ref()?; + Some(Arc::new(NodeResolver::new( + deno_runtime::deno_node::DenoFsNodeResolverEnv::new(self.fs.clone()), + self.in_npm_pkg_checker().clone(), + npm_resolver.clone().into_npm_pkg_folder_resolver(), + self.pkg_json_resolver.clone(), + ))) + }) + .as_ref() + } -fn create_graph_resolver( - config_data: Option<&ConfigData>, - npm_resolver: Option<&Arc>, - node_resolver: Option<&Arc>, -) -> Arc { - let workspace = config_data.map(|d| &d.member_dir.workspace); - Arc::new(CliGraphResolver::new(CliGraphResolverOptions { - node_resolver: node_resolver.cloned(), - npm_resolver: npm_resolver.cloned(), - workspace_resolver: config_data.map(|d| d.resolver.clone()).unwrap_or_else( - || { - Arc::new(WorkspaceResolver::new_raw( - // this is fine because this is only used before initialization - Arc::new(ModuleSpecifier::parse("file:///").unwrap()), - None, - Vec::new(), - Vec::new(), - PackageJsonDepResolution::Disabled, - )) - }, - ), - maybe_jsx_import_source_config: workspace.and_then(|workspace| { - workspace.to_maybe_jsx_import_source_config().ok().flatten() - }), - maybe_vendor_dir: config_data.and_then(|d| d.vendor_dir.as_ref()), - bare_node_builtins_enabled: workspace - .is_some_and(|workspace| workspace.has_unstable("bare-node-builtins")), - sloppy_imports_resolver: config_data - .and_then(|d| d.sloppy_imports_resolver.clone()), - })) + pub fn npm_pkg_req_resolver(&self) -> Option<&Arc> { + self + .services + .npm_pkg_req_resolver + .get_or_init(|| { + let node_resolver = self.node_resolver()?; + let npm_resolver = self.npm_resolver()?; + Some(Arc::new(CliNpmReqResolver::new(NpmReqResolverOptions { + byonm_resolver: (npm_resolver.clone()).into_maybe_byonm(), + fs: CliDenoResolverFs(self.fs.clone()), + in_npm_pkg_checker: self.in_npm_pkg_checker().clone(), + node_resolver: node_resolver.clone(), + npm_req_resolver: npm_resolver.clone().into_npm_req_resolver(), + }))) + }) + .as_ref() + } } #[derive(Debug, Eq, PartialEq)] @@ -565,6 +764,141 @@ impl std::fmt::Debug for RedirectResolver { } } +#[derive(Debug)] +pub struct LspIsCjsResolver { + inner: IsCjsResolver, +} + +impl Default for LspIsCjsResolver { + fn default() -> Self { + LspIsCjsResolver::new(&Default::default()) + } +} + +impl LspIsCjsResolver { + pub fn new(cache: &LspCache) -> Self { + #[derive(Debug)] + struct LspInNpmPackageChecker { + global_cache_dir: ModuleSpecifier, + } + + impl LspInNpmPackageChecker { + pub fn new(cache: &LspCache) -> Self { + let npm_folder_path = cache.deno_dir().npm_folder_path(); + Self { + global_cache_dir: url_from_directory_path( + &canonicalize_path_maybe_not_exists(&npm_folder_path) + .unwrap_or(npm_folder_path), + ) + .unwrap_or_else(|_| { + ModuleSpecifier::parse("file:///invalid/").unwrap() + }), + } + } + } + + impl InNpmPackageChecker for LspInNpmPackageChecker { + fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { + if specifier.scheme() != "file" { + return false; + } + if specifier + .as_str() + .starts_with(self.global_cache_dir.as_str()) + { + return true; + } + specifier.as_str().contains("/node_modules/") + } + } + + let fs = Arc::new(deno_fs::RealFs); + let pkg_json_resolver = Arc::new(PackageJsonResolver::new( + deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), + )); + + LspIsCjsResolver { + inner: IsCjsResolver::new( + Arc::new(LspInNpmPackageChecker::new(cache)), + pkg_json_resolver, + crate::resolver::IsCjsResolverOptions { + detect_cjs: true, + is_node_main: false, + }, + ), + } + } + + pub fn get_maybe_doc_module_kind( + &self, + specifier: &ModuleSpecifier, + maybe_document: Option<&Document>, + ) -> NodeModuleKind { + self.get_lsp_referrer_kind( + specifier, + maybe_document.and_then(|d| d.is_script()), + ) + } + + pub fn get_doc_module_kind(&self, document: &Document) -> NodeModuleKind { + self.get_lsp_referrer_kind(document.specifier(), document.is_script()) + } + + pub fn get_lsp_referrer_kind( + &self, + specifier: &ModuleSpecifier, + is_script: Option, + ) -> NodeModuleKind { + self.inner.get_lsp_referrer_kind(specifier, is_script) + } +} + +#[derive(Debug)] +pub struct SingleReferrerGraphResolver<'a> { + pub valid_referrer: &'a ModuleSpecifier, + pub referrer_kind: NodeModuleKind, + pub cli_resolver: &'a CliResolver, + pub jsx_import_source_config: Option<&'a JsxImportSourceConfig>, +} + +impl<'a> deno_graph::source::Resolver for SingleReferrerGraphResolver<'a> { + fn default_jsx_import_source(&self) -> Option { + self + .jsx_import_source_config + .and_then(|c| c.default_specifier.clone()) + } + + fn default_jsx_import_source_types(&self) -> Option { + self + .jsx_import_source_config + .and_then(|c| c.default_types_specifier.clone()) + } + + fn jsx_import_source_module(&self) -> &str { + self + .jsx_import_source_config + .map(|c| c.module.as_str()) + .unwrap_or(deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE) + } + + fn resolve( + &self, + specifier_text: &str, + referrer_range: &Range, + mode: ResolutionMode, + ) -> Result { + // this resolver assumes it will only be used with a single referrer + // with the provided referrer kind + debug_assert_eq!(referrer_range.specifier, *self.valid_referrer); + self.cli_resolver.resolve( + specifier_text, + referrer_range, + self.referrer_kind, + mode, + ) + } +} + impl RedirectResolver { fn new( cache: Arc, diff --git a/cli/lsp/testing/collectors.rs b/cli/lsp/testing/collectors.rs index 2f2ddb8773..2dd7ec0d96 100644 --- a/cli/lsp/testing/collectors.rs +++ b/cli/lsp/testing/collectors.rs @@ -650,7 +650,7 @@ pub mod tests { .unwrap(); let text_info = parsed_module.text_info_lazy().clone(); let mut collector = TestCollector::new(specifier, text_info); - parsed_module.module().visit_with(&mut collector); + parsed_module.program().visit_with(&mut collector); collector.take() } diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index fe3708d3cb..48dcb79644 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -34,6 +34,7 @@ use crate::util::path::relative_specifier; use crate::util::path::to_percent_decoded_str; use crate::util::result::InfallibleResultExt; use crate::util::v8::convert; +use crate::worker::create_isolate_create_params; use deno_core::convert::Smi; use deno_core::convert::ToV8; use deno_core::error::StdAnyError; @@ -62,13 +63,14 @@ use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_core::PollEventLoopOptions; use deno_core::RuntimeOptions; -use deno_runtime::fs_util::specifier_to_file_path; +use deno_path_util::url_to_file_path; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::tokio_util::create_basic_runtime; use indexmap::IndexMap; use indexmap::IndexSet; use lazy_regex::lazy_regex; use log::error; +use node_resolver::NodeModuleKind; use once_cell::sync::Lazy; use regex::Captures; use regex::Regex; @@ -236,7 +238,7 @@ pub struct TsServer { performance: Arc, sender: mpsc::UnboundedSender, receiver: Mutex>>, - specifier_map: Arc, + pub specifier_map: Arc, inspector_server: Mutex>>, pending_change: Mutex>, } @@ -882,20 +884,22 @@ impl TsServer { options: GetCompletionsAtPositionOptions, format_code_settings: FormatCodeSettings, scope: Option, - ) -> Option { + ) -> Result, AnyError> { let req = TscRequest::GetCompletionsAtPosition(Box::new(( self.specifier_map.denormalize(&specifier), position, options, format_code_settings, ))); - match self.request(snapshot, req, scope).await { - Ok(maybe_info) => maybe_info, - Err(err) => { - log::error!("Unable to get completion info from TypeScript: {:#}", err); - None - } - } + self + .request::>(snapshot, req, scope) + .await + .map(|mut info| { + if let Some(info) = &mut info { + info.normalize(&self.specifier_map); + } + info + }) } pub async fn get_completion_details( @@ -2182,6 +2186,50 @@ impl NavigateToItem { } } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintDisplayPart { + pub text: String, + pub span: Option, + pub file: Option, +} + +impl InlayHintDisplayPart { + pub fn to_lsp( + &self, + language_server: &language_server::Inner, + ) -> lsp::InlayHintLabelPart { + let location = self.file.as_ref().map(|f| { + let specifier = + resolve_url(f).unwrap_or_else(|_| INVALID_SPECIFIER.clone()); + let file_referrer = + language_server.documents.get_file_referrer(&specifier); + let uri = language_server + .url_map + .specifier_to_uri(&specifier, file_referrer.as_deref()) + .unwrap_or_else(|_| INVALID_URI.clone()); + let range = self + .span + .as_ref() + .and_then(|s| { + let asset_or_doc = + language_server.get_asset_or_document(&specifier).ok()?; + Some(s.to_range(asset_or_doc.line_index())) + }) + .unwrap_or_else(|| { + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)) + }); + lsp::Location { uri, range } + }); + lsp::InlayHintLabelPart { + value: self.text.clone(), + tooltip: None, + location, + command: None, + } + } +} + #[derive(Debug, Clone, Deserialize)] pub enum InlayHintKind { Type, @@ -2203,6 +2251,7 @@ impl InlayHintKind { #[serde(rename_all = "camelCase")] pub struct InlayHint { pub text: String, + pub display_parts: Option>, pub position: u32, pub kind: InlayHintKind, pub whitespace_before: Option, @@ -2210,10 +2259,23 @@ pub struct InlayHint { } impl InlayHint { - pub fn to_lsp(&self, line_index: Arc) -> lsp::InlayHint { + pub fn to_lsp( + &self, + line_index: Arc, + language_server: &language_server::Inner, + ) -> lsp::InlayHint { lsp::InlayHint { position: line_index.position_tsc(self.position.into()), - label: lsp::InlayHintLabel::String(self.text.clone()), + label: if let Some(display_parts) = &self.display_parts { + lsp::InlayHintLabel::LabelParts( + display_parts + .iter() + .map(|p| p.to_lsp(language_server)) + .collect(), + ) + } else { + lsp::InlayHintLabel::String(self.text.clone()) + }, kind: self.kind.to_lsp(), padding_left: self.whitespace_before, padding_right: self.whitespace_after, @@ -3191,7 +3253,7 @@ impl CallHierarchyItem { let use_file_name = self.is_source_file_item(); let maybe_file_path = if uri.scheme().is_some_and(|s| s.as_str() == "file") { - specifier_to_file_path(&uri_to_url(&uri)).ok() + url_to_file_path(&uri_to_url(&uri)).ok() } else { None }; @@ -3584,6 +3646,12 @@ pub struct CompletionInfo { } impl CompletionInfo { + fn normalize(&mut self, specifier_map: &TscSpecifierMap) { + for entry in &mut self.entries { + entry.normalize(specifier_map); + } + } + pub fn as_completion_response( &self, line_index: Arc, @@ -3645,11 +3713,17 @@ pub struct CompletionItemData { #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -struct CompletionEntryDataImport { +struct CompletionEntryDataAutoImport { module_specifier: String, file_name: String, } +#[derive(Debug)] +pub struct CompletionNormalizedAutoImportData { + raw: CompletionEntryDataAutoImport, + normalized: ModuleSpecifier, +} + #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionEntry { @@ -3682,9 +3756,28 @@ pub struct CompletionEntry { is_import_statement_completion: Option, #[serde(skip_serializing_if = "Option::is_none")] data: Option, + /// This is not from tsc, we add it for convenience during normalization. + /// Represents `self.data.file_name`, but normalized. + #[serde(skip)] + auto_import_data: Option, } impl CompletionEntry { + fn normalize(&mut self, specifier_map: &TscSpecifierMap) { + let Some(data) = &self.data else { + return; + }; + let Ok(raw) = + serde_json::from_value::(data.clone()) + else { + return; + }; + if let Ok(normalized) = specifier_map.normalize(&raw.file_name) { + self.auto_import_data = + Some(CompletionNormalizedAutoImportData { raw, normalized }); + } + } + fn get_commit_characters( &self, info: &CompletionInfo, @@ -3833,25 +3926,24 @@ impl CompletionEntry { if let Some(source) = &self.source { let mut display_source = source.clone(); - if let Some(data) = &self.data { - if let Ok(import_data) = - serde_json::from_value::(data.clone()) + if let Some(import_data) = &self.auto_import_data { + if let Some(new_module_specifier) = language_server + .get_ts_response_import_mapper(specifier) + .check_specifier(&import_data.normalized, specifier) + .or_else(|| relative_specifier(specifier, &import_data.normalized)) { - if let Ok(import_specifier) = resolve_url(&import_data.file_name) { - if let Some(new_module_specifier) = language_server - .get_ts_response_import_mapper(specifier) - .check_specifier(&import_specifier, specifier) - .or_else(|| relative_specifier(specifier, &import_specifier)) - { - display_source.clone_from(&new_module_specifier); - if new_module_specifier != import_data.module_specifier { - specifier_rewrite = - Some((import_data.module_specifier, new_module_specifier)); - } - } else if source.starts_with(jsr_url().as_str()) { - return None; - } + if new_module_specifier.contains("/node_modules/") { + return None; } + display_source.clone_from(&new_module_specifier); + if new_module_specifier != import_data.raw.module_specifier { + specifier_rewrite = Some(( + import_data.raw.module_specifier.clone(), + new_module_specifier, + )); + } + } else if source.starts_with(jsr_url().as_str()) { + return None; } } // We want relative or bare (import-mapped or otherwise) specifiers to @@ -3939,7 +4031,7 @@ pub struct OutliningSpan { kind: OutliningSpanKind, } -const FOLD_END_PAIR_CHARACTERS: &[u8] = &[b'}', b']', b')', b'`']; +const FOLD_END_PAIR_CHARACTERS: &[u8] = b"}])`"; impl OutliningSpan { pub fn to_folding_range( @@ -4154,6 +4246,13 @@ impl TscSpecifierMap { return specifier.to_string(); } let mut specifier = original.to_string(); + if specifier.contains("/node_modules/.deno/") + && !specifier.contains("/node_modules/@types/node/") + { + // The ts server doesn't give completions from files in + // `node_modules/.deno/`. We work around it like this. + specifier = specifier.replace("/node_modules/", "/$node_modules/"); + } let media_type = MediaType::from_specifier(original); // If the URL-inferred media type doesn't correspond to tsc's path-inferred // media type, force it to be the same by appending an extension. @@ -4271,7 +4370,7 @@ fn op_is_cancelled(state: &mut OpState) -> bool { fn op_is_node_file(state: &mut OpState, #[string] path: String) -> bool { let state = state.borrow::(); let mark = state.performance.mark("tsc.op.op_is_node_file"); - let r = match ModuleSpecifier::parse(&path) { + let r = match state.specifier_map.normalize(path) { Ok(specifier) => state.state_snapshot.resolver.in_node_modules(&specifier), Err(_) => false, }; @@ -4308,10 +4407,11 @@ fn op_load<'s>( data: doc.text(), script_kind: crate::tsc::as_ts_script_kind(doc.media_type()), version: state.script_version(&specifier), - is_cjs: matches!( - doc.media_type(), - MediaType::Cjs | MediaType::Cts | MediaType::Dcts - ), + is_cjs: doc + .document() + .map(|d| state.state_snapshot.is_cjs_resolver.get_doc_module_kind(d)) + .unwrap_or(NodeModuleKind::Esm) + == NodeModuleKind::Cjs, }) }; @@ -4540,7 +4640,10 @@ fn op_script_names(state: &mut OpState) -> ScriptNames { for doc in &docs { let specifier = doc.specifier(); let is_open = doc.is_open(); - if is_open || specifier.scheme() == "file" { + if is_open + || (specifier.scheme() == "file" + && !state.state_snapshot.resolver.in_node_modules(specifier)) + { let script_names = doc .scope() .and_then(|s| result.by_scope.get_mut(s)) @@ -4551,6 +4654,10 @@ fn op_script_names(state: &mut OpState) -> ScriptNames { let (types, _) = documents.resolve_dependency( types, specifier, + state + .state_snapshot + .is_cjs_resolver + .get_doc_module_kind(doc), doc.file_referrer(), )?; let types_doc = documents.get_or_load(&types, doc.file_referrer())?; @@ -4654,6 +4761,7 @@ fn run_tsc_thread( specifier_map, request_rx, )], + create_params: create_isolate_create_params(), startup_snapshot: Some(tsc::compiler_snapshot()), inspector: has_inspector_server, ..Default::default() @@ -4892,6 +5000,10 @@ pub struct UserPreferences { pub allow_rename_of_import_path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auto_import_file_exclude_patterns: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub interactive_inlay_hints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prefer_type_only_auto_imports: Option, } impl UserPreferences { @@ -4909,6 +5021,7 @@ impl UserPreferences { include_completions_with_snippet_text: Some( config.snippet_support_capable(), ), + interactive_inlay_hints: Some(true), provide_refactor_not_applicable_reason: Some(true), quote_preference: Some(fmt_config.into()), use_label_details_in_completion_entries: Some(true), @@ -5013,6 +5126,9 @@ impl UserPreferences { } else { Some(language_settings.preferences.quote_style) }, + prefer_type_only_auto_imports: Some( + language_settings.preferences.prefer_type_only_auto_imports, + ), ..base_preferences } } @@ -5425,6 +5541,7 @@ mod tests { documents: Arc::new(documents), assets: Default::default(), config: Arc::new(config), + is_cjs_resolver: Default::default(), resolver, }); let performance = Arc::new(Performance::default()); @@ -5958,6 +6075,7 @@ mod tests { Some(temp_dir.url()), ) .await + .unwrap() .unwrap(); assert_eq!(info.entries.len(), 22); let details = ts_server @@ -6117,6 +6235,7 @@ mod tests { Some(temp_dir.url()), ) .await + .unwrap() .unwrap(); let entry = info .entries @@ -6154,7 +6273,7 @@ mod tests { let change = changes.text_changes.first().unwrap(); assert_eq!( change.new_text, - "import type { someLongVariable } from './b.ts'\n" + "import { someLongVariable } from './b.ts'\n" ); } diff --git a/cli/main.rs b/cli/main.rs index c0eccab5df..7d3ef0e6a0 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -15,7 +15,6 @@ mod js; mod jsr; mod lsp; mod module_loader; -mod napi; mod node; mod npm; mod ops; @@ -37,6 +36,8 @@ use crate::util::v8::get_v8_flags_from_env; use crate::util::v8::init_v8_flags; use args::TaskFlags; +use deno_resolver::npm::ByonmResolvePkgFolderFromDenoReqError; +use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; use deno_runtime::WorkerExecutionMode; pub use deno_runtime::UNSTABLE_GRANULAR_FLAGS; @@ -46,8 +47,7 @@ use deno_core::error::JsError; use deno_core::futures::FutureExt; use deno_core::unsync::JoinHandle; use deno_npm::resolution::SnapshotFromLockfileError; -use deno_runtime::fmt_errors::format_js_error_with_suggestions; -use deno_runtime::fmt_errors::FixSuggestion; +use deno_runtime::fmt_errors::format_js_error; use deno_runtime::tokio_util::create_and_run_current_thread_with_maybe_metrics; use deno_terminal::colors; use factory::CliFactory; @@ -55,10 +55,15 @@ use standalone::MODULE_NOT_FOUND; use standalone::UNSUPPORTED_SCHEME; use std::env; use std::future::Future; +use std::io::IsTerminal; use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + /// Ensures that all subcommands return an i32 exit code and an [`AnyError`] error type. trait SubcommandOutput { fn output(self) -> Result; @@ -130,7 +135,7 @@ async fn run_subcommand(flags: Arc) -> Result { tools::compile::compile(flags, compile_flags).await }), DenoSubcommand::Coverage(coverage_flags) => spawn_subcommand(async { - tools::coverage::cover_files(flags, coverage_flags).await + tools::coverage::cover_files(flags, coverage_flags) }), DenoSubcommand::Fmt(fmt_flags) => { spawn_subcommand( @@ -159,7 +164,19 @@ async fn run_subcommand(flags: Arc) -> Result { DenoSubcommand::Uninstall(uninstall_flags) => spawn_subcommand(async { tools::installer::uninstall(flags, uninstall_flags).await }), - DenoSubcommand::Lsp => spawn_subcommand(async { lsp::start().await }), + DenoSubcommand::Lsp => spawn_subcommand(async { + if std::io::stderr().is_terminal() { + log::warn!( + "{} command is intended to be run by text editors and IDEs and shouldn't be run manually. + + Visit https://docs.deno.com/runtime/getting_started/setup_your_environment/ for instruction + how to setup your favorite text editor. + + Press Ctrl+C to exit. + ", colors::cyan("deno lsp")); + } + lsp::start().await + }), DenoSubcommand::Lint(lint_flags) => spawn_subcommand(async { if lint_flags.rules { tools::lint::print_rules_list( @@ -182,6 +199,21 @@ async fn run_subcommand(flags: Arc) -> Result { match result { Ok(v) => Ok(v), Err(script_err) => { + if let Some(ResolvePkgFolderFromDenoReqError::Byonm(ByonmResolvePkgFolderFromDenoReqError::UnmatchedReq(_))) = script_err.downcast_ref::() { + if flags.node_modules_dir.is_none() { + let mut flags = flags.deref().clone(); + let watch = match &flags.subcommand { + DenoSubcommand::Run(run_flags) => run_flags.watch.clone(), + _ => unreachable!(), + }; + flags.node_modules_dir = Some(deno_config::deno_json::NodeModulesDirMode::None); + // use the current lockfile, but don't write it out + if flags.frozen_lockfile.is_none() { + flags.internal.lockfile_skip_write = true; + } + return tools::run::run_script(WorkerExecutionMode::Run, Arc::new(flags), watch).await; + } + } let script_err_msg = script_err.to_string(); if script_err_msg.starts_with(MODULE_NOT_FOUND) || script_err_msg.starts_with(UNSUPPORTED_SCHEME) { if run_flags.bare { @@ -318,84 +350,17 @@ fn setup_panic_hook() { eprintln!("Args: {:?}", env::args().collect::>()); eprintln!(); orig_hook(panic_info); - std::process::exit(1); + deno_runtime::exit(1); })); } -#[allow(clippy::print_stderr)] fn exit_with_message(message: &str, code: i32) -> ! { - eprintln!( + log::error!( "{}: {}", colors::red_bold("error"), message.trim_start_matches("error: ") ); - std::process::exit(code); -} - -fn get_suggestions_for_terminal_errors(e: &JsError) -> Vec { - if let Some(msg) = &e.message { - if msg.contains("module is not defined") - || msg.contains("exports is not defined") - { - return vec![ - FixSuggestion::info( - "Deno does not support CommonJS modules without `.cjs` extension.", - ), - FixSuggestion::hint( - "Rewrite this module to ESM or change the file extension to `.cjs`.", - ), - ]; - } else if msg.contains("openKv is not a function") { - return vec![ - FixSuggestion::info("Deno.openKv() is an unstable API."), - FixSuggestion::hint( - "Run again with `--unstable-kv` flag to enable this API.", - ), - ]; - } else if msg.contains("cron is not a function") { - return vec![ - FixSuggestion::info("Deno.cron() is an unstable API."), - FixSuggestion::hint( - "Run again with `--unstable-cron` flag to enable this API.", - ), - ]; - } else if msg.contains("WebSocketStream is not defined") { - return vec![ - FixSuggestion::info("new WebSocketStream() is an unstable API."), - FixSuggestion::hint( - "Run again with `--unstable-net` flag to enable this API.", - ), - ]; - } else if msg.contains("Temporal is not defined") { - return vec![ - FixSuggestion::info("Temporal is an unstable API."), - FixSuggestion::hint( - "Run again with `--unstable-temporal` flag to enable this API.", - ), - ]; - } else if msg.contains("BroadcastChannel is not defined") { - return vec![ - FixSuggestion::info("BroadcastChannel is an unstable API."), - FixSuggestion::hint( - "Run again with `--unstable-broadcast-channel` flag to enable this API.", - ), - ]; - } else if msg.contains("window is not defined") { - return vec![ - FixSuggestion::info("window global is not available in Deno 2."), - FixSuggestion::hint("Replace `window` with `globalThis`."), - ]; - } else if msg.contains("UnsafeWindowSurface is not a constructor") { - return vec![ - FixSuggestion::info("Deno.UnsafeWindowSurface is an unstable API."), - FixSuggestion::hint( - "Run again with `--unstable-webgpu` flag to enable this API.", - ), - ]; - } - } - - vec![] + deno_runtime::exit(code); } fn exit_for_error(error: AnyError) -> ! { @@ -403,8 +368,7 @@ fn exit_for_error(error: AnyError) -> ! { let mut error_code = 1; if let Some(e) = error.downcast_ref::() { - let suggestions = get_suggestions_for_terminal_errors(e); - error_string = format_js_error_with_suggestions(e, suggestions); + error_string = format_js_error(e); } else if let Some(SnapshotFromLockfileError::IntegrityCheckFailed(e)) = error.downcast_ref::() { @@ -415,16 +379,18 @@ fn exit_for_error(error: AnyError) -> ! { exit_with_message(&error_string, error_code); } -#[allow(clippy::print_stderr)] pub(crate) fn unstable_exit_cb(feature: &str, api_name: &str) { - eprintln!( + log::error!( "Unstable API '{api_name}'. The `--unstable-{}` flag must be provided.", feature ); - std::process::exit(70); + deno_runtime::exit(70); } pub fn main() { + #[cfg(feature = "dhat-heap")] + let profiler = dhat::Profiler::new_heap(); + setup_panic_hook(); util::unix::raise_fd_limit(); @@ -445,8 +411,13 @@ pub fn main() { run_subcommand(Arc::new(flags)).await }; - match create_and_run_current_thread_with_maybe_metrics(future) { - Ok(exit_code) => std::process::exit(exit_code), + let result = create_and_run_current_thread_with_maybe_metrics(future); + + #[cfg(feature = "dhat-heap")] + drop(profiler); + + match result { + Ok(exit_code) => deno_runtime::exit(exit_code), Err(err) => exit_for_error(err), } } @@ -460,12 +431,21 @@ fn resolve_flags_and_init( if err.kind() == clap::error::ErrorKind::DisplayVersion => { // Ignore results to avoid BrokenPipe errors. + util::logger::init(None); let _ = err.print(); - std::process::exit(0); + deno_runtime::exit(0); + } + Err(err) => { + util::logger::init(None); + exit_for_error(AnyError::from(err)) } - Err(err) => exit_for_error(AnyError::from(err)), }; + if let Some(otel_config) = flags.otel_config() { + deno_runtime::ops::otel::init(otel_config)?; + } + util::logger::init(flags.log_level); + // TODO(bartlomieju): remove in Deno v2.5 and hard error then. if flags.unstable_config.legacy_flag_enabled { log::warn!( @@ -494,7 +474,6 @@ fn resolve_flags_and_init( deno_core::JsRuntime::init_platform( None, /* import assertions enabled */ false, ); - util::logger::init(flags.log_level); Ok(flags) } diff --git a/cli/mainrt.rs b/cli/mainrt.rs index 02d58fcee1..2951aa711a 100644 --- a/cli/mainrt.rs +++ b/cli/mainrt.rs @@ -40,23 +40,21 @@ use std::env::current_exe; use crate::args::Flags; -#[allow(clippy::print_stderr)] pub(crate) fn unstable_exit_cb(feature: &str, api_name: &str) { - eprintln!( + log::error!( "Unstable API '{api_name}'. The `--unstable-{}` flag must be provided.", feature ); - std::process::exit(70); + deno_runtime::exit(70); } -#[allow(clippy::print_stderr)] fn exit_with_message(message: &str, code: i32) -> ! { - eprintln!( + log::error!( "{}: {}", colors::red_bold("error"), message.trim_start_matches("error: ") ); - std::process::exit(code); + deno_runtime::exit(code); } fn unwrap_or_exit(result: Result) -> T { @@ -88,15 +86,20 @@ fn main() { let standalone = standalone::extract_standalone(Cow::Owned(args)); let future = async move { match standalone { - Ok(Some(future)) => { - let (metadata, eszip) = future.await?; - util::logger::init(metadata.log_level); - load_env_vars(&metadata.env_vars_from_env_file); - let exit_code = standalone::run(eszip, metadata).await?; - std::process::exit(exit_code); + Ok(Some(data)) => { + if let Some(otel_config) = data.metadata.otel_config.clone() { + deno_runtime::ops::otel::init(otel_config)?; + } + util::logger::init(data.metadata.log_level); + load_env_vars(&data.metadata.env_vars_from_env_file); + let exit_code = standalone::run(data).await?; + deno_runtime::exit(exit_code); } Ok(None) => Ok(()), - Err(err) => Err(err), + Err(err) => { + util::logger::init(None); + Err(err) + } } }; diff --git a/cli/module_loader.rs b/cli/module_loader.rs index f4e219bea1..b9adfe642c 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::cell::RefCell; +use std::path::Path; use std::path::PathBuf; use std::pin::Pin; use std::rc::Rc; @@ -23,18 +24,23 @@ use crate::graph_container::ModuleGraphUpdatePermit; use crate::graph_util::CreateGraphOptions; use crate::graph_util::ModuleGraphBuilder; use crate::node; -use crate::resolver::CliGraphResolver; -use crate::resolver::CliNodeResolver; +use crate::node::CliNodeCodeTranslator; +use crate::npm::CliNpmResolver; +use crate::resolver::CjsTracker; +use crate::resolver::CliNpmReqResolver; +use crate::resolver::CliResolver; use crate::resolver::ModuleCodeStringSource; +use crate::resolver::NotSupportedKindInNpmError; use crate::resolver::NpmModuleLoader; use crate::tools::check; use crate::tools::check::TypeChecker; use crate::util::progress_bar::ProgressBar; use crate::util::text_encoding::code_without_source_map; use crate::util::text_encoding::source_map_from_code; -use crate::worker::ModuleLoaderAndSourceMapGetter; +use crate::worker::CreateModuleLoaderResult; use crate::worker::ModuleLoaderFactory; use deno_ast::MediaType; +use deno_ast::ModuleKind; use deno_core::anyhow::anyhow; use deno_core::anyhow::bail; use deno_core::anyhow::Context; @@ -54,7 +60,6 @@ use deno_core::RequestedModuleType; use deno_core::ResolutionKind; use deno_core::SourceCodeCacheInfo; use deno_graph::source::ResolutionMode; -use deno_graph::source::Resolver; use deno_graph::GraphKind; use deno_graph::JsModule; use deno_graph::JsonModule; @@ -62,9 +67,14 @@ use deno_graph::Module; use deno_graph::ModuleGraph; use deno_graph::Resolution; use deno_runtime::code_cache; +use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::create_host_defined_options; +use deno_runtime::deno_node::NodeRequireLoader; +use deno_runtime::deno_node::NodeResolver; use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::npm::NpmPackageReqReference; +use node_resolver::errors::ClosestPkgJsonError; +use node_resolver::InNpmPackageChecker; use node_resolver::NodeResolutionMode; pub struct ModuleLoadPreparer { @@ -198,14 +208,20 @@ struct SharedCliModuleLoaderState { initial_cwd: PathBuf, is_inspecting: bool, is_repl: bool, + cjs_tracker: Arc, code_cache: Option>, emitter: Arc, + fs: Arc, + in_npm_pkg_checker: Arc, main_module_graph_container: Arc, module_load_preparer: Arc, - node_resolver: Arc, + node_code_translator: Arc, + node_resolver: Arc, + npm_req_resolver: Arc, + npm_resolver: Arc, npm_module_loader: NpmModuleLoader, parsed_source_cache: Arc, - resolver: Arc, + resolver: Arc, } pub struct CliModuleLoaderFactory { @@ -216,14 +232,20 @@ impl CliModuleLoaderFactory { #[allow(clippy::too_many_arguments)] pub fn new( options: &CliOptions, + cjs_tracker: Arc, code_cache: Option>, emitter: Arc, + fs: Arc, + in_npm_pkg_checker: Arc, main_module_graph_container: Arc, module_load_preparer: Arc, - node_resolver: Arc, + node_code_translator: Arc, + node_resolver: Arc, + npm_req_resolver: Arc, + npm_resolver: Arc, npm_module_loader: NpmModuleLoader, parsed_source_cache: Arc, - resolver: Arc, + resolver: Arc, ) -> Self { Self { shared: Arc::new(SharedCliModuleLoaderState { @@ -236,11 +258,17 @@ impl CliModuleLoaderFactory { options.sub_command(), DenoSubcommand::Repl(_) | DenoSubcommand::Jupyter(_) ), + cjs_tracker, code_cache, emitter, + fs, + in_npm_pkg_checker, main_module_graph_container, module_load_preparer, + node_code_translator, node_resolver, + npm_req_resolver, + npm_resolver, npm_module_loader, parsed_source_cache, resolver, @@ -255,19 +283,30 @@ impl CliModuleLoaderFactory { is_worker: bool, parent_permissions: PermissionsContainer, permissions: PermissionsContainer, - ) -> ModuleLoaderAndSourceMapGetter { - let loader = Rc::new(CliModuleLoader(Rc::new(CliModuleLoaderInner { - lib, - is_worker, - parent_permissions, - permissions, - graph_container, + ) -> CreateModuleLoaderResult { + let module_loader = + Rc::new(CliModuleLoader(Rc::new(CliModuleLoaderInner { + lib, + is_worker, + parent_permissions, + permissions, + graph_container: graph_container.clone(), + node_code_translator: self.shared.node_code_translator.clone(), + emitter: self.shared.emitter.clone(), + parsed_source_cache: self.shared.parsed_source_cache.clone(), + shared: self.shared.clone(), + }))); + let node_require_loader = Rc::new(CliNodeRequireLoader { + cjs_tracker: self.shared.cjs_tracker.clone(), emitter: self.shared.emitter.clone(), - parsed_source_cache: self.shared.parsed_source_cache.clone(), - shared: self.shared.clone(), - }))); - ModuleLoaderAndSourceMapGetter { - module_loader: loader, + fs: self.shared.fs.clone(), + graph_container, + in_npm_pkg_checker: self.shared.in_npm_pkg_checker.clone(), + npm_resolver: self.shared.npm_resolver.clone(), + }); + CreateModuleLoaderResult { + module_loader, + node_require_loader, } } } @@ -276,7 +315,7 @@ impl ModuleLoaderFactory for CliModuleLoaderFactory { fn create_for_main( &self, root_permissions: PermissionsContainer, - ) -> ModuleLoaderAndSourceMapGetter { + ) -> CreateModuleLoaderResult { self.create_with_lib( (*self.shared.main_module_graph_container).clone(), self.shared.lib_window, @@ -290,7 +329,7 @@ impl ModuleLoaderFactory for CliModuleLoaderFactory { &self, parent_permissions: PermissionsContainer, permissions: PermissionsContainer, - ) -> ModuleLoaderAndSourceMapGetter { + ) -> CreateModuleLoaderResult { self.create_with_lib( // create a fresh module graph for the worker WorkerModuleGraphContainer::new(Arc::new(ModuleGraph::new( @@ -314,6 +353,7 @@ struct CliModuleLoaderInner { permissions: PermissionsContainer, shared: Arc, emitter: Arc, + node_code_translator: Arc, parsed_source_cache: Arc, graph_container: TGraphContainer, } @@ -327,16 +367,7 @@ impl maybe_referrer: Option<&ModuleSpecifier>, requested_module_type: RequestedModuleType, ) -> Result { - let code_source = if let Some(result) = self - .shared - .npm_module_loader - .load_if_in_npm_package(specifier, maybe_referrer) - .await - { - result? - } else { - self.load_prepared_module(specifier, maybe_referrer).await? - }; + let code_source = self.load_code_source(specifier, maybe_referrer).await?; let code = if self.shared.is_inspecting { // we need the code with the source map in order for // it to work with --inspect or --inspect-brk @@ -390,6 +421,29 @@ impl )) } + async fn load_code_source( + &self, + specifier: &ModuleSpecifier, + maybe_referrer: Option<&ModuleSpecifier>, + ) -> Result { + if let Some(code_source) = self.load_prepared_module(specifier).await? { + return Ok(code_source); + } + if self.shared.in_npm_pkg_checker.in_npm_package(specifier) { + return self + .shared + .npm_module_loader + .load(specifier, maybe_referrer) + .await; + } + + let mut msg = format!("Loading unprepared module: {specifier}"); + if let Some(referrer) = maybe_referrer { + msg = format!("{}, imported from: {}", msg, referrer.as_str()); + } + Err(anyhow!(msg)) + } + fn resolve_referrer( &self, referrer: &str, @@ -397,7 +451,7 @@ impl let referrer = if referrer.is_empty() && self.shared.is_repl { // FIXME(bartlomieju): this is a hacky way to provide compatibility with REPL // and `Deno.core.evalContext` API. Ideally we should always have a referrer filled - "./$deno$repl.ts" + "./$deno$repl.mts" } else { referrer }; @@ -420,12 +474,17 @@ impl raw_specifier: &str, referrer: &ModuleSpecifier, ) -> Result { - if self.shared.node_resolver.in_npm_package(referrer) { + if self.shared.in_npm_pkg_checker.in_npm_package(referrer) { return Ok( self .shared .node_resolver - .resolve(raw_specifier, referrer, NodeResolutionMode::Execution)? + .resolve( + raw_specifier, + referrer, + self.shared.cjs_tracker.get_referrer_kind(referrer), + NodeResolutionMode::Execution, + )? .into_url(), ); } @@ -455,6 +514,7 @@ impl start: deno_graph::Position::zeroed(), end: deno_graph::Position::zeroed(), }, + self.shared.cjs_tracker.get_referrer_kind(referrer), ResolutionMode::Execution, )?), }; @@ -464,13 +524,14 @@ impl { return self .shared - .node_resolver + .npm_req_resolver .resolve_req_reference( &reference, referrer, + self.shared.cjs_tracker.get_referrer_kind(referrer), NodeResolutionMode::Execution, ) - .map(|res| res.into_url()); + .map_err(AnyError::from); } } @@ -478,7 +539,6 @@ impl Some(Module::Npm(module)) => { let package_folder = self .shared - .node_resolver .npm_resolver .as_managed() .unwrap() // byonm won't create a Module::Npm @@ -486,22 +546,25 @@ impl self .shared .node_resolver - .resolve_package_sub_path_from_deno_module( + .resolve_package_subpath_from_deno_module( &package_folder, module.nv_reference.sub_path(), Some(referrer), + self.shared.cjs_tracker.get_referrer_kind(referrer), NodeResolutionMode::Execution, ) .with_context(|| { format!("Could not resolve '{}'.", module.nv_reference) })? - .into_url() } Some(Module::Node(module)) => module.specifier.clone(), Some(Module::Js(module)) => module.specifier.clone(), Some(Module::Json(module)) => module.specifier.clone(), Some(Module::External(module)) => { - node::resolve_specifier_into_node_modules(&module.specifier) + node::resolve_specifier_into_node_modules( + &module.specifier, + self.shared.fs.as_ref(), + ) } None => specifier.into_owned(), }; @@ -511,71 +574,82 @@ impl async fn load_prepared_module( &self, specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - ) -> Result { + ) -> Result, AnyError> { // Note: keep this in sync with the sync version below let graph = self.graph_container.graph(); - match self.load_prepared_module_or_defer_emit( - &graph, - specifier, - maybe_referrer, - ) { - Ok(CodeOrDeferredEmit::Code(code_source)) => Ok(code_source), - Ok(CodeOrDeferredEmit::DeferredEmit { + match self.load_prepared_module_or_defer_emit(&graph, specifier)? { + Some(CodeOrDeferredEmit::Code(code_source)) => Ok(Some(code_source)), + Some(CodeOrDeferredEmit::DeferredEmit { specifier, media_type, source, }) => { let transpile_result = self .emitter - .emit_parsed_source(specifier, media_type, source) + .emit_parsed_source(specifier, media_type, ModuleKind::Esm, source) .await?; // at this point, we no longer need the parsed source in memory, so free it self.parsed_source_cache.free(specifier); - Ok(ModuleCodeStringSource { - code: ModuleSourceCode::Bytes(transpile_result), + Ok(Some(ModuleCodeStringSource { + // note: it's faster to provide a string if we know it's a string + code: ModuleSourceCode::String(transpile_result.into()), found_url: specifier.clone(), media_type, - }) + })) } - Err(err) => Err(err), + Some(CodeOrDeferredEmit::Cjs { + specifier, + media_type, + source, + }) => self + .load_maybe_cjs(specifier, media_type, source) + .await + .map(Some), + None => Ok(None), } } - fn load_prepared_module_sync( + fn load_prepared_module_for_source_map_sync( &self, specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - ) -> Result { + ) -> Result, AnyError> { // Note: keep this in sync with the async version above let graph = self.graph_container.graph(); - match self.load_prepared_module_or_defer_emit( - &graph, - specifier, - maybe_referrer, - ) { - Ok(CodeOrDeferredEmit::Code(code_source)) => Ok(code_source), - Ok(CodeOrDeferredEmit::DeferredEmit { + match self.load_prepared_module_or_defer_emit(&graph, specifier)? { + Some(CodeOrDeferredEmit::Code(code_source)) => Ok(Some(code_source)), + Some(CodeOrDeferredEmit::DeferredEmit { specifier, media_type, source, }) => { - let transpile_result = self - .emitter - .emit_parsed_source_sync(specifier, media_type, source)?; + let transpile_result = self.emitter.emit_parsed_source_sync( + specifier, + media_type, + ModuleKind::Esm, + source, + )?; // at this point, we no longer need the parsed source in memory, so free it self.parsed_source_cache.free(specifier); - Ok(ModuleCodeStringSource { - code: ModuleSourceCode::Bytes(transpile_result), + Ok(Some(ModuleCodeStringSource { + // note: it's faster to provide a string if we know it's a string + code: ModuleSourceCode::String(transpile_result.into()), found_url: specifier.clone(), media_type, - }) + })) } - Err(err) => Err(err), + Some(CodeOrDeferredEmit::Cjs { .. }) => { + self.parsed_source_cache.free(specifier); + + // todo(dsherret): to make this work, we should probably just + // rely on the CJS export cache. At the moment this is hard because + // cjs export analysis is only async + Ok(None) + } + None => Ok(None), } } @@ -583,8 +657,7 @@ impl &self, graph: &'graph ModuleGraph, specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - ) -> Result, AnyError> { + ) -> Result>, AnyError> { if specifier.scheme() == "node" { // Node built-in modules should be handled internally. unreachable!("Deno bug. {} was misconfigured internally.", specifier); @@ -596,38 +669,55 @@ impl media_type, specifier, .. - })) => Ok(CodeOrDeferredEmit::Code(ModuleCodeStringSource { + })) => Ok(Some(CodeOrDeferredEmit::Code(ModuleCodeStringSource { code: ModuleSourceCode::String(source.clone().into()), found_url: specifier.clone(), media_type: *media_type, - })), + }))), Some(deno_graph::Module::Js(JsModule { source, media_type, specifier, + is_script, .. })) => { + if self.shared.cjs_tracker.is_cjs_with_known_is_script( + specifier, + *media_type, + *is_script, + )? { + return Ok(Some(CodeOrDeferredEmit::Cjs { + specifier, + media_type: *media_type, + source, + })); + } let code: ModuleCodeString = match media_type { MediaType::JavaScript | MediaType::Unknown - | MediaType::Cjs | MediaType::Mjs | MediaType::Json => source.clone().into(), MediaType::Dts | MediaType::Dcts | MediaType::Dmts => { Default::default() } - MediaType::TypeScript - | MediaType::Mts - | MediaType::Cts - | MediaType::Jsx - | MediaType::Tsx => { - return Ok(CodeOrDeferredEmit::DeferredEmit { + MediaType::Cjs | MediaType::Cts => { + return Ok(Some(CodeOrDeferredEmit::Cjs { specifier, media_type: *media_type, source, - }); + })); } - MediaType::TsBuildInfo | MediaType::Wasm | MediaType::SourceMap => { + MediaType::TypeScript + | MediaType::Mts + | MediaType::Jsx + | MediaType::Tsx => { + return Ok(Some(CodeOrDeferredEmit::DeferredEmit { + specifier, + media_type: *media_type, + source, + })); + } + MediaType::Css | MediaType::Wasm | MediaType::SourceMap => { panic!("Unexpected media type {media_type} for {specifier}") } }; @@ -635,26 +725,62 @@ impl // at this point, we no longer need the parsed source in memory, so free it self.parsed_source_cache.free(specifier); - Ok(CodeOrDeferredEmit::Code(ModuleCodeStringSource { + Ok(Some(CodeOrDeferredEmit::Code(ModuleCodeStringSource { code: ModuleSourceCode::String(code), found_url: specifier.clone(), media_type: *media_type, - })) + }))) } Some( deno_graph::Module::External(_) | deno_graph::Module::Node(_) | deno_graph::Module::Npm(_), ) - | None => { - let mut msg = format!("Loading unprepared module: {specifier}"); - if let Some(referrer) = maybe_referrer { - msg = format!("{}, imported from: {}", msg, referrer.as_str()); - } - Err(anyhow!(msg)) - } + | None => Ok(None), } } + + async fn load_maybe_cjs( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + original_source: &Arc, + ) -> Result { + let js_source = if media_type.is_emittable() { + Cow::Owned( + self + .emitter + .emit_parsed_source( + specifier, + media_type, + ModuleKind::Cjs, + original_source, + ) + .await?, + ) + } else { + Cow::Borrowed(original_source.as_ref()) + }; + let text = self + .node_code_translator + .translate_cjs_to_esm(specifier, Some(js_source)) + .await?; + // at this point, we no longer need the parsed source in memory, so free it + self.parsed_source_cache.free(specifier); + Ok(ModuleCodeStringSource { + code: match text { + // perf: if the text is borrowed, that means it didn't make any changes + // to the original source, so we can just provide that instead of cloning + // the borrowed text + Cow::Borrowed(_) => { + ModuleSourceCode::String(original_source.clone().into()) + } + Cow::Owned(text) => ModuleSourceCode::String(text.into()), + }, + found_url: specifier.clone(), + media_type, + }) + } } enum CodeOrDeferredEmit<'a> { @@ -664,6 +790,11 @@ enum CodeOrDeferredEmit<'a> { media_type: MediaType, source: &'a Arc, }, + Cjs { + specifier: &'a ModuleSpecifier, + media_type: MediaType, + source: &'a Arc, + }, } // todo(dsherret): this double Rc boxing is not ideal @@ -705,7 +836,7 @@ impl ModuleLoader name: &str, ) -> Option> { let name = deno_core::ModuleSpecifier::parse(name).ok()?; - if self.0.shared.node_resolver.in_npm_package(&name) { + if self.0.shared.in_npm_pkg_checker.in_npm_package(&name) { Some(create_host_defined_options(scope)) } else { None @@ -742,7 +873,7 @@ impl ModuleLoader _maybe_referrer: Option, is_dynamic: bool, ) -> Pin>>> { - if self.0.shared.node_resolver.in_npm_package(specifier) { + if self.0.shared.in_npm_pkg_checker.in_npm_package(specifier) { return Box::pin(deno_core::futures::future::ready(Ok(()))); } @@ -825,7 +956,10 @@ impl ModuleLoader "wasm" | "file" | "http" | "https" | "data" | "blob" => (), _ => return None, } - let source = self.0.load_prepared_module_sync(&specifier, None).ok()?; + let source = self + .0 + .load_prepared_module_for_source_map_sync(&specifier) + .ok()??; source_map_from_code(source.code.as_bytes()) } @@ -904,3 +1038,68 @@ impl ModuleGraphUpdatePermit for WorkerModuleGraphUpdatePermit { drop(self.permit); // explicit drop for clarity } } + +#[derive(Debug)] +struct CliNodeRequireLoader { + cjs_tracker: Arc, + emitter: Arc, + fs: Arc, + graph_container: TGraphContainer, + in_npm_pkg_checker: Arc, + npm_resolver: Arc, +} + +impl NodeRequireLoader + for CliNodeRequireLoader +{ + fn ensure_read_permission<'a>( + &self, + permissions: &mut dyn deno_runtime::deno_node::NodePermissions, + path: &'a Path, + ) -> Result, AnyError> { + if let Ok(url) = deno_path_util::url_from_file_path(path) { + // allow reading if it's in the module graph + if self.graph_container.graph().get(&url).is_some() { + return Ok(std::borrow::Cow::Borrowed(path)); + } + } + self.npm_resolver.ensure_read_permission(permissions, path) + } + + fn load_text_file_lossy(&self, path: &Path) -> Result { + // todo(dsherret): use the preloaded module from the graph if available? + let media_type = MediaType::from_path(path); + let text = self.fs.read_text_file_lossy_sync(path, None)?; + if media_type.is_emittable() { + let specifier = deno_path_util::url_from_file_path(path)?; + if self.in_npm_pkg_checker.in_npm_package(&specifier) { + return Err( + NotSupportedKindInNpmError { + media_type, + specifier, + } + .into(), + ); + } + self.emitter.emit_parsed_source_sync( + &specifier, + media_type, + // this is probably not super accurate due to require esm, but probably ok. + // If we find this causes a lot of churn in the emit cache then we should + // investigate how we can make this better + ModuleKind::Cjs, + &text.into(), + ) + } else { + Ok(text) + } + } + + fn is_maybe_cjs( + &self, + specifier: &ModuleSpecifier, + ) -> Result { + let media_type = MediaType::from_specifier(specifier); + self.cjs_tracker.is_maybe_cjs(specifier, media_type) + } +} diff --git a/cli/napi/README.md b/cli/napi/README.md deleted file mode 100644 index 7b359ac6ec..0000000000 --- a/cli/napi/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# napi - -This directory contains source for Deno's Node-API implementation. It depends on -`napi_sym` and `deno_napi`. - -Files are generally organized the same as in Node.js's implementation to ease in -ensuring compatibility. - -## Adding a new function - -Add the symbol name to -[`cli/napi_sym/symbol_exports.json`](../napi_sym/symbol_exports.json). - -```diff -{ - "symbols": [ - ... - "napi_get_undefined", -- "napi_get_null" -+ "napi_get_null", -+ "napi_get_boolean" - ] -} -``` - -Determine where to place the implementation. `napi_get_boolean` is related to JS -values so we will place it in `js_native_api.rs`. If something is not clear, -just create a new file module. - -See [`napi_sym`](../napi_sym/) for writing the implementation: - -```rust -#[napi_sym::napi_sym] -pub fn napi_get_boolean( - env: *mut Env, - value: bool, - result: *mut napi_value, -) -> Result { - // ... - Ok(()) -} -``` - -Update the generated symbol lists using the script: - -``` -deno run --allow-write tools/napi/generate_symbols_lists.js -``` - -Add a test in [`/tests/napi`](../../tests/napi/). You can also refer to Node.js -test suite for Node-API. - -```js -// tests/napi/boolean_test.js -import { assertEquals, loadTestLibrary } from "./common.js"; -const lib = loadTestLibrary(); -Deno.test("napi get boolean", function () { - assertEquals(lib.test_get_boolean(true), true); - assertEquals(lib.test_get_boolean(false), false); -}); -``` - -```rust -// tests/napi/src/boolean.rs - -use napi_sys::Status::napi_ok; -use napi_sys::ValueType::napi_boolean; -use napi_sys::*; - -extern "C" fn test_boolean( - env: napi_env, - info: napi_callback_info, -) -> napi_value { - let (args, argc, _) = crate::get_callback_info!(env, info, 1); - assert_eq!(argc, 1); - - let mut ty = -1; - assert!(unsafe { napi_typeof(env, args[0], &mut ty) } == napi_ok); - assert_eq!(ty, napi_boolean); - - // Use napi_get_boolean here... - - value -} - -pub fn init(env: napi_env, exports: napi_value) { - let properties = &[crate::new_property!(env, "test_boolean\0", test_boolean)]; - - unsafe { - napi_define_properties(env, exports, properties.len(), properties.as_ptr()) - }; -} -``` - -```diff -// tests/napi/src/lib.rs - -+ mod boolean; - -... - -#[no_mangle] -unsafe extern "C" fn napi_register_module_v1( - env: napi_env, - exports: napi_value, -) -> napi_value { - ... -+ boolean::init(env, exports); - - exports -} -``` - -Run the test using `cargo test -p tests/napi`. diff --git a/cli/napi/mod.rs b/cli/napi/mod.rs deleted file mode 100644 index 122d2ff060..0000000000 --- a/cli/napi/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -#![allow(unused_mut)] -#![allow(non_camel_case_types)] -#![allow(clippy::undocumented_unsafe_blocks)] - -//! Symbols to be exported are now defined in this JSON file. -//! The `#[napi_sym]` macro checks for missing entries and panics. -//! -//! `./tools/napi/generate_symbols_list.js` is used to generate the LINK `cli/exports.def` on Windows, -//! which is also checked into git. -//! -//! To add a new napi function: -//! 1. Place `#[napi_sym]` on top of your implementation. -//! 2. Add the function's identifier to this JSON list. -//! 3. Finally, run `tools/napi/generate_symbols_list.js` to update `cli/napi/generated_symbol_exports_list_*.def`. - -pub mod js_native_api; -pub mod node_api; -pub mod util; diff --git a/cli/node.rs b/cli/node.rs index a3cee8ddee..bc39cdbde9 100644 --- a/cli/node.rs +++ b/cli/node.rs @@ -1,10 +1,12 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::borrow::Cow; use std::sync::Arc; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; +use deno_graph::ParsedSourceStore; use deno_runtime::deno_fs; use deno_runtime::deno_node::DenoFsNodeResolverEnv; use node_resolver::analyze::CjsAnalysis as ExtNodeCjsAnalysis; @@ -16,8 +18,8 @@ use serde::Serialize; use crate::cache::CacheDBHash; use crate::cache::NodeAnalysisCache; -use crate::resolver::CliNodeResolver; -use crate::util::fs::canonicalize_path_maybe_not_exists; +use crate::cache::ParsedSourceCache; +use crate::resolver::CjsTracker; pub type CliNodeCodeTranslator = NodeCodeTranslator; @@ -30,15 +32,11 @@ pub type CliNodeCodeTranslator = /// because the node_modules folder might not exist at that time. pub fn resolve_specifier_into_node_modules( specifier: &ModuleSpecifier, + fs: &dyn deno_fs::FileSystem, ) -> 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()) + node_resolver::resolve_specifier_into_node_modules(specifier, &|path| { + fs.realpath_sync(path).map_err(|err| err.into_io_error()) + }) } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -54,20 +52,23 @@ pub enum CliCjsAnalysis { pub struct CliCjsCodeAnalyzer { cache: NodeAnalysisCache, + cjs_tracker: Arc, fs: deno_fs::FileSystemRc, - node_resolver: Arc, + parsed_source_cache: Option>, } impl CliCjsCodeAnalyzer { pub fn new( cache: NodeAnalysisCache, + cjs_tracker: Arc, fs: deno_fs::FileSystemRc, - node_resolver: Arc, + parsed_source_cache: Option>, ) -> Self { Self { cache, + cjs_tracker, fs, - node_resolver, + parsed_source_cache, } } @@ -83,7 +84,7 @@ impl CliCjsCodeAnalyzer { return Ok(analysis); } - let mut media_type = MediaType::from_specifier(specifier); + let media_type = MediaType::from_specifier(specifier); if media_type == MediaType::Json { return Ok(CliCjsAnalysis::Cjs { exports: vec![], @@ -91,54 +92,51 @@ impl CliCjsCodeAnalyzer { }); } - if media_type == MediaType::JavaScript { - if let Some(package_json) = - self.node_resolver.get_closest_package_json(specifier)? - { - match package_json.typ.as_str() { - "commonjs" => { - media_type = MediaType::Cjs; - } - "module" => { - media_type = MediaType::Mjs; - } - _ => {} - } - } - } + let cjs_tracker = self.cjs_tracker.clone(); + let is_maybe_cjs = cjs_tracker.is_maybe_cjs(specifier, media_type)?; + let analysis = if is_maybe_cjs { + let maybe_parsed_source = self + .parsed_source_cache + .as_ref() + .and_then(|c| c.remove_parsed_source(specifier)); - 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 if media_type == MediaType::Cjs { - // FIXME: `deno_ast` should internally handle MediaType::Cjs implying that - // the result must never be Esm - Ok(CliCjsAnalysis::Cjs { - exports: vec![], - reexports: vec![], - }) - } else { - Ok(CliCjsAnalysis::Esm) + deno_core::unsync::spawn_blocking({ + let specifier = specifier.clone(); + let source: Arc = source.into(); + move || -> Result<_, AnyError> { + let parsed_source = + maybe_parsed_source.map(Ok).unwrap_or_else(|| { + deno_ast::parse_program(deno_ast::ParseParams { + specifier, + text: source, + media_type, + capture_tokens: true, + scope_analysis: false, + maybe_syntax: None, + }) + })?; + let is_script = parsed_source.compute_is_script(); + let is_cjs = cjs_tracker.is_cjs_with_known_is_script( + parsed_source.specifier(), + media_type, + is_script, + )?; + if is_cjs { + let analysis = parsed_source.analyze_cjs(); + Ok(CliCjsAnalysis::Cjs { + exports: analysis.exports, + reexports: analysis.reexports, + }) + } else { + Ok(CliCjsAnalysis::Esm) + } } - } - }) - .await - .unwrap()?; + }) + .await + .unwrap()? + } else { + CliCjsAnalysis::Esm + }; self .cache @@ -150,11 +148,11 @@ impl CliCjsCodeAnalyzer { #[async_trait::async_trait(?Send)] impl CjsCodeAnalyzer for CliCjsCodeAnalyzer { - async fn analyze_cjs( + async fn analyze_cjs<'a>( &self, specifier: &ModuleSpecifier, - source: Option, - ) -> Result { + source: Option>, + ) -> Result, AnyError> { let source = match source { Some(source) => source, None => { @@ -162,7 +160,7 @@ impl CjsCodeAnalyzer for CliCjsCodeAnalyzer { if let Ok(source_from_file) = self.fs.read_text_file_lossy_async(path, None).await { - source_from_file + Cow::Owned(source_from_file) } else { return Ok(ExtNodeCjsAnalysis::Cjs(CjsAnalysisExports { exports: vec![], diff --git a/cli/npm/byonm.rs b/cli/npm/byonm.rs index 4c8bbd3948..eca399251b 100644 --- a/cli/npm/byonm.rs +++ b/cli/npm/byonm.rs @@ -2,393 +2,92 @@ use std::borrow::Cow; use std::path::Path; -use std::path::PathBuf; use std::sync::Arc; -use deno_ast::ModuleSpecifier; -use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::serde_json; -use deno_package_json::PackageJsonDepValue; -use deno_runtime::deno_fs::FileSystem; -use deno_runtime::deno_node::DenoPkgJsonFsAdapter; +use deno_resolver::npm::ByonmNpmResolver; +use deno_resolver::npm::ByonmNpmResolverCreateOptions; +use deno_resolver::npm::CliNpmReqResolver; +use deno_runtime::deno_node::DenoFsNodeResolverEnv; use deno_runtime::deno_node::NodePermissions; -use deno_runtime::deno_node::NodeRequireResolver; -use deno_runtime::deno_node::PackageJson; -use deno_runtime::fs_util::specifier_to_file_path; use deno_runtime::ops::process::NpmProcessStateProvider; -use deno_semver::package::PackageReq; -use deno_semver::Version; -use node_resolver::errors::PackageFolderResolveError; -use node_resolver::errors::PackageFolderResolveIoError; -use node_resolver::errors::PackageJsonLoadError; -use node_resolver::errors::PackageNotFoundError; -use node_resolver::load_pkg_json; -use node_resolver::NpmResolver; +use node_resolver::NpmPackageFolderResolver; use crate::args::NpmProcessState; use crate::args::NpmProcessStateKind; -use crate::util::fs::canonicalize_path_maybe_not_exists_with_fs; +use crate::resolver::CliDenoResolverFs; -use super::managed::normalize_pkg_name_for_node_modules_deno_folder; use super::CliNpmResolver; use super::InnerCliNpmResolverRef; -pub struct CliNpmResolverByonmCreateOptions { - pub fs: Arc, - // todo(dsherret): investigate removing this - pub root_node_modules_dir: Option, -} - -pub fn create_byonm_npm_resolver( - options: CliNpmResolverByonmCreateOptions, -) -> Arc { - Arc::new(ByonmCliNpmResolver { - fs: options.fs, - root_node_modules_dir: options.root_node_modules_dir, - }) -} +pub type CliByonmNpmResolverCreateOptions = + ByonmNpmResolverCreateOptions; +pub type CliByonmNpmResolver = + ByonmNpmResolver; +// todo(dsherret): the services hanging off `CliNpmResolver` doesn't seem ideal. We should probably decouple. #[derive(Debug)] -pub struct ByonmCliNpmResolver { - fs: Arc, - root_node_modules_dir: Option, -} +struct CliByonmWrapper(Arc); -impl ByonmCliNpmResolver { - fn load_pkg_json( - &self, - path: &Path, - ) -> Result>, PackageJsonLoadError> { - load_pkg_json(&DenoPkgJsonFsAdapter(self.fs.as_ref()), path) - } - - /// Finds the ancestor package.json that contains the specified dependency. - pub fn find_ancestor_package_json_with_dep( - &self, - dep_name: &str, - referrer: &ModuleSpecifier, - ) -> 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(Some(pkg_json)) = self.load_pkg_json(&pkg_json_path) { - if let Some(deps) = &pkg_json.dependencies { - if deps.contains_key(dep_name) { - return Some(pkg_json); - } - } - if let Some(deps) = &pkg_json.dev_dependencies { - if deps.contains_key(dep_name) { - return Some(pkg_json); - } - } - } - - if let Some(parent) = current_folder.parent() { - current_folder = parent; - } else { - return None; - } - } - } - - fn resolve_pkg_json_and_alias_for_req( - &self, - req: &PackageReq, - referrer: &ModuleSpecifier, - ) -> Result, 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) = self.load_pkg_json(&package_json_path)? { - if let Some(alias) = - resolve_alias_from_pkg_json(req, pkg_json.as_ref()) - { - return Ok(Some((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) = self.load_pkg_json(&root_pkg_json_path)? { - if let Some(alias) = resolve_alias_from_pkg_json(req, pkg_json.as_ref()) - { - return Ok(Some((pkg_json, alias))); - } - } - } - - Ok(None) - } - - fn resolve_folder_in_root_node_modules( - &self, - req: &PackageReq, - ) -> Option { - // now check if node_modules/.deno/ matches this constraint - let root_node_modules_dir = self.root_node_modules_dir.as_ref()?; - let node_modules_deno_dir = root_node_modules_dir.join(".deno"); - let Ok(entries) = self.fs.read_dir_sync(&node_modules_deno_dir) else { - return None; - }; - let search_prefix = format!( - "{}@", - normalize_pkg_name_for_node_modules_deno_folder(&req.name) - ); - let mut best_version = None; - - // example entries: - // - @denotest+add@1.0.0 - // - @denotest+add@1.0.0_1 - for entry in entries { - if !entry.is_directory { - continue; - } - let Some(version_and_copy_idx) = entry.name.strip_prefix(&search_prefix) - else { - continue; - }; - let version = version_and_copy_idx - .rsplit_once('_') - .map(|(v, _)| v) - .unwrap_or(version_and_copy_idx); - let Ok(version) = Version::parse_from_npm(version) else { - continue; - }; - if req.version_req.matches(&version) { - if let Some((best_version_version, _)) = &best_version { - if version > *best_version_version { - best_version = Some((version, entry.name)); - } - } else { - best_version = Some((version, entry.name)); - } - } - } - - best_version.map(|(_version, entry_name)| { - join_package_name( - &node_modules_deno_dir.join(entry_name).join("node_modules"), - &req.name, - ) - }) - } -} - -impl NpmResolver for ByonmCliNpmResolver { - fn resolve_package_folder_from_package( - &self, - name: &str, - referrer: &ModuleSpecifier, - ) -> Result { - fn inner( - fs: &dyn FileSystem, - name: &str, - referrer: &ModuleSpecifier, - ) -> 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")) - }; - - let sub_dir = join_package_name(&node_modules_folder, name); - if fs.is_dir_sync(&sub_dir) { - return Ok(sub_dir); - } - } - } - - Err( - PackageNotFoundError { - package_name: name.to_string(), - referrer: referrer.clone(), - referrer_extra: None, - } - .into(), - ) - } - - let path = inner(&*self.fs, name, referrer)?; - self.fs.realpath_sync(&path).map_err(|err| { - PackageFolderResolveIoError { - package_name: name.to_string(), - referrer: referrer.clone(), - source: err.into_io_error(), - } - .into() - }) - } - - fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { - specifier.scheme() == "file" - && specifier - .path() - .to_ascii_lowercase() - .contains("/node_modules/") - } -} - -impl NodeRequireResolver for ByonmCliNpmResolver { - fn ensure_read_permission( - &self, - permissions: &mut dyn NodePermissions, - path: &Path, - ) -> Result<(), AnyError> { - if !path - .components() - .any(|c| c.as_os_str().to_ascii_lowercase() == "node_modules") - { - _ = permissions.check_read_path(path)?; - } - Ok(()) - } -} - -impl NpmProcessStateProvider for ByonmCliNpmResolver { +impl NpmProcessStateProvider for CliByonmWrapper { fn get_npm_process_state(&self) -> String { serde_json::to_string(&NpmProcessState { kind: NpmProcessStateKind::Byonm, local_node_modules_path: self - .root_node_modules_dir - .as_ref() + .0 + .root_node_modules_dir() .map(|p| p.to_string_lossy().to_string()), }) .unwrap() } } -impl CliNpmResolver for ByonmCliNpmResolver { - fn into_npm_resolver(self: Arc) -> Arc { +impl CliNpmResolver for CliByonmNpmResolver { + fn into_npm_pkg_folder_resolver( + self: Arc, + ) -> Arc { self } - fn into_require_resolver(self: Arc) -> Arc { + fn into_npm_req_resolver(self: Arc) -> Arc { self } fn into_process_state_provider( self: Arc, ) -> Arc { - self + Arc::new(CliByonmWrapper(self)) + } + + fn into_maybe_byonm(self: Arc) -> Option> { + Some(self) } fn clone_snapshotted(&self) -> Arc { - Arc::new(Self { - fs: self.fs.clone(), - root_node_modules_dir: self.root_node_modules_dir.clone(), - }) + Arc::new(self.clone()) } fn as_inner(&self) -> InnerCliNpmResolverRef { InnerCliNpmResolverRef::Byonm(self) } - fn root_node_modules_path(&self) -> Option<&PathBuf> { - self.root_node_modules_dir.as_ref() + fn root_node_modules_path(&self) -> Option<&Path> { + self.root_node_modules_dir() } - fn resolve_pkg_folder_from_deno_module_req( + fn ensure_read_permission<'a>( &self, - req: &PackageReq, - referrer: &ModuleSpecifier, - ) -> Result { - fn node_resolve_dir( - fs: &dyn FileSystem, - alias: &str, - start_dir: &Path, - ) -> Result, AnyError> { - for ancestor in start_dir.ancestors() { - let node_modules_folder = ancestor.join("node_modules"); - let sub_dir = join_package_name(&node_modules_folder, alias); - if fs.is_dir_sync(&sub_dir) { - return Ok(Some(canonicalize_path_maybe_not_exists_with_fs( - &sub_dir, fs, - )?)); - } - } - Ok(None) - } - - // now attempt to resolve if it's found in any package.json - let maybe_pkg_json_and_alias = - self.resolve_pkg_json_and_alias_for_req(req, referrer)?; - match maybe_pkg_json_and_alias { - Some((pkg_json, alias)) => { - // now try node resolution - if let Some(resolved) = - node_resolve_dir(self.fs.as_ref(), &alias, pkg_json.dir_path())? - { - return Ok(resolved); - } - - 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 `deno install`?" - ), - alias, - ); - } - None => { - // now check if node_modules/.deno/ matches this constraint - if let Some(folder) = self.resolve_folder_in_root_node_modules(req) { - return Ok(folder); - } - - bail!( - concat!( - "Could not find a matching package for 'npm:{}' in the node_modules ", - "directory. Ensure you have all your JSR and npm dependencies listed ", - "in your deno.json or package.json, then run `deno install`. Alternatively, ", - r#"turn on auto-install by specifying `"nodeModulesDir": "auto"` in your "#, - "deno.json file." - ), - req, - ); - } + permissions: &mut dyn NodePermissions, + path: &'a Path, + ) -> Result, AnyError> { + if !path + .components() + .any(|c| c.as_os_str().to_ascii_lowercase() == "node_modules") + { + permissions.check_read_path(path).map_err(Into::into) + } else { + Ok(Cow::Borrowed(path)) } } @@ -398,12 +97,3 @@ impl CliNpmResolver for ByonmCliNpmResolver { None } } - -fn join_package_name(path: &Path, package_name: &str) -> PathBuf { - let mut path = path.to_path_buf(); - // ensure backslashes are used on windows - for part in package_name.split('/') { - path = path.join(part); - } - path -} diff --git a/cli/npm/cache_dir.rs b/cli/npm/cache_dir.rs deleted file mode 100644 index 4467d685ec..0000000000 --- a/cli/npm/cache_dir.rs +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -use std::path::Path; -use std::path::PathBuf; - -use deno_ast::ModuleSpecifier; -use deno_core::anyhow::Context; -use deno_core::error::AnyError; -use deno_core::url::Url; -use deno_npm::NpmPackageCacheFolderId; -use deno_semver::package::PackageNv; -use deno_semver::Version; - -use crate::util::fs::canonicalize_path; -use crate::util::path::root_url_to_safe_local_dirname; - -/// The global cache directory of npm packages. -#[derive(Clone, Debug)] -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, 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) - .with_context(|| format!("Error creating {}", root_dir.display()))?; - } - Ok(canonicalize_path(root_dir)?) - } - - // 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 - } - - pub fn package_folder_for_id( - &self, - folder_id: &NpmPackageCacheFolderId, - registry_url: &Url, - ) -> PathBuf { - if folder_id.copy_index == 0 { - 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_nv( - &self, - package: &PackageNv, - registry_url: &Url, - ) -> PathBuf { - self - .package_name_folder(&package.name, registry_url) - .join(package.version.to_string()) - } - - pub fn package_name_folder(&self, name: &str, registry_url: &Url) -> PathBuf { - let mut dir = self.registry_folder(registry_url); - if name.to_lowercase() != name { - let encoded_name = mixed_case_package_name_encode(name); - // Using the encoded directory may have a collision with an actual package name - // so prefix it with an underscore since npm packages can't start with that - dir.join(format!("_{encoded_name}")) - } else { - // ensure backslashes are used on windows - for part in name.split('/') { - dir = dir.join(part); - } - dir - } - } - - fn registry_folder(&self, registry_url: &Url) -> PathBuf { - self - .root_dir - .join(root_url_to_safe_local_dirname(registry_url)) - } - - pub fn resolve_package_folder_id_from_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Option { - 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('_') { - let mut parts = end_url - .split('/') - .map(ToOwned::to_owned) - .collect::>(); - match mixed_case_package_name_decode(&parts[0]) { - Some(part) => { - parts[0] = part; - } - None => return None, - } - relative_url = parts.join("/"); - } - - // examples: - // * chalk/5.0.1/ - // * @types/chalk/5.0.1/ - // * some-package/5.0.1_1/ -- where the `_1` (/_\d+/) is a copy of the folder for peer deps - let is_scoped_package = relative_url.starts_with('@'); - let mut parts = relative_url - .split('/') - .enumerate() - .take(if is_scoped_package { 3 } else { 2 }) - .map(|(_, part)| part) - .collect::>(); - if parts.len() < 2 { - return None; - } - let version_part = parts.pop().unwrap(); - let name = parts.join("/"); - let (version, copy_index) = - if let Some((version, copy_count)) = version_part.split_once('_') { - (version, copy_count.parse::().ok()?) - } else { - (version_part, 0) - }; - Some(NpmPackageCacheFolderId { - nv: PackageNv { - name, - version: Version::parse_from_npm(version).ok()?, - }, - copy_index, - }) - } - - pub fn get_cache_location(&self) -> PathBuf { - self.root_dir.clone() - } -} - -pub fn mixed_case_package_name_encode(name: &str) -> String { - // use base32 encoding because it's reversible and the character set - // only includes the characters within 0-9 and A-Z so it can be lower cased - base32::encode( - base32::Alphabet::Rfc4648Lower { padding: false }, - name.as_bytes(), - ) - .to_lowercase() -} - -pub fn mixed_case_package_name_decode(name: &str) -> Option { - base32::decode(base32::Alphabet::Rfc4648Lower { padding: false }, name) - .and_then(|b| String::from_utf8(b).ok()) -} - -#[cfg(test)] -mod test { - use deno_core::url::Url; - use deno_semver::package::PackageNv; - use deno_semver::Version; - - use super::NpmCacheDir; - use crate::npm::cache_dir::NpmPackageCacheFolderId; - - #[test] - fn should_get_package_folder() { - let deno_dir = crate::cache::DenoDir::new(None).unwrap(); - let root_dir = deno_dir.npm_folder_path(); - let registry_url = Url::parse("https://registry.npmjs.org/").unwrap(); - let cache = NpmCacheDir::new(root_dir.clone(), vec![registry_url.clone()]); - - assert_eq!( - cache.package_folder_for_id( - &NpmPackageCacheFolderId { - nv: PackageNv { - name: "json".to_string(), - version: Version::parse_from_npm("1.2.5").unwrap(), - }, - copy_index: 0, - }, - ®istry_url, - ), - root_dir - .join("registry.npmjs.org") - .join("json") - .join("1.2.5"), - ); - - assert_eq!( - cache.package_folder_for_id( - &NpmPackageCacheFolderId { - nv: PackageNv { - name: "json".to_string(), - version: Version::parse_from_npm("1.2.5").unwrap(), - }, - copy_index: 1, - }, - ®istry_url, - ), - root_dir - .join("registry.npmjs.org") - .join("json") - .join("1.2.5_1"), - ); - - assert_eq!( - cache.package_folder_for_id( - &NpmPackageCacheFolderId { - nv: PackageNv { - name: "JSON".to_string(), - version: Version::parse_from_npm("2.1.5").unwrap(), - }, - copy_index: 0, - }, - ®istry_url, - ), - root_dir - .join("registry.npmjs.org") - .join("_jjju6tq") - .join("2.1.5"), - ); - - assert_eq!( - cache.package_folder_for_id( - &NpmPackageCacheFolderId { - nv: PackageNv { - name: "@types/JSON".to_string(), - version: Version::parse_from_npm("2.1.5").unwrap(), - }, - copy_index: 0, - }, - ®istry_url, - ), - root_dir - .join("registry.npmjs.org") - .join("_ib2hs4dfomxuuu2pjy") - .join("2.1.5"), - ); - } -} diff --git a/cli/npm/common.rs b/cli/npm/common.rs index a3a828e745..55f1bc086d 100644 --- a/cli/npm/common.rs +++ b/cli/npm/common.rs @@ -3,6 +3,7 @@ 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::npm_rc::RegistryConfig; use http::header; @@ -36,17 +37,21 @@ pub fn maybe_auth_header_for_npm_registry( } if username.is_some() && password.is_some() { + // The npm client does some double encoding when generating the + // bearer token value, see + // https://github.com/npm/cli/blob/780afc50e3a345feb1871a28e33fa48235bc3bd5/workspaces/config/lib/index.js#L846-L851 + let pw_base64 = BASE64_STANDARD + .decode(password.unwrap()) + .with_context(|| "The password in npmrc is an invalid base64 string")?; + let bearer = BASE64_STANDARD.encode(format!( + "{}:{}", + username.unwrap(), + String::from_utf8_lossy(&pw_base64) + )); + return Ok(Some(( header::AUTHORIZATION, - header::HeaderValue::from_str(&format!( - "Basic {}", - BASE64_STANDARD.encode(&format!( - "{}:{}", - username.unwrap(), - password.unwrap() - )) - )) - .unwrap(), + header::HeaderValue::from_str(&format!("Basic {}", bearer)).unwrap(), ))); } diff --git a/cli/npm/managed/cache/mod.rs b/cli/npm/managed/cache/mod.rs index f409744b98..8ae99f41e0 100644 --- a/cli/npm/managed/cache/mod.rs +++ b/cli/npm/managed/cache/mod.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; use std::sync::Arc; use deno_ast::ModuleSpecifier; +use deno_cache_dir::npm::NpmCacheDir; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; @@ -18,14 +19,14 @@ use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::registry::NpmPackageInfo; use deno_npm::NpmPackageCacheFolderId; use deno_semver::package::PackageNv; +use deno_semver::Version; use crate::args::CacheSetting; use crate::cache::CACHE_PERM; -use crate::npm::NpmCacheDir; use crate::util::fs::atomic_write_file_with_retries; use crate::util::fs::hard_link_dir_recursive; -mod registry_info; +pub mod registry_info; mod tarball; mod tarball_extract; @@ -35,7 +36,7 @@ pub use tarball::TarballCache; /// Stores a single copy of npm packages in a cache. #[derive(Debug)] pub struct NpmCache { - cache_dir: NpmCacheDir, + cache_dir: Arc, cache_setting: CacheSetting, npmrc: Arc, /// ensures a package is only downloaded once per run @@ -44,7 +45,7 @@ pub struct NpmCache { impl NpmCache { pub fn new( - cache_dir: NpmCacheDir, + cache_dir: Arc, cache_setting: CacheSetting, npmrc: Arc, ) -> Self { @@ -60,6 +61,10 @@ impl NpmCache { &self.cache_setting } + pub fn root_dir_path(&self) -> &Path { + self.cache_dir.root_dir() + } + pub fn root_dir_url(&self) -> &Url { self.cache_dir.root_dir_url() } @@ -87,9 +92,12 @@ impl NpmCache { ) -> 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); + let package_folder = self.cache_dir.package_folder_for_id( + &folder_id.nv.name, + &folder_id.nv.version.to_string(), + folder_id.copy_index, + registry_url, + ); if package_folder.exists() // if this file exists, then the package didn't successfully initialize @@ -100,9 +108,12 @@ impl NpmCache { return Ok(()); } - let original_package_folder = self - .cache_dir - .package_folder_for_nv(&folder_id.nv, registry_url); + let original_package_folder = self.cache_dir.package_folder_for_id( + &folder_id.nv.name, + &folder_id.nv.version.to_string(), + 0, // original copy index + registry_url, + ); // it seems Windows does an "AccessDenied" error when moving a // directory with hard links, so that's why this solution is done @@ -114,7 +125,12 @@ impl NpmCache { 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) + self.cache_dir.package_folder_for_id( + &id.nv.name, + &id.nv.version.to_string(), + id.copy_index, + registry_url, + ) } pub fn package_folder_for_nv(&self, package: &PackageNv) -> PathBuf { @@ -127,7 +143,12 @@ impl NpmCache { package: &PackageNv, registry_url: &Url, ) -> PathBuf { - self.cache_dir.package_folder_for_nv(package, registry_url) + self.cache_dir.package_folder_for_id( + &package.name, + &package.version.to_string(), + 0, // original copy_index + registry_url, + ) } pub fn package_name_folder(&self, name: &str) -> PathBuf { @@ -135,10 +156,6 @@ impl NpmCache { self.cache_dir.package_name_folder(name, 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, @@ -146,6 +163,15 @@ impl NpmCache { self .cache_dir .resolve_package_folder_id_from_specifier(specifier) + .and_then(|cache_id| { + Some(NpmPackageCacheFolderId { + nv: PackageNv { + name: cache_id.name, + version: Version::parse_from_npm(&cache_id.version).ok()?, + }, + copy_index: cache_id.copy_index, + }) + }) } pub fn load_package_info( diff --git a/cli/npm/managed/cache/registry_info.rs b/cli/npm/managed/cache/registry_info.rs index 28b19373e9..6d39d3c13f 100644 --- a/cli/npm/managed/cache/registry_info.rs +++ b/cli/npm/managed/cache/registry_info.rs @@ -84,7 +84,7 @@ impl RegistryInfoDownloader { self.load_package_info_inner(name).await.with_context(|| { format!( "Error getting response at {} for package \"{}\"", - self.get_package_url(name), + get_package_url(&self.npmrc, name), name ) }) @@ -190,7 +190,7 @@ impl RegistryInfoDownloader { fn create_load_future(self: &Arc, name: &str) -> LoadFuture { let downloader = self.clone(); - let package_url = self.get_package_url(name); + let package_url = get_package_url(&self.npmrc, name); let registry_config = self.npmrc.get_registry_config(name); let maybe_auth_header = match maybe_auth_header_for_npm_registry(registry_config) { @@ -202,10 +202,13 @@ impl RegistryInfoDownloader { let guard = self.progress_bar.update(package_url.as_str()); 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, &guard) + let client = downloader.http_client_provider.get_or_create()?; + let maybe_bytes = client + .download_with_progress_and_retries( + package_url, + maybe_auth_header, + &guard, + ) .await?; match maybe_bytes { Some(bytes) => { @@ -236,25 +239,36 @@ impl RegistryInfoDownloader { .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() - } +} + +pub fn get_package_url(npmrc: &ResolvedNpmRc, name: &str) -> Url { + let registry_url = npmrc.get_registry_url(name); + // The '/' character in scoped package names "@scope/name" must be + // encoded for older third party registries. Newer registries and + // npm itself support both ways + // - encoded: https://registry.npmjs.org/@rollup%2fplugin-json + // - non-ecoded: https://registry.npmjs.org/@rollup/plugin-json + // To support as many third party registries as possible we'll + // always encode the '/' character. + + // 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'~'); + let name = percent_encoding::utf8_percent_encode(name, &ASCII_SET); + registry_url + // Ensure that scoped package name percent encoding is lower cased + // to match npm. + .join(&name.to_string().replace("%2F", "%2f")) + .unwrap() } diff --git a/cli/npm/managed/cache/tarball.rs b/cli/npm/managed/cache/tarball.rs index 4bcee38ea8..7cf88d6d64 100644 --- a/cli/npm/managed/cache/tarball.rs +++ b/cli/npm/managed/cache/tarball.rs @@ -172,7 +172,7 @@ impl TarballCache { let guard = tarball_cache.progress_bar.update(&dist.tarball); let result = tarball_cache.http_client_provider .get_or_create()? - .download_with_progress(tarball_uri, maybe_auth_header, &guard) + .download_with_progress_and_retries(tarball_uri, maybe_auth_header, &guard) .await; let maybe_bytes = match result { Ok(maybe_bytes) => maybe_bytes, diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs index 40c92cd46c..2e64f5f188 100644 --- a/cli/npm/managed/mod.rs +++ b/cli/npm/managed/mod.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::borrow::Cow; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -7,9 +8,11 @@ use std::sync::Arc; use cache::RegistryInfoDownloader; use cache::TarballCache; use deno_ast::ModuleSpecifier; +use deno_cache_dir::npm::NpmCacheDir; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::serde_json; +use deno_core::url::Url; use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::registry::NpmPackageInfo; use deno_npm::registry::NpmRegistryApi; @@ -19,15 +22,17 @@ use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; use deno_npm::NpmSystemInfo; +use deno_resolver::npm::CliNpmReqResolver; +use deno_runtime::colors; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::NodePermissions; -use deno_runtime::deno_node::NodeRequireResolver; use deno_runtime::ops::process::NpmProcessStateProvider; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use node_resolver::errors::PackageFolderResolveError; use node_resolver::errors::PackageFolderResolveIoError; -use node_resolver::NpmResolver; +use node_resolver::InNpmPackageChecker; +use node_resolver::NpmPackageFolderResolver; use resolution::AddPkgReqsResult; use crate::args::CliLockfile; @@ -35,6 +40,7 @@ use crate::args::LifecycleScriptsConfig; use crate::args::NpmInstallDepsProvider; use crate::args::NpmProcessState; use crate::args::NpmProcessStateKind; +use crate::args::PackageJsonDepValueParseWithLocationError; use crate::cache::FastInsecureHasher; use crate::http_util::HttpClientProvider; use crate::util::fs::canonicalize_path_maybe_not_exists_with_fs; @@ -45,14 +51,13 @@ use self::cache::NpmCache; use self::registry::CliNpmRegistryApi; use self::resolution::NpmResolution; use self::resolvers::create_npm_fs_resolver; -pub use self::resolvers::normalize_pkg_name_for_node_modules_deno_folder; use self::resolvers::NpmPackageFsResolver; use super::CliNpmResolver; use super::InnerCliNpmResolverRef; -use super::NpmCacheDir; +use super::ResolvePkgFolderFromDenoReqError; -mod cache; +pub mod cache; mod registry; mod resolution; mod resolvers; @@ -62,12 +67,12 @@ pub enum CliNpmResolverManagedSnapshotOption { Specified(Option), } -pub struct CliNpmResolverManagedCreateOptions { +pub struct CliManagedNpmResolverCreateOptions { pub snapshot: CliNpmResolverManagedSnapshotOption, pub maybe_lockfile: Option>, pub fs: Arc, pub http_client_provider: Arc, - pub npm_global_cache_dir: PathBuf, + pub npm_cache_dir: Arc, pub cache_setting: crate::args::CacheSetting, pub text_only_progress_bar: crate::util::progress_bar::ProgressBar, pub maybe_node_modules_path: Option, @@ -78,7 +83,7 @@ pub struct CliNpmResolverManagedCreateOptions { } pub async fn create_managed_npm_resolver_for_lsp( - options: CliNpmResolverManagedCreateOptions, + options: CliManagedNpmResolverCreateOptions, ) -> Arc { let npm_cache = create_cache(&options); let npm_api = create_api(&options, npm_cache.clone()); @@ -111,7 +116,7 @@ pub async fn create_managed_npm_resolver_for_lsp( } pub async fn create_managed_npm_resolver( - options: CliNpmResolverManagedCreateOptions, + options: CliManagedNpmResolverCreateOptions, ) -> Result, AnyError> { let npm_cache = create_cache(&options); let npm_api = create_api(&options, npm_cache.clone()); @@ -185,19 +190,16 @@ fn create_inner( )) } -fn create_cache(options: &CliNpmResolverManagedCreateOptions) -> Arc { +fn create_cache(options: &CliManagedNpmResolverCreateOptions) -> Arc { Arc::new(NpmCache::new( - NpmCacheDir::new( - options.npm_global_cache_dir.clone(), - options.npmrc.get_all_known_registries_urls(), - ), + options.npm_cache_dir.clone(), options.cache_setting.clone(), options.npmrc.clone(), )) } fn create_api( - options: &CliNpmResolverManagedCreateOptions, + options: &CliManagedNpmResolverCreateOptions, npm_cache: Arc, ) -> Arc { Arc::new(CliNpmRegistryApi::new( @@ -254,6 +256,35 @@ async fn snapshot_from_lockfile( Ok(snapshot) } +#[derive(Debug)] +struct ManagedInNpmPackageChecker { + root_dir: Url, +} + +impl InNpmPackageChecker for ManagedInNpmPackageChecker { + fn in_npm_package(&self, specifier: &Url) -> bool { + specifier.as_ref().starts_with(self.root_dir.as_str()) + } +} + +pub struct CliManagedInNpmPkgCheckerCreateOptions<'a> { + pub root_cache_dir_url: &'a Url, + pub maybe_node_modules_path: Option<&'a Path>, +} + +pub fn create_managed_in_npm_pkg_checker( + options: CliManagedInNpmPkgCheckerCreateOptions, +) -> Arc { + let root_dir = match options.maybe_node_modules_path { + Some(node_modules_folder) => { + deno_path_util::url_from_directory_path(node_modules_folder).unwrap() + } + None => options.root_cache_dir_url.clone(), + }; + debug_assert!(root_dir.as_str().ends_with('/')); + Arc::new(ManagedInNpmPackageChecker { root_dir }) +} + /// 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 { @@ -427,6 +458,16 @@ impl ManagedCliNpmResolver { self.resolution.snapshot() } + pub fn top_package_req_for_name(&self, name: &str) -> Option { + let package_reqs = self.resolution.package_reqs(); + let mut entries = package_reqs + .iter() + .filter(|(_, nv)| nv.name == name) + .collect::>(); + entries.sort_by_key(|(_, nv)| &nv.version); + Some(entries.last()?.0.clone()) + } + pub fn serialized_valid_snapshot_for_system( &self, system_info: &NpmSystemInfo, @@ -466,6 +507,30 @@ impl ManagedCliNpmResolver { self.resolution.resolve_pkg_id_from_pkg_req(req) } + pub fn ensure_no_pkg_json_dep_errors( + &self, + ) -> Result<(), Box> { + for err in self.npm_install_deps_provider.pkg_json_dep_errors() { + match &err.source { + deno_package_json::PackageJsonDepValueParseError::VersionReq(_) => { + return Err(Box::new(err.clone())); + } + deno_package_json::PackageJsonDepValueParseError::Unsupported { + .. + } => { + // only warn for this one + log::warn!( + "{} {}\n at {}", + colors::yellow("Warning"), + err.source, + err.location, + ) + } + } + } + Ok(()) + } + /// Ensures that the top level `package.json` dependencies are installed. /// This may set up the `node_modules` directory. /// @@ -477,6 +542,7 @@ impl ManagedCliNpmResolver { if !self.top_level_install_flag.raise() { return Ok(false); // already did this } + let pkg_json_remote_pkgs = self.npm_install_deps_provider.remote_pkgs(); if pkg_json_remote_pkgs.is_empty() { return Ok(false); @@ -515,8 +581,16 @@ impl ManagedCliNpmResolver { .map_err(|err| err.into()) } - pub fn global_cache_root_folder(&self) -> PathBuf { - self.npm_cache.root_folder() + pub fn maybe_node_modules_path(&self) -> Option<&Path> { + self.fs_resolver.node_modules_path() + } + + pub fn global_cache_root_path(&self) -> &Path { + self.npm_cache.root_dir_path() + } + + pub fn global_cache_root_url(&self) -> &Url { + self.npm_cache.root_dir_url() } } @@ -532,7 +606,7 @@ fn npm_process_state( .unwrap() } -impl NpmResolver for ManagedCliNpmResolver { +impl NpmPackageFolderResolver for ManagedCliNpmResolver { fn resolve_package_folder_from_package( &self, name: &str, @@ -551,39 +625,40 @@ impl NpmResolver for ManagedCliNpmResolver { log::debug!("Resolved {} from {} to {}", name, referrer, path.display()); Ok(path) } - - fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { - let root_dir_url = self.fs_resolver.root_dir_url(); - debug_assert!(root_dir_url.as_str().ends_with('/')); - specifier.as_ref().starts_with(root_dir_url.as_str()) - } -} - -impl NodeRequireResolver for ManagedCliNpmResolver { - fn ensure_read_permission( - &self, - permissions: &mut dyn NodePermissions, - path: &Path, - ) -> Result<(), AnyError> { - self.fs_resolver.ensure_read_permission(permissions, path) - } } impl NpmProcessStateProvider for ManagedCliNpmResolver { fn get_npm_process_state(&self) -> String { npm_process_state( self.resolution.serialized_valid_snapshot(), - self.fs_resolver.node_modules_path().map(|p| p.as_path()), + self.fs_resolver.node_modules_path(), ) } } +impl CliNpmReqResolver for ManagedCliNpmResolver { + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + _referrer: &ModuleSpecifier, + ) -> Result { + let pkg_id = self + .resolve_pkg_id_from_pkg_req(req) + .map_err(|err| ResolvePkgFolderFromDenoReqError::Managed(err.into()))?; + self + .resolve_pkg_folder_from_pkg_id(&pkg_id) + .map_err(ResolvePkgFolderFromDenoReqError::Managed) + } +} + impl CliNpmResolver for ManagedCliNpmResolver { - fn into_npm_resolver(self: Arc) -> Arc { + fn into_npm_pkg_folder_resolver( + self: Arc, + ) -> Arc { self } - fn into_require_resolver(self: Arc) -> Arc { + fn into_npm_req_resolver(self: Arc) -> Arc { self } @@ -630,17 +705,16 @@ impl CliNpmResolver for ManagedCliNpmResolver { InnerCliNpmResolverRef::Managed(self) } - fn root_node_modules_path(&self) -> Option<&PathBuf> { + fn root_node_modules_path(&self) -> Option<&Path> { self.fs_resolver.node_modules_path() } - fn resolve_pkg_folder_from_deno_module_req( + fn ensure_read_permission<'a>( &self, - req: &PackageReq, - _referrer: &ModuleSpecifier, - ) -> Result { - let pkg_id = self.resolve_pkg_id_from_pkg_req(req)?; - self.resolve_pkg_folder_from_pkg_id(&pkg_id) + permissions: &mut dyn NodePermissions, + path: &'a Path, + ) -> Result, AnyError> { + self.fs_resolver.ensure_read_permission(permissions, path) } fn check_state_hash(&self) -> Option { diff --git a/cli/npm/managed/resolvers/common.rs b/cli/npm/managed/resolvers/common.rs index 620daf4b30..eee11c7604 100644 --- a/cli/npm/managed/resolvers/common.rs +++ b/cli/npm/managed/resolvers/common.rs @@ -3,6 +3,7 @@ pub mod bin_entries; pub mod lifecycle_scripts; +use std::borrow::Cow; use std::collections::HashMap; use std::io::ErrorKind; use std::path::Path; @@ -16,7 +17,6 @@ use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures; use deno_core::futures::StreamExt; -use deno_core::url::Url; use deno_npm::NpmPackageCacheFolderId; use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; @@ -29,11 +29,8 @@ use crate::npm::managed::cache::TarballCache; /// Part of the resolution that interacts with the file system. #[async_trait(?Send)] pub trait NpmPackageFsResolver: Send + Sync { - /// Specifier for the root directory. - fn root_dir_url(&self) -> &Url; - /// The local node_modules folder if it is applicable to the implementation. - fn node_modules_path(&self) -> Option<&PathBuf>; + fn node_modules_path(&self) -> Option<&Path>; fn maybe_package_folder(&self, package_id: &NpmPackageId) -> Option; @@ -62,11 +59,12 @@ pub trait NpmPackageFsResolver: Send + Sync { async fn cache_packages(&self) -> Result<(), AnyError>; - fn ensure_read_permission( + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn ensure_read_permission<'a>( &self, permissions: &mut dyn NodePermissions, - path: &Path, - ) -> Result<(), AnyError>; + path: &'a Path, + ) -> Result, AnyError>; } #[derive(Debug)] @@ -85,11 +83,15 @@ impl RegistryReadPermissionChecker { } } - pub fn ensure_registry_read_permission( + pub fn ensure_registry_read_permission<'a>( &self, permissions: &mut dyn NodePermissions, - path: &Path, - ) -> Result<(), AnyError> { + path: &'a Path, + ) -> Result, AnyError> { + if permissions.query_read_all() { + return Ok(Cow::Borrowed(path)); // skip permissions checks below + } + // allow reading if it's in the node_modules let is_path_in_node_modules = path.starts_with(&self.registry_path) && path @@ -118,20 +120,20 @@ impl RegistryReadPermissionChecker { }, } }; - 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(()); + if let Some(registry_path_canon) = canonicalize(&self.registry_path)? { + if let Some(path_canon) = canonicalize(path)? { + if path_canon.starts_with(registry_path_canon) { + return Ok(Cow::Owned(path_canon)); + } + } else if path.starts_with(registry_path_canon) + || path.starts_with(&self.registry_path) + { + return Ok(Cow::Borrowed(path)); + } } } - _ = permissions.check_read_path(path)?; - Ok(()) + permissions.check_read_path(path).map_err(Into::into) } } diff --git a/cli/npm/managed/resolvers/common/bin_entries.rs b/cli/npm/managed/resolvers/common/bin_entries.rs index 4524ce8326..e4a1845689 100644 --- a/cli/npm/managed/resolvers/common/bin_entries.rs +++ b/cli/npm/managed/resolvers/common/bin_entries.rs @@ -18,6 +18,7 @@ pub struct BinEntries<'a> { seen_names: HashMap<&'a str, &'a NpmPackageId>, /// The bin entries entries: Vec<(&'a NpmResolutionPackage, PathBuf)>, + sorted: bool, } /// Returns the name of the default binary for the given package. @@ -31,6 +32,20 @@ fn default_bin_name(package: &NpmResolutionPackage) -> &str { .map_or(package.id.nv.name.as_str(), |(_, name)| name) } +pub fn warn_missing_entrypoint( + bin_name: &str, + package_path: &Path, + entrypoint: &Path, +) { + log::warn!( + "{} Trying to set up '{}' bin for \"{}\", but the entry point \"{}\" doesn't exist.", + deno_terminal::colors::yellow("Warning"), + bin_name, + package_path.display(), + entrypoint.display() + ); +} + impl<'a> BinEntries<'a> { pub fn new() -> Self { Self::default() @@ -42,6 +57,7 @@ impl<'a> BinEntries<'a> { package: &'a NpmResolutionPackage, package_path: PathBuf, ) { + self.sorted = false; // check for a new collision, if we haven't already // found one match package.bin.as_ref().unwrap() { @@ -79,16 +95,21 @@ impl<'a> BinEntries<'a> { &str, // bin name &str, // bin script ) -> Result<(), AnyError>, + mut filter: impl FnMut(&NpmResolutionPackage) -> bool, ) -> Result<(), AnyError> { - if !self.collisions.is_empty() { + if !self.collisions.is_empty() && !self.sorted { // walking the dependency tree to find out the depth of each package // is sort of expensive, so we only do it if there's a collision sort_by_depth(snapshot, &mut self.entries, &mut self.collisions); + self.sorted = true; } let mut seen = HashSet::new(); for (package, package_path) in &self.entries { + if !filter(package) { + continue; + } if let Some(bin_entries) = &package.bin { match bin_entries { deno_npm::registry::NpmPackageVersionBinEntry::String(script) => { @@ -118,8 +139,8 @@ impl<'a> BinEntries<'a> { } /// Collect the bin entries into a vec of (name, script path) - pub fn into_bin_files( - mut self, + pub fn collect_bin_files( + &mut self, snapshot: &NpmResolutionSnapshot, ) -> Vec<(String, PathBuf)> { let mut bins = Vec::new(); @@ -131,17 +152,18 @@ impl<'a> BinEntries<'a> { bins.push((name.to_string(), package_path.join(script))); Ok(()) }, + |_| true, ) .unwrap(); bins } - /// Finish setting up the bin entries, writing the necessary files - /// to disk. - pub fn finish( + fn set_up_entries_filtered( mut self, snapshot: &NpmResolutionSnapshot, bin_node_modules_dir_path: &Path, + filter: impl FnMut(&NpmResolutionPackage) -> bool, + mut handler: impl FnMut(&EntrySetupOutcome<'_>), ) -> Result<(), AnyError> { if !self.entries.is_empty() && !bin_node_modules_dir_path.exists() { std::fs::create_dir_all(bin_node_modules_dir_path).with_context( @@ -160,18 +182,54 @@ impl<'a> BinEntries<'a> { Ok(()) }, |package, package_path, name, script| { - set_up_bin_entry( + let outcome = set_up_bin_entry( package, name, script, package_path, bin_node_modules_dir_path, - ) + )?; + handler(&outcome); + Ok(()) }, + filter, )?; Ok(()) } + + /// Finish setting up the bin entries, writing the necessary files + /// to disk. + pub fn finish( + self, + snapshot: &NpmResolutionSnapshot, + bin_node_modules_dir_path: &Path, + handler: impl FnMut(&EntrySetupOutcome<'_>), + ) -> Result<(), AnyError> { + self.set_up_entries_filtered( + snapshot, + bin_node_modules_dir_path, + |_| true, + handler, + ) + } + + /// Finish setting up the bin entries, writing the necessary files + /// to disk. + pub fn finish_only( + self, + snapshot: &NpmResolutionSnapshot, + bin_node_modules_dir_path: &Path, + handler: impl FnMut(&EntrySetupOutcome<'_>), + only: &HashSet<&NpmPackageId>, + ) -> Result<(), AnyError> { + self.set_up_entries_filtered( + snapshot, + bin_node_modules_dir_path, + |package| only.contains(&package.id), + handler, + ) + } } // walk the dependency tree to find out the depth of each package @@ -233,16 +291,17 @@ fn sort_by_depth( }); } -pub fn set_up_bin_entry( - package: &NpmResolutionPackage, - bin_name: &str, +pub fn set_up_bin_entry<'a>( + package: &'a NpmResolutionPackage, + bin_name: &'a str, #[allow(unused_variables)] bin_script: &str, - #[allow(unused_variables)] package_path: &Path, + #[allow(unused_variables)] package_path: &'a Path, bin_node_modules_dir_path: &Path, -) -> Result<(), AnyError> { +) -> Result, AnyError> { #[cfg(windows)] { set_up_bin_shim(package, bin_name, bin_node_modules_dir_path)?; + Ok(EntrySetupOutcome::Success) } #[cfg(unix)] { @@ -252,9 +311,8 @@ pub fn set_up_bin_entry( bin_script, package_path, bin_node_modules_dir_path, - )?; + ) } - Ok(()) } #[cfg(windows)] @@ -301,14 +359,39 @@ fn make_executable_if_exists(path: &Path) -> Result { Ok(true) } +pub enum EntrySetupOutcome<'a> { + #[cfg_attr(windows, allow(dead_code))] + MissingEntrypoint { + bin_name: &'a str, + package_path: &'a Path, + entrypoint: PathBuf, + package: &'a NpmResolutionPackage, + }, + Success, +} + +impl<'a> EntrySetupOutcome<'a> { + pub fn warn_if_failed(&self) { + match self { + EntrySetupOutcome::MissingEntrypoint { + bin_name, + package_path, + entrypoint, + .. + } => warn_missing_entrypoint(bin_name, package_path, entrypoint), + EntrySetupOutcome::Success => {} + } + } +} + #[cfg(unix)] -fn symlink_bin_entry( - _package: &NpmResolutionPackage, - bin_name: &str, +fn symlink_bin_entry<'a>( + package: &'a NpmResolutionPackage, + bin_name: &'a str, bin_script: &str, - package_path: &Path, + package_path: &'a Path, bin_node_modules_dir_path: &Path, -) -> Result<(), AnyError> { +) -> Result, AnyError> { use std::io; use std::os::unix::fs::symlink; let link = bin_node_modules_dir_path.join(bin_name); @@ -318,14 +401,12 @@ fn symlink_bin_entry( format!("Can't set up '{}' bin at {}", bin_name, original.display()) })?; if !found { - log::warn!( - "{} Trying to set up '{}' bin for \"{}\", but the entry point \"{}\" doesn't exist.", - deno_terminal::colors::yellow("Warning"), + return Ok(EntrySetupOutcome::MissingEntrypoint { bin_name, - package_path.display(), - original.display() - ); - return Ok(()); + package_path, + entrypoint: original, + package, + }); } let original_relative = @@ -348,7 +429,7 @@ fn symlink_bin_entry( original_relative.display() ) })?; - return Ok(()); + return Ok(EntrySetupOutcome::Success); } return Err(err).with_context(|| { format!( @@ -359,5 +440,5 @@ fn symlink_bin_entry( }); } - Ok(()) + Ok(EntrySetupOutcome::Success) } diff --git a/cli/npm/managed/resolvers/common/lifecycle_scripts.rs b/cli/npm/managed/resolvers/common/lifecycle_scripts.rs index b358c35856..5c5755c819 100644 --- a/cli/npm/managed/resolvers/common/lifecycle_scripts.rs +++ b/cli/npm/managed/resolvers/common/lifecycle_scripts.rs @@ -2,12 +2,15 @@ use super::bin_entries::BinEntries; use crate::args::LifecycleScriptsConfig; +use crate::task_runner::TaskStdio; +use crate::util::progress_bar::ProgressBar; use deno_core::anyhow::Context; use deno_npm::resolution::NpmResolutionSnapshot; use deno_runtime::deno_io::FromRawIoHandle; use deno_semver::package::PackageNv; use deno_semver::Version; use std::borrow::Cow; +use std::collections::HashSet; use std::rc::Rc; use std::path::Path; @@ -59,7 +62,7 @@ impl<'a> LifecycleScripts<'a> { } } -fn has_lifecycle_scripts( +pub fn has_lifecycle_scripts( package: &NpmResolutionPackage, package_path: &Path, ) -> bool { @@ -81,7 +84,7 @@ fn is_broken_default_install_script(script: &str, package_path: &Path) -> bool { } impl<'a> LifecycleScripts<'a> { - fn can_run_scripts(&self, package_nv: &PackageNv) -> bool { + pub fn can_run_scripts(&self, package_nv: &PackageNv) -> bool { if !self.strategy.can_run_scripts() { return false; } @@ -96,6 +99,9 @@ impl<'a> LifecycleScripts<'a> { PackagesAllowedScripts::None => false, } } + pub fn has_run_scripts(&self, package: &NpmResolutionPackage) -> bool { + self.strategy.has_run(package) + } /// Register a package for running lifecycle scripts, if applicable. /// /// `package_path` is the path containing the package's code (its root dir). @@ -108,12 +114,12 @@ impl<'a> LifecycleScripts<'a> { ) { if has_lifecycle_scripts(package, &package_path) { if self.can_run_scripts(&package.id.nv) { - if !self.strategy.has_run(package) { + if !self.has_run_scripts(package) { self .packages_with_scripts .push((package, package_path.into_owned())); } - } else if !self.strategy.has_run(package) + } else if !self.has_run_scripts(package) && (self.config.explicit_install || !self.strategy.has_warned(package)) { // Skip adding `esbuild` as it is known that it can work properly without lifecycle script @@ -147,21 +153,32 @@ impl<'a> LifecycleScripts<'a> { self, snapshot: &NpmResolutionSnapshot, packages: &[NpmResolutionPackage], - root_node_modules_dir_path: Option<&Path>, + root_node_modules_dir_path: &Path, + progress_bar: &ProgressBar, ) -> Result<(), AnyError> { self.warn_not_run_scripts()?; let get_package_path = |p: &NpmResolutionPackage| self.strategy.package_path(p); let mut failed_packages = Vec::new(); + let mut bin_entries = BinEntries::new(); if !self.packages_with_scripts.is_empty() { + let package_ids = self + .packages_with_scripts + .iter() + .map(|(p, _)| &p.id) + .collect::>(); // get custom commands for each bin available in the node_modules dir (essentially // the scripts that are in `node_modules/.bin`) - let base = - resolve_baseline_custom_commands(snapshot, packages, get_package_path)?; + let base = resolve_baseline_custom_commands( + &mut bin_entries, + snapshot, + packages, + get_package_path, + )?; let init_cwd = &self.config.initial_cwd; let process_state = crate::npm::managed::npm_process_state( snapshot.as_valid_serialized(), - root_node_modules_dir_path, + Some(root_node_modules_dir_path), ); let mut env_vars = crate::task_runner::real_env_vars(); @@ -201,7 +218,15 @@ impl<'a> LifecycleScripts<'a> { { continue; } - let exit_code = crate::task_runner::run_task( + let _guard = progress_bar.update_with_prompt( + crate::util::progress_bar::ProgressMessagePrompt::Initialize, + &format!("{}: running '{script_name}' script", package.id.nv), + ); + let crate::task_runner::TaskResult { + exit_code, + stderr, + stdout, + } = crate::task_runner::run_task( crate::task_runner::RunTaskOptions { task_name: script_name, script, @@ -210,16 +235,38 @@ impl<'a> LifecycleScripts<'a> { custom_commands: custom_commands.clone(), init_cwd, argv: &[], - root_node_modules_dir: root_node_modules_dir_path, + root_node_modules_dir: Some(root_node_modules_dir_path), + stdio: Some(crate::task_runner::TaskIo { + stderr: TaskStdio::piped(), + stdout: TaskStdio::piped(), + }), }, ) .await?; + let stdout = stdout.unwrap(); + let stderr = stderr.unwrap(); if exit_code != 0 { log::warn!( - "error: script '{}' in '{}' failed with exit code {}", + "error: script '{}' in '{}' failed with exit code {}{}{}", script_name, package.id.nv, exit_code, + if !stdout.trim_ascii().is_empty() { + format!( + "\nstdout:\n{}\n", + String::from_utf8_lossy(&stdout).trim() + ) + } else { + String::new() + }, + if !stderr.trim_ascii().is_empty() { + format!( + "\nstderr:\n{}\n", + String::from_utf8_lossy(&stderr).trim() + ) + } else { + String::new() + }, ); failed_packages.push(&package.id.nv); // assume if earlier script fails, later ones will fail too @@ -229,6 +276,17 @@ impl<'a> LifecycleScripts<'a> { } self.strategy.did_run_scripts(package)?; } + + // re-set up bin entries for the packages which we've run scripts for. + // lifecycle scripts can create files that are linked to by bin entries, + // and the only reliable way to handle this is to re-link bin entries + // (this is what PNPM does as well) + bin_entries.finish_only( + snapshot, + &root_node_modules_dir_path.join(".bin"), + |outcome| outcome.warn_if_failed(), + &package_ids, + )?; } if failed_packages.is_empty() { Ok(()) @@ -248,9 +306,10 @@ impl<'a> LifecycleScripts<'a> { // take in all (non copy) packages from snapshot, // and resolve the set of available binaries to create // custom commands available to the task runner -fn resolve_baseline_custom_commands( - snapshot: &NpmResolutionSnapshot, - packages: &[NpmResolutionPackage], +fn resolve_baseline_custom_commands<'a>( + bin_entries: &mut BinEntries<'a>, + snapshot: &'a NpmResolutionSnapshot, + packages: &'a [NpmResolutionPackage], get_package_path: impl Fn(&NpmResolutionPackage) -> PathBuf, ) -> Result { let mut custom_commands = crate::task_runner::TaskCustomCommands::new(); @@ -273,6 +332,7 @@ fn resolve_baseline_custom_commands( // doing it for packages that are set up already. // realistically, scripts won't be run very often so it probably isn't too big of an issue. resolve_custom_commands_from_packages( + bin_entries, custom_commands, snapshot, packages, @@ -287,12 +347,12 @@ fn resolve_custom_commands_from_packages< 'a, P: IntoIterator, >( + bin_entries: &mut BinEntries<'a>, mut commands: crate::task_runner::TaskCustomCommands, snapshot: &'a NpmResolutionSnapshot, packages: P, get_package_path: impl Fn(&'a NpmResolutionPackage) -> PathBuf, ) -> Result { - let mut bin_entries = BinEntries::new(); for package in packages { let package_path = get_package_path(package); @@ -300,7 +360,7 @@ fn resolve_custom_commands_from_packages< bin_entries.add(package, package_path); } } - let bins = bin_entries.into_bin_files(snapshot); + let bins: Vec<(String, PathBuf)> = bin_entries.collect_bin_files(snapshot); for (bin_name, script_path) in bins { commands.insert( bin_name.clone(), @@ -323,7 +383,9 @@ fn resolve_custom_commands_from_deps( snapshot: &NpmResolutionSnapshot, get_package_path: impl Fn(&NpmResolutionPackage) -> PathBuf, ) -> Result { + let mut bin_entries = BinEntries::new(); resolve_custom_commands_from_packages( + &mut bin_entries, baseline, snapshot, package diff --git a/cli/npm/managed/resolvers/global.rs b/cli/npm/managed/resolvers/global.rs index 187e6b2774..f0193e78e9 100644 --- a/cli/npm/managed/resolvers/global.rs +++ b/cli/npm/managed/resolvers/global.rs @@ -11,7 +11,6 @@ use crate::colors; use async_trait::async_trait; use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; -use deno_core::url::Url; use deno_npm::NpmPackageCacheFolderId; use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; @@ -56,7 +55,7 @@ impl GlobalNpmPackageResolver { Self { registry_read_permission_checker: RegistryReadPermissionChecker::new( fs, - cache.root_folder(), + cache.root_dir_path().to_path_buf(), ), cache, tarball_cache, @@ -69,11 +68,7 @@ impl GlobalNpmPackageResolver { #[async_trait(?Send)] impl NpmPackageFsResolver for GlobalNpmPackageResolver { - fn root_dir_url(&self) -> &Url { - self.cache.root_dir_url() - } - - fn node_modules_path(&self) -> Option<&PathBuf> { + fn node_modules_path(&self) -> Option<&Path> { None } @@ -183,11 +178,11 @@ impl NpmPackageFsResolver for GlobalNpmPackageResolver { Ok(()) } - fn ensure_read_permission( + fn ensure_read_permission<'a>( &self, permissions: &mut dyn NodePermissions, - path: &Path, - ) -> Result<(), AnyError> { + path: &'a Path, + ) -> Result, AnyError> { self .registry_read_permission_checker .ensure_registry_read_permission(permissions, path) diff --git a/cli/npm/managed/resolvers/local.rs b/cli/npm/managed/resolvers/local.rs index 5a90f252de..50c5bd2689 100644 --- a/cli/npm/managed/resolvers/local.rs +++ b/cli/npm/managed/resolvers/local.rs @@ -19,6 +19,7 @@ use crate::args::LifecycleScriptsConfig; use crate::colors; use async_trait::async_trait; use deno_ast::ModuleSpecifier; +use deno_cache_dir::npm::mixed_case_package_name_decode; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures::stream::FuturesUnordered; @@ -30,6 +31,7 @@ use deno_npm::NpmPackageCacheFolderId; use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; use deno_npm::NpmSystemInfo; +use deno_resolver::npm::normalize_pkg_name_for_node_modules_deno_folder; use deno_runtime::deno_fs; use deno_runtime::deno_node::NodePermissions; use deno_semver::package::PackageNv; @@ -42,8 +44,6 @@ use serde::Serialize; use crate::args::NpmInstallDepsProvider; use crate::cache::CACHE_PERM; -use crate::npm::cache_dir::mixed_case_package_name_decode; -use crate::npm::cache_dir::mixed_case_package_name_encode; use crate::util::fs::atomic_write_file_with_retries; use crate::util::fs::canonicalize_path_maybe_not_exists_with_fs; use crate::util::fs::clone_dir_recursive; @@ -55,6 +55,7 @@ use crate::util::progress_bar::ProgressMessagePrompt; use super::super::cache::NpmCache; use super::super::cache::TarballCache; use super::super::resolution::NpmResolution; +use super::common::bin_entries; use super::common::NpmPackageFsResolver; use super::common::RegistryReadPermissionChecker; @@ -155,12 +156,8 @@ impl LocalNpmPackageResolver { #[async_trait(?Send)] impl NpmPackageFsResolver for LocalNpmPackageResolver { - fn root_dir_url(&self) -> &Url { - &self.root_node_modules_url - } - - fn node_modules_path(&self) -> Option<&PathBuf> { - Some(&self.root_node_modules_path) + fn node_modules_path(&self) -> Option<&Path> { + Some(self.root_node_modules_path.as_ref()) } fn maybe_package_folder(&self, id: &NpmPackageId) -> Option { @@ -257,11 +254,11 @@ impl NpmPackageFsResolver for LocalNpmPackageResolver { .await } - fn ensure_read_permission( + fn ensure_read_permission<'a>( &self, permissions: &mut dyn NodePermissions, - path: &Path, - ) -> Result<(), AnyError> { + path: &'a Path, + ) -> Result, AnyError> { self .registry_read_permission_checker .ensure_registry_read_permission(permissions, path) @@ -333,8 +330,7 @@ async fn sync_resolution_with_fs( let mut cache_futures = FuturesUnordered::new(); let mut newest_packages_by_name: HashMap<&String, &NpmResolutionPackage> = HashMap::with_capacity(package_partitions.packages.len()); - let bin_entries = - Rc::new(RefCell::new(super::common::bin_entries::BinEntries::new())); + let bin_entries = Rc::new(RefCell::new(bin_entries::BinEntries::new())); let mut lifecycle_scripts = super::common::lifecycle_scripts::LifecycleScripts::new( lifecycle_scripts, @@ -343,6 +339,14 @@ async fn sync_resolution_with_fs( }, ); let packages_with_deprecation_warnings = Arc::new(Mutex::new(Vec::new())); + + let mut package_tags: HashMap<&PackageNv, Vec<&str>> = HashMap::new(); + for (package_req, package_nv) in snapshot.package_reqs() { + if let Some(tag) = package_req.version_req.tag() { + package_tags.entry(package_nv).or_default().push(tag); + } + } + for package in &package_partitions.packages { if let Some(current_pkg) = newest_packages_by_name.get_mut(&package.id.nv.name) @@ -357,11 +361,29 @@ async fn sync_resolution_with_fs( let package_folder_name = get_package_folder_id_folder_name(&package.get_package_cache_folder_id()); let folder_path = deno_local_registry_dir.join(&package_folder_name); + let tags = package_tags + .get(&package.id.nv) + .map(|tags| tags.join(",")) + .unwrap_or_default(); + enum PackageFolderState { + UpToDate, + Uninitialized, + TagsOutdated, + } let initialized_file = folder_path.join(".initialized"); + let package_state = std::fs::read_to_string(&initialized_file) + .map(|s| { + if s != tags { + PackageFolderState::TagsOutdated + } else { + PackageFolderState::UpToDate + } + }) + .unwrap_or(PackageFolderState::Uninitialized); if !cache .cache_setting() .should_use_for_npm_package(&package.id.nv.name) - || !initialized_file.exists() + || matches!(package_state, PackageFolderState::Uninitialized) { // cache bust the dep from the dep setup cache so the symlinks // are forced to be recreated @@ -371,6 +393,7 @@ async fn sync_resolution_with_fs( let bin_entries_to_setup = bin_entries.clone(); let packages_with_deprecation_warnings = packages_with_deprecation_warnings.clone(); + cache_futures.push(async move { tarball_cache .ensure_package(&package.id.nv, &package.dist) @@ -389,7 +412,7 @@ async fn sync_resolution_with_fs( move || { clone_dir_recursive(&cache_folder, &package_path)?; // write out a file that indicates this folder has been initialized - fs::write(initialized_file, "")?; + fs::write(initialized_file, tags)?; Ok::<_, AnyError>(()) } @@ -410,6 +433,8 @@ async fn sync_resolution_with_fs( drop(pb_guard); // explicit for clarity Ok::<_, AnyError>(()) }); + } else if matches!(package_state, PackageFolderState::TagsOutdated) { + fs::write(initialized_file, tags)?; } let sub_node_modules = folder_path.join("node_modules"); @@ -518,9 +543,9 @@ async fn sync_resolution_with_fs( // linked into the root match found_names.entry(remote_alias) { Entry::Occupied(nv) => { - alias_clashes - || remote.req.name != nv.get().name // alias to a different package (in case of duplicate aliases) - || !remote.req.version_req.matches(&nv.get().version) // incompatible version + // alias to a different package (in case of duplicate aliases) + // or the version doesn't match the version in the root node_modules + alias_clashes || &remote_pkg.id.nv != *nv.get() } Entry::Vacant(entry) => { entry.insert(&remote_pkg.id.nv); @@ -633,7 +658,28 @@ async fn sync_resolution_with_fs( // 7. Set up `node_modules/.bin` entries for packages that need it. { let bin_entries = std::mem::take(&mut *bin_entries.borrow_mut()); - bin_entries.finish(snapshot, &bin_node_modules_dir_path)?; + bin_entries.finish( + snapshot, + &bin_node_modules_dir_path, + |setup_outcome| { + match setup_outcome { + bin_entries::EntrySetupOutcome::MissingEntrypoint { + package, + package_path, + .. + } if super::common::lifecycle_scripts::has_lifecycle_scripts( + package, + package_path, + ) && lifecycle_scripts.can_run_scripts(&package.id.nv) + && !lifecycle_scripts.has_run_scripts(package) => + { + // ignore, it might get fixed when the lifecycle scripts run. + // if not, we'll warn then + } + outcome => outcome.warn_if_failed(), + } + }, + )?; } // 8. Create symlinks for the workspace packages @@ -683,7 +729,8 @@ async fn sync_resolution_with_fs( .finish( snapshot, &package_partitions.packages, - Some(root_node_modules_dir_path), + root_node_modules_dir_path, + progress_bar, ) .await?; @@ -920,20 +967,6 @@ impl SetupCache { } } -/// Normalizes a package name for use at `node_modules/.deno/@[_]` -pub fn normalize_pkg_name_for_node_modules_deno_folder(name: &str) -> Cow { - let name = if name.to_lowercase() == name { - Cow::Borrowed(name) - } else { - Cow::Owned(format!("_{}", mixed_case_package_name_encode(name))) - }; - if name.starts_with('@') { - name.replace('/', "+").into() - } else { - name - } -} - fn get_package_folder_id_folder_name( folder_id: &NpmPackageCacheFolderId, ) -> String { @@ -1023,12 +1056,18 @@ fn junction_or_symlink_dir( if symlink_err.kind() == std::io::ErrorKind::PermissionDenied => { USE_JUNCTIONS.store(true, std::sync::atomic::Ordering::Relaxed); - junction::create(old_path, new_path).map_err(Into::into) + junction::create(old_path, new_path) + .context("Failed creating junction in node_modules folder") + } + Err(symlink_err) => { + log::warn!( + "{} Unexpected error symlinking node_modules: {symlink_err}", + colors::yellow("Warning") + ); + USE_JUNCTIONS.store(true, std::sync::atomic::Ordering::Relaxed); + junction::create(old_path, new_path) + .context("Failed creating junction in node_modules folder") } - Err(symlink_err) => Err( - AnyError::from(symlink_err) - .context("Failed creating symlink in node_modules folder"), - ), } } diff --git a/cli/npm/managed/resolvers/mod.rs b/cli/npm/managed/resolvers/mod.rs index 234a6e4dba..36d795ee7e 100644 --- a/cli/npm/managed/resolvers/mod.rs +++ b/cli/npm/managed/resolvers/mod.rs @@ -15,7 +15,6 @@ use crate::args::NpmInstallDepsProvider; use crate::util::progress_bar::ProgressBar; pub use self::common::NpmPackageFsResolver; -pub use self::local::normalize_pkg_name_for_node_modules_deno_folder; use self::global::GlobalNpmPackageResolver; use self::local::LocalNpmPackageResolver; diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index 15ac8ebb28..0e955ac5b4 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -1,37 +1,44 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. mod byonm; -mod cache_dir; mod common; mod managed; -use std::path::PathBuf; +use std::borrow::Cow; +use std::path::Path; use std::sync::Arc; +use common::maybe_auth_header_for_npm_registry; use dashmap::DashMap; -use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::serde_json; +use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::registry::NpmPackageInfo; -use deno_runtime::deno_node::NodeRequireResolver; +use deno_resolver::npm::ByonmInNpmPackageChecker; +use deno_resolver::npm::ByonmNpmResolver; +use deno_resolver::npm::CliNpmReqResolver; +use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; +use deno_runtime::deno_node::NodePermissions; use deno_runtime::ops::process::NpmProcessStateProvider; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; -use node_resolver::NpmResolver; +use managed::cache::registry_info::get_package_url; +use managed::create_managed_in_npm_pkg_checker; +use node_resolver::InNpmPackageChecker; +use node_resolver::NpmPackageFolderResolver; -use crate::args::npm_registry_url; use crate::file_fetcher::FileFetcher; -pub use self::byonm::ByonmCliNpmResolver; -pub use self::byonm::CliNpmResolverByonmCreateOptions; -pub use self::cache_dir::NpmCacheDir; -pub use self::managed::CliNpmResolverManagedCreateOptions; +pub use self::byonm::CliByonmNpmResolver; +pub use self::byonm::CliByonmNpmResolverCreateOptions; +pub use self::managed::CliManagedInNpmPkgCheckerCreateOptions; +pub use self::managed::CliManagedNpmResolverCreateOptions; pub use self::managed::CliNpmResolverManagedSnapshotOption; pub use self::managed::ManagedCliNpmResolver; pub enum CliNpmResolverCreateOptions { - Managed(CliNpmResolverManagedCreateOptions), - Byonm(CliNpmResolverByonmCreateOptions), + Managed(CliManagedNpmResolverCreateOptions), + Byonm(CliByonmNpmResolverCreateOptions), } pub async fn create_cli_npm_resolver_for_lsp( @@ -42,7 +49,7 @@ pub async fn create_cli_npm_resolver_for_lsp( Managed(options) => { managed::create_managed_npm_resolver_for_lsp(options).await } - Byonm(options) => byonm::create_byonm_npm_resolver(options), + Byonm(options) => Arc::new(ByonmNpmResolver::new(options)), } } @@ -52,22 +59,43 @@ pub async fn create_cli_npm_resolver( use CliNpmResolverCreateOptions::*; match options { Managed(options) => managed::create_managed_npm_resolver(options).await, - Byonm(options) => Ok(byonm::create_byonm_npm_resolver(options)), + Byonm(options) => Ok(Arc::new(ByonmNpmResolver::new(options))), + } +} + +pub enum CreateInNpmPkgCheckerOptions<'a> { + Managed(CliManagedInNpmPkgCheckerCreateOptions<'a>), + Byonm, +} + +pub fn create_in_npm_pkg_checker( + options: CreateInNpmPkgCheckerOptions, +) -> Arc { + match options { + CreateInNpmPkgCheckerOptions::Managed(options) => { + create_managed_in_npm_pkg_checker(options) + } + CreateInNpmPkgCheckerOptions::Byonm => Arc::new(ByonmInNpmPackageChecker), } } pub enum InnerCliNpmResolverRef<'a> { Managed(&'a ManagedCliNpmResolver), #[allow(dead_code)] - Byonm(&'a ByonmCliNpmResolver), + Byonm(&'a CliByonmNpmResolver), } -pub trait CliNpmResolver: NpmResolver { - fn into_npm_resolver(self: Arc) -> Arc; - fn into_require_resolver(self: Arc) -> Arc; +pub trait CliNpmResolver: NpmPackageFolderResolver + CliNpmReqResolver { + fn into_npm_pkg_folder_resolver( + self: Arc, + ) -> Arc; + fn into_npm_req_resolver(self: Arc) -> Arc; fn into_process_state_provider( self: Arc, ) -> Arc; + fn into_maybe_byonm(self: Arc) -> Option> { + None + } fn clone_snapshotted(&self) -> Arc; @@ -80,20 +108,20 @@ pub trait CliNpmResolver: NpmResolver { } } - fn as_byonm(&self) -> Option<&ByonmCliNpmResolver> { + fn as_byonm(&self) -> Option<&CliByonmNpmResolver> { match self.as_inner() { InnerCliNpmResolverRef::Managed(_) => None, InnerCliNpmResolverRef::Byonm(inner) => Some(inner), } } - fn root_node_modules_path(&self) -> Option<&PathBuf>; + fn root_node_modules_path(&self) -> Option<&Path>; - fn resolve_pkg_folder_from_deno_module_req( + fn ensure_read_permission<'a>( &self, - req: &PackageReq, - referrer: &ModuleSpecifier, - ) -> Result; + permissions: &mut dyn NodePermissions, + path: &'a Path, + ) -> Result, AnyError>; /// Returns a hash returning the state of the npm resolver /// or `None` if the state currently can't be determined. @@ -105,14 +133,19 @@ pub struct NpmFetchResolver { nv_by_req: DashMap>, info_by_name: DashMap>>, file_fetcher: Arc, + npmrc: Arc, } impl NpmFetchResolver { - pub fn new(file_fetcher: Arc) -> Self { + pub fn new( + file_fetcher: Arc, + npmrc: Arc, + ) -> Self { Self { nv_by_req: Default::default(), info_by_name: Default::default(), file_fetcher, + npmrc, } } @@ -147,11 +180,21 @@ impl NpmFetchResolver { return info.value().clone(); } let fetch_package_info = || async { - let info_url = npm_registry_url().join(name).ok()?; + let info_url = get_package_url(&self.npmrc, name); let file_fetcher = self.file_fetcher.clone(); + let registry_config = self.npmrc.get_registry_config(name); + // TODO(bartlomieju): this should error out, not use `.ok()`. + let maybe_auth_header = + maybe_auth_header_for_npm_registry(registry_config).ok()?; // spawn due to the lsp's `Send` requirement let file = deno_core::unsync::spawn(async move { - file_fetcher.fetch_bypass_permissions(&info_url).await.ok() + file_fetcher + .fetch_bypass_permissions_with_maybe_auth( + &info_url, + maybe_auth_header, + ) + .await + .ok() }) .await .ok()??; @@ -162,3 +205,15 @@ impl NpmFetchResolver { info } } + +pub const NPM_CONFIG_USER_AGENT_ENV_VAR: &str = "npm_config_user_agent"; + +pub fn get_npm_config_user_agent() -> String { + format!( + "deno/{} npm/? deno/{} {} {}", + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH + ) +} diff --git a/cli/ops/bench.rs b/cli/ops/bench.rs index edd8c118ca..1f4a4bd9b5 100644 --- a/cli/ops/bench.rs +++ b/cli/ops/bench.rs @@ -2,8 +2,6 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time; use deno_core::error::generic_error; use deno_core::error::type_error; @@ -12,10 +10,9 @@ use deno_core::op2; use deno_core::v8; use deno_core::ModuleSpecifier; use deno_core::OpState; -use deno_runtime::deno_permissions::create_child_permissions; use deno_runtime::deno_permissions::ChildPermissionsArg; -use deno_runtime::deno_permissions::PermissionDescriptorParser; use deno_runtime::deno_permissions::PermissionsContainer; +use deno_runtime::deno_web::StartTime; use tokio::sync::mpsc::UnboundedSender; use uuid::Uuid; @@ -59,21 +56,10 @@ struct PermissionsHolder(Uuid, PermissionsContainer); pub fn op_pledge_test_permissions( state: &mut OpState, #[serde] args: ChildPermissionsArg, -) -> Result { +) -> Result { let token = Uuid::new_v4(); - let permission_desc_parser = state - .borrow::>() - .clone(); let parent_permissions = state.borrow_mut::(); - let worker_permissions = { - let mut parent_permissions = parent_permissions.inner.lock(); - let perms = create_child_permissions( - permission_desc_parser.as_ref(), - &mut parent_permissions, - args, - )?; - PermissionsContainer::new(permission_desc_parser, perms) - }; + let worker_permissions = parent_permissions.create_child_permissions(args)?; let parent_permissions = parent_permissions.clone(); if state.try_take::().is_some() { @@ -83,7 +69,6 @@ pub fn op_pledge_test_permissions( state.put::(PermissionsHolder(token, parent_permissions)); // NOTE: This call overrides current permission set for the worker - state.put(worker_permissions.inner.clone()); state.put::(worker_permissions); Ok(token) @@ -100,7 +85,6 @@ pub fn op_restore_test_permissions( } let permissions = permissions_holder.1; - state.put(permissions.inner.clone()); state.put::(permissions); Ok(()) } else { @@ -163,8 +147,8 @@ fn op_dispatch_bench_event(state: &mut OpState, #[serde] event: BenchEvent) { #[op2(fast)] #[number] -fn op_bench_now(state: &mut OpState) -> Result { - let ns = state.borrow::().elapsed().as_nanos(); +fn op_bench_now(state: &mut OpState) -> Result { + let ns = state.borrow::().elapsed().as_nanos(); let ns_u64 = u64::try_from(ns)?; Ok(ns_u64) } diff --git a/cli/ops/jupyter.rs b/cli/ops/jupyter.rs index f7f006d9bd..5bdf97e60f 100644 --- a/cli/ops/jupyter.rs +++ b/cli/ops/jupyter.rs @@ -46,7 +46,7 @@ pub fn op_jupyter_input( state: &mut OpState, #[string] prompt: String, is_password: bool, -) -> Result, AnyError> { +) -> Option { let (last_execution_request, stdin_connection_proxy) = { ( state.borrow::>>>().clone(), @@ -58,11 +58,11 @@ pub fn op_jupyter_input( if let Some(last_request) = maybe_last_request { let JupyterMessageContent::ExecuteRequest(msg) = &last_request.content else { - return Ok(None); + return None; }; if !msg.allow_stdin { - return Ok(None); + return None; } let content = InputRequest { @@ -73,7 +73,7 @@ pub fn op_jupyter_input( let msg = JupyterMessage::new(content, Some(&last_request)); let Ok(()) = stdin_connection_proxy.lock().tx.send(msg) else { - return Ok(None); + return None; }; // Need to spawn a separate thread here, because `blocking_recv()` can't @@ -82,17 +82,25 @@ pub fn op_jupyter_input( stdin_connection_proxy.lock().rx.blocking_recv() }); let Ok(Some(response)) = join_handle.join() else { - return Ok(None); + return None; }; let JupyterMessageContent::InputReply(msg) = response.content else { - return Ok(None); + return None; }; - return Ok(Some(msg.value)); + return Some(msg.value); } - Ok(None) + None +} + +#[derive(Debug, thiserror::Error)] +pub enum JupyterBroadcastError { + #[error(transparent)] + SerdeJson(serde_json::Error), + #[error(transparent)] + ZeroMq(AnyError), } #[op2(async)] @@ -102,7 +110,7 @@ pub async fn op_jupyter_broadcast( #[serde] content: serde_json::Value, #[serde] metadata: serde_json::Value, #[serde] buffers: Vec, -) -> Result<(), AnyError> { +) -> Result<(), JupyterBroadcastError> { let (iopub_connection, last_execution_request) = { let s = state.borrow(); @@ -125,36 +133,35 @@ pub async fn op_jupyter_broadcast( content, err ); - err + JupyterBroadcastError::SerdeJson(err) })?; let jupyter_message = JupyterMessage::new(content, Some(&last_request)) .with_metadata(metadata) .with_buffers(buffers.into_iter().map(|b| b.to_vec().into()).collect()); - iopub_connection.lock().send(jupyter_message).await?; + iopub_connection + .lock() + .send(jupyter_message) + .await + .map_err(JupyterBroadcastError::ZeroMq)?; } Ok(()) } #[op2(fast)] -pub fn op_print( - state: &mut OpState, - #[string] msg: &str, - is_err: bool, -) -> Result<(), AnyError> { +pub fn op_print(state: &mut OpState, #[string] msg: &str, is_err: bool) { let sender = state.borrow_mut::>(); if is_err { if let Err(err) = sender.send(StreamContent::stderr(msg)) { log::error!("Failed to send stderr message: {}", err); } - return Ok(()); + return; } if let Err(err) = sender.send(StreamContent::stdout(msg)) { log::error!("Failed to send stdout message: {}", err); } - Ok(()) } diff --git a/cli/ops/testing.rs b/cli/ops/testing.rs index 6a8d310062..00aafb8286 100644 --- a/cli/ops/testing.rs +++ b/cli/ops/testing.rs @@ -16,13 +16,10 @@ use deno_core::op2; use deno_core::v8; use deno_core::ModuleSpecifier; use deno_core::OpState; -use deno_runtime::deno_permissions::create_child_permissions; use deno_runtime::deno_permissions::ChildPermissionsArg; -use deno_runtime::deno_permissions::PermissionDescriptorParser; use deno_runtime::deno_permissions::PermissionsContainer; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use std::sync::Arc; use uuid::Uuid; deno_core::extension!(deno_test, @@ -54,21 +51,10 @@ struct PermissionsHolder(Uuid, PermissionsContainer); pub fn op_pledge_test_permissions( state: &mut OpState, #[serde] args: ChildPermissionsArg, -) -> Result { +) -> Result { let token = Uuid::new_v4(); - let permission_desc_parser = state - .borrow::>() - .clone(); let parent_permissions = state.borrow_mut::(); - let worker_permissions = { - let mut parent_permissions = parent_permissions.inner.lock(); - let perms = create_child_permissions( - permission_desc_parser.as_ref(), - &mut parent_permissions, - args, - )?; - PermissionsContainer::new(permission_desc_parser, perms) - }; + let worker_permissions = parent_permissions.create_child_permissions(args)?; let parent_permissions = parent_permissions.clone(); if state.try_take::().is_some() { @@ -77,7 +63,6 @@ pub fn op_pledge_test_permissions( state.put::(PermissionsHolder(token, parent_permissions)); // NOTE: This call overrides current permission set for the worker - state.put(worker_permissions.inner.clone()); state.put::(worker_permissions); Ok(token) @@ -94,7 +79,6 @@ pub fn op_restore_test_permissions( } let permissions = permissions_holder.1; - state.put(permissions.inner.clone()); state.put::(permissions); Ok(()) } else { @@ -166,7 +150,7 @@ fn op_register_test_step( #[smi] parent_id: usize, #[smi] root_id: usize, #[string] root_name: String, -) -> Result { +) -> usize { let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); let origin = state.borrow::().to_string(); let description = TestStepDescription { @@ -185,7 +169,7 @@ fn op_register_test_step( }; let sender = state.borrow_mut::(); sender.send(TestEvent::StepRegister(description)).ok(); - Ok(id) + id } #[op2(fast)] diff --git a/cli/resolver.rs b/cli/resolver.rs index 07cb6931df..a2dd47430f 100644 --- a/cli/resolver.rs +++ b/cli/resolver.rs @@ -4,55 +4,55 @@ use async_trait::async_trait; use dashmap::DashMap; use dashmap::DashSet; use deno_ast::MediaType; -use deno_config::workspace::MappedResolution; use deno_config::workspace::MappedResolutionDiagnostic; use deno_config::workspace::MappedResolutionError; -use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::anyhow; use deno_core::anyhow::Context; use deno_core::error::AnyError; +use deno_core::url::Url; use deno_core::ModuleSourceCode; use deno_core::ModuleSpecifier; use deno_graph::source::ResolutionMode; use deno_graph::source::ResolveError; -use deno_graph::source::Resolver; use deno_graph::source::UnknownBuiltInNodeModuleError; -use deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE; use deno_graph::NpmLoadError; use deno_graph::NpmResolvePkgReqsResult; use deno_npm::resolution::NpmResolutionError; -use deno_package_json::PackageJsonDepValue; +use deno_resolver::sloppy_imports::SloppyImportsResolver; use deno_runtime::colors; use deno_runtime::deno_fs; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::is_builtin_node_module; -use deno_runtime::deno_node::NodeResolver; -use deno_runtime::fs_util::specifier_to_file_path; -use deno_semver::npm::NpmPackageReqReference; +use deno_runtime::deno_node::DenoFsNodeResolverEnv; use deno_semver::package::PackageReq; -use node_resolver::errors::ClosestPkgJsonError; -use node_resolver::errors::NodeResolveError; -use node_resolver::errors::NodeResolveErrorKind; -use node_resolver::errors::PackageFolderResolveErrorKind; -use node_resolver::errors::PackageFolderResolveIoError; -use node_resolver::errors::PackageNotFoundError; -use node_resolver::errors::PackageResolveErrorKind; -use node_resolver::errors::UrlToNodeResolutionError; use node_resolver::NodeModuleKind; -use node_resolver::NodeResolution; use node_resolver::NodeResolutionMode; -use node_resolver::PackageJson; use std::borrow::Cow; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use thiserror::Error; -use crate::args::JsxImportSourceConfig; use crate::args::DENO_DISABLE_PEDANTIC_NODE_WARNINGS; use crate::node::CliNodeCodeTranslator; use crate::npm::CliNpmResolver; use crate::npm::InnerCliNpmResolverRef; use crate::util::sync::AtomicFlag; +use crate::util::text_encoding::from_utf8_lossy_owned; + +pub type CjsTracker = deno_resolver::cjs::CjsTracker; +pub type IsCjsResolver = + deno_resolver::cjs::IsCjsResolver; +pub type IsCjsResolverOptions = deno_resolver::cjs::IsCjsResolverOptions; +pub type CliSloppyImportsResolver = + SloppyImportsResolver; +pub type CliDenoResolver = deno_resolver::DenoResolver< + CliDenoResolverFs, + DenoFsNodeResolverEnv, + SloppyImportsCachedFs, +>; +pub type CliNpmReqResolver = + deno_resolver::npm::NpmReqResolver; pub struct ModuleCodeStringSource { pub code: ModuleSourceCode, @@ -60,279 +60,75 @@ pub struct ModuleCodeStringSource { 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, -} +#[derive(Debug, Clone)] +pub struct CliDenoResolverFs(pub 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, - } +impl deno_resolver::fs::DenoResolverFs for CliDenoResolverFs { + fn read_to_string_lossy(&self, path: &Path) -> std::io::Result { + self + .0 + .read_text_file_lossy_sync(path, None) + .map_err(|e| e.into_io_error()) } - pub fn in_npm_package(&self, specifier: &ModuleSpecifier) -> bool { - self.npm_resolver.in_npm_package(specifier) + fn realpath_sync(&self, path: &Path) -> std::io::Result { + self.0.realpath_sync(path).map_err(|e| e.into_io_error()) } - pub fn get_closest_package_json( + fn exists_sync(&self, path: &Path) -> bool { + self.0.exists_sync(path) + } + + fn is_dir_sync(&self, path: &Path) -> bool { + self.0.is_dir_sync(path) + } + + fn read_dir_sync( &self, - referrer: &ModuleSpecifier, - ) -> Result>, ClosestPkgJsonError> { - self.node_resolver.get_closest_package_json(referrer) - } - - pub fn resolve_if_for_npm_pkg( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result, AnyError> { - let resolution_result = self.resolve(specifier, referrer, mode); - match resolution_result { - Ok(res) => Ok(Some(res)), - Err(err) => { - let err = err.into_kind(); - match err { - NodeResolveErrorKind::RelativeJoin(_) - | NodeResolveErrorKind::PackageImportsResolve(_) - | NodeResolveErrorKind::UnsupportedEsmUrlScheme(_) - | NodeResolveErrorKind::DataUrlReferrer(_) - | NodeResolveErrorKind::TypesNotFound(_) - | NodeResolveErrorKind::FinalizeResolution(_) - | NodeResolveErrorKind::UrlToNodeResolution(_) => Err(err.into()), - NodeResolveErrorKind::PackageResolve(err) => { - let err = err.into_kind(); - match err { - PackageResolveErrorKind::ClosestPkgJson(_) - | PackageResolveErrorKind::InvalidModuleSpecifier(_) - | PackageResolveErrorKind::ExportsResolve(_) - | PackageResolveErrorKind::SubpathResolve(_) => Err(err.into()), - PackageResolveErrorKind::PackageFolderResolve(err) => { - match err.as_kind() { - PackageFolderResolveErrorKind::Io( - PackageFolderResolveIoError { package_name, .. }, - ) - | PackageFolderResolveErrorKind::PackageNotFound( - PackageNotFoundError { package_name, .. }, - ) => { - if self.in_npm_package(referrer) { - return Err(err.into()); - } - if let Some(byonm_npm_resolver) = - self.npm_resolver.as_byonm() - { - if byonm_npm_resolver - .find_ancestor_package_json_with_dep( - package_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 `deno install`?" - ), - specifier - )); - } - } - Ok(None) - } - PackageFolderResolveErrorKind::ReferrerNotFound(_) => { - if self.in_npm_package(referrer) { - return Err(err.into()); - } - Ok(None) - } - } - } - } - } - } - } - } - } - - pub fn resolve( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - mode: NodeResolutionMode, - ) -> Result { - let referrer_kind = if self.cjs_resolutions.contains(referrer) { - NodeModuleKind::Cjs - } else { - NodeModuleKind::Esm - }; - - let res = - self - .node_resolver - .resolve(specifier, referrer, referrer_kind, mode)?; - Ok(self.handle_node_resolution(res)) - } - - 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 resolution_result = self.resolve_package_sub_path_from_deno_module( - &package_folder, - sub_path, - Some(referrer), - mode, - ); - match resolution_result { - Ok(resolution) => Ok(resolution), - Err(err) => { - 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 `deno install`?", - package_json_path.display(), - )); - } - } - Err(err) - } - } - } - - pub fn resolve_package_sub_path_from_deno_module( - &self, - package_folder: &Path, - sub_path: Option<&str>, - maybe_referrer: Option<&ModuleSpecifier>, - mode: NodeResolutionMode, - ) -> Result { - let res = self - .node_resolver - .resolve_package_subpath_from_deno_module( - package_folder, - sub_path, - maybe_referrer, - mode, - )?; - Ok(self.handle_node_resolution(res)) - } - - pub fn handle_if_in_node_modules( - &self, - specifier: &ModuleSpecifier, - ) -> Result, AnyError> { - // 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 = - crate::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(Some(resolution.into_url())); - } - } - - Ok(None) - } - - pub fn url_to_node_resolution( - &self, - specifier: ModuleSpecifier, - ) -> Result { - self.node_resolver.url_to_node_resolution(specifier) - } - - fn handle_node_resolution( - &self, - resolution: NodeResolution, - ) -> NodeResolution { - if let NodeResolution::CommonJs(specifier) = &resolution { - // remember that this was a common js resolution - self.cjs_resolutions.insert(specifier.clone()); - } - resolution + dir_path: &Path, + ) -> std::io::Result> { + self + .0 + .read_dir_sync(dir_path) + .map(|entries| { + entries + .into_iter() + .map(|e| deno_resolver::fs::DirEntry { + name: e.name, + is_file: e.is_file, + is_directory: e.is_directory, + }) + .collect::>() + }) + .map_err(|err| err.into_io_error()) } } +#[derive(Debug, Error)] +#[error("{media_type} files are not supported in npm packages: {specifier}")] +pub struct NotSupportedKindInNpmError { + pub media_type: MediaType, + pub specifier: Url, +} + +// todo(dsherret): move to module_loader.rs (it seems to be here due to use in standalone) #[derive(Clone)] pub struct NpmModuleLoader { - cjs_resolutions: Arc, - node_code_translator: Arc, + cjs_tracker: Arc, fs: Arc, - node_resolver: Arc, + node_code_translator: Arc, } impl NpmModuleLoader { pub fn new( - cjs_resolutions: Arc, - node_code_translator: Arc, + cjs_tracker: Arc, fs: Arc, - node_resolver: Arc, + node_code_translator: Arc, ) -> Self { Self { - cjs_resolutions, + cjs_tracker, 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) - || (specifier.scheme() == "file" && specifier.path().ends_with(".cjs")) - { - Some(self.load(specifier, maybe_referrer).await) - } else { - None } } @@ -378,27 +174,30 @@ impl NpmModuleLoader { } })?; - let code = if self.cjs_resolutions.contains(specifier) - || (specifier.scheme() == "file" && specifier.path().ends_with(".cjs")) - { + let media_type = MediaType::from_specifier(specifier); + if media_type.is_emittable() { + return Err(AnyError::from(NotSupportedKindInNpmError { + media_type, + specifier: specifier.clone(), + })); + } + + let code = if self.cjs_tracker.is_maybe_cjs(specifier, media_type)? { // 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) }, - }; + let code = from_utf8_lossy_owned(code); ModuleSourceCode::String( self .node_code_translator - .translate_cjs_to_esm(specifier, Some(code)) + .translate_cjs_to_esm(specifier, Some(Cow::Owned(code))) .await? + .into_owned() .into(), ) } else { // esm and json code is untouched ModuleSourceCode::Bytes(code.into_boxed_slice().into()) }; + Ok(ModuleCodeStringSource { code, found_url: specifier.clone(), @@ -407,78 +206,36 @@ impl NpmModuleLoader { } } -/// 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); - } +pub struct CliResolverOptions { + pub deno_resolver: Arc, + pub npm_resolver: Option>, + pub bare_node_builtins_enabled: bool, } /// A resolver that takes care of resolution, taking into account loaded /// import map, JSX settings. #[derive(Debug)] -pub struct CliGraphResolver { - node_resolver: Option>, +pub struct CliResolver { + deno_resolver: Arc, npm_resolver: Option>, - sloppy_imports_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, warned_pkgs: DashSet, } -pub struct CliGraphResolverOptions<'a> { - pub node_resolver: Option>, - pub npm_resolver: Option>, - pub sloppy_imports_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 { +impl CliResolver { + pub fn new(options: CliResolverOptions) -> Self { Self { - node_resolver: options.node_resolver, + deno_resolver: options.deno_resolver, npm_resolver: options.npm_resolver, - sloppy_imports_resolver: options.sloppy_imports_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, warned_pkgs: Default::default(), } } - pub fn as_graph_resolver(&self) -> &dyn Resolver { - self - } - + // todo(dsherret): move this off CliResolver as CliResolver is acting + // like a factory by doing this (it's beyond its responsibility) pub fn create_graph_npm_resolver(&self) -> WorkerCliNpmGraphResolver { WorkerCliNpmGraphResolver { npm_resolver: self.npm_resolver.as_ref(), @@ -486,28 +243,12 @@ impl CliGraphResolver { bare_node_builtins_enabled: self.bare_node_builtins_enabled, } } -} -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( + pub fn resolve( &self, raw_specifier: &str, referrer_range: &deno_graph::Range, + referrer_kind: NodeModuleKind, mode: ResolutionMode, ) -> Result { fn to_node_mode(mode: ResolutionMode) -> NodeResolutionMode { @@ -517,210 +258,50 @@ impl Resolver for CliGraphResolver { } } - let referrer = &referrer_range.specifier; + let resolution = self + .deno_resolver + .resolve( + raw_specifier, + &referrer_range.specifier, + referrer_kind, + to_node_mode(mode), + ) + .map_err(|err| match err.into_kind() { + deno_resolver::DenoResolveErrorKind::MappedResolution( + mapped_resolution_error, + ) => match mapped_resolution_error { + MappedResolutionError::Specifier(e) => ResolveError::Specifier(e), + // deno_graph checks specifically for an ImportMapError + MappedResolutionError::ImportMap(e) => ResolveError::Other(e.into()), + err => ResolveError::Other(err.into()), + }, + err => ResolveError::Other(err.into()), + })?; - // Use node resolution if we're in an npm package - if let Some(node_resolver) = self.node_resolver.as_ref() { - if referrer.scheme() == "file" && node_resolver.in_npm_package(referrer) { - return node_resolver - .resolve(raw_specifier, referrer, to_node_mode(mode)) - .map(|res| res.into_url()) - .map_err(|e| ResolveError::Other(e.into())); - } + if resolution.found_package_json_dep { + // mark that we need to do an "npm install" later + self.found_package_json_dep_flag.raise(); } - // Attempt to resolve with the workspace resolver - let result: Result<_, ResolveError> = self - .workspace_resolver - .resolve(raw_specifier, referrer) - .map_err(|err| match err { - MappedResolutionError::Specifier(err) => ResolveError::Specifier(err), - MappedResolutionError::ImportMap(err) => { - ResolveError::Other(err.into()) - } - MappedResolutionError::Workspace(err) => { - ResolveError::Other(err.into()) - } - }); - let result = match result { - Ok(resolution) => match resolution { - MappedResolution::Normal { - specifier, - maybe_diagnostic, - } - | MappedResolution::ImportMap { - specifier, - maybe_diagnostic, - } => { - if let Some(diagnostic) = maybe_diagnostic { - match &*diagnostic { - MappedResolutionDiagnostic::ConstraintNotMatchedLocalVersion { reference, .. } => { - if self.warned_pkgs.insert(reference.req().clone()) { - log::warn!("{} {}\n at {}", colors::yellow("Warning"), diagnostic, referrer_range); - } - } - } - } - // do sloppy imports resolution if enabled - if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver { - Ok( - sloppy_imports_resolver - .resolve(&specifier, mode) - .map(|s| s.into_specifier()) - .unwrap_or(specifier), - ) - } else { - Ok(specifier) - } - } - MappedResolution::WorkspaceJsrPackage { specifier, .. } => { - Ok(specifier) - } - MappedResolution::WorkspaceNpmPackage { - target_pkg_json: pkg_json, - sub_path, - .. - } => self - .node_resolver - .as_ref() - .unwrap() - .resolve_package_sub_path_from_deno_module( - pkg_json.dir_path(), - sub_path.as_deref(), - Some(referrer), - to_node_mode(mode), - ) - .map_err(ResolveError::Other) - .map(|res| res.into_url()), - MappedResolution::PackageJson { - dep_result, - alias, - sub_path, + if let Some(diagnostic) = resolution.maybe_diagnostic { + match &*diagnostic { + MappedResolutionDiagnostic::ConstraintNotMatchedLocalVersion { + reference, .. } => { - // 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), - }; - - // 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."))); - } - } - } - - let Some(node_resolver) = &self.node_resolver else { - return result; - }; - - let is_byonm = self - .npm_resolver - .as_ref() - .is_some_and(|r| r.as_byonm().is_some()); - match result { - Ok(specifier) => { - if let Ok(npm_req_ref) = - NpmPackageReqReference::from_specifier(&specifier) - { - // check if the npm specifier resolves to a workspace member - if let Some(pkg_folder) = self - .workspace_resolver - .resolve_workspace_pkg_json_folder_for_npm_specifier( - npm_req_ref.req(), - ) - { - return Ok( - node_resolver - .resolve_package_sub_path_from_deno_module( - pkg_folder, - npm_req_ref.sub_path(), - Some(referrer), - to_node_mode(mode), - )? - .into_url(), + if self.warned_pkgs.insert(reference.req().clone()) { + log::warn!( + "{} {}\n at {}", + colors::yellow("Warning"), + diagnostic, + referrer_range ); } - - // do npm resolution for byonm - if is_byonm { - return node_resolver - .resolve_req_reference(&npm_req_ref, referrer, to_node_mode(mode)) - .map(|res| res.into_url()) - .map_err(|err| err.into()); - } } - - Ok(match node_resolver.handle_if_in_node_modules(&specifier)? { - Some(specifier) => specifier, - None => specifier, - }) - } - Err(err) => { - // If byonm, check if the bare specifier resolves to an npm package - if is_byonm && referrer.scheme() == "file" { - let maybe_resolution = node_resolver - .resolve_if_for_npm_pkg(raw_specifier, referrer, to_node_mode(mode)) - .map_err(ResolveError::Other)?; - if let Some(res) = maybe_resolution { - match res { - NodeResolution::Esm(url) | NodeResolution::CommonJs(url) => { - return Ok(url) - } - NodeResolution::BuiltIn(_) => { - // don't resolve bare specifiers for built-in modules via node resolution - } - } - } - } - - Err(err) } } + + Ok(resolution.url) } } @@ -847,96 +428,18 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SloppyImportsFsEntry { - File, - Dir, -} - -impl SloppyImportsFsEntry { - pub fn from_fs_stat( - stat: &deno_runtime::deno_io::fs::FsStat, - ) -> Option { - if stat.is_file { - Some(SloppyImportsFsEntry::File) - } else if stat.is_directory { - Some(SloppyImportsFsEntry::Dir) - } else { - None - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SloppyImportsResolution { - /// Ex. `./file.js` to `./file.ts` - JsToTs(ModuleSpecifier), - /// Ex. `./file` to `./file.ts` - NoExtension(ModuleSpecifier), - /// Ex. `./dir` to `./dir/index.ts` - Directory(ModuleSpecifier), -} - -impl SloppyImportsResolution { - pub fn as_specifier(&self) -> &ModuleSpecifier { - match self { - Self::JsToTs(specifier) => specifier, - Self::NoExtension(specifier) => specifier, - Self::Directory(specifier) => specifier, - } - } - - pub fn into_specifier(self) -> ModuleSpecifier { - match self { - Self::JsToTs(specifier) => specifier, - Self::NoExtension(specifier) => specifier, - Self::Directory(specifier) => specifier, - } - } - - pub fn as_suggestion_message(&self) -> String { - format!("Maybe {}", self.as_base_message()) - } - - pub fn as_quick_fix_message(&self) -> String { - let message = self.as_base_message(); - let mut chars = message.chars(); - format!( - "{}{}.", - chars.next().unwrap().to_uppercase(), - chars.as_str() - ) - } - - fn as_base_message(&self) -> String { - match self { - SloppyImportsResolution::JsToTs(specifier) => { - let media_type = MediaType::from_specifier(specifier); - format!("change the extension to '{}'", media_type.as_ts_extension()) - } - SloppyImportsResolution::NoExtension(specifier) => { - let media_type = MediaType::from_specifier(specifier); - format!("add a '{}' extension", media_type.as_ts_extension()) - } - SloppyImportsResolution::Directory(specifier) => { - let file_name = specifier - .path() - .rsplit_once('/') - .map(|(_, file_name)| file_name) - .unwrap_or(specifier.path()); - format!("specify path to '{}' file in directory instead", file_name) - } - } - } -} - #[derive(Debug)] -pub struct SloppyImportsResolver { - fs: Arc, - cache: Option>>, +pub struct SloppyImportsCachedFs { + fs: Arc, + cache: Option< + DashMap< + PathBuf, + Option, + >, + >, } -impl SloppyImportsResolver { +impl SloppyImportsCachedFs { pub fn new(fs: Arc) -> Self { Self { fs, @@ -947,409 +450,34 @@ impl SloppyImportsResolver { pub fn new_without_stat_cache(fs: Arc) -> Self { Self { fs, cache: None } } +} - pub fn resolve( +impl deno_resolver::sloppy_imports::SloppyImportResolverFs + for SloppyImportsCachedFs +{ + fn stat_sync( &self, - specifier: &ModuleSpecifier, - mode: ResolutionMode, - ) -> Option { - fn path_without_ext( - path: &Path, - media_type: MediaType, - ) -> Option> { - let old_path_str = path.to_string_lossy(); - match media_type { - MediaType::Unknown => Some(old_path_str), - _ => old_path_str - .strip_suffix(media_type.as_ts_extension()) - .map(|s| Cow::Owned(s.to_string())), - } - } - - fn media_types_to_paths( - path_no_ext: &str, - original_media_type: MediaType, - probe_media_type_types: Vec, - reason: SloppyImportsResolutionReason, - ) -> Vec<(PathBuf, SloppyImportsResolutionReason)> { - probe_media_type_types - .into_iter() - .filter(|media_type| *media_type != original_media_type) - .map(|media_type| { - ( - PathBuf::from(format!( - "{}{}", - path_no_ext, - media_type.as_ts_extension() - )), - reason, - ) - }) - .collect::>() - } - - if specifier.scheme() != "file" { - return None; - } - - let path = specifier_to_file_path(specifier).ok()?; - - #[derive(Clone, Copy)] - enum SloppyImportsResolutionReason { - JsToTs, - NoExtension, - Directory, - } - - let probe_paths: Vec<(PathBuf, SloppyImportsResolutionReason)> = - match self.stat_sync(&path) { - Some(SloppyImportsFsEntry::File) => { - if mode.is_types() { - let media_type = MediaType::from_specifier(specifier); - // attempt to resolve the .d.ts file before the .js file - let probe_media_type_types = match media_type { - MediaType::JavaScript => { - vec![(MediaType::Dts), MediaType::JavaScript] - } - MediaType::Mjs => { - vec![MediaType::Dmts, MediaType::Dts, MediaType::Mjs] - } - MediaType::Cjs => { - vec![MediaType::Dcts, MediaType::Dts, MediaType::Cjs] - } - _ => return None, - }; - let path_no_ext = path_without_ext(&path, media_type)?; - media_types_to_paths( - &path_no_ext, - media_type, - probe_media_type_types, - SloppyImportsResolutionReason::JsToTs, - ) - } else { - return None; - } - } - entry @ None | entry @ Some(SloppyImportsFsEntry::Dir) => { - let media_type = MediaType::from_specifier(specifier); - let probe_media_type_types = match media_type { - MediaType::JavaScript => ( - if mode.is_types() { - vec![MediaType::TypeScript, MediaType::Tsx, MediaType::Dts] - } else { - vec![MediaType::TypeScript, MediaType::Tsx] - }, - SloppyImportsResolutionReason::JsToTs, - ), - MediaType::Jsx => { - (vec![MediaType::Tsx], SloppyImportsResolutionReason::JsToTs) - } - MediaType::Mjs => ( - if mode.is_types() { - vec![MediaType::Mts, MediaType::Dmts, MediaType::Dts] - } else { - vec![MediaType::Mts] - }, - SloppyImportsResolutionReason::JsToTs, - ), - MediaType::Cjs => ( - if mode.is_types() { - vec![MediaType::Cts, MediaType::Dcts, MediaType::Dts] - } else { - vec![MediaType::Cts] - }, - SloppyImportsResolutionReason::JsToTs, - ), - MediaType::TypeScript - | MediaType::Mts - | MediaType::Cts - | MediaType::Dts - | MediaType::Dmts - | MediaType::Dcts - | MediaType::Tsx - | MediaType::Json - | MediaType::Wasm - | MediaType::TsBuildInfo - | MediaType::SourceMap => { - return None; - } - MediaType::Unknown => ( - if mode.is_types() { - vec![ - MediaType::TypeScript, - MediaType::Tsx, - MediaType::Mts, - MediaType::Dts, - MediaType::Dmts, - MediaType::Dcts, - MediaType::JavaScript, - MediaType::Jsx, - MediaType::Mjs, - ] - } else { - vec![ - MediaType::TypeScript, - MediaType::JavaScript, - MediaType::Tsx, - MediaType::Jsx, - MediaType::Mts, - MediaType::Mjs, - ] - }, - SloppyImportsResolutionReason::NoExtension, - ), - }; - let mut probe_paths = match path_without_ext(&path, media_type) { - Some(path_no_ext) => media_types_to_paths( - &path_no_ext, - media_type, - probe_media_type_types.0, - probe_media_type_types.1, - ), - None => vec![], - }; - - if matches!(entry, Some(SloppyImportsFsEntry::Dir)) { - // try to resolve at the index file - if mode.is_types() { - probe_paths.push(( - path.join("index.ts"), - SloppyImportsResolutionReason::Directory, - )); - - probe_paths.push(( - path.join("index.mts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.d.ts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.d.mts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.js"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.mjs"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.tsx"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.jsx"), - SloppyImportsResolutionReason::Directory, - )); - } else { - probe_paths.push(( - path.join("index.ts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.mts"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.tsx"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.js"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.mjs"), - SloppyImportsResolutionReason::Directory, - )); - probe_paths.push(( - path.join("index.jsx"), - SloppyImportsResolutionReason::Directory, - )); - } - } - if probe_paths.is_empty() { - return None; - } - probe_paths - } - }; - - for (probe_path, reason) in probe_paths { - if self.stat_sync(&probe_path) == Some(SloppyImportsFsEntry::File) { - if let Ok(specifier) = ModuleSpecifier::from_file_path(probe_path) { - match reason { - SloppyImportsResolutionReason::JsToTs => { - return Some(SloppyImportsResolution::JsToTs(specifier)); - } - SloppyImportsResolutionReason::NoExtension => { - return Some(SloppyImportsResolution::NoExtension(specifier)); - } - SloppyImportsResolutionReason::Directory => { - return Some(SloppyImportsResolution::Directory(specifier)); - } - } - } - } - } - - None - } - - fn stat_sync(&self, path: &Path) -> Option { + path: &Path, + ) -> Option { if let Some(cache) = &self.cache { if let Some(entry) = cache.get(path) { return *entry; } } - let entry = self - .fs - .stat_sync(path) - .ok() - .and_then(|stat| SloppyImportsFsEntry::from_fs_stat(&stat)); + let entry = self.fs.stat_sync(path).ok().and_then(|stat| { + if stat.is_file { + Some(deno_resolver::sloppy_imports::SloppyImportsFsEntry::File) + } else if stat.is_directory { + Some(deno_resolver::sloppy_imports::SloppyImportsFsEntry::Dir) + } else { + None + } + }); + if let Some(cache) = &self.cache { cache.insert(path.to_owned(), entry); } entry } } - -#[cfg(test)] -mod test { - use test_util::TestContext; - - use super::*; - - #[test] - fn test_unstable_sloppy_imports() { - fn resolve(specifier: &ModuleSpecifier) -> Option { - resolve_with_mode(specifier, ResolutionMode::Execution) - } - - fn resolve_types( - specifier: &ModuleSpecifier, - ) -> Option { - resolve_with_mode(specifier, ResolutionMode::Types) - } - - fn resolve_with_mode( - specifier: &ModuleSpecifier, - mode: ResolutionMode, - ) -> Option { - SloppyImportsResolver::new(Arc::new(deno_fs::RealFs)) - .resolve(specifier, mode) - } - - let context = TestContext::default(); - let temp_dir = context.temp_dir().path(); - - // scenarios like resolving ./example.js to ./example.ts - for (ext_from, ext_to) in [("js", "ts"), ("js", "tsx"), ("mjs", "mts")] { - let ts_file = temp_dir.join(format!("file.{}", ext_to)); - ts_file.write(""); - assert_eq!(resolve(&ts_file.url_file()), None); - assert_eq!( - resolve( - &temp_dir - .url_dir() - .join(&format!("file.{}", ext_from)) - .unwrap() - ), - Some(SloppyImportsResolution::JsToTs(ts_file.url_file())), - ); - ts_file.remove_file(); - } - - // no extension scenarios - for ext in ["js", "ts", "js", "tsx", "jsx", "mjs", "mts"] { - let file = temp_dir.join(format!("file.{}", ext)); - file.write(""); - assert_eq!( - resolve( - &temp_dir - .url_dir() - .join("file") // no ext - .unwrap() - ), - Some(SloppyImportsResolution::NoExtension(file.url_file())) - ); - file.remove_file(); - } - - // .ts and .js exists, .js specified (goes to specified) - { - let ts_file = temp_dir.join("file.ts"); - ts_file.write(""); - let js_file = temp_dir.join("file.js"); - js_file.write(""); - assert_eq!(resolve(&js_file.url_file()), None); - } - - // only js exists, .js specified - { - let js_only_file = temp_dir.join("js_only.js"); - js_only_file.write(""); - assert_eq!(resolve(&js_only_file.url_file()), None); - assert_eq!(resolve_types(&js_only_file.url_file()), None); - } - - // resolving a directory to an index file - { - let routes_dir = temp_dir.join("routes"); - routes_dir.create_dir_all(); - let index_file = routes_dir.join("index.ts"); - index_file.write(""); - assert_eq!( - resolve(&routes_dir.url_file()), - Some(SloppyImportsResolution::Directory(index_file.url_file())), - ); - } - - // both a directory and a file with specifier is present - { - let api_dir = temp_dir.join("api"); - api_dir.create_dir_all(); - let bar_file = api_dir.join("bar.ts"); - bar_file.write(""); - let api_file = temp_dir.join("api.ts"); - api_file.write(""); - assert_eq!( - resolve(&api_dir.url_file()), - Some(SloppyImportsResolution::NoExtension(api_file.url_file())), - ); - } - } - - #[test] - fn test_sloppy_import_resolution_suggestion_message() { - // directory - assert_eq!( - SloppyImportsResolution::Directory( - ModuleSpecifier::parse("file:///dir/index.js").unwrap() - ) - .as_suggestion_message(), - "Maybe specify path to 'index.js' file in directory instead" - ); - // no ext - assert_eq!( - SloppyImportsResolution::NoExtension( - ModuleSpecifier::parse("file:///dir/index.mjs").unwrap() - ) - .as_suggestion_message(), - "Maybe add a '.mjs' extension" - ); - // js to ts - assert_eq!( - SloppyImportsResolution::JsToTs( - ModuleSpecifier::parse("file:///dir/index.mts").unwrap() - ) - .as_suggestion_message(), - "Maybe change the extension to '.mts'" - ); - } -} diff --git a/cli/schemas/config-file.v1.json b/cli/schemas/config-file.v1.json index af18e6f21c..9f4737fa0a 100644 --- a/cli/schemas/config-file.v1.json +++ b/cli/schemas/config-file.v1.json @@ -291,7 +291,7 @@ "type": "array", "description": "List of tag names that will be run. Empty list disables all tags and will only use rules from `include`.", "items": { - "type": "string" + "$ref": "https://raw.githubusercontent.com/denoland/deno_lint/main/schemas/tags.v1.json" }, "minItems": 0, "uniqueItems": true @@ -300,7 +300,7 @@ "type": "array", "description": "List of rule names that will be excluded from configured tag sets. If the same rule is in `include` it will be run.", "items": { - "type": "string" + "$ref": "https://raw.githubusercontent.com/denoland/deno_lint/main/schemas/rules.v1.json" }, "minItems": 0, "uniqueItems": true @@ -309,7 +309,7 @@ "type": "array", "description": "List of rule names that will be run. Even if the same rule is in `exclude` it will be run.", "items": { - "type": "string" + "$ref": "https://raw.githubusercontent.com/denoland/deno_lint/main/schemas/rules.v1.json" }, "minItems": 0, "uniqueItems": true @@ -530,9 +530,11 @@ "cron", "ffi", "fs", + "fmt-component", "http", "kv", "net", + "node-globals", "sloppy-imports", "temporal", "unsafe-proto", diff --git a/cli/standalone/binary.rs b/cli/standalone/binary.rs index 1290a238f3..ebcbf3ee62 100644 --- a/cli/standalone/binary.rs +++ b/cli/standalone/binary.rs @@ -9,14 +9,19 @@ use std::ffi::OsString; use std::fs; use std::fs::File; use std::future::Future; +use std::io::ErrorKind; use std::io::Read; use std::io::Seek; use std::io::SeekFrom; use std::io::Write; +use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::process::Command; +use std::sync::Arc; +use deno_ast::MediaType; +use deno_ast::ModuleKind; use deno_ast::ModuleSpecifier; use deno_config::workspace::PackageJsonDepResolution; use deno_config::workspace::ResolverWorkspaceJsrPackage; @@ -30,13 +35,23 @@ use deno_core::futures::AsyncReadExt; use deno_core::futures::AsyncSeekExt; use deno_core::serde_json; use deno_core::url::Url; +use deno_graph::source::RealFileSystem; +use deno_graph::ModuleGraph; +use deno_npm::resolution::SerializedNpmResolutionSnapshot; +use deno_npm::resolution::SerializedNpmResolutionSnapshotPackage; +use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; +use deno_npm::NpmPackageId; use deno_npm::NpmSystemInfo; +use deno_runtime::deno_fs; +use deno_runtime::deno_fs::FileSystem; +use deno_runtime::deno_fs::RealFs; +use deno_runtime::deno_io::fs::FsError; use deno_runtime::deno_node::PackageJson; +use deno_runtime::ops::otel::OtelConfig; use deno_semver::npm::NpmVersionReqParseError; use deno_semver::package::PackageReq; use deno_semver::Version; use deno_semver::VersionReqSpecifierParseError; -use eszip::EszipRelativeFileBaseUrl; use indexmap::IndexMap; use log::Level; use serde::Deserialize; @@ -49,10 +64,12 @@ use crate::args::NpmInstallDepsProvider; use crate::args::PermissionFlags; use crate::args::UnstableConfig; use crate::cache::DenoDir; +use crate::emit::Emitter; use crate::file_fetcher::FileFetcher; use crate::http_util::HttpClientProvider; use crate::npm::CliNpmResolver; use crate::npm::InnerCliNpmResolverRef; +use crate::resolver::CjsTracker; use crate::shared::ReleaseChannel; use crate::standalone::virtual_fs::VfsEntry; use crate::util::archive; @@ -60,12 +77,63 @@ use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; +use super::file_system::DenoCompileFileSystem; +use super::serialization::deserialize_binary_data_section; +use super::serialization::serialize_binary_data_section; +use super::serialization::DenoCompileModuleData; +use super::serialization::DeserializedDataSection; +use super::serialization::RemoteModulesStore; +use super::serialization::RemoteModulesStoreBuilder; use super::virtual_fs::FileBackedVfs; use super::virtual_fs::VfsBuilder; use super::virtual_fs::VfsRoot; use super::virtual_fs::VirtualDirectory; -const MAGIC_TRAILER: &[u8; 8] = b"d3n0l4nd"; +/// A URL that can be designated as the base for relative URLs. +/// +/// After creation, this URL may be used to get the key for a +/// module in the binary. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct StandaloneRelativeFileBaseUrl<'a>(&'a Url); + +impl<'a> From<&'a Url> for StandaloneRelativeFileBaseUrl<'a> { + fn from(url: &'a Url) -> Self { + Self(url) + } +} + +impl<'a> StandaloneRelativeFileBaseUrl<'a> { + pub fn new(url: &'a Url) -> Self { + debug_assert_eq!(url.scheme(), "file"); + Self(url) + } + + /// Gets the module map key of the provided specifier. + /// + /// * Descendant file specifiers will be made relative to the base. + /// * Non-descendant file specifiers will stay as-is (absolute). + /// * Non-file specifiers will stay as-is. + pub fn specifier_key<'b>(&self, target: &'b Url) -> Cow<'b, str> { + if target.scheme() != "file" { + return Cow::Borrowed(target.as_str()); + } + + match self.0.make_relative(target) { + Some(relative) => { + if relative.starts_with("../") { + Cow::Borrowed(target.as_str()) + } else { + Cow::Owned(relative) + } + } + None => Cow::Borrowed(target.as_str()), + } + } + + pub fn inner(&self) -> &Url { + self.0 + } +} #[derive(Deserialize, Serialize)] pub enum NodeModules { @@ -118,80 +186,26 @@ pub struct Metadata { pub entrypoint_key: String, pub node_modules: Option, pub unstable_config: UnstableConfig, -} - -pub fn load_npm_vfs(root_dir_path: PathBuf) -> Result { - let data = libsui::find_section("d3n0l4nd").unwrap(); - - // We do the first part sync so it can complete quickly - let trailer: [u8; TRAILER_SIZE] = data[0..TRAILER_SIZE].try_into().unwrap(); - let trailer = match Trailer::parse(&trailer)? { - None => panic!("Could not find trailer"), - Some(trailer) => trailer, - }; - let data = &data[TRAILER_SIZE..]; - - let vfs_data = - &data[trailer.npm_vfs_pos as usize..trailer.npm_files_pos as usize]; - let mut dir: VirtualDirectory = serde_json::from_slice(vfs_data)?; - - // align the name of the directory with the root dir - dir.name = root_dir_path - .file_name() - .unwrap() - .to_string_lossy() - .to_string(); - - let fs_root = VfsRoot { - dir, - root_path: root_dir_path, - start_file_offset: trailer.npm_files_pos, - }; - Ok(FileBackedVfs::new(data.to_vec(), fs_root)) + pub otel_config: Option, // None means disabled. } fn write_binary_bytes( mut file_writer: File, original_bin: Vec, metadata: &Metadata, - eszip: eszip::EszipV2, - npm_vfs: Option<&VirtualDirectory>, - npm_files: &Vec>, + npm_snapshot: Option, + remote_modules: &RemoteModulesStoreBuilder, + vfs: VfsBuilder, compile_flags: &CompileFlags, ) -> Result<(), AnyError> { - let metadata = serde_json::to_string(metadata)?.as_bytes().to_vec(); - let npm_vfs = serde_json::to_string(&npm_vfs)?.as_bytes().to_vec(); - let eszip_archive = eszip.into_bytes(); - - let mut writer = Vec::new(); - - // write the trailer, which includes the positions - // of the data blocks in the file - writer.write_all(&{ - let metadata_pos = eszip_archive.len() as u64; - let npm_vfs_pos = metadata_pos + (metadata.len() as u64); - let npm_files_pos = npm_vfs_pos + (npm_vfs.len() as u64); - Trailer { - eszip_pos: 0, - metadata_pos, - npm_vfs_pos, - npm_files_pos, - } - .as_bytes() - })?; - - writer.write_all(&eszip_archive)?; - writer.write_all(&metadata)?; - writer.write_all(&npm_vfs)?; - for file in npm_files { - writer.write_all(file)?; - } + let data_section_bytes = + serialize_binary_data_section(metadata, npm_snapshot, remote_modules, vfs)?; let target = compile_flags.resolve_target(); if target.contains("linux") { libsui::Elf::new(&original_bin).append( "d3n0l4nd", - &writer, + &data_section_bytes, &mut file_writer, )?; } else if target.contains("windows") { @@ -201,11 +215,11 @@ fn write_binary_bytes( pe = pe.set_icon(&icon)?; } - pe.write_resource("d3n0l4nd", writer)? + pe.write_resource("d3n0l4nd", data_section_bytes)? .build(&mut file_writer)?; } else if target.contains("darwin") { libsui::Macho::from(original_bin)? - .write_section("d3n0l4nd", writer)? + .write_section("d3n0l4nd", data_section_bytes)? .build_and_sign(&mut file_writer)?; } Ok(()) @@ -221,6 +235,67 @@ pub fn is_standalone_binary(exe_path: &Path) -> bool { || libsui::utils::is_macho(&data) } +pub struct StandaloneData { + pub fs: Arc, + pub metadata: Metadata, + pub modules: StandaloneModules, + pub npm_snapshot: Option, + pub root_path: PathBuf, + pub vfs: Arc, +} + +pub struct StandaloneModules { + remote_modules: RemoteModulesStore, + vfs: Arc, +} + +impl StandaloneModules { + pub fn resolve_specifier<'a>( + &'a self, + specifier: &'a ModuleSpecifier, + ) -> Result, AnyError> { + if specifier.scheme() == "file" { + Ok(Some(specifier)) + } else { + self.remote_modules.resolve_specifier(specifier) + } + } + + pub fn has_file(&self, path: &Path) -> bool { + self.vfs.file_entry(path).is_ok() + } + + pub fn read<'a>( + &'a self, + specifier: &'a ModuleSpecifier, + ) -> Result>, AnyError> { + if specifier.scheme() == "file" { + let path = deno_path_util::url_to_file_path(specifier)?; + let bytes = match self.vfs.file_entry(&path) { + Ok(entry) => self.vfs.read_file_all(entry)?, + Err(err) if err.kind() == ErrorKind::NotFound => { + let bytes = match RealFs.read_file_sync(&path, None) { + Ok(bytes) => bytes, + Err(FsError::Io(err)) if err.kind() == ErrorKind::NotFound => { + return Ok(None) + } + Err(err) => return Err(err.into()), + }; + Cow::Owned(bytes) + } + Err(err) => return Err(err.into()), + }; + Ok(Some(DenoCompileModuleData { + media_type: MediaType::from_specifier(specifier), + specifier, + data: bytes, + })) + } else { + self.remote_modules.read(specifier) + } + } +} + /// This function will try to run this binary as a standalone binary /// produced by `deno compile`. It determines if this is a standalone /// binary by skipping over the trailer width at the end of the file, @@ -228,110 +303,67 @@ pub fn is_standalone_binary(exe_path: &Path) -> bool { /// the bundle is executed. If not, this function exits with `Ok(None)`. pub fn extract_standalone( cli_args: Cow>, -) -> Result< - Option>>, - AnyError, -> { +) -> Result, AnyError> { let Some(data) = libsui::find_section("d3n0l4nd") else { return Ok(None); }; - // We do the first part sync so it can complete quickly - let trailer = match Trailer::parse(&data[0..TRAILER_SIZE])? { + let DeserializedDataSection { + mut metadata, + npm_snapshot, + remote_modules, + mut vfs_dir, + vfs_files_data, + } = match deserialize_binary_data_section(data)? { + Some(data_section) => data_section, None => return Ok(None), - Some(trailer) => trailer, }; + let root_path = { + let maybe_current_exe = std::env::current_exe().ok(); + let current_exe_name = maybe_current_exe + .as_ref() + .and_then(|p| p.file_name()) + .map(|p| p.to_string_lossy()) + // should never happen + .unwrap_or_else(|| Cow::Borrowed("binary")); + std::env::temp_dir().join(format!("deno-compile-{}", current_exe_name)) + }; let cli_args = cli_args.into_owned(); - // If we have an eszip, read it out - Ok(Some(async move { - let bufreader = - deno_core::futures::io::BufReader::new(&data[TRAILER_SIZE..]); + metadata.argv.reserve(cli_args.len() - 1); + for arg in cli_args.into_iter().skip(1) { + metadata.argv.push(arg.into_string().unwrap()); + } + let vfs = { + // align the name of the directory with the root dir + vfs_dir.name = root_path.file_name().unwrap().to_string_lossy().to_string(); - let (eszip, loader) = eszip::EszipV2::parse(bufreader) - .await - .context("Failed to parse eszip header")?; - - let bufreader = loader.await.context("Failed to parse eszip archive")?; - - let mut metadata = String::new(); - - bufreader - .take(trailer.metadata_len()) - .read_to_string(&mut metadata) - .await - .context("Failed to read metadata from the current executable")?; - - let mut metadata: Metadata = serde_json::from_str(&metadata).unwrap(); - metadata.argv.reserve(cli_args.len() - 1); - for arg in cli_args.into_iter().skip(1) { - metadata.argv.push(arg.into_string().unwrap()); - } - - Ok((metadata, eszip)) + let fs_root = VfsRoot { + dir: vfs_dir, + root_path: root_path.clone(), + start_file_offset: 0, + }; + Arc::new(FileBackedVfs::new(Cow::Borrowed(vfs_files_data), fs_root)) + }; + let fs: Arc = + Arc::new(DenoCompileFileSystem::new(vfs.clone())); + Ok(Some(StandaloneData { + fs, + metadata, + modules: StandaloneModules { + remote_modules, + vfs: vfs.clone(), + }, + npm_snapshot, + root_path, + vfs, })) } -const TRAILER_SIZE: usize = std::mem::size_of::() + 8; // 8 bytes for the magic trailer string - -struct Trailer { - eszip_pos: u64, - metadata_pos: u64, - npm_vfs_pos: u64, - npm_files_pos: u64, -} - -impl Trailer { - pub fn parse(trailer: &[u8]) -> Result, AnyError> { - let (magic_trailer, rest) = trailer.split_at(8); - if magic_trailer != MAGIC_TRAILER { - return Ok(None); - } - - let (eszip_archive_pos, rest) = rest.split_at(8); - let (metadata_pos, rest) = rest.split_at(8); - let (npm_vfs_pos, npm_files_pos) = rest.split_at(8); - let eszip_archive_pos = u64_from_bytes(eszip_archive_pos)?; - let metadata_pos = u64_from_bytes(metadata_pos)?; - let npm_vfs_pos = u64_from_bytes(npm_vfs_pos)?; - let npm_files_pos = u64_from_bytes(npm_files_pos)?; - Ok(Some(Trailer { - eszip_pos: eszip_archive_pos, - metadata_pos, - npm_vfs_pos, - npm_files_pos, - })) - } - - pub fn metadata_len(&self) -> u64 { - self.npm_vfs_pos - self.metadata_pos - } - - pub fn npm_vfs_len(&self) -> u64 { - self.npm_files_pos - self.npm_vfs_pos - } - - pub fn as_bytes(&self) -> Vec { - let mut trailer = MAGIC_TRAILER.to_vec(); - trailer.write_all(&self.eszip_pos.to_be_bytes()).unwrap(); - trailer.write_all(&self.metadata_pos.to_be_bytes()).unwrap(); - trailer.write_all(&self.npm_vfs_pos.to_be_bytes()).unwrap(); - trailer - .write_all(&self.npm_files_pos.to_be_bytes()) - .unwrap(); - trailer - } -} - -fn u64_from_bytes(arr: &[u8]) -> Result { - let fixed_arr: &[u8; 8] = arr - .try_into() - .context("Failed to convert the buffer into a fixed-size array")?; - Ok(u64::from_be_bytes(*fixed_arr)) -} - pub struct DenoCompileBinaryWriter<'a> { + cjs_tracker: &'a CjsTracker, deno_dir: &'a DenoDir, + emitter: &'a Emitter, file_fetcher: &'a FileFetcher, http_client_provider: &'a HttpClientProvider, npm_resolver: &'a dyn CliNpmResolver, @@ -342,7 +374,9 @@ pub struct DenoCompileBinaryWriter<'a> { impl<'a> DenoCompileBinaryWriter<'a> { #[allow(clippy::too_many_arguments)] pub fn new( + cjs_tracker: &'a CjsTracker, deno_dir: &'a DenoDir, + emitter: &'a Emitter, file_fetcher: &'a FileFetcher, http_client_provider: &'a HttpClientProvider, npm_resolver: &'a dyn CliNpmResolver, @@ -350,7 +384,9 @@ impl<'a> DenoCompileBinaryWriter<'a> { npm_system_info: NpmSystemInfo, ) -> Self { Self { + cjs_tracker, deno_dir, + emitter, file_fetcher, http_client_provider, npm_resolver, @@ -362,8 +398,8 @@ impl<'a> DenoCompileBinaryWriter<'a> { pub async fn write_bin( &self, writer: File, - eszip: eszip::EszipV2, - root_dir_url: EszipRelativeFileBaseUrl<'_>, + graph: &ModuleGraph, + root_dir_url: StandaloneRelativeFileBaseUrl<'_>, entrypoint: &ModuleSpecifier, compile_flags: &CompileFlags, cli_options: &CliOptions, @@ -390,15 +426,17 @@ impl<'a> DenoCompileBinaryWriter<'a> { ) } } - self.write_standalone_binary( - writer, - original_binary, - eszip, - root_dir_url, - entrypoint, - cli_options, - compile_flags, - ) + self + .write_standalone_binary( + writer, + original_binary, + graph, + root_dir_url, + entrypoint, + cli_options, + compile_flags, + ) + .await } async fn get_base_binary( @@ -468,14 +506,18 @@ impl<'a> DenoCompileBinaryWriter<'a> { self .http_client_provider .get_or_create()? - .download_with_progress(download_url.parse()?, None, &progress) + .download_with_progress_and_retries( + download_url.parse()?, + None, + &progress, + ) .await? }; let bytes = match maybe_bytes { Some(bytes) => bytes, None => { log::info!("Download could not be found, aborting"); - std::process::exit(1) + deno_runtime::exit(1); } }; @@ -489,12 +531,12 @@ impl<'a> DenoCompileBinaryWriter<'a> { /// This functions creates a standalone deno binary by appending a bundle /// and magic trailer to the currently executing binary. #[allow(clippy::too_many_arguments)] - fn write_standalone_binary( + async fn write_standalone_binary( &self, writer: File, original_bin: Vec, - mut eszip: eszip::EszipV2, - root_dir_url: EszipRelativeFileBaseUrl<'_>, + graph: &ModuleGraph, + root_dir_url: StandaloneRelativeFileBaseUrl<'_>, entrypoint: &ModuleSpecifier, cli_options: &CliOptions, compile_flags: &CompileFlags, @@ -508,19 +550,17 @@ impl<'a> DenoCompileBinaryWriter<'a> { None => None, }; let root_path = root_dir_url.inner().to_file_path().unwrap(); - let (npm_vfs, npm_files, node_modules) = match self.npm_resolver.as_inner() + let (maybe_npm_vfs, node_modules, npm_snapshot) = match self + .npm_resolver + .as_inner() { InnerCliNpmResolverRef::Managed(managed) => { let snapshot = managed.serialized_valid_snapshot_for_system(&self.npm_system_info); if !snapshot.as_serialized().packages.is_empty() { - let (root_dir, files) = self - .build_vfs(&root_path, cli_options)? - .into_dir_and_files(); - eszip.add_npm_snapshot(snapshot); + let npm_vfs_builder = self.build_npm_vfs(&root_path, cli_options)?; ( - Some(root_dir), - files, + Some(npm_vfs_builder), Some(NodeModules::Managed { node_modules_dir: self.npm_resolver.root_node_modules_path().map( |path| { @@ -532,18 +572,16 @@ impl<'a> DenoCompileBinaryWriter<'a> { }, ), }), + Some(snapshot), ) } else { - (None, Vec::new(), None) + (None, None, None) } } InnerCliNpmResolverRef::Byonm(resolver) => { - let (root_dir, files) = self - .build_vfs(&root_path, cli_options)? - .into_dir_and_files(); + let npm_vfs_builder = self.build_npm_vfs(&root_path, cli_options)?; ( - Some(root_dir), - files, + Some(npm_vfs_builder), Some(NodeModules::Byonm { root_node_modules_dir: resolver.root_node_modules_path().map( |node_modules_dir| { @@ -556,9 +594,69 @@ impl<'a> DenoCompileBinaryWriter<'a> { }, ), }), + None, ) } }; + let mut vfs = if let Some(npm_vfs) = maybe_npm_vfs { + npm_vfs + } else { + VfsBuilder::new(root_path.clone())? + }; + let mut remote_modules_store = RemoteModulesStoreBuilder::default(); + for module in graph.modules() { + if module.specifier().scheme() == "data" { + continue; // don't store data urls as an entry as they're in the code + } + let (maybe_source, media_type) = match module { + deno_graph::Module::Js(m) => { + let source = if m.media_type.is_emittable() { + let is_cjs = self.cjs_tracker.is_cjs_with_known_is_script( + &m.specifier, + m.media_type, + m.is_script, + )?; + let module_kind = ModuleKind::from_is_cjs(is_cjs); + let source = self + .emitter + .emit_parsed_source( + &m.specifier, + m.media_type, + module_kind, + &m.source, + ) + .await?; + source.into_bytes() + } else { + m.source.as_bytes().to_vec() + }; + (Some(source), m.media_type) + } + deno_graph::Module::Json(m) => { + (Some(m.source.as_bytes().to_vec()), m.media_type) + } + deno_graph::Module::Npm(_) + | deno_graph::Module::Node(_) + | deno_graph::Module::External(_) => (None, MediaType::Unknown), + }; + if module.specifier().scheme() == "file" { + let file_path = deno_path_util::url_to_file_path(module.specifier())?; + vfs + .add_file_with_data( + &file_path, + match maybe_source { + Some(source) => source, + None => RealFs.read_file_sync(&file_path, None)?, + }, + ) + .with_context(|| { + format!("Failed adding '{}'", file_path.display()) + })?; + } else if let Some(source) = maybe_source { + remote_modules_store.add(module.specifier(), media_type, source); + } + } + remote_modules_store.add_redirects(&graph.redirects); let env_vars_from_env_file = match cli_options.env_file_name() { Some(env_filename) => { @@ -625,20 +723,21 @@ impl<'a> DenoCompileBinaryWriter<'a> { sloppy_imports: cli_options.unstable_sloppy_imports(), features: cli_options.unstable_features(), }, + otel_config: cli_options.otel_config(), }; write_binary_bytes( writer, original_bin, &metadata, - eszip, - npm_vfs.as_ref(), - &npm_files, + npm_snapshot.map(|s| s.into_serialized()), + &remote_modules_store, + vfs, compile_flags, ) } - fn build_vfs( + fn build_npm_vfs( &self, root_path: &Path, cli_options: &CliOptions, @@ -659,8 +758,9 @@ impl<'a> DenoCompileBinaryWriter<'a> { } 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 root_path = npm_resolver.global_cache_root_folder(); - let mut builder = VfsBuilder::new(root_path)?; + let global_cache_root_path = npm_resolver.global_cache_root_path(); + let mut builder = + VfsBuilder::new(global_cache_root_path.to_path_buf())?; let mut packages = npm_resolver.all_system_packages(&self.npm_system_info); packages.sort_by(|a, b| a.id.cmp(&b.id)); // determinism @@ -670,12 +770,12 @@ impl<'a> DenoCompileBinaryWriter<'a> { builder.add_dir_recursive(&folder)?; } - // Flatten all the registries folders into a single "node_modules/localhost" folder + // Flatten all the registries folders into a single ".deno_compile_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(); + root_dir.name = ".deno_compile_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) { @@ -710,6 +810,8 @@ impl<'a> DenoCompileBinaryWriter<'a> { root_dir.entries = new_entries; }); + builder.set_new_root_path(root_path.to_path_buf())?; + Ok(builder) } } diff --git a/cli/standalone/file_system.rs b/cli/standalone/file_system.rs index 536b17f277..712c6ee918 100644 --- a/cli/standalone/file_system.rs +++ b/cli/standalone/file_system.rs @@ -22,8 +22,8 @@ use super::virtual_fs::FileBackedVfs; pub struct DenoCompileFileSystem(Arc); impl DenoCompileFileSystem { - pub fn new(vfs: FileBackedVfs) -> Self { - Self(Arc::new(vfs)) + pub fn new(vfs: Arc) -> Self { + Self(vfs) } fn error_if_in_vfs(&self, path: &Path) -> FsResult<()> { @@ -102,7 +102,7 @@ impl FileSystem for DenoCompileFileSystem { &self, path: &Path, recursive: bool, - mode: u32, + mode: Option, ) -> FsResult<()> { self.error_if_in_vfs(path)?; RealFs.mkdir_sync(path, recursive, mode) @@ -111,7 +111,7 @@ impl FileSystem for DenoCompileFileSystem { &self, path: PathBuf, recursive: bool, - mode: u32, + mode: Option, ) -> FsResult<()> { self.error_if_in_vfs(&path)?; RealFs.mkdir_async(path, recursive, mode).await diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index c501d2d6bc..e3449c1520 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -5,7 +5,10 @@ #![allow(dead_code)] #![allow(unused_imports)] +use binary::StandaloneData; +use binary::StandaloneModules; use deno_ast::MediaType; +use deno_cache_dir::npm::NpmCacheDir; use deno_config::workspace::MappedResolution; use deno_config::workspace::MappedResolutionError; use deno_config::workspace::ResolverWorkspaceJsrPackage; @@ -16,6 +19,7 @@ use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::v8_set_flags; +use deno_core::FastString; use deno_core::FeatureChecker; use deno_core::ModuleLoader; use deno_core::ModuleSourceCode; @@ -25,9 +29,12 @@ use deno_core::RequestedModuleType; use deno_core::ResolutionKind; use deno_npm::npm_rc::ResolvedNpmRc; use deno_package_json::PackageJsonDepValue; +use deno_resolver::npm::NpmReqResolverOptions; use deno_runtime::deno_fs; use deno_runtime::deno_node::create_host_defined_options; +use deno_runtime::deno_node::NodeRequireLoader; use deno_runtime::deno_node::NodeResolver; +use deno_runtime::deno_node::PackageJsonResolver; use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_tls::rustls::RootCertStore; @@ -37,10 +44,12 @@ use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::WorkerExecutionMode; use deno_runtime::WorkerLogLevel; use deno_semver::npm::NpmPackageReqReference; -use eszip::EszipRelativeFileBaseUrl; use import_map::parse_from_json; use node_resolver::analyze::NodeCodeTranslator; +use node_resolver::errors::ClosestPkgJsonError; +use node_resolver::NodeModuleKind; use node_resolver::NodeResolutionMode; +use serialization::DenoCompileModuleSource; use std::borrow::Cow; use std::rc::Rc; use std::sync::Arc; @@ -53,78 +62,57 @@ use crate::args::CacheSetting; use crate::args::NpmInstallDepsProvider; use crate::args::StorageKeyResolver; use crate::cache::Caches; +use crate::cache::DenoCacheEnvFsAdapter; use crate::cache::DenoDirProvider; use crate::cache::NodeAnalysisCache; +use crate::cache::RealDenoCacheEnv; use crate::http_util::HttpClientProvider; use crate::node::CliCjsCodeAnalyzer; +use crate::node::CliNodeCodeTranslator; use crate::npm::create_cli_npm_resolver; -use crate::npm::CliNpmResolverByonmCreateOptions; +use crate::npm::create_in_npm_pkg_checker; +use crate::npm::CliByonmNpmResolverCreateOptions; +use crate::npm::CliManagedInNpmPkgCheckerCreateOptions; +use crate::npm::CliManagedNpmResolverCreateOptions; +use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolverCreateOptions; -use crate::npm::CliNpmResolverManagedCreateOptions; use crate::npm::CliNpmResolverManagedSnapshotOption; -use crate::npm::NpmCacheDir; -use crate::resolver::CjsResolutionStore; -use crate::resolver::CliNodeResolver; +use crate::npm::CreateInNpmPkgCheckerOptions; +use crate::resolver::CjsTracker; +use crate::resolver::CliDenoResolverFs; +use crate::resolver::CliNpmReqResolver; +use crate::resolver::IsCjsResolverOptions; use crate::resolver::NpmModuleLoader; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; use crate::util::v8::construct_v8_flags; use crate::worker::CliMainWorkerFactory; use crate::worker::CliMainWorkerOptions; -use crate::worker::ModuleLoaderAndSourceMapGetter; +use crate::worker::CreateModuleLoaderResult; use crate::worker::ModuleLoaderFactory; pub mod binary; mod file_system; +mod serialization; mod virtual_fs; pub use binary::extract_standalone; pub use binary::is_standalone_binary; pub use binary::DenoCompileBinaryWriter; -use self::binary::load_npm_vfs; use self::binary::Metadata; use self::file_system::DenoCompileFileSystem; -struct WorkspaceEszipModule { - specifier: ModuleSpecifier, - inner: eszip::Module, -} - -struct WorkspaceEszip { - eszip: eszip::EszipV2, - root_dir_url: Arc, -} - -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, - }) - } - } -} - struct SharedModuleLoaderState { - eszip: WorkspaceEszip, - workspace_resolver: WorkspaceResolver, - node_resolver: Arc, + cjs_tracker: Arc, + fs: Arc, + modules: StandaloneModules, + node_code_translator: Arc, + node_resolver: Arc, npm_module_loader: Arc, + npm_req_resolver: Arc, + npm_resolver: Arc, + workspace_resolver: WorkspaceResolver, } #[derive(Clone)] @@ -132,6 +120,12 @@ struct EmbeddedModuleLoader { shared: Arc, } +impl std::fmt::Debug for EmbeddedModuleLoader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EmbeddedModuleLoader").finish() + } +} + pub const MODULE_NOT_FOUND: &str = "Module not found"; pub const UNSUPPORTED_SCHEME: &str = "Unsupported scheme"; @@ -156,13 +150,27 @@ impl ModuleLoader for EmbeddedModuleLoader { type_error(format!("Referrer uses invalid specifier: {}", err)) })? }; + let referrer_kind = if self + .shared + .cjs_tracker + .is_maybe_cjs(&referrer, MediaType::from_specifier(&referrer))? + { + NodeModuleKind::Cjs + } else { + NodeModuleKind::Esm + }; if self.shared.node_resolver.in_npm_package(&referrer) { return Ok( self .shared .node_resolver - .resolve(raw_specifier, &referrer, NodeResolutionMode::Execution)? + .resolve( + raw_specifier, + &referrer, + referrer_kind, + NodeResolutionMode::Execution, + )? .into_url(), ); } @@ -184,13 +192,13 @@ impl ModuleLoader for EmbeddedModuleLoader { self .shared .node_resolver - .resolve_package_sub_path_from_deno_module( + .resolve_package_subpath_from_deno_module( pkg_json.dir_path(), sub_path.as_deref(), Some(&referrer), + referrer_kind, NodeResolutionMode::Execution, - )? - .into_url(), + )?, ), Ok(MappedResolution::PackageJson { dep_result, @@ -200,14 +208,15 @@ impl ModuleLoader for EmbeddedModuleLoader { }) => match dep_result.as_ref().map_err(|e| AnyError::from(e.clone()))? { PackageJsonDepValue::Req(req) => self .shared - .node_resolver + .npm_req_resolver .resolve_req_with_sub_path( req, sub_path.as_deref(), &referrer, + referrer_kind, NodeResolutionMode::Execution, ) - .map(|res| res.into_url()), + .map_err(AnyError::from), PackageJsonDepValue::Workspace(version_req) => { let pkg_folder = self .shared @@ -220,13 +229,13 @@ impl ModuleLoader for EmbeddedModuleLoader { self .shared .node_resolver - .resolve_package_sub_path_from_deno_module( + .resolve_package_subpath_from_deno_module( pkg_folder, sub_path.as_deref(), Some(&referrer), + referrer_kind, NodeResolutionMode::Execution, - )? - .into_url(), + )?, ) } }, @@ -235,20 +244,19 @@ impl ModuleLoader for EmbeddedModuleLoader { 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()); + return Ok(self.shared.npm_req_resolver.resolve_req_reference( + &reference, + &referrer, + referrer_kind, + NodeResolutionMode::Execution, + )?); } if specifier.scheme() == "jsr" { - if let Some(module) = self.shared.eszip.get_module(&specifier) { - return Ok(module.specifier); + if let Some(specifier) = + self.shared.modules.resolve_specifier(&specifier)? + { + return Ok(specifier.clone()); } } @@ -256,16 +264,17 @@ impl ModuleLoader for EmbeddedModuleLoader { self .shared .node_resolver - .handle_if_in_node_modules(&specifier)? + .handle_if_in_node_modules(&specifier) .unwrap_or(specifier), ) } Err(err) if err.is_unmapped_bare_specifier() && referrer.scheme() == "file" => { - let maybe_res = self.shared.node_resolver.resolve_if_for_npm_pkg( + let maybe_res = self.shared.npm_req_resolver.resolve_if_for_npm_pkg( raw_specifier, &referrer, + referrer_kind, NodeResolutionMode::Execution, )?; if let Some(res) = maybe_res { @@ -343,82 +352,148 @@ impl ModuleLoader for EmbeddedModuleLoader { ); } - 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 - )))); - }; - let original_specifier = original_specifier.clone(); - - deno_core::ModuleLoadResponse::Async( - async move { - 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"))?; - Ok(deno_core::ModuleSource::new_with_redirect( - match module.inner.kind { - eszip::ModuleKind::JavaScript => ModuleType::JavaScript, - eszip::ModuleKind::Json => ModuleType::Json, - eszip::ModuleKind::Jsonc => { - return Err(type_error("jsonc modules not supported")) + match self.shared.modules.read(original_specifier) { + Ok(Some(module)) => { + let media_type = module.media_type; + let (module_specifier, module_type, module_source) = + module.into_parts(); + let is_maybe_cjs = match self + .shared + .cjs_tracker + .is_maybe_cjs(original_specifier, media_type) + { + Ok(is_maybe_cjs) => is_maybe_cjs, + Err(err) => { + return deno_core::ModuleLoadResponse::Sync(Err(type_error( + format!("{:?}", err), + ))); + } + }; + if is_maybe_cjs { + let original_specifier = original_specifier.clone(); + let module_specifier = module_specifier.clone(); + let shared = self.shared.clone(); + deno_core::ModuleLoadResponse::Async( + async move { + let source = match module_source { + DenoCompileModuleSource::String(string) => { + Cow::Borrowed(string) + } + DenoCompileModuleSource::Bytes(module_code_bytes) => { + match module_code_bytes { + Cow::Owned(bytes) => Cow::Owned( + crate::util::text_encoding::from_utf8_lossy_owned(bytes), + ), + Cow::Borrowed(bytes) => String::from_utf8_lossy(bytes), + } + } + }; + let source = shared + .node_code_translator + .translate_cjs_to_esm(&module_specifier, Some(source)) + .await?; + let module_source = match source { + Cow::Owned(source) => ModuleSourceCode::String(source.into()), + Cow::Borrowed(source) => { + ModuleSourceCode::String(FastString::from_static(source)) + } + }; + Ok(deno_core::ModuleSource::new_with_redirect( + module_type, + module_source, + &original_specifier, + &module_specifier, + None, + )) } - eszip::ModuleKind::OpaqueData => { - unreachable!(); - } - }, - ModuleSourceCode::String(code.into()), - &original_specifier, - &module.specifier, - None, - )) + .boxed_local(), + ) + } else { + let module_source = module_source.into_for_v8(); + deno_core::ModuleLoadResponse::Sync(Ok( + deno_core::ModuleSource::new_with_redirect( + module_type, + module_source, + original_specifier, + module_specifier, + None, + ), + )) + } } - .boxed_local(), - ) + Ok(None) => deno_core::ModuleLoadResponse::Sync(Err(type_error( + format!("{MODULE_NOT_FOUND}: {}", original_specifier), + ))), + Err(err) => deno_core::ModuleLoadResponse::Sync(Err(type_error( + format!("{:?}", err), + ))), + } } } -fn arc_u8_to_arc_str( - arc_u8: Arc<[u8]>, -) -> Result, std::str::Utf8Error> { - // Check that the string is valid UTF-8. - std::str::from_utf8(&arc_u8)?; - // 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::, std::sync::Arc>(arc_u8) - }) +impl NodeRequireLoader for EmbeddedModuleLoader { + fn ensure_read_permission<'a>( + &self, + permissions: &mut dyn deno_runtime::deno_node::NodePermissions, + path: &'a std::path::Path, + ) -> Result, AnyError> { + if self.shared.modules.has_file(path) { + // allow reading if the file is in the snapshot + return Ok(Cow::Borrowed(path)); + } + + self + .shared + .npm_resolver + .ensure_read_permission(permissions, path) + } + + fn load_text_file_lossy( + &self, + path: &std::path::Path, + ) -> Result { + Ok(self.shared.fs.read_text_file_lossy_sync(path, None)?) + } + + fn is_maybe_cjs( + &self, + specifier: &ModuleSpecifier, + ) -> Result { + let media_type = MediaType::from_specifier(specifier); + self.shared.cjs_tracker.is_maybe_cjs(specifier, media_type) + } } struct StandaloneModuleLoaderFactory { shared: Arc, } +impl StandaloneModuleLoaderFactory { + pub fn create_result(&self) -> CreateModuleLoaderResult { + let loader = Rc::new(EmbeddedModuleLoader { + shared: self.shared.clone(), + }); + CreateModuleLoaderResult { + module_loader: loader.clone(), + node_require_loader: loader, + } + } +} + impl ModuleLoaderFactory for StandaloneModuleLoaderFactory { fn create_for_main( &self, _root_permissions: PermissionsContainer, - ) -> ModuleLoaderAndSourceMapGetter { - ModuleLoaderAndSourceMapGetter { - module_loader: Rc::new(EmbeddedModuleLoader { - shared: self.shared.clone(), - }), - } + ) -> CreateModuleLoaderResult { + self.create_result() } fn create_for_worker( &self, _parent_permissions: PermissionsContainer, _permissions: PermissionsContainer, - ) -> ModuleLoaderAndSourceMapGetter { - ModuleLoaderAndSourceMapGetter { - module_loader: Rc::new(EmbeddedModuleLoader { - shared: self.shared.clone(), - }), - } + ) -> CreateModuleLoaderResult { + self.create_result() } } @@ -437,13 +512,15 @@ impl RootCertStoreProvider for StandaloneRootCertStoreProvider { } } -pub async fn run( - mut eszip: eszip::EszipV2, - metadata: Metadata, -) -> Result { - let current_exe_path = std::env::current_exe().unwrap(); - let current_exe_name = - current_exe_path.file_name().unwrap().to_string_lossy(); +pub async fn run(data: StandaloneData) -> Result { + let StandaloneData { + fs, + metadata, + modules, + npm_snapshot, + root_path, + vfs, + } = data; let deno_dir_provider = Arc::new(DenoDirProvider::new(None)); let root_cert_store_provider = Arc::new(StandaloneRootCertStoreProvider { ca_stores: metadata.ca_stores, @@ -457,43 +534,50 @@ pub async fn run( )); // use a dummy npm registry url let npm_registry_url = ModuleSpecifier::parse("https://localhost/").unwrap(); - let root_path = - std::env::temp_dir().join(format!("deno-compile-{}", current_exe_name)); let root_dir_url = Arc::new(ModuleSpecifier::from_directory_path(&root_path).unwrap()); let main_module = root_dir_url.join(&metadata.entrypoint_key).unwrap(); - let root_node_modules_path = root_path.join("node_modules"); - 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 npm_global_cache_dir = root_path.join(".deno_compile_node_modules"); let cache_setting = CacheSetting::Only; - let (fs, npm_resolver, maybe_vfs_root) = match metadata.node_modules { + let pkg_json_resolver = Arc::new(PackageJsonResolver::new( + deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), + )); + let (in_npm_pkg_checker, npm_resolver) = match metadata.node_modules { Some(binary::NodeModules::Managed { node_modules_dir }) => { - // this will always have a snapshot - let snapshot = eszip.take_npm_snapshot().unwrap(); - let vfs_root_dir_path = if node_modules_dir.is_some() { - root_path.clone() - } else { - npm_cache_dir.root_dir().to_owned() - }; - let vfs = load_npm_vfs(vfs_root_dir_path.clone()) - .context("Failed to load npm vfs.")?; + // create an npmrc that uses the fake npm_registry_url to resolve packages + let 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(), + }); + let npm_cache_dir = Arc::new(NpmCacheDir::new( + &DenoCacheEnvFsAdapter(fs.as_ref()), + npm_global_cache_dir, + npmrc.get_all_known_registries_urls(), + )); + let snapshot = npm_snapshot.unwrap(); let maybe_node_modules_path = node_modules_dir - .map(|node_modules_dir| vfs_root_dir_path.join(node_modules_dir)); - let fs = Arc::new(DenoCompileFileSystem::new(vfs)) - as Arc; + .map(|node_modules_dir| root_path.join(node_modules_dir)); + let in_npm_pkg_checker = + create_in_npm_pkg_checker(CreateInNpmPkgCheckerOptions::Managed( + CliManagedInNpmPkgCheckerCreateOptions { + root_cache_dir_url: npm_cache_dir.root_dir_url(), + maybe_node_modules_path: maybe_node_modules_path.as_deref(), + }, + )); let npm_resolver = create_cli_npm_resolver(CliNpmResolverCreateOptions::Managed( - CliNpmResolverManagedCreateOptions { + CliManagedNpmResolverCreateOptions { snapshot: CliNpmResolverManagedSnapshotOption::Specified(Some( snapshot, )), maybe_lockfile: None, fs: fs.clone(), http_client_provider: http_client_provider.clone(), - npm_global_cache_dir, + npm_cache_dir, cache_setting, text_only_progress_bar: progress_bar, maybe_node_modules_path, @@ -502,50 +586,54 @@ pub async fn run( // this is only used for installing packages, which isn't necessary with deno compile NpmInstallDepsProvider::empty(), ), - // create an npmrc that uses the fake npm_registry_url to resolve packages - 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(), - }), + npmrc, lifecycle_scripts: Default::default(), }, )) .await?; - (fs, npm_resolver, Some(vfs_root_dir_path)) + (in_npm_pkg_checker, npm_resolver) } Some(binary::NodeModules::Byonm { root_node_modules_dir, }) => { - let vfs_root_dir_path = root_path.clone(); - let vfs = load_npm_vfs(vfs_root_dir_path.clone()) - .context("Failed to load vfs.")?; let root_node_modules_dir = root_node_modules_dir.map(|p| vfs.root().join(p)); - let fs = Arc::new(DenoCompileFileSystem::new(vfs)) - as Arc; + let in_npm_pkg_checker = + create_in_npm_pkg_checker(CreateInNpmPkgCheckerOptions::Byonm); let npm_resolver = create_cli_npm_resolver( - CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions { - fs: fs.clone(), + CliNpmResolverCreateOptions::Byonm(CliByonmNpmResolverCreateOptions { + fs: CliDenoResolverFs(fs.clone()), + pkg_json_resolver: pkg_json_resolver.clone(), root_node_modules_dir, }), ) .await?; - (fs, npm_resolver, Some(vfs_root_dir_path)) + (in_npm_pkg_checker, npm_resolver) } None => { - let fs = Arc::new(deno_fs::RealFs) as Arc; + // Packages from different registries are already inlined in the binary, + // so no need to create actual `.npmrc` configuration. + let npmrc = create_default_npmrc(); + let npm_cache_dir = Arc::new(NpmCacheDir::new( + &DenoCacheEnvFsAdapter(fs.as_ref()), + npm_global_cache_dir, + npmrc.get_all_known_registries_urls(), + )); + let in_npm_pkg_checker = + create_in_npm_pkg_checker(CreateInNpmPkgCheckerOptions::Managed( + CliManagedInNpmPkgCheckerCreateOptions { + root_cache_dir_url: npm_cache_dir.root_dir_url(), + maybe_node_modules_path: None, + }, + )); let npm_resolver = create_cli_npm_resolver(CliNpmResolverCreateOptions::Managed( - CliNpmResolverManagedCreateOptions { + CliManagedNpmResolverCreateOptions { snapshot: CliNpmResolverManagedSnapshotOption::Specified(None), maybe_lockfile: None, fs: fs.clone(), http_client_provider: http_client_provider.clone(), - npm_global_cache_dir, + npm_cache_dir, cache_setting, text_only_progress_bar: progress_bar, maybe_node_modules_path: None, @@ -554,41 +642,53 @@ pub async fn run( // this is only used for installing packages, which isn't necessary with deno compile NpmInstallDepsProvider::empty(), ), - // Packages from different registries are already inlined in the ESZip, - // so no need to create actual `.npmrc` configuration. npmrc: create_default_npmrc(), lifecycle_scripts: Default::default(), }, )) .await?; - (fs, npm_resolver, None) + (in_npm_pkg_checker, npm_resolver) } }; let has_node_modules_dir = npm_resolver.root_node_modules_path().is_some(); let node_resolver = Arc::new(NodeResolver::new( deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), - npm_resolver.clone().into_npm_resolver(), + in_npm_pkg_checker.clone(), + npm_resolver.clone().into_npm_pkg_folder_resolver(), + pkg_json_resolver.clone(), + )); + let cjs_tracker = Arc::new(CjsTracker::new( + in_npm_pkg_checker.clone(), + pkg_json_resolver.clone(), + IsCjsResolverOptions { + detect_cjs: !metadata.workspace_resolver.package_jsons.is_empty(), + is_node_main: false, + }, )); - let cjs_resolutions = Arc::new(CjsResolutionStore::default()); let cache_db = Caches::new(deno_dir_provider.clone()); let node_analysis_cache = NodeAnalysisCache::new(cache_db.node_analysis_db()); - let cli_node_resolver = Arc::new(CliNodeResolver::new( - cjs_resolutions.clone(), - fs.clone(), - node_resolver.clone(), - npm_resolver.clone(), - )); + let npm_req_resolver = + Arc::new(CliNpmReqResolver::new(NpmReqResolverOptions { + byonm_resolver: (npm_resolver.clone()).into_maybe_byonm(), + fs: CliDenoResolverFs(fs.clone()), + in_npm_pkg_checker: in_npm_pkg_checker.clone(), + node_resolver: node_resolver.clone(), + npm_req_resolver: npm_resolver.clone().into_npm_req_resolver(), + })); let cjs_esm_code_analyzer = CliCjsCodeAnalyzer::new( node_analysis_cache, + cjs_tracker.clone(), fs.clone(), - cli_node_resolver.clone(), + None, ); let node_code_translator = Arc::new(NodeCodeTranslator::new( cjs_esm_code_analyzer, deno_runtime::deno_node::DenoFsNodeResolverEnv::new(fs.clone()), + in_npm_pkg_checker, node_resolver.clone(), - npm_resolver.clone().into_npm_resolver(), + npm_resolver.clone().into_npm_pkg_folder_resolver(), + pkg_json_resolver.clone(), )); let workspace_resolver = { let import_map = match metadata.workspace_resolver.import_map { @@ -641,37 +741,36 @@ pub async fn run( }; let module_loader_factory = StandaloneModuleLoaderFactory { shared: Arc::new(SharedModuleLoaderState { - eszip: WorkspaceEszip { - eszip, - root_dir_url, - }, - workspace_resolver, - node_resolver: cli_node_resolver.clone(), + cjs_tracker: cjs_tracker.clone(), + fs: fs.clone(), + modules, + node_code_translator: node_code_translator.clone(), + node_resolver: node_resolver.clone(), npm_module_loader: Arc::new(NpmModuleLoader::new( - cjs_resolutions, - node_code_translator, + cjs_tracker.clone(), fs.clone(), - cli_node_resolver, + node_code_translator, )), + npm_resolver: npm_resolver.clone(), + workspace_resolver, + npm_req_resolver, }), }; let permissions = { let mut permissions = metadata.permissions.to_options(/* cli_arg_urls */ &[]); - // if running with an npm vfs, grant read access to it - if let Some(vfs_root) = maybe_vfs_root { - match &mut permissions.allow_read { - Some(vec) if vec.is_empty() => { - // do nothing, already granted - } - Some(vec) => { - vec.push(vfs_root.to_string_lossy().to_string()); - } - None => { - permissions.allow_read = - Some(vec![vfs_root.to_string_lossy().to_string()]); - } + // grant read access to the vfs + match &mut permissions.allow_read { + Some(vec) if vec.is_empty() => { + // do nothing, already granted + } + Some(vec) => { + vec.push(root_path.to_string_lossy().to_string()); + } + None => { + permissions.allow_read = + Some(vec![root_path.to_string_lossy().to_string()]); } } @@ -691,8 +790,6 @@ pub async fn run( } checker }); - let permission_desc_parser = - Arc::new(RuntimePermissionDescriptorParser::new(fs.clone())); let worker_factory = CliMainWorkerFactory::new( Arc::new(BlobStore::default()), // Code cache is not supported for standalone binary yet. @@ -705,7 +802,7 @@ pub async fn run( Box::new(module_loader_factory), node_resolver, npm_resolver, - permission_desc_parser, + pkg_json_resolver, root_cert_store_provider, permissions, StorageKeyResolver::empty(), @@ -721,7 +818,6 @@ pub async fn run( inspect_wait: false, strace_ops: None, is_inspecting: false, - is_npm_main: main_module.scheme() == "npm", skip_op_registration: true, location: metadata.location, argv0: NpmPackageReqReference::from_specifier(&main_module) @@ -739,6 +835,7 @@ pub async fn run( serve_port: None, serve_host: None, }, + metadata.otel_config, ); // Initialize v8 once from the main thread. diff --git a/cli/standalone/serialization.rs b/cli/standalone/serialization.rs new file mode 100644 index 0000000000..a5eb649bfd --- /dev/null +++ b/cli/standalone/serialization.rs @@ -0,0 +1,661 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::io::Write; + +use deno_ast::MediaType; +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::url::Url; +use deno_core::FastString; +use deno_core::ModuleSourceCode; +use deno_core::ModuleType; +use deno_npm::resolution::SerializedNpmResolutionSnapshot; +use deno_npm::resolution::SerializedNpmResolutionSnapshotPackage; +use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; +use deno_npm::NpmPackageId; +use deno_semver::package::PackageReq; + +use crate::standalone::virtual_fs::VirtualDirectory; + +use super::binary::Metadata; +use super::virtual_fs::VfsBuilder; + +const MAGIC_BYTES: &[u8; 8] = b"d3n0l4nd"; + +/// Binary format: +/// * d3n0l4nd +/// * +/// * +/// * +/// * +/// * +/// * d3n0l4nd +pub fn serialize_binary_data_section( + metadata: &Metadata, + npm_snapshot: Option, + remote_modules: &RemoteModulesStoreBuilder, + vfs: VfsBuilder, +) -> Result, AnyError> { + fn write_bytes_with_len(bytes: &mut Vec, data: &[u8]) { + bytes.extend_from_slice(&(data.len() as u64).to_le_bytes()); + bytes.extend_from_slice(data); + } + + let mut bytes = Vec::new(); + bytes.extend_from_slice(MAGIC_BYTES); + + // 1. Metadata + { + let metadata = serde_json::to_string(metadata)?; + write_bytes_with_len(&mut bytes, metadata.as_bytes()); + } + // 2. Npm snapshot + { + let npm_snapshot = + npm_snapshot.map(serialize_npm_snapshot).unwrap_or_default(); + write_bytes_with_len(&mut bytes, &npm_snapshot); + } + // 3. Remote modules + { + let update_index = bytes.len(); + bytes.extend_from_slice(&(0_u64).to_le_bytes()); + let start_index = bytes.len(); + remote_modules.write(&mut bytes)?; + let length = bytes.len() - start_index; + let length_bytes = (length as u64).to_le_bytes(); + bytes[update_index..update_index + length_bytes.len()] + .copy_from_slice(&length_bytes); + } + // 4. VFS + { + let (vfs, vfs_files) = vfs.into_dir_and_files(); + let vfs = serde_json::to_string(&vfs)?; + write_bytes_with_len(&mut bytes, vfs.as_bytes()); + let vfs_bytes_len = vfs_files.iter().map(|f| f.len() as u64).sum::(); + bytes.extend_from_slice(&vfs_bytes_len.to_le_bytes()); + for file in &vfs_files { + bytes.extend_from_slice(file); + } + } + + // write the magic bytes at the end so we can use it + // to make sure we've deserialized correctly + bytes.extend_from_slice(MAGIC_BYTES); + + Ok(bytes) +} + +pub struct DeserializedDataSection { + pub metadata: Metadata, + pub npm_snapshot: Option, + pub remote_modules: RemoteModulesStore, + pub vfs_dir: VirtualDirectory, + pub vfs_files_data: &'static [u8], +} + +pub fn deserialize_binary_data_section( + data: &'static [u8], +) -> Result, AnyError> { + fn read_bytes_with_len(input: &[u8]) -> Result<(&[u8], &[u8]), AnyError> { + let (input, len) = read_u64(input)?; + let (input, data) = read_bytes(input, len as usize)?; + Ok((input, data)) + } + + fn read_magic_bytes(input: &[u8]) -> Result<(&[u8], bool), AnyError> { + if input.len() < MAGIC_BYTES.len() { + bail!("Unexpected end of data. Could not find magic bytes."); + } + let (magic_bytes, input) = input.split_at(MAGIC_BYTES.len()); + if magic_bytes != MAGIC_BYTES { + return Ok((input, false)); + } + Ok((input, true)) + } + + let (input, found) = read_magic_bytes(data)?; + if !found { + return Ok(None); + } + + // 1. Metadata + let (input, data) = read_bytes_with_len(input).context("reading metadata")?; + let metadata: Metadata = + serde_json::from_slice(data).context("deserializing metadata")?; + // 2. Npm snapshot + let (input, data) = + read_bytes_with_len(input).context("reading npm snapshot")?; + let npm_snapshot = if data.is_empty() { + None + } else { + Some(deserialize_npm_snapshot(data).context("deserializing npm snapshot")?) + }; + // 3. Remote modules + let (input, data) = + read_bytes_with_len(input).context("reading remote modules data")?; + let remote_modules = + RemoteModulesStore::build(data).context("deserializing remote modules")?; + // 4. VFS + let (input, data) = read_bytes_with_len(input).context("vfs")?; + let vfs_dir: VirtualDirectory = + serde_json::from_slice(data).context("deserializing vfs data")?; + let (input, vfs_files_data) = + read_bytes_with_len(input).context("reading vfs files data")?; + + // finally ensure we read the magic bytes at the end + let (_input, found) = read_magic_bytes(input)?; + if !found { + bail!("Could not find magic bytes at the end of the data."); + } + + Ok(Some(DeserializedDataSection { + metadata, + npm_snapshot, + remote_modules, + vfs_dir, + vfs_files_data, + })) +} + +#[derive(Default)] +pub struct RemoteModulesStoreBuilder { + specifiers: Vec<(String, u64)>, + data: Vec<(MediaType, Vec)>, + data_byte_len: u64, + redirects: Vec<(String, String)>, + redirects_len: u64, +} + +impl RemoteModulesStoreBuilder { + pub fn add(&mut self, specifier: &Url, media_type: MediaType, data: Vec) { + log::debug!("Adding '{}' ({})", specifier, media_type); + let specifier = specifier.to_string(); + self.specifiers.push((specifier, self.data_byte_len)); + self.data_byte_len += 1 + 8 + data.len() as u64; // media type (1 byte), data length (8 bytes), data + self.data.push((media_type, data)); + } + + pub fn add_redirects(&mut self, redirects: &BTreeMap) { + self.redirects.reserve(redirects.len()); + for (from, to) in redirects { + log::debug!("Adding redirect '{}' -> '{}'", from, to); + let from = from.to_string(); + let to = to.to_string(); + self.redirects_len += (4 + from.len() + 4 + to.len()) as u64; + self.redirects.push((from, to)); + } + } + + fn write(&self, writer: &mut dyn Write) -> Result<(), AnyError> { + writer.write_all(&(self.specifiers.len() as u32).to_le_bytes())?; + writer.write_all(&(self.redirects.len() as u32).to_le_bytes())?; + for (specifier, offset) in &self.specifiers { + writer.write_all(&(specifier.len() as u32).to_le_bytes())?; + writer.write_all(specifier.as_bytes())?; + writer.write_all(&offset.to_le_bytes())?; + } + for (from, to) in &self.redirects { + writer.write_all(&(from.len() as u32).to_le_bytes())?; + writer.write_all(from.as_bytes())?; + writer.write_all(&(to.len() as u32).to_le_bytes())?; + writer.write_all(to.as_bytes())?; + } + for (media_type, data) in &self.data { + writer.write_all(&[serialize_media_type(*media_type)])?; + writer.write_all(&(data.len() as u64).to_le_bytes())?; + writer.write_all(data)?; + } + Ok(()) + } +} + +pub enum DenoCompileModuleSource { + String(&'static str), + Bytes(Cow<'static, [u8]>), +} + +impl DenoCompileModuleSource { + pub fn into_for_v8(self) -> ModuleSourceCode { + fn into_bytes(data: Cow<'static, [u8]>) -> ModuleSourceCode { + ModuleSourceCode::Bytes(match data { + Cow::Borrowed(d) => d.into(), + Cow::Owned(d) => d.into_boxed_slice().into(), + }) + } + + match self { + // todo(https://github.com/denoland/deno_core/pull/943): store whether + // the string is ascii or not ahead of time so we can avoid the is_ascii() + // check in FastString::from_static + Self::String(s) => ModuleSourceCode::String(FastString::from_static(s)), + Self::Bytes(b) => into_bytes(b), + } + } +} + +pub struct DenoCompileModuleData<'a> { + pub specifier: &'a Url, + pub media_type: MediaType, + pub data: Cow<'static, [u8]>, +} + +impl<'a> DenoCompileModuleData<'a> { + pub fn into_parts(self) -> (&'a Url, ModuleType, DenoCompileModuleSource) { + fn into_string_unsafe(data: Cow<'static, [u8]>) -> DenoCompileModuleSource { + match data { + Cow::Borrowed(d) => DenoCompileModuleSource::String( + // SAFETY: we know this is a valid utf8 string + unsafe { std::str::from_utf8_unchecked(d) }, + ), + Cow::Owned(d) => DenoCompileModuleSource::Bytes(Cow::Owned(d)), + } + } + + let (media_type, source) = match self.media_type { + MediaType::JavaScript + | MediaType::Jsx + | MediaType::Mjs + | MediaType::Cjs + | MediaType::TypeScript + | MediaType::Mts + | MediaType::Cts + | MediaType::Dts + | MediaType::Dmts + | MediaType::Dcts + | MediaType::Tsx => { + (ModuleType::JavaScript, into_string_unsafe(self.data)) + } + MediaType::Json => (ModuleType::Json, into_string_unsafe(self.data)), + MediaType::Wasm => { + (ModuleType::Wasm, DenoCompileModuleSource::Bytes(self.data)) + } + // just assume javascript if we made it here + MediaType::Css | MediaType::SourceMap | MediaType::Unknown => ( + ModuleType::JavaScript, + DenoCompileModuleSource::Bytes(self.data), + ), + }; + (self.specifier, media_type, source) + } +} + +enum RemoteModulesStoreSpecifierValue { + Data(usize), + Redirect(Url), +} + +pub struct RemoteModulesStore { + specifiers: HashMap, + files_data: &'static [u8], +} + +impl RemoteModulesStore { + fn build(data: &'static [u8]) -> Result { + fn read_specifier(input: &[u8]) -> Result<(&[u8], (Url, u64)), AnyError> { + let (input, specifier) = read_string_lossy(input)?; + let specifier = Url::parse(&specifier)?; + let (input, offset) = read_u64(input)?; + Ok((input, (specifier, offset))) + } + + fn read_redirect(input: &[u8]) -> Result<(&[u8], (Url, Url)), AnyError> { + let (input, from) = read_string_lossy(input)?; + let from = Url::parse(&from)?; + let (input, to) = read_string_lossy(input)?; + let to = Url::parse(&to)?; + Ok((input, (from, to))) + } + + fn read_headers( + input: &[u8], + ) -> Result<(&[u8], HashMap), AnyError> + { + let (input, specifiers_len) = read_u32_as_usize(input)?; + let (mut input, redirects_len) = read_u32_as_usize(input)?; + let mut specifiers = + HashMap::with_capacity(specifiers_len + redirects_len); + for _ in 0..specifiers_len { + let (current_input, (specifier, offset)) = + read_specifier(input).context("reading specifier")?; + input = current_input; + specifiers.insert( + specifier, + RemoteModulesStoreSpecifierValue::Data(offset as usize), + ); + } + + for _ in 0..redirects_len { + let (current_input, (from, to)) = read_redirect(input)?; + input = current_input; + specifiers.insert(from, RemoteModulesStoreSpecifierValue::Redirect(to)); + } + + Ok((input, specifiers)) + } + + let (files_data, specifiers) = read_headers(data)?; + + Ok(Self { + specifiers, + files_data, + }) + } + + pub fn resolve_specifier<'a>( + &'a self, + specifier: &'a Url, + ) -> Result, AnyError> { + let mut count = 0; + let mut current = specifier; + loop { + if count > 10 { + bail!("Too many redirects resolving '{}'", specifier); + } + match self.specifiers.get(current) { + Some(RemoteModulesStoreSpecifierValue::Redirect(to)) => { + current = to; + count += 1; + } + Some(RemoteModulesStoreSpecifierValue::Data(_)) => { + return Ok(Some(current)); + } + None => { + return Ok(None); + } + } + } + } + + pub fn read<'a>( + &'a self, + original_specifier: &'a Url, + ) -> Result>, AnyError> { + let mut count = 0; + let mut specifier = original_specifier; + loop { + if count > 10 { + bail!("Too many redirects resolving '{}'", original_specifier); + } + match self.specifiers.get(specifier) { + Some(RemoteModulesStoreSpecifierValue::Redirect(to)) => { + specifier = to; + count += 1; + } + Some(RemoteModulesStoreSpecifierValue::Data(offset)) => { + let input = &self.files_data[*offset..]; + let (input, media_type_byte) = read_bytes(input, 1)?; + let media_type = deserialize_media_type(media_type_byte[0])?; + let (input, len) = read_u64(input)?; + let (_input, data) = read_bytes(input, len as usize)?; + return Ok(Some(DenoCompileModuleData { + specifier, + media_type, + data: Cow::Borrowed(data), + })); + } + None => { + return Ok(None); + } + } + } + } +} + +fn serialize_npm_snapshot( + mut snapshot: SerializedNpmResolutionSnapshot, +) -> Vec { + fn append_string(bytes: &mut Vec, string: &str) { + let len = string.len() as u32; + bytes.extend_from_slice(&len.to_le_bytes()); + bytes.extend_from_slice(string.as_bytes()); + } + + snapshot.packages.sort_by(|a, b| a.id.cmp(&b.id)); // determinism + let ids_to_stored_ids = snapshot + .packages + .iter() + .enumerate() + .map(|(i, pkg)| (&pkg.id, i as u32)) + .collect::>(); + + let mut root_packages: Vec<_> = snapshot.root_packages.iter().collect(); + root_packages.sort(); + let mut bytes = Vec::new(); + + bytes.extend_from_slice(&(snapshot.packages.len() as u32).to_le_bytes()); + for pkg in &snapshot.packages { + append_string(&mut bytes, &pkg.id.as_serialized()); + } + + bytes.extend_from_slice(&(root_packages.len() as u32).to_le_bytes()); + for (req, id) in root_packages { + append_string(&mut bytes, &req.to_string()); + let id = ids_to_stored_ids.get(&id).unwrap(); + bytes.extend_from_slice(&id.to_le_bytes()); + } + + for pkg in &snapshot.packages { + let deps_len = pkg.dependencies.len() as u32; + bytes.extend_from_slice(&deps_len.to_le_bytes()); + let mut deps: Vec<_> = pkg.dependencies.iter().collect(); + deps.sort(); + for (req, id) in deps { + append_string(&mut bytes, req); + let id = ids_to_stored_ids.get(&id).unwrap(); + bytes.extend_from_slice(&id.to_le_bytes()); + } + } + + bytes +} + +fn deserialize_npm_snapshot( + input: &[u8], +) -> Result { + fn parse_id(input: &[u8]) -> Result<(&[u8], NpmPackageId), AnyError> { + let (input, id) = read_string_lossy(input)?; + let id = NpmPackageId::from_serialized(&id)?; + Ok((input, id)) + } + + #[allow(clippy::needless_lifetimes)] // clippy bug + fn parse_root_package<'a>( + id_to_npm_id: &'a impl Fn(usize) -> Result, + ) -> impl Fn(&[u8]) -> Result<(&[u8], (PackageReq, NpmPackageId)), AnyError> + 'a + { + |input| { + let (input, req) = read_string_lossy(input)?; + let req = PackageReq::from_str(&req)?; + let (input, id) = read_u32_as_usize(input)?; + Ok((input, (req, id_to_npm_id(id)?))) + } + } + + #[allow(clippy::needless_lifetimes)] // clippy bug + fn parse_package_dep<'a>( + id_to_npm_id: &'a impl Fn(usize) -> Result, + ) -> impl Fn(&[u8]) -> Result<(&[u8], (String, NpmPackageId)), AnyError> + 'a + { + |input| { + let (input, req) = read_string_lossy(input)?; + let (input, id) = read_u32_as_usize(input)?; + Ok((input, (req.into_owned(), id_to_npm_id(id)?))) + } + } + + fn parse_package<'a>( + input: &'a [u8], + id: NpmPackageId, + id_to_npm_id: &impl Fn(usize) -> Result, + ) -> Result<(&'a [u8], SerializedNpmResolutionSnapshotPackage), AnyError> { + let (input, deps_len) = read_u32_as_usize(input)?; + let (input, dependencies) = + parse_hashmap_n_times(input, deps_len, parse_package_dep(id_to_npm_id))?; + Ok(( + input, + SerializedNpmResolutionSnapshotPackage { + id, + system: Default::default(), + dist: Default::default(), + dependencies, + optional_dependencies: Default::default(), + bin: None, + scripts: Default::default(), + deprecated: Default::default(), + }, + )) + } + + let (input, packages_len) = read_u32_as_usize(input)?; + + // get a hashmap of all the npm package ids to their serialized ids + let (input, data_ids_to_npm_ids) = + parse_vec_n_times(input, packages_len, parse_id) + .context("deserializing id")?; + let data_id_to_npm_id = |id: usize| { + data_ids_to_npm_ids + .get(id) + .cloned() + .ok_or_else(|| deno_core::anyhow::anyhow!("Invalid npm package id")) + }; + + let (input, root_packages_len) = read_u32_as_usize(input)?; + let (input, root_packages) = parse_hashmap_n_times( + input, + root_packages_len, + parse_root_package(&data_id_to_npm_id), + ) + .context("deserializing root package")?; + let (input, packages) = + parse_vec_n_times_with_index(input, packages_len, |input, index| { + parse_package(input, data_id_to_npm_id(index)?, &data_id_to_npm_id) + }) + .context("deserializing package")?; + + if !input.is_empty() { + bail!("Unexpected data left over"); + } + + Ok( + SerializedNpmResolutionSnapshot { + packages, + root_packages, + } + // this is ok because we have already verified that all the + // identifiers found in the snapshot are valid via the + // npm package id -> npm package id mapping + .into_valid_unsafe(), + ) +} + +fn serialize_media_type(media_type: MediaType) -> u8 { + match media_type { + MediaType::JavaScript => 0, + MediaType::Jsx => 1, + MediaType::Mjs => 2, + MediaType::Cjs => 3, + MediaType::TypeScript => 4, + MediaType::Mts => 5, + MediaType::Cts => 6, + MediaType::Dts => 7, + MediaType::Dmts => 8, + MediaType::Dcts => 9, + MediaType::Tsx => 10, + MediaType::Json => 11, + MediaType::Wasm => 12, + MediaType::Css => 13, + MediaType::SourceMap => 14, + MediaType::Unknown => 15, + } +} + +fn deserialize_media_type(value: u8) -> Result { + match value { + 0 => Ok(MediaType::JavaScript), + 1 => Ok(MediaType::Jsx), + 2 => Ok(MediaType::Mjs), + 3 => Ok(MediaType::Cjs), + 4 => Ok(MediaType::TypeScript), + 5 => Ok(MediaType::Mts), + 6 => Ok(MediaType::Cts), + 7 => Ok(MediaType::Dts), + 8 => Ok(MediaType::Dmts), + 9 => Ok(MediaType::Dcts), + 10 => Ok(MediaType::Tsx), + 11 => Ok(MediaType::Json), + 12 => Ok(MediaType::Wasm), + 13 => Ok(MediaType::Css), + 14 => Ok(MediaType::SourceMap), + 15 => Ok(MediaType::Unknown), + _ => bail!("Unknown media type value: {}", value), + } +} + +fn parse_hashmap_n_times( + mut input: &[u8], + times: usize, + parse: impl Fn(&[u8]) -> Result<(&[u8], (TKey, TValue)), AnyError>, +) -> Result<(&[u8], HashMap), AnyError> { + let mut results = HashMap::with_capacity(times); + for _ in 0..times { + let result = parse(input); + let (new_input, (key, value)) = result?; + results.insert(key, value); + input = new_input; + } + Ok((input, results)) +} + +fn parse_vec_n_times( + input: &[u8], + times: usize, + parse: impl Fn(&[u8]) -> Result<(&[u8], TResult), AnyError>, +) -> Result<(&[u8], Vec), AnyError> { + parse_vec_n_times_with_index(input, times, |input, _index| parse(input)) +} + +fn parse_vec_n_times_with_index( + mut input: &[u8], + times: usize, + parse: impl Fn(&[u8], usize) -> Result<(&[u8], TResult), AnyError>, +) -> Result<(&[u8], Vec), AnyError> { + let mut results = Vec::with_capacity(times); + for i in 0..times { + let result = parse(input, i); + let (new_input, result) = result?; + results.push(result); + input = new_input; + } + Ok((input, results)) +} + +fn read_bytes(input: &[u8], len: usize) -> Result<(&[u8], &[u8]), AnyError> { + if input.len() < len { + bail!("Unexpected end of data.",); + } + let (len_bytes, input) = input.split_at(len); + Ok((input, len_bytes)) +} + +fn read_string_lossy(input: &[u8]) -> Result<(&[u8], Cow), AnyError> { + let (input, str_len) = read_u32_as_usize(input)?; + let (input, data_bytes) = read_bytes(input, str_len)?; + Ok((input, String::from_utf8_lossy(data_bytes))) +} + +fn read_u32_as_usize(input: &[u8]) -> Result<(&[u8], usize), AnyError> { + let (input, len_bytes) = read_bytes(input, 4)?; + let len = u32::from_le_bytes(len_bytes.try_into()?); + Ok((input, len as usize)) +} + +fn read_u64(input: &[u8]) -> Result<(&[u8], u64), AnyError> { + let (input, len_bytes) = read_bytes(input, 8)?; + let len = u64::from_le_bytes(len_bytes.try_into()?); + Ok((input, len)) +} diff --git a/cli/standalone/virtual_fs.rs b/cli/standalone/virtual_fs.rs index 53d045b625..26bb0db75f 100644 --- a/cli/standalone/virtual_fs.rs +++ b/cli/standalone/virtual_fs.rs @@ -7,6 +7,7 @@ use std::fs::File; use std::io::Read; use std::io::Seek; use std::io::SeekFrom; +use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; @@ -67,6 +68,26 @@ impl VfsBuilder { }) } + pub fn set_new_root_path( + &mut self, + root_path: PathBuf, + ) -> Result<(), AnyError> { + let root_path = canonicalize_path(&root_path)?; + self.root_path = root_path; + self.root_dir = VirtualDirectory { + name: self + .root_path + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or("root".to_string()), + entries: vec![VfsEntry::Dir(VirtualDirectory { + name: std::mem::take(&mut self.root_dir.name), + entries: std::mem::take(&mut self.root_dir.entries), + })], + }; + Ok(()) + } + pub fn with_root_dir( &mut self, with_root: impl FnOnce(&mut VirtualDirectory) -> R, @@ -119,7 +140,7 @@ impl VfsBuilder { // 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)?; + self.add_file_with_data_inner(&path, file_bytes)?; } else { log::warn!( "{} Symlink target is outside '{}'. Excluding symlink at '{}' with target '{}'.", @@ -191,16 +212,32 @@ impl VfsBuilder { self.add_file_at_path_not_symlink(&target_path) } - pub fn add_file_at_path_not_symlink( + 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) + self.add_file_with_data_inner(path, file_bytes) } - fn add_file(&mut self, path: &Path, data: Vec) -> Result<(), AnyError> { + pub fn add_file_with_data( + &mut self, + path: &Path, + data: Vec, + ) -> Result<(), AnyError> { + let target_path = canonicalize_path(path)?; + if target_path != path { + self.add_symlink(path, &target_path)?; + } + self.add_file_with_data_inner(&target_path, data) + } + + fn add_file_with_data_inner( + &mut self, + path: &Path, + data: Vec, + ) -> Result<(), AnyError> { log::debug!("Adding file '{}'", path.display()); let checksum = util::checksum::gen(&[&data]); let offset = if let Some(offset) = self.file_offsets.get(&checksum) { @@ -249,8 +286,15 @@ impl VfsBuilder { path.display(), target.display() ); - let dest = self.path_relative_root(target)?; - if dest == self.path_relative_root(path)? { + let relative_target = self.path_relative_root(target)?; + let relative_path = match self.path_relative_root(path) { + Ok(path) => path, + Err(StripRootError { .. }) => { + // ignore if the original path is outside the root directory + return Ok(()); + } + }; + if relative_target == relative_path { // it's the same, ignore return Ok(()); } @@ -263,7 +307,7 @@ impl VfsBuilder { insert_index, VfsEntry::Symlink(VirtualSymlink { name: name.to_string(), - dest_parts: dest + dest_parts: relative_target .components() .map(|c| c.as_os_str().to_string_lossy().to_string()) .collect::>(), @@ -306,6 +350,7 @@ impl<'a> VfsEntryRef<'a> { atime: None, birthtime: None, mtime: None, + ctime: None, blksize: 0, size: 0, dev: 0, @@ -328,6 +373,7 @@ impl<'a> VfsEntryRef<'a> { atime: None, birthtime: None, mtime: None, + ctime: None, blksize: 0, size: file.len, dev: 0, @@ -350,6 +396,7 @@ impl<'a> VfsEntryRef<'a> { atime: None, birthtime: None, mtime: None, + ctime: None, blksize: 0, size: 0, dev: 0, @@ -751,14 +798,14 @@ impl deno_io::fs::File for FileBackedVfsFile { #[derive(Debug)] pub struct FileBackedVfs { - file: Mutex>, + vfs_data: Cow<'static, [u8]>, fs_root: VfsRoot, } impl FileBackedVfs { - pub fn new(file: Vec, fs_root: VfsRoot) -> Self { + pub fn new(data: Cow<'static, [u8]>, fs_root: VfsRoot) -> Self { Self { - file: Mutex::new(file), + vfs_data: data, fs_root, } } @@ -827,10 +874,15 @@ impl FileBackedVfs { Ok(path) } - pub fn read_file_all(&self, file: &VirtualFile) -> std::io::Result> { - let mut buf = vec![0; file.len as usize]; - self.read_file(file, 0, &mut buf)?; - Ok(buf) + pub fn read_file_all( + &self, + file: &VirtualFile, + ) -> std::io::Result> { + let read_range = self.get_read_range(file, 0, file.len)?; + match &self.vfs_data { + Cow::Borrowed(data) => Ok(Cow::Borrowed(&data[read_range])), + Cow::Owned(data) => Ok(Cow::Owned(data[read_range].to_vec())), + } } pub fn read_file( @@ -839,18 +891,27 @@ impl FileBackedVfs { pos: u64, buf: &mut [u8], ) -> std::io::Result { - let data = self.file.lock(); + let read_range = self.get_read_range(file, pos, buf.len() as u64)?; + buf.copy_from_slice(&self.vfs_data[read_range]); + Ok(buf.len()) + } + + fn get_read_range( + &self, + file: &VirtualFile, + pos: u64, + len: u64, + ) -> std::io::Result> { + let data = &self.vfs_data; let start = self.fs_root.start_file_offset + file.offset + pos; - let end = start + buf.len() as u64; + let end = start + len; if end > data.len() as u64 { return Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "unexpected EOF", )); } - - buf.copy_from_slice(&data[start as usize..end as usize]); - Ok(buf.len()) + Ok(start as usize..end as usize) } pub fn dir_entry(&self, path: &Path) -> std::io::Result<&VirtualDirectory> { @@ -888,7 +949,7 @@ mod test { #[track_caller] fn read_file(vfs: &FileBackedVfs, path: &Path) -> String { let file = vfs.file_entry(path).unwrap(); - String::from_utf8(vfs.read_file_all(file).unwrap()).unwrap() + String::from_utf8(vfs.read_file_all(file).unwrap().into_owned()).unwrap() } #[test] @@ -901,20 +962,23 @@ mod test { let src_path = src_path.to_path_buf(); let mut builder = VfsBuilder::new(src_path.clone()).unwrap(); builder - .add_file(&src_path.join("a.txt"), "data".into()) + .add_file_with_data_inner(&src_path.join("a.txt"), "data".into()) .unwrap(); builder - .add_file(&src_path.join("b.txt"), "data".into()) + .add_file_with_data_inner(&src_path.join("b.txt"), "data".into()) .unwrap(); assert_eq!(builder.files.len(), 1); // because duplicate data builder - .add_file(&src_path.join("c.txt"), "c".into()) + .add_file_with_data_inner(&src_path.join("c.txt"), "c".into()) .unwrap(); builder - .add_file(&src_path.join("sub_dir").join("d.txt"), "d".into()) + .add_file_with_data_inner( + &src_path.join("sub_dir").join("d.txt"), + "d".into(), + ) .unwrap(); builder - .add_file(&src_path.join("e.txt"), "e".into()) + .add_file_with_data_inner(&src_path.join("e.txt"), "e".into()) .unwrap(); builder .add_symlink( @@ -1031,7 +1095,7 @@ mod test { ( dest_path.to_path_buf(), FileBackedVfs::new( - data, + Cow::Owned(data), VfsRoot { dir: root_dir, root_path: dest_path.to_path_buf(), @@ -1082,7 +1146,7 @@ mod test { let temp_path = temp_dir.path().canonicalize(); let mut builder = VfsBuilder::new(temp_path.to_path_buf()).unwrap(); builder - .add_file( + .add_file_with_data_inner( temp_path.join("a.txt").as_path(), "0123456789".to_string().into_bytes(), ) diff --git a/cli/task_runner.rs b/cli/task_runner.rs index ab7163bc93..43840e868d 100644 --- a/cli/task_runner.rs +++ b/cli/task_runner.rs @@ -16,8 +16,11 @@ use deno_task_shell::ExecutableCommand; use deno_task_shell::ExecuteResult; use deno_task_shell::ShellCommand; use deno_task_shell::ShellCommandContext; +use deno_task_shell::ShellPipeReader; +use deno_task_shell::ShellPipeWriter; use lazy_regex::Lazy; use regex::Regex; +use tokio::task::JoinHandle; use tokio::task::LocalSet; use crate::npm::CliNpmResolver; @@ -36,6 +39,35 @@ pub fn get_script_with_args(script: &str, argv: &[String]) -> String { script.trim().to_owned() } +pub struct TaskStdio(Option, ShellPipeWriter); + +impl TaskStdio { + pub fn stdout() -> Self { + Self(None, ShellPipeWriter::stdout()) + } + pub fn stderr() -> Self { + Self(None, ShellPipeWriter::stderr()) + } + pub fn piped() -> Self { + let (r, w) = deno_task_shell::pipe(); + Self(Some(r), w) + } +} + +pub struct TaskIo { + pub stdout: TaskStdio, + pub stderr: TaskStdio, +} + +impl Default for TaskIo { + fn default() -> Self { + Self { + stderr: TaskStdio::stderr(), + stdout: TaskStdio::stdout(), + } + } +} + pub struct RunTaskOptions<'a> { pub task_name: &'a str, pub script: &'a str, @@ -45,24 +77,69 @@ pub struct RunTaskOptions<'a> { pub argv: &'a [String], pub custom_commands: HashMap>, pub root_node_modules_dir: Option<&'a Path>, + pub stdio: Option, } pub type TaskCustomCommands = HashMap>; -pub async fn run_task(opts: RunTaskOptions<'_>) -> Result { +pub struct TaskResult { + pub exit_code: i32, + pub stdout: Option>, + pub stderr: Option>, +} + +pub async fn run_task( + opts: RunTaskOptions<'_>, +) -> Result { let script = get_script_with_args(opts.script, opts.argv); let seq_list = deno_task_shell::parser::parse(&script) .with_context(|| format!("Error parsing script '{}'.", opts.task_name))?; let env_vars = prepare_env_vars(opts.env_vars, opts.init_cwd, opts.root_node_modules_dir); + let state = + deno_task_shell::ShellState::new(env_vars, opts.cwd, opts.custom_commands); + let stdio = opts.stdio.unwrap_or_default(); + let ( + TaskStdio(stdout_read, stdout_write), + TaskStdio(stderr_read, stderr_write), + ) = (stdio.stdout, stdio.stderr); + + fn read(reader: ShellPipeReader) -> JoinHandle, AnyError>> { + tokio::task::spawn_blocking(move || { + let mut buf = Vec::new(); + reader.pipe_to(&mut buf)?; + Ok(buf) + }) + } + + let stdout = stdout_read.map(read); + let stderr = stderr_read.map(read); + let local = LocalSet::new(); - let future = deno_task_shell::execute( - seq_list, - env_vars, - opts.cwd, - opts.custom_commands, - ); - Ok(local.run_until(future).await) + let future = async move { + let exit_code = deno_task_shell::execute_with_pipes( + seq_list, + state, + ShellPipeReader::stdin(), + stdout_write, + stderr_write, + ) + .await; + Ok::<_, AnyError>(TaskResult { + exit_code, + stdout: if let Some(stdout) = stdout { + Some(stdout.await??) + } else { + None + }, + stderr: if let Some(stderr) = stderr { + Some(stderr.await??) + } else { + None + }, + }) + }; + local.run_until(future).await } fn prepare_env_vars( @@ -78,6 +155,12 @@ fn prepare_env_vars( initial_cwd.to_string_lossy().to_string(), ); } + if !env_vars.contains_key(crate::npm::NPM_CONFIG_USER_AGENT_ENV_VAR) { + env_vars.insert( + crate::npm::NPM_CONFIG_USER_AGENT_ENV_VAR.into(), + crate::npm::get_npm_config_user_agent(), + ); + } if let Some(node_modules_dir) = node_modules_dir { prepend_to_path( &mut env_vars, @@ -127,7 +210,7 @@ impl ShellCommand for NpmCommand { mut context: ShellCommandContext, ) -> LocalBoxFuture<'static, ExecuteResult> { if context.args.first().map(|s| s.as_str()) == Some("run") - && context.args.len() > 2 + && context.args.len() >= 2 // for now, don't run any npm scripts that have a flag because // we don't handle stuff like `--workspaces` properly && !context.args.iter().any(|s| s.starts_with('-')) @@ -190,10 +273,12 @@ impl ShellCommand for NodeCommand { ) .execute(context); } + args.extend(["run", "-A"].into_iter().map(|s| s.to_string())); args.extend(context.args.iter().cloned()); let mut state = context.state; + state.apply_env_var(USE_PKG_JSON_HIDDEN_ENV_VAR_NAME, "1"); ExecutableCommand::new("deno".to_string(), std::env::current_exe().unwrap()) .execute(ShellCommandContext { diff --git a/cli/tools/bench/mod.rs b/cli/tools/bench/mod.rs index be5d0ad0e1..272d063355 100644 --- a/cli/tools/bench/mod.rs +++ b/cli/tools/bench/mod.rs @@ -193,7 +193,7 @@ async fn bench_specifier_inner( .await?; // We execute the main module as a side module so that import.meta.main is not set. - worker.execute_side_module_possibly_with_npm().await?; + worker.execute_side_module().await?; let mut worker = worker.into_main_worker(); diff --git a/cli/tools/check.rs b/cli/tools/check.rs index 7edb392d48..d880278884 100644 --- a/cli/tools/check.rs +++ b/cli/tools/check.rs @@ -32,6 +32,7 @@ use crate::graph_util::ModuleGraphBuilder; use crate::npm::CliNpmResolver; use crate::tsc; use crate::tsc::Diagnostics; +use crate::tsc::TypeCheckingCjsTracker; use crate::util::extract; use crate::util::path::to_percent_decoded_str; @@ -99,6 +100,7 @@ pub struct CheckOptions { pub struct TypeChecker { caches: Arc, + cjs_tracker: Arc, cli_options: Arc, module_graph_builder: Arc, node_resolver: Arc, @@ -108,6 +110,7 @@ pub struct TypeChecker { impl TypeChecker { pub fn new( caches: Arc, + cjs_tracker: Arc, cli_options: Arc, module_graph_builder: Arc, node_resolver: Arc, @@ -115,6 +118,7 @@ impl TypeChecker { ) -> Self { Self { caches, + cjs_tracker, cli_options, module_graph_builder, node_resolver, @@ -244,6 +248,7 @@ impl TypeChecker { graph: graph.clone(), hash_data, maybe_npm: Some(tsc::RequestNpmState { + cjs_tracker: self.cjs_tracker.clone(), node_resolver: self.node_resolver.clone(), npm_resolver: self.npm_resolver.clone(), }), @@ -346,7 +351,7 @@ fn get_check_hash( } } MediaType::Json - | MediaType::TsBuildInfo + | MediaType::Css | MediaType::SourceMap | MediaType::Wasm | MediaType::Unknown => continue, @@ -428,7 +433,7 @@ fn get_tsc_roots( } MediaType::Json | MediaType::Wasm - | MediaType::TsBuildInfo + | MediaType::Css | MediaType::SourceMap | MediaType::Unknown => None, }, @@ -536,7 +541,7 @@ fn has_ts_check(media_type: MediaType, file_text: &str) -> bool { | MediaType::Tsx | MediaType::Json | MediaType::Wasm - | MediaType::TsBuildInfo + | MediaType::Css | MediaType::SourceMap | MediaType::Unknown => false, } diff --git a/cli/tools/compile.rs b/cli/tools/compile.rs index c1f98bc08a..b3e9993379 100644 --- a/cli/tools/compile.rs +++ b/cli/tools/compile.rs @@ -5,6 +5,7 @@ use crate::args::CompileFlags; use crate::args::Flags; use crate::factory::CliFactory; use crate::http_util::HttpClientProvider; +use crate::standalone::binary::StandaloneRelativeFileBaseUrl; use crate::standalone::is_standalone_binary; use deno_ast::ModuleSpecifier; use deno_core::anyhow::bail; @@ -14,7 +15,6 @@ use deno_core::error::AnyError; use deno_core::resolve_url_or_path; use deno_graph::GraphKind; use deno_terminal::colors; -use eszip::EszipRelativeFileBaseUrl; use rand::Rng; use std::path::Path; use std::path::PathBuf; @@ -29,7 +29,6 @@ pub async fn compile( let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let module_graph_creator = factory.module_graph_creator().await?; - let parsed_source_cache = factory.parsed_source_cache(); let binary_writer = factory.create_compile_binary_writer().await?; let http_client = factory.http_client_provider(); let module_specifier = cli_options.resolve_main_module()?; @@ -70,7 +69,7 @@ pub async fn compile( let graph = if cli_options.type_check_mode().is_true() { // In this case, the previous graph creation did type checking, which will // create a module graph with types information in it. We don't want to - // store that in the eszip so create a code only module graph from scratch. + // store that in the binary so create a code only module graph from scratch. module_graph_creator .create_graph(GraphKind::CodeOnly, module_roots) .await? @@ -81,11 +80,6 @@ pub async fn compile( let ts_config_for_emit = cli_options .resolve_ts_config_for_emit(deno_config::deno_json::TsConfigType::Emit)?; check_warn_tsconfig(&ts_config_for_emit); - let (transpile_options, emit_options) = - crate::args::ts_config_to_transpile_and_emit_options( - ts_config_for_emit.ts_config, - )?; - let parser = parsed_source_cache.as_capturing_parser(); let root_dir_url = resolve_root_dir_from_specifiers( cli_options.workspace().root_dir(), graph.specifiers().map(|(s, _)| s).chain( @@ -96,17 +90,6 @@ pub async fn compile( ), ); log::debug!("Binary root dir: {}", root_dir_url); - let root_dir_url = EszipRelativeFileBaseUrl::new(&root_dir_url); - let eszip = eszip::EszipV2::from_graph(eszip::FromGraphOptions { - graph, - parser, - transpile_options, - emit_options, - // make all the modules relative to the root folder - relative_file_base: Some(root_dir_url), - npm_packages: None, - })?; - log::info!( "{} {} to {}", colors::green("Compile"), @@ -133,15 +116,18 @@ pub async fn compile( let write_result = binary_writer .write_bin( file, - eszip, - root_dir_url, + &graph, + StandaloneRelativeFileBaseUrl::from(&root_dir_url), module_specifier, &compile_flags, cli_options, ) .await .with_context(|| { - format!("Writing temporary file '{}'", temp_path.display()) + format!( + "Writing deno compile executable to temporary file '{}'", + temp_path.display() + ) }); // set it as executable diff --git a/cli/tools/coverage/mod.rs b/cli/tools/coverage/mod.rs index 260c0c8424..2a554c1335 100644 --- a/cli/tools/coverage/mod.rs +++ b/cli/tools/coverage/mod.rs @@ -6,12 +6,12 @@ use crate::args::FileFlags; use crate::args::Flags; use crate::cdp; use crate::factory::CliFactory; -use crate::npm::CliNpmResolver; use crate::tools::fmt::format_json; use crate::tools::test::is_supported_test_path; use crate::util::text_encoding::source_map_from_code; use deno_ast::MediaType; +use deno_ast::ModuleKind; use deno_ast::ModuleSpecifier; use deno_config::glob::FileCollector; use deno_config::glob::FilePatterns; @@ -25,6 +25,7 @@ use deno_core::serde_json; use deno_core::sourcemap::SourceMap; use deno_core::url::Url; use deno_core::LocalInspectorSession; +use node_resolver::InNpmPackageChecker; use regex::Regex; use std::fs; use std::fs::File; @@ -327,6 +328,7 @@ fn generate_coverage_report( coverage_report.found_lines = if let Some(source_map) = maybe_source_map.as_ref() { + let script_source_lines = script_source.lines().collect::>(); let mut found_lines = line_counts .iter() .enumerate() @@ -334,7 +336,23 @@ fn generate_coverage_report( // get all the mappings from this destination line to a different src line let mut results = source_map .tokens() - .filter(move |token| token.get_dst_line() as usize == index) + .filter(|token| { + let dst_line = token.get_dst_line() as usize; + dst_line == index && { + let dst_col = token.get_dst_col() as usize; + let content = script_source_lines + .get(dst_line) + .and_then(|line| { + line.get(dst_col..std::cmp::min(dst_col + 2, line.len())) + }) + .unwrap_or(""); + + !content.is_empty() + && content != "/*" + && content != "*/" + && content != "//" + } + }) .map(move |token| (token.get_src_line() as usize, *count)) .collect::>(); // only keep the results that point at different src lines @@ -444,7 +462,7 @@ fn filter_coverages( coverages: Vec, include: Vec, exclude: Vec, - npm_resolver: &dyn CliNpmResolver, + in_npm_pkg_checker: &dyn InNpmPackageChecker, ) -> Vec { let include: Vec = include.iter().map(|e| Regex::new(e).unwrap()).collect(); @@ -462,13 +480,13 @@ fn filter_coverages( .filter(|e| { let is_internal = e.url.starts_with("ext:") || e.url.ends_with("__anonymous__") - || e.url.ends_with("$deno$test.js") + || e.url.ends_with("$deno$test.mjs") || e.url.ends_with(".snap") || is_supported_test_path(Path::new(e.url.as_str())) || doc_test_re.is_match(e.url.as_str()) || Url::parse(&e.url) .ok() - .map(|url| npm_resolver.in_npm_package(&url)) + .map(|url| in_npm_pkg_checker.in_npm_package(&url)) .unwrap_or(false); let is_included = include.iter().any(|p| p.is_match(&e.url)); @@ -479,7 +497,7 @@ fn filter_coverages( .collect::>() } -pub async fn cover_files( +pub fn cover_files( flags: Arc, coverage_flags: CoverageFlags, ) -> Result<(), AnyError> { @@ -489,9 +507,10 @@ pub async fn cover_files( let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; - let npm_resolver = factory.npm_resolver().await?; + let in_npm_pkg_checker = factory.in_npm_pkg_checker()?; let file_fetcher = factory.file_fetcher()?; let emitter = factory.emitter()?; + let cjs_tracker = factory.cjs_tracker()?; assert!(!coverage_flags.files.include.is_empty()); @@ -511,7 +530,7 @@ pub async fn cover_files( script_coverages, coverage_flags.include, coverage_flags.exclude, - npm_resolver.as_ref(), + in_npm_pkg_checker.as_ref(), ); if script_coverages.is_empty() { return Err(generic_error("No covered files included in the report")); @@ -568,16 +587,21 @@ pub async fn cover_files( let transpiled_code = match file.media_type { MediaType::JavaScript | MediaType::Unknown + | MediaType::Css + | MediaType::Wasm | MediaType::Cjs | MediaType::Mjs | MediaType::Json => None, - MediaType::Dts | MediaType::Dmts | MediaType::Dcts => Some(Vec::new()), + MediaType::Dts | MediaType::Dmts | MediaType::Dcts => Some(String::new()), MediaType::TypeScript | MediaType::Jsx | MediaType::Mts | MediaType::Cts | MediaType::Tsx => { - Some(match emitter.maybe_cached_emit(&file.specifier, &file.source) { + let module_kind = ModuleKind::from_is_cjs( + cjs_tracker.is_maybe_cjs(&file.specifier, file.media_type)?, + ); + Some(match emitter.maybe_cached_emit(&file.specifier, module_kind, &file.source) { Some(code) => code, None => { return Err(anyhow!( @@ -588,13 +612,12 @@ pub async fn cover_files( } }) } - MediaType::Wasm | MediaType::TsBuildInfo | MediaType::SourceMap => { + MediaType::SourceMap => { unreachable!() } }; let runtime_code: String = match transpiled_code { - Some(code) => String::from_utf8(code) - .with_context(|| format!("Failed decoding {}", file.specifier))?, + Some(code) => code, None => original_source.to_string(), }; diff --git a/cli/tools/doc.rs b/cli/tools/doc.rs index 8f37632c84..e33da4efb2 100644 --- a/cli/tools/doc.rs +++ b/cli/tools/doc.rs @@ -22,9 +22,9 @@ use deno_core::serde_json; use deno_doc as doc; use deno_doc::html::UrlResolveKind; use deno_graph::source::NullFileSystem; +use deno_graph::EsParser; use deno_graph::GraphKind; use deno_graph::ModuleAnalyzer; -use deno_graph::ModuleParser; use deno_graph::ModuleSpecifier; use doc::html::ShortPath; use doc::DocDiagnostic; @@ -37,7 +37,7 @@ const JSON_SCHEMA_VERSION: u8 = 1; async fn generate_doc_nodes_for_builtin_types( doc_flags: DocFlags, - parser: &dyn ModuleParser, + parser: &dyn EsParser, analyzer: &dyn ModuleAnalyzer, ) -> Result>, AnyError> { let source_file_specifier = @@ -96,7 +96,7 @@ pub async fn doc( let module_info_cache = factory.module_info_cache()?; let parsed_source_cache = factory.parsed_source_cache(); let capturing_parser = parsed_source_cache.as_capturing_parser(); - let analyzer = module_info_cache.as_module_analyzer(parsed_source_cache); + let analyzer = module_info_cache.as_module_analyzer(); let doc_nodes_by_url = match doc_flags.source_files { DocSourceFileFlag::Builtin => { @@ -195,7 +195,7 @@ pub async fn doc( kind_with_drilldown: deno_doc::html::DocNodeKindWithDrilldown::Other(node.kind()), inner: Rc::new(node), - drilldown_parent_kind: None, + drilldown_name: None, parent: None, }) .collect::>(), @@ -262,7 +262,7 @@ pub async fn doc( } struct DocResolver { - deno_ns: std::collections::HashSet>, + deno_ns: std::collections::HashMap, Option>>, strip_trailing_html: bool, } @@ -286,7 +286,7 @@ impl deno_doc::html::HrefResolver for DocResolver { } fn resolve_global_symbol(&self, symbol: &[String]) -> Option { - if self.deno_ns.contains(symbol) { + if self.deno_ns.contains_key(symbol) { Some(format!( "https://deno.land/api@v{}?s={}", env!("CARGO_PKG_VERSION"), @@ -455,7 +455,7 @@ impl deno_doc::html::HrefResolver for NodeDocResolver { fn generate_docs_directory( doc_nodes_by_url: IndexMap>, html_options: &DocHtmlFlag, - deno_ns: std::collections::HashSet>, + deno_ns: std::collections::HashMap, Option>>, rewrite_map: Option>, ) -> Result<(), AnyError> { let cwd = std::env::current_dir().context("Failed to get CWD")?; @@ -513,7 +513,6 @@ fn generate_docs_directory( rewrite_map, href_resolver, usage_composer: None, - composable_output: false, category_docs, disable_search: internal_env.is_some(), symbol_redirect_map, diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs index 8a4bc4e6c8..56b1632cfb 100644 --- a/cli/tools/fmt.rs +++ b/cli/tools/fmt.rs @@ -353,6 +353,21 @@ fn format_yaml( file_text: &str, fmt_options: &FmtOptionsConfig, ) -> Result, AnyError> { + let ignore_file = file_text + .lines() + .take_while(|line| line.starts_with('#')) + .any(|line| { + line + .strip_prefix('#') + .unwrap() + .trim() + .starts_with("deno-fmt-ignore-file") + }); + + if ignore_file { + return Ok(None); + } + let formatted_str = pretty_yaml::format_text(file_text, &get_resolved_yaml_config(fmt_options)) .map_err(AnyError::from)?; @@ -775,28 +790,26 @@ fn format_ensure_stable( return Ok(Some(current_text)); } Err(err) => { - panic!( + bail!( concat!( "Formatting succeeded initially, but failed when ensuring a ", "stable format. This indicates a bug in the formatter where ", "the text it produces is not syntactically correct. As a temporary ", - "workaround you can ignore this file ({}).\n\n{:#}" + "workaround you can ignore this file.\n\n{:#}" ), - file_path.display(), err, ) } } count += 1; if count == 5 { - panic!( + bail!( concat!( "Formatting not stable. Bailed after {} tries. This indicates a bug ", - "in the formatter where it formats the file ({}) differently each time. As a ", + "in the formatter where it formats the file differently each time. As a ", "temporary workaround you can ignore this file." ), count, - file_path.display(), ) } } @@ -978,6 +991,7 @@ fn get_resolved_malva_config( single_line_top_level_declarations: false, selector_override_comment_directive: "deno-fmt-selector-override".into(), ignore_comment_directive: "deno-fmt-ignore".into(), + ignore_file_comment_directive: "deno-fmt-ignore-file".into(), }; FormatOptions { @@ -1016,7 +1030,7 @@ fn get_resolved_markup_fmt_config( max_attrs_per_line: None, prefer_attrs_single_line: false, html_normal_self_closing: None, - html_void_self_closing: Some(true), + html_void_self_closing: None, component_self_closing: None, svg_self_closing: None, mathml_self_closing: None, @@ -1036,6 +1050,7 @@ fn get_resolved_markup_fmt_config( svelte_directive_shorthand: Some(true), astro_attr_shorthand: Some(true), ignore_comment_directive: "deno-fmt-ignore".into(), + ignore_file_comment_directive: "deno-fmt-ignore-file".into(), }; FormatOptions { @@ -1198,6 +1213,8 @@ fn is_supported_ext_fmt(path: &Path) -> bool { #[cfg(test)] mod test { + use test_util::assert_starts_with; + use super::*; #[test] @@ -1253,12 +1270,16 @@ mod test { } #[test] - #[should_panic(expected = "Formatting not stable. Bailed after 5 tries.")] fn test_format_ensure_stable_unstable_format() { - format_ensure_stable(&PathBuf::from("mod.ts"), "1", |_, file_text| { - Ok(Some(format!("1{file_text}"))) - }) - .unwrap(); + let err = + format_ensure_stable(&PathBuf::from("mod.ts"), "1", |_, file_text| { + Ok(Some(format!("1{file_text}"))) + }) + .unwrap_err(); + assert_starts_with!( + err.to_string(), + "Formatting not stable. Bailed after 5 tries." + ); } #[test] @@ -1272,16 +1293,20 @@ mod test { } #[test] - #[should_panic(expected = "Formatting succeeded initially, but failed when")] fn test_format_ensure_stable_error_second() { - format_ensure_stable(&PathBuf::from("mod.ts"), "1", |_, file_text| { - if file_text == "1" { - Ok(Some("11".to_string())) - } else { - bail!("Error formatting.") - } - }) - .unwrap(); + let err = + format_ensure_stable(&PathBuf::from("mod.ts"), "1", |_, file_text| { + if file_text == "1" { + Ok(Some("11".to_string())) + } else { + bail!("Error formatting.") + } + }) + .unwrap_err(); + assert_starts_with!( + err.to_string(), + "Formatting succeeded initially, but failed when" + ); } #[test] diff --git a/cli/tools/info.rs b/cli/tools/info.rs index 1c83abe3bb..c2f5a8cb8d 100644 --- a/cli/tools/info.rs +++ b/cli/tools/info.rs @@ -11,12 +11,14 @@ use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::resolve_url_or_path; use deno_core::serde_json; +use deno_core::url; use deno_graph::Dependency; use deno_graph::GraphKind; use deno_graph::Module; use deno_graph::ModuleError; use deno_graph::ModuleGraph; use deno_graph::Resolution; +use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::resolution::NpmResolutionSnapshot; use deno_npm::NpmPackageId; use deno_npm::NpmResolutionPackage; @@ -47,20 +49,23 @@ pub async fn info( let module_graph_creator = factory.module_graph_creator().await?; let npm_resolver = factory.npm_resolver().await?; let maybe_lockfile = cli_options.maybe_lockfile(); + let npmrc = cli_options.npmrc(); let resolver = factory.workspace_resolver().await?; - let maybe_import_specifier = - if let Some(import_map) = resolver.maybe_import_map() { - if let Ok(imports_specifier) = - import_map.resolve(&specifier, import_map.base_url()) - { - Some(imports_specifier) - } else { - None - } + let cwd_url = + url::Url::from_directory_path(cli_options.initial_cwd()).unwrap(); + + let maybe_import_specifier = if let Some(import_map) = + resolver.maybe_import_map() + { + if let Ok(imports_specifier) = import_map.resolve(&specifier, &cwd_url) { + Some(imports_specifier) } else { None - }; + } + } else { + None + }; let specifier = match maybe_import_specifier { Some(specifier) => specifier, @@ -88,7 +93,8 @@ pub async fn info( JSON_SCHEMA_VERSION.into(), ); } - add_npm_packages_to_json(&mut json_graph, npm_resolver.as_ref()); + + add_npm_packages_to_json(&mut json_graph, npm_resolver.as_ref(), npmrc); display::write_json_to_stdout(&json_graph)?; } else { let mut output = String::new(); @@ -185,6 +191,7 @@ fn print_cache_info( fn add_npm_packages_to_json( json: &mut serde_json::Value, npm_resolver: &dyn CliNpmResolver, + npmrc: &ResolvedNpmRc, ) { let Some(npm_resolver) = npm_resolver.as_managed() else { return; // does not include byonm to deno info's output @@ -195,45 +202,28 @@ fn add_npm_packages_to_json( let json = json.as_object_mut().unwrap(); let modules = json.get_mut("modules").and_then(|m| m.as_array_mut()); if let Some(modules) = modules { - if modules.len() == 1 - && modules[0].get("kind").and_then(|k| k.as_str()) == Some("npm") - { - // If there is only one module and it's "external", then that means - // someone provided an npm specifier as a cli argument. In this case, - // we want to show which npm package the cli argument resolved to. - let module = &mut modules[0]; - let maybe_package = module - .get("specifier") - .and_then(|k| k.as_str()) - .and_then(|specifier| NpmPackageNvReference::from_str(specifier).ok()) - .and_then(|package_ref| { - snapshot - .resolve_package_from_deno_module(package_ref.nv()) - .ok() - }); - if let Some(pkg) = maybe_package { - if let Some(module) = module.as_object_mut() { - module - .insert("npmPackage".to_string(), pkg.id.as_serialized().into()); - } - } - } else { - // Filter out npm package references from the modules and instead - // have them only listed as dependencies. This is done because various - // npm specifiers modules in the graph are really just unresolved - // references. So there could be listed multiple npm specifiers - // that would resolve to a single npm package. - for i in (0..modules.len()).rev() { - if matches!( - modules[i].get("kind").and_then(|k| k.as_str()), - Some("npm") | Some("external") - ) { - modules.remove(i); - } - } - } - for module in modules.iter_mut() { + if matches!(module.get("kind").and_then(|k| k.as_str()), Some("npm")) { + // If there is only one module and it's "external", then that means + // someone provided an npm specifier as a cli argument. In this case, + // we want to show which npm package the cli argument resolved to. + let maybe_package = module + .get("specifier") + .and_then(|k| k.as_str()) + .and_then(|specifier| NpmPackageNvReference::from_str(specifier).ok()) + .and_then(|package_ref| { + snapshot + .resolve_package_from_deno_module(package_ref.nv()) + .ok() + }); + if let Some(pkg) = maybe_package { + if let Some(module) = module.as_object_mut() { + module + .insert("npmPackage".to_string(), pkg.id.as_serialized().into()); + } + } + } + let dependencies = module .get_mut("dependencies") .and_then(|d| d.as_array_mut()); @@ -265,7 +255,7 @@ fn add_npm_packages_to_json( let mut json_packages = serde_json::Map::with_capacity(sorted_packages.len()); for pkg in sorted_packages { let mut kv = serde_json::Map::new(); - kv.insert("name".to_string(), pkg.id.nv.name.to_string().into()); + kv.insert("name".to_string(), pkg.id.nv.name.clone().into()); kv.insert("version".to_string(), pkg.id.nv.version.to_string().into()); let mut deps = pkg.dependencies.values().collect::>(); deps.sort(); @@ -274,6 +264,8 @@ fn add_npm_packages_to_json( .map(|id| serde_json::Value::String(id.as_serialized())) .collect::>(); kv.insert("dependencies".to_string(), deps.into()); + let registry_url = npmrc.get_registry_url(&pkg.id.nv.name); + kv.insert("registryUrl".to_string(), registry_url.to_string().into()); json_packages.insert(pkg.id.as_serialized(), kv.into()); } @@ -538,7 +530,7 @@ impl<'a> GraphDisplayContext<'a> { fn build_module_info(&mut self, module: &Module, type_dep: bool) -> TreeNode { enum PackageOrSpecifier { - Package(NpmResolutionPackage), + Package(Box), Specifier(ModuleSpecifier), } @@ -546,7 +538,7 @@ impl<'a> GraphDisplayContext<'a> { let package_or_specifier = match module.npm() { Some(npm) => match self.npm_info.resolve_package(npm.nv_reference.nv()) { - Some(package) => Package(package.clone()), + Some(package) => Package(Box::new(package.clone())), None => Specifier(module.specifier().clone()), // should never happen }, None => Specifier(module.specifier().clone()), @@ -653,10 +645,12 @@ impl<'a> GraphDisplayContext<'a> { let message = match err { HttpsChecksumIntegrity(_) => "(checksum integrity error)", Decode(_) => "(loading decode error)", - Loader(err) => match deno_core::error::get_custom_error_class(err) { - Some("NotCapable") => "(not capable, requires --allow-import)", - _ => "(loading error)", - }, + Loader(err) => { + match deno_runtime::errors::get_error_class_name(err) { + Some("NotCapable") => "(not capable, requires --allow-import)", + _ => "(loading error)", + } + } Jsr(_) => "(loading error)", NodeUnknownBuiltinModule(_) => "(unknown node built-in error)", Npm(_) => "(npm loading error)", diff --git a/cli/tools/init/mod.rs b/cli/tools/init/mod.rs index 2d6a894e13..4e4a686c5f 100644 --- a/cli/tools/init/mod.rs +++ b/cli/tools/init/mod.rs @@ -24,32 +24,29 @@ pub fn init_project(init_flags: InitFlags) -> Result<(), AnyError> { create_file( &dir, "main.ts", - r#"import { type Route, route, serveDir } from "@std/http"; + r#"import { serveDir } from "@std/http"; -const routes: Route[] = [ - { - pattern: new URLPattern({ pathname: "/" }), - handler: () => new Response("Home page"), - }, - { - pattern: new URLPattern({ pathname: "/users/:id" }), - handler: (_req, _info, params) => new Response(params?.pathname.groups.id), - }, - { - pattern: new URLPattern({ pathname: "/static/*" }), - handler: (req) => serveDir(req), - }, -]; - -function defaultHandler(_req: Request) { - return new Response("Not found", { status: 404 }); -} - -const handler = route(routes, defaultHandler); +const userPagePattern = new URLPattern({ pathname: "/users/:id" }); +const staticPathPattern = new URLPattern({ pathname: "/static/*" }); export default { fetch(req) { - return handler(req); + const url = new URL(req.url); + + if (url.pathname === "/") { + return new Response("Home page"); + } + + const userPageMatch = userPagePattern.exec(url); + if (userPageMatch) { + return new Response(userPageMatch.pathname.groups.id); + } + + if (staticPathPattern.test(url)) { + return serveDir(req); + } + + return new Response("Not found", { status: 404 }); }, } satisfies Deno.ServeDefaultExport; "#, diff --git a/cli/tools/installer.rs b/cli/tools/installer.rs index bc8769de1c..1655f0a322 100644 --- a/cli/tools/installer.rs +++ b/cli/tools/installer.rs @@ -298,6 +298,10 @@ async fn install_local( } InstallFlagsLocal::TopLevel => { let factory = CliFactory::from_flags(flags); + // surface any errors in the package.json + if let Some(npm_resolver) = factory.npm_resolver().await?.as_managed() { + npm_resolver.ensure_no_pkg_json_dep_errors()?; + } crate::tools::registry::cache_top_level_deps(&factory, None).await?; if let Some(lockfile) = factory.cli_options()?.maybe_lockfile() { @@ -1392,6 +1396,7 @@ mod tests { .env_clear() // use the deno binary in the target directory .env("PATH", test_util::target_dir()) + .env("RUST_BACKTRACE", "1") .spawn() .unwrap() .wait() diff --git a/cli/tools/jupyter/install.rs b/cli/tools/jupyter/install.rs index b0ddc948d6..aeff89ccf4 100644 --- a/cli/tools/jupyter/install.rs +++ b/cli/tools/jupyter/install.rs @@ -58,9 +58,9 @@ pub fn install() -> Result<(), AnyError> { let f = std::fs::File::create(kernel_json_path)?; serde_json::to_writer_pretty(f, &json_data)?; - install_icon(&user_data_dir, "logo-32x32.png", DENO_ICON_32)?; - install_icon(&user_data_dir, "logo-64x64.png", DENO_ICON_64)?; - install_icon(&user_data_dir, "logo-svg.svg", DENO_ICON_SVG)?; + install_icon(&kernel_dir, "logo-32x32.png", DENO_ICON_32)?; + install_icon(&kernel_dir, "logo-64x64.png", DENO_ICON_64)?; + install_icon(&kernel_dir, "logo-svg.svg", DENO_ICON_SVG)?; log::info!("✅ Deno kernelspec installed successfully."); Ok(()) diff --git a/cli/tools/jupyter/mod.rs b/cli/tools/jupyter/mod.rs index 71e947dddb..732f95c49f 100644 --- a/cli/tools/jupyter/mod.rs +++ b/cli/tools/jupyter/mod.rs @@ -61,7 +61,7 @@ pub async fn kernel( let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let main_module = - resolve_url_or_path("./$deno$jupyter.ts", cli_options.initial_cwd()) + resolve_url_or_path("./$deno$jupyter.mts", cli_options.initial_cwd()) .unwrap(); // TODO(bartlomieju): should we run with all permissions? let permissions = @@ -357,56 +357,74 @@ pub struct JupyterReplSession { impl JupyterReplSession { pub async fn start(&mut self) { + let mut poll_worker = true; loop { - let Some(msg) = self.rx.recv().await else { - break; - }; - let resp = match msg { - JupyterReplRequest::LspCompletions { - line_text, - position, - } => JupyterReplResponse::LspCompletions( - self.lsp_completions(&line_text, position).await, - ), - JupyterReplRequest::JsGetProperties { object_id } => { - JupyterReplResponse::JsGetProperties( - self.get_properties(object_id).await, - ) - } - JupyterReplRequest::JsEvaluate { expr } => { - JupyterReplResponse::JsEvaluate(self.evaluate(expr).await) - } - JupyterReplRequest::JsGlobalLexicalScopeNames => { - JupyterReplResponse::JsGlobalLexicalScopeNames( - self.global_lexical_scope_names().await, - ) - } - JupyterReplRequest::JsEvaluateLineWithObjectWrapping { line } => { - JupyterReplResponse::JsEvaluateLineWithObjectWrapping( - self.evaluate_line_with_object_wrapping(&line).await, - ) - } - JupyterReplRequest::JsCallFunctionOnArgs { - function_declaration, - args, - } => JupyterReplResponse::JsCallFunctionOnArgs( - self - .call_function_on_args(function_declaration, &args) - .await, - ), - JupyterReplRequest::JsCallFunctionOn { arg0, arg1 } => { - JupyterReplResponse::JsCallFunctionOn( - self.call_function_on(arg0, arg1).await, - ) - } - }; + tokio::select! { + biased; - let Ok(()) = self.tx.send(resp) else { - break; - }; + maybe_message = self.rx.recv() => { + let Some(msg) = maybe_message else { + break; + }; + if self.handle_message(msg).await.is_err() { + break; + } + poll_worker = true; + }, + _ = self.repl_session.run_event_loop(), if poll_worker => { + poll_worker = false; + } + } } } + async fn handle_message( + &mut self, + msg: JupyterReplRequest, + ) -> Result<(), AnyError> { + let resp = match msg { + JupyterReplRequest::LspCompletions { + line_text, + position, + } => JupyterReplResponse::LspCompletions( + self.lsp_completions(&line_text, position).await, + ), + JupyterReplRequest::JsGetProperties { object_id } => { + JupyterReplResponse::JsGetProperties( + self.get_properties(object_id).await, + ) + } + JupyterReplRequest::JsEvaluate { expr } => { + JupyterReplResponse::JsEvaluate(self.evaluate(expr).await) + } + JupyterReplRequest::JsGlobalLexicalScopeNames => { + JupyterReplResponse::JsGlobalLexicalScopeNames( + self.global_lexical_scope_names().await, + ) + } + JupyterReplRequest::JsEvaluateLineWithObjectWrapping { line } => { + JupyterReplResponse::JsEvaluateLineWithObjectWrapping( + self.evaluate_line_with_object_wrapping(&line).await, + ) + } + JupyterReplRequest::JsCallFunctionOnArgs { + function_declaration, + args, + } => JupyterReplResponse::JsCallFunctionOnArgs( + self + .call_function_on_args(function_declaration, &args) + .await, + ), + JupyterReplRequest::JsCallFunctionOn { arg0, arg1 } => { + JupyterReplResponse::JsCallFunctionOn( + self.call_function_on(arg0, arg1).await, + ) + } + }; + + self.tx.send(resp).map_err(|e| e.into()) + } + pub async fn lsp_completions( &mut self, line_text: &str, diff --git a/cli/tools/jupyter/resources/deno-logo-32x32.png b/cli/tools/jupyter/resources/deno-logo-32x32.png index 97871a02ee..d59f251a24 100644 Binary files a/cli/tools/jupyter/resources/deno-logo-32x32.png and b/cli/tools/jupyter/resources/deno-logo-32x32.png differ diff --git a/cli/tools/jupyter/resources/deno-logo-64x64.png b/cli/tools/jupyter/resources/deno-logo-64x64.png index 1b9444ef63..37e98abafd 100644 Binary files a/cli/tools/jupyter/resources/deno-logo-64x64.png and b/cli/tools/jupyter/resources/deno-logo-64x64.png differ diff --git a/cli/tools/jupyter/resources/deno-logo-svg.svg b/cli/tools/jupyter/resources/deno-logo-svg.svg index d7bb9ef804..fbc22cd910 100644 --- a/cli/tools/jupyter/resources/deno-logo-svg.svg +++ b/cli/tools/jupyter/resources/deno-logo-svg.svg @@ -1 +1,17 @@ - + + + + + + + + + + + diff --git a/cli/tools/jupyter/server.rs b/cli/tools/jupyter/server.rs index 0cd80f7ddd..5680ed4c13 100644 --- a/cli/tools/jupyter/server.rs +++ b/cli/tools/jupyter/server.rs @@ -329,7 +329,12 @@ impl JupyterServer { }) .collect(); - (candidates, cursor_pos - prop_name.len()) + if prop_name.len() > cursor_pos { + // TODO(bartlomieju): most likely not correct, but better than panicking because of sub with overflow + (candidates, cursor_pos) + } else { + (candidates, cursor_pos - prop_name.len()) + } } else { // combine results of declarations and globalThis properties let mut candidates = get_expression_property_names( @@ -349,7 +354,12 @@ impl JupyterServer { candidates.sort(); candidates.dedup(); // make sure to sort first - (candidates, cursor_pos - expr.len()) + if expr.len() > cursor_pos { + // TODO(bartlomieju): most likely not correct, but better than panicking because of sub with overflow + (candidates, cursor_pos) + } else { + (candidates, cursor_pos - expr.len()) + } }; connection diff --git a/cli/tools/lint/mod.rs b/cli/tools/lint/mod.rs index e096b486ef..36f1cc0491 100644 --- a/cli/tools/lint/mod.rs +++ b/cli/tools/lint/mod.rs @@ -63,7 +63,7 @@ pub use rules::LintRuleProvider; const JSON_SCHEMA_VERSION: u8 = 1; -static STDIN_FILE_NAME: &str = "$deno$stdin.ts"; +static STDIN_FILE_NAME: &str = "$deno$stdin.mts"; pub async fn lint( flags: Arc, @@ -191,7 +191,7 @@ pub async fn lint( linter.finish() }; if !success { - std::process::exit(1); + deno_runtime::exit(1); } } diff --git a/cli/tools/lint/rules/mod.rs b/cli/tools/lint/rules/mod.rs index 2669ffda15..dd723ad159 100644 --- a/cli/tools/lint/rules/mod.rs +++ b/cli/tools/lint/rules/mod.rs @@ -14,7 +14,7 @@ use deno_graph::ModuleGraph; use deno_lint::diagnostic::LintDiagnostic; use deno_lint::rules::LintRule; -use crate::resolver::SloppyImportsResolver; +use crate::resolver::CliSloppyImportsResolver; mod no_sloppy_imports; mod no_slow_types; @@ -144,13 +144,13 @@ impl ConfiguredRules { } pub struct LintRuleProvider { - sloppy_imports_resolver: Option>, + sloppy_imports_resolver: Option>, workspace_resolver: Option>, } impl LintRuleProvider { pub fn new( - sloppy_imports_resolver: Option>, + sloppy_imports_resolver: Option>, workspace_resolver: Option>, ) -> Self { Self { diff --git a/cli/tools/lint/rules/no_sloppy_imports.rs b/cli/tools/lint/rules/no_sloppy_imports.rs index 4180be5be1..94bf9a7c67 100644 --- a/cli/tools/lint/rules/no_sloppy_imports.rs +++ b/cli/tools/lint/rules/no_sloppy_imports.rs @@ -16,24 +16,25 @@ use deno_lint::diagnostic::LintDiagnosticRange; use deno_lint::diagnostic::LintFix; use deno_lint::diagnostic::LintFixChange; use deno_lint::rules::LintRule; +use deno_resolver::sloppy_imports::SloppyImportsResolution; +use deno_resolver::sloppy_imports::SloppyImportsResolutionMode; use text_lines::LineAndColumnIndex; use crate::graph_util::CliJsrUrlProvider; -use crate::resolver::SloppyImportsResolution; -use crate::resolver::SloppyImportsResolver; +use crate::resolver::CliSloppyImportsResolver; use super::ExtendedLintRule; #[derive(Debug)] pub struct NoSloppyImportsRule { - sloppy_imports_resolver: Option>, + sloppy_imports_resolver: Option>, // None for making printing out the lint rules easy workspace_resolver: Option>, } impl NoSloppyImportsRule { pub fn new( - sloppy_imports_resolver: Option>, + sloppy_imports_resolver: Option>, workspace_resolver: Option>, ) -> Self { NoSloppyImportsRule { @@ -86,6 +87,7 @@ impl LintRule for NoSloppyImportsRule { captures: Default::default(), }; + // fill this and capture the sloppy imports in the resolver deno_graph::parse_module_from_ast(deno_graph::ParseModuleFromAstOptions { graph_kind: deno_graph::GraphKind::All, specifier: context.specifier().clone(), @@ -172,7 +174,7 @@ impl LintRule for NoSloppyImportsRule { #[derive(Debug)] struct SloppyImportCaptureResolver<'a> { workspace_resolver: &'a WorkspaceResolver, - sloppy_imports_resolver: &'a SloppyImportsResolver, + sloppy_imports_resolver: &'a CliSloppyImportsResolver, captures: RefCell>, } @@ -194,7 +196,13 @@ impl<'a> deno_graph::source::Resolver for SloppyImportCaptureResolver<'a> { } | deno_config::workspace::MappedResolution::ImportMap { specifier, .. - } => match self.sloppy_imports_resolver.resolve(&specifier, mode) { + } => match self.sloppy_imports_resolver.resolve( + &specifier, + match mode { + ResolutionMode::Execution => SloppyImportsResolutionMode::Execution, + ResolutionMode::Types => SloppyImportsResolutionMode::Types, + }, + ) { Some(res) => { self .captures diff --git a/cli/tools/registry/mod.rs b/cli/tools/registry/mod.rs index 941514b045..4098d62e37 100644 --- a/cli/tools/registry/mod.rs +++ b/cli/tools/registry/mod.rs @@ -43,7 +43,8 @@ use crate::cache::ParsedSourceCache; use crate::factory::CliFactory; use crate::graph_util::ModuleGraphCreator; use crate::http_util::HttpClient; -use crate::resolver::SloppyImportsResolver; +use crate::resolver::CliSloppyImportsResolver; +use crate::resolver::SloppyImportsCachedFs; use crate::tools::check::CheckOptions; use crate::tools::lint::collect_no_slow_type_diagnostics; use crate::tools::registry::diagnostics::PublishDiagnostic; @@ -108,7 +109,9 @@ pub async fn publish( } let specifier_unfurler = Arc::new(SpecifierUnfurler::new( if cli_options.unstable_sloppy_imports() { - Some(SloppyImportsResolver::new(cli_factory.fs().clone())) + Some(CliSloppyImportsResolver::new(SloppyImportsCachedFs::new( + cli_factory.fs().clone(), + ))) } else { None }, diff --git a/cli/tools/registry/pm.rs b/cli/tools/registry/pm.rs index c92710f460..68913e2591 100644 --- a/cli/tools/registry/pm.rs +++ b/cli/tools/registry/pm.rs @@ -1,32 +1,25 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -mod cache_deps; - -pub use cache_deps::cache_top_level_deps; -use deno_semver::jsr::JsrPackageReqReference; -use deno_semver::npm::NpmPackageReqReference; -use deno_semver::VersionReq; - -use std::borrow::Cow; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; -use deno_ast::TextChange; -use deno_config::deno_json::FmtOptionsConfig; -use deno_core::anyhow::anyhow; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::futures::StreamExt; -use deno_core::serde_json; -use deno_core::ModuleSpecifier; -use deno_runtime::deno_node; +use deno_path_util::url_to_file_path; +use deno_semver::jsr::JsrPackageReqReference; +use deno_semver::npm::NpmPackageReqReference; +use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; -use indexmap::IndexMap; -use jsonc_parser::ast::ObjectProp; -use jsonc_parser::ast::Value; -use yoke::Yoke; +use deno_semver::Version; +use deno_semver::VersionReq; +use jsonc_parser::cst::CstObject; +use jsonc_parser::cst::CstObjectProp; +use jsonc_parser::cst::CstRootNode; +use jsonc_parser::json; use crate::args::AddFlags; use crate::args::CacheSetting; @@ -38,234 +31,181 @@ use crate::file_fetcher::FileFetcher; use crate::jsr::JsrFetchResolver; use crate::npm::NpmFetchResolver; -enum DenoConfigFormat { - Json, - Jsonc, +mod cache_deps; + +pub use cache_deps::cache_top_level_deps; + +#[derive(Debug, Copy, Clone)] +enum ConfigKind { + DenoJson, + PackageJson, } -impl DenoConfigFormat { - fn from_specifier(spec: &ModuleSpecifier) -> Result { - let file_name = spec - .path_segments() - .ok_or_else(|| anyhow!("Empty path in deno config specifier: {spec}"))? - .last() - .unwrap(); - match file_name { - "deno.json" => Ok(Self::Json), - "deno.jsonc" => Ok(Self::Jsonc), - _ => bail!("Unsupported deno config file: {file_name}"), - } - } -} - -struct DenoConfig { - config: Arc, - format: DenoConfigFormat, - imports: IndexMap, -} - -fn deno_json_imports( - config: &deno_config::deno_json::ConfigFile, -) -> Result, AnyError> { - Ok( - config - .json - .imports - .clone() - .map(|imports| { - serde_json::from_value(imports) - .map_err(|err| anyhow!("Malformed \"imports\" configuration: {err}")) - }) - .transpose()? - .unwrap_or_default(), - ) -} -impl DenoConfig { - fn from_options(options: &CliOptions) -> Result, AnyError> { - let start_dir = &options.start_dir; - if let Some(config) = start_dir.maybe_deno_json() { - Ok(Some(Self { - imports: deno_json_imports(config)?, - config: config.clone(), - format: DenoConfigFormat::from_specifier(&config.specifier)?, - })) - } else { - Ok(None) - } - } - - fn add(&mut self, selected: SelectedPackage) { - self.imports.insert( - selected.import_name, - format!("{}@{}", selected.package_name, selected.version_req), - ); - } - - fn remove(&mut self, package: &str) -> bool { - self.imports.shift_remove(package).is_some() - } - - fn take_import_fields( - &mut self, - ) -> Vec<(&'static str, IndexMap)> { - vec![("imports", std::mem::take(&mut self.imports))] - } -} - -impl NpmConfig { - fn from_options(options: &CliOptions) -> Result, AnyError> { - let start_dir = &options.start_dir; - if let Some(pkg_json) = start_dir.maybe_pkg_json() { - Ok(Some(Self { - dependencies: pkg_json.dependencies.clone().unwrap_or_default(), - dev_dependencies: pkg_json.dev_dependencies.clone().unwrap_or_default(), - config: pkg_json.clone(), - fmt_options: None, - })) - } else { - Ok(None) - } - } - - fn add(&mut self, selected: SelectedPackage, dev: bool) { - let (name, version) = package_json_dependency_entry(selected); - if dev { - self.dev_dependencies.insert(name, version); - } else { - self.dependencies.insert(name, version); - } - } - - fn remove(&mut self, package: &str) -> bool { - let in_deps = self.dependencies.shift_remove(package).is_some(); - let in_dev_deps = self.dev_dependencies.shift_remove(package).is_some(); - in_deps || in_dev_deps - } - - fn take_import_fields( - &mut self, - ) -> Vec<(&'static str, IndexMap)> { - vec![ - ("dependencies", std::mem::take(&mut self.dependencies)), - ( - "devDependencies", - std::mem::take(&mut self.dev_dependencies), - ), - ] - } -} - -struct NpmConfig { - config: Arc, - fmt_options: Option, - dependencies: IndexMap, - dev_dependencies: IndexMap, -} - -enum DenoOrPackageJson { - Deno(DenoConfig), - Npm(NpmConfig), -} - -impl From for DenoOrPackageJson { - fn from(config: DenoConfig) -> Self { - Self::Deno(config) - } -} - -impl From for DenoOrPackageJson { - fn from(config: NpmConfig) -> Self { - Self::Npm(config) - } -} - -/// Wrapper around `jsonc_parser::ast::Object` that can be stored in a `Yoke` -#[derive(yoke::Yokeable)] -struct JsoncObjectView<'a>(jsonc_parser::ast::Object<'a>); - struct ConfigUpdater { - config: DenoOrPackageJson, - // the `Yoke` is so we can carry the parsed object (which borrows from - // the source) along with the source itself - ast: Yoke, String>, + kind: ConfigKind, + cst: CstRootNode, + root_object: CstObject, path: PathBuf, modified: bool, } impl ConfigUpdater { - fn obj(&self) -> &jsonc_parser::ast::Object<'_> { - &self.ast.get().0 - } - fn contents(&self) -> &str { - self.ast.backing_cart() - } - async fn maybe_new( - config: Option>, - ) -> Result, AnyError> { - if let Some(config) = config { - Ok(Some(Self::new(config.into()).await?)) - } else { - Ok(None) - } - } - async fn new(config: DenoOrPackageJson) -> Result { - let specifier = config.specifier(); - if specifier.scheme() != "file" { - bail!("Can't update a remote configuration file"); - } - let config_file_path = specifier.to_file_path().map_err(|_| { - anyhow!("Specifier {specifier:?} is an invalid file path") - })?; - let config_file_contents = { - let contents = tokio::fs::read_to_string(&config_file_path) - .await - .with_context(|| { - format!("Reading config file at: {}", config_file_path.display()) - })?; - if contents.trim().is_empty() { - "{}\n".into() - } else { - contents - } - }; - let ast = Yoke::try_attach_to_cart(config_file_contents, |contents| { - let ast = jsonc_parser::parse_to_ast( - contents, - &Default::default(), - &Default::default(), - ) + fn new( + kind: ConfigKind, + config_file_path: PathBuf, + ) -> Result { + let config_file_contents = std::fs::read_to_string(&config_file_path) .with_context(|| { - format!("Failed to parse config file at {}", specifier) + format!("Reading config file '{}'", config_file_path.display()) })?; - let obj = match ast.value { - Some(Value::Object(obj)) => obj, - _ => bail!( - "Failed to update config file at {}, expected an object", - specifier - ), - }; - Ok(JsoncObjectView(obj)) - })?; + let cst = CstRootNode::parse(&config_file_contents, &Default::default()) + .with_context(|| { + format!("Parsing config file '{}'", config_file_path.display()) + })?; + let root_object = cst.object_value_or_set(); Ok(Self { - config, - ast, + kind, + cst, + root_object, path: config_file_path, modified: false, }) } + fn display_path(&self) -> String { + deno_path_util::url_from_file_path(&self.path) + .map(|u| u.to_string()) + .unwrap_or_else(|_| self.path.display().to_string()) + } + + fn obj(&self) -> &CstObject { + &self.root_object + } + + fn contents(&self) -> String { + self.cst.to_string() + } + fn add(&mut self, selected: SelectedPackage, dev: bool) { - match &mut self.config { - DenoOrPackageJson::Deno(deno) => deno.add(selected), - DenoOrPackageJson::Npm(npm) => npm.add(selected, dev), + fn insert_index(object: &CstObject, searching_name: &str) -> usize { + object + .properties() + .into_iter() + .take_while(|prop| { + let prop_name = + prop.name().and_then(|name| name.decoded_value().ok()); + match prop_name { + Some(current_name) => { + searching_name.cmp(¤t_name) == std::cmp::Ordering::Greater + } + None => true, + } + }) + .count() } + + match self.kind { + ConfigKind::DenoJson => { + let imports = self.root_object.object_value_or_set("imports"); + let value = + format!("{}@{}", selected.package_name, selected.version_req); + if let Some(prop) = imports.get(&selected.import_name) { + prop.set_value(json!(value)); + } else { + let index = insert_index(&imports, &selected.import_name); + imports.insert(index, &selected.import_name, json!(value)); + } + } + ConfigKind::PackageJson => { + let deps_prop = self.root_object.get("dependencies"); + let dev_deps_prop = self.root_object.get("devDependencies"); + + let dependencies = if dev { + self + .root_object + .object_value("devDependencies") + .unwrap_or_else(|| { + let index = deps_prop + .as_ref() + .map(|p| p.property_index() + 1) + .unwrap_or_else(|| self.root_object.properties().len()); + self + .root_object + .insert(index, "devDependencies", json!({})) + .object_value_or_set() + }) + } else { + self + .root_object + .object_value("dependencies") + .unwrap_or_else(|| { + let index = dev_deps_prop + .as_ref() + .map(|p| p.property_index()) + .unwrap_or_else(|| self.root_object.properties().len()); + self + .root_object + .insert(index, "dependencies", json!({})) + .object_value_or_set() + }) + }; + let other_dependencies = if dev { + deps_prop.and_then(|p| p.value().and_then(|v| v.as_object())) + } else { + dev_deps_prop.and_then(|p| p.value().and_then(|v| v.as_object())) + }; + + let (alias, value) = package_json_dependency_entry(selected); + + if let Some(other) = other_dependencies { + if let Some(prop) = other.get(&alias) { + remove_prop_and_maybe_parent_prop(prop); + } + } + + if let Some(prop) = dependencies.get(&alias) { + prop.set_value(json!(value)); + } else { + let index = insert_index(&dependencies, &alias); + dependencies.insert(index, &alias, json!(value)); + } + } + } + self.modified = true; } fn remove(&mut self, package: &str) -> bool { - let removed = match &mut self.config { - DenoOrPackageJson::Deno(deno) => deno.remove(package), - DenoOrPackageJson::Npm(npm) => npm.remove(package), + let removed = match self.kind { + ConfigKind::DenoJson => { + if let Some(prop) = self + .root_object + .object_value("imports") + .and_then(|i| i.get(package)) + { + remove_prop_and_maybe_parent_prop(prop); + true + } else { + false + } + } + ConfigKind::PackageJson => { + let deps = [ + self + .root_object + .object_value("dependencies") + .and_then(|deps| deps.get(package)), + self + .root_object + .object_value("devDependencies") + .and_then(|deps| deps.get(package)), + ]; + let removed = deps.iter().any(|d| d.is_some()); + for dep in deps.into_iter().flatten() { + remove_prop_and_maybe_parent_prop(dep); + } + removed + } }; if removed { self.modified = true; @@ -273,76 +213,28 @@ impl ConfigUpdater { removed } - async fn commit(mut self) -> Result<(), AnyError> { + fn commit(&self) -> Result<(), AnyError> { if !self.modified { return Ok(()); } - let import_fields = self.config.take_import_fields(); - - let fmt_config_options = self.config.fmt_options(); - - let new_text = update_config_file_content( - self.obj(), - self.contents(), - fmt_config_options, - import_fields.into_iter().map(|(k, v)| { - ( - k, - if v.is_empty() { - None - } else { - Some(generate_imports(v.into_iter().collect())) - }, - ) - }), - self.config.file_name(), - ); - - tokio::fs::write(&self.path, new_text).await?; + let new_text = self.contents(); + std::fs::write(&self.path, new_text).with_context(|| { + format!("failed writing to '{}'", self.path.display()) + })?; Ok(()) } } -impl DenoOrPackageJson { - fn specifier(&self) -> Cow { - match self { - Self::Deno(d, ..) => Cow::Borrowed(&d.config.specifier), - Self::Npm(n, ..) => Cow::Owned(n.config.specifier()), - } - } - - fn fmt_options(&self) -> FmtOptionsConfig { - match self { - DenoOrPackageJson::Deno(deno, ..) => deno - .config - .to_fmt_config() - .ok() - .map(|f| f.options) - .unwrap_or_default(), - DenoOrPackageJson::Npm(config) => { - config.fmt_options.clone().unwrap_or_default() - } - } - } - - fn take_import_fields( - &mut self, - ) -> Vec<(&'static str, IndexMap)> { - match self { - Self::Deno(d) => d.take_import_fields(), - Self::Npm(n) => n.take_import_fields(), - } - } - - fn file_name(&self) -> &'static str { - match self { - DenoOrPackageJson::Deno(config) => match config.format { - DenoConfigFormat::Json => "deno.json", - DenoConfigFormat::Jsonc => "deno.jsonc", - }, - DenoOrPackageJson::Npm(..) => "package.json", - } +fn remove_prop_and_maybe_parent_prop(prop: CstObjectProp) { + let parent = prop.parent().unwrap().as_object().unwrap(); + prop.remove(); + if parent.properties().is_empty() { + let parent_property = parent.parent().unwrap(); + let root_object = parent_property.parent().unwrap().as_object().unwrap(); + // remove the property + parent_property.remove(); + root_object.ensure_multiline(); } } @@ -361,7 +253,14 @@ fn package_json_dependency_entry( selected: SelectedPackage, ) -> (String, String) { if let Some(npm_package) = selected.package_name.strip_prefix("npm:") { - (npm_package.into(), selected.version_req) + if selected.import_name == npm_package { + (npm_package.into(), selected.version_req) + } else { + ( + selected.import_name, + format!("npm:{}@{}", npm_package, selected.version_req), + ) + } } else if let Some(jsr_package) = selected.package_name.strip_prefix("jsr:") { let jsr_package = jsr_package.strip_prefix('@').unwrap_or(jsr_package); let scope_replaced = jsr_package.replace('/', "__"); @@ -391,21 +290,45 @@ impl std::fmt::Display for AddCommandName { fn load_configs( flags: &Arc, -) -> Result<(CliFactory, Option, Option), AnyError> { + has_jsr_specifiers: impl FnOnce() -> bool, +) -> Result<(CliFactory, Option, Option), AnyError> +{ let cli_factory = CliFactory::from_flags(flags.clone()); let options = cli_factory.cli_options()?; - let npm_config = NpmConfig::from_options(options)?; - let (cli_factory, deno_config) = match DenoConfig::from_options(options)? { + let start_dir = &options.start_dir; + let npm_config = match start_dir.maybe_pkg_json() { + Some(pkg_json) => Some(ConfigUpdater::new( + ConfigKind::PackageJson, + pkg_json.path.clone(), + )?), + None => None, + }; + let deno_config = match start_dir.maybe_deno_json() { + Some(deno_json) => Some(ConfigUpdater::new( + ConfigKind::DenoJson, + url_to_file_path(&deno_json.specifier)?, + )?), + None => None, + }; + + let (cli_factory, deno_config) = match deno_config { Some(config) => (cli_factory, Some(config)), - None if npm_config.is_some() => (cli_factory, None), - None => { + None if npm_config.is_some() && !has_jsr_specifiers() => { + (cli_factory, None) + } + _ => { let factory = create_deno_json(flags, options)?; let options = factory.cli_options()?.clone(); + let deno_json = options + .start_dir + .maybe_deno_json() + .expect("Just created deno.json"); ( factory, - Some( - DenoConfig::from_options(&options)?.expect("Just created deno.json"), - ), + Some(ConfigUpdater::new( + ConfigKind::DenoJson, + url_to_file_path(&deno_json.specifier)?, + )?), ) } }; @@ -413,18 +336,26 @@ fn load_configs( Ok((cli_factory, npm_config, deno_config)) } +fn path_distance(a: &Path, b: &Path) -> usize { + let diff = pathdiff::diff_paths(a, b); + let Some(diff) = diff else { + return usize::MAX; + }; + diff.components().count() +} + pub async fn add( flags: Arc, add_flags: AddFlags, cmd_name: AddCommandName, ) -> Result<(), AnyError> { - let (cli_factory, npm_config, deno_config) = load_configs(&flags)?; - let mut npm_config = ConfigUpdater::maybe_new(npm_config).await?; - let mut deno_config = ConfigUpdater::maybe_new(deno_config).await?; + let (cli_factory, mut npm_config, mut deno_config) = + load_configs(&flags, || { + add_flags.packages.iter().any(|s| s.starts_with("jsr:")) + })?; if let Some(deno) = &deno_config { - let specifier = deno.config.specifier(); - if deno.obj().get_string("importMap").is_some() { + if deno.obj().get("importMap").is_some() { bail!( concat!( "`deno {}` is not supported when configuration file contains an \"importMap\" field. ", @@ -432,11 +363,26 @@ pub async fn add( " at {}", ), cmd_name, - specifier + deno.display_path(), ); } } + let start_dir = cli_factory.cli_options()?.start_dir.dir_path(); + + // only prefer to add npm deps to `package.json` if there isn't a closer deno.json. + // example: if deno.json is in the CWD and package.json is in the parent, we should add + // npm deps to deno.json, since it's closer + let prefer_npm_config = match (npm_config.as_ref(), deno_config.as_ref()) { + (Some(npm), Some(deno)) => { + let npm_distance = path_distance(&npm.path, &start_dir); + let deno_distance = path_distance(&deno.path, &start_dir); + npm_distance <= deno_distance + } + (Some(_), None) => true, + (None, _) => false, + }; + let http_client = cli_factory.http_client_provider(); let deps_http_cache = cli_factory.global_http_cache()?; let mut deps_file_fetcher = FileFetcher::new( @@ -447,16 +393,20 @@ pub async fn add( Default::default(), None, ); + + let npmrc = cli_factory.cli_options().unwrap().npmrc(); + deps_file_fetcher.set_download_log_level(log::Level::Trace); let deps_file_fetcher = Arc::new(deps_file_fetcher); let jsr_resolver = Arc::new(JsrFetchResolver::new(deps_file_fetcher.clone())); - let npm_resolver = Arc::new(NpmFetchResolver::new(deps_file_fetcher)); + let npm_resolver = + Arc::new(NpmFetchResolver::new(deps_file_fetcher, npmrc.clone())); let mut selected_packages = Vec::with_capacity(add_flags.packages.len()); let mut package_reqs = Vec::with_capacity(add_flags.packages.len()); for entry_text in add_flags.packages.iter() { - let req = AddPackageReq::parse(entry_text).with_context(|| { + let req = AddRmPackageReq::parse(entry_text).with_context(|| { format!("Failed to parse package required: {}", entry_text) })?; @@ -507,15 +457,32 @@ pub async fn add( match package_and_version { PackageAndVersion::NotFound { package: package_name, - found_npm_package, + help, package_req, - } => { - if found_npm_package { - bail!("{} was not found, but a matching npm package exists. Did you mean `{}`?", crate::colors::red(package_name), crate::colors::yellow(format!("deno {cmd_name} npm:{package_req}"))); - } else { - bail!("{} was not found.", crate::colors::red(package_name)); + } => match help { + Some(NotFoundHelp::NpmPackage) => { + bail!( + "{} was not found, but a matching npm package exists. Did you mean `{}`?", + crate::colors::red(package_name), + crate::colors::yellow(format!("deno {cmd_name} npm:{package_req}")) + ); } - } + Some(NotFoundHelp::JsrPackage) => { + bail!( + "{} was not found, but a matching jsr package exists. Did you mean `{}`?", + crate::colors::red(package_name), + crate::colors::yellow(format!("deno {cmd_name} jsr:{package_req}")) + ) + } + Some(NotFoundHelp::PreReleaseVersion(version)) => { + bail!( + "{} has only pre-release versions available. Try specifying a version: `{}`", + crate::colors::red(&package_name), + crate::colors::yellow(format!("deno {cmd_name} {package_name}@^{version}")) + ) + } + None => bail!("{} was not found.", crate::colors::red(package_name)), + }, PackageAndVersion::Selected(selected) => { selected_packages.push(selected); } @@ -531,7 +498,7 @@ pub async fn add( selected_package.selected_version ); - if selected_package.package_name.starts_with("npm:") { + if selected_package.package_name.starts_with("npm:") && prefer_npm_config { if let Some(npm) = &mut npm_config { npm.add(selected_package, dev); } else { @@ -544,26 +511,14 @@ pub async fn add( } } - let mut commit_futures = vec![]; if let Some(npm) = npm_config { - commit_futures.push(npm.commit()); + npm.commit()?; } if let Some(deno) = deno_config { - commit_futures.push(deno.commit()); - } - let commit_futures = - deno_core::futures::future::join_all(commit_futures).await; - - for result in commit_futures { - result.context("Failed to update configuration file")?; + deno.commit()?; } - // clear the previously cached package.json from memory before reloading it - node_resolver::PackageJsonThreadLocalCache::clear(); - // make a new CliFactory to pick up the updated config file - let cli_factory = CliFactory::from_flags(flags); - // cache deps - cache_deps::cache_top_level_deps(&cli_factory, Some(jsr_resolver)).await?; + npm_install_after_modification(flags, Some(jsr_resolver)).await?; Ok(()) } @@ -575,87 +530,161 @@ struct SelectedPackage { selected_version: String, } +enum NotFoundHelp { + NpmPackage, + JsrPackage, + PreReleaseVersion(Version), +} + enum PackageAndVersion { NotFound { package: String, - found_npm_package: bool, package_req: PackageReq, + help: Option, }, Selected(SelectedPackage), } +fn best_version<'a>( + versions: impl Iterator, +) -> Option<&'a Version> { + let mut maybe_best_version: Option<&Version> = None; + for version in versions { + let is_best_version = maybe_best_version + .as_ref() + .map(|best_version| (*best_version).cmp(version).is_lt()) + .unwrap_or(true); + if is_best_version { + maybe_best_version = Some(version); + } + } + maybe_best_version +} + +trait PackageInfoProvider { + const SPECIFIER_PREFIX: &str; + /// The help to return if a package is found by this provider + const HELP: NotFoundHelp; + async fn req_to_nv(&self, req: &PackageReq) -> Option; + async fn latest_version<'a>(&self, req: &PackageReq) -> Option; +} + +impl PackageInfoProvider for Arc { + const HELP: NotFoundHelp = NotFoundHelp::JsrPackage; + const SPECIFIER_PREFIX: &str = "jsr"; + async fn req_to_nv(&self, req: &PackageReq) -> Option { + (**self).req_to_nv(req).await + } + + async fn latest_version<'a>(&self, req: &PackageReq) -> Option { + let info = self.package_info(&req.name).await?; + best_version( + info + .versions + .iter() + .filter(|(_, version_info)| !version_info.yanked) + .map(|(version, _)| version), + ) + .cloned() + } +} + +impl PackageInfoProvider for Arc { + const HELP: NotFoundHelp = NotFoundHelp::NpmPackage; + const SPECIFIER_PREFIX: &str = "npm"; + async fn req_to_nv(&self, req: &PackageReq) -> Option { + (**self).req_to_nv(req).await + } + + async fn latest_version<'a>(&self, req: &PackageReq) -> Option { + let info = self.package_info(&req.name).await?; + best_version(info.versions.keys()).cloned() + } +} + async fn find_package_and_select_version_for_req( jsr_resolver: Arc, npm_resolver: Arc, - add_package_req: AddPackageReq, + add_package_req: AddRmPackageReq, ) -> Result { - match add_package_req.value { - AddPackageReqValue::Jsr(req) => { - let jsr_prefixed_name = format!("jsr:{}", &req.name); - let Some(nv) = jsr_resolver.req_to_nv(&req).await else { - if npm_resolver.req_to_nv(&req).await.is_some() { + async fn select( + main_resolver: T, + fallback_resolver: S, + add_package_req: AddRmPackageReq, + ) -> Result { + let req = match &add_package_req.value { + AddRmPackageReqValue::Jsr(req) => req, + AddRmPackageReqValue::Npm(req) => req, + }; + let prefixed_name = format!("{}:{}", T::SPECIFIER_PREFIX, req.name); + let help_if_found_in_fallback = S::HELP; + let Some(nv) = main_resolver.req_to_nv(req).await else { + if fallback_resolver.req_to_nv(req).await.is_some() { + // it's in the other registry + return Ok(PackageAndVersion::NotFound { + package: prefixed_name, + help: Some(help_if_found_in_fallback), + package_req: req.clone(), + }); + } + if req.version_req.version_text() == "*" { + if let Some(pre_release_version) = + main_resolver.latest_version(req).await + { return Ok(PackageAndVersion::NotFound { - package: jsr_prefixed_name, - found_npm_package: true, - package_req: req, + package: prefixed_name, + package_req: req.clone(), + help: Some(NotFoundHelp::PreReleaseVersion( + pre_release_version.clone(), + )), }); } + } - return Ok(PackageAndVersion::NotFound { - package: jsr_prefixed_name, - found_npm_package: false, - package_req: req, - }); - }; - let range_symbol = if req.version_req.version_text().starts_with('~') { - '~' - } else { - '^' - }; - Ok(PackageAndVersion::Selected(SelectedPackage { - import_name: add_package_req.alias, - package_name: jsr_prefixed_name, - version_req: format!("{}{}", range_symbol, &nv.version), - selected_version: nv.version.to_string(), - })) + return Ok(PackageAndVersion::NotFound { + package: prefixed_name, + help: None, + package_req: req.clone(), + }); + }; + let range_symbol = if req.version_req.version_text().starts_with('~') { + "~" + } else if req.version_req.version_text() == nv.version.to_string() { + "" + } else { + "^" + }; + Ok(PackageAndVersion::Selected(SelectedPackage { + import_name: add_package_req.alias, + package_name: prefixed_name, + version_req: format!("{}{}", range_symbol, &nv.version), + selected_version: nv.version.to_string(), + })) + } + + match &add_package_req.value { + AddRmPackageReqValue::Jsr(_) => { + select(jsr_resolver, npm_resolver, add_package_req).await } - AddPackageReqValue::Npm(req) => { - let npm_prefixed_name = format!("npm:{}", &req.name); - let Some(nv) = npm_resolver.req_to_nv(&req).await else { - return Ok(PackageAndVersion::NotFound { - package: npm_prefixed_name, - found_npm_package: false, - package_req: req, - }); - }; - let range_symbol = if req.version_req.version_text().starts_with('~') { - '~' - } else { - '^' - }; - Ok(PackageAndVersion::Selected(SelectedPackage { - import_name: add_package_req.alias, - package_name: npm_prefixed_name, - version_req: format!("{}{}", range_symbol, &nv.version), - selected_version: nv.version.to_string(), - })) + AddRmPackageReqValue::Npm(_) => { + select(npm_resolver, jsr_resolver, add_package_req).await } } } #[derive(Debug, PartialEq, Eq)] -enum AddPackageReqValue { +enum AddRmPackageReqValue { Jsr(PackageReq), Npm(PackageReq), } #[derive(Debug, PartialEq, Eq)] -struct AddPackageReq { +struct AddRmPackageReq { alias: String, - value: AddPackageReqValue, + value: AddRmPackageReqValue, } -impl AddPackageReq { +impl AddRmPackageReq { pub fn parse(entry_text: &str) -> Result, AnyError> { enum Prefix { Jsr, @@ -710,9 +739,9 @@ impl AddPackageReq { let req_ref = JsrPackageReqReference::from_str(&format!("jsr:{}", entry_text))?; let package_req = req_ref.into_inner().req; - Ok(Ok(AddPackageReq { + Ok(Ok(AddRmPackageReq { alias: maybe_alias.unwrap_or_else(|| package_req.name.to_string()), - value: AddPackageReqValue::Jsr(package_req), + value: AddRmPackageReqValue::Jsr(package_req), })) } Prefix::Npm => { @@ -730,49 +759,48 @@ impl AddPackageReq { deno_semver::RangeSetOrTag::Tag("latest".into()), ); } - Ok(Ok(AddPackageReq { + Ok(Ok(AddRmPackageReq { alias: maybe_alias.unwrap_or_else(|| package_req.name.to_string()), - value: AddPackageReqValue::Npm(package_req), + value: AddRmPackageReqValue::Npm(package_req), })) } } } } -fn generate_imports(mut packages_to_version: Vec<(String, String)>) -> String { - packages_to_version.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); - let mut contents = vec![]; - let len = packages_to_version.len(); - for (index, (package, version)) in packages_to_version.iter().enumerate() { - // TODO(bartlomieju): fix it, once we start support specifying version on the cli - contents.push(format!("\"{}\": \"{}\"", package, version)); - if index != len - 1 { - contents.push(",".to_string()); - } - } - contents.join("\n") -} - pub async fn remove( flags: Arc, remove_flags: RemoveFlags, ) -> Result<(), AnyError> { - let (_, npm_config, deno_config) = load_configs(&flags)?; + let (_, npm_config, deno_config) = load_configs(&flags, || false)?; - let mut configs = [ - ConfigUpdater::maybe_new(npm_config).await?, - ConfigUpdater::maybe_new(deno_config).await?, - ]; + let mut configs = [npm_config, deno_config]; let mut removed_packages = vec![]; for package in &remove_flags.packages { - let mut removed = false; + let req = AddRmPackageReq::parse(package).with_context(|| { + format!("Failed to parse package required: {}", package) + })?; + let mut parsed_pkg_name = None; for config in configs.iter_mut().flatten() { - removed |= config.remove(package); + match &req { + Ok(rm_pkg) => { + if config.remove(&rm_pkg.alias) && parsed_pkg_name.is_none() { + parsed_pkg_name = Some(rm_pkg.alias.clone()); + } + } + Err(pkg) => { + // An alias or a package name without registry/version + // constraints. Try to remove the package anyway. + if config.remove(&pkg.name) && parsed_pkg_name.is_none() { + parsed_pkg_name = Some(pkg.name.clone()); + } + } + } } - if removed { - removed_packages.push(package.clone()); + if let Some(pkg) = parsed_pkg_name { + removed_packages.push(pkg); } } @@ -783,97 +811,38 @@ pub async fn remove( log::info!("Removed {}", crate::colors::green(package)); } for config in configs.into_iter().flatten() { - config.commit().await?; + config.commit()?; } - // Update deno.lock - node_resolver::PackageJsonThreadLocalCache::clear(); - let cli_factory = CliFactory::from_flags(flags); - cache_deps::cache_top_level_deps(&cli_factory, None).await?; + npm_install_after_modification(flags, None).await?; } Ok(()) } -fn update_config_file_content< - I: IntoIterator)>, ->( - obj: &jsonc_parser::ast::Object, - config_file_contents: &str, - fmt_options: FmtOptionsConfig, - entries: I, - file_name: &str, -) -> String { - let mut text_changes = vec![]; - for (key, value) in entries { - match obj.properties.iter().enumerate().find_map(|(idx, k)| { - if k.name.as_str() == key { - Some((idx, k)) - } else { - None - } - }) { - Some(( - idx, - ObjectProp { - value: Value::Object(lit), - range, - .. - }, - )) => { - if let Some(value) = value { - text_changes.push(TextChange { - range: (lit.range.start + 1)..(lit.range.end - 1), - new_text: value, - }) - } else { - text_changes.push(TextChange { - // remove field entirely, making sure to - // remove the comma if it's not the last field - range: range.start..(if idx == obj.properties.len() - 1 { - range.end - } else { - obj.properties[idx + 1].range.start - }), - new_text: "".to_string(), - }) - } - } +async fn npm_install_after_modification( + flags: Arc, + // explicitly provided to prevent redownloading + jsr_resolver: Option>, +) -> Result<(), AnyError> { + // clear the previously cached package.json from memory before reloading it + node_resolver::PackageJsonThreadLocalCache::clear(); - // need to add field - None => { - if let Some(value) = value { - let insert_position = obj.range.end - 1; - text_changes.push(TextChange { - range: insert_position..insert_position, - // NOTE(bartlomieju): adding `\n` here to force the formatter to always - // produce a config file that is multiline, like so: - // ``` - // { - // "imports": { - // "": ":@" - // } - // } - new_text: format!("\"{key}\": {{\n {value} }}"), - }) - } - } - // we verified the shape of `imports`/`dependencies` above - Some(_) => unreachable!(), - } + // make a new CliFactory to pick up the updated config file + let cli_factory = CliFactory::from_flags(flags); + // surface any errors in the package.json + let npm_resolver = cli_factory.npm_resolver().await?; + if let Some(npm_resolver) = npm_resolver.as_managed() { + npm_resolver.ensure_no_pkg_json_dep_errors()?; + } + // npm install + cache_deps::cache_top_level_deps(&cli_factory, jsr_resolver).await?; + + if let Some(lockfile) = cli_factory.cli_options()?.maybe_lockfile() { + lockfile.write_if_changed()?; } - let new_text = - deno_ast::apply_text_changes(config_file_contents, text_changes); - - crate::tools::fmt::format_json( - &PathBuf::from(file_name), - &new_text, - &fmt_options, - ) - .ok() - .map(|formatted_text| formatted_text.unwrap_or_else(|| new_text.clone())) - .unwrap_or(new_text) + Ok(()) } #[cfg(test)] @@ -883,48 +852,52 @@ mod test { #[test] fn test_parse_add_package_req() { assert_eq!( - AddPackageReq::parse("jsr:foo").unwrap().unwrap(), - AddPackageReq { + AddRmPackageReq::parse("jsr:foo").unwrap().unwrap(), + AddRmPackageReq { alias: "foo".to_string(), - value: AddPackageReqValue::Jsr(PackageReq::from_str("foo").unwrap()) + value: AddRmPackageReqValue::Jsr(PackageReq::from_str("foo").unwrap()) } ); assert_eq!( - AddPackageReq::parse("alias@jsr:foo").unwrap().unwrap(), - AddPackageReq { + AddRmPackageReq::parse("alias@jsr:foo").unwrap().unwrap(), + AddRmPackageReq { alias: "alias".to_string(), - value: AddPackageReqValue::Jsr(PackageReq::from_str("foo").unwrap()) + value: AddRmPackageReqValue::Jsr(PackageReq::from_str("foo").unwrap()) } ); assert_eq!( - AddPackageReq::parse("@alias/pkg@npm:foo").unwrap().unwrap(), - AddPackageReq { + AddRmPackageReq::parse("@alias/pkg@npm:foo") + .unwrap() + .unwrap(), + AddRmPackageReq { alias: "@alias/pkg".to_string(), - value: AddPackageReqValue::Npm( + value: AddRmPackageReqValue::Npm( PackageReq::from_str("foo@latest").unwrap() ) } ); assert_eq!( - AddPackageReq::parse("@alias/pkg@jsr:foo").unwrap().unwrap(), - AddPackageReq { + AddRmPackageReq::parse("@alias/pkg@jsr:foo") + .unwrap() + .unwrap(), + AddRmPackageReq { alias: "@alias/pkg".to_string(), - value: AddPackageReqValue::Jsr(PackageReq::from_str("foo").unwrap()) + value: AddRmPackageReqValue::Jsr(PackageReq::from_str("foo").unwrap()) } ); assert_eq!( - AddPackageReq::parse("alias@jsr:foo@^1.5.0") + AddRmPackageReq::parse("alias@jsr:foo@^1.5.0") .unwrap() .unwrap(), - AddPackageReq { + AddRmPackageReq { alias: "alias".to_string(), - value: AddPackageReqValue::Jsr( + value: AddRmPackageReqValue::Jsr( PackageReq::from_str("foo@^1.5.0").unwrap() ) } ); assert_eq!( - AddPackageReq::parse("@scope/pkg@tag") + AddRmPackageReq::parse("@scope/pkg@tag") .unwrap() .unwrap_err() .to_string(), diff --git a/cli/tools/registry/pm/cache_deps.rs b/cli/tools/registry/pm/cache_deps.rs index 7d1773b343..d3c8da868c 100644 --- a/cli/tools/registry/pm/cache_deps.rs +++ b/cli/tools/registry/pm/cache_deps.rs @@ -11,6 +11,7 @@ use deno_core::futures::StreamExt; use deno_semver::package::PackageReq; pub async fn cache_top_level_deps( + // todo(dsherret): don't pass the factory into this function. Instead use ctor deps factory: &CliFactory, jsr_resolver: Option>, ) -> Result<(), AnyError> { @@ -43,7 +44,11 @@ pub async fn cache_top_level_deps( let mut seen_reqs = std::collections::HashSet::new(); - for entry in import_map.imports().entries() { + for entry in import_map.imports().entries().chain( + import_map + .scopes() + .flat_map(|scope| scope.imports.entries()), + ) { let Some(specifier) = entry.value else { continue; }; @@ -74,6 +79,13 @@ pub async fn cache_top_level_deps( if entry.key.ends_with('/') && specifier.as_str().ends_with('/') { continue; } + if specifier.scheme() == "file" { + if let Ok(path) = specifier.to_file_path() { + if !path.is_file() { + continue; + } + } + } roots.push(specifier.clone()); } } @@ -81,10 +93,6 @@ pub async fn cache_top_level_deps( while let Some(info_future) = info_futures.next().await { if let Some((specifier, info)) = info_future { - if info.export(".").is_some() { - roots.push(specifier.clone()); - continue; - } let exports = info.exports(); for (k, _) in exports { if let Ok(spec) = specifier.join(k) { diff --git a/cli/tools/registry/tar.rs b/cli/tools/registry/tar.rs index aca125e00b..6d1801ce69 100644 --- a/cli/tools/registry/tar.rs +++ b/cli/tools/registry/tar.rs @@ -120,7 +120,7 @@ fn resolve_content_maybe_unfurling( | MediaType::Unknown | MediaType::Json | MediaType::Wasm - | MediaType::TsBuildInfo => { + | MediaType::Css => { // not unfurlable data return Ok(data); } diff --git a/cli/tools/registry/unfurl.rs b/cli/tools/registry/unfurl.rs index 0f5b9fdd32..5ec726a640 100644 --- a/cli/tools/registry/unfurl.rs +++ b/cli/tools/registry/unfurl.rs @@ -12,9 +12,10 @@ use deno_graph::DynamicTemplatePart; use deno_graph::ParserModuleAnalyzer; use deno_graph::TypeScriptReference; use deno_package_json::PackageJsonDepValue; +use deno_resolver::sloppy_imports::SloppyImportsResolutionMode; use deno_runtime::deno_node::is_builtin_node_module; -use crate::resolver::SloppyImportsResolver; +use crate::resolver::CliSloppyImportsResolver; #[derive(Debug, Clone)] pub enum SpecifierUnfurlerDiagnostic { @@ -42,14 +43,14 @@ impl SpecifierUnfurlerDiagnostic { } pub struct SpecifierUnfurler { - sloppy_imports_resolver: Option, + sloppy_imports_resolver: Option, workspace_resolver: WorkspaceResolver, bare_node_builtins: bool, } impl SpecifierUnfurler { pub fn new( - sloppy_imports_resolver: Option, + sloppy_imports_resolver: Option, workspace_resolver: WorkspaceResolver, bare_node_builtins: bool, ) -> Self { @@ -179,7 +180,7 @@ impl SpecifierUnfurler { let resolved = if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver { sloppy_imports_resolver - .resolve(&resolved, deno_graph::source::ResolutionMode::Execution) + .resolve(&resolved, SloppyImportsResolutionMode::Execution) .map(|res| res.into_specifier()) .unwrap_or(resolved) } else { @@ -388,6 +389,8 @@ fn to_range( mod tests { use std::sync::Arc; + use crate::resolver::SloppyImportsCachedFs; + use super::*; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; @@ -455,7 +458,9 @@ mod tests { ); let fs = Arc::new(RealFs); let unfurler = SpecifierUnfurler::new( - Some(SloppyImportsResolver::new(fs)), + Some(CliSloppyImportsResolver::new(SloppyImportsCachedFs::new( + fs, + ))), workspace_resolver, true, ); diff --git a/cli/tools/repl/session.rs b/cli/tools/repl/session.rs index 484664dae4..8e05c4abbc 100644 --- a/cli/tools/repl/session.rs +++ b/cli/tools/repl/session.rs @@ -7,7 +7,7 @@ use crate::cdp; use crate::colors; use crate::lsp::ReplLanguageServer; use crate::npm::CliNpmResolver; -use crate::resolver::CliGraphResolver; +use crate::resolver::CliResolver; use crate::tools::test::report_tests; use crate::tools::test::reporters::PrettyTestReporter; use crate::tools::test::reporters::TestReporter; @@ -25,6 +25,7 @@ use deno_ast::swc::visit::noop_visit_type; use deno_ast::swc::visit::Visit; use deno_ast::swc::visit::VisitWith; use deno_ast::ImportsNotUsedAsValues; +use deno_ast::ModuleKind; use deno_ast::ModuleSpecifier; use deno_ast::ParseDiagnosticsError; use deno_ast::ParsedSource; @@ -43,12 +44,12 @@ use deno_core::url::Url; use deno_core::LocalInspectorSession; use deno_core::PollEventLoopOptions; use deno_graph::source::ResolutionMode; -use deno_graph::source::Resolver; use deno_graph::Position; use deno_graph::PositionRange; use deno_graph::SpecifierWithRange; use deno_runtime::worker::MainWorker; use deno_semver::npm::NpmPackageReqReference; +use node_resolver::NodeModuleKind; use once_cell::sync::Lazy; use regex::Match; use regex::Regex; @@ -179,7 +180,7 @@ struct ReplJsxState { pub struct ReplSession { npm_resolver: Arc, - resolver: Arc, + resolver: Arc, pub worker: MainWorker, session: LocalInspectorSession, pub context_id: u64, @@ -198,7 +199,7 @@ impl ReplSession { pub async fn initialize( cli_options: &CliOptions, npm_resolver: Arc, - resolver: Arc, + resolver: Arc, mut worker: MainWorker, main_module: ModuleSpecifier, test_event_receiver: TestEventReceiver, @@ -244,7 +245,7 @@ impl ReplSession { assert_ne!(context_id, 0); let referrer = - deno_core::resolve_path("./$deno$repl.ts", cli_options.initial_cwd()) + deno_core::resolve_path("./$deno$repl.mts", cli_options.initial_cwd()) .unwrap(); let cwd_url = @@ -641,6 +642,10 @@ impl ReplSession { jsx_fragment_factory: self.jsx.frag_factory.clone(), jsx_import_source: self.jsx.import_source.clone(), var_decl_imports: true, + verbatim_module_syntax: false, + }, + &deno_ast::TranspileModuleOptions { + module_kind: Some(ModuleKind::Esm), }, &deno_ast::EmitOptions { source_map: deno_ast::SourceMapOption::None, @@ -651,7 +656,6 @@ impl ReplSession { }, )? .into_source() - .into_string()? .text; let value = self @@ -708,7 +712,12 @@ impl ReplSession { .flat_map(|i| { self .resolver - .resolve(i, &referrer_range, ResolutionMode::Execution) + .resolve( + i, + &referrer_range, + NodeModuleKind::Esm, + ResolutionMode::Execution, + ) .ok() .or_else(|| ModuleSpecifier::parse(i).ok()) }) diff --git a/cli/tools/run/hmr.rs b/cli/tools/run/hmr.rs index 6ccf8e344b..373c207d69 100644 --- a/cli/tools/run/hmr.rs +++ b/cli/tools/run/hmr.rs @@ -1,9 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use crate::cdp; -use crate::emit::Emitter; -use crate::util::file_watcher::WatcherCommunicator; -use crate::util::file_watcher::WatcherRestartMode; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; + use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::futures::StreamExt; @@ -12,11 +12,13 @@ use deno_core::serde_json::{self}; use deno_core::url::Url; use deno_core::LocalInspectorSession; use deno_terminal::colors; -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; use tokio::select; +use crate::cdp; +use crate::emit::Emitter; +use crate::util::file_watcher::WatcherCommunicator; +use crate::util::file_watcher::WatcherRestartMode; + fn explain(status: &cdp::Status) -> &'static str { match status { cdp::Status::Ok => "OK", @@ -139,7 +141,7 @@ impl crate::worker::HmrRunner for HmrRunner { }; let source_code = self.emitter.load_and_emit_for_hmr( - &module_url + &module_url, ).await?; let mut tries = 1; diff --git a/cli/tools/run/mod.rs b/cli/tools/run/mod.rs index 152e2650bd..bebb3f5881 100644 --- a/cli/tools/run/mod.rs +++ b/cli/tools/run/mod.rs @@ -30,6 +30,16 @@ To grant permissions, set them before the script argument. For example: } } +fn set_npm_user_agent() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + std::env::set_var( + crate::npm::NPM_CONFIG_USER_AGENT_ENV_VAR, + crate::npm::get_npm_config_user_agent(), + ); + }); +} + pub async fn run_script( mode: WorkerExecutionMode, flags: Arc, @@ -58,6 +68,10 @@ pub async fn run_script( let main_module = cli_options.resolve_main_module()?; + if main_module.scheme() == "npm" { + set_npm_user_agent(); + } + maybe_npm_install(&factory).await?; let worker_factory = factory.create_cli_main_worker_factory().await?; @@ -119,6 +133,10 @@ async fn run_with_watch( let cli_options = factory.cli_options()?; let main_module = cli_options.resolve_main_module()?; + if main_module.scheme() == "npm" { + set_npm_user_agent(); + } + maybe_npm_install(&factory).await?; let _ = watcher_communicator.watch_paths(cli_options.watch_paths()); diff --git a/cli/tools/serve.rs b/cli/tools/serve.rs index 4ce1cad6f2..e3f9e94f8e 100644 --- a/cli/tools/serve.rs +++ b/cli/tools/serve.rs @@ -44,12 +44,15 @@ pub async fn serve( maybe_npm_install(&factory).await?; let worker_factory = factory.create_cli_main_worker_factory().await?; - + let hmr = serve_flags + .watch + .map(|watch_flags| watch_flags.hmr) + .unwrap_or(false); do_serve( worker_factory, main_module.clone(), serve_flags.worker_count, - false, + hmr, ) .await } @@ -109,8 +112,6 @@ async fn do_serve( } } Ok(exit_code) - - // main.await? } async fn run_worker( @@ -119,7 +120,7 @@ async fn run_worker( main_module: ModuleSpecifier, hmr: bool, ) -> Result { - let mut worker = worker_factory + let mut worker: crate::worker::CliMainWorker = worker_factory .create_main_worker( deno_runtime::WorkerExecutionMode::Serve { is_main: false, diff --git a/cli/tools/task.rs b/cli/tools/task.rs index ae91f53a7a..502b09d2c2 100644 --- a/cli/tools/task.rs +++ b/cli/tools/task.rs @@ -16,7 +16,7 @@ use deno_core::anyhow::anyhow; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; -use deno_core::normalize_path; +use deno_path_util::normalize_path; use deno_task_shell::ShellCommand; use crate::args::CliOptions; @@ -182,19 +182,21 @@ async fn run_task(opts: RunTaskOptions<'_>) -> Result { &task_runner::get_script_with_args(script, cli_options.argv()), ); - task_runner::run_task(task_runner::RunTaskOptions { - task_name, - script, - cwd, - env_vars, - custom_commands, - init_cwd: opts.cli_options.initial_cwd(), - argv: cli_options.argv(), - root_node_modules_dir: npm_resolver - .root_node_modules_path() - .map(|p| p.as_path()), - }) - .await + Ok( + task_runner::run_task(task_runner::RunTaskOptions { + task_name, + script, + cwd, + env_vars, + custom_commands, + init_cwd: opts.cli_options.initial_cwd(), + argv: cli_options.argv(), + root_node_modules_dir: npm_resolver.root_node_modules_path(), + stdio: None, + }) + .await? + .exit_code, + ) } fn output_task(task_name: &str, script: &str) { diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs index e81abad0b2..966b0d285a 100644 --- a/cli/tools/test/mod.rs +++ b/cli/tools/test/mod.rs @@ -631,7 +631,7 @@ async fn configure_main_worker( "Deno[Deno.internal].core.setLeakTracingEnabled(true);", )?; } - let res = worker.execute_side_module_possibly_with_npm().await; + let res = worker.execute_side_module().await; let mut worker = worker.into_main_worker(); match res { Ok(()) => Ok(()), @@ -1357,6 +1357,7 @@ pub async fn report_tests( if let Err(err) = reporter.flush_report(&elapsed, &tests, &test_steps) { eprint!("Test reporter failed to flush: {}", err) } + #[allow(clippy::disallowed_methods)] std::process::exit(130); } } @@ -1642,6 +1643,7 @@ pub async fn run_tests_with_watch( loop { signal::ctrl_c().await.unwrap(); if !HAS_TEST_RUN_SIGINT_HANDLER.load(Ordering::Relaxed) { + #[allow(clippy::disallowed_methods)] std::process::exit(130); } } diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs index 7f21e6649c..cb85859f7a 100644 --- a/cli/tools/upgrade.rs +++ b/cli/tools/upgrade.rs @@ -540,7 +540,7 @@ pub async fn upgrade( let Some(archive_data) = download_package(&client, download_url).await? else { log::error!("Download could not be found, aborting"); - std::process::exit(1) + deno_runtime::exit(1) }; log::info!( @@ -579,6 +579,10 @@ pub async fn upgrade( let output_exe_path = full_path_output_flag.as_ref().unwrap_or(¤t_exe_path); + + #[cfg(windows)] + kill_running_deno_lsp_processes(); + let output_result = if *output_exe_path == current_exe_path { replace_exe(&new_exe_path, output_exe_path) } else { @@ -913,7 +917,7 @@ async fn download_package( // text above which will stay alive after the progress bars are complete let progress = progress_bar.update(""); let maybe_bytes = client - .download_with_progress(download_url.clone(), None, &progress) + .download_with_progress_and_retries(download_url.clone(), None, &progress) .await .with_context(|| format!("Failed downloading {download_url}. The version you requested may not have been built for the current architecture."))?; Ok(maybe_bytes) @@ -966,6 +970,34 @@ fn check_windows_access_denied_error( }) } +#[cfg(windows)] +fn kill_running_deno_lsp_processes() { + // limit this to `deno lsp` invocations to avoid killing important programs someone might be running + let is_debug = log::log_enabled!(log::Level::Debug); + let get_pipe = || { + if is_debug { + std::process::Stdio::inherit() + } else { + std::process::Stdio::null() + } + }; + let _ = Command::new("powershell.exe") + .args([ + "-Command", + r#"Get-WmiObject Win32_Process | Where-Object { + $_.Name -eq 'deno.exe' -and + $_.CommandLine -match '^(?:\"[^\"]+\"|\S+)\s+lsp\b' +} | ForEach-Object { + if ($_.Terminate()) { + Write-Host 'Terminated:' $_.ProcessId + } +}"#, + ]) + .stdout(get_pipe()) + .stderr(get_pipe()) + .output(); +} + fn set_exe_permissions( current_exe_path: &Path, output_exe_path: &Path, diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 719f2b9824..68d099253a 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -121,8 +121,8 @@ delete Object.prototype.__proto__; /** @type {Map} */ const sourceFileCache = new Map(); - /** @type {Map} */ - const sourceTextCache = new Map(); + /** @type {Map} */ + const scriptSnapshotCache = new Map(); /** @type {Map} */ const sourceRefCounts = new Map(); @@ -133,9 +133,6 @@ delete Object.prototype.__proto__; /** @type {Map} */ const isNodeSourceFileCache = new Map(); - /** @type {Map} */ - const isCjsCache = new Map(); - // Maps asset specifiers to the first scope that the asset was loaded into. /** @type {Map} */ const assetScopes = new Map(); @@ -210,12 +207,13 @@ delete Object.prototype.__proto__; const mapKey = path + key; let sourceFile = documentRegistrySourceFileCache.get(mapKey); if (!sourceFile || sourceFile.version !== version) { + const isCjs = /** @type {any} */ (scriptSnapshot).isCjs; sourceFile = ts.createLanguageServiceSourceFile( fileName, scriptSnapshot, { ...getCreateSourceFileOptions(sourceFileOptions), - impliedNodeFormat: (isCjsCache.get(fileName) ?? false) + impliedNodeFormat: isCjs ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext, // in the lsp we want to be able to show documentation @@ -320,7 +318,7 @@ delete Object.prototype.__proto__; if (lastRequestMethod != "cleanupSemanticCache") { const mapKey = path + key; documentRegistrySourceFileCache.delete(mapKey); - sourceTextCache.delete(path); + scriptSnapshotCache.delete(path); ops.op_release(path); } } else { @@ -516,7 +514,6 @@ delete Object.prototype.__proto__; /** @typedef {{ * ls: ts.LanguageService & { [k:string]: any }, * compilerOptions: ts.CompilerOptions, - * forceEnabledVerbatimModuleSyntax: boolean, * }} LanguageServiceEntry */ /** @type {{ unscoped: LanguageServiceEntry, byScope: Map }} */ const languageServiceEntries = { @@ -625,8 +622,6 @@ delete Object.prototype.__proto__; `"data" is unexpectedly null for "${specifier}".`, ); - isCjsCache.set(specifier, isCjs); - sourceFile = ts.createSourceFile( specifier, data, @@ -700,7 +695,7 @@ delete Object.prototype.__proto__; /** @type {[string, ts.Extension] | undefined} */ const resolved = ops.op_resolve( containingFilePath, - isCjsCache.get(containingFilePath) ?? false, + containingFileMode === ts.ModuleKind.CommonJS, [fileReference.fileName], )?.[0]; if (resolved) { @@ -724,7 +719,14 @@ delete Object.prototype.__proto__; } }); }, - resolveModuleNames(specifiers, base) { + resolveModuleNames( + specifiers, + base, + _reusedNames, + _redirectedReference, + _options, + containingSourceFile, + ) { if (logDebug) { debug(`host.resolveModuleNames()`); debug(` base: ${base}`); @@ -733,7 +735,7 @@ delete Object.prototype.__proto__; /** @type {Array<[string, ts.Extension] | undefined>} */ const resolved = ops.op_resolve( base, - isCjsCache.get(base) ?? false, + containingSourceFile?.impliedNodeFormat === ts.ModuleKind.CommonJS, specifiers, ); if (resolved) { @@ -802,27 +804,32 @@ delete Object.prototype.__proto__; if (logDebug) { debug(`host.getScriptSnapshot("${specifier}")`); } - const sourceFile = sourceFileCache.get(specifier); - if (sourceFile) { - if (!assetScopes.has(specifier)) { - assetScopes.set(specifier, lastRequestScope); + if (specifier.startsWith(ASSETS_URL_PREFIX)) { + const sourceFile = this.getSourceFile( + specifier, + ts.ScriptTarget.ESNext, + ); + if (sourceFile) { + if (!assetScopes.has(specifier)) { + assetScopes.set(specifier, lastRequestScope); + } + // This case only occurs for assets. + return ts.ScriptSnapshot.fromString(sourceFile.text); } - // This case only occurs for assets. - return ts.ScriptSnapshot.fromString(sourceFile.text); } - let sourceText = sourceTextCache.get(specifier); - if (sourceText == undefined) { + let scriptSnapshot = scriptSnapshotCache.get(specifier); + if (scriptSnapshot == undefined) { /** @type {{ data: string, version: string, isCjs: boolean }} */ const fileInfo = ops.op_load(specifier); if (!fileInfo) { return undefined; } - isCjsCache.set(specifier, fileInfo.isCjs); - sourceTextCache.set(specifier, fileInfo.data); + scriptSnapshot = ts.ScriptSnapshot.fromString(fileInfo.data); + scriptSnapshot.isCjs = fileInfo.isCjs; + scriptSnapshotCache.set(specifier, scriptSnapshot); scriptVersionCache.set(specifier, fileInfo.version); - sourceText = fileInfo.data; } - return ts.ScriptSnapshot.fromString(sourceText); + return scriptSnapshot; }, }; @@ -846,6 +853,8 @@ delete Object.prototype.__proto__; jqueryMessage, "Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slash_2592": jqueryMessage, + "Module_0_was_resolved_to_1_but_allowArbitraryExtensions_is_not_set_6263": + "Module '{0}' was resolved to '{1}', but importing these modules is not supported.", }; })()); @@ -1026,7 +1035,7 @@ delete Object.prototype.__proto__; : ts.sortAndDeduplicateDiagnostics( checkFiles.map((s) => program.getSemanticDiagnostics(s)).flat(), )), - ].filter(filterMapDiagnostic.bind(null, false)); + ].filter(filterMapDiagnostic); // emit the tsbuildinfo file // @ts-ignore: emitBuildInfo is not exposed (https://github.com/microsoft/TypeScript/issues/49871) @@ -1041,28 +1050,11 @@ delete Object.prototype.__proto__; debug("<<< exec stop"); } - /** - * @param {boolean} isLsp - * @param {ts.Diagnostic} diagnostic - */ - function filterMapDiagnostic(isLsp, diagnostic) { + /** @param {ts.Diagnostic} diagnostic */ + function filterMapDiagnostic(diagnostic) { if (IGNORED_DIAGNOSTICS.includes(diagnostic.code)) { return false; } - if (isLsp) { - // TS1484: `...` is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled. - // We force-enable `verbatimModuleSyntax` in the LSP so the `type` - // modifier is used when auto-importing types. But we don't want this - // diagnostic unless it was explicitly enabled by the user. - if (diagnostic.code == 1484) { - const entry = (lastRequestScope - ? languageServiceEntries.byScope.get(lastRequestScope) - : null) ?? languageServiceEntries.unscoped; - if (entry.forceEnabledVerbatimModuleSyntax) { - return false; - } - } - } // make the diagnostic for using an `export =` in an es module a warning if (diagnostic.code === 1203) { diagnostic.category = ts.DiagnosticCategory.Warning; @@ -1159,12 +1151,10 @@ delete Object.prototype.__proto__; "strict": true, "target": "esnext", "useDefineForClassFields": true, - "verbatimModuleSyntax": true, "jsx": "react", "jsxFactory": "React.createElement", "jsxFragmentFactory": "React.Fragment", }), - forceEnabledVerbatimModuleSyntax: true, }; setLogDebug(enableDebugLogging, "TSLS"); debug("serverInit()"); @@ -1230,17 +1220,8 @@ delete Object.prototype.__proto__; const ls = oldEntry ? oldEntry.ls : ts.createLanguageService(host, documentRegistry); - let forceEnabledVerbatimModuleSyntax = false; - if (!config["verbatimModuleSyntax"]) { - config["verbatimModuleSyntax"] = true; - forceEnabledVerbatimModuleSyntax = true; - } const compilerOptions = lspTsConfigToCompilerOptions(config); - newByScope.set(scope, { - ls, - compilerOptions, - forceEnabledVerbatimModuleSyntax, - }); + newByScope.set(scope, { ls, compilerOptions }); languageServiceEntries.byScope.delete(scope); } for (const oldEntry of languageServiceEntries.byScope.values()) { @@ -1260,7 +1241,7 @@ delete Object.prototype.__proto__; closed = true; } scriptVersionCache.delete(script); - sourceTextCache.delete(script); + scriptSnapshotCache.delete(script); } if (newConfigsByScope || opened || closed) { @@ -1305,7 +1286,7 @@ delete Object.prototype.__proto__; ...ls.getSemanticDiagnostics(specifier), ...ls.getSuggestionDiagnostics(specifier), ...ls.getSyntacticDiagnostics(specifier), - ].filter(filterMapDiagnostic.bind(null, true))); + ].filter(filterMapDiagnostic)); } return respond(id, diagnosticMap); } catch (e) { @@ -1366,18 +1347,12 @@ delete Object.prototype.__proto__; "console", "Console", "ErrorConstructor", - "exports", "gc", "Global", "ImportMeta", "localStorage", - "module", - "NodeModule", - "NodeRequire", - "process", "queueMicrotask", "RequestInit", - "require", "ResponseInit", "sessionStorage", "setImmediate", diff --git a/cli/tsc/diagnostics.rs b/cli/tsc/diagnostics.rs index b0394ec177..d3795706eb 100644 --- a/cli/tsc/diagnostics.rs +++ b/cli/tsc/diagnostics.rs @@ -323,7 +323,7 @@ impl Diagnostics { // todo(dsherret): use a short lived cache to prevent parsing // source maps so often if let Ok(source_map) = - SourceMap::from_slice(&fast_check_module.source_map) + SourceMap::from_slice(fast_check_module.source_map.as_bytes()) { if let Some(start) = d.start.as_mut() { let maybe_token = source_map diff --git a/cli/tsc/dts/lib.deno.ns.d.ts b/cli/tsc/dts/lib.deno.ns.d.ts index 36592e10dc..8179e4223c 100644 --- a/cli/tsc/dts/lib.deno.ns.d.ts +++ b/cli/tsc/dts/lib.deno.ns.d.ts @@ -556,14 +556,23 @@ declare namespace Deno { */ env?: "inherit" | boolean | string[]; - /** Specifies if the `sys` permission should be requested or revoked. - * If set to `"inherit"`, the current `sys` permission will be inherited. - * If set to `true`, the global `sys` permission will be requested. - * If set to `false`, the global `sys` permission will be revoked. + /** Specifies if the `ffi` permission should be requested or revoked. + * If set to `"inherit"`, the current `ffi` permission will be inherited. + * If set to `true`, the global `ffi` permission will be requested. + * If set to `false`, the global `ffi` permission will be revoked. * * @default {false} */ - sys?: "inherit" | boolean | string[]; + ffi?: "inherit" | boolean | Array; + + /** Specifies if the `import` permission should be requested or revoked. + * If set to `"inherit"` the current `import` permission will be inherited. + * If set to `true`, the global `import` permission will be requested. + * If set to `false`, the global `import` permission will be revoked. + * If set to `Array`, the `import` permissions will be requested with the + * specified domains. + */ + import?: "inherit" | boolean | Array; /** Specifies if the `net` permission should be requested or revoked. * if set to `"inherit"`, the current `net` permission will be inherited. @@ -638,15 +647,6 @@ declare namespace Deno { */ net?: "inherit" | boolean | string[]; - /** Specifies if the `ffi` permission should be requested or revoked. - * If set to `"inherit"`, the current `ffi` permission will be inherited. - * If set to `true`, the global `ffi` permission will be requested. - * If set to `false`, the global `ffi` permission will be revoked. - * - * @default {false} - */ - ffi?: "inherit" | boolean | Array; - /** Specifies if the `read` permission should be requested or revoked. * If set to `"inherit"`, the current `read` permission will be inherited. * If set to `true`, the global `read` permission will be requested. @@ -667,6 +667,15 @@ declare namespace Deno { */ run?: "inherit" | boolean | Array; + /** Specifies if the `sys` permission should be requested or revoked. + * If set to `"inherit"`, the current `sys` permission will be inherited. + * If set to `true`, the global `sys` permission will be requested. + * If set to `false`, the global `sys` permission will be revoked. + * + * @default {false} + */ + sys?: "inherit" | boolean | string[]; + /** Specifies if the `write` permission should be requested or revoked. * If set to `"inherit"`, the current `write` permission will be inherited. * If set to `true`, the global `write` permission will be requested. @@ -2962,6 +2971,10 @@ declare namespace Deno { * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may * not be available on all platforms. */ birthtime: Date | null; + /** The last change time of the file. This corresponds to the `ctime` + * field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may + * not be available on all platforms. */ + ctime: Date | null; /** ID of the device containing the file. */ dev: number; /** Inode number. @@ -2970,8 +2983,7 @@ declare namespace Deno { ino: number | null; /** The underlying raw `st_mode` bits that contain the standard Unix * permissions for this file/directory. - * - * _Linux/Mac OS only._ */ + */ mode: number | null; /** Number of hard links pointing to this file. * diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index 973a09d92a..6234268c39 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -1225,6 +1225,108 @@ declare namespace Deno { export {}; // only export exports } + /** + * @category Telemetry + * @experimental + */ + export namespace tracing { + /** + * Whether tracing is enabled. + * @category Telemetry + * @experimental + */ + export const enabled: boolean; + + /** + * Allowed attribute type. + * @category Telemetry + * @experimental + */ + export type AttributeValue = string | number | boolean | bigint; + + /** + * A tracing span. + * @category Telemetry + * @experimental + */ + export class Span implements Disposable { + readonly traceId: string; + readonly spanId: string; + readonly parentSpanId: string; + readonly kind: string; + readonly name: string; + readonly startTime: number; + readonly endTime: number; + readonly status: null | { code: 1 } | { code: 2; message: string }; + readonly attributes: Record; + readonly traceFlags: number; + + /** + * Construct a new Span and enter it as the "current" span. + */ + constructor( + name: string, + kind?: "internal" | "server" | "client" | "producer" | "consumer", + ); + + /** + * Set an attribute on this span. + */ + setAttribute( + name: string, + value: AttributeValue, + ): void; + + /** + * Enter this span as the "current" span. + */ + enter(): void; + + /** + * Exit this span as the "current" span and restore the previous one. + */ + exit(): void; + + /** + * End this span, and exit it as the "current" span. + */ + end(): void; + + [Symbol.dispose](): void; + + /** + * Get the "current" span, if one exists. + */ + static current(): Span | undefined | null; + } + + /** + * A SpanExporter compatible with OpenTelemetry.js + * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_trace_base.SpanExporter.html + * @category Telemetry + * @experimental + */ + export class SpanExporter {} + + /** + * A ContextManager compatible with OpenTelemetry.js + * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.ContextManager.html + * @category Telemetry + * @experimental + */ + export class ContextManager {} + + export {}; // only export exports + } + + /** + * @category Telemetry + * @experimental + */ + export namespace metrics { + export {}; // only export exports + } + export {}; // only export exports } diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs index 0e3387494a..8f8bed20a4 100644 --- a/cli/tsc/mod.rs +++ b/cli/tsc/mod.rs @@ -3,10 +3,13 @@ use crate::args::TsConfig; use crate::args::TypeCheckMode; use crate::cache::FastInsecureHasher; +use crate::cache::ModuleInfoCache; use crate::node; use crate::npm::CliNpmResolver; +use crate::resolver::CjsTracker; use crate::util::checksum; use crate::util::path::mapped_specifier_for_tsc; +use crate::worker::create_isolate_create_params; use deno_ast::MediaType; use deno_core::anyhow::anyhow; @@ -31,12 +34,14 @@ use deno_graph::GraphKind; use deno_graph::Module; use deno_graph::ModuleGraph; use deno_graph::ResolutionResolved; +use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; +use deno_runtime::deno_fs; use deno_runtime::deno_node::NodeResolver; use deno_semver::npm::NpmPackageReqReference; use node_resolver::errors::NodeJsErrorCode; use node_resolver::errors::NodeJsErrorCoded; +use node_resolver::errors::PackageSubpathResolveError; use node_resolver::NodeModuleKind; -use node_resolver::NodeResolution; use node_resolver::NodeResolutionMode; use once_cell::sync::Lazy; use std::borrow::Cow; @@ -45,6 +50,7 @@ use std::fmt; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use thiserror::Error; mod diagnostics; @@ -299,8 +305,81 @@ pub struct EmittedFile { pub media_type: MediaType, } +pub fn into_specifier_and_media_type( + specifier: Option, +) -> (ModuleSpecifier, MediaType) { + match specifier { + Some(specifier) => { + let media_type = MediaType::from_specifier(&specifier); + + (specifier, media_type) + } + None => ( + Url::parse("internal:///missing_dependency.d.ts").unwrap(), + MediaType::Dts, + ), + } +} + +#[derive(Debug)] +pub struct TypeCheckingCjsTracker { + cjs_tracker: Arc, + module_info_cache: Arc, +} + +impl TypeCheckingCjsTracker { + pub fn new( + cjs_tracker: Arc, + module_info_cache: Arc, + ) -> Self { + Self { + cjs_tracker, + module_info_cache, + } + } + + pub fn is_cjs( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + code: &Arc, + ) -> bool { + let maybe_is_script = self + .module_info_cache + .as_module_analyzer() + .analyze_sync(specifier, media_type, code) + .ok() + .map(|info| info.is_script); + maybe_is_script + .and_then(|is_script| { + self + .cjs_tracker + .is_cjs_with_known_is_script(specifier, media_type, is_script) + .ok() + }) + .unwrap_or_else(|| { + self + .cjs_tracker + .is_maybe_cjs(specifier, media_type) + .unwrap_or(false) + }) + } + + pub fn is_cjs_with_known_is_script( + &self, + specifier: &ModuleSpecifier, + media_type: MediaType, + is_script: bool, + ) -> Result { + self + .cjs_tracker + .is_cjs_with_known_is_script(specifier, media_type, is_script) + } +} + #[derive(Debug)] pub struct RequestNpmState { + pub cjs_tracker: Arc, pub node_resolver: Arc, pub npm_resolver: Arc, } @@ -453,7 +532,7 @@ pub fn as_ts_script_kind(media_type: MediaType) -> i32 { MediaType::Tsx => 4, MediaType::Json => 6, MediaType::SourceMap - | MediaType::TsBuildInfo + | MediaType::Css | MediaType::Wasm | MediaType::Unknown => 0, } @@ -486,25 +565,22 @@ fn op_load_inner( ) -> Result, AnyError> { fn load_from_node_modules( specifier: &ModuleSpecifier, - node_resolver: Option<&NodeResolver>, + npm_state: Option<&RequestNpmState>, media_type: &mut MediaType, is_cjs: &mut bool, ) -> Result { *media_type = MediaType::from_specifier(specifier); - *is_cjs = node_resolver - .map(|node_resolver| { - match node_resolver.url_to_node_resolution(specifier.clone()) { - Ok(NodeResolution::CommonJs(_)) => true, - Ok(NodeResolution::Esm(_)) - | Ok(NodeResolution::BuiltIn(_)) - | Err(_) => false, - } - }) - .unwrap_or(false); let file_path = specifier.to_file_path().unwrap(); let code = std::fs::read_to_string(&file_path) .with_context(|| format!("Unable to load {}", file_path.display()))?; - Ok(code) + let code: Arc = code.into(); + *is_cjs = npm_state + .map(|npm_state| { + npm_state.cjs_tracker.is_cjs(specifier, *media_type, &code) + }) + .unwrap_or(false); + // todo(dsherret): how to avoid cloning here? + Ok(code.to_string()) } let state = state.borrow_mut::(); @@ -557,6 +633,13 @@ fn op_load_inner( match module { Module::Js(module) => { media_type = module.media_type; + if let Some(npm_state) = &state.maybe_npm { + is_cjs = npm_state.cjs_tracker.is_cjs_with_known_is_script( + specifier, + module.media_type, + module.is_script, + )?; + } let source = module .fast_check_module() .map(|m| &*m.source) @@ -570,11 +653,13 @@ fn op_load_inner( Module::Npm(_) | Module::Node(_) => None, Module::External(module) => { // means it's Deno code importing an npm module - let specifier = - node::resolve_specifier_into_node_modules(&module.specifier); + let specifier = node::resolve_specifier_into_node_modules( + &module.specifier, + &deno_fs::RealFs, + ); Some(Cow::Owned(load_from_node_modules( &specifier, - state.maybe_npm.as_ref().map(|n| n.node_resolver.as_ref()), + state.maybe_npm.as_ref(), &mut media_type, &mut is_cjs, )?)) @@ -587,7 +672,7 @@ fn op_load_inner( { Some(Cow::Owned(load_from_node_modules( specifier, - Some(npm.node_resolver.as_ref()), + Some(npm), &mut media_type, &mut is_cjs, )?)) @@ -662,6 +747,7 @@ fn op_resolve_inner( "Error converting a string module specifier for \"op_resolve\".", )? }; + let referrer_module = state.graph.get(&referrer); for specifier in args.specifiers { if specifier.starts_with("node:") { resolved.push(( @@ -677,23 +763,44 @@ fn op_resolve_inner( continue; } - let graph = &state.graph; - let resolved_dep = graph - .get(&referrer) + let resolved_dep = referrer_module .and_then(|m| m.js()) .and_then(|m| m.dependencies_prefer_fast_check().get(&specifier)) .and_then(|d| d.maybe_type.ok().or_else(|| d.maybe_code.ok())); let maybe_result = match resolved_dep { Some(ResolutionResolved { specifier, .. }) => { - resolve_graph_specifier_types(specifier, &referrer, state)? + resolve_graph_specifier_types( + specifier, + &referrer, + referrer_kind, + state, + )? + } + _ => { + match resolve_non_graph_specifier_types( + &specifier, + &referrer, + referrer_kind, + state, + ) { + Ok(maybe_result) => maybe_result, + Err( + err @ ResolveNonGraphSpecifierTypesError::ResolvePkgFolderFromDenoReq( + ResolvePkgFolderFromDenoReqError::Managed(_), + ), + ) => { + // it's most likely requesting the jsxImportSource, which isn't loaded + // into the graph when not using jsx, so just ignore this error + if specifier.ends_with("/jsx-runtime") { + None + } else { + return Err(err.into()); + } + } + Err(err) => return Err(err.into()), + } } - _ => resolve_non_graph_specifier_types( - &specifier, - &referrer, - referrer_kind, - state, - )?, }; let result = match maybe_result { Some((specifier, media_type)) => { @@ -718,7 +825,13 @@ fn op_resolve_inner( } } }; - (specifier_str, media_type.as_ts_extension()) + ( + specifier_str, + match media_type { + MediaType::Css => ".js", // surface these as .js for typescript + media_type => media_type.as_ts_extension(), + }, + ) } None => ( MISSING_DEPENDENCY_SPECIFIER.to_string(), @@ -735,6 +848,7 @@ fn op_resolve_inner( fn resolve_graph_specifier_types( specifier: &ModuleSpecifier, referrer: &ModuleSpecifier, + referrer_kind: NodeModuleKind, state: &State, ) -> Result, AnyError> { let graph = &state.graph; @@ -787,43 +901,53 @@ fn resolve_graph_specifier_types( &package_folder, module.nv_reference.sub_path(), Some(referrer), + referrer_kind, NodeResolutionMode::Types, ); - let maybe_resolution = match res_result { - Ok(res) => Some(res), + let maybe_url = match res_result { + Ok(url) => Some(url), Err(err) => match err.code() { NodeJsErrorCode::ERR_TYPES_NOT_FOUND | NodeJsErrorCode::ERR_MODULE_NOT_FOUND => None, _ => return Err(err.into()), }, }; - Ok(Some(NodeResolution::into_specifier_and_media_type( - maybe_resolution, - ))) + Ok(Some(into_specifier_and_media_type(maybe_url))) } else { Ok(None) } } Some(Module::External(module)) => { // we currently only use "External" for when the module is in an npm package - Ok(state.maybe_npm.as_ref().map(|npm| { - let specifier = - node::resolve_specifier_into_node_modules(&module.specifier); - NodeResolution::into_specifier_and_media_type( - npm.node_resolver.url_to_node_resolution(specifier).ok(), - ) + Ok(state.maybe_npm.as_ref().map(|_| { + let specifier = node::resolve_specifier_into_node_modules( + &module.specifier, + &deno_fs::RealFs, + ); + into_specifier_and_media_type(Some(specifier)) })) } Some(Module::Node(_)) | None => Ok(None), } } +#[derive(Debug, Error)] +enum ResolveNonGraphSpecifierTypesError { + #[error(transparent)] + ResolvePkgFolderFromDenoReq(#[from] ResolvePkgFolderFromDenoReqError), + #[error(transparent)] + PackageSubpathResolve(#[from] PackageSubpathResolveError), +} + fn resolve_non_graph_specifier_types( raw_specifier: &str, referrer: &ModuleSpecifier, referrer_kind: NodeModuleKind, state: &State, -) -> Result, AnyError> { +) -> Result< + Option<(ModuleSpecifier, MediaType)>, + ResolveNonGraphSpecifierTypesError, +> { let npm = match state.maybe_npm.as_ref() { Some(npm) => npm, None => return Ok(None), // we only support non-graph types for npm packages @@ -831,7 +955,7 @@ fn resolve_non_graph_specifier_types( let node_resolver = &npm.node_resolver; if node_resolver.in_npm_package(referrer) { // we're in an npm package, so use node resolution - Ok(Some(NodeResolution::into_specifier_and_media_type( + Ok(Some(into_specifier_and_media_type( node_resolver .resolve( raw_specifier, @@ -839,7 +963,8 @@ fn resolve_non_graph_specifier_types( referrer_kind, NodeResolutionMode::Types, ) - .ok(), + .ok() + .map(|res| res.into_url()), ))) } else if let Ok(npm_req_ref) = NpmPackageReqReference::from_str(raw_specifier) @@ -856,19 +981,18 @@ fn resolve_non_graph_specifier_types( &package_folder, npm_req_ref.sub_path(), Some(referrer), + referrer_kind, NodeResolutionMode::Types, ); - let maybe_resolution = match res_result { - Ok(res) => Some(res), + let maybe_url = match res_result { + Ok(url) => Some(url), Err(err) => match err.code() { NodeJsErrorCode::ERR_TYPES_NOT_FOUND | NodeJsErrorCode::ERR_MODULE_NOT_FOUND => None, _ => return Err(err.into()), }, }; - Ok(Some(NodeResolution::into_specifier_and_media_type( - maybe_resolution, - ))) + Ok(Some(into_specifier_and_media_type(maybe_url))) } else { Ok(None) } @@ -981,6 +1105,7 @@ pub fn exec(request: Request) -> Result { root_map, remapped_specifiers, )], + create_params: create_isolate_create_params(), ..Default::default() }); diff --git a/cli/util/extract.rs b/cli/util/extract.rs index 841cf6eb0f..be68202aa1 100644 --- a/cli/util/extract.rs +++ b/cli/util/extract.rs @@ -64,7 +64,7 @@ fn extract_inner( }) { Ok(parsed) => { let mut c = ExportCollector::default(); - c.visit_program(parsed.program_ref()); + c.visit_program(parsed.program().as_ref()); c } Err(_) => ExportCollector::default(), @@ -254,7 +254,11 @@ impl ExportCollector { let mut import_specifiers = vec![]; if let Some(default_export) = &self.default_export { - if !symbols_to_exclude.contains(default_export) { + // If the default export conflicts with a named export, a named one + // takes precedence. + if !symbols_to_exclude.contains(default_export) + && !self.named_exports.contains(default_export) + { import_specifiers.push(ast::ImportSpecifier::Default( ast::ImportDefaultSpecifier { span: DUMMY_SP, @@ -566,14 +570,14 @@ fn generate_pseudo_file( })?; let top_level_atoms = swc_utils::collect_decls_with_ctxt::( - parsed.program_ref(), + &parsed.program_ref(), parsed.top_level_context(), ); let transformed = parsed .program_ref() - .clone() + .to_owned() .fold_with(&mut as_folder(Transform { specifier: &file.specifier, base_file_specifier, @@ -582,7 +586,10 @@ fn generate_pseudo_file( wrap_kind, })); - let source = deno_ast::swc::codegen::to_code(&transformed); + let source = deno_ast::swc::codegen::to_code_with_comments( + Some(&parsed.comments().as_single_threaded()), + &transformed, + ); log::debug!("{}:\n{}", file.specifier, source); @@ -1137,6 +1144,57 @@ Deno.test("file:///README.md$6-12.js", async ()=>{ media_type: MediaType::JavaScript, }], }, + // https://github.com/denoland/deno/issues/26009 + Test { + input: Input { + source: r#" +/** + * ```ts + * console.log(Foo) + * ``` + */ +export class Foo {} +export default Foo +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { Foo } from "file:///main.ts"; +Deno.test("file:///main.ts$3-6.ts", async ()=>{ + console.log(Foo); +}); +"#, + specifier: "file:///main.ts$3-6.ts", + media_type: MediaType::TypeScript, + }], + }, + // https://github.com/denoland/deno/issues/26728 + Test { + input: Input { + source: r#" +/** + * ```ts + * // @ts-expect-error: can only add numbers + * add('1', '2'); + * ``` + */ +export function add(first: number, second: number) { + return first + second; +} +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { add } from "file:///main.ts"; +Deno.test("file:///main.ts$3-7.ts", async ()=>{ + // @ts-expect-error: can only add numbers + add('1', '2'); +}); +"#, + specifier: "file:///main.ts$3-7.ts", + media_type: MediaType::TypeScript, + }], + }, ]; for test in tests { @@ -1326,6 +1384,53 @@ assertEquals(add(1, 2), 3); media_type: MediaType::JavaScript, }], }, + // https://github.com/denoland/deno/issues/26009 + Test { + input: Input { + source: r#" +/** + * ```ts + * console.log(Foo) + * ``` + */ +export class Foo {} +export default Foo +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { Foo } from "file:///main.ts"; +console.log(Foo); +"#, + specifier: "file:///main.ts$3-6.ts", + media_type: MediaType::TypeScript, + }], + }, + // https://github.com/denoland/deno/issues/26728 + Test { + input: Input { + source: r#" +/** + * ```ts + * // @ts-expect-error: can only add numbers + * add('1', '2'); + * ``` + */ +export function add(first: number, second: number) { + return first + second; +} +"#, + specifier: "file:///main.ts", + }, + expected: vec![Expected { + source: r#"import { add } from "file:///main.ts"; +// @ts-expect-error: can only add numbers +add('1', '2'); +"#, + specifier: "file:///main.ts$3-7.ts", + media_type: MediaType::TypeScript, + }], + }, ]; for test in tests { @@ -1366,7 +1471,7 @@ assertEquals(add(1, 2), 3); }) .unwrap(); - collector.visit_program(parsed.program_ref()); + parsed.program_ref().visit_with(&mut collector); collector } @@ -1581,6 +1686,16 @@ declare global { named_expected: atom_set!(), default_expected: None, }, + // The identifier `Foo` conflicts, but `ExportCollector` doesn't do + // anything about it. It is handled by `to_import_specifiers` method. + Test { + input: r#" +export class Foo {} +export default Foo +"#, + named_expected: atom_set!("Foo"), + default_expected: Some("Foo".into()), + }, ]; for test in tests { diff --git a/cli/util/file_watcher.rs b/cli/util/file_watcher.rs index d92d880bc1..21ea95e067 100644 --- a/cli/util/file_watcher.rs +++ b/cli/util/file_watcher.rs @@ -30,7 +30,7 @@ use tokio::sync::mpsc; use tokio::sync::mpsc::UnboundedReceiver; use tokio::time::sleep; -const CLEAR_SCREEN: &str = "\x1B[2J\x1B[1;1H"; +const CLEAR_SCREEN: &str = "\x1B[H\x1B[2J\x1B[3J"; const DEBOUNCE_INTERVAL: Duration = Duration::from_millis(200); struct DebouncedReceiver { @@ -73,7 +73,6 @@ impl DebouncedReceiver { } } -#[allow(clippy::print_stderr)] async fn error_handler(watch_future: F) -> bool where F: Future>, @@ -84,7 +83,7 @@ where Some(e) => format_js_error(e), None => format!("{err:?}"), }; - eprintln!( + log::error!( "{}: {}", colors::red_bold("error"), error_string.trim_start_matches("error: ") diff --git a/cli/util/fs.rs b/cli/util/fs.rs index fdf6035ecd..d36c02242c 100644 --- a/cli/util/fs.rs +++ b/cli/util/fs.rs @@ -20,7 +20,6 @@ use deno_core::error::AnyError; use deno_core::unsync::spawn_blocking; use deno_core::ModuleSpecifier; use deno_runtime::deno_fs::FileSystem; -use deno_runtime::deno_node::PathClean; use crate::util::path::get_atomic_file_path; use crate::util::progress_bar::ProgressBar; @@ -37,10 +36,98 @@ pub fn atomic_write_file_with_retries>( file_path: &Path, data: T, mode: u32, +) -> std::io::Result<()> { + struct RealAtomicWriteFileFs { + mode: u32, + } + + impl AtomicWriteFileFs for RealAtomicWriteFileFs { + fn write_file(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> { + write_file(path, bytes, self.mode) + } + fn rename_file(&self, from: &Path, to: &Path) -> std::io::Result<()> { + std::fs::rename(from, to) + } + fn remove_file(&self, path: &Path) -> std::io::Result<()> { + std::fs::remove_file(path) + } + fn create_dir_all(&self, dir_path: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(dir_path) + } + fn path_exists(&self, path: &Path) -> bool { + path.exists() + } + } + + atomic_write_file_with_retries_and_fs( + &RealAtomicWriteFileFs { mode }, + file_path, + data.as_ref(), + ) +} + +pub trait AtomicWriteFileFs { + fn write_file(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()>; + fn rename_file(&self, from: &Path, to: &Path) -> std::io::Result<()>; + fn remove_file(&self, path: &Path) -> std::io::Result<()>; + fn create_dir_all(&self, dir_path: &Path) -> std::io::Result<()>; + fn path_exists(&self, path: &Path) -> bool; +} + +pub struct AtomicWriteFileFsAdapter<'a> { + pub fs: &'a dyn FileSystem, + pub write_mode: u32, +} + +impl<'a> AtomicWriteFileFs for AtomicWriteFileFsAdapter<'a> { + fn write_file(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> { + self + .fs + .write_file_sync( + path, + deno_runtime::deno_fs::OpenOptions::write( + true, + false, + false, + Some(self.write_mode), + ), + None, + bytes, + ) + .map_err(|e| e.into_io_error()) + } + + fn rename_file(&self, from: &Path, to: &Path) -> std::io::Result<()> { + self.fs.rename_sync(from, to).map_err(|e| e.into_io_error()) + } + + fn remove_file(&self, path: &Path) -> std::io::Result<()> { + self + .fs + .remove_sync(path, false) + .map_err(|e| e.into_io_error()) + } + + fn create_dir_all(&self, dir_path: &Path) -> std::io::Result<()> { + self + .fs + .mkdir_sync(dir_path, /* recursive */ true, None) + .map_err(|e| e.into_io_error()) + } + + fn path_exists(&self, path: &Path) -> bool { + self.fs.exists_sync(path) + } +} + +pub fn atomic_write_file_with_retries_and_fs>( + fs: &impl AtomicWriteFileFs, + file_path: &Path, + data: T, ) -> std::io::Result<()> { let mut count = 0; loop { - match atomic_write_file(file_path, data.as_ref(), mode) { + match atomic_write_file(fs, file_path, data.as_ref()) { Ok(()) => return Ok(()), Err(err) => { if count >= 5 { @@ -61,63 +148,54 @@ pub fn atomic_write_file_with_retries>( /// /// This also handles creating the directory if a NotFound error /// occurs. -fn atomic_write_file>( +fn atomic_write_file( + fs: &impl AtomicWriteFileFs, file_path: &Path, - data: T, - mode: u32, + data: &[u8], ) -> std::io::Result<()> { fn atomic_write_file_raw( + fs: &impl AtomicWriteFileFs, temp_file_path: &Path, file_path: &Path, data: &[u8], - mode: u32, ) -> std::io::Result<()> { - write_file(temp_file_path, data, mode)?; - std::fs::rename(temp_file_path, file_path).map_err(|err| { - // clean up the created temp file on error - let _ = std::fs::remove_file(temp_file_path); - err - }) + fs.write_file(temp_file_path, data)?; + fs.rename_file(temp_file_path, file_path) + .inspect_err(|_err| { + // clean up the created temp file on error + let _ = fs.remove_file(temp_file_path); + }) } - fn inner(file_path: &Path, data: &[u8], mode: u32) -> std::io::Result<()> { - let temp_file_path = get_atomic_file_path(file_path); + let temp_file_path = get_atomic_file_path(file_path); - if let Err(write_err) = - atomic_write_file_raw(&temp_file_path, file_path, data, mode) - { - if write_err.kind() == ErrorKind::NotFound { - let parent_dir_path = file_path.parent().unwrap(); - match std::fs::create_dir_all(parent_dir_path) { - Ok(()) => { - return atomic_write_file_raw( - &temp_file_path, - file_path, - data, - mode, - ) + if let Err(write_err) = + atomic_write_file_raw(fs, &temp_file_path, file_path, data) + { + if write_err.kind() == ErrorKind::NotFound { + let parent_dir_path = file_path.parent().unwrap(); + match fs.create_dir_all(parent_dir_path) { + Ok(()) => { + return atomic_write_file_raw(fs, &temp_file_path, file_path, data) .map_err(|err| add_file_context_to_err(file_path, err)); - } - Err(create_err) => { - if !parent_dir_path.exists() { - return Err(Error::new( - create_err.kind(), - format!( - "{:#} (for '{}')\nCheck the permission of the directory.", - create_err, - parent_dir_path.display() - ), - )); - } + } + Err(create_err) => { + if !fs.path_exists(parent_dir_path) { + return Err(Error::new( + create_err.kind(), + format!( + "{:#} (for '{}')\nCheck the permission of the directory.", + create_err, + parent_dir_path.display() + ), + )); } } } - return Err(add_file_context_to_err(file_path, write_err)); } - Ok(()) + return Err(add_file_context_to_err(file_path, write_err)); } - - inner(file_path, data.as_ref(), mode) + Ok(()) } /// Creates a std::fs::File handling if the parent does not exist. @@ -199,7 +277,7 @@ pub fn write_file_2>( /// Similar to `std::fs::canonicalize()` but strips UNC prefixes on Windows. pub fn canonicalize_path(path: &Path) -> Result { - Ok(deno_core::strip_unc_prefix(path.canonicalize()?)) + Ok(deno_path_util::strip_unc_prefix(path.canonicalize()?)) } /// Canonicalizes a path which might be non-existent by going up the @@ -211,48 +289,18 @@ pub fn canonicalize_path(path: &Path) -> Result { pub fn canonicalize_path_maybe_not_exists( path: &Path, ) -> Result { - canonicalize_path_maybe_not_exists_with_custom_fn(path, canonicalize_path) + deno_path_util::canonicalize_path_maybe_not_exists(path, &canonicalize_path) } pub fn canonicalize_path_maybe_not_exists_with_fs( path: &Path, fs: &dyn FileSystem, ) -> Result { - canonicalize_path_maybe_not_exists_with_custom_fn(path, |path| { + deno_path_util::canonicalize_path_maybe_not_exists(path, &|path| { fs.realpath_sync(path).map_err(|err| err.into_io_error()) }) } -fn canonicalize_path_maybe_not_exists_with_custom_fn( - path: &Path, - canonicalize: impl Fn(&Path) -> Result, -) -> Result { - let path = path.to_path_buf().clean(); - let mut path = path.as_path(); - let mut names_stack = Vec::new(); - loop { - match canonicalize(path) { - Ok(mut canonicalized_path) => { - for name in names_stack.into_iter().rev() { - canonicalized_path = canonicalized_path.join(name); - } - return Ok(canonicalized_path); - } - Err(err) if err.kind() == ErrorKind::NotFound => { - names_stack.push(match path.file_name() { - Some(name) => name.to_owned(), - None => return Err(err), - }); - path = match path.parent() { - Some(parent) => parent, - None => return Err(err), - }; - } - Err(err) => return Err(err), - } - } -} - /// Collects module specifiers that satisfy the given predicate as a file path, by recursively walking `include`. /// Specifiers that start with http and https are left intact. /// Note: This ignores all .git and node_modules folders. @@ -517,7 +565,9 @@ pub fn symlink_dir(oldpath: &Path, newpath: &Path) -> Result<(), Error> { use std::os::windows::fs::symlink_dir; symlink_dir(oldpath, newpath).map_err(|err| { if let Some(code) = err.raw_os_error() { - if code as u32 == winapi::shared::winerror::ERROR_PRIVILEGE_NOT_HELD { + if code as u32 == winapi::shared::winerror::ERROR_PRIVILEGE_NOT_HELD + || code as u32 == winapi::shared::winerror::ERROR_INVALID_FUNCTION + { return err_mapper(err, Some(ErrorKind::PermissionDenied)); } } @@ -708,8 +758,8 @@ pub fn specifier_from_file_path( mod tests { use super::*; use deno_core::futures; - use deno_core::normalize_path; use deno_core::parking_lot::Mutex; + use deno_path_util::normalize_path; use pretty_assertions::assert_eq; use test_util::PathRef; use test_util::TempDir; diff --git a/cli/util/logger.rs b/cli/util/logger.rs index cdc89411fe..f76663df2c 100644 --- a/cli/util/logger.rs +++ b/cli/util/logger.rs @@ -29,6 +29,7 @@ impl log::Log for CliLogger { // thread's state DrawThread::hide(); self.0.log(record); + deno_runtime::ops::otel::handle_log(record); DrawThread::show(); } } @@ -65,6 +66,8 @@ pub fn init(maybe_level: Option) { .filter_module("swc_ecma_parser", log::LevelFilter::Error) // Suppress span lifecycle logs since they are too verbose .filter_module("tracing::span", log::LevelFilter::Off) + // for deno_compile, this is too verbose + .filter_module("editpe", log::LevelFilter::Error) .format(|buf, record| { let mut target = record.target().to_string(); if let Some(line_no) = record.line() { diff --git a/cli/util/mod.rs b/cli/util/mod.rs index e59b09d2c7..f81a74c449 100644 --- a/cli/util/mod.rs +++ b/cli/util/mod.rs @@ -14,6 +14,7 @@ pub mod logger; pub mod path; pub mod progress_bar; pub mod result; +pub mod retry; pub mod sync; pub mod text_encoding; pub mod unix; diff --git a/cli/util/path.rs b/cli/util/path.rs index 6f09cf1eac..58bed664f9 100644 --- a/cli/util/path.rs +++ b/cli/util/path.rs @@ -42,21 +42,6 @@ pub fn get_extension(file_path: &Path) -> Option { .map(|e| e.to_lowercase()); } -pub fn specifier_has_extension( - specifier: &ModuleSpecifier, - searching_ext: &str, -) -> bool { - let Some((_, ext)) = specifier.path().rsplit_once('.') else { - return false; - }; - let searching_ext = searching_ext.strip_prefix('.').unwrap_or(searching_ext); - debug_assert!(!searching_ext.contains('.')); // exts like .d.ts are not implemented here - if ext.len() != searching_ext.len() { - return false; - } - ext.eq_ignore_ascii_case(searching_ext) -} - pub fn get_atomic_dir_path(file_path: &Path) -> PathBuf { let rand = gen_rand_path_component(); let new_file_name = format!( @@ -165,48 +150,6 @@ pub fn relative_path(from: &Path, to: &Path) -> Option { pathdiff::diff_paths(to, from) } -/// Gets if the provided character is not supported on all -/// kinds of file systems. -pub fn is_banned_path_char(c: char) -> bool { - matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*') -} - -/// Gets a safe local directory name for the provided url. -/// -/// For example: -/// https://deno.land:8080/path -> deno.land_8080/path -pub fn root_url_to_safe_local_dirname(root: &ModuleSpecifier) -> PathBuf { - fn sanitize_segment(text: &str) -> String { - text - .chars() - .map(|c| if is_banned_segment_char(c) { '_' } else { c }) - .collect() - } - - fn is_banned_segment_char(c: char) -> bool { - matches!(c, '/' | '\\') || is_banned_path_char(c) - } - - let mut result = String::new(); - if let Some(domain) = root.domain() { - result.push_str(&sanitize_segment(domain)); - } - if let Some(port) = root.port() { - if !result.is_empty() { - result.push('_'); - } - result.push_str(&port.to_string()); - } - let mut result = PathBuf::from(result); - if let Some(segments) = root.path_segments() { - for segment in segments.filter(|s| !s.is_empty()) { - result = result.join(sanitize_segment(segment)); - } - } - - result -} - /// Slightly different behaviour than the default matching /// where an exact path needs to be matched to be opted-in /// rather than just a partial directory match. @@ -392,18 +335,6 @@ mod test { } } - #[test] - fn test_specifier_has_extension() { - fn get(specifier: &str, ext: &str) -> bool { - specifier_has_extension(&ModuleSpecifier::parse(specifier).unwrap(), ext) - } - - assert!(get("file:///a/b/c.ts", "ts")); - assert!(get("file:///a/b/c.ts", ".ts")); - assert!(!get("file:///a/b/c.ts", ".cts")); - assert!(get("file:///a/b/c.CtS", ".cts")); - } - #[test] fn test_to_percent_decoded_str() { let str = to_percent_decoded_str("%F0%9F%A6%95"); diff --git a/cli/util/progress_bar/renderer.rs b/cli/util/progress_bar/renderer.rs index a83ceb3334..6b08dada12 100644 --- a/cli/util/progress_bar/renderer.rs +++ b/cli/util/progress_bar/renderer.rs @@ -193,10 +193,16 @@ impl ProgressBarRenderer for TextOnlyProgressBarRenderer { } }; + // TODO(@marvinhagemeister): We're trying to reconstruct the original + // specifier from the resolved one, but we lack the information about + // private registries URLs and other things here. let message = display_entry .message .replace("https://registry.npmjs.org/", "npm:") - .replace("https://jsr.io/", "jsr:"); + .replace("https://jsr.io/", "jsr:") + .replace("%2f", "/") + .replace("%2F", "/"); + display_str.push_str( &colors::gray(format!(" - {}{}\n", message, bytes_text)).to_string(), ); diff --git a/cli/util/retry.rs b/cli/util/retry.rs new file mode 100644 index 0000000000..a8febe60de --- /dev/null +++ b/cli/util/retry.rs @@ -0,0 +1,41 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::future::Future; +use std::time::Duration; + +pub fn retry< + F: FnMut() -> Fut, + T, + E, + Fut: Future>, + ShouldRetry: FnMut(&E) -> bool, +>( + mut f: F, + mut should_retry: ShouldRetry, +) -> impl Future> { + const WAITS: [Duration; 3] = [ + Duration::from_millis(100), + Duration::from_millis(250), + Duration::from_millis(500), + ]; + + let mut waits = WAITS.into_iter(); + async move { + let mut first_result = None; + loop { + let result = f().await; + match result { + Ok(r) => return Ok(r), + Err(e) if !should_retry(&e) => return Err(e), + _ => {} + } + if first_result.is_none() { + first_result = Some(result); + } + let Some(wait) = waits.next() else { + return first_result.unwrap(); + }; + tokio::time::sleep(wait).await; + } + } +} diff --git a/cli/util/text_encoding.rs b/cli/util/text_encoding.rs index d2e0832c95..8524e63ebb 100644 --- a/cli/util/text_encoding.rs +++ b/cli/util/text_encoding.rs @@ -1,6 +1,8 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::borrow::Cow; use std::ops::Range; +use std::sync::Arc; use base64::prelude::BASE64_STANDARD; use base64::Engine; @@ -9,6 +11,15 @@ use deno_core::ModuleSourceCode; static SOURCE_MAP_PREFIX: &[u8] = b"//# sourceMappingURL=data:application/json;base64,"; +pub fn from_utf8_lossy_owned(bytes: Vec) -> String { + match String::from_utf8_lossy(&bytes) { + 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(bytes) }, + } +} + pub fn source_map_from_code(code: &[u8]) -> Option> { let range = find_source_map_range(code)?; let source_map_range = &code[range]; @@ -85,6 +96,29 @@ fn find_source_map_range(code: &[u8]) -> Option> { } } +/// Converts an `Arc` to an `Arc<[u8]>`. +#[allow(dead_code)] +pub fn arc_str_to_bytes(arc_str: Arc) -> Arc<[u8]> { + let raw = Arc::into_raw(arc_str); + // SAFETY: This is safe because they have the same memory layout. + unsafe { Arc::from_raw(raw as *const [u8]) } +} + +/// Converts an `Arc` to an `Arc` if able. +#[allow(dead_code)] +pub fn arc_u8_to_arc_str( + arc_u8: Arc<[u8]>, +) -> Result, std::str::Utf8Error> { + // Check that the string is valid UTF-8. + std::str::from_utf8(&arc_u8)?; + // 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::, std::sync::Arc>(arc_u8) + }) +} + #[cfg(test)] mod tests { use std::sync::Arc; diff --git a/cli/util/v8.rs b/cli/util/v8.rs index fb16e67b77..6e690e6f30 100644 --- a/cli/util/v8.rs +++ b/cli/util/v8.rs @@ -46,15 +46,14 @@ pub fn init_v8_flags( .skip(1) .collect::>(); - #[allow(clippy::print_stderr)] if !unrecognized_v8_flags.is_empty() { for f in unrecognized_v8_flags { - eprintln!("error: V8 did not recognize flag '{f}'"); + log::error!("error: V8 did not recognize flag '{f}'"); } - eprintln!("\nFor a list of V8 flags, use '--v8-flags=--help'"); - std::process::exit(1); + log::error!("\nFor a list of V8 flags, use '--v8-flags=--help'"); + deno_runtime::exit(1); } if v8_flags_includes_help { - std::process::exit(0); + deno_runtime::exit(0); } } diff --git a/cli/worker.rs b/cli/worker.rs index c355d18bd5..c6cbf77f1b 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -14,34 +14,37 @@ use deno_core::v8; use deno_core::CompiledWasmModuleStore; use deno_core::Extension; use deno_core::FeatureChecker; -use deno_core::ModuleId; use deno_core::ModuleLoader; use deno_core::PollEventLoopOptions; use deno_core::SharedArrayBufferStore; use deno_runtime::code_cache; use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel; use deno_runtime::deno_fs; -use deno_runtime::deno_node; use deno_runtime::deno_node::NodeExtInitServices; +use deno_runtime::deno_node::NodeRequireLoader; +use deno_runtime::deno_node::NodeRequireLoaderRc; use deno_runtime::deno_node::NodeResolver; +use deno_runtime::deno_node::PackageJsonResolver; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_web::BlobStore; use deno_runtime::fmt_errors::format_js_error; use deno_runtime::inspector_server::InspectorServer; +use deno_runtime::ops::otel::OtelConfig; use deno_runtime::ops::process::NpmProcessStateProviderRc; use deno_runtime::ops::worker_host::CreateWebWorkerCb; -use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::web_worker::WebWorker; use deno_runtime::web_worker::WebWorkerOptions; +use deno_runtime::web_worker::WebWorkerServiceOptions; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; +use deno_runtime::worker::WorkerServiceOptions; use deno_runtime::BootstrapOptions; use deno_runtime::WorkerExecutionMode; use deno_runtime::WorkerLogLevel; use deno_semver::npm::NpmPackageReqReference; use deno_terminal::colors; -use node_resolver::NodeResolution; +use node_resolver::NodeModuleKind; use node_resolver::NodeResolutionMode; use tokio::select; @@ -55,21 +58,22 @@ use crate::util::file_watcher::WatcherCommunicator; use crate::util::file_watcher::WatcherRestartMode; use crate::version; -pub struct ModuleLoaderAndSourceMapGetter { +pub struct CreateModuleLoaderResult { pub module_loader: Rc, + pub node_require_loader: Rc, } pub trait ModuleLoaderFactory: Send + Sync { fn create_for_main( &self, root_permissions: PermissionsContainer, - ) -> ModuleLoaderAndSourceMapGetter; + ) -> CreateModuleLoaderResult; fn create_for_worker( &self, parent_permissions: PermissionsContainer, permissions: PermissionsContainer, - ) -> ModuleLoaderAndSourceMapGetter; + ) -> CreateModuleLoaderResult; } #[async_trait::async_trait(?Send)] @@ -106,7 +110,6 @@ pub struct CliMainWorkerOptions { pub inspect_wait: bool, pub strace_ops: Option>, pub is_inspecting: bool, - pub is_npm_main: bool, pub location: Option, pub argv0: Option, pub node_debug: Option, @@ -134,21 +137,26 @@ struct SharedWorkerState { module_loader_factory: Box, node_resolver: Arc, npm_resolver: Arc, - permission_desc_parser: Arc, + pkg_json_resolver: Arc, root_cert_store_provider: Arc, root_permissions: PermissionsContainer, shared_array_buffer_store: SharedArrayBufferStore, storage_key_resolver: StorageKeyResolver, options: CliMainWorkerOptions, subcommand: DenoSubcommand, + otel_config: Option, // `None` means OpenTelemetry is disabled. } impl SharedWorkerState { - pub fn create_node_init_services(&self) -> NodeExtInitServices { + pub fn create_node_init_services( + &self, + node_require_loader: NodeRequireLoaderRc, + ) -> NodeExtInitServices { NodeExtInitServices { - node_require_resolver: self.npm_resolver.clone().into_require_resolver(), + node_require_loader, node_resolver: self.node_resolver.clone(), - npm_resolver: self.npm_resolver.clone().into_npm_resolver(), + npm_resolver: self.npm_resolver.clone().into_npm_pkg_folder_resolver(), + pkg_json_resolver: self.pkg_json_resolver.clone(), } } @@ -159,7 +167,6 @@ impl SharedWorkerState { pub struct CliMainWorker { main_module: ModuleSpecifier, - is_main_cjs: bool, worker: MainWorker, shared: Arc, } @@ -181,17 +188,7 @@ impl CliMainWorker { log::debug!("main_module {}", self.main_module); - if self.is_main_cjs { - deno_node::load_cjs_module( - &mut self.worker.js_runtime, - &self.main_module.to_file_path().unwrap().to_string_lossy(), - true, - self.shared.options.inspect_brk, - )?; - } else { - self.execute_main_module_possibly_with_npm().await?; - } - + self.execute_main_module().await?; self.worker.dispatch_load_event()?; loop { @@ -279,22 +276,7 @@ impl CliMainWorker { /// Execute the given main module emitting load and unload events before and after execution /// respectively. pub async fn execute(&mut self) -> Result<(), AnyError> { - if self.inner.is_main_cjs { - deno_node::load_cjs_module( - &mut self.inner.worker.js_runtime, - &self - .inner - .main_module - .to_file_path() - .unwrap() - .to_string_lossy(), - true, - self.inner.shared.options.inspect_brk, - )?; - } else { - self.inner.execute_main_module_possibly_with_npm().await?; - } - + self.inner.execute_main_module().await?; self.inner.worker.dispatch_load_event()?; self.pending_unload = true; @@ -335,24 +317,13 @@ impl CliMainWorker { executor.execute().await } - pub async fn execute_main_module_possibly_with_npm( - &mut self, - ) -> Result<(), AnyError> { + pub async fn execute_main_module(&mut self) -> Result<(), AnyError> { let id = self.worker.preload_main_module(&self.main_module).await?; - self.evaluate_module_possibly_with_npm(id).await + self.worker.evaluate_module(id).await } - pub async fn execute_side_module_possibly_with_npm( - &mut self, - ) -> Result<(), AnyError> { + pub async fn execute_side_module(&mut self) -> Result<(), AnyError> { let id = self.worker.preload_side_module(&self.main_module).await?; - self.evaluate_module_possibly_with_npm(id).await - } - - async fn evaluate_module_possibly_with_npm( - &mut self, - id: ModuleId, - ) -> Result<(), AnyError> { self.worker.evaluate_module(id).await } @@ -431,12 +402,13 @@ impl CliMainWorkerFactory { module_loader_factory: Box, node_resolver: Arc, npm_resolver: Arc, - permission_parser: Arc, + pkg_json_resolver: Arc, root_cert_store_provider: Arc, root_permissions: PermissionsContainer, storage_key_resolver: StorageKeyResolver, subcommand: DenoSubcommand, options: CliMainWorkerOptions, + otel_config: Option, ) -> Self { Self { shared: Arc::new(SharedWorkerState { @@ -452,13 +424,14 @@ impl CliMainWorkerFactory { module_loader_factory, node_resolver, npm_resolver, - permission_desc_parser: permission_parser, + pkg_json_resolver, root_cert_store_provider, root_permissions, shared_array_buffer_store: Default::default(), storage_key_resolver, options, subcommand, + otel_config, }), } } @@ -488,7 +461,13 @@ impl CliMainWorkerFactory { stdio: deno_runtime::deno_io::Stdio, ) -> Result { let shared = &self.shared; - let (main_module, is_main_cjs) = if let Ok(package_ref) = + let CreateModuleLoaderResult { + module_loader, + node_require_loader, + } = shared + .module_loader_factory + .create_for_main(permissions.clone()); + let main_module = if let Ok(package_ref) = NpmPackageReqReference::from_specifier(&main_module) { if let Some(npm_resolver) = shared.npm_resolver.as_managed() { @@ -508,9 +487,8 @@ impl CliMainWorkerFactory { package_ref.req(), &referrer, )?; - let node_resolution = self + let main_module = self .resolve_binary_entrypoint(&package_folder, package_ref.sub_path())?; - let is_main_cjs = matches!(node_resolution, NodeResolution::CommonJs(_)); if let Some(lockfile) = &shared.maybe_lockfile { // For npm binary commands, ensure that the lockfile gets updated @@ -519,22 +497,11 @@ impl CliMainWorkerFactory { lockfile.write_if_changed()?; } - (node_resolution.into_url(), is_main_cjs) - } else if shared.options.is_npm_main - || shared.node_resolver.in_npm_package(&main_module) - { - let node_resolution = - shared.node_resolver.url_to_node_resolution(main_module)?; - let is_main_cjs = matches!(node_resolution, NodeResolution::CommonJs(_)); - (node_resolution.into_url(), is_main_cjs) + main_module } else { - let is_cjs = main_module.path().ends_with(".cjs"); - (main_module, is_cjs) + main_module }; - let ModuleLoaderAndSourceMapGetter { module_loader } = shared - .module_loader_factory - .create_for_main(permissions.clone()); let maybe_inspector_server = shared.maybe_inspector_server.clone(); let create_web_worker_cb = @@ -570,6 +537,25 @@ impl CliMainWorkerFactory { } } + let services = WorkerServiceOptions { + root_cert_store_provider: Some(shared.root_cert_store_provider.clone()), + module_loader, + fs: shared.fs.clone(), + node_services: Some( + shared.create_node_init_services(node_require_loader), + ), + npm_process_state_provider: Some(shared.npm_process_state_provider()), + blob_store: shared.blob_store.clone(), + broadcast_channel: shared.broadcast_channel.clone(), + shared_array_buffer_store: Some(shared.shared_array_buffer_store.clone()), + compiled_wasm_module_store: Some( + shared.compiled_wasm_module_store.clone(), + ), + feature_checker, + permissions, + v8_code_cache: shared.code_cache.clone(), + }; + let options = WorkerOptions { bootstrap: BootstrapOptions { deno_version: crate::version::DENO_VERSION_INFO.deno.to_string(), @@ -596,15 +582,15 @@ impl CliMainWorkerFactory { mode, serve_port: shared.options.serve_port, serve_host: shared.options.serve_host.clone(), + otel_config: shared.otel_config.clone(), }, extensions: custom_extensions, startup_snapshot: crate::js::deno_isolate_init(), - create_params: None, + create_params: create_isolate_create_params(), unsafely_ignore_certificate_errors: shared .options .unsafely_ignore_certificate_errors .clone(), - root_cert_store_provider: Some(shared.root_cert_store_provider.clone()), seed: shared.options.seed, format_js_error_fn: Some(Arc::new(format_js_error)), create_web_worker_cb, @@ -612,29 +598,16 @@ impl CliMainWorkerFactory { should_break_on_first_statement: shared.options.inspect_brk, should_wait_for_inspector_session: shared.options.inspect_wait, strace_ops: shared.options.strace_ops.clone(), - module_loader, - fs: shared.fs.clone(), - node_services: Some(shared.create_node_init_services()), - npm_process_state_provider: Some(shared.npm_process_state_provider()), get_error_class_fn: Some(&errors::get_error_class_name), cache_storage_dir, origin_storage_dir, - blob_store: shared.blob_store.clone(), - broadcast_channel: shared.broadcast_channel.clone(), - shared_array_buffer_store: Some(shared.shared_array_buffer_store.clone()), - compiled_wasm_module_store: Some( - shared.compiled_wasm_module_store.clone(), - ), stdio, - feature_checker, - permission_desc_parser: shared.permission_desc_parser.clone(), skip_op_registration: shared.options.skip_op_registration, - v8_code_cache: shared.code_cache.clone(), }; let mut worker = MainWorker::bootstrap_from_options( main_module.clone(), - permissions, + services, options, ); @@ -657,7 +630,6 @@ impl CliMainWorkerFactory { Ok(CliMainWorker { main_module, - is_main_cjs, worker, shared: shared.clone(), }) @@ -667,19 +639,19 @@ impl CliMainWorkerFactory { &self, package_folder: &Path, sub_path: Option<&str>, - ) -> Result { + ) -> Result { match self .shared .node_resolver .resolve_binary_export(package_folder, sub_path) { - Ok(node_resolution) => Ok(node_resolution), + Ok(specifier) => Ok(specifier), Err(original_err) => { // if the binary entrypoint was not found, fallback to regular node resolution let result = self.resolve_binary_entrypoint_fallback(package_folder, sub_path); match result { - Ok(Some(resolution)) => Ok(resolution), + Ok(Some(specifier)) => Ok(specifier), Ok(None) => Err(original_err.into()), Err(fallback_err) => { bail!("{:#}\n\nFallback failed: {:#}", original_err, fallback_err) @@ -694,7 +666,7 @@ impl CliMainWorkerFactory { &self, package_folder: &Path, sub_path: Option<&str>, - ) -> Result, AnyError> { + ) -> Result, AnyError> { // only fallback if the user specified a sub path if sub_path.is_none() { // it's confusing to users if the package doesn't have any binary @@ -703,28 +675,24 @@ impl CliMainWorkerFactory { return Ok(None); } - let resolution = self + let specifier = self .shared .node_resolver .resolve_package_subpath_from_deno_module( package_folder, sub_path, /* referrer */ None, + NodeModuleKind::Esm, NodeResolutionMode::Execution, )?; - match &resolution { - NodeResolution::BuiltIn(_) => Ok(None), - NodeResolution::CommonJs(specifier) | NodeResolution::Esm(specifier) => { - if specifier - .to_file_path() - .map(|p| p.exists()) - .unwrap_or(false) - { - Ok(Some(resolution)) - } else { - bail!("Cannot find module '{}'", specifier) - } - } + if specifier + .to_file_path() + .map(|p| p.exists()) + .unwrap_or(false) + { + Ok(Some(specifier)) + } else { + bail!("Cannot find module '{}'", specifier) } } } @@ -736,11 +704,13 @@ fn create_web_worker_callback( Arc::new(move |args| { let maybe_inspector_server = shared.maybe_inspector_server.clone(); - let ModuleLoaderAndSourceMapGetter { module_loader } = - shared.module_loader_factory.create_for_worker( - args.parent_permissions.clone(), - args.permissions.clone(), - ); + let CreateModuleLoaderResult { + module_loader, + node_require_loader, + } = shared.module_loader_factory.create_for_worker( + args.parent_permissions.clone(), + args.permissions.clone(), + ); let create_web_worker_cb = create_web_worker_callback(shared.clone(), stdio.clone()); @@ -766,7 +736,28 @@ fn create_web_worker_callback( } } + let services = WebWorkerServiceOptions { + root_cert_store_provider: Some(shared.root_cert_store_provider.clone()), + module_loader, + fs: shared.fs.clone(), + node_services: Some( + shared.create_node_init_services(node_require_loader), + ), + blob_store: shared.blob_store.clone(), + broadcast_channel: shared.broadcast_channel.clone(), + shared_array_buffer_store: Some(shared.shared_array_buffer_store.clone()), + compiled_wasm_module_store: Some( + shared.compiled_wasm_module_store.clone(), + ), + maybe_inspector_server, + feature_checker, + npm_process_state_provider: Some(shared.npm_process_state_provider()), + permissions: args.permissions, + }; let options = WebWorkerOptions { + name: args.name, + main_module: args.main_module.clone(), + worker_id: args.worker_id, bootstrap: BootstrapOptions { deno_version: crate::version::DENO_VERSION_INFO.deno.to_string(), args: shared.options.argv.clone(), @@ -777,7 +768,7 @@ fn create_web_worker_callback( enable_op_summary_metrics: shared.options.enable_op_summary_metrics, enable_testing_features: shared.options.enable_testing_features, locale: deno_core::v8::icu::get_language_tag(), - location: Some(args.main_module.clone()), + location: Some(args.main_module), no_color: !colors::use_color(), color_level: colors::get_color_level(), is_stdout_tty: deno_terminal::is_stdout_tty(), @@ -792,46 +783,39 @@ fn create_web_worker_callback( mode: WorkerExecutionMode::Worker, serve_port: shared.options.serve_port, serve_host: shared.options.serve_host.clone(), + otel_config: shared.otel_config.clone(), }, extensions: vec![], startup_snapshot: crate::js::deno_isolate_init(), + create_params: create_isolate_create_params(), unsafely_ignore_certificate_errors: shared .options .unsafely_ignore_certificate_errors .clone(), - root_cert_store_provider: Some(shared.root_cert_store_provider.clone()), seed: shared.options.seed, create_web_worker_cb, format_js_error_fn: Some(Arc::new(format_js_error)), - module_loader, - fs: shared.fs.clone(), - node_services: Some(shared.create_node_init_services()), worker_type: args.worker_type, - maybe_inspector_server, get_error_class_fn: Some(&errors::get_error_class_name), - blob_store: shared.blob_store.clone(), - broadcast_channel: shared.broadcast_channel.clone(), - shared_array_buffer_store: Some(shared.shared_array_buffer_store.clone()), - compiled_wasm_module_store: Some( - shared.compiled_wasm_module_store.clone(), - ), stdio: stdio.clone(), cache_storage_dir, - feature_checker, - permission_desc_parser: shared.permission_desc_parser.clone(), strace_ops: shared.options.strace_ops.clone(), close_on_idle: args.close_on_idle, maybe_worker_metadata: args.maybe_worker_metadata, - npm_process_state_provider: Some(shared.npm_process_state_provider()), }; - WebWorker::bootstrap_from_options( - args.name, - args.permissions, - args.main_module, - args.worker_id, - options, - ) + WebWorker::bootstrap_from_options(services, options) + }) +} + +/// By default V8 uses 1.4Gb heap limit which is meant for browser tabs. +/// Instead probe for the total memory on the system and use it instead +/// as a default. +pub fn create_isolate_create_params() -> Option { + let maybe_mem_info = deno_runtime::sys_info::mem_info(); + maybe_mem_info.map(|mem_info| { + v8::CreateParams::default() + .heap_limits_from_system_memory(mem_info.total, 0) }) } @@ -841,23 +825,43 @@ fn create_web_worker_callback( mod tests { use super::*; use deno_core::resolve_path; + use deno_core::FsModuleLoader; use deno_fs::RealFs; use deno_runtime::deno_permissions::Permissions; + use deno_runtime::permissions::RuntimePermissionDescriptorParser; fn create_test_worker() -> MainWorker { let main_module = resolve_path("./hello.js", &std::env::current_dir().unwrap()).unwrap(); - let permissions = PermissionsContainer::new( - Arc::new(RuntimePermissionDescriptorParser::new(Arc::new(RealFs))), - Permissions::none_without_prompt(), - ); - + let fs = Arc::new(RealFs); + let permission_desc_parser = + Arc::new(RuntimePermissionDescriptorParser::new(fs.clone())); let options = WorkerOptions { startup_snapshot: crate::js::deno_isolate_init(), ..Default::default() }; - MainWorker::bootstrap_from_options(main_module, permissions, options) + MainWorker::bootstrap_from_options( + main_module, + WorkerServiceOptions { + module_loader: Rc::new(FsModuleLoader), + permissions: PermissionsContainer::new( + permission_desc_parser, + Permissions::none_without_prompt(), + ), + blob_store: Default::default(), + broadcast_channel: Default::default(), + feature_checker: Default::default(), + node_services: Default::default(), + npm_process_state_provider: Default::default(), + root_cert_store_provider: Default::default(), + shared_array_buffer_store: Default::default(), + compiled_wasm_module_store: Default::default(), + v8_code_cache: Default::default(), + fs, + }, + options, + ) } #[tokio::test] diff --git a/ext/broadcast_channel/Cargo.toml b/ext/broadcast_channel/Cargo.toml index 8b8c9a1883..90ac038357 100644 --- a/ext/broadcast_channel/Cargo.toml +++ b/ext/broadcast_channel/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_broadcast_channel" -version = "0.162.0" +version = "0.171.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -16,5 +16,6 @@ path = "lib.rs" [dependencies] async-trait.workspace = true deno_core.workspace = true +thiserror.workspace = true tokio.workspace = true uuid.workspace = true diff --git a/ext/broadcast_channel/in_memory_broadcast_channel.rs b/ext/broadcast_channel/in_memory_broadcast_channel.rs index 00b52a9d60..61dc68e17d 100644 --- a/ext/broadcast_channel/in_memory_broadcast_channel.rs +++ b/ext/broadcast_channel/in_memory_broadcast_channel.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use async_trait::async_trait; -use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use tokio::sync::broadcast; use tokio::sync::mpsc; use uuid::Uuid; use crate::BroadcastChannel; +use crate::BroadcastChannelError; #[derive(Clone)] pub struct InMemoryBroadcastChannel(Arc>>); @@ -41,7 +41,7 @@ impl Default for InMemoryBroadcastChannel { impl BroadcastChannel for InMemoryBroadcastChannel { type Resource = InMemoryBroadcastChannelResource; - fn subscribe(&self) -> Result { + fn subscribe(&self) -> Result { let (cancel_tx, cancel_rx) = mpsc::unbounded_channel(); let broadcast_rx = self.0.lock().subscribe(); let rx = tokio::sync::Mutex::new((broadcast_rx, cancel_rx)); @@ -53,7 +53,10 @@ impl BroadcastChannel for InMemoryBroadcastChannel { }) } - fn unsubscribe(&self, resource: &Self::Resource) -> Result<(), AnyError> { + fn unsubscribe( + &self, + resource: &Self::Resource, + ) -> Result<(), BroadcastChannelError> { Ok(resource.cancel_tx.send(())?) } @@ -62,7 +65,7 @@ impl BroadcastChannel for InMemoryBroadcastChannel { resource: &Self::Resource, name: String, data: Vec, - ) -> Result<(), AnyError> { + ) -> Result<(), BroadcastChannelError> { let name = Arc::new(name); let data = Arc::new(data); let uuid = resource.uuid; @@ -73,7 +76,7 @@ impl BroadcastChannel for InMemoryBroadcastChannel { async fn recv( &self, resource: &Self::Resource, - ) -> Result, AnyError> { + ) -> Result, BroadcastChannelError> { let mut g = resource.rx.lock().await; let (broadcast_rx, cancel_rx) = &mut *g; loop { diff --git a/ext/broadcast_channel/lib.rs b/ext/broadcast_channel/lib.rs index 47c48656d8..c1de118a36 100644 --- a/ext/broadcast_channel/lib.rs +++ b/ext/broadcast_channel/lib.rs @@ -10,34 +10,69 @@ use std::path::PathBuf; use std::rc::Rc; use async_trait::async_trait; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::JsBuffer; use deno_core::OpState; use deno_core::Resource; use deno_core::ResourceId; +use tokio::sync::broadcast::error::SendError as BroadcastSendError; +use tokio::sync::mpsc::error::SendError as MpscSendError; pub const UNSTABLE_FEATURE_NAME: &str = "broadcast-channel"; +#[derive(Debug, thiserror::Error)] +pub enum BroadcastChannelError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + MPSCSendError(MpscSendError>), + #[error(transparent)] + BroadcastSendError( + BroadcastSendError>, + ), + #[error(transparent)] + Other(deno_core::error::AnyError), +} + +impl From> + for BroadcastChannelError +{ + fn from(value: MpscSendError) -> Self { + BroadcastChannelError::MPSCSendError(MpscSendError(Box::new(value.0))) + } +} +impl From> + for BroadcastChannelError +{ + fn from(value: BroadcastSendError) -> Self { + BroadcastChannelError::BroadcastSendError(BroadcastSendError(Box::new( + value.0, + ))) + } +} + #[async_trait] pub trait BroadcastChannel: Clone { type Resource: Resource; - fn subscribe(&self) -> Result; + fn subscribe(&self) -> Result; - fn unsubscribe(&self, resource: &Self::Resource) -> Result<(), AnyError>; + fn unsubscribe( + &self, + resource: &Self::Resource, + ) -> Result<(), BroadcastChannelError>; async fn send( &self, resource: &Self::Resource, name: String, data: Vec, - ) -> Result<(), AnyError>; + ) -> Result<(), BroadcastChannelError>; async fn recv( &self, resource: &Self::Resource, - ) -> Result, AnyError>; + ) -> Result, BroadcastChannelError>; } pub type Message = (String, Vec); @@ -46,7 +81,7 @@ pub type Message = (String, Vec); #[smi] pub fn op_broadcast_subscribe( state: &mut OpState, -) -> Result +) -> Result where BC: BroadcastChannel + 'static, { @@ -62,11 +97,14 @@ where pub fn op_broadcast_unsubscribe( state: &mut OpState, #[smi] rid: ResourceId, -) -> Result<(), AnyError> +) -> Result<(), BroadcastChannelError> where BC: BroadcastChannel + 'static, { - let resource = state.resource_table.get::(rid)?; + let resource = state + .resource_table + .get::(rid) + .map_err(BroadcastChannelError::Resource)?; let bc = state.borrow::(); bc.unsubscribe(&resource) } @@ -77,11 +115,15 @@ pub async fn op_broadcast_send( #[smi] rid: ResourceId, #[string] name: String, #[buffer] buf: JsBuffer, -) -> Result<(), AnyError> +) -> Result<(), BroadcastChannelError> where BC: BroadcastChannel + 'static, { - let resource = state.borrow().resource_table.get::(rid)?; + let resource = state + .borrow() + .resource_table + .get::(rid) + .map_err(BroadcastChannelError::Resource)?; let bc = state.borrow().borrow::().clone(); bc.send(&resource, name, buf.to_vec()).await } @@ -91,11 +133,15 @@ where pub async fn op_broadcast_recv( state: Rc>, #[smi] rid: ResourceId, -) -> Result, AnyError> +) -> Result, BroadcastChannelError> where BC: BroadcastChannel + 'static, { - let resource = state.borrow().resource_table.get::(rid)?; + let resource = state + .borrow() + .resource_table + .get::(rid) + .map_err(BroadcastChannelError::Resource)?; let bc = state.borrow().borrow::().clone(); bc.recv(&resource).await } diff --git a/ext/cache/Cargo.toml b/ext/cache/Cargo.toml index 5c6f162f17..56fa0a527f 100644 --- a/ext/cache/Cargo.toml +++ b/ext/cache/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_cache" -version = "0.100.0" +version = "0.109.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -19,4 +19,5 @@ deno_core.workspace = true rusqlite.workspace = true serde.workspace = true sha2.workspace = true +thiserror.workspace = true tokio.workspace = true diff --git a/ext/cache/lib.rs b/ext/cache/lib.rs index f6d758b95c..524d4cea05 100644 --- a/ext/cache/lib.rs +++ b/ext/cache/lib.rs @@ -7,7 +7,6 @@ use std::sync::Arc; use async_trait::async_trait; use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; @@ -19,8 +18,24 @@ use deno_core::ResourceId; mod sqlite; pub use sqlite::SqliteBackedCache; +#[derive(Debug, thiserror::Error)] +pub enum CacheError { + #[error(transparent)] + Sqlite(#[from] rusqlite::Error), + #[error(transparent)] + JoinError(#[from] tokio::task::JoinError), + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + Other(deno_core::error::AnyError), + #[error("{0}")] + Io(#[from] std::io::Error), +} + #[derive(Clone)] -pub struct CreateCache(pub Arc C>); +pub struct CreateCache( + pub Arc Result>, +); deno_core::extension!(deno_cache, deps = [ deno_webidl, deno_web, deno_url, deno_fetch ], @@ -92,26 +107,31 @@ pub struct CacheDeleteRequest { pub trait Cache: Clone + 'static { type CacheMatchResourceType: Resource; - async fn storage_open(&self, cache_name: String) -> Result; - async fn storage_has(&self, cache_name: String) -> Result; - async fn storage_delete(&self, cache_name: String) -> Result; + async fn storage_open(&self, cache_name: String) -> Result; + async fn storage_has(&self, cache_name: String) -> Result; + async fn storage_delete( + &self, + cache_name: String, + ) -> Result; /// Put a resource into the cache. async fn put( &self, request_response: CachePutRequest, resource: Option>, - ) -> Result<(), AnyError>; + ) -> Result<(), CacheError>; async fn r#match( &self, request: CacheMatchRequest, ) -> Result< Option<(CacheMatchResponseMeta, Option)>, - AnyError, + CacheError, >; - async fn delete(&self, request: CacheDeleteRequest) - -> Result; + async fn delete( + &self, + request: CacheDeleteRequest, + ) -> Result; } #[op2(async)] @@ -119,7 +139,7 @@ pub trait Cache: Clone + 'static { pub async fn op_cache_storage_open( state: Rc>, #[string] cache_name: String, -) -> Result +) -> Result where CA: Cache, { @@ -131,7 +151,7 @@ where pub async fn op_cache_storage_has( state: Rc>, #[string] cache_name: String, -) -> Result +) -> Result where CA: Cache, { @@ -143,7 +163,7 @@ where pub async fn op_cache_storage_delete( state: Rc>, #[string] cache_name: String, -) -> Result +) -> Result where CA: Cache, { @@ -155,13 +175,19 @@ where pub async fn op_cache_put( state: Rc>, #[serde] request_response: CachePutRequest, -) -> Result<(), AnyError> +) -> Result<(), CacheError> where CA: Cache, { let cache = get_cache::(&state)?; let resource = match request_response.response_rid { - Some(rid) => Some(state.borrow_mut().resource_table.take_any(rid)?), + Some(rid) => Some( + state + .borrow_mut() + .resource_table + .take_any(rid) + .map_err(CacheError::Resource)?, + ), None => None, }; cache.put(request_response, resource).await @@ -172,7 +198,7 @@ where pub async fn op_cache_match( state: Rc>, #[serde] request: CacheMatchRequest, -) -> Result, AnyError> +) -> Result, CacheError> where CA: Cache, { @@ -191,7 +217,7 @@ where pub async fn op_cache_delete( state: Rc>, #[serde] request: CacheDeleteRequest, -) -> Result +) -> Result where CA: Cache, { @@ -199,7 +225,7 @@ where cache.delete(request).await } -pub fn get_cache(state: &Rc>) -> Result +pub fn get_cache(state: &Rc>) -> Result where CA: Cache, { @@ -207,11 +233,13 @@ where if let Some(cache) = state.try_borrow::() { Ok(cache.clone()) } else if let Some(create_cache) = state.try_borrow::>() { - let cache = create_cache.0(); + let cache = create_cache.0()?; state.put(cache); Ok(state.borrow::().clone()) } else { - Err(type_error("CacheStorage is not available in this context")) + Err(CacheError::Other(type_error( + "CacheStorage is not available in this context", + ))) } } diff --git a/ext/cache/sqlite.rs b/ext/cache/sqlite.rs index c3c55dd5e9..6efceda11e 100644 --- a/ext/cache/sqlite.rs +++ b/ext/cache/sqlite.rs @@ -30,6 +30,7 @@ use crate::serialize_headers; use crate::vary_header_matches; use crate::Cache; use crate::CacheDeleteRequest; +use crate::CacheError; use crate::CacheMatchRequest; use crate::CacheMatchResponseMeta; use crate::CachePutRequest; @@ -41,7 +42,7 @@ pub struct SqliteBackedCache { } impl SqliteBackedCache { - pub fn new(cache_storage_dir: PathBuf) -> Self { + pub fn new(cache_storage_dir: PathBuf) -> Result { { std::fs::create_dir_all(&cache_storage_dir) .expect("failed to create cache dir"); @@ -56,18 +57,14 @@ impl SqliteBackedCache { PRAGMA synchronous=NORMAL; PRAGMA optimize; "; - connection - .execute_batch(initial_pragmas) - .expect("failed to execute pragmas"); - connection - .execute( - "CREATE TABLE IF NOT EXISTS cache_storage ( + connection.execute_batch(initial_pragmas)?; + connection.execute( + "CREATE TABLE IF NOT EXISTS cache_storage ( id INTEGER PRIMARY KEY, cache_name TEXT NOT NULL UNIQUE )", - (), - ) - .expect("failed to create cache_storage table"); + (), + )?; connection .execute( "CREATE TABLE IF NOT EXISTS request_response_list ( @@ -85,12 +82,11 @@ impl SqliteBackedCache { UNIQUE (cache_id, request_url) )", (), - ) - .expect("failed to create request_response_list table"); - SqliteBackedCache { + )?; + Ok(SqliteBackedCache { connection: Arc::new(Mutex::new(connection)), cache_storage_dir, - } + }) } } } @@ -102,7 +98,7 @@ impl Cache for SqliteBackedCache { /// Open a cache storage. Internally, this creates a row in the /// sqlite db if the cache doesn't exist and returns the internal id /// of the cache. - async fn storage_open(&self, cache_name: String) -> Result { + async fn storage_open(&self, cache_name: String) -> Result { let db = self.connection.clone(); let cache_storage_dir = self.cache_storage_dir.clone(); spawn_blocking(move || { @@ -121,14 +117,14 @@ impl Cache for SqliteBackedCache { )?; let responses_dir = get_responses_dir(cache_storage_dir, cache_id); std::fs::create_dir_all(responses_dir)?; - Ok::(cache_id) + Ok::(cache_id) }) .await? } /// Check if a cache with the provided name exists. /// Note: this doesn't check the disk, it only checks the sqlite db. - async fn storage_has(&self, cache_name: String) -> Result { + async fn storage_has(&self, cache_name: String) -> Result { let db = self.connection.clone(); spawn_blocking(move || { let db = db.lock(); @@ -140,13 +136,16 @@ impl Cache for SqliteBackedCache { Ok(count > 0) }, )?; - Ok::(cache_exists) + Ok::(cache_exists) }) .await? } /// Delete a cache storage. Internally, this deletes the row in the sqlite db. - async fn storage_delete(&self, cache_name: String) -> Result { + async fn storage_delete( + &self, + cache_name: String, + ) -> Result { let db = self.connection.clone(); let cache_storage_dir = self.cache_storage_dir.clone(); spawn_blocking(move || { @@ -167,7 +166,7 @@ impl Cache for SqliteBackedCache { std::fs::remove_dir_all(cache_dir)?; } } - Ok::(maybe_cache_id.is_some()) + Ok::(maybe_cache_id.is_some()) }) .await? } @@ -176,10 +175,12 @@ impl Cache for SqliteBackedCache { &self, request_response: CachePutRequest, resource: Option>, - ) -> Result<(), AnyError> { + ) -> Result<(), CacheError> { let db = self.connection.clone(); let cache_storage_dir = self.cache_storage_dir.clone(); - let now = SystemTime::now().duration_since(UNIX_EPOCH)?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("SystemTime is before unix epoch"); if let Some(resource) = resource { let body_key = hash(&format!( @@ -193,7 +194,11 @@ impl Cache for SqliteBackedCache { let mut file = tokio::fs::File::create(response_path).await?; let mut buf = BufMutView::new(64 * 1024); loop { - let (size, buf2) = resource.clone().read_byob(buf).await?; + let (size, buf2) = resource + .clone() + .read_byob(buf) + .await + .map_err(CacheError::Other)?; if size == 0 { break; } @@ -224,7 +229,7 @@ impl Cache for SqliteBackedCache { request: CacheMatchRequest, ) -> Result< Option<(CacheMatchResponseMeta, Option)>, - AnyError, + CacheError, > { let db = self.connection.clone(); let cache_storage_dir = self.cache_storage_dir.clone(); @@ -290,19 +295,17 @@ impl Cache for SqliteBackedCache { } Err(err) => return Err(err.into()), }; - return Ok(Some((cache_meta, Some(CacheResponseResource::new(file))))); + Ok(Some((cache_meta, Some(CacheResponseResource::new(file))))) } - Some((cache_meta, None)) => { - return Ok(Some((cache_meta, None))); - } - None => return Ok(None), + Some((cache_meta, None)) => Ok(Some((cache_meta, None))), + None => Ok(None), } } async fn delete( &self, request: CacheDeleteRequest, - ) -> Result { + ) -> Result { let db = self.connection.clone(); spawn_blocking(move || { // TODO(@satyarohith): remove the response body from disk if one exists @@ -311,17 +314,17 @@ impl Cache for SqliteBackedCache { "DELETE FROM request_response_list WHERE cache_id = ?1 AND request_url = ?2", (request.cache_id, &request.request_url), )?; - Ok::(rows_effected > 0) + Ok::(rows_effected > 0) }) .await? } } async fn insert_cache_asset( - db: Arc>, + db: Arc>, put: CachePutRequest, response_body_key: Option, -) -> Result, deno_core::anyhow::Error> { +) -> Result, CacheError> { spawn_blocking(move || { let maybe_response_body = { let db = db.lock(); @@ -339,7 +342,7 @@ async fn insert_cache_asset( response_body_key, put.response_status, put.response_status_text, - SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), + SystemTime::now().duration_since(UNIX_EPOCH).expect("SystemTime is before unix epoch").as_secs(), ), |row| { let response_body_key: Option = row.get(0)?; @@ -347,7 +350,7 @@ async fn insert_cache_asset( }, )? }; - Ok::, AnyError>(maybe_response_body) + Ok::, CacheError>(maybe_response_body) }).await? } diff --git a/ext/canvas/Cargo.toml b/ext/canvas/Cargo.toml index 96881682a4..4231d7c84d 100644 --- a/ext/canvas/Cargo.toml +++ b/ext/canvas/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_canvas" -version = "0.37.0" +version = "0.46.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -18,3 +18,4 @@ deno_core.workspace = true deno_webgpu.workspace = true image = { version = "0.24.7", default-features = false, features = ["png"] } serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true diff --git a/ext/canvas/lib.rs b/ext/canvas/lib.rs index 72173f1331..defb288ac9 100644 --- a/ext/canvas/lib.rs +++ b/ext/canvas/lib.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::ToJsBuffer; use image::imageops::FilterType; @@ -13,6 +11,14 @@ use serde::Deserialize; use serde::Serialize; use std::path::PathBuf; +#[derive(Debug, thiserror::Error)] +pub enum CanvasError { + #[error("Color type '{0:?}' not supported")] + UnsupportedColorType(ColorType), + #[error(transparent)] + Image(#[from] image::ImageError), +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "snake_case")] enum ImageResizeQuality { @@ -43,7 +49,7 @@ struct ImageProcessArgs { fn op_image_process( #[buffer] buf: &[u8], #[serde] args: ImageProcessArgs, -) -> Result { +) -> ToJsBuffer { let view = RgbaImage::from_vec(args.width, args.height, buf.to_vec()).unwrap(); @@ -105,7 +111,7 @@ fn op_image_process( } } - Ok(image_out.to_vec().into()) + image_out.to_vec().into() } #[derive(Debug, Serialize)] @@ -117,17 +123,16 @@ struct DecodedPng { #[op2] #[serde] -fn op_image_decode_png(#[buffer] buf: &[u8]) -> Result { +fn op_image_decode_png( + #[buffer] buf: &[u8], +) -> Result { let png = image::codecs::png::PngDecoder::new(buf)?; let (width, height) = png.dimensions(); // TODO(@crowlKats): maybe use DynamicImage https://docs.rs/image/0.24.7/image/enum.DynamicImage.html ? if png.color_type() != ColorType::Rgba8 { - return Err(type_error(format!( - "Color type '{:?}' not supported", - png.color_type() - ))); + return Err(CanvasError::UnsupportedColorType(png.color_type())); } // read_image will assert that the buffer is the correct size, so we need to fill it with zeros diff --git a/ext/console/01_console.js b/ext/console/01_console.js index 1ca23d5a49..3803492b90 100644 --- a/ext/console/01_console.js +++ b/ext/console/01_console.js @@ -84,6 +84,7 @@ const { NumberIsInteger, NumberIsNaN, NumberParseInt, + NumberParseFloat, NumberPrototypeToFixed, NumberPrototypeToString, NumberPrototypeValueOf, @@ -1301,7 +1302,9 @@ function getKeys(value, showHidden) { ArrayPrototypePushApply(keys, ArrayPrototypeFilter(symbols, filter)); } } - keys = ArrayPrototypeFilter(keys, (key) => key !== "cause"); + if (ObjectPrototypeIsPrototypeOf(ErrorPrototype, value)) { + keys = ArrayPrototypeFilter(keys, (key) => key !== "cause"); + } return keys; } @@ -2650,6 +2653,7 @@ const HSL_PATTERN = new SafeRegExp( ); function parseCssColor(colorString) { + colorString = StringPrototypeToLowerCase(colorString); if (colorKeywords.has(colorString)) { colorString = colorKeywords.get(colorString); } @@ -3008,20 +3012,18 @@ function inspectArgs(args, inspectOptions = { __proto__: null }) { } else if (ArrayPrototypeIncludes(["d", "i"], char)) { // Format as an integer. const value = args[a++]; - if (typeof value == "bigint") { - formattedArg = `${value}n`; - } else if (typeof value == "number") { - formattedArg = `${NumberParseInt(String(value))}`; - } else { + if (typeof value === "symbol") { formattedArg = "NaN"; + } else { + formattedArg = `${NumberParseInt(value)}`; } } else if (char == "f") { // Format as a floating point value. const value = args[a++]; - if (typeof value == "number") { - formattedArg = `${value}`; - } else { + if (typeof value === "symbol") { formattedArg = "NaN"; + } else { + formattedArg = `${NumberParseFloat(value)}`; } } else if (ArrayPrototypeIncludes(["O", "o"], char)) { // Format as an object. @@ -3255,7 +3257,7 @@ class Console { const stringifyValue = (value) => inspectValueWithQuotes(value, { - ...getDefaultInspectOptions(), + ...getConsoleInspectOptions(noColorStdout()), depth: 1, compact: true, }); diff --git a/ext/console/Cargo.toml b/ext/console/Cargo.toml index ef64398584..80f1cca84d 100644 --- a/ext/console/Cargo.toml +++ b/ext/console/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_console" -version = "0.168.0" +version = "0.177.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/cron/Cargo.toml b/ext/cron/Cargo.toml index 966ac450e9..966ccdc958 100644 --- a/ext/cron/Cargo.toml +++ b/ext/cron/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_cron" -version = "0.48.0" +version = "0.57.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -19,4 +19,5 @@ async-trait.workspace = true chrono = { workspace = true, features = ["now"] } deno_core.workspace = true saffron.workspace = true +thiserror.workspace = true tokio.workspace = true diff --git a/ext/cron/interface.rs b/ext/cron/interface.rs index 01b1d17895..a19525cc4e 100644 --- a/ext/cron/interface.rs +++ b/ext/cron/interface.rs @@ -1,17 +1,17 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use crate::CronError; use async_trait::async_trait; -use deno_core::error::AnyError; pub trait CronHandler { type EH: CronHandle + 'static; - fn create(&self, spec: CronSpec) -> Result; + fn create(&self, spec: CronSpec) -> Result; } #[async_trait(?Send)] pub trait CronHandle { - async fn next(&self, prev_success: bool) -> Result; + async fn next(&self, prev_success: bool) -> Result; fn close(&self); } diff --git a/ext/cron/lib.rs b/ext/cron/lib.rs index e350e4d698..feffb5e511 100644 --- a/ext/cron/lib.rs +++ b/ext/cron/lib.rs @@ -7,16 +7,13 @@ use std::borrow::Cow; use std::cell::RefCell; use std::rc::Rc; +pub use crate::interface::*; use deno_core::error::get_custom_error_class; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; use deno_core::ResourceId; -pub use crate::interface::*; - pub const UNSTABLE_FEATURE_NAME: &str = "cron"; deno_core::extension!(deno_cron, @@ -49,6 +46,28 @@ impl Resource for CronResource { } } +#[derive(Debug, thiserror::Error)] +pub enum CronError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error("Cron name cannot exceed 64 characters: current length {0}")] + NameExceeded(usize), + #[error("Invalid cron name: only alphanumeric characters, whitespace, hyphens, and underscores are allowed")] + NameInvalid, + #[error("Cron with this name already exists")] + AlreadyExists, + #[error("Too many crons")] + TooManyCrons, + #[error("Invalid cron schedule")] + InvalidCron, + #[error("Invalid backoff schedule")] + InvalidBackoff, + #[error(transparent)] + AcquireError(#[from] tokio::sync::AcquireError), + #[error(transparent)] + Other(deno_core::error::AnyError), +} + #[op2] #[smi] fn op_cron_create( @@ -56,7 +75,7 @@ fn op_cron_create( #[string] name: String, #[string] cron_schedule: String, #[serde] backoff_schedule: Option>, -) -> Result +) -> Result where C: CronHandler + 'static, { @@ -90,7 +109,7 @@ async fn op_cron_next( state: Rc>, #[smi] rid: ResourceId, prev_success: bool, -) -> Result +) -> Result where C: CronHandler + 'static, { @@ -102,7 +121,7 @@ where if get_custom_error_class(&err) == Some("BadResource") { return Ok(false); } else { - return Err(err); + return Err(CronError::Resource(err)); } } }; @@ -112,17 +131,14 @@ where cron_handler.next(prev_success).await } -fn validate_cron_name(name: &str) -> Result<(), AnyError> { +fn validate_cron_name(name: &str) -> Result<(), CronError> { if name.len() > 64 { - return Err(type_error(format!( - "Cron name cannot exceed 64 characters: current length {}", - name.len() - ))); + return Err(CronError::NameExceeded(name.len())); } if !name.chars().all(|c| { c.is_ascii_whitespace() || c.is_ascii_alphanumeric() || c == '_' || c == '-' }) { - return Err(type_error("Invalid cron name: only alphanumeric characters, whitespace, hyphens, and underscores are allowed")); + return Err(CronError::NameInvalid); } Ok(()) } diff --git a/ext/cron/local.rs b/ext/cron/local.rs index dd60e750a0..1110baadb8 100644 --- a/ext/cron/local.rs +++ b/ext/cron/local.rs @@ -10,8 +10,6 @@ use std::rc::Weak; use std::sync::Arc; use async_trait::async_trait; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::futures; use deno_core::futures::FutureExt; use deno_core::unsync::spawn; @@ -21,6 +19,7 @@ use tokio::sync::mpsc::WeakSender; use tokio::sync::OwnedSemaphorePermit; use tokio::sync::Semaphore; +use crate::CronError; use crate::CronHandle; use crate::CronHandler; use crate::CronSpec; @@ -81,7 +80,7 @@ impl LocalCronHandler { async fn cron_loop( runtime_state: Rc>, mut cron_schedule_rx: mpsc::Receiver<(String, bool)>, - ) -> Result<(), AnyError> { + ) -> Result<(), CronError> { loop { let earliest_deadline = runtime_state .borrow() @@ -154,7 +153,7 @@ impl LocalCronHandler { impl RuntimeState { fn get_ready_crons( &mut self, - ) -> Result)>, AnyError> { + ) -> Result)>, CronError> { let now = chrono::Utc::now().timestamp_millis() as u64; let ready = { @@ -191,7 +190,7 @@ impl RuntimeState { impl CronHandler for LocalCronHandler { type EH = CronExecutionHandle; - fn create(&self, spec: CronSpec) -> Result { + fn create(&self, spec: CronSpec) -> Result { // Ensure that the cron loop is started. self.cron_loop_join_handle.get_or_init(|| { let (cron_schedule_tx, cron_schedule_rx) = @@ -208,17 +207,17 @@ impl CronHandler for LocalCronHandler { let mut runtime_state = self.runtime_state.borrow_mut(); if runtime_state.crons.len() > MAX_CRONS { - return Err(type_error("Too many crons")); + return Err(CronError::TooManyCrons); } if runtime_state.crons.contains_key(&spec.name) { - return Err(type_error("Cron with this name already exists")); + return Err(CronError::AlreadyExists); } // Validate schedule expression. spec .cron_schedule .parse::() - .map_err(|_| type_error("Invalid cron schedule"))?; + .map_err(|_| CronError::InvalidCron)?; // Validate backoff_schedule. if let Some(backoff_schedule) = &spec.backoff_schedule { @@ -263,7 +262,7 @@ struct Inner { #[async_trait(?Send)] impl CronHandle for CronExecutionHandle { - async fn next(&self, prev_success: bool) -> Result { + async fn next(&self, prev_success: bool) -> Result { self.inner.borrow_mut().permit.take(); if self @@ -300,7 +299,7 @@ impl CronHandle for CronExecutionHandle { } } -fn compute_next_deadline(cron_expression: &str) -> Result { +fn compute_next_deadline(cron_expression: &str) -> Result { let now = chrono::Utc::now(); if let Ok(test_schedule) = env::var("DENO_CRON_TEST_SCHEDULE_OFFSET") { @@ -311,19 +310,21 @@ fn compute_next_deadline(cron_expression: &str) -> Result { let cron = cron_expression .parse::() - .map_err(|_| anyhow::anyhow!("invalid cron expression"))?; + .map_err(|_| CronError::InvalidCron)?; let Some(next_deadline) = cron.next_after(now) else { - return Err(anyhow::anyhow!("invalid cron expression")); + return Err(CronError::InvalidCron); }; Ok(next_deadline.timestamp_millis() as u64) } -fn validate_backoff_schedule(backoff_schedule: &[u32]) -> Result<(), AnyError> { +fn validate_backoff_schedule( + backoff_schedule: &[u32], +) -> Result<(), CronError> { if backoff_schedule.len() > MAX_BACKOFF_COUNT { - return Err(type_error("Invalid backoff schedule")); + return Err(CronError::InvalidBackoff); } if backoff_schedule.iter().any(|s| *s > MAX_BACKOFF_MS) { - return Err(type_error("Invalid backoff schedule")); + return Err(CronError::InvalidBackoff); } Ok(()) } diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js index 8e43b76f7c..63b1905145 100644 --- a/ext/crypto/00_crypto.js +++ b/ext/crypto/00_crypto.js @@ -18,21 +18,27 @@ import { op_crypto_decrypt, op_crypto_derive_bits, op_crypto_derive_bits_x25519, + op_crypto_derive_bits_x448, op_crypto_encrypt, op_crypto_export_key, op_crypto_export_pkcs8_ed25519, op_crypto_export_pkcs8_x25519, + op_crypto_export_pkcs8_x448, op_crypto_export_spki_ed25519, op_crypto_export_spki_x25519, + op_crypto_export_spki_x448, op_crypto_generate_ed25519_keypair, op_crypto_generate_key, op_crypto_generate_x25519_keypair, + op_crypto_generate_x448_keypair, op_crypto_get_random_values, op_crypto_import_key, op_crypto_import_pkcs8_ed25519, op_crypto_import_pkcs8_x25519, + op_crypto_import_pkcs8_x448, op_crypto_import_spki_ed25519, op_crypto_import_spki_x25519, + op_crypto_import_spki_x448, op_crypto_jwk_x_ed25519, op_crypto_random_uuid, op_crypto_sign_ed25519, @@ -134,6 +140,7 @@ const supportedAlgorithms = { "AES-KW": "AesKeyGenParams", "HMAC": "HmacKeyGenParams", "X25519": null, + "X448": null, "Ed25519": null, }, "sign": { @@ -165,12 +172,14 @@ const supportedAlgorithms = { "AES-KW": null, "Ed25519": null, "X25519": null, + "X448": null, }, "deriveBits": { "HKDF": "HkdfParams", "PBKDF2": "Pbkdf2Params", "ECDH": "EcdhKeyDeriveParams", "X25519": "EcdhKeyDeriveParams", + "X448": "EcdhKeyDeriveParams", }, "encrypt": { "RSA-OAEP": "RsaOaepParams", @@ -1037,6 +1046,10 @@ class SubtleCrypto { result = exportKeyEd25519(format, key, innerKey); break; } + case "X448": { + result = exportKeyX448(format, key, innerKey); + break; + } case "X25519": { result = exportKeyX25519(format, key, innerKey); break; @@ -1954,6 +1967,48 @@ async function generateKey(normalizedAlgorithm, extractable, usages) { return generateKeyAES(normalizedAlgorithm, extractable, usages); } + case "X448": { + if ( + ArrayPrototypeFind( + usages, + (u) => !ArrayPrototypeIncludes(["deriveKey", "deriveBits"], u), + ) !== undefined + ) { + throw new DOMException("Invalid key usage", "SyntaxError"); + } + const privateKeyData = new Uint8Array(56); + const publicKeyData = new Uint8Array(56); + + op_crypto_generate_x448_keypair(privateKeyData, publicKeyData); + + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, privateKeyData); + + const publicHandle = {}; + WeakMapPrototypeSet(KEY_STORE, publicHandle, publicKeyData); + + const algorithm = { + name: algorithmName, + }; + + const publicKey = constructKey( + "public", + true, + usageIntersection(usages, []), + algorithm, + publicHandle, + ); + + const privateKey = constructKey( + "private", + extractable, + usageIntersection(usages, ["deriveKey", "deriveBits"]), + algorithm, + handle, + ); + + return { publicKey, privateKey }; + } case "X25519": { if ( ArrayPrototypeFind( @@ -2100,6 +2155,211 @@ async function generateKey(normalizedAlgorithm, extractable, usages) { } } +function importKeyX448( + format, + keyData, + extractable, + keyUsages, +) { + switch (format) { + case "raw": { + // 1. + if (keyUsages.length > 0) { + throw new DOMException("Invalid key usage", "SyntaxError"); + } + + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, keyData); + + // 2-3. + const algorithm = { + name: "X448", + }; + + // 4-6. + return constructKey( + "public", + extractable, + [], + algorithm, + handle, + ); + } + case "spki": { + // 1. + if (keyUsages.length > 0) { + throw new DOMException("Invalid key usage", "SyntaxError"); + } + + const publicKeyData = new Uint8Array(56); + if (!op_crypto_import_spki_x448(keyData, publicKeyData)) { + throw new DOMException("Invalid key data", "DataError"); + } + + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, publicKeyData); + + const algorithm = { + name: "X448", + }; + + return constructKey( + "public", + extractable, + [], + algorithm, + handle, + ); + } + case "pkcs8": { + // 1. + if ( + ArrayPrototypeFind( + keyUsages, + (u) => !ArrayPrototypeIncludes(["deriveKey", "deriveBits"], u), + ) !== undefined + ) { + throw new DOMException("Invalid key usage", "SyntaxError"); + } + + const privateKeyData = new Uint8Array(32); + if (!op_crypto_import_pkcs8_x448(keyData, privateKeyData)) { + throw new DOMException("Invalid key data", "DataError"); + } + + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, privateKeyData); + + const algorithm = { + name: "X448", + }; + + return constructKey( + "private", + extractable, + usageIntersection(keyUsages, recognisedUsages), + algorithm, + handle, + ); + } + case "jwk": { + // 1. + const jwk = keyData; + + // 2. + if (jwk.d !== undefined) { + if ( + ArrayPrototypeFind( + keyUsages, + (u) => + !ArrayPrototypeIncludes( + ["deriveKey", "deriveBits"], + u, + ), + ) !== undefined + ) { + throw new DOMException("Invalid key usage", "SyntaxError"); + } + } + + // 3. + if (jwk.d === undefined && keyUsages.length > 0) { + throw new DOMException("Invalid key usage", "SyntaxError"); + } + + // 4. + if (jwk.kty !== "OKP") { + throw new DOMException("Invalid key type", "DataError"); + } + + // 5. + if (jwk.crv !== "X448") { + throw new DOMException("Invalid curve", "DataError"); + } + + // 6. + if (keyUsages.length > 0 && jwk.use !== undefined) { + if (jwk.use !== "enc") { + throw new DOMException("Invalid key use", "DataError"); + } + } + + // 7. + if (jwk.key_ops !== undefined) { + if ( + ArrayPrototypeFind( + jwk.key_ops, + (u) => !ArrayPrototypeIncludes(recognisedUsages, u), + ) !== undefined + ) { + throw new DOMException( + "'key_ops' property of JsonWebKey is invalid", + "DataError", + ); + } + + if ( + !ArrayPrototypeEvery( + jwk.key_ops, + (u) => ArrayPrototypeIncludes(keyUsages, u), + ) + ) { + throw new DOMException( + "'key_ops' property of JsonWebKey is invalid", + "DataError", + ); + } + } + + // 8. + if (jwk.ext !== undefined && jwk.ext === false && extractable) { + throw new DOMException("Invalid key extractability", "DataError"); + } + + // 9. + if (jwk.d !== undefined) { + // https://www.rfc-editor.org/rfc/rfc8037#section-2 + const privateKeyData = op_crypto_base64url_decode(jwk.d); + + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, privateKeyData); + + const algorithm = { + name: "X448", + }; + + return constructKey( + "private", + extractable, + usageIntersection(keyUsages, ["deriveKey", "deriveBits"]), + algorithm, + handle, + ); + } else { + // https://www.rfc-editor.org/rfc/rfc8037#section-2 + const publicKeyData = op_crypto_base64url_decode(jwk.x); + + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, publicKeyData); + + const algorithm = { + name: "X448", + }; + + return constructKey( + "public", + extractable, + [], + algorithm, + handle, + ); + } + } + default: + throw new DOMException("Not implemented", "NotSupportedError"); + } +} + function importKeyEd25519( format, keyData, @@ -3358,6 +3618,14 @@ async function importKeyInner( ["wrapKey", "unwrapKey"], ); } + case "X448": { + return importKeyX448( + format, + keyData, + extractable, + keyUsages, + ); + } case "X25519": { return importKeyX25519( format, @@ -4162,6 +4430,66 @@ function exportKeyEd25519(format, key, innerKey) { } } +function exportKeyX448(format, key, innerKey) { + switch (format) { + case "raw": { + // 1. + if (key[_type] !== "public") { + throw new DOMException( + "Key is not a public key", + "InvalidAccessError", + ); + } + + // 2-3. + return TypedArrayPrototypeGetBuffer(innerKey); + } + case "spki": { + // 1. + if (key[_type] !== "public") { + throw new DOMException( + "Key is not a public key", + "InvalidAccessError", + ); + } + + const spkiDer = op_crypto_export_spki_x448(innerKey); + return TypedArrayPrototypeGetBuffer(spkiDer); + } + case "pkcs8": { + // 1. + if (key[_type] !== "private") { + throw new DOMException( + "Key is not a private key", + "InvalidAccessError", + ); + } + + const pkcs8Der = op_crypto_export_pkcs8_x448( + new Uint8Array([0x04, 0x22, ...new SafeArrayIterator(innerKey)]), + ); + pkcs8Der[15] = 0x20; + return TypedArrayPrototypeGetBuffer(pkcs8Der); + } + case "jwk": { + if (key[_type] === "private") { + throw new DOMException("Not implemented", "NotSupportedError"); + } + const x = op_crypto_base64url_encode(innerKey); + const jwk = { + kty: "OKP", + crv: "X448", + x, + "key_ops": key.usages, + ext: key[_extractable], + }; + return jwk; + } + default: + throw new DOMException("Not implemented", "NotSupportedError"); + } +} + function exportKeyX25519(format, key, innerKey) { switch (format) { case "raw": { @@ -4519,6 +4847,55 @@ async function deriveBits(normalizedAlgorithm, baseKey, length) { return TypedArrayPrototypeGetBuffer(buf); } + case "X448": { + // 1. + if (baseKey[_type] !== "private") { + throw new DOMException("Invalid key type", "InvalidAccessError"); + } + // 2. + const publicKey = normalizedAlgorithm.public; + // 3. + if (publicKey[_type] !== "public") { + throw new DOMException("Invalid key type", "InvalidAccessError"); + } + // 4. + if (publicKey[_algorithm].name !== baseKey[_algorithm].name) { + throw new DOMException( + "Algorithm mismatch", + "InvalidAccessError", + ); + } + + // 5. + const kHandle = baseKey[_handle]; + const k = WeakMapPrototypeGet(KEY_STORE, kHandle); + + const uHandle = publicKey[_handle]; + const u = WeakMapPrototypeGet(KEY_STORE, uHandle); + + const secret = new Uint8Array(56); + const isIdentity = op_crypto_derive_bits_x448(k, u, secret); + + // 6. + if (isIdentity) { + throw new DOMException("Invalid key", "OperationError"); + } + + // 7. + if (length === null) { + return TypedArrayPrototypeGetBuffer(secret); + } else if ( + TypedArrayPrototypeGetByteLength(secret) * 8 < length + ) { + throw new DOMException("Invalid length", "OperationError"); + } else { + return ArrayBufferPrototypeSlice( + TypedArrayPrototypeGetBuffer(secret), + 0, + MathCeil(length / 8), + ); + } + } case "X25519": { // 1. if (baseKey[_type] !== "private") { diff --git a/ext/crypto/Cargo.toml b/ext/crypto/Cargo.toml index 3f2a8d7030..a5794dc68b 100644 --- a/ext/crypto/Cargo.toml +++ b/ext/crypto/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_crypto" -version = "0.182.0" +version = "0.191.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -24,6 +24,7 @@ ctr = "0.9.1" curve25519-dalek = "4.1.3" deno_core.workspace = true deno_web.workspace = true +ed448-goldilocks = { version = "0.8.3", features = ["zeroize"] } elliptic-curve = { version = "0.13.1", features = ["std", "pem"] } num-traits = "0.2.14" once_cell.workspace = true @@ -40,5 +41,7 @@ sha1.workspace = true sha2.workspace = true signature.workspace = true spki.workspace = true +thiserror.workspace = true +tokio.workspace = true uuid.workspace = true x25519-dalek = "2.0.0" diff --git a/ext/crypto/decrypt.rs b/ext/crypto/decrypt.rs index 9b104e1784..1140475183 100644 --- a/ext/crypto/decrypt.rs +++ b/ext/crypto/decrypt.rs @@ -16,9 +16,6 @@ use ctr::cipher::StreamCipher; use ctr::Ctr128BE; use ctr::Ctr32BE; use ctr::Ctr64BE; -use deno_core::error::custom_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::unsync::spawn_blocking; use deno_core::JsBuffer; @@ -73,12 +70,36 @@ pub enum DecryptAlgorithm { }, } +#[derive(Debug, thiserror::Error)] +pub enum DecryptError { + #[error(transparent)] + General(#[from] SharedError), + #[error(transparent)] + Pkcs1(#[from] rsa::pkcs1::Error), + #[error("Decryption failed")] + Failed, + #[error("invalid length")] + InvalidLength, + #[error("invalid counter length. Currently supported 32/64/128 bits")] + InvalidCounterLength, + #[error("tag length not equal to 128")] + InvalidTagLength, + #[error("invalid key or iv")] + InvalidKeyOrIv, + #[error("tried to decrypt too much data")] + TooMuchData, + #[error("iv length not equal to 12 or 16")] + InvalidIvLength, + #[error("{0}")] + Rsa(rsa::Error), +} + #[op2(async)] #[serde] pub async fn op_crypto_decrypt( #[serde] opts: DecryptOptions, #[buffer] data: JsBuffer, -) -> Result { +) -> Result { let key = opts.key; let fun = move || match opts.algorithm { DecryptAlgorithm::RsaOaep { hash, label } => { @@ -108,7 +129,7 @@ fn decrypt_rsa_oaep( hash: ShaHash, label: Vec, data: &[u8], -) -> Result, deno_core::anyhow::Error> { +) -> Result, DecryptError> { let key = key.as_rsa_private_key()?; let private_key = rsa::RsaPrivateKey::from_pkcs1_der(key)?; @@ -139,7 +160,7 @@ fn decrypt_rsa_oaep( private_key .decrypt(padding, data) - .map_err(|e| custom_error("DOMExceptionOperationError", e.to_string())) + .map_err(DecryptError::Rsa) } fn decrypt_aes_cbc( @@ -147,7 +168,7 @@ fn decrypt_aes_cbc( length: usize, iv: Vec, data: &[u8], -) -> Result, deno_core::anyhow::Error> { +) -> Result, DecryptError> { let key = key.as_secret_key()?; // 2. @@ -155,53 +176,32 @@ fn decrypt_aes_cbc( 128 => { // Section 10.3 Step 2 of RFC 2315 https://www.rfc-editor.org/rfc/rfc2315 type Aes128CbcDec = cbc::Decryptor; - let cipher = Aes128CbcDec::new_from_slices(key, &iv).map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "Invalid key or iv".to_string(), - ) - })?; + let cipher = Aes128CbcDec::new_from_slices(key, &iv) + .map_err(|_| DecryptError::InvalidKeyOrIv)?; - cipher.decrypt_padded_vec_mut::(data).map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "Decryption failed".to_string(), - ) - })? + cipher + .decrypt_padded_vec_mut::(data) + .map_err(|_| DecryptError::Failed)? } 192 => { // Section 10.3 Step 2 of RFC 2315 https://www.rfc-editor.org/rfc/rfc2315 type Aes192CbcDec = cbc::Decryptor; - let cipher = Aes192CbcDec::new_from_slices(key, &iv).map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "Invalid key or iv".to_string(), - ) - })?; + let cipher = Aes192CbcDec::new_from_slices(key, &iv) + .map_err(|_| DecryptError::InvalidKeyOrIv)?; - cipher.decrypt_padded_vec_mut::(data).map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "Decryption failed".to_string(), - ) - })? + cipher + .decrypt_padded_vec_mut::(data) + .map_err(|_| DecryptError::Failed)? } 256 => { // Section 10.3 Step 2 of RFC 2315 https://www.rfc-editor.org/rfc/rfc2315 type Aes256CbcDec = cbc::Decryptor; - let cipher = Aes256CbcDec::new_from_slices(key, &iv).map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "Invalid key or iv".to_string(), - ) - })?; + let cipher = Aes256CbcDec::new_from_slices(key, &iv) + .map_err(|_| DecryptError::InvalidKeyOrIv)?; - cipher.decrypt_padded_vec_mut::(data).map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "Decryption failed".to_string(), - ) - })? + cipher + .decrypt_padded_vec_mut::(data) + .map_err(|_| DecryptError::Failed)? } _ => unreachable!(), }; @@ -214,7 +214,7 @@ fn decrypt_aes_ctr_gen( key: &[u8], counter: &[u8], data: &[u8], -) -> Result, AnyError> +) -> Result, DecryptError> where B: KeyIvInit + StreamCipher, { @@ -223,7 +223,7 @@ where let mut plaintext = data.to_vec(); cipher .try_apply_keystream(&mut plaintext) - .map_err(|_| operation_error("tried to decrypt too much data"))?; + .map_err(|_| DecryptError::TooMuchData)?; Ok(plaintext) } @@ -235,12 +235,12 @@ fn decrypt_aes_gcm_gen>( length: usize, additional_data: Vec, plaintext: &mut [u8], -) -> Result<(), AnyError> { +) -> Result<(), DecryptError> { let nonce = Nonce::from_slice(nonce); match length { 128 => { let cipher = aes_gcm::AesGcm::::new_from_slice(key) - .map_err(|_| operation_error("Decryption failed"))?; + .map_err(|_| DecryptError::Failed)?; cipher .decrypt_in_place_detached( nonce, @@ -248,11 +248,11 @@ fn decrypt_aes_gcm_gen>( plaintext, tag, ) - .map_err(|_| operation_error("Decryption failed"))? + .map_err(|_| DecryptError::Failed)? } 192 => { let cipher = aes_gcm::AesGcm::::new_from_slice(key) - .map_err(|_| operation_error("Decryption failed"))?; + .map_err(|_| DecryptError::Failed)?; cipher .decrypt_in_place_detached( nonce, @@ -260,11 +260,11 @@ fn decrypt_aes_gcm_gen>( plaintext, tag, ) - .map_err(|_| operation_error("Decryption failed"))? + .map_err(|_| DecryptError::Failed)? } 256 => { let cipher = aes_gcm::AesGcm::::new_from_slice(key) - .map_err(|_| operation_error("Decryption failed"))?; + .map_err(|_| DecryptError::Failed)?; cipher .decrypt_in_place_detached( nonce, @@ -272,9 +272,9 @@ fn decrypt_aes_gcm_gen>( plaintext, tag, ) - .map_err(|_| operation_error("Decryption failed"))? + .map_err(|_| DecryptError::Failed)? } - _ => return Err(type_error("invalid length")), + _ => return Err(DecryptError::InvalidLength), }; Ok(()) @@ -286,7 +286,7 @@ fn decrypt_aes_ctr( counter: &[u8], ctr_length: usize, data: &[u8], -) -> Result, deno_core::anyhow::Error> { +) -> Result, DecryptError> { let key = key.as_secret_key()?; match ctr_length { @@ -294,23 +294,21 @@ fn decrypt_aes_ctr( 128 => decrypt_aes_ctr_gen::>(key, counter, data), 192 => decrypt_aes_ctr_gen::>(key, counter, data), 256 => decrypt_aes_ctr_gen::>(key, counter, data), - _ => Err(type_error("invalid length")), + _ => Err(DecryptError::InvalidLength), }, 64 => match key_length { 128 => decrypt_aes_ctr_gen::>(key, counter, data), 192 => decrypt_aes_ctr_gen::>(key, counter, data), 256 => decrypt_aes_ctr_gen::>(key, counter, data), - _ => Err(type_error("invalid length")), + _ => Err(DecryptError::InvalidLength), }, 128 => match key_length { 128 => decrypt_aes_ctr_gen::>(key, counter, data), 192 => decrypt_aes_ctr_gen::>(key, counter, data), 256 => decrypt_aes_ctr_gen::>(key, counter, data), - _ => Err(type_error("invalid length")), + _ => Err(DecryptError::InvalidLength), }, - _ => Err(type_error( - "invalid counter length. Currently supported 32/64/128 bits", - )), + _ => Err(DecryptError::InvalidCounterLength), } } @@ -321,7 +319,7 @@ fn decrypt_aes_gcm( iv: Vec, additional_data: Option>, data: &[u8], -) -> Result, AnyError> { +) -> Result, DecryptError> { let key = key.as_secret_key()?; let additional_data = additional_data.unwrap_or_default(); @@ -330,7 +328,7 @@ fn decrypt_aes_gcm( // Note that encryption won't fail, it instead truncates the tag // to the specified tag length as specified in the spec. if tag_length != 128 { - return Err(type_error("tag length not equal to 128")); + return Err(DecryptError::InvalidTagLength); } let sep = data.len() - (tag_length / 8); @@ -357,7 +355,7 @@ fn decrypt_aes_gcm( additional_data, &mut plaintext, )?, - _ => return Err(type_error("iv length not equal to 12 or 16")), + _ => return Err(DecryptError::InvalidIvLength), } Ok(plaintext) diff --git a/ext/crypto/ed25519.rs b/ext/crypto/ed25519.rs index 4f604fe513..da34b7d25d 100644 --- a/ext/crypto/ed25519.rs +++ b/ext/crypto/ed25519.rs @@ -2,8 +2,6 @@ use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; -use deno_core::error::custom_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::ToJsBuffer; use elliptic_curve::pkcs8::PrivateKeyInfo; @@ -15,6 +13,16 @@ use spki::der::asn1::BitString; use spki::der::Decode; use spki::der::Encode; +#[derive(Debug, thiserror::Error)] +pub enum Ed25519Error { + #[error("Failed to export key")] + FailedExport, + #[error(transparent)] + Der(#[from] rsa::pkcs1::der::Error), + #[error(transparent)] + KeyRejected(#[from] ring::error::KeyRejected), +} + #[op2(fast)] pub fn op_crypto_generate_ed25519_keypair( #[buffer] pkey: &mut [u8], @@ -116,7 +124,7 @@ pub fn op_crypto_import_pkcs8_ed25519( #[serde] pub fn op_crypto_export_spki_ed25519( #[buffer] pubkey: &[u8], -) -> Result { +) -> Result { let key_info = spki::SubjectPublicKeyInfo { algorithm: spki::AlgorithmIdentifierOwned { // id-Ed25519 @@ -128,9 +136,7 @@ pub fn op_crypto_export_spki_ed25519( Ok( key_info .to_der() - .map_err(|_| { - custom_error("DOMExceptionOperationError", "Failed to export key") - })? + .map_err(|_| Ed25519Error::FailedExport)? .into(), ) } @@ -139,7 +145,7 @@ pub fn op_crypto_export_spki_ed25519( #[serde] pub fn op_crypto_export_pkcs8_ed25519( #[buffer] pkey: &[u8], -) -> Result { +) -> Result { use rsa::pkcs1::der::Encode; // This should probably use OneAsymmetricKey instead @@ -164,7 +170,7 @@ pub fn op_crypto_export_pkcs8_ed25519( #[string] pub fn op_crypto_jwk_x_ed25519( #[buffer] pkey: &[u8], -) -> Result { +) -> Result { let pair = Ed25519KeyPair::from_seed_unchecked(pkey)?; Ok(BASE64_URL_SAFE_NO_PAD.encode(pair.public_key().as_ref())) } diff --git a/ext/crypto/encrypt.rs b/ext/crypto/encrypt.rs index 204648e892..66b27657f8 100644 --- a/ext/crypto/encrypt.rs +++ b/ext/crypto/encrypt.rs @@ -16,8 +16,6 @@ use aes_gcm::Nonce; use ctr::Ctr128BE; use ctr::Ctr32BE; use ctr::Ctr64BE; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::unsync::spawn_blocking; use deno_core::JsBuffer; @@ -73,12 +71,30 @@ pub enum EncryptAlgorithm { }, } +#[derive(Debug, thiserror::Error)] +pub enum EncryptError { + #[error(transparent)] + General(#[from] SharedError), + #[error("invalid length")] + InvalidLength, + #[error("invalid key or iv")] + InvalidKeyOrIv, + #[error("iv length not equal to 12 or 16")] + InvalidIvLength, + #[error("invalid counter length. Currently supported 32/64/128 bits")] + InvalidCounterLength, + #[error("tried to encrypt too much data")] + TooMuchData, + #[error("Encryption failed")] + Failed, +} + #[op2(async)] #[serde] pub async fn op_crypto_encrypt( #[serde] opts: EncryptOptions, #[buffer] data: JsBuffer, -) -> Result { +) -> Result { let key = opts.key; let fun = move || match opts.algorithm { EncryptAlgorithm::RsaOaep { hash, label } => { @@ -108,12 +124,12 @@ fn encrypt_rsa_oaep( hash: ShaHash, label: Vec, data: &[u8], -) -> Result, AnyError> { +) -> Result, EncryptError> { let label = String::from_utf8_lossy(&label).to_string(); let public_key = key.as_rsa_public_key()?; let public_key = rsa::RsaPublicKey::from_pkcs1_der(&public_key) - .map_err(|_| operation_error("failed to decode public key"))?; + .map_err(|_| SharedError::FailedDecodePublicKey)?; let mut rng = OsRng; let padding = match hash { ShaHash::Sha1 => rsa::Oaep { @@ -139,7 +155,7 @@ fn encrypt_rsa_oaep( }; let encrypted = public_key .encrypt(&mut rng, padding, data) - .map_err(|_| operation_error("Encryption failed"))?; + .map_err(|_| EncryptError::Failed)?; Ok(encrypted) } @@ -148,7 +164,7 @@ fn encrypt_aes_cbc( length: usize, iv: Vec, data: &[u8], -) -> Result, AnyError> { +) -> Result, EncryptError> { let key = key.as_secret_key()?; let ciphertext = match length { 128 => { @@ -156,7 +172,7 @@ fn encrypt_aes_cbc( type Aes128CbcEnc = cbc::Encryptor; let cipher = Aes128CbcEnc::new_from_slices(key, &iv) - .map_err(|_| operation_error("invalid key or iv".to_string()))?; + .map_err(|_| EncryptError::InvalidKeyOrIv)?; cipher.encrypt_padded_vec_mut::(data) } 192 => { @@ -164,7 +180,7 @@ fn encrypt_aes_cbc( type Aes192CbcEnc = cbc::Encryptor; let cipher = Aes192CbcEnc::new_from_slices(key, &iv) - .map_err(|_| operation_error("invalid key or iv".to_string()))?; + .map_err(|_| EncryptError::InvalidKeyOrIv)?; cipher.encrypt_padded_vec_mut::(data) } 256 => { @@ -172,10 +188,10 @@ fn encrypt_aes_cbc( type Aes256CbcEnc = cbc::Encryptor; let cipher = Aes256CbcEnc::new_from_slices(key, &iv) - .map_err(|_| operation_error("invalid key or iv".to_string()))?; + .map_err(|_| EncryptError::InvalidKeyOrIv)?; cipher.encrypt_padded_vec_mut::(data) } - _ => return Err(type_error("invalid length")), + _ => return Err(EncryptError::InvalidLength), }; Ok(ciphertext) } @@ -186,31 +202,31 @@ fn encrypt_aes_gcm_general>( length: usize, ciphertext: &mut [u8], additional_data: Vec, -) -> Result { +) -> Result { let nonce = Nonce::::from_slice(&iv); let tag = match length { 128 => { let cipher = aes_gcm::AesGcm::::new_from_slice(key) - .map_err(|_| operation_error("Encryption failed"))?; + .map_err(|_| EncryptError::Failed)?; cipher .encrypt_in_place_detached(nonce, &additional_data, ciphertext) - .map_err(|_| operation_error("Encryption failed"))? + .map_err(|_| EncryptError::Failed)? } 192 => { let cipher = aes_gcm::AesGcm::::new_from_slice(key) - .map_err(|_| operation_error("Encryption failed"))?; + .map_err(|_| EncryptError::Failed)?; cipher .encrypt_in_place_detached(nonce, &additional_data, ciphertext) - .map_err(|_| operation_error("Encryption failed"))? + .map_err(|_| EncryptError::Failed)? } 256 => { let cipher = aes_gcm::AesGcm::::new_from_slice(key) - .map_err(|_| operation_error("Encryption failed"))?; + .map_err(|_| EncryptError::Failed)?; cipher .encrypt_in_place_detached(nonce, &additional_data, ciphertext) - .map_err(|_| operation_error("Encryption failed"))? + .map_err(|_| EncryptError::Failed)? } - _ => return Err(type_error("invalid length")), + _ => return Err(EncryptError::InvalidLength), }; Ok(tag) @@ -223,7 +239,7 @@ fn encrypt_aes_gcm( iv: Vec, additional_data: Option>, data: &[u8], -) -> Result, AnyError> { +) -> Result, EncryptError> { let key = key.as_secret_key()?; let additional_data = additional_data.unwrap_or_default(); @@ -244,7 +260,7 @@ fn encrypt_aes_gcm( &mut ciphertext, additional_data, )?, - _ => return Err(type_error("iv length not equal to 12 or 16")), + _ => return Err(EncryptError::InvalidIvLength), }; // Truncated tag to the specified tag length. @@ -261,7 +277,7 @@ fn encrypt_aes_ctr_gen( key: &[u8], counter: &[u8], data: &[u8], -) -> Result, AnyError> +) -> Result, EncryptError> where B: KeyIvInit + StreamCipher, { @@ -270,7 +286,7 @@ where let mut ciphertext = data.to_vec(); cipher .try_apply_keystream(&mut ciphertext) - .map_err(|_| operation_error("tried to encrypt too much data"))?; + .map_err(|_| EncryptError::TooMuchData)?; Ok(ciphertext) } @@ -281,7 +297,7 @@ fn encrypt_aes_ctr( counter: &[u8], ctr_length: usize, data: &[u8], -) -> Result, AnyError> { +) -> Result, EncryptError> { let key = key.as_secret_key()?; match ctr_length { @@ -289,22 +305,20 @@ fn encrypt_aes_ctr( 128 => encrypt_aes_ctr_gen::>(key, counter, data), 192 => encrypt_aes_ctr_gen::>(key, counter, data), 256 => encrypt_aes_ctr_gen::>(key, counter, data), - _ => Err(type_error("invalid length")), + _ => Err(EncryptError::InvalidLength), }, 64 => match key_length { 128 => encrypt_aes_ctr_gen::>(key, counter, data), 192 => encrypt_aes_ctr_gen::>(key, counter, data), 256 => encrypt_aes_ctr_gen::>(key, counter, data), - _ => Err(type_error("invalid length")), + _ => Err(EncryptError::InvalidLength), }, 128 => match key_length { 128 => encrypt_aes_ctr_gen::>(key, counter, data), 192 => encrypt_aes_ctr_gen::>(key, counter, data), 256 => encrypt_aes_ctr_gen::>(key, counter, data), - _ => Err(type_error("invalid length")), + _ => Err(EncryptError::InvalidLength), }, - _ => Err(type_error( - "invalid counter length. Currently supported 32/64/128 bits", - )), + _ => Err(EncryptError::InvalidCounterLength), } } diff --git a/ext/crypto/export_key.rs b/ext/crypto/export_key.rs index 00ce7e11c6..edf0d7239c 100644 --- a/ext/crypto/export_key.rs +++ b/ext/crypto/export_key.rs @@ -4,8 +4,6 @@ use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; use const_oid::AssociatedOid; use const_oid::ObjectIdentifier; -use deno_core::error::custom_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::ToJsBuffer; use elliptic_curve::sec1::ToEncodedPoint; @@ -22,6 +20,16 @@ use spki::AlgorithmIdentifierOwned; use crate::shared::*; +#[derive(Debug, thiserror::Error)] +pub enum ExportKeyError { + #[error(transparent)] + General(#[from] SharedError), + #[error(transparent)] + Der(#[from] spki::der::Error), + #[error("Unsupported named curve")] + UnsupportedNamedCurve, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExportKeyOptions { @@ -99,7 +107,7 @@ pub enum ExportKeyResult { pub fn op_crypto_export_key( #[serde] opts: ExportKeyOptions, #[serde] key_data: V8RawKeyData, -) -> Result { +) -> Result { match opts.algorithm { ExportKeyAlgorithm::RsassaPkcs1v15 {} | ExportKeyAlgorithm::RsaPss {} @@ -125,7 +133,7 @@ fn bytes_to_b64(bytes: &[u8]) -> String { fn export_key_rsa( format: ExportKeyFormat, key_data: V8RawKeyData, -) -> Result { +) -> Result { match format { ExportKeyFormat::Spki => { let subject_public_key = &key_data.as_rsa_public_key()?; @@ -181,12 +189,7 @@ fn export_key_rsa( ExportKeyFormat::JwkPublic => { let public_key = key_data.as_rsa_public_key()?; let public_key = rsa::pkcs1::RsaPublicKey::from_der(&public_key) - .map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "failed to decode public key", - ) - })?; + .map_err(|_| SharedError::FailedDecodePublicKey)?; Ok(ExportKeyResult::JwkPublicRsa { n: uint_to_b64(public_key.modulus), @@ -196,12 +199,7 @@ fn export_key_rsa( ExportKeyFormat::JwkPrivate => { let private_key = key_data.as_rsa_private_key()?; let private_key = rsa::pkcs1::RsaPrivateKey::from_der(private_key) - .map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "failed to decode private key", - ) - })?; + .map_err(|_| SharedError::FailedDecodePrivateKey)?; Ok(ExportKeyResult::JwkPrivateRsa { n: uint_to_b64(private_key.modulus), @@ -214,14 +212,14 @@ fn export_key_rsa( qi: uint_to_b64(private_key.coefficient), }) } - _ => Err(unsupported_format()), + _ => Err(SharedError::UnsupportedFormat.into()), } } fn export_key_symmetric( format: ExportKeyFormat, key_data: V8RawKeyData, -) -> Result { +) -> Result { match format { ExportKeyFormat::JwkSecret => { let bytes = key_data.as_secret_key()?; @@ -230,7 +228,7 @@ fn export_key_symmetric( k: bytes_to_b64(bytes), }) } - _ => Err(unsupported_format()), + _ => Err(SharedError::UnsupportedFormat.into()), } } @@ -239,7 +237,7 @@ fn export_key_ec( key_data: V8RawKeyData, algorithm: ExportKeyAlgorithm, named_curve: EcNamedCurve, -) -> Result { +) -> Result { match format { ExportKeyFormat::Raw => { let subject_public_key = match named_curve { @@ -332,10 +330,7 @@ fn export_key_ec( y: bytes_to_b64(y), }) } else { - Err(custom_error( - "DOMExceptionOperationError", - "failed to decode public key", - )) + Err(SharedError::FailedDecodePublicKey.into()) } } EcNamedCurve::P384 => { @@ -350,10 +345,7 @@ fn export_key_ec( y: bytes_to_b64(y), }) } else { - Err(custom_error( - "DOMExceptionOperationError", - "failed to decode public key", - )) + Err(SharedError::FailedDecodePublicKey.into()) } } EcNamedCurve::P521 => { @@ -368,10 +360,7 @@ fn export_key_ec( y: bytes_to_b64(y), }) } else { - Err(custom_error( - "DOMExceptionOperationError", - "failed to decode public key", - )) + Err(SharedError::FailedDecodePublicKey.into()) } } }, @@ -380,13 +369,8 @@ fn export_key_ec( match named_curve { EcNamedCurve::P256 => { - let ec_key = - p256::SecretKey::from_pkcs8_der(private_key).map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "failed to decode private key", - ) - })?; + let ec_key = p256::SecretKey::from_pkcs8_der(private_key) + .map_err(|_| SharedError::FailedDecodePrivateKey)?; let point = ec_key.public_key().to_encoded_point(false); if let elliptic_curve::sec1::Coordinates::Uncompressed { x, y } = @@ -398,18 +382,13 @@ fn export_key_ec( d: bytes_to_b64(&ec_key.to_bytes()), }) } else { - Err(data_error("expected valid public EC key")) + Err(SharedError::ExpectedValidPublicECKey.into()) } } EcNamedCurve::P384 => { - let ec_key = - p384::SecretKey::from_pkcs8_der(private_key).map_err(|_| { - custom_error( - "DOMExceptionOperationError", - "failed to decode private key", - ) - })?; + let ec_key = p384::SecretKey::from_pkcs8_der(private_key) + .map_err(|_| SharedError::FailedDecodePrivateKey)?; let point = ec_key.public_key().to_encoded_point(false); if let elliptic_curve::sec1::Coordinates::Uncompressed { x, y } = @@ -421,12 +400,12 @@ fn export_key_ec( d: bytes_to_b64(&ec_key.to_bytes()), }) } else { - Err(data_error("expected valid public EC key")) + Err(SharedError::ExpectedValidPublicECKey.into()) } } - _ => Err(not_supported_error("Unsupported namedCurve")), + _ => Err(ExportKeyError::UnsupportedNamedCurve), } } - ExportKeyFormat::JwkSecret => Err(unsupported_format()), + ExportKeyFormat::JwkSecret => Err(SharedError::UnsupportedFormat.into()), } } diff --git a/ext/crypto/generate_key.rs b/ext/crypto/generate_key.rs index 43aea2c705..3c0bd77c22 100644 --- a/ext/crypto/generate_key.rs +++ b/ext/crypto/generate_key.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use deno_core::unsync::spawn_blocking; use deno_core::ToJsBuffer; @@ -16,6 +15,26 @@ use serde::Deserialize; use crate::shared::*; +#[derive(Debug, thiserror::Error)] +pub enum GenerateKeyError { + #[error(transparent)] + General(#[from] SharedError), + #[error("Bad public exponent")] + BadPublicExponent, + #[error("Invalid HMAC key length")] + InvalidHMACKeyLength, + #[error("Failed to serialize RSA key")] + FailedRSAKeySerialization, + #[error("Invalid AES key length")] + InvalidAESKeyLength, + #[error("Failed to generate RSA key")] + FailedRSAKeyGeneration, + #[error("Failed to generate EC key")] + FailedECKeyGeneration, + #[error("Failed to generate key")] + FailedKeyGeneration, +} + // Allowlist for RSA public exponents. static PUB_EXPONENT_1: Lazy = Lazy::new(|| BigUint::from_u64(3).unwrap()); @@ -46,7 +65,7 @@ pub enum GenerateKeyOptions { #[serde] pub async fn op_crypto_generate_key( #[serde] opts: GenerateKeyOptions, -) -> Result { +) -> Result { let fun = || match opts { GenerateKeyOptions::Rsa { modulus_length, @@ -65,21 +84,21 @@ pub async fn op_crypto_generate_key( fn generate_key_rsa( modulus_length: u32, public_exponent: &[u8], -) -> Result, AnyError> { +) -> Result, GenerateKeyError> { let exponent = BigUint::from_bytes_be(public_exponent); if exponent != *PUB_EXPONENT_1 && exponent != *PUB_EXPONENT_2 { - return Err(operation_error("Bad public exponent")); + return Err(GenerateKeyError::BadPublicExponent); } let mut rng = OsRng; let private_key = RsaPrivateKey::new_with_exp(&mut rng, modulus_length as usize, &exponent) - .map_err(|_| operation_error("Failed to generate RSA key"))?; + .map_err(|_| GenerateKeyError::FailedRSAKeyGeneration)?; let private_key = private_key .to_pkcs1_der() - .map_err(|_| operation_error("Failed to serialize RSA key"))?; + .map_err(|_| GenerateKeyError::FailedRSAKeySerialization)?; Ok(private_key.as_bytes().to_vec()) } @@ -90,7 +109,9 @@ fn generate_key_ec_p521() -> Vec { key.to_nonzero_scalar().to_bytes().to_vec() } -fn generate_key_ec(named_curve: EcNamedCurve) -> Result, AnyError> { +fn generate_key_ec( + named_curve: EcNamedCurve, +) -> Result, GenerateKeyError> { let curve = match named_curve { EcNamedCurve::P256 => &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, EcNamedCurve::P384 => &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING, @@ -100,21 +121,21 @@ fn generate_key_ec(named_curve: EcNamedCurve) -> Result, AnyError> { let rng = ring::rand::SystemRandom::new(); let pkcs8 = EcdsaKeyPair::generate_pkcs8(curve, &rng) - .map_err(|_| operation_error("Failed to generate EC key"))?; + .map_err(|_| GenerateKeyError::FailedECKeyGeneration)?; Ok(pkcs8.as_ref().to_vec()) } -fn generate_key_aes(length: usize) -> Result, AnyError> { +fn generate_key_aes(length: usize) -> Result, GenerateKeyError> { if length % 8 != 0 || length > 256 { - return Err(operation_error("Invalid AES key length")); + return Err(GenerateKeyError::InvalidAESKeyLength); } let mut key = vec![0u8; length / 8]; let rng = ring::rand::SystemRandom::new(); rng .fill(&mut key) - .map_err(|_| operation_error("Failed to generate key"))?; + .map_err(|_| GenerateKeyError::FailedKeyGeneration)?; Ok(key) } @@ -122,7 +143,7 @@ fn generate_key_aes(length: usize) -> Result, AnyError> { fn generate_key_hmac( hash: ShaHash, length: Option, -) -> Result, AnyError> { +) -> Result, GenerateKeyError> { let hash = match hash { ShaHash::Sha1 => &ring::hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, ShaHash::Sha256 => &ring::hmac::HMAC_SHA256, @@ -132,12 +153,12 @@ fn generate_key_hmac( let length = if let Some(length) = length { if length % 8 != 0 { - return Err(operation_error("Invalid HMAC key length")); + return Err(GenerateKeyError::InvalidHMACKeyLength); } let length = length / 8; if length > ring::digest::MAX_BLOCK_LEN { - return Err(operation_error("Invalid HMAC key length")); + return Err(GenerateKeyError::InvalidHMACKeyLength); } length @@ -149,7 +170,7 @@ fn generate_key_hmac( let mut key = vec![0u8; length]; rng .fill(&mut key) - .map_err(|_| operation_error("Failed to generate key"))?; + .map_err(|_| GenerateKeyError::FailedKeyGeneration)?; Ok(key) } diff --git a/ext/crypto/import_key.rs b/ext/crypto/import_key.rs index e30baea03a..3463ca2beb 100644 --- a/ext/crypto/import_key.rs +++ b/ext/crypto/import_key.rs @@ -1,7 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use base64::Engine; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::JsBuffer; use deno_core::ToJsBuffer; @@ -15,6 +14,70 @@ use spki::der::Decode; use crate::shared::*; +#[derive(Debug, thiserror::Error)] +pub enum ImportKeyError { + #[error(transparent)] + General(#[from] SharedError), + #[error("invalid modulus")] + InvalidModulus, + #[error("invalid public exponent")] + InvalidPublicExponent, + #[error("invalid private exponent")] + InvalidPrivateExponent, + #[error("invalid first prime factor")] + InvalidFirstPrimeFactor, + #[error("invalid second prime factor")] + InvalidSecondPrimeFactor, + #[error("invalid first CRT exponent")] + InvalidFirstCRTExponent, + #[error("invalid second CRT exponent")] + InvalidSecondCRTExponent, + #[error("invalid CRT coefficient")] + InvalidCRTCoefficient, + #[error("invalid b64 coordinate")] + InvalidB64Coordinate, + #[error("invalid RSA public key")] + InvalidRSAPublicKey, + #[error("invalid RSA private key")] + InvalidRSAPrivateKey, + #[error("unsupported algorithm")] + UnsupportedAlgorithm, + #[error("public key is invalid (too long)")] + PublicKeyTooLong, + #[error("private key is invalid (too long)")] + PrivateKeyTooLong, + #[error("invalid P-256 elliptic curve point")] + InvalidP256ECPoint, + #[error("invalid P-384 elliptic curve point")] + InvalidP384ECPoint, + #[error("invalid P-521 elliptic curve point")] + InvalidP521ECPoint, + #[error("invalid P-256 elliptic curve SPKI data")] + InvalidP256ECSPKIData, + #[error("invalid P-384 elliptic curve SPKI data")] + InvalidP384ECSPKIData, + #[error("invalid P-521 elliptic curve SPKI data")] + InvalidP521ECSPKIData, + #[error("curve mismatch")] + CurveMismatch, + #[error("Unsupported named curve")] + UnsupportedNamedCurve, + #[error("invalid key data")] + InvalidKeyData, + #[error("invalid JWK private key")] + InvalidJWKPrivateKey, + #[error(transparent)] + EllipticCurve(#[from] elliptic_curve::Error), + #[error("expected valid PKCS#8 data")] + ExpectedValidPkcs8Data, + #[error("malformed parameters")] + MalformedParameters, + #[error(transparent)] + Spki(#[from] spki::Error), + #[error(transparent)] + Der(#[from] rsa::pkcs1::der::Error), +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub enum KeyData { @@ -93,7 +156,7 @@ pub enum ImportKeyResult { pub fn op_crypto_import_key( #[serde] opts: ImportKeyOptions, #[serde] key_data: KeyData, -) -> Result { +) -> Result { match opts { ImportKeyOptions::RsassaPkcs1v15 {} => import_key_rsassa(key_data), ImportKeyOptions::RsaPss {} => import_key_rsapss(key_data), @@ -117,21 +180,21 @@ const BASE64_URL_SAFE_FORGIVING: ); macro_rules! jwt_b64_int_or_err { - ($name:ident, $b64:expr, $err:expr) => { + ($name:ident, $b64:expr, $err:tt) => { let bytes = BASE64_URL_SAFE_FORGIVING .decode($b64) - .map_err(|_| data_error($err))?; - let $name = UintRef::new(&bytes).map_err(|_| data_error($err))?; + .map_err(|_| ImportKeyError::$err)?; + let $name = UintRef::new(&bytes).map_err(|_| ImportKeyError::$err)?; }; } fn import_key_rsa_jwk( key_data: KeyData, -) -> Result { +) -> Result { match key_data { KeyData::JwkPublicRsa { n, e } => { - jwt_b64_int_or_err!(modulus, &n, "invalid modulus"); - jwt_b64_int_or_err!(public_exponent, &e, "invalid public exponent"); + jwt_b64_int_or_err!(modulus, &n, InvalidModulus); + jwt_b64_int_or_err!(public_exponent, &e, InvalidPublicExponent); let public_key = rsa::pkcs1::RsaPublicKey { modulus, @@ -141,7 +204,7 @@ fn import_key_rsa_jwk( let mut data = Vec::new(); public_key .encode_to_vec(&mut data) - .map_err(|_| data_error("invalid rsa public key"))?; + .map_err(|_| ImportKeyError::InvalidRSAPublicKey)?; let public_exponent = public_key.public_exponent.as_bytes().to_vec().into(); @@ -163,14 +226,14 @@ fn import_key_rsa_jwk( dq, qi, } => { - jwt_b64_int_or_err!(modulus, &n, "invalid modulus"); - jwt_b64_int_or_err!(public_exponent, &e, "invalid public exponent"); - jwt_b64_int_or_err!(private_exponent, &d, "invalid private exponent"); - jwt_b64_int_or_err!(prime1, &p, "invalid first prime factor"); - jwt_b64_int_or_err!(prime2, &q, "invalid second prime factor"); - jwt_b64_int_or_err!(exponent1, &dp, "invalid first CRT exponent"); - jwt_b64_int_or_err!(exponent2, &dq, "invalid second CRT exponent"); - jwt_b64_int_or_err!(coefficient, &qi, "invalid CRT coefficient"); + jwt_b64_int_or_err!(modulus, &n, InvalidModulus); + jwt_b64_int_or_err!(public_exponent, &e, InvalidPublicExponent); + jwt_b64_int_or_err!(private_exponent, &d, InvalidPrivateExponent); + jwt_b64_int_or_err!(prime1, &p, InvalidFirstPrimeFactor); + jwt_b64_int_or_err!(prime2, &q, InvalidSecondPrimeFactor); + jwt_b64_int_or_err!(exponent1, &dp, InvalidFirstCRTExponent); + jwt_b64_int_or_err!(exponent2, &dq, InvalidSecondCRTExponent); + jwt_b64_int_or_err!(coefficient, &qi, InvalidCRTCoefficient); let private_key = rsa::pkcs1::RsaPrivateKey { modulus, @@ -187,7 +250,7 @@ fn import_key_rsa_jwk( let mut data = Vec::new(); private_key .encode_to_vec(&mut data) - .map_err(|_| data_error("invalid rsa private key"))?; + .map_err(|_| ImportKeyError::InvalidRSAPrivateKey)?; let public_exponent = private_key.public_exponent.as_bytes().to_vec().into(); @@ -205,37 +268,33 @@ fn import_key_rsa_jwk( fn import_key_rsassa( key_data: KeyData, -) -> Result { +) -> Result { match key_data { KeyData::Spki(data) => { // 2-3. - let pk_info = spki::SubjectPublicKeyInfoRef::try_from(&*data) - .map_err(|e| data_error(e.to_string()))?; + let pk_info = spki::SubjectPublicKeyInfoRef::try_from(&*data)?; // 4-5. let alg = pk_info.algorithm.oid; // 6-7. (skipped, only support rsaEncryption for interoperability) if alg != RSA_ENCRYPTION_OID { - return Err(data_error("unsupported algorithm")); + return Err(ImportKeyError::UnsupportedAlgorithm); } // 8-9. let public_key = rsa::pkcs1::RsaPublicKey::from_der( pk_info.subject_public_key.raw_bytes(), - ) - .map_err(|e| data_error(e.to_string()))?; + )?; - let bytes_consumed = public_key - .encoded_len() - .map_err(|e| data_error(e.to_string()))?; + let bytes_consumed = public_key.encoded_len()?; if bytes_consumed != rsa::pkcs1::der::Length::new( pk_info.subject_public_key.raw_bytes().len() as u16, ) { - return Err(data_error("public key is invalid (too long)")); + return Err(ImportKeyError::PublicKeyTooLong); } let data = pk_info.subject_public_key.raw_bytes().to_vec().into(); @@ -251,30 +310,26 @@ fn import_key_rsassa( } KeyData::Pkcs8(data) => { // 2-3. - let pk_info = PrivateKeyInfo::from_der(&data) - .map_err(|e| data_error(e.to_string()))?; + let pk_info = PrivateKeyInfo::from_der(&data)?; // 4-5. let alg = pk_info.algorithm.oid; // 6-7. (skipped, only support rsaEncryption for interoperability) if alg != RSA_ENCRYPTION_OID { - return Err(data_error("unsupported algorithm")); + return Err(ImportKeyError::UnsupportedAlgorithm); } // 8-9. let private_key = - rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key) - .map_err(|e| data_error(e.to_string()))?; + rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key)?; - let bytes_consumed = private_key - .encoded_len() - .map_err(|e| data_error(e.to_string()))?; + let bytes_consumed = private_key.encoded_len()?; if bytes_consumed != rsa::pkcs1::der::Length::new(pk_info.private_key.len() as u16) { - return Err(data_error("private key is invalid (too long)")); + return Err(ImportKeyError::PrivateKeyTooLong); } let data = pk_info.private_key.to_vec().into(); @@ -291,43 +346,39 @@ fn import_key_rsassa( KeyData::JwkPublicRsa { .. } | KeyData::JwkPrivateRsa { .. } => { import_key_rsa_jwk(key_data) } - _ => Err(unsupported_format()), + _ => Err(SharedError::UnsupportedFormat.into()), } } fn import_key_rsapss( key_data: KeyData, -) -> Result { +) -> Result { match key_data { KeyData::Spki(data) => { // 2-3. - let pk_info = spki::SubjectPublicKeyInfoRef::try_from(&*data) - .map_err(|e| data_error(e.to_string()))?; + let pk_info = spki::SubjectPublicKeyInfoRef::try_from(&*data)?; // 4-5. let alg = pk_info.algorithm.oid; // 6-7. (skipped, only support rsaEncryption for interoperability) if alg != RSA_ENCRYPTION_OID { - return Err(data_error("unsupported algorithm")); + return Err(ImportKeyError::UnsupportedAlgorithm); } // 8-9. let public_key = rsa::pkcs1::RsaPublicKey::from_der( pk_info.subject_public_key.raw_bytes(), - ) - .map_err(|e| data_error(e.to_string()))?; + )?; - let bytes_consumed = public_key - .encoded_len() - .map_err(|e| data_error(e.to_string()))?; + let bytes_consumed = public_key.encoded_len()?; if bytes_consumed != rsa::pkcs1::der::Length::new( pk_info.subject_public_key.raw_bytes().len() as u16, ) { - return Err(data_error("public key is invalid (too long)")); + return Err(ImportKeyError::PublicKeyTooLong); } let data = pk_info.subject_public_key.raw_bytes().to_vec().into(); @@ -343,30 +394,26 @@ fn import_key_rsapss( } KeyData::Pkcs8(data) => { // 2-3. - let pk_info = PrivateKeyInfo::from_der(&data) - .map_err(|e| data_error(e.to_string()))?; + let pk_info = PrivateKeyInfo::from_der(&data)?; // 4-5. let alg = pk_info.algorithm.oid; // 6-7. (skipped, only support rsaEncryption for interoperability) if alg != RSA_ENCRYPTION_OID { - return Err(data_error("unsupported algorithm")); + return Err(ImportKeyError::UnsupportedAlgorithm); } // 8-9. let private_key = - rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key) - .map_err(|e| data_error(e.to_string()))?; + rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key)?; - let bytes_consumed = private_key - .encoded_len() - .map_err(|e| data_error(e.to_string()))?; + let bytes_consumed = private_key.encoded_len()?; if bytes_consumed != rsa::pkcs1::der::Length::new(pk_info.private_key.len() as u16) { - return Err(data_error("private key is invalid (too long)")); + return Err(ImportKeyError::PrivateKeyTooLong); } let data = pk_info.private_key.to_vec().into(); @@ -383,43 +430,39 @@ fn import_key_rsapss( KeyData::JwkPublicRsa { .. } | KeyData::JwkPrivateRsa { .. } => { import_key_rsa_jwk(key_data) } - _ => Err(unsupported_format()), + _ => Err(SharedError::UnsupportedFormat.into()), } } fn import_key_rsaoaep( key_data: KeyData, -) -> Result { +) -> Result { match key_data { KeyData::Spki(data) => { // 2-3. - let pk_info = spki::SubjectPublicKeyInfoRef::try_from(&*data) - .map_err(|e| data_error(e.to_string()))?; + let pk_info = spki::SubjectPublicKeyInfoRef::try_from(&*data)?; // 4-5. let alg = pk_info.algorithm.oid; // 6-7. (skipped, only support rsaEncryption for interoperability) if alg != RSA_ENCRYPTION_OID { - return Err(data_error("unsupported algorithm")); + return Err(ImportKeyError::UnsupportedAlgorithm); } // 8-9. let public_key = rsa::pkcs1::RsaPublicKey::from_der( pk_info.subject_public_key.raw_bytes(), - ) - .map_err(|e| data_error(e.to_string()))?; + )?; - let bytes_consumed = public_key - .encoded_len() - .map_err(|e| data_error(e.to_string()))?; + let bytes_consumed = public_key.encoded_len()?; if bytes_consumed != rsa::pkcs1::der::Length::new( pk_info.subject_public_key.raw_bytes().len() as u16, ) { - return Err(data_error("public key is invalid (too long)")); + return Err(ImportKeyError::PublicKeyTooLong); } let data = pk_info.subject_public_key.raw_bytes().to_vec().into(); @@ -435,30 +478,26 @@ fn import_key_rsaoaep( } KeyData::Pkcs8(data) => { // 2-3. - let pk_info = PrivateKeyInfo::from_der(&data) - .map_err(|e| data_error(e.to_string()))?; + let pk_info = PrivateKeyInfo::from_der(&data)?; // 4-5. let alg = pk_info.algorithm.oid; // 6-7. (skipped, only support rsaEncryption for interoperability) if alg != RSA_ENCRYPTION_OID { - return Err(data_error("unsupported algorithm")); + return Err(ImportKeyError::UnsupportedAlgorithm); } // 8-9. let private_key = - rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key) - .map_err(|e| data_error(e.to_string()))?; + rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key)?; - let bytes_consumed = private_key - .encoded_len() - .map_err(|e| data_error(e.to_string()))?; + let bytes_consumed = private_key.encoded_len()?; if bytes_consumed != rsa::pkcs1::der::Length::new(pk_info.private_key.len() as u16) { - return Err(data_error("private key is invalid (too long)")); + return Err(ImportKeyError::PrivateKeyTooLong); } let data = pk_info.private_key.to_vec().into(); @@ -475,14 +514,14 @@ fn import_key_rsaoaep( KeyData::JwkPublicRsa { .. } | KeyData::JwkPrivateRsa { .. } => { import_key_rsa_jwk(key_data) } - _ => Err(unsupported_format()), + _ => Err(SharedError::UnsupportedFormat.into()), } } fn decode_b64url_to_field_bytes( b64: &str, -) -> Result, deno_core::anyhow::Error> { - jwt_b64_int_or_err!(val, b64, "invalid b64 coordinate"); +) -> Result, ImportKeyError> { + jwt_b64_int_or_err!(val, b64, InvalidB64Coordinate); let mut bytes = elliptic_curve::FieldBytes::::default(); let original_bytes = val.as_bytes(); @@ -495,7 +534,7 @@ fn decode_b64url_to_field_bytes( let val = new_bytes.as_slice(); if val.len() != bytes.len() { - return Err(data_error("invalid b64 coordinate")); + return Err(ImportKeyError::InvalidB64Coordinate); } bytes.copy_from_slice(val); @@ -506,7 +545,7 @@ fn import_key_ec_jwk_to_point( x: String, y: String, named_curve: EcNamedCurve, -) -> Result, deno_core::anyhow::Error> { +) -> Result, ImportKeyError> { let point_bytes = match named_curve { EcNamedCurve::P256 => { let x = decode_b64url_to_field_bytes::(&x)?; @@ -534,7 +573,7 @@ fn import_key_ec_jwk_to_point( fn import_key_ec_jwk( key_data: KeyData, named_curve: EcNamedCurve, -) -> Result { +) -> Result { match key_data { KeyData::JwkPublicEc { x, y } => { let point_bytes = import_key_ec_jwk_to_point(x, y, named_curve)?; @@ -550,21 +589,21 @@ fn import_key_ec_jwk( let pk = p256::SecretKey::from_bytes(&d)?; pk.to_pkcs8_der() - .map_err(|_| data_error("invalid JWK private key"))? + .map_err(|_| ImportKeyError::InvalidJWKPrivateKey)? } EcNamedCurve::P384 => { let d = decode_b64url_to_field_bytes::(&d)?; let pk = p384::SecretKey::from_bytes(&d)?; pk.to_pkcs8_der() - .map_err(|_| data_error("invalid JWK private key"))? + .map_err(|_| ImportKeyError::InvalidJWKPrivateKey)? } EcNamedCurve::P521 => { let d = decode_b64url_to_field_bytes::(&d)?; let pk = p521::SecretKey::from_bytes(&d)?; pk.to_pkcs8_der() - .map_err(|_| data_error("invalid JWK private key"))? + .map_err(|_| ImportKeyError::InvalidJWKPrivateKey)? } }; @@ -595,7 +634,7 @@ impl<'a> TryFrom> for ECParametersSpki { fn import_key_ec( key_data: KeyData, named_curve: EcNamedCurve, -) -> Result { +) -> Result { match key_data { KeyData::Raw(data) => { // The point is parsed and validated, ultimately the original data is @@ -604,28 +643,28 @@ fn import_key_ec( EcNamedCurve::P256 => { // 1-2. let point = p256::EncodedPoint::from_bytes(&data) - .map_err(|_| data_error("invalid P-256 elliptic curve point"))?; + .map_err(|_| ImportKeyError::InvalidP256ECPoint)?; // 3. if point.is_identity() { - return Err(data_error("invalid P-256 elliptic curve point")); + return Err(ImportKeyError::InvalidP256ECPoint); } } EcNamedCurve::P384 => { // 1-2. let point = p384::EncodedPoint::from_bytes(&data) - .map_err(|_| data_error("invalid P-384 elliptic curve point"))?; + .map_err(|_| ImportKeyError::InvalidP384ECPoint)?; // 3. if point.is_identity() { - return Err(data_error("invalid P-384 elliptic curve point")); + return Err(ImportKeyError::InvalidP384ECPoint); } } EcNamedCurve::P521 => { // 1-2. let point = p521::EncodedPoint::from_bytes(&data) - .map_err(|_| data_error("invalid P-521 elliptic curve point"))?; + .map_err(|_| ImportKeyError::InvalidP521ECPoint)?; // 3. if point.is_identity() { - return Err(data_error("invalid P-521 elliptic curve point")); + return Err(ImportKeyError::InvalidP521ECPoint); } } }; @@ -635,11 +674,11 @@ fn import_key_ec( } KeyData::Pkcs8(data) => { let pk = PrivateKeyInfo::from_der(data.as_ref()) - .map_err(|_| data_error("expected valid PKCS#8 data"))?; + .map_err(|_| ImportKeyError::ExpectedValidPkcs8Data)?; let named_curve_alg = pk .algorithm .parameters - .ok_or_else(|| data_error("malformed parameters"))? + .ok_or(ImportKeyError::MalformedParameters)? .try_into() .unwrap(); @@ -654,7 +693,7 @@ fn import_key_ec( }; if pk_named_curve != Some(named_curve) { - return Err(data_error("curve mismatch")); + return Err(ImportKeyError::CurveMismatch); } Ok(ImportKeyResult::Ec { @@ -663,14 +702,13 @@ fn import_key_ec( } KeyData::Spki(data) => { // 2-3. - let pk_info = spki::SubjectPublicKeyInfoRef::try_from(&*data) - .map_err(|e| data_error(e.to_string()))?; + let pk_info = spki::SubjectPublicKeyInfoRef::try_from(&*data)?; // 4. let alg = pk_info.algorithm.oid; // id-ecPublicKey if alg != elliptic_curve::ALGORITHM_OID { - return Err(data_error("unsupported algorithm")); + return Err(ImportKeyError::UnsupportedAlgorithm); } // 5-7. @@ -678,9 +716,9 @@ fn import_key_ec( pk_info .algorithm .parameters - .ok_or_else(|| data_error("malformed parameters"))?, + .ok_or(ImportKeyError::MalformedParameters)?, ) - .map_err(|_| data_error("malformed parameters"))?; + .map_err(|_| ImportKeyError::MalformedParameters)?; // 8-9. let named_curve_alg = params.named_curve_alg; @@ -704,36 +742,30 @@ fn import_key_ec( let bytes_consumed = match named_curve { EcNamedCurve::P256 => { - let point = - p256::EncodedPoint::from_bytes(&*encoded_key).map_err(|_| { - data_error("invalid P-256 elliptic curve SPKI data") - })?; + let point = p256::EncodedPoint::from_bytes(&*encoded_key) + .map_err(|_| ImportKeyError::InvalidP256ECSPKIData)?; if point.is_identity() { - return Err(data_error("invalid P-256 elliptic curve point")); + return Err(ImportKeyError::InvalidP256ECPoint); } point.as_bytes().len() } EcNamedCurve::P384 => { - let point = - p384::EncodedPoint::from_bytes(&*encoded_key).map_err(|_| { - data_error("invalid P-384 elliptic curve SPKI data") - })?; + let point = p384::EncodedPoint::from_bytes(&*encoded_key) + .map_err(|_| ImportKeyError::InvalidP384ECSPKIData)?; if point.is_identity() { - return Err(data_error("invalid P-384 elliptic curve point")); + return Err(ImportKeyError::InvalidP384ECPoint); } point.as_bytes().len() } EcNamedCurve::P521 => { - let point = - p521::EncodedPoint::from_bytes(&*encoded_key).map_err(|_| { - data_error("invalid P-521 elliptic curve SPKI data") - })?; + let point = p521::EncodedPoint::from_bytes(&*encoded_key) + .map_err(|_| ImportKeyError::InvalidP521ECSPKIData)?; if point.is_identity() { - return Err(data_error("invalid P-521 elliptic curve point")); + return Err(ImportKeyError::InvalidP521ECPoint); } point.as_bytes().len() @@ -741,15 +773,15 @@ fn import_key_ec( }; if bytes_consumed != pk_info.subject_public_key.raw_bytes().len() { - return Err(data_error("public key is invalid (too long)")); + return Err(ImportKeyError::PublicKeyTooLong); } // 11. if named_curve != pk_named_curve { - return Err(data_error("curve mismatch")); + return Err(ImportKeyError::CurveMismatch); } } else { - return Err(data_error("Unsupported named curve")); + return Err(ImportKeyError::UnsupportedNamedCurve); } Ok(ImportKeyResult::Ec { @@ -759,34 +791,38 @@ fn import_key_ec( KeyData::JwkPublicEc { .. } | KeyData::JwkPrivateEc { .. } => { import_key_ec_jwk(key_data, named_curve) } - _ => Err(unsupported_format()), + _ => Err(SharedError::UnsupportedFormat.into()), } } -fn import_key_aes(key_data: KeyData) -> Result { +fn import_key_aes( + key_data: KeyData, +) -> Result { Ok(match key_data { KeyData::JwkSecret { k } => { let data = BASE64_URL_SAFE_FORGIVING .decode(k) - .map_err(|_| data_error("invalid key data"))?; + .map_err(|_| ImportKeyError::InvalidKeyData)?; ImportKeyResult::Hmac { raw_data: RustRawKeyData::Secret(data.into()), } } - _ => return Err(unsupported_format()), + _ => return Err(SharedError::UnsupportedFormat.into()), }) } -fn import_key_hmac(key_data: KeyData) -> Result { +fn import_key_hmac( + key_data: KeyData, +) -> Result { Ok(match key_data { KeyData::JwkSecret { k } => { let data = BASE64_URL_SAFE_FORGIVING .decode(k) - .map_err(|_| data_error("invalid key data"))?; + .map_err(|_| ImportKeyError::InvalidKeyData)?; ImportKeyResult::Hmac { raw_data: RustRawKeyData::Secret(data.into()), } } - _ => return Err(unsupported_format()), + _ => return Err(SharedError::UnsupportedFormat.into()), }) } diff --git a/ext/crypto/lib.rs b/ext/crypto/lib.rs index 7aa3462c76..69dcd1413a 100644 --- a/ext/crypto/lib.rs +++ b/ext/crypto/lib.rs @@ -6,10 +6,7 @@ use aes_kw::KekAes256; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; -use deno_core::error::custom_error; use deno_core::error::not_supported; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::ToJsBuffer; @@ -17,7 +14,6 @@ use deno_core::unsync::spawn_blocking; use deno_core::JsBuffer; use deno_core::OpState; use serde::Deserialize; -use shared::operation_error; use p256::elliptic_curve::sec1::FromEncodedPoint; use p256::pkcs8::DecodePrivateKey; @@ -64,17 +60,27 @@ mod import_key; mod key; mod shared; mod x25519; +mod x448; pub use crate::decrypt::op_crypto_decrypt; +pub use crate::decrypt::DecryptError; +pub use crate::ed25519::Ed25519Error; pub use crate::encrypt::op_crypto_encrypt; +pub use crate::encrypt::EncryptError; pub use crate::export_key::op_crypto_export_key; +pub use crate::export_key::ExportKeyError; pub use crate::generate_key::op_crypto_generate_key; +pub use crate::generate_key::GenerateKeyError; pub use crate::import_key::op_crypto_import_key; +pub use crate::import_key::ImportKeyError; use crate::key::Algorithm; use crate::key::CryptoHash; use crate::key::CryptoNamedCurve; use crate::key::HkdfOutput; +pub use crate::shared::SharedError; use crate::shared::V8RawKeyData; +pub use crate::x25519::X25519Error; +pub use crate::x448::X448Error; deno_core::extension!(deno_crypto, deps = [ deno_webidl, deno_web ], @@ -98,6 +104,14 @@ deno_core::extension!(deno_crypto, x25519::op_crypto_derive_bits_x25519, x25519::op_crypto_import_spki_x25519, x25519::op_crypto_import_pkcs8_x25519, + x25519::op_crypto_export_spki_x25519, + x25519::op_crypto_export_pkcs8_x25519, + x448::op_crypto_generate_x448_keypair, + x448::op_crypto_derive_bits_x448, + x448::op_crypto_import_spki_x448, + x448::op_crypto_import_pkcs8_x448, + x448::op_crypto_export_spki_x448, + x448::op_crypto_export_pkcs8_x448, ed25519::op_crypto_generate_ed25519_keypair, ed25519::op_crypto_import_spki_ed25519, ed25519::op_crypto_import_pkcs8_ed25519, @@ -106,8 +120,6 @@ deno_core::extension!(deno_crypto, ed25519::op_crypto_export_spki_ed25519, ed25519::op_crypto_export_pkcs8_ed25519, ed25519::op_crypto_jwk_x_ed25519, - x25519::op_crypto_export_spki_x25519, - x25519::op_crypto_export_pkcs8_x25519, ], esm = [ "00_crypto.js" ], options = { @@ -120,11 +132,63 @@ deno_core::extension!(deno_crypto, }, ); +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + General(#[from] SharedError), + #[error(transparent)] + JoinError(#[from] tokio::task::JoinError), + #[error(transparent)] + Der(#[from] rsa::pkcs1::der::Error), + #[error("Missing argument hash")] + MissingArgumentHash, + #[error("Missing argument saltLength")] + MissingArgumentSaltLength, + #[error("unsupported algorithm")] + UnsupportedAlgorithm, + #[error(transparent)] + KeyRejected(#[from] ring::error::KeyRejected), + #[error(transparent)] + RSA(#[from] rsa::Error), + #[error(transparent)] + Pkcs1(#[from] rsa::pkcs1::Error), + #[error(transparent)] + Unspecified(#[from] ring::error::Unspecified), + #[error("Invalid key format")] + InvalidKeyFormat, + #[error(transparent)] + P256Ecdsa(#[from] p256::ecdsa::Error), + #[error("Unexpected error decoding private key")] + DecodePrivateKey, + #[error("Missing argument publicKey")] + MissingArgumentPublicKey, + #[error("Missing argument namedCurve")] + MissingArgumentNamedCurve, + #[error("Missing argument info")] + MissingArgumentInfo, + #[error("The length provided for HKDF is too large")] + HKDFLengthTooLarge, + #[error(transparent)] + Base64Decode(#[from] base64::DecodeError), + #[error("Data must be multiple of 8 bytes")] + DataInvalidSize, + #[error("Invalid key length")] + InvalidKeyLength, + #[error("encryption error")] + EncryptionError, + #[error("decryption error - integrity check failed")] + DecryptionError, + #[error("The ArrayBufferView's byte length ({0}) exceeds the number of bytes of entropy available via this API (65536)")] + ArrayBufferViewLengthExceeded(usize), + #[error(transparent)] + Other(deno_core::error::AnyError), +} + #[op2] #[serde] pub fn op_crypto_base64url_decode( #[string] data: String, -) -> Result { +) -> Result { let data: Vec = BASE64_URL_SAFE_NO_PAD.decode(data)?; Ok(data.into()) } @@ -140,12 +204,9 @@ pub fn op_crypto_base64url_encode(#[buffer] data: JsBuffer) -> String { pub fn op_crypto_get_random_values( state: &mut OpState, #[buffer] out: &mut [u8], -) -> Result<(), AnyError> { +) -> Result<(), Error> { if out.len() > 65536 { - return Err( - deno_web::DomExceptionQuotaExceededError::new(&format!("The ArrayBufferView's byte length ({}) exceeds the number of bytes of entropy available via this API (65536)", out.len())) - .into(), - ); + return Err(Error::ArrayBufferViewLengthExceeded(out.len())); } let maybe_seeded_rng = state.try_borrow_mut::(); @@ -197,7 +258,7 @@ pub struct SignArg { pub async fn op_crypto_sign_key( #[serde] args: SignArg, #[buffer] zero_copy: JsBuffer, -) -> Result { +) -> Result { deno_core::unsync::spawn_blocking(move || { let data = &*zero_copy; let algorithm = args.algorithm; @@ -206,10 +267,7 @@ pub async fn op_crypto_sign_key( Algorithm::RsassaPkcs1v15 => { use rsa::pkcs1v15::SigningKey; let private_key = RsaPrivateKey::from_pkcs1_der(&args.key.data)?; - match args - .hash - .ok_or_else(|| type_error("Missing argument hash".to_string()))? - { + match args.hash.ok_or_else(|| Error::MissingArgumentHash)? { CryptoHash::Sha1 => { let signing_key = SigningKey::::new(private_key); signing_key.sign(data) @@ -232,15 +290,13 @@ pub async fn op_crypto_sign_key( Algorithm::RsaPss => { let private_key = RsaPrivateKey::from_pkcs1_der(&args.key.data)?; - let salt_len = args.salt_length.ok_or_else(|| { - type_error("Missing argument saltLength".to_string()) - })? as usize; + let salt_len = args + .salt_length + .ok_or_else(|| Error::MissingArgumentSaltLength)? + as usize; let mut rng = OsRng; - match args - .hash - .ok_or_else(|| type_error("Missing argument hash".to_string()))? - { + match args.hash.ok_or_else(|| Error::MissingArgumentHash)? { CryptoHash::Sha1 => { let signing_key = Pss::new_with_salt::(salt_len); let hashed = Sha1::digest(data); @@ -265,8 +321,10 @@ pub async fn op_crypto_sign_key( .to_vec() } Algorithm::Ecdsa => { - let curve: &EcdsaSigningAlgorithm = - args.named_curve.ok_or_else(not_supported)?.into(); + let curve: &EcdsaSigningAlgorithm = args + .named_curve + .ok_or_else(|| Error::Other(not_supported()))? + .into(); let rng = RingRand::SystemRandom::new(); let key_pair = EcdsaKeyPair::from_pkcs8(curve, &args.key.data, &rng)?; @@ -275,7 +333,7 @@ pub async fn op_crypto_sign_key( if let Some(hash) = args.hash { match hash { CryptoHash::Sha256 | CryptoHash::Sha384 => (), - _ => return Err(type_error("Unsupported algorithm")), + _ => return Err(Error::UnsupportedAlgorithm), } }; @@ -285,14 +343,17 @@ pub async fn op_crypto_sign_key( signature.as_ref().to_vec() } Algorithm::Hmac => { - let hash: HmacAlgorithm = args.hash.ok_or_else(not_supported)?.into(); + let hash: HmacAlgorithm = args + .hash + .ok_or_else(|| Error::Other(not_supported()))? + .into(); let key = HmacKey::new(hash, &args.key.data); let signature = ring::hmac::sign(&key, data); signature.as_ref().to_vec() } - _ => return Err(type_error("Unsupported algorithm".to_string())), + _ => return Err(Error::UnsupportedAlgorithm), }; Ok(signature.into()) @@ -315,7 +376,7 @@ pub struct VerifyArg { pub async fn op_crypto_verify_key( #[serde] args: VerifyArg, #[buffer] zero_copy: JsBuffer, -) -> Result { +) -> Result { deno_core::unsync::spawn_blocking(move || { let data = &*zero_copy; let algorithm = args.algorithm; @@ -326,10 +387,7 @@ pub async fn op_crypto_verify_key( use rsa::pkcs1v15::VerifyingKey; let public_key = read_rsa_public_key(args.key)?; let signature: Signature = args.signature.as_ref().try_into()?; - match args - .hash - .ok_or_else(|| type_error("Missing argument hash".to_string()))? - { + match args.hash.ok_or_else(|| Error::MissingArgumentHash)? { CryptoHash::Sha1 => { let verifying_key = VerifyingKey::::new(public_key); verifying_key.verify(data, &signature).is_ok() @@ -352,14 +410,12 @@ pub async fn op_crypto_verify_key( let public_key = read_rsa_public_key(args.key)?; let signature = args.signature.as_ref(); - let salt_len = args.salt_length.ok_or_else(|| { - type_error("Missing argument saltLength".to_string()) - })? as usize; + let salt_len = args + .salt_length + .ok_or_else(|| Error::MissingArgumentSaltLength)? + as usize; - match args - .hash - .ok_or_else(|| type_error("Missing argument hash".to_string()))? - { + match args.hash.ok_or_else(|| Error::MissingArgumentHash)? { CryptoHash::Sha1 => { let pss = Pss::new_with_salt::(salt_len); let hashed = Sha1::digest(data); @@ -383,15 +439,22 @@ pub async fn op_crypto_verify_key( } } Algorithm::Hmac => { - let hash: HmacAlgorithm = args.hash.ok_or_else(not_supported)?.into(); + let hash: HmacAlgorithm = args + .hash + .ok_or_else(|| Error::Other(not_supported()))? + .into(); let key = HmacKey::new(hash, &args.key.data); ring::hmac::verify(&key, data, &args.signature).is_ok() } Algorithm::Ecdsa => { - let signing_alg: &EcdsaSigningAlgorithm = - args.named_curve.ok_or_else(not_supported)?.into(); - let verify_alg: &EcdsaVerificationAlgorithm = - args.named_curve.ok_or_else(not_supported)?.into(); + let signing_alg: &EcdsaSigningAlgorithm = args + .named_curve + .ok_or_else(|| Error::Other(not_supported()))? + .into(); + let verify_alg: &EcdsaVerificationAlgorithm = args + .named_curve + .ok_or_else(|| Error::Other(not_supported()))? + .into(); let private_key; @@ -404,7 +467,7 @@ pub async fn op_crypto_verify_key( private_key.public_key().as_ref() } KeyType::Public => &*args.key.data, - _ => return Err(type_error("Invalid Key format".to_string())), + _ => return Err(Error::InvalidKeyFormat), }; let public_key = @@ -412,7 +475,7 @@ pub async fn op_crypto_verify_key( public_key.verify(data, &args.signature).is_ok() } - _ => return Err(type_error("Unsupported algorithm".to_string())), + _ => return Err(Error::UnsupportedAlgorithm), }; Ok(verification) @@ -440,70 +503,68 @@ pub struct DeriveKeyArg { pub async fn op_crypto_derive_bits( #[serde] args: DeriveKeyArg, #[buffer] zero_copy: Option, -) -> Result { +) -> Result { deno_core::unsync::spawn_blocking(move || { let algorithm = args.algorithm; match algorithm { Algorithm::Pbkdf2 => { - let zero_copy = zero_copy.ok_or_else(not_supported)?; + let zero_copy = + zero_copy.ok_or_else(|| Error::Other(not_supported()))?; let salt = &*zero_copy; // The caller must validate these cases. assert!(args.length > 0); assert!(args.length % 8 == 0); - let algorithm = match args.hash.ok_or_else(not_supported)? { - CryptoHash::Sha1 => pbkdf2::PBKDF2_HMAC_SHA1, - CryptoHash::Sha256 => pbkdf2::PBKDF2_HMAC_SHA256, - CryptoHash::Sha384 => pbkdf2::PBKDF2_HMAC_SHA384, - CryptoHash::Sha512 => pbkdf2::PBKDF2_HMAC_SHA512, - }; + let algorithm = + match args.hash.ok_or_else(|| Error::Other(not_supported()))? { + CryptoHash::Sha1 => pbkdf2::PBKDF2_HMAC_SHA1, + CryptoHash::Sha256 => pbkdf2::PBKDF2_HMAC_SHA256, + CryptoHash::Sha384 => pbkdf2::PBKDF2_HMAC_SHA384, + CryptoHash::Sha512 => pbkdf2::PBKDF2_HMAC_SHA512, + }; // This will never panic. We have already checked length earlier. - let iterations = - NonZeroU32::new(args.iterations.ok_or_else(not_supported)?).unwrap(); + let iterations = NonZeroU32::new( + args + .iterations + .ok_or_else(|| Error::Other(not_supported()))?, + ) + .unwrap(); let secret = args.key.data; let mut out = vec![0; args.length / 8]; pbkdf2::derive(algorithm, iterations, salt, &secret, &mut out); Ok(out.into()) } Algorithm::Ecdh => { - let named_curve = args.named_curve.ok_or_else(|| { - type_error("Missing argument namedCurve".to_string()) - })?; + let named_curve = args + .named_curve + .ok_or_else(|| Error::MissingArgumentNamedCurve)?; let public_key = args .public_key - .ok_or_else(|| type_error("Missing argument publicKey"))?; + .ok_or_else(|| Error::MissingArgumentPublicKey)?; match named_curve { CryptoNamedCurve::P256 => { let secret_key = p256::SecretKey::from_pkcs8_der(&args.key.data) - .map_err(|_| { - type_error("Unexpected error decoding private key") - })?; + .map_err(|_| Error::DecodePrivateKey)?; let public_key = match public_key.r#type { KeyType::Private => { p256::SecretKey::from_pkcs8_der(&public_key.data) - .map_err(|_| { - type_error("Unexpected error decoding private key") - })? + .map_err(|_| Error::DecodePrivateKey)? .public_key() } KeyType::Public => { let point = p256::EncodedPoint::from_bytes(public_key.data) - .map_err(|_| { - type_error("Unexpected error decoding private key") - })?; + .map_err(|_| Error::DecodePrivateKey)?; let pk = p256::PublicKey::from_encoded_point(&point); // pk is a constant time Option. if pk.is_some().into() { pk.unwrap() } else { - return Err(type_error( - "Unexpected error decoding private key", - )); + return Err(Error::DecodePrivateKey); } } _ => unreachable!(), @@ -519,32 +580,24 @@ pub async fn op_crypto_derive_bits( } CryptoNamedCurve::P384 => { let secret_key = p384::SecretKey::from_pkcs8_der(&args.key.data) - .map_err(|_| { - type_error("Unexpected error decoding private key") - })?; + .map_err(|_| Error::DecodePrivateKey)?; let public_key = match public_key.r#type { KeyType::Private => { p384::SecretKey::from_pkcs8_der(&public_key.data) - .map_err(|_| { - type_error("Unexpected error decoding private key") - })? + .map_err(|_| Error::DecodePrivateKey)? .public_key() } KeyType::Public => { let point = p384::EncodedPoint::from_bytes(public_key.data) - .map_err(|_| { - type_error("Unexpected error decoding private key") - })?; + .map_err(|_| Error::DecodePrivateKey)?; let pk = p384::PublicKey::from_encoded_point(&point); // pk is a constant time Option. if pk.is_some().into() { pk.unwrap() } else { - return Err(type_error( - "Unexpected error decoding private key", - )); + return Err(Error::DecodePrivateKey); } } _ => unreachable!(), @@ -561,18 +614,18 @@ pub async fn op_crypto_derive_bits( } } Algorithm::Hkdf => { - let zero_copy = zero_copy.ok_or_else(not_supported)?; + let zero_copy = + zero_copy.ok_or_else(|| Error::Other(not_supported()))?; let salt = &*zero_copy; - let algorithm = match args.hash.ok_or_else(not_supported)? { - CryptoHash::Sha1 => hkdf::HKDF_SHA1_FOR_LEGACY_USE_ONLY, - CryptoHash::Sha256 => hkdf::HKDF_SHA256, - CryptoHash::Sha384 => hkdf::HKDF_SHA384, - CryptoHash::Sha512 => hkdf::HKDF_SHA512, - }; + let algorithm = + match args.hash.ok_or_else(|| Error::Other(not_supported()))? { + CryptoHash::Sha1 => hkdf::HKDF_SHA1_FOR_LEGACY_USE_ONLY, + CryptoHash::Sha256 => hkdf::HKDF_SHA256, + CryptoHash::Sha384 => hkdf::HKDF_SHA384, + CryptoHash::Sha512 => hkdf::HKDF_SHA512, + }; - let info = args - .info - .ok_or_else(|| type_error("Missing argument info".to_string()))?; + let info = args.info.ok_or_else(|| Error::MissingArgumentInfo)?; // IKM let secret = args.key.data; // L @@ -581,23 +634,20 @@ pub async fn op_crypto_derive_bits( let salt = hkdf::Salt::new(algorithm, salt); let prk = salt.extract(&secret); let info = &[&*info]; - let okm = prk.expand(info, HkdfOutput(length)).map_err(|_e| { - custom_error( - "DOMExceptionOperationError", - "The length provided for HKDF is too large", - ) - })?; + let okm = prk + .expand(info, HkdfOutput(length)) + .map_err(|_e| Error::HKDFLengthTooLarge)?; let mut r = vec![0u8; length]; okm.fill(&mut r)?; Ok(r.into()) } - _ => Err(type_error("Unsupported algorithm".to_string())), + _ => Err(Error::UnsupportedAlgorithm), } }) .await? } -fn read_rsa_public_key(key_data: KeyData) -> Result { +fn read_rsa_public_key(key_data: KeyData) -> Result { let public_key = match key_data.r#type { KeyType::Private => { RsaPrivateKey::from_pkcs1_der(&key_data.data)?.to_public_key() @@ -610,7 +660,7 @@ fn read_rsa_public_key(key_data: KeyData) -> Result { #[op2] #[string] -pub fn op_crypto_random_uuid(state: &mut OpState) -> Result { +pub fn op_crypto_random_uuid(state: &mut OpState) -> Result { let maybe_seeded_rng = state.try_borrow_mut::(); let uuid = if let Some(seeded_rng) = maybe_seeded_rng { let mut bytes = [0u8; 16]; @@ -631,7 +681,7 @@ pub fn op_crypto_random_uuid(state: &mut OpState) -> Result { pub async fn op_crypto_subtle_digest( #[serde] algorithm: CryptoHash, #[buffer] data: JsBuffer, -) -> Result { +) -> Result { let output = spawn_blocking(move || { digest::digest(algorithm.into(), &data) .as_ref() @@ -655,7 +705,7 @@ pub struct WrapUnwrapKeyArg { pub fn op_crypto_wrap_key( #[serde] args: WrapUnwrapKeyArg, #[buffer] data: JsBuffer, -) -> Result { +) -> Result { let algorithm = args.algorithm; match algorithm { @@ -663,20 +713,20 @@ pub fn op_crypto_wrap_key( let key = args.key.as_secret_key()?; if data.len() % 8 != 0 { - return Err(type_error("Data must be multiple of 8 bytes")); + return Err(Error::DataInvalidSize); } let wrapped_key = match key.len() { 16 => KekAes128::new(key.into()).wrap_vec(&data), 24 => KekAes192::new(key.into()).wrap_vec(&data), 32 => KekAes256::new(key.into()).wrap_vec(&data), - _ => return Err(type_error("Invalid key length")), + _ => return Err(Error::InvalidKeyLength), } - .map_err(|_| operation_error("encryption error"))?; + .map_err(|_| Error::EncryptionError)?; Ok(wrapped_key.into()) } - _ => Err(type_error("Unsupported algorithm")), + _ => Err(Error::UnsupportedAlgorithm), } } @@ -685,29 +735,27 @@ pub fn op_crypto_wrap_key( pub fn op_crypto_unwrap_key( #[serde] args: WrapUnwrapKeyArg, #[buffer] data: JsBuffer, -) -> Result { +) -> Result { let algorithm = args.algorithm; match algorithm { Algorithm::AesKw => { let key = args.key.as_secret_key()?; if data.len() % 8 != 0 { - return Err(type_error("Data must be multiple of 8 bytes")); + return Err(Error::DataInvalidSize); } let unwrapped_key = match key.len() { 16 => KekAes128::new(key.into()).unwrap_vec(&data), 24 => KekAes192::new(key.into()).unwrap_vec(&data), 32 => KekAes256::new(key.into()).unwrap_vec(&data), - _ => return Err(type_error("Invalid key length")), + _ => return Err(Error::InvalidKeyLength), } - .map_err(|_| { - operation_error("decryption error - integrity check failed") - })?; + .map_err(|_| Error::DecryptionError)?; Ok(unwrapped_key.into()) } - _ => Err(type_error("Unsupported algorithm")), + _ => Err(Error::UnsupportedAlgorithm), } } diff --git a/ext/crypto/shared.rs b/ext/crypto/shared.rs index d06a268cd6..f70d32856c 100644 --- a/ext/crypto/shared.rs +++ b/ext/crypto/shared.rs @@ -2,9 +2,6 @@ use std::borrow::Cow; -use deno_core::error::custom_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::JsBuffer; use deno_core::ToJsBuffer; use elliptic_curve::sec1::ToEncodedPoint; @@ -63,47 +60,73 @@ pub enum RustRawKeyData { Public(ToJsBuffer), } +#[derive(Debug, thiserror::Error)] +pub enum SharedError { + #[error("expected valid private key")] + ExpectedValidPrivateKey, + #[error("expected valid public key")] + ExpectedValidPublicKey, + #[error("expected valid private EC key")] + ExpectedValidPrivateECKey, + #[error("expected valid public EC key")] + ExpectedValidPublicECKey, + #[error("expected private key")] + ExpectedPrivateKey, + #[error("expected public key")] + ExpectedPublicKey, + #[error("expected secret key")] + ExpectedSecretKey, + #[error("failed to decode private key")] + FailedDecodePrivateKey, + #[error("failed to decode public key")] + FailedDecodePublicKey, + #[error("unsupported format")] + UnsupportedFormat, +} + impl V8RawKeyData { - pub fn as_rsa_public_key(&self) -> Result, AnyError> { + pub fn as_rsa_public_key(&self) -> Result, SharedError> { match self { V8RawKeyData::Public(data) => Ok(Cow::Borrowed(data)), V8RawKeyData::Private(data) => { let private_key = RsaPrivateKey::from_pkcs1_der(data) - .map_err(|_| type_error("expected valid private key"))?; + .map_err(|_| SharedError::ExpectedValidPrivateKey)?; let public_key_doc = private_key .to_public_key() .to_pkcs1_der() - .map_err(|_| type_error("expected valid public key"))?; + .map_err(|_| SharedError::ExpectedValidPublicKey)?; Ok(Cow::Owned(public_key_doc.as_bytes().into())) } - _ => Err(type_error("expected public key")), + _ => Err(SharedError::ExpectedPublicKey), } } - pub fn as_rsa_private_key(&self) -> Result<&[u8], AnyError> { + pub fn as_rsa_private_key(&self) -> Result<&[u8], SharedError> { match self { V8RawKeyData::Private(data) => Ok(data), - _ => Err(type_error("expected private key")), + _ => Err(SharedError::ExpectedPrivateKey), } } - pub fn as_secret_key(&self) -> Result<&[u8], AnyError> { + pub fn as_secret_key(&self) -> Result<&[u8], SharedError> { match self { V8RawKeyData::Secret(data) => Ok(data), - _ => Err(type_error("expected secret key")), + _ => Err(SharedError::ExpectedSecretKey), } } - pub fn as_ec_public_key_p256(&self) -> Result { + pub fn as_ec_public_key_p256( + &self, + ) -> Result { match self { V8RawKeyData::Public(data) => p256::PublicKey::from_sec1_bytes(data) .map(|p| p.to_encoded_point(false)) - .map_err(|_| type_error("expected valid public EC key")), + .map_err(|_| SharedError::ExpectedValidPublicECKey), V8RawKeyData::Private(data) => { let signing_key = p256::SecretKey::from_pkcs8_der(data) - .map_err(|_| type_error("expected valid private EC key"))?; + .map_err(|_| SharedError::ExpectedValidPrivateECKey)?; Ok(signing_key.public_key().to_encoded_point(false)) } // Should never reach here. @@ -111,14 +134,16 @@ impl V8RawKeyData { } } - pub fn as_ec_public_key_p384(&self) -> Result { + pub fn as_ec_public_key_p384( + &self, + ) -> Result { match self { V8RawKeyData::Public(data) => p384::PublicKey::from_sec1_bytes(data) .map(|p| p.to_encoded_point(false)) - .map_err(|_| type_error("expected valid public EC key")), + .map_err(|_| SharedError::ExpectedValidPublicECKey), V8RawKeyData::Private(data) => { let signing_key = p384::SecretKey::from_pkcs8_der(data) - .map_err(|_| type_error("expected valid private EC key"))?; + .map_err(|_| SharedError::ExpectedValidPrivateECKey)?; Ok(signing_key.public_key().to_encoded_point(false)) } // Should never reach here. @@ -126,16 +151,18 @@ impl V8RawKeyData { } } - pub fn as_ec_public_key_p521(&self) -> Result { + pub fn as_ec_public_key_p521( + &self, + ) -> Result { match self { V8RawKeyData::Public(data) => { // public_key is a serialized EncodedPoint p521::EncodedPoint::from_bytes(data) - .map_err(|_| type_error("expected valid public EC key")) + .map_err(|_| SharedError::ExpectedValidPublicECKey) } V8RawKeyData::Private(data) => { let signing_key = p521::SecretKey::from_pkcs8_der(data) - .map_err(|_| type_error("expected valid private EC key"))?; + .map_err(|_| SharedError::ExpectedValidPrivateECKey)?; Ok(signing_key.public_key().to_encoded_point(false)) } // Should never reach here. @@ -143,26 +170,10 @@ impl V8RawKeyData { } } - pub fn as_ec_private_key(&self) -> Result<&[u8], AnyError> { + pub fn as_ec_private_key(&self) -> Result<&[u8], SharedError> { match self { V8RawKeyData::Private(data) => Ok(data), - _ => Err(type_error("expected private key")), + _ => Err(SharedError::ExpectedPrivateKey), } } } - -pub fn data_error(msg: impl Into>) -> AnyError { - custom_error("DOMExceptionDataError", msg) -} - -pub fn not_supported_error(msg: impl Into>) -> AnyError { - custom_error("DOMExceptionNotSupportedError", msg) -} - -pub fn operation_error(msg: impl Into>) -> AnyError { - custom_error("DOMExceptionOperationError", msg) -} - -pub fn unsupported_format() -> AnyError { - not_supported_error("unsupported format") -} diff --git a/ext/crypto/x25519.rs b/ext/crypto/x25519.rs index 8fcad3ef21..d2c4d986b9 100644 --- a/ext/crypto/x25519.rs +++ b/ext/crypto/x25519.rs @@ -1,8 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use curve25519_dalek::montgomery::MontgomeryPoint; -use deno_core::error::custom_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::ToJsBuffer; use elliptic_curve::pkcs8::PrivateKeyInfo; @@ -13,6 +11,14 @@ use spki::der::asn1::BitString; use spki::der::Decode; use spki::der::Encode; +#[derive(Debug, thiserror::Error)] +pub enum X25519Error { + #[error("Failed to export key")] + FailedExport, + #[error(transparent)] + Der(#[from] spki::der::Error), +} + #[op2(fast)] pub fn op_crypto_generate_x25519_keypair( #[buffer] pkey: &mut [u8], @@ -47,10 +53,10 @@ pub fn op_crypto_derive_bits_x25519( let sh_sec = x25519_dalek::x25519(k, u); let point = MontgomeryPoint(sh_sec); if point.ct_eq(&MONTGOMERY_IDENTITY).unwrap_u8() == 1 { - return false; + return true; } secret.copy_from_slice(&sh_sec); - true + false } // id-X25519 OBJECT IDENTIFIER ::= { 1 3 101 110 } @@ -113,7 +119,7 @@ pub fn op_crypto_import_pkcs8_x25519( #[serde] pub fn op_crypto_export_spki_x25519( #[buffer] pubkey: &[u8], -) -> Result { +) -> Result { let key_info = spki::SubjectPublicKeyInfo { algorithm: spki::AlgorithmIdentifierRef { // id-X25519 @@ -125,9 +131,7 @@ pub fn op_crypto_export_spki_x25519( Ok( key_info .to_der() - .map_err(|_| { - custom_error("DOMExceptionOperationError", "Failed to export key") - })? + .map_err(|_| X25519Error::FailedExport)? .into(), ) } @@ -136,7 +140,7 @@ pub fn op_crypto_export_spki_x25519( #[serde] pub fn op_crypto_export_pkcs8_x25519( #[buffer] pkey: &[u8], -) -> Result { +) -> Result { use rsa::pkcs1::der::Encode; // This should probably use OneAsymmetricKey instead diff --git a/ext/crypto/x448.rs b/ext/crypto/x448.rs new file mode 100644 index 0000000000..89bf48e28b --- /dev/null +++ b/ext/crypto/x448.rs @@ -0,0 +1,152 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::op2; +use deno_core::ToJsBuffer; +use ed448_goldilocks::curve::MontgomeryPoint; +use ed448_goldilocks::Scalar; +use elliptic_curve::pkcs8::PrivateKeyInfo; +use elliptic_curve::subtle::ConstantTimeEq; +use rand::rngs::OsRng; +use rand::RngCore; +use spki::der::asn1::BitString; +use spki::der::Decode; +use spki::der::Encode; + +#[derive(Debug, thiserror::Error)] +pub enum X448Error { + #[error("Failed to export key")] + FailedExport, + #[error(transparent)] + Der(#[from] spki::der::Error), +} + +#[op2(fast)] +pub fn op_crypto_generate_x448_keypair( + #[buffer] pkey: &mut [u8], + #[buffer] pubkey: &mut [u8], +) { + let mut rng = OsRng; + rng.fill_bytes(pkey); + + // x448(pkey, 5) + let point = &MontgomeryPoint::generator() + * &Scalar::from_bytes(pkey.try_into().unwrap()); + pubkey.copy_from_slice(&point.0); +} + +const MONTGOMERY_IDENTITY: MontgomeryPoint = MontgomeryPoint([0; 56]); + +#[op2(fast)] +pub fn op_crypto_derive_bits_x448( + #[buffer] k: &[u8], + #[buffer] u: &[u8], + #[buffer] secret: &mut [u8], +) -> bool { + let k: [u8; 56] = k.try_into().expect("Expected byteLength 56"); + let u: [u8; 56] = u.try_into().expect("Expected byteLength 56"); + + // x448(k, u) + let point = &MontgomeryPoint(u) * &Scalar::from_bytes(k); + if point.ct_eq(&MONTGOMERY_IDENTITY).unwrap_u8() == 1 { + return true; + } + + secret.copy_from_slice(&point.0); + false +} + +// id-X448 OBJECT IDENTIFIER ::= { 1 3 101 111 } +const X448_OID: const_oid::ObjectIdentifier = + const_oid::ObjectIdentifier::new_unwrap("1.3.101.111"); + +#[op2] +#[serde] +pub fn op_crypto_export_spki_x448( + #[buffer] pubkey: &[u8], +) -> Result { + let key_info = spki::SubjectPublicKeyInfo { + algorithm: spki::AlgorithmIdentifierRef { + oid: X448_OID, + parameters: None, + }, + subject_public_key: BitString::from_bytes(pubkey)?, + }; + Ok( + key_info + .to_der() + .map_err(|_| X448Error::FailedExport)? + .into(), + ) +} + +#[op2] +#[serde] +pub fn op_crypto_export_pkcs8_x448( + #[buffer] pkey: &[u8], +) -> Result { + use rsa::pkcs1::der::Encode; + + let pk_info = rsa::pkcs8::PrivateKeyInfo { + public_key: None, + algorithm: rsa::pkcs8::AlgorithmIdentifierRef { + oid: X448_OID, + parameters: None, + }, + private_key: pkey, // OCTET STRING + }; + + let mut buf = Vec::new(); + pk_info.encode_to_vec(&mut buf)?; + Ok(buf.into()) +} + +#[op2(fast)] +pub fn op_crypto_import_spki_x448( + #[buffer] key_data: &[u8], + #[buffer] out: &mut [u8], +) -> bool { + // 2-3. + let pk_info = match spki::SubjectPublicKeyInfoRef::try_from(key_data) { + Ok(pk_info) => pk_info, + Err(_) => return false, + }; + // 4. + let alg = pk_info.algorithm.oid; + if alg != X448_OID { + return false; + } + // 5. + if pk_info.algorithm.parameters.is_some() { + return false; + } + out.copy_from_slice(pk_info.subject_public_key.raw_bytes()); + true +} + +#[op2(fast)] +pub fn op_crypto_import_pkcs8_x448( + #[buffer] key_data: &[u8], + #[buffer] out: &mut [u8], +) -> bool { + // 2-3. + let pk_info = match PrivateKeyInfo::from_der(key_data) { + Ok(pk_info) => pk_info, + Err(_) => return false, + }; + // 4. + let alg = pk_info.algorithm.oid; + if alg != X448_OID { + return false; + } + // 5. + if pk_info.algorithm.parameters.is_some() { + return false; + } + // 6. + // CurvePrivateKey ::= OCTET STRING + if pk_info.private_key.len() != 56 { + return false; + } + out.copy_from_slice(&pk_info.private_key[2..]); + true +} diff --git a/ext/fetch/23_request.js b/ext/fetch/23_request.js index 6211e927d9..61cac22d2e 100644 --- a/ext/fetch/23_request.js +++ b/ext/fetch/23_request.js @@ -269,12 +269,6 @@ class Request { /** @type {AbortSignal} */ get [_signal]() { const signal = this[_signalCache]; - // This signal not been created yet, and the request is still in progress - if (signal === undefined) { - const signal = newSignal(); - this[_signalCache] = signal; - return signal; - } // This signal has not been created yet, but the request has already completed if (signal === false) { const signal = newSignal(); @@ -282,6 +276,18 @@ class Request { signal[signalAbort](signalAbortError); return signal; } + + // This signal not been created yet, and the request is still in progress + if (signal === undefined) { + const signal = newSignal(); + this[_signalCache] = signal; + this[_request].onCancel?.(() => { + signal[signalAbort](signalAbortError); + }); + + return signal; + } + return signal; } get [_mimeType]() { diff --git a/ext/fetch/23_response.js b/ext/fetch/23_response.js index ff4ad5facd..278dcb7dec 100644 --- a/ext/fetch/23_response.js +++ b/ext/fetch/23_response.js @@ -61,6 +61,15 @@ const _mimeType = Symbol("mime type"); const _body = Symbol("body"); const _brand = webidl.brand; +// it's slightly faster to cache these +const webidlConvertersBodyInitDomString = + webidl.converters["BodyInit_DOMString?"]; +const webidlConvertersUSVString = webidl.converters["USVString"]; +const webidlConvertersUnsignedShort = webidl.converters["unsigned short"]; +const webidlConvertersAny = webidl.converters["any"]; +const webidlConvertersByteString = webidl.converters["ByteString"]; +const webidlConvertersHeadersInit = webidl.converters["HeadersInit"]; + /** * @typedef InnerResponse * @property {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"} type @@ -259,8 +268,8 @@ class Response { */ static redirect(url, status = 302) { const prefix = "Failed to execute 'Response.redirect'"; - url = webidl.converters["USVString"](url, prefix, "Argument 1"); - status = webidl.converters["unsigned short"](status, prefix, "Argument 2"); + url = webidlConvertersUSVString(url, prefix, "Argument 1"); + status = webidlConvertersUnsignedShort(status, prefix, "Argument 2"); const baseURL = getLocationHref(); const parsedURL = new URL(url, baseURL); @@ -286,8 +295,8 @@ class Response { */ static json(data = undefined, init = { __proto__: null }) { const prefix = "Failed to execute 'Response.json'"; - data = webidl.converters.any(data); - init = webidl.converters["ResponseInit_fast"](init, prefix, "Argument 2"); + data = webidlConvertersAny(data); + init = webidlConvertersResponseInitFast(init, prefix, "Argument 2"); const str = serializeJSValueToJSONString(data); const res = extractBody(str); @@ -313,8 +322,8 @@ class Response { } const prefix = "Failed to construct 'Response'"; - body = webidl.converters["BodyInit_DOMString?"](body, prefix, "Argument 1"); - init = webidl.converters["ResponseInit_fast"](init, prefix, "Argument 2"); + body = webidlConvertersBodyInitDomString(body, prefix, "Argument 1"); + init = webidlConvertersResponseInitFast(init, prefix, "Argument 2"); this[_response] = newInnerResponse(); this[_headers] = headersFromHeaderList( @@ -443,47 +452,49 @@ webidl.converters["Response"] = webidl.createInterfaceConverter( "Response", ResponsePrototype, ); -webidl.converters["ResponseInit"] = webidl.createDictionaryConverter( - "ResponseInit", - [{ - key: "status", - defaultValue: 200, - converter: webidl.converters["unsigned short"], - }, { - key: "statusText", - defaultValue: "", - converter: webidl.converters["ByteString"], - }, { - key: "headers", - converter: webidl.converters["HeadersInit"], - }], -); -webidl.converters["ResponseInit_fast"] = function ( - init, - prefix, - context, - opts, -) { - if (init === undefined || init === null) { - return { status: 200, statusText: "", headers: undefined }; - } - // Fast path, if not a proxy - if (typeof init === "object" && !core.isProxy(init)) { - // Not a proxy fast path - const status = init.status !== undefined - ? webidl.converters["unsigned short"](init.status) - : 200; - const statusText = init.statusText !== undefined - ? webidl.converters["ByteString"](init.statusText) - : ""; - const headers = init.headers !== undefined - ? webidl.converters["HeadersInit"](init.headers) - : undefined; - return { status, statusText, headers }; - } - // Slow default path - return webidl.converters["ResponseInit"](init, prefix, context, opts); -}; +const webidlConvertersResponseInit = webidl.converters["ResponseInit"] = webidl + .createDictionaryConverter( + "ResponseInit", + [{ + key: "status", + defaultValue: 200, + converter: webidlConvertersUnsignedShort, + }, { + key: "statusText", + defaultValue: "", + converter: webidlConvertersByteString, + }, { + key: "headers", + converter: webidlConvertersHeadersInit, + }], + ); +const webidlConvertersResponseInitFast = webidl + .converters["ResponseInit_fast"] = function ( + init, + prefix, + context, + opts, + ) { + if (init === undefined || init === null) { + return { status: 200, statusText: "", headers: undefined }; + } + // Fast path, if not a proxy + if (typeof init === "object" && !core.isProxy(init)) { + // Not a proxy fast path + const status = init.status !== undefined + ? webidlConvertersUnsignedShort(init.status) + : 200; + const statusText = init.statusText !== undefined + ? webidlConvertersByteString(init.statusText) + : ""; + const headers = init.headers !== undefined + ? webidlConvertersHeadersInit(init.headers) + : undefined; + return { status, statusText, headers }; + } + // Slow default path + return webidlConvertersResponseInit(init, prefix, context, opts); + }; /** * @param {Response} response diff --git a/ext/fetch/Cargo.toml b/ext/fetch/Cargo.toml index 2ec1c25f0c..56d416bbb8 100644 --- a/ext/fetch/Cargo.toml +++ b/ext/fetch/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_fetch" -version = "0.192.0" +version = "0.201.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -32,6 +32,7 @@ percent-encoding.workspace = true rustls-webpki.workspace = true serde.workspace = true serde_json.workspace = true +thiserror.workspace = true tokio.workspace = true tokio-rustls.workspace = true tokio-socks.workspace = true diff --git a/ext/fetch/fs_fetch_handler.rs b/ext/fetch/fs_fetch_handler.rs index 4c2b81f356..c236dd9c67 100644 --- a/ext/fetch/fs_fetch_handler.rs +++ b/ext/fetch/fs_fetch_handler.rs @@ -4,7 +4,6 @@ use crate::CancelHandle; use crate::CancelableResponseFuture; use crate::FetchHandler; -use deno_core::error::type_error; use deno_core::futures::FutureExt; use deno_core::futures::TryFutureExt; use deno_core::futures::TryStreamExt; @@ -42,9 +41,7 @@ impl FetchHandler for FsFetchHandler { .map_err(|_| ())?; Ok::<_, ()>(response) } - .map_err(move |_| { - type_error("NetworkError when attempting to fetch resource") - }) + .map_err(move |_| super::FetchError::NetworkError) .or_cancel(&cancel_handle) .boxed_local(); diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 88f3038528..7ef26431c2 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -17,10 +17,6 @@ use std::sync::Arc; use std::task::Context; use std::task::Poll; -use deno_core::anyhow::anyhow; -use deno_core::anyhow::Error; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::futures::stream::Peekable; use deno_core::futures::Future; use deno_core::futures::FutureExt; @@ -28,6 +24,7 @@ use deno_core::futures::Stream; use deno_core::futures::StreamExt; use deno_core::futures::TryFutureExt; use deno_core::op2; +use deno_core::url; use deno_core::url::Url; use deno_core::AsyncRefCell; use deno_core::AsyncResult; @@ -42,6 +39,7 @@ use deno_core::OpState; use deno_core::RcRef; use deno_core::Resource; use deno_core::ResourceId; +use deno_permissions::PermissionCheckError; use deno_tls::rustls::RootCertStore; use deno_tls::Proxy; use deno_tls::RootCertStoreProvider; @@ -87,15 +85,18 @@ pub struct Options { pub root_cert_store_provider: Option>, pub proxy: Option, #[allow(clippy::type_complexity)] - pub request_builder_hook: - Option) -> Result<(), AnyError>>, + pub request_builder_hook: Option< + fn(&mut http::Request) -> Result<(), deno_core::error::AnyError>, + >, pub unsafely_ignore_certificate_errors: Option>, pub client_cert_chain_and_key: TlsKeys, pub file_fetch_handler: Rc, } impl Options { - pub fn root_cert_store(&self) -> Result, AnyError> { + pub fn root_cert_store( + &self, + ) -> Result, deno_core::error::AnyError> { Ok(match &self.root_cert_store_provider { Some(provider) => Some(provider.get_or_try_init()?.clone()), None => None, @@ -144,6 +145,51 @@ deno_core::extension!(deno_fetch, }, ); +#[derive(Debug, thiserror::Error)] +pub enum FetchError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + Permission(#[from] PermissionCheckError), + #[error("NetworkError when attempting to fetch resource")] + NetworkError, + #[error("Fetching files only supports the GET method: received {0}")] + FsNotGet(Method), + #[error("Invalid URL {0}")] + InvalidUrl(Url), + #[error(transparent)] + InvalidHeaderName(#[from] http::header::InvalidHeaderName), + #[error(transparent)] + InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), + #[error("{0:?}")] + DataUrl(data_url::DataUrlError), + #[error("{0:?}")] + Base64(data_url::forgiving_base64::InvalidBase64), + #[error("Blob for the given URL not found.")] + BlobNotFound, + #[error("Url scheme '{0}' not supported")] + SchemeNotSupported(String), + #[error("Request was cancelled")] + RequestCanceled, + #[error(transparent)] + Http(#[from] http::Error), + #[error(transparent)] + ClientCreate(#[from] HttpClientCreateError), + #[error(transparent)] + Url(#[from] url::ParseError), + #[error(transparent)] + Method(#[from] http::method::InvalidMethod), + #[error(transparent)] + ClientSend(#[from] ClientSendError), + #[error(transparent)] + RequestBuilderHook(deno_core::error::AnyError), + #[error(transparent)] + Io(#[from] std::io::Error), + // Only used for node upgrade + #[error(transparent)] + Hyper(#[from] hyper::Error), +} + pub type CancelableResponseFuture = Pin>>; @@ -170,11 +216,7 @@ impl FetchHandler for DefaultFileFetchHandler { _state: &mut OpState, _url: &Url, ) -> (CancelableResponseFuture, Option>) { - let fut = async move { - Ok(Err(type_error( - "NetworkError when attempting to fetch resource", - ))) - }; + let fut = async move { Ok(Err(FetchError::NetworkError)) }; (Box::pin(fut), None) } } @@ -191,7 +233,7 @@ pub struct FetchReturn { pub fn get_or_create_client_from_state( state: &mut OpState, -) -> Result { +) -> Result { if let Some(client) = state.try_borrow::() { Ok(client.clone()) } else { @@ -204,11 +246,13 @@ pub fn get_or_create_client_from_state( pub fn create_client_from_options( options: &Options, -) -> Result { +) -> Result { create_http_client( &options.user_agent, CreateHttpClientOptions { - root_cert_store: options.root_cert_store()?, + root_cert_store: options + .root_cert_store() + .map_err(HttpClientCreateError::RootCertStore)?, ca_certs: vec![], proxy: options.proxy.clone(), unsafely_ignore_certificate_errors: options @@ -230,7 +274,9 @@ pub fn create_client_from_options( #[allow(clippy::type_complexity)] pub struct ResourceToBodyAdapter( Rc, - Option>>>>, + Option< + Pin>>>, + >, ); impl ResourceToBodyAdapter { @@ -246,7 +292,7 @@ unsafe impl Send for ResourceToBodyAdapter {} unsafe impl Sync for ResourceToBodyAdapter {} impl Stream for ResourceToBodyAdapter { - type Item = Result; + type Item = Result; fn poll_next( self: Pin<&mut Self>, @@ -276,7 +322,7 @@ impl Stream for ResourceToBodyAdapter { impl hyper::body::Body for ResourceToBodyAdapter { type Data = Bytes; - type Error = Error; + type Error = deno_core::error::AnyError; fn poll_frame( self: Pin<&mut Self>, @@ -301,13 +347,13 @@ pub trait FetchPermissions { &mut self, url: &Url, api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result<(), PermissionCheckError>; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_read<'a>( &mut self, p: &'a Path, api_name: &str, - ) -> Result, AnyError>; + ) -> Result, PermissionCheckError>; } impl FetchPermissions for deno_permissions::PermissionsContainer { @@ -316,7 +362,7 @@ impl FetchPermissions for deno_permissions::PermissionsContainer { &mut self, url: &Url, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_net_url(self, url, api_name) } @@ -325,7 +371,7 @@ impl FetchPermissions for deno_permissions::PermissionsContainer { &mut self, path: &'a Path, api_name: &str, - ) -> Result, AnyError> { + ) -> Result, PermissionCheckError> { deno_permissions::PermissionsContainer::check_read_path( self, path, @@ -346,12 +392,15 @@ pub fn op_fetch( has_body: bool, #[buffer] data: Option, #[smi] resource: Option, -) -> Result +) -> Result where FP: FetchPermissions + 'static, { let (client, allow_host) = if let Some(rid) = client_rid { - let r = state.resource_table.get::(rid)?; + let r = state + .resource_table + .get::(rid) + .map_err(FetchError::Resource)?; (r.client.clone(), r.allow_host) } else { (get_or_create_client_from_state(state)?, false) @@ -364,9 +413,7 @@ where let scheme = url.scheme(); let (request_rid, cancel_handle_rid) = match scheme { "file" => { - let path = url.to_file_path().map_err(|_| { - type_error("NetworkError when attempting to fetch resource") - })?; + let path = url.to_file_path().map_err(|_| FetchError::NetworkError)?; let permissions = state.borrow_mut::(); let path = permissions.check_read(&path, "fetch()")?; let url = match path { @@ -375,9 +422,7 @@ where }; if method != Method::GET { - return Err(type_error(format!( - "Fetching files only supports the GET method: received {method}" - ))); + return Err(FetchError::FsNotGet(method)); } let Options { @@ -402,7 +447,7 @@ where let uri = url .as_str() .parse::() - .map_err(|_| type_error(format!("Invalid URL {url}")))?; + .map_err(|_| FetchError::InvalidUrl(url.clone()))?; let mut con_len = None; let body = if has_body { @@ -416,7 +461,10 @@ where .boxed() } (_, Some(resource)) => { - let resource = state.resource_table.take_any(resource)?; + let resource = state + .resource_table + .take_any(resource) + .map_err(FetchError::Resource)?; match resource.size_hint() { (body_size, Some(n)) if body_size == n && body_size > 0 => { con_len = Some(body_size); @@ -453,10 +501,8 @@ where } for (key, value) in headers { - let name = HeaderName::from_bytes(&key) - .map_err(|err| type_error(err.to_string()))?; - let v = HeaderValue::from_bytes(&value) - .map_err(|err| type_error(err.to_string()))?; + let name = HeaderName::from_bytes(&key)?; + let v = HeaderValue::from_bytes(&value)?; if (name != HOST || allow_host) && name != CONTENT_LENGTH { request.headers_mut().append(name, v); @@ -474,20 +520,18 @@ where let options = state.borrow::(); if let Some(request_builder_hook) = options.request_builder_hook { request_builder_hook(&mut request) - .map_err(|err| type_error(err.to_string()))?; + .map_err(FetchError::RequestBuilderHook)?; } let cancel_handle = CancelHandle::new_rc(); let cancel_handle_ = cancel_handle.clone(); - let fut = { - async move { - client - .send(request) - .map_err(Into::into) - .or_cancel(cancel_handle_) - .await - } + let fut = async move { + client + .send(request) + .map_err(Into::into) + .or_cancel(cancel_handle_) + .await }; let request_rid = state.resource_table.add(FetchRequestResource { @@ -501,12 +545,10 @@ where (request_rid, Some(cancel_handle_rid)) } "data" => { - let data_url = DataUrl::process(url.as_str()) - .map_err(|e| type_error(format!("{e:?}")))?; + let data_url = + DataUrl::process(url.as_str()).map_err(FetchError::DataUrl)?; - let (body, _) = data_url - .decode_to_vec() - .map_err(|e| type_error(format!("{e:?}")))?; + let (body, _) = data_url.decode_to_vec().map_err(FetchError::Base64)?; let body = http_body_util::Full::new(body.into()) .map_err(|never| match never {}) .boxed(); @@ -528,11 +570,9 @@ where "blob" => { // Blob URL resolution happens in the JS side of fetch. If we got here is // because the URL isn't an object URL. - return Err(type_error("Blob for the given URL not found.")); - } - _ => { - return Err(type_error(format!("Url scheme '{scheme}' not supported"))) + return Err(FetchError::BlobNotFound); } + _ => return Err(FetchError::SchemeNotSupported(scheme.to_string())), }; Ok(FetchReturn { @@ -564,11 +604,12 @@ pub struct FetchResponse { pub async fn op_fetch_send( state: Rc>, #[smi] rid: ResourceId, -) -> Result { +) -> Result { let request = state .borrow_mut() .resource_table - .take::(rid)?; + .take::(rid) + .map_err(FetchError::Resource)?; let request = Rc::try_unwrap(request) .ok() @@ -581,22 +622,23 @@ pub async fn op_fetch_send( // If any error in the chain is a hyper body error, return that as a special result we can use to // reconstruct an error chain (eg: `new TypeError(..., { cause: new Error(...) })`). // TODO(mmastrac): it would be a lot easier if we just passed a v8::Global through here instead - let mut err_ref: &dyn std::error::Error = err.as_ref(); - while let Some(err_src) = std::error::Error::source(err_ref) { - if let Some(err_src) = err_src.downcast_ref::() { - if let Some(err_src) = std::error::Error::source(err_src) { - return Ok(FetchResponse { - error: Some((err.to_string(), err_src.to_string())), - ..Default::default() - }); + + if let FetchError::ClientSend(err_src) = &err { + if let Some(client_err) = std::error::Error::source(&err_src.source) { + if let Some(err_src) = client_err.downcast_ref::() { + if let Some(err_src) = std::error::Error::source(err_src) { + return Ok(FetchResponse { + error: Some((err.to_string(), err_src.to_string())), + ..Default::default() + }); + } } } - err_ref = err_src; } - return Err(type_error(err.to_string())); + return Err(err); } - Err(_) => return Err(type_error("Request was cancelled")), + Err(_) => return Err(FetchError::RequestCanceled), }; let status = res.status(); @@ -636,7 +678,7 @@ pub async fn op_fetch_send( } type CancelableResponseResult = - Result, AnyError>, Canceled>; + Result, FetchError>, Canceled>; pub struct FetchRequestResource { pub future: Pin>>, @@ -691,7 +733,7 @@ impl FetchResponseResource { } } - pub async fn upgrade(self) -> Result { + pub async fn upgrade(self) -> Result { let reader = self.response_reader.into_inner(); match reader { FetchResponseReader::Start(resp) => Ok(hyper::upgrade::on(resp).await?), @@ -746,7 +788,9 @@ impl Resource for FetchResponseResource { // safely call `await` on it without creating a race condition. Some(_) => match reader.as_mut().next().await.unwrap() { Ok(chunk) => assert!(chunk.is_empty()), - Err(err) => break Err(type_error(err.to_string())), + Err(err) => { + break Err(deno_core::error::type_error(err.to_string())) + } }, None => break Ok(BufView::empty()), } @@ -809,7 +853,7 @@ pub fn op_fetch_custom_client( state: &mut OpState, #[serde] args: CreateHttpClientArgs, #[cppgc] tls_keys: &TlsKeysHolder, -) -> Result +) -> Result where FP: FetchPermissions + 'static, { @@ -829,7 +873,9 @@ where let client = create_http_client( &options.user_agent, CreateHttpClientOptions { - root_cert_store: options.root_cert_store()?, + root_cert_store: options + .root_cert_store() + .map_err(HttpClientCreateError::RootCertStore)?, ca_certs, proxy: args.proxy, unsafely_ignore_certificate_errors: options @@ -887,19 +933,34 @@ impl Default for CreateHttpClientOptions { } } +#[derive(Debug, thiserror::Error)] +pub enum HttpClientCreateError { + #[error(transparent)] + Tls(deno_tls::TlsError), + #[error("Illegal characters in User-Agent: received {0}")] + InvalidUserAgent(String), + #[error("invalid proxy url")] + InvalidProxyUrl, + #[error("Cannot create Http Client: either `http1` or `http2` needs to be set to true")] + HttpVersionSelectionInvalid, + #[error(transparent)] + RootCertStore(deno_core::error::AnyError), +} + /// Create new instance of async Client. This client supports /// proxies and doesn't follow redirects. pub fn create_http_client( user_agent: &str, options: CreateHttpClientOptions, -) -> Result { +) -> Result { let mut tls_config = deno_tls::create_client_config( options.root_cert_store, options.ca_certs, options.unsafely_ignore_certificate_errors, options.client_cert_chain_and_key.into(), deno_tls::SocketUse::Http, - )?; + ) + .map_err(HttpClientCreateError::Tls)?; // Proxy TLS should not send ALPN tls_config.alpn_protocols.clear(); @@ -919,9 +980,7 @@ pub fn create_http_client( http_connector.enforce_http(false); let user_agent = user_agent.parse::().map_err(|_| { - type_error(format!( - "Illegal characters in User-Agent: received {user_agent}" - )) + HttpClientCreateError::InvalidUserAgent(user_agent.to_string()) })?; let mut builder = @@ -932,7 +991,7 @@ pub fn create_http_client( let mut proxies = proxy::from_env(); if let Some(proxy) = options.proxy { let mut intercept = proxy::Intercept::all(&proxy.url) - .ok_or_else(|| type_error("invalid proxy url"))?; + .ok_or_else(|| HttpClientCreateError::InvalidProxyUrl)?; if let Some(basic_auth) = &proxy.basic_auth { intercept.set_auth(&basic_auth.username, &basic_auth.password); } @@ -964,7 +1023,7 @@ pub fn create_http_client( } (true, true) => {} (false, false) => { - return Err(type_error("Cannot create Http Client: either `http1` or `http2` needs to be set to true")) + return Err(HttpClientCreateError::HttpVersionSelectionInvalid) } } @@ -980,10 +1039,8 @@ pub fn create_http_client( #[op2] #[serde] -pub fn op_utf8_to_byte_string( - #[string] input: String, -) -> Result { - Ok(input.into()) +pub fn op_utf8_to_byte_string(#[string] input: String) -> ByteString { + input.into() } #[derive(Clone, Debug)] @@ -1003,7 +1060,7 @@ const STAR_STAR: HeaderValue = HeaderValue::from_static("*/*"); #[derive(Debug)] pub struct ClientSendError { uri: Uri, - source: hyper_util::client::legacy::Error, + pub source: hyper_util::client::legacy::Error, } impl ClientSendError { @@ -1075,12 +1132,14 @@ impl Client { .oneshot(req) .await .map_err(|e| ClientSendError { uri, source: e })?; - Ok(resp.map(|b| b.map_err(|e| anyhow!(e)).boxed())) + Ok(resp.map(|b| b.map_err(|e| deno_core::anyhow::anyhow!(e)).boxed())) } } -pub type ReqBody = http_body_util::combinators::BoxBody; -pub type ResBody = http_body_util::combinators::BoxBody; +pub type ReqBody = + http_body_util::combinators::BoxBody; +pub type ResBody = + http_body_util::combinators::BoxBody; /// Copied from https://github.com/seanmonstar/reqwest/blob/b9d62a0323d96f11672a61a17bf8849baec00275/src/async_impl/request.rs#L572 /// Check the request URL for a "username:password" type authority, and if diff --git a/ext/ffi/Cargo.toml b/ext/ffi/Cargo.toml index 8c4ccdc9ba..295e8be846 100644 --- a/ext/ffi/Cargo.toml +++ b/ext/ffi/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_ffi" -version = "0.155.0" +version = "0.164.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -21,9 +21,12 @@ dynasmrt = "1.2.3" libffi = "=3.2.0" libffi-sys = "=2.3.0" log.workspace = true +num-bigint.workspace = true serde.workspace = true serde-value = "0.7" serde_json = "1.0" +thiserror.workspace = true +tokio.workspace = true [target.'cfg(windows)'.dependencies] winapi = { workspace = true, features = ["errhandlingapi", "minwindef", "ntdef", "winbase", "winnt"] } diff --git a/ext/ffi/call.rs b/ext/ffi/call.rs index 3572b9e813..bbff0ee48f 100644 --- a/ext/ffi/call.rs +++ b/ext/ffi/call.rs @@ -7,23 +7,38 @@ use crate::symbol::NativeType; use crate::symbol::Symbol; use crate::FfiPermissions; use crate::ForeignFunction; -use deno_core::anyhow::anyhow; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::serde_json::Value; +use deno_core::serde_v8::BigInt as V8BigInt; use deno_core::serde_v8::ExternalPointer; use deno_core::unsync::spawn_blocking; use deno_core::v8; use deno_core::OpState; use deno_core::ResourceId; use libffi::middle::Arg; +use num_bigint::BigInt; use serde::Serialize; use std::cell::RefCell; use std::ffi::c_void; use std::future::Future; use std::rc::Rc; +#[derive(Debug, thiserror::Error)] +pub enum CallError { + #[error(transparent)] + IR(#[from] IRError), + #[error("Nonblocking FFI call failed: {0}")] + NonblockingCallFailure(#[source] tokio::task::JoinError), + #[error("Invalid FFI symbol name: '{0}'")] + InvalidSymbol(String), + #[error(transparent)] + Permission(#[from] deno_permissions::PermissionCheckError), + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + Callback(#[from] super::CallbackError), +} + // SAFETY: Makes an FFI call unsafe fn ffi_call_rtype_struct( cif: &libffi::middle::Cif, @@ -45,7 +60,7 @@ pub(crate) fn ffi_call_sync<'scope>( args: v8::FunctionCallbackArguments, symbol: &Symbol, out_buffer: Option, -) -> Result +) -> Result where 'scope: 'scope, { @@ -191,6 +206,7 @@ where #[serde(untagged)] pub enum FfiValue { Value(Value), + BigInt(V8BigInt), External(ExternalPointer), } @@ -201,7 +217,7 @@ fn ffi_call( parameter_types: &[NativeType], result_type: NativeType, out_buffer: Option, -) -> Result { +) -> FfiValue { let call_args: Vec = call_args .iter() .enumerate() @@ -214,7 +230,7 @@ fn ffi_call( // SAFETY: types in the `Cif` match the actual calling convention and // types of symbol. unsafe { - Ok(match result_type { + match result_type { NativeType::Void => { cif.call::<()>(fun_ptr, &call_args); FfiValue::Value(Value::from(())) @@ -240,18 +256,18 @@ fn ffi_call( NativeType::I32 => { FfiValue::Value(Value::from(cif.call::(fun_ptr, &call_args))) } - NativeType::U64 => { - FfiValue::Value(Value::from(cif.call::(fun_ptr, &call_args))) - } - NativeType::I64 => { - FfiValue::Value(Value::from(cif.call::(fun_ptr, &call_args))) - } - NativeType::USize => { - FfiValue::Value(Value::from(cif.call::(fun_ptr, &call_args))) - } - NativeType::ISize => { - FfiValue::Value(Value::from(cif.call::(fun_ptr, &call_args))) - } + NativeType::U64 => FfiValue::BigInt(V8BigInt::from(BigInt::from( + cif.call::(fun_ptr, &call_args), + ))), + NativeType::I64 => FfiValue::BigInt(V8BigInt::from(BigInt::from( + cif.call::(fun_ptr, &call_args), + ))), + NativeType::USize => FfiValue::BigInt(V8BigInt::from(BigInt::from( + cif.call::(fun_ptr, &call_args), + ))), + NativeType::ISize => FfiValue::BigInt(V8BigInt::from(BigInt::from( + cif.call::(fun_ptr, &call_args), + ))), NativeType::F32 => { FfiValue::Value(Value::from(cif.call::(fun_ptr, &call_args))) } @@ -267,7 +283,7 @@ fn ffi_call( ffi_call_rtype_struct(cif, &fun_ptr, call_args, out_buffer.unwrap().0); FfiValue::Value(Value::Null) } - }) + } } } @@ -280,7 +296,7 @@ pub fn op_ffi_call_ptr_nonblocking( #[serde] def: ForeignFunction, parameters: v8::Local, out_buffer: Option>, -) -> Result>, AnyError> +) -> Result>, CallError> where FP: FfiPermissions + 'static, { @@ -309,7 +325,7 @@ where Ok(async move { let result = join_handle .await - .map_err(|err| anyhow!("Nonblocking FFI call failed: {}", err))??; + .map_err(CallError::NonblockingCallFailure)?; // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. Ok(result) }) @@ -325,16 +341,17 @@ pub fn op_ffi_call_nonblocking( #[string] symbol: String, parameters: v8::Local, out_buffer: Option>, -) -> Result>, AnyError> { +) -> Result>, CallError> { let symbol = { let state = state.borrow(); - let resource = state.resource_table.get::(rid)?; + let resource = state + .resource_table + .get::(rid) + .map_err(CallError::Resource)?; let symbols = &resource.symbols; *symbols .get(&symbol) - .ok_or_else(|| { - type_error(format!("Invalid FFI symbol name: '{symbol}'")) - })? + .ok_or_else(|| CallError::InvalidSymbol(symbol))? .clone() }; @@ -362,7 +379,7 @@ pub fn op_ffi_call_nonblocking( Ok(async move { let result = join_handle .await - .map_err(|err| anyhow!("Nonblocking FFI call failed: {}", err))??; + .map_err(CallError::NonblockingCallFailure)?; // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. Ok(result) }) @@ -377,7 +394,7 @@ pub fn op_ffi_call_ptr( #[serde] def: ForeignFunction, parameters: v8::Local, out_buffer: Option>, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -399,7 +416,7 @@ where &def.parameters, def.result.clone(), out_buffer_ptr, - )?; + ); // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. Ok(result) } diff --git a/ext/ffi/callback.rs b/ext/ffi/callback.rs index 6fa166f52b..29583c800c 100644 --- a/ext/ffi/callback.rs +++ b/ext/ffi/callback.rs @@ -3,7 +3,6 @@ use crate::symbol::NativeType; use crate::FfiPermissions; use crate::ForeignFunction; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::v8; use deno_core::v8::TryCatch; @@ -34,6 +33,16 @@ thread_local! { static LOCAL_THREAD_ID: RefCell = const { RefCell::new(0) }; } +#[derive(Debug, thiserror::Error)] +pub enum CallbackError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + Permission(#[from] deno_permissions::PermissionCheckError), + #[error(transparent)] + Other(deno_core::error::AnyError), +} + #[derive(Clone)] pub struct PtrSymbol { pub cif: libffi::middle::Cif, @@ -44,7 +53,7 @@ impl PtrSymbol { pub fn new( fn_ptr: *mut c_void, def: &ForeignFunction, - ) -> Result { + ) -> Result { let ptr = libffi::middle::CodePtr::from_ptr(fn_ptr as _); let cif = libffi::middle::Cif::new( def @@ -52,8 +61,13 @@ impl PtrSymbol { .clone() .into_iter() .map(libffi::middle::Type::try_from) - .collect::, _>>()?, - def.result.clone().try_into()?, + .collect::, _>>() + .map_err(CallbackError::Other)?, + def + .result + .clone() + .try_into() + .map_err(CallbackError::Other)?, ); Ok(Self { cif, ptr }) @@ -522,10 +536,12 @@ unsafe fn do_ffi_callback( pub fn op_ffi_unsafe_callback_ref( state: Rc>, #[smi] rid: ResourceId, -) -> Result>, AnyError> { +) -> Result, CallbackError> { let state = state.borrow(); - let callback_resource = - state.resource_table.get::(rid)?; + let callback_resource = state + .resource_table + .get::(rid) + .map_err(CallbackError::Resource)?; Ok(async move { let info: &mut CallbackInfo = @@ -536,7 +552,6 @@ pub fn op_ffi_unsafe_callback_ref( .into_future() .or_cancel(callback_resource.cancel.clone()) .await; - Ok(()) }) } @@ -552,7 +567,7 @@ pub fn op_ffi_unsafe_callback_create( scope: &mut v8::HandleScope<'scope>, #[serde] args: RegisterCallbackArgs, cb: v8::Local, -) -> Result, AnyError> +) -> Result, CallbackError> where FP: FfiPermissions + 'static, { @@ -593,8 +608,10 @@ where .parameters .into_iter() .map(libffi::middle::Type::try_from) - .collect::, _>>()?, - libffi::middle::Type::try_from(args.result)?, + .collect::, _>>() + .map_err(CallbackError::Other)?, + libffi::middle::Type::try_from(args.result) + .map_err(CallbackError::Other)?, ); // SAFETY: CallbackInfo is leaked, is not null and stays valid as long as the callback exists. @@ -624,14 +641,16 @@ pub fn op_ffi_unsafe_callback_close( state: &mut OpState, scope: &mut v8::HandleScope, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { +) -> Result<(), CallbackError> { // SAFETY: This drops the closure and the callback info associated with it. // Any retained function pointers to the closure become dangling pointers. // It is up to the user to know that it is safe to call the `close()` on the // UnsafeCallback instance. unsafe { - let callback_resource = - state.resource_table.take::(rid)?; + let callback_resource = state + .resource_table + .take::(rid) + .map_err(CallbackError::Resource)?; let info = Box::from_raw(callback_resource.info); let _ = v8::Global::from_raw(scope, info.callback); let _ = v8::Global::from_raw(scope, info.context); diff --git a/ext/ffi/dlfcn.rs b/ext/ffi/dlfcn.rs index 10199bf858..55909468f8 100644 --- a/ext/ffi/dlfcn.rs +++ b/ext/ffi/dlfcn.rs @@ -6,8 +6,6 @@ use crate::symbol::Symbol; use crate::turbocall; use crate::turbocall::Turbocall; use crate::FfiPermissions; -use deno_core::error::generic_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::v8; use deno_core::GarbageCollected; @@ -21,6 +19,22 @@ use std::collections::HashMap; use std::ffi::c_void; use std::rc::Rc; +#[derive(Debug, thiserror::Error)] +pub enum DlfcnError { + #[error("Failed to register symbol {symbol}: {error}")] + RegisterSymbol { + symbol: String, + #[source] + error: dlopen2::Error, + }, + #[error(transparent)] + Dlopen(#[from] dlopen2::Error), + #[error(transparent)] + Permission(#[from] deno_permissions::PermissionCheckError), + #[error(transparent)] + Other(deno_core::error::AnyError), +} + pub struct DynamicLibraryResource { lib: Library, pub symbols: HashMap>, @@ -37,7 +51,7 @@ impl Resource for DynamicLibraryResource { } impl DynamicLibraryResource { - pub fn get_static(&self, symbol: String) -> Result<*mut c_void, AnyError> { + pub fn get_static(&self, symbol: String) -> Result<*mut c_void, DlfcnError> { // By default, Err returned by this function does not tell // which symbol wasn't exported. So we'll modify the error // message to include the name of symbol. @@ -45,9 +59,7 @@ impl DynamicLibraryResource { // SAFETY: The obtained T symbol is the size of a pointer. match unsafe { self.lib.symbol::<*mut c_void>(&symbol) } { Ok(value) => Ok(Ok(value)), - Err(err) => Err(generic_error(format!( - "Failed to register symbol {symbol}: {err}" - ))), + Err(error) => Err(DlfcnError::RegisterSymbol { symbol, error }), }? } } @@ -116,7 +128,7 @@ pub fn op_ffi_load<'scope, FP>( scope: &mut v8::HandleScope<'scope>, state: &mut OpState, #[serde] args: FfiLoadArgs, -) -> Result, AnyError> +) -> Result, DlfcnError> where FP: FfiPermissions + 'static, { @@ -152,15 +164,16 @@ where // SAFETY: The obtained T symbol is the size of a pointer. match unsafe { resource.lib.symbol::<*const c_void>(symbol) } { Ok(value) => Ok(value), - Err(err) => if foreign_fn.optional { + Err(error) => if foreign_fn.optional { let null: v8::Local = v8::null(scope).into(); let func_key = v8::String::new(scope, &symbol_key).unwrap(); obj.set(scope, func_key.into(), null); break 'register_symbol; } else { - Err(generic_error(format!( - "Failed to register symbol {symbol}: {err}" - ))) + Err(DlfcnError::RegisterSymbol { + symbol: symbol.to_owned(), + error, + }) }, }?; @@ -171,8 +184,13 @@ where .clone() .into_iter() .map(libffi::middle::Type::try_from) - .collect::, _>>()?, - foreign_fn.result.clone().try_into()?, + .collect::, _>>() + .map_err(DlfcnError::Other)?, + foreign_fn + .result + .clone() + .try_into() + .map_err(DlfcnError::Other)?, ); let func_key = v8::String::new(scope, &symbol_key).unwrap(); diff --git a/ext/ffi/ir.rs b/ext/ffi/ir.rs index ebf64945b4..2e80842166 100644 --- a/ext/ffi/ir.rs +++ b/ext/ffi/ir.rs @@ -1,13 +1,55 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::symbol::NativeType; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::v8; use libffi::middle::Arg; use std::ffi::c_void; use std::ptr; +#[derive(Debug, thiserror::Error)] +pub enum IRError { + #[error("Invalid FFI u8 type, expected boolean")] + InvalidU8ExpectedBoolean, + #[error("Invalid FFI u8 type, expected unsigned integer")] + InvalidU8ExpectedUnsignedInteger, + #[error("Invalid FFI i8 type, expected integer")] + InvalidI8, + #[error("Invalid FFI u16 type, expected unsigned integer")] + InvalidU16, + #[error("Invalid FFI i16 type, expected integer")] + InvalidI16, + #[error("Invalid FFI u32 type, expected unsigned integer")] + InvalidU32, + #[error("Invalid FFI i32 type, expected integer")] + InvalidI32, + #[error("Invalid FFI u64 type, expected unsigned integer")] + InvalidU64, + #[error("Invalid FFI i64 type, expected integer")] + InvalidI64, + #[error("Invalid FFI usize type, expected unsigned integer")] + InvalidUsize, + #[error("Invalid FFI isize type, expected integer")] + InvalidIsize, + #[error("Invalid FFI f32 type, expected number")] + InvalidF32, + #[error("Invalid FFI f64 type, expected number")] + InvalidF64, + #[error("Invalid FFI pointer type, expected null, or External")] + InvalidPointerType, + #[error( + "Invalid FFI buffer type, expected null, ArrayBuffer, or ArrayBufferView" + )] + InvalidBufferType, + #[error("Invalid FFI ArrayBufferView, expected data in the buffer")] + InvalidArrayBufferView, + #[error("Invalid FFI ArrayBuffer, expected data in buffer")] + InvalidArrayBuffer, + #[error("Invalid FFI struct type, expected ArrayBuffer, or ArrayBufferView")] + InvalidStructType, + #[error("Invalid FFI function type, expected null, or External")] + InvalidFunctionType, +} + pub struct OutBuffer(pub *mut u8); // SAFETY: OutBuffer is allocated by us in 00_ffi.js and is guaranteed to be @@ -126,9 +168,9 @@ unsafe impl Send for NativeValue {} #[inline] pub fn ffi_parse_bool_arg( arg: v8::Local, -) -> Result { +) -> Result { let bool_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI u8 type, expected boolean"))? + .map_err(|_| IRError::InvalidU8ExpectedBoolean)? .is_true(); Ok(NativeValue { bool_value }) } @@ -136,9 +178,9 @@ pub fn ffi_parse_bool_arg( #[inline] pub fn ffi_parse_u8_arg( arg: v8::Local, -) -> Result { +) -> Result { let u8_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI u8 type, expected unsigned integer"))? + .map_err(|_| IRError::InvalidU8ExpectedUnsignedInteger)? .value() as u8; Ok(NativeValue { u8_value }) } @@ -146,9 +188,9 @@ pub fn ffi_parse_u8_arg( #[inline] pub fn ffi_parse_i8_arg( arg: v8::Local, -) -> Result { +) -> Result { let i8_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI i8 type, expected integer"))? + .map_err(|_| IRError::InvalidI8)? .value() as i8; Ok(NativeValue { i8_value }) } @@ -156,9 +198,9 @@ pub fn ffi_parse_i8_arg( #[inline] pub fn ffi_parse_u16_arg( arg: v8::Local, -) -> Result { +) -> Result { let u16_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI u16 type, expected unsigned integer"))? + .map_err(|_| IRError::InvalidU16)? .value() as u16; Ok(NativeValue { u16_value }) } @@ -166,9 +208,9 @@ pub fn ffi_parse_u16_arg( #[inline] pub fn ffi_parse_i16_arg( arg: v8::Local, -) -> Result { +) -> Result { let i16_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI i16 type, expected integer"))? + .map_err(|_| IRError::InvalidI16)? .value() as i16; Ok(NativeValue { i16_value }) } @@ -176,9 +218,9 @@ pub fn ffi_parse_i16_arg( #[inline] pub fn ffi_parse_u32_arg( arg: v8::Local, -) -> Result { +) -> Result { let u32_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI u32 type, expected unsigned integer"))? + .map_err(|_| IRError::InvalidU32)? .value(); Ok(NativeValue { u32_value }) } @@ -186,9 +228,9 @@ pub fn ffi_parse_u32_arg( #[inline] pub fn ffi_parse_i32_arg( arg: v8::Local, -) -> Result { +) -> Result { let i32_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI i32 type, expected integer"))? + .map_err(|_| IRError::InvalidI32)? .value(); Ok(NativeValue { i32_value }) } @@ -197,7 +239,7 @@ pub fn ffi_parse_i32_arg( pub fn ffi_parse_u64_arg( scope: &mut v8::HandleScope, arg: v8::Local, -) -> Result { +) -> Result { // Order of checking: // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. // 2. Number: Common, supported by Fast API, so let that be the optimal case. @@ -207,9 +249,7 @@ pub fn ffi_parse_u64_arg( } else if let Ok(value) = v8::Local::::try_from(arg) { value.integer_value(scope).unwrap() as u64 } else { - return Err(type_error( - "Invalid FFI u64 type, expected unsigned integer", - )); + return Err(IRError::InvalidU64); }; Ok(NativeValue { u64_value }) } @@ -218,7 +258,7 @@ pub fn ffi_parse_u64_arg( pub fn ffi_parse_i64_arg( scope: &mut v8::HandleScope, arg: v8::Local, -) -> Result { +) -> Result { // Order of checking: // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. // 2. Number: Common, supported by Fast API, so let that be the optimal case. @@ -228,7 +268,7 @@ pub fn ffi_parse_i64_arg( } else if let Ok(value) = v8::Local::::try_from(arg) { value.integer_value(scope).unwrap() } else { - return Err(type_error("Invalid FFI i64 type, expected integer")); + return Err(IRError::InvalidI64); }; Ok(NativeValue { i64_value }) } @@ -237,7 +277,7 @@ pub fn ffi_parse_i64_arg( pub fn ffi_parse_usize_arg( scope: &mut v8::HandleScope, arg: v8::Local, -) -> Result { +) -> Result { // Order of checking: // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. // 2. Number: Common, supported by Fast API, so let that be the optimal case. @@ -247,7 +287,7 @@ pub fn ffi_parse_usize_arg( } else if let Ok(value) = v8::Local::::try_from(arg) { value.integer_value(scope).unwrap() as usize } else { - return Err(type_error("Invalid FFI usize type, expected integer")); + return Err(IRError::InvalidUsize); }; Ok(NativeValue { usize_value }) } @@ -256,7 +296,7 @@ pub fn ffi_parse_usize_arg( pub fn ffi_parse_isize_arg( scope: &mut v8::HandleScope, arg: v8::Local, -) -> Result { +) -> Result { // Order of checking: // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. // 2. Number: Common, supported by Fast API, so let that be the optimal case. @@ -266,7 +306,7 @@ pub fn ffi_parse_isize_arg( } else if let Ok(value) = v8::Local::::try_from(arg) { value.integer_value(scope).unwrap() as isize } else { - return Err(type_error("Invalid FFI isize type, expected integer")); + return Err(IRError::InvalidIsize); }; Ok(NativeValue { isize_value }) } @@ -274,9 +314,9 @@ pub fn ffi_parse_isize_arg( #[inline] pub fn ffi_parse_f32_arg( arg: v8::Local, -) -> Result { +) -> Result { let f32_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI f32 type, expected number"))? + .map_err(|_| IRError::InvalidF32)? .value() as f32; Ok(NativeValue { f32_value }) } @@ -284,9 +324,9 @@ pub fn ffi_parse_f32_arg( #[inline] pub fn ffi_parse_f64_arg( arg: v8::Local, -) -> Result { +) -> Result { let f64_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI f64 type, expected number"))? + .map_err(|_| IRError::InvalidF64)? .value(); Ok(NativeValue { f64_value }) } @@ -295,15 +335,13 @@ pub fn ffi_parse_f64_arg( pub fn ffi_parse_pointer_arg( _scope: &mut v8::HandleScope, arg: v8::Local, -) -> Result { +) -> Result { let pointer = if let Ok(value) = v8::Local::::try_from(arg) { value.value() } else if arg.is_null() { ptr::null_mut() } else { - return Err(type_error( - "Invalid FFI pointer type, expected null, or External", - )); + return Err(IRError::InvalidPointerType); }; Ok(NativeValue { pointer }) } @@ -312,7 +350,7 @@ pub fn ffi_parse_pointer_arg( pub fn ffi_parse_buffer_arg( scope: &mut v8::HandleScope, arg: v8::Local, -) -> Result { +) -> Result { // Order of checking: // 1. ArrayBuffer: Fairly common and not supported by Fast API, optimise this case. // 2. ArrayBufferView: Common and supported by Fast API @@ -328,9 +366,7 @@ pub fn ffi_parse_buffer_arg( let byte_offset = value.byte_offset(); let pointer = value .buffer(scope) - .ok_or_else(|| { - type_error("Invalid FFI ArrayBufferView, expected data in the buffer") - })? + .ok_or(IRError::InvalidArrayBufferView)? .data(); if let Some(non_null) = pointer { // SAFETY: Pointer is non-null, and V8 guarantees that the byte_offset @@ -342,9 +378,7 @@ pub fn ffi_parse_buffer_arg( } else if arg.is_null() { ptr::null_mut() } else { - return Err(type_error( - "Invalid FFI buffer type, expected null, ArrayBuffer, or ArrayBufferView", - )); + return Err(IRError::InvalidBufferType); }; Ok(NativeValue { pointer }) } @@ -353,7 +387,7 @@ pub fn ffi_parse_buffer_arg( pub fn ffi_parse_struct_arg( scope: &mut v8::HandleScope, arg: v8::Local, -) -> Result { +) -> Result { // Order of checking: // 1. ArrayBuffer: Fairly common and not supported by Fast API, optimise this case. // 2. ArrayBufferView: Common and supported by Fast API @@ -362,31 +396,23 @@ pub fn ffi_parse_struct_arg( if let Some(non_null) = value.data() { non_null.as_ptr() } else { - return Err(type_error( - "Invalid FFI ArrayBuffer, expected data in buffer", - )); + return Err(IRError::InvalidArrayBuffer); } } else if let Ok(value) = v8::Local::::try_from(arg) { let byte_offset = value.byte_offset(); let pointer = value .buffer(scope) - .ok_or_else(|| { - type_error("Invalid FFI ArrayBufferView, expected data in the buffer") - })? + .ok_or(IRError::InvalidArrayBufferView)? .data(); if let Some(non_null) = pointer { // SAFETY: Pointer is non-null, and V8 guarantees that the byte_offset // is within the buffer backing store. unsafe { non_null.as_ptr().add(byte_offset) } } else { - return Err(type_error( - "Invalid FFI ArrayBufferView, expected data in buffer", - )); + return Err(IRError::InvalidArrayBufferView); } } else { - return Err(type_error( - "Invalid FFI struct type, expected ArrayBuffer, or ArrayBufferView", - )); + return Err(IRError::InvalidStructType); }; Ok(NativeValue { pointer }) } @@ -395,15 +421,13 @@ pub fn ffi_parse_struct_arg( pub fn ffi_parse_function_arg( _scope: &mut v8::HandleScope, arg: v8::Local, -) -> Result { +) -> Result { let pointer = if let Ok(value) = v8::Local::::try_from(arg) { value.value() } else if arg.is_null() { ptr::null_mut() } else { - return Err(type_error( - "Invalid FFI function type, expected null, or External", - )); + return Err(IRError::InvalidFunctionType); }; Ok(NativeValue { pointer }) } @@ -412,7 +436,7 @@ pub fn ffi_parse_args<'scope>( scope: &mut v8::HandleScope<'scope>, args: v8::Local, parameter_types: &[NativeType], -) -> Result, AnyError> +) -> Result, IRError> where 'scope: 'scope, { diff --git a/ext/ffi/lib.rs b/ext/ffi/lib.rs index 77ec3c85e3..73ec7757ab 100644 --- a/ext/ffi/lib.rs +++ b/ext/ffi/lib.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; - use std::mem::size_of; use std::os::raw::c_char; use std::os::raw::c_short; @@ -29,6 +27,14 @@ use repr::*; use symbol::NativeType; use symbol::Symbol; +pub use call::CallError; +pub use callback::CallbackError; +use deno_permissions::PermissionCheckError; +pub use dlfcn::DlfcnError; +pub use ir::IRError; +pub use r#static::StaticError; +pub use repr::ReprError; + #[cfg(not(target_pointer_width = "64"))] compile_error!("platform not supported"); @@ -41,17 +47,17 @@ const _: () = { pub const UNSTABLE_FEATURE_NAME: &str = "ffi"; pub trait FfiPermissions { - fn check_partial_no_path(&mut self) -> Result<(), AnyError>; + fn check_partial_no_path(&mut self) -> Result<(), PermissionCheckError>; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_partial_with_path( &mut self, path: &str, - ) -> Result; + ) -> Result; } impl FfiPermissions for deno_permissions::PermissionsContainer { #[inline(always)] - fn check_partial_no_path(&mut self) -> Result<(), AnyError> { + fn check_partial_no_path(&mut self) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_ffi_partial_no_path(self) } @@ -59,7 +65,7 @@ impl FfiPermissions for deno_permissions::PermissionsContainer { fn check_partial_with_path( &mut self, path: &str, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_ffi_partial_with_path( self, path, ) diff --git a/ext/ffi/repr.rs b/ext/ffi/repr.rs index 315e6d53bc..fd8a2c8e70 100644 --- a/ext/ffi/repr.rs +++ b/ext/ffi/repr.rs @@ -1,9 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::FfiPermissions; -use deno_core::error::range_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::v8; use deno_core::OpState; @@ -12,11 +9,51 @@ use std::ffi::c_void; use std::ffi::CStr; use std::ptr; +#[derive(Debug, thiserror::Error)] +pub enum ReprError { + #[error("Invalid pointer to offset, pointer is null")] + InvalidOffset, + #[error("Invalid ArrayBuffer pointer, pointer is null")] + InvalidArrayBuffer, + #[error("Destination length is smaller than source length")] + DestinationLengthTooShort, + #[error("Invalid CString pointer, pointer is null")] + InvalidCString, + #[error("Invalid CString pointer, string exceeds max length")] + CStringTooLong, + #[error("Invalid bool pointer, pointer is null")] + InvalidBool, + #[error("Invalid u8 pointer, pointer is null")] + InvalidU8, + #[error("Invalid i8 pointer, pointer is null")] + InvalidI8, + #[error("Invalid u16 pointer, pointer is null")] + InvalidU16, + #[error("Invalid i16 pointer, pointer is null")] + InvalidI16, + #[error("Invalid u32 pointer, pointer is null")] + InvalidU32, + #[error("Invalid i32 pointer, pointer is null")] + InvalidI32, + #[error("Invalid u64 pointer, pointer is null")] + InvalidU64, + #[error("Invalid i64 pointer, pointer is null")] + InvalidI64, + #[error("Invalid f32 pointer, pointer is null")] + InvalidF32, + #[error("Invalid f64 pointer, pointer is null")] + InvalidF64, + #[error("Invalid pointer pointer, pointer is null")] + InvalidPointer, + #[error(transparent)] + Permission(#[from] deno_permissions::PermissionCheckError), +} + #[op2(fast)] pub fn op_ffi_ptr_create( state: &mut OpState, #[bigint] ptr_number: usize, -) -> Result<*mut c_void, AnyError> +) -> Result<*mut c_void, ReprError> where FP: FfiPermissions + 'static, { @@ -31,7 +68,7 @@ pub fn op_ffi_ptr_equals( state: &mut OpState, a: *const c_void, b: *const c_void, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -45,7 +82,7 @@ where pub fn op_ffi_ptr_of( state: &mut OpState, #[anybuffer] buf: *const u8, -) -> Result<*mut c_void, AnyError> +) -> Result<*mut c_void, ReprError> where FP: FfiPermissions + 'static, { @@ -59,7 +96,7 @@ where pub fn op_ffi_ptr_of_exact( state: &mut OpState, buf: v8::Local, -) -> Result<*mut c_void, AnyError> +) -> Result<*mut c_void, ReprError> where FP: FfiPermissions + 'static, { @@ -80,7 +117,7 @@ pub fn op_ffi_ptr_offset( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result<*mut c_void, AnyError> +) -> Result<*mut c_void, ReprError> where FP: FfiPermissions + 'static, { @@ -88,7 +125,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid pointer to offset, pointer is null")); + return Err(ReprError::InvalidOffset); } // TODO(mmastrac): Create a RawPointer that can safely do pointer math. @@ -110,7 +147,7 @@ unsafe extern "C" fn noop_deleter_callback( pub fn op_ffi_ptr_value( state: &mut OpState, ptr: *mut c_void, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -127,7 +164,7 @@ pub fn op_ffi_get_buf( ptr: *mut c_void, #[number] offset: isize, #[number] len: usize, -) -> Result, AnyError> +) -> Result, ReprError> where FP: FfiPermissions + 'static, { @@ -135,7 +172,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid ArrayBuffer pointer, pointer is null")); + return Err(ReprError::InvalidArrayBuffer); } // SAFETY: Trust the user to have provided a real pointer, offset, and a valid matching size to it. Since this is a foreign pointer, we should not do any deletion. @@ -144,7 +181,7 @@ where ptr.offset(offset), len, noop_deleter_callback, - std::ptr::null_mut(), + ptr::null_mut(), ) } .make_shared(); @@ -159,7 +196,7 @@ pub fn op_ffi_buf_copy_into( #[number] offset: isize, #[anybuffer] dst: &mut [u8], #[number] len: usize, -) -> Result<(), AnyError> +) -> Result<(), ReprError> where FP: FfiPermissions + 'static, { @@ -167,11 +204,9 @@ where permissions.check_partial_no_path()?; if src.is_null() { - Err(type_error("Invalid ArrayBuffer pointer, pointer is null")) + Err(ReprError::InvalidArrayBuffer) } else if dst.len() < len { - Err(range_error( - "Destination length is smaller than source length", - )) + Err(ReprError::DestinationLengthTooShort) } else { let src = src as *const c_void; @@ -190,7 +225,7 @@ pub fn op_ffi_cstr_read( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result, AnyError> +) -> Result, ReprError> where FP: FfiPermissions + 'static, { @@ -198,16 +233,14 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid CString pointer, pointer is null")); + return Err(ReprError::InvalidCString); } let cstr = // SAFETY: Pointer and offset are user provided. unsafe { CStr::from_ptr(ptr.offset(offset) as *const c_char) }.to_bytes(); let value = v8::String::new_from_utf8(scope, cstr, v8::NewStringType::Normal) - .ok_or_else(|| { - type_error("Invalid CString pointer, string exceeds max length") - })?; + .ok_or_else(|| ReprError::CStringTooLong)?; Ok(value) } @@ -216,7 +249,7 @@ pub fn op_ffi_read_bool( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -224,7 +257,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid bool pointer, pointer is null")); + return Err(ReprError::InvalidBool); } // SAFETY: ptr and offset are user provided. @@ -236,7 +269,7 @@ pub fn op_ffi_read_u8( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -244,7 +277,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid u8 pointer, pointer is null")); + return Err(ReprError::InvalidU8); } // SAFETY: ptr and offset are user provided. @@ -258,7 +291,7 @@ pub fn op_ffi_read_i8( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -266,7 +299,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid i8 pointer, pointer is null")); + return Err(ReprError::InvalidI8); } // SAFETY: ptr and offset are user provided. @@ -280,7 +313,7 @@ pub fn op_ffi_read_u16( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -288,7 +321,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid u16 pointer, pointer is null")); + return Err(ReprError::InvalidU16); } // SAFETY: ptr and offset are user provided. @@ -302,7 +335,7 @@ pub fn op_ffi_read_i16( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -310,7 +343,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid i16 pointer, pointer is null")); + return Err(ReprError::InvalidI16); } // SAFETY: ptr and offset are user provided. @@ -324,7 +357,7 @@ pub fn op_ffi_read_u32( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -332,7 +365,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid u32 pointer, pointer is null")); + return Err(ReprError::InvalidU32); } // SAFETY: ptr and offset are user provided. @@ -344,7 +377,7 @@ pub fn op_ffi_read_i32( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -352,7 +385,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid i32 pointer, pointer is null")); + return Err(ReprError::InvalidI32); } // SAFETY: ptr and offset are user provided. @@ -367,7 +400,7 @@ pub fn op_ffi_read_u64( // Note: The representation of 64-bit integers is function-wide. We cannot // choose to take this parameter as a number while returning a bigint. #[bigint] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -375,7 +408,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid u64 pointer, pointer is null")); + return Err(ReprError::InvalidU64); } let value = @@ -393,7 +426,7 @@ pub fn op_ffi_read_i64( // Note: The representation of 64-bit integers is function-wide. We cannot // choose to take this parameter as a number while returning a bigint. #[bigint] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -401,7 +434,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid i64 pointer, pointer is null")); + return Err(ReprError::InvalidI64); } let value = @@ -416,7 +449,7 @@ pub fn op_ffi_read_f32( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -424,7 +457,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid f32 pointer, pointer is null")); + return Err(ReprError::InvalidF32); } // SAFETY: ptr and offset are user provided. @@ -436,7 +469,7 @@ pub fn op_ffi_read_f64( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result +) -> Result where FP: FfiPermissions + 'static, { @@ -444,7 +477,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid f64 pointer, pointer is null")); + return Err(ReprError::InvalidF64); } // SAFETY: ptr and offset are user provided. @@ -456,7 +489,7 @@ pub fn op_ffi_read_ptr( state: &mut OpState, ptr: *mut c_void, #[number] offset: isize, -) -> Result<*mut c_void, AnyError> +) -> Result<*mut c_void, ReprError> where FP: FfiPermissions + 'static, { @@ -464,7 +497,7 @@ where permissions.check_partial_no_path()?; if ptr.is_null() { - return Err(type_error("Invalid pointer pointer, pointer is null")); + return Err(ReprError::InvalidPointer); } // SAFETY: ptr and offset are user provided. diff --git a/ext/ffi/static.rs b/ext/ffi/static.rs index f08605754b..61b4059336 100644 --- a/ext/ffi/static.rs +++ b/ext/ffi/static.rs @@ -2,14 +2,24 @@ use crate::dlfcn::DynamicLibraryResource; use crate::symbol::NativeType; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::v8; use deno_core::OpState; use deno_core::ResourceId; use std::ptr; +#[derive(Debug, thiserror::Error)] +pub enum StaticError { + #[error(transparent)] + Dlfcn(super::DlfcnError), + #[error("Invalid FFI static type 'void'")] + InvalidTypeVoid, + #[error("Invalid FFI static type 'struct'")] + InvalidTypeStruct, + #[error(transparent)] + Resource(deno_core::error::AnyError), +} + #[op2] pub fn op_ffi_get_static<'scope>( scope: &mut v8::HandleScope<'scope>, @@ -18,24 +28,27 @@ pub fn op_ffi_get_static<'scope>( #[string] name: String, #[serde] static_type: NativeType, optional: bool, -) -> Result, AnyError> { - let resource = state.resource_table.get::(rid)?; +) -> Result, StaticError> { + let resource = state + .resource_table + .get::(rid) + .map_err(StaticError::Resource)?; let data_ptr = match resource.get_static(name) { - Ok(data_ptr) => Ok(data_ptr), + Ok(data_ptr) => data_ptr, Err(err) => { if optional { let null: v8::Local = v8::null(scope).into(); return Ok(null); } else { - Err(err) + return Err(StaticError::Dlfcn(err)); } } - }?; + }; Ok(match static_type { NativeType::Void => { - return Err(type_error("Invalid FFI static type 'void'")); + return Err(StaticError::InvalidTypeVoid); } NativeType::Bool => { // SAFETY: ptr is user provided @@ -132,7 +145,7 @@ pub fn op_ffi_get_static<'scope>( external } NativeType::Struct(_) => { - return Err(type_error("Invalid FFI static type 'struct'")); + return Err(StaticError::InvalidTypeStruct); } }) } diff --git a/ext/fs/30_fs.js b/ext/fs/30_fs.js index 0ee1ba82c7..fc2b18be13 100644 --- a/ext/fs/30_fs.js +++ b/ext/fs/30_fs.js @@ -346,9 +346,10 @@ const { 0: statStruct, 1: statBuf } = createByteStruct({ mtime: "date", atime: "date", birthtime: "date", + ctime: "date", dev: "u64", ino: "?u64", - mode: "?u64", + mode: "u64", nlink: "?u64", uid: "?u64", gid: "?u64", @@ -377,9 +378,10 @@ function parseFileInfo(response) { birthtime: response.birthtimeSet === true ? new Date(response.birthtime) : null, + ctime: response.ctimeSet === true ? new Date(response.ctime) : null, dev: response.dev, + mode: response.mode, ino: unix ? response.ino : null, - mode: unix ? response.mode : null, nlink: unix ? response.nlink : null, uid: unix ? response.uid : null, gid: unix ? response.gid : null, diff --git a/ext/fs/Cargo.toml b/ext/fs/Cargo.toml index 894b307efa..e85f349b15 100644 --- a/ext/fs/Cargo.toml +++ b/ext/fs/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_fs" -version = "0.78.0" +version = "0.87.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -21,15 +21,17 @@ async-trait.workspace = true base32.workspace = true deno_core.workspace = true deno_io.workspace = true +deno_path_util.workspace = true deno_permissions.workspace = true filetime.workspace = true libc.workspace = true rand.workspace = true rayon = "1.8.0" serde.workspace = true +thiserror.workspace = true [target.'cfg(unix)'.dependencies] -nix.workspace = true +nix = { workspace = true, features = ["fs", "user"] } [target.'cfg(windows)'.dependencies] winapi = { workspace = true, features = ["winbase"] } diff --git a/ext/fs/in_memory_fs.rs b/ext/fs/in_memory_fs.rs index 027539e849..34b77836d9 100644 --- a/ext/fs/in_memory_fs.rs +++ b/ext/fs/in_memory_fs.rs @@ -12,12 +12,12 @@ use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; -use deno_core::normalize_path; use deno_core::parking_lot::Mutex; use deno_io::fs::File; use deno_io::fs::FsError; use deno_io::fs::FsResult; use deno_io::fs::FsStat; +use deno_path_util::normalize_path; use crate::interface::AccessCheckCb; use crate::interface::FsDirEntry; @@ -44,7 +44,7 @@ impl InMemoryFs { pub fn setup_text_files(&self, files: Vec<(String, String)>) { for (path, text) in files { let path = PathBuf::from(path); - self.mkdir_sync(path.parent().unwrap(), true, 0).unwrap(); + self.mkdir_sync(path.parent().unwrap(), true, None).unwrap(); self .write_file_sync( &path, @@ -101,7 +101,7 @@ impl FileSystem for InMemoryFs { &self, path: &Path, recursive: bool, - _mode: u32, + _mode: Option, ) -> FsResult<()> { let path = normalize_path(path); @@ -119,7 +119,7 @@ impl FileSystem for InMemoryFs { }, None => { if recursive { - self.mkdir_sync(parent, true, 0)?; + self.mkdir_sync(parent, true, None)?; } else { return Err(FsError::Io(Error::new( ErrorKind::NotFound, @@ -149,7 +149,7 @@ impl FileSystem for InMemoryFs { &self, path: PathBuf, recursive: bool, - mode: u32, + mode: Option, ) -> FsResult<()> { self.mkdir_sync(&path, recursive, mode) } @@ -229,6 +229,7 @@ impl FileSystem for InMemoryFs { mtime: None, atime: None, birthtime: None, + ctime: None, dev: 0, ino: 0, mode: 0, @@ -251,6 +252,7 @@ impl FileSystem for InMemoryFs { mtime: None, atime: None, birthtime: None, + ctime: None, dev: 0, ino: 0, mode: 0, diff --git a/ext/fs/interface.rs b/ext/fs/interface.rs index af4beb248b..73333b0fd1 100644 --- a/ext/fs/interface.rs +++ b/ext/fs/interface.rs @@ -121,13 +121,17 @@ pub trait FileSystem: std::fmt::Debug + MaybeSend + MaybeSync { access_check: Option>, ) -> FsResult>; - fn mkdir_sync(&self, path: &Path, recursive: bool, mode: u32) - -> FsResult<()>; + fn mkdir_sync( + &self, + path: &Path, + recursive: bool, + mode: Option, + ) -> FsResult<()>; async fn mkdir_async( &self, path: PathBuf, recursive: bool, - mode: u32, + mode: Option, ) -> FsResult<()>; fn chmod_sync(&self, path: &Path, mode: u32) -> FsResult<()>; diff --git a/ext/fs/lib.rs b/ext/fs/lib.rs index bd49078b2e..dd852e6be8 100644 --- a/ext/fs/lib.rs +++ b/ext/fs/lib.rs @@ -14,14 +14,16 @@ pub use crate::interface::FileSystemRc; pub use crate::interface::FsDirEntry; pub use crate::interface::FsFileType; pub use crate::interface::OpenOptions; +pub use crate::ops::FsOpsError; +pub use crate::ops::OperationError; pub use crate::std_fs::RealFs; pub use crate::sync::MaybeSend; pub use crate::sync::MaybeSync; use crate::ops::*; -use deno_core::error::AnyError; use deno_io::fs::FsError; +use deno_permissions::PermissionCheckError; use std::borrow::Cow; use std::path::Path; use std::path::PathBuf; @@ -40,45 +42,51 @@ pub trait FsPermissions { &mut self, path: &str, api_name: &str, - ) -> Result; + ) -> Result; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_read_path<'a>( &mut self, path: &'a Path, api_name: &str, - ) -> Result, AnyError>; - fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>; + ) -> Result, PermissionCheckError>; + fn check_read_all( + &mut self, + api_name: &str, + ) -> Result<(), PermissionCheckError>; fn check_read_blind( &mut self, p: &Path, display: &str, api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result<(), PermissionCheckError>; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write( &mut self, path: &str, api_name: &str, - ) -> Result; + ) -> Result; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write_path<'a>( &mut self, path: &'a Path, api_name: &str, - ) -> Result, AnyError>; + ) -> Result, PermissionCheckError>; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write_partial( &mut self, path: &str, api_name: &str, - ) -> Result; - fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError>; + ) -> Result; + fn check_write_all( + &mut self, + api_name: &str, + ) -> Result<(), PermissionCheckError>; fn check_write_blind( &mut self, p: &Path, display: &str, api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result<(), PermissionCheckError>; fn check<'a>( &mut self, @@ -138,7 +146,7 @@ impl FsPermissions for deno_permissions::PermissionsContainer { &mut self, path: &str, api_name: &str, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_read(self, path, api_name) } @@ -146,7 +154,7 @@ impl FsPermissions for deno_permissions::PermissionsContainer { &mut self, path: &'a Path, api_name: &str, - ) -> Result, AnyError> { + ) -> Result, PermissionCheckError> { deno_permissions::PermissionsContainer::check_read_path( self, path, @@ -158,7 +166,7 @@ impl FsPermissions for deno_permissions::PermissionsContainer { path: &Path, display: &str, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_read_blind( self, path, display, api_name, ) @@ -168,7 +176,7 @@ impl FsPermissions for deno_permissions::PermissionsContainer { &mut self, path: &str, api_name: &str, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_write(self, path, api_name) } @@ -176,7 +184,7 @@ impl FsPermissions for deno_permissions::PermissionsContainer { &mut self, path: &'a Path, api_name: &str, - ) -> Result, AnyError> { + ) -> Result, PermissionCheckError> { deno_permissions::PermissionsContainer::check_write_path( self, path, api_name, ) @@ -186,7 +194,7 @@ impl FsPermissions for deno_permissions::PermissionsContainer { &mut self, path: &str, api_name: &str, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_write_partial( self, path, api_name, ) @@ -197,17 +205,23 @@ impl FsPermissions for deno_permissions::PermissionsContainer { p: &Path, display: &str, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_write_blind( self, p, display, api_name, ) } - fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError> { + fn check_read_all( + &mut self, + api_name: &str, + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_read_all(self, api_name) } - fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError> { + fn check_write_all( + &mut self, + api_name: &str, + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_write_all(self, api_name) } } diff --git a/ext/fs/ops.rs b/ext/fs/ops.rs index 150d3b9555..3d0d96ce66 100644 --- a/ext/fs/ops.rs +++ b/ext/fs/ops.rs @@ -1,6 +1,8 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::cell::RefCell; +use std::error::Error; +use std::fmt::Formatter; use std::io; use std::io::SeekFrom; use std::path::Path; @@ -8,10 +10,12 @@ use std::path::PathBuf; use std::path::StripPrefixError; use std::rc::Rc; -use deno_core::anyhow::bail; -use deno_core::error::custom_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; +use crate::interface::AccessCheckFn; +use crate::interface::FileSystemRc; +use crate::interface::FsDirEntry; +use crate::interface::FsFileType; +use crate::FsPermissions; +use crate::OpenOptions; use deno_core::op2; use deno_core::CancelFuture; use deno_core::CancelHandle; @@ -22,17 +26,72 @@ use deno_core::ToJsBuffer; use deno_io::fs::FileResource; use deno_io::fs::FsError; use deno_io::fs::FsStat; +use deno_permissions::PermissionCheckError; use rand::rngs::ThreadRng; use rand::thread_rng; use rand::Rng; use serde::Serialize; -use crate::interface::AccessCheckFn; -use crate::interface::FileSystemRc; -use crate::interface::FsDirEntry; -use crate::interface::FsFileType; -use crate::FsPermissions; -use crate::OpenOptions; +#[derive(Debug, thiserror::Error)] +pub enum FsOpsError { + #[error("{0}")] + Io(#[source] std::io::Error), + #[error("{0}")] + OperationError(#[source] OperationError), + #[error(transparent)] + Permission(#[from] PermissionCheckError), + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error("File name or path {0:?} is not valid UTF-8")] + InvalidUtf8(std::ffi::OsString), + #[error("{0}")] + StripPrefix(#[from] StripPrefixError), + #[error("{0}")] + Canceled(#[from] deno_core::Canceled), + #[error("Invalid seek mode: {0}")] + InvalidSeekMode(i32), + #[error("Invalid control character in prefix or suffix: {0:?}")] + InvalidControlCharacter(String), + #[error("Invalid character in prefix or suffix: {0:?}")] + InvalidCharacter(String), + #[cfg(windows)] + #[error("Invalid trailing character in suffix")] + InvalidTrailingCharacter, + #[error("Requires {err} access to {path}, {}", print_not_capable_info(*.standalone, .err))] + NotCapableAccess { + // NotCapable + standalone: bool, + err: &'static str, + path: String, + }, + #[error("permission denied: {0}")] + NotCapable(&'static str), // NotCapable + #[error(transparent)] + Other(deno_core::error::AnyError), +} + +impl From for FsOpsError { + fn from(err: FsError) -> Self { + match err { + FsError::Io(err) => FsOpsError::Io(err), + FsError::FileBusy => { + FsOpsError::Other(deno_core::error::resource_unavailable()) + } + FsError::NotSupported => { + FsOpsError::Other(deno_core::error::not_supported()) + } + FsError::NotCapable(err) => FsOpsError::NotCapable(err), + } + } +} + +fn print_not_capable_info(standalone: bool, err: &'static str) -> String { + if standalone { + format!("specify the required permissions during compilation using `deno compile --allow-{err}`") + } else { + format!("run again with the --allow-{err} flag") + } +} fn sync_permission_check<'a, P: FsPermissions + 'static>( permissions: &'a mut P, @@ -58,7 +117,7 @@ fn map_permission_error( operation: &'static str, error: FsError, path: &Path, -) -> AnyError { +) -> FsOpsError { match error { FsError::NotCapable(err) => { let path = format!("{path:?}"); @@ -67,14 +126,12 @@ fn map_permission_error( } else { (path.as_str(), "") }; - let msg = if deno_permissions::is_standalone() { - format!( - "Requires {err} access to {path}{truncated}, specify the required permissions during compilation using `deno compile --allow-{err}`") - } else { - format!( - "Requires {err} access to {path}{truncated}, run again with the --allow-{err} flag") - }; - custom_error("NotCapable", msg) + + FsOpsError::NotCapableAccess { + standalone: deno_permissions::is_standalone(), + err, + path: format!("{path}{truncated}"), + } } err => Err::<(), _>(err) .context_path(operation, path) @@ -85,7 +142,7 @@ fn map_permission_error( #[op2] #[string] -pub fn op_fs_cwd

(state: &mut OpState) -> Result +pub fn op_fs_cwd

(state: &mut OpState) -> Result where P: FsPermissions + 'static, { @@ -102,7 +159,7 @@ where pub fn op_fs_chdir

( state: &mut OpState, #[string] directory: &str, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -119,7 +176,7 @@ where pub fn op_fs_umask( state: &mut OpState, mask: Option, -) -> Result +) -> Result where { state.borrow::().umask(mask).context("umask") @@ -131,7 +188,7 @@ pub fn op_fs_open_sync

( state: &mut OpState, #[string] path: String, #[serde] options: Option, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -158,7 +215,7 @@ pub async fn op_fs_open_async

( state: Rc>, #[string] path: String, #[serde] options: Option, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -186,7 +243,7 @@ pub fn op_fs_mkdir_sync

( #[string] path: String, recursive: bool, mode: Option, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -197,7 +254,7 @@ where .check_write(&path, "Deno.mkdirSync()")?; let fs = state.borrow::(); - fs.mkdir_sync(&path, recursive, mode) + fs.mkdir_sync(&path, recursive, Some(mode)) .context_path("mkdir", &path)?; Ok(()) @@ -209,7 +266,7 @@ pub async fn op_fs_mkdir_async

( #[string] path: String, recursive: bool, mode: Option, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -221,7 +278,7 @@ where (state.borrow::().clone(), path) }; - fs.mkdir_async(path.clone(), recursive, mode) + fs.mkdir_async(path.clone(), recursive, Some(mode)) .await .context_path("mkdir", &path)?; @@ -233,7 +290,7 @@ pub fn op_fs_chmod_sync

( state: &mut OpState, #[string] path: String, mode: u32, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -250,7 +307,7 @@ pub async fn op_fs_chmod_async

( state: Rc>, #[string] path: String, mode: u32, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -271,7 +328,7 @@ pub fn op_fs_chown_sync

( #[string] path: String, uid: Option, gid: Option, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -290,7 +347,7 @@ pub async fn op_fs_chown_async

( #[string] path: String, uid: Option, gid: Option, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -310,7 +367,7 @@ pub fn op_fs_remove_sync

( state: &mut OpState, #[string] path: &str, recursive: bool, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -330,7 +387,7 @@ pub async fn op_fs_remove_async

( state: Rc>, #[string] path: String, recursive: bool, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -361,7 +418,7 @@ pub fn op_fs_copy_file_sync

( state: &mut OpState, #[string] from: &str, #[string] to: &str, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -381,7 +438,7 @@ pub async fn op_fs_copy_file_async

( state: Rc>, #[string] from: String, #[string] to: String, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -405,7 +462,7 @@ pub fn op_fs_stat_sync

( state: &mut OpState, #[string] path: String, #[buffer] stat_out_buf: &mut [u32], -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -424,7 +481,7 @@ where pub async fn op_fs_stat_async

( state: Rc>, #[string] path: String, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -446,7 +503,7 @@ pub fn op_fs_lstat_sync

( state: &mut OpState, #[string] path: String, #[buffer] stat_out_buf: &mut [u32], -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -465,7 +522,7 @@ where pub async fn op_fs_lstat_async

( state: Rc>, #[string] path: String, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -487,7 +544,7 @@ where pub fn op_fs_realpath_sync

( state: &mut OpState, #[string] path: String, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -510,7 +567,7 @@ where pub async fn op_fs_realpath_async

( state: Rc>, #[string] path: String, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -538,7 +595,7 @@ where pub fn op_fs_read_dir_sync

( state: &mut OpState, #[string] path: String, -) -> Result, AnyError> +) -> Result, FsOpsError> where P: FsPermissions + 'static, { @@ -557,7 +614,7 @@ where pub async fn op_fs_read_dir_async

( state: Rc>, #[string] path: String, -) -> Result, AnyError> +) -> Result, FsOpsError> where P: FsPermissions + 'static, { @@ -582,7 +639,7 @@ pub fn op_fs_rename_sync

( state: &mut OpState, #[string] oldpath: String, #[string] newpath: String, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -603,7 +660,7 @@ pub async fn op_fs_rename_async

( state: Rc>, #[string] oldpath: String, #[string] newpath: String, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -628,7 +685,7 @@ pub fn op_fs_link_sync

( state: &mut OpState, #[string] oldpath: &str, #[string] newpath: &str, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -650,7 +707,7 @@ pub async fn op_fs_link_async

( state: Rc>, #[string] oldpath: String, #[string] newpath: String, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -677,7 +734,7 @@ pub fn op_fs_symlink_sync

( #[string] oldpath: &str, #[string] newpath: &str, #[serde] file_type: Option, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -701,7 +758,7 @@ pub async fn op_fs_symlink_async

( #[string] oldpath: String, #[string] newpath: String, #[serde] file_type: Option, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -728,7 +785,7 @@ where pub fn op_fs_read_link_sync

( state: &mut OpState, #[string] path: String, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -748,7 +805,7 @@ where pub async fn op_fs_read_link_async

( state: Rc>, #[string] path: String, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -773,7 +830,7 @@ pub fn op_fs_truncate_sync

( state: &mut OpState, #[string] path: &str, #[number] len: u64, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -793,7 +850,7 @@ pub async fn op_fs_truncate_async

( state: Rc>, #[string] path: String, #[number] len: u64, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -820,7 +877,7 @@ pub fn op_fs_utime_sync

( #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -841,7 +898,7 @@ pub async fn op_fs_utime_async

( #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -871,7 +928,7 @@ pub fn op_fs_make_temp_dir_sync

( #[string] dir_arg: Option, #[string] prefix: Option, #[string] suffix: Option, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -886,7 +943,7 @@ where const MAX_TRIES: u32 = 10; for _ in 0..MAX_TRIES { let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?; - match fs.mkdir_sync(&path, false, 0o700) { + match fs.mkdir_sync(&path, false, Some(0o700)) { Ok(_) => { // PERMISSIONS: ensure the absolute path is not leaked let path = strip_dir_prefix(&dir, dir_arg.as_deref(), path)?; @@ -913,7 +970,7 @@ pub async fn op_fs_make_temp_dir_async

( #[string] dir_arg: Option, #[string] prefix: Option, #[string] suffix: Option, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -928,7 +985,11 @@ where const MAX_TRIES: u32 = 10; for _ in 0..MAX_TRIES { let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?; - match fs.clone().mkdir_async(path.clone(), false, 0o700).await { + match fs + .clone() + .mkdir_async(path.clone(), false, Some(0o700)) + .await + { Ok(_) => { // PERMISSIONS: ensure the absolute path is not leaked let path = strip_dir_prefix(&dir, dir_arg.as_deref(), path)?; @@ -955,7 +1016,7 @@ pub fn op_fs_make_temp_file_sync

( #[string] dir_arg: Option, #[string] prefix: Option, #[string] suffix: Option, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -1003,7 +1064,7 @@ pub async fn op_fs_make_temp_file_async

( #[string] dir_arg: Option, #[string] prefix: Option, #[string] suffix: Option, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -1065,7 +1126,7 @@ fn make_temp_check_sync

( state: &mut OpState, dir: Option<&str>, api_name: &str, -) -> Result<(PathBuf, FileSystemRc), AnyError> +) -> Result<(PathBuf, FileSystemRc), FsOpsError> where P: FsPermissions + 'static, { @@ -1087,7 +1148,7 @@ fn make_temp_check_async

( state: Rc>, dir: Option<&str>, api_name: &str, -) -> Result<(PathBuf, FileSystemRc), AnyError> +) -> Result<(PathBuf, FileSystemRc), FsOpsError> where P: FsPermissions + 'static, { @@ -1112,10 +1173,10 @@ where fn validate_temporary_filename_component( component: &str, #[allow(unused_variables)] suffix: bool, -) -> Result<(), AnyError> { +) -> Result<(), FsOpsError> { // Ban ASCII and Unicode control characters: these will often fail if let Some(c) = component.matches(|c: char| c.is_control()).next() { - bail!("Invalid control character in prefix or suffix: {:?}", c); + return Err(FsOpsError::InvalidControlCharacter(c.to_string())); } // Windows has the most restrictive filenames. As temp files aren't normal files, we just // use this set of banned characters for all platforms because wildcard-like files can also @@ -1131,13 +1192,13 @@ fn validate_temporary_filename_component( .matches(|c: char| "<>:\"/\\|?*".contains(c)) .next() { - bail!("Invalid character in prefix or suffix: {:?}", c); + return Err(FsOpsError::InvalidCharacter(c.to_string())); } // This check is only for Windows #[cfg(windows)] if suffix && component.ends_with(|c: char| ". ".contains(c)) { - bail!("Invalid trailing character in suffix"); + return Err(FsOpsError::InvalidTrailingCharacter); } Ok(()) @@ -1148,7 +1209,7 @@ fn tmp_name( dir: &Path, prefix: Option<&str>, suffix: Option<&str>, -) -> Result { +) -> Result { let prefix = prefix.unwrap_or(""); validate_temporary_filename_component(prefix, false)?; let suffix = suffix.unwrap_or(""); @@ -1175,7 +1236,7 @@ pub fn op_fs_write_file_sync

( create: bool, create_new: bool, #[buffer] data: JsBuffer, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -1203,7 +1264,7 @@ pub async fn op_fs_write_file_async

( create_new: bool, #[buffer] data: JsBuffer, #[smi] cancel_rid: Option, -) -> Result<(), AnyError> +) -> Result<(), FsOpsError> where P: FsPermissions + 'static, { @@ -1251,7 +1312,7 @@ where pub fn op_fs_read_file_sync

( state: &mut OpState, #[string] path: String, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -1273,7 +1334,7 @@ pub async fn op_fs_read_file_async

( state: Rc>, #[string] path: String, #[smi] cancel_rid: Option, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -1314,7 +1375,7 @@ where pub fn op_fs_read_file_text_sync

( state: &mut OpState, #[string] path: String, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -1336,7 +1397,7 @@ pub async fn op_fs_read_file_text_async

( state: Rc>, #[string] path: String, #[smi] cancel_rid: Option, -) -> Result +) -> Result where P: FsPermissions + 'static, { @@ -1373,13 +1434,13 @@ where Ok(str) } -fn to_seek_from(offset: i64, whence: i32) -> Result { +fn to_seek_from(offset: i64, whence: i32) -> Result { let seek_from = match whence { 0 => SeekFrom::Start(offset as u64), 1 => SeekFrom::Current(offset), 2 => SeekFrom::End(offset), _ => { - return Err(type_error(format!("Invalid seek mode: {whence}"))); + return Err(FsOpsError::InvalidSeekMode(whence)); } }; Ok(seek_from) @@ -1392,9 +1453,10 @@ pub fn op_fs_seek_sync( #[smi] rid: ResourceId, #[number] offset: i64, #[smi] whence: i32, -) -> Result { +) -> Result { let pos = to_seek_from(offset, whence)?; - let file = FileResource::get_file(state, rid)?; + let file = + FileResource::get_file(state, rid).map_err(FsOpsError::Resource)?; let cursor = file.seek_sync(pos)?; Ok(cursor) } @@ -1406,9 +1468,10 @@ pub async fn op_fs_seek_async( #[smi] rid: ResourceId, #[number] offset: i64, #[smi] whence: i32, -) -> Result { +) -> Result { let pos = to_seek_from(offset, whence)?; - let file = FileResource::get_file(&state.borrow(), rid)?; + let file = FileResource::get_file(&state.borrow(), rid) + .map_err(FsOpsError::Resource)?; let cursor = file.seek_async(pos).await?; Ok(cursor) } @@ -1417,8 +1480,9 @@ pub async fn op_fs_seek_async( pub fn op_fs_file_sync_data_sync( state: &mut OpState, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { - let file = FileResource::get_file(state, rid)?; +) -> Result<(), FsOpsError> { + let file = + FileResource::get_file(state, rid).map_err(FsOpsError::Resource)?; file.datasync_sync()?; Ok(()) } @@ -1427,8 +1491,9 @@ pub fn op_fs_file_sync_data_sync( pub async fn op_fs_file_sync_data_async( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { - let file = FileResource::get_file(&state.borrow(), rid)?; +) -> Result<(), FsOpsError> { + let file = FileResource::get_file(&state.borrow(), rid) + .map_err(FsOpsError::Resource)?; file.datasync_async().await?; Ok(()) } @@ -1437,8 +1502,9 @@ pub async fn op_fs_file_sync_data_async( pub fn op_fs_file_sync_sync( state: &mut OpState, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { - let file = FileResource::get_file(state, rid)?; +) -> Result<(), FsOpsError> { + let file = + FileResource::get_file(state, rid).map_err(FsOpsError::Resource)?; file.sync_sync()?; Ok(()) } @@ -1447,8 +1513,9 @@ pub fn op_fs_file_sync_sync( pub async fn op_fs_file_sync_async( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { - let file = FileResource::get_file(&state.borrow(), rid)?; +) -> Result<(), FsOpsError> { + let file = FileResource::get_file(&state.borrow(), rid) + .map_err(FsOpsError::Resource)?; file.sync_async().await?; Ok(()) } @@ -1458,8 +1525,9 @@ pub fn op_fs_file_stat_sync( state: &mut OpState, #[smi] rid: ResourceId, #[buffer] stat_out_buf: &mut [u32], -) -> Result<(), AnyError> { - let file = FileResource::get_file(state, rid)?; +) -> Result<(), FsOpsError> { + let file = + FileResource::get_file(state, rid).map_err(FsOpsError::Resource)?; let stat = file.stat_sync()?; let serializable_stat = SerializableStat::from(stat); serializable_stat.write(stat_out_buf); @@ -1471,8 +1539,9 @@ pub fn op_fs_file_stat_sync( pub async fn op_fs_file_stat_async( state: Rc>, #[smi] rid: ResourceId, -) -> Result { - let file = FileResource::get_file(&state.borrow(), rid)?; +) -> Result { + let file = FileResource::get_file(&state.borrow(), rid) + .map_err(FsOpsError::Resource)?; let stat = file.stat_async().await?; Ok(stat.into()) } @@ -1482,8 +1551,9 @@ pub fn op_fs_flock_sync( state: &mut OpState, #[smi] rid: ResourceId, exclusive: bool, -) -> Result<(), AnyError> { - let file = FileResource::get_file(state, rid)?; +) -> Result<(), FsOpsError> { + let file = + FileResource::get_file(state, rid).map_err(FsOpsError::Resource)?; file.lock_sync(exclusive)?; Ok(()) } @@ -1493,8 +1563,9 @@ pub async fn op_fs_flock_async( state: Rc>, #[smi] rid: ResourceId, exclusive: bool, -) -> Result<(), AnyError> { - let file = FileResource::get_file(&state.borrow(), rid)?; +) -> Result<(), FsOpsError> { + let file = FileResource::get_file(&state.borrow(), rid) + .map_err(FsOpsError::Resource)?; file.lock_async(exclusive).await?; Ok(()) } @@ -1503,8 +1574,9 @@ pub async fn op_fs_flock_async( pub fn op_fs_funlock_sync( state: &mut OpState, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { - let file = FileResource::get_file(state, rid)?; +) -> Result<(), FsOpsError> { + let file = + FileResource::get_file(state, rid).map_err(FsOpsError::Resource)?; file.unlock_sync()?; Ok(()) } @@ -1513,8 +1585,9 @@ pub fn op_fs_funlock_sync( pub async fn op_fs_funlock_async( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { - let file = FileResource::get_file(&state.borrow(), rid)?; +) -> Result<(), FsOpsError> { + let file = FileResource::get_file(&state.borrow(), rid) + .map_err(FsOpsError::Resource)?; file.unlock_async().await?; Ok(()) } @@ -1524,8 +1597,9 @@ pub fn op_fs_ftruncate_sync( state: &mut OpState, #[smi] rid: ResourceId, #[number] len: u64, -) -> Result<(), AnyError> { - let file = FileResource::get_file(state, rid)?; +) -> Result<(), FsOpsError> { + let file = + FileResource::get_file(state, rid).map_err(FsOpsError::Resource)?; file.truncate_sync(len)?; Ok(()) } @@ -1535,8 +1609,9 @@ pub async fn op_fs_file_truncate_async( state: Rc>, #[smi] rid: ResourceId, #[number] len: u64, -) -> Result<(), AnyError> { - let file = FileResource::get_file(&state.borrow(), rid)?; +) -> Result<(), FsOpsError> { + let file = FileResource::get_file(&state.borrow(), rid) + .map_err(FsOpsError::Resource)?; file.truncate_async(len).await?; Ok(()) } @@ -1549,8 +1624,9 @@ pub fn op_fs_futime_sync( #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, -) -> Result<(), AnyError> { - let file = FileResource::get_file(state, rid)?; +) -> Result<(), FsOpsError> { + let file = + FileResource::get_file(state, rid).map_err(FsOpsError::Resource)?; file.utime_sync(atime_secs, atime_nanos, mtime_secs, mtime_nanos)?; Ok(()) } @@ -1563,42 +1639,64 @@ pub async fn op_fs_futime_async( #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, -) -> Result<(), AnyError> { - let file = FileResource::get_file(&state.borrow(), rid)?; +) -> Result<(), FsOpsError> { + let file = FileResource::get_file(&state.borrow(), rid) + .map_err(FsOpsError::Resource)?; file .utime_async(atime_secs, atime_nanos, mtime_secs, mtime_nanos) .await?; Ok(()) } -trait WithContext { - fn context>>( - self, - desc: E, - ) -> AnyError; +#[derive(Debug)] +pub struct OperationError { + operation: &'static str, + kind: OperationErrorKind, + pub err: FsError, } -impl WithContext for FsError { - fn context>>( - self, - desc: E, - ) -> AnyError { - match self { - FsError::Io(io) => { - AnyError::new(io::Error::new(io.kind(), desc)).context(io) +impl std::fmt::Display for OperationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let FsError::Io(e) = &self.err { + std::fmt::Display::fmt(&e, f)?; + f.write_str(": ")?; + } + + f.write_str(self.operation)?; + + match &self.kind { + OperationErrorKind::Bare => Ok(()), + OperationErrorKind::WithPath(path) => write!(f, " '{}'", path.display()), + OperationErrorKind::WithTwoPaths(from, to) => { + write!(f, " '{}' -> '{}'", from.display(), to.display()) } - _ => self.into(), } } } +impl std::error::Error for OperationError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + if let FsError::Io(err) = &self.err { + Some(err) + } else { + None + } + } +} + +#[derive(Debug)] +pub enum OperationErrorKind { + Bare, + WithPath(PathBuf), + WithTwoPaths(PathBuf, PathBuf), +} + trait MapErrContext { type R; - fn context_fn(self, f: F) -> Self::R + fn context_fn(self, f: F) -> Self::R where - F: FnOnce() -> E, - E: Into>; + F: FnOnce(FsError) -> OperationError; fn context(self, desc: &'static str) -> Self::R; @@ -1613,25 +1711,29 @@ trait MapErrContext { } impl MapErrContext for Result { - type R = Result; + type R = Result; - fn context_fn(self, f: F) -> Self::R + fn context_fn(self, f: F) -> Self::R where - F: FnOnce() -> E, - E: Into>, + F: FnOnce(FsError) -> OperationError, { - self.map_err(|err| { - let message = f(); - err.context(message) + self.map_err(|err| FsOpsError::OperationError(f(err))) + } + + fn context(self, operation: &'static str) -> Self::R { + self.context_fn(move |err| OperationError { + operation, + kind: OperationErrorKind::Bare, + err, }) } - fn context(self, desc: &'static str) -> Self::R { - self.context_fn(move || desc) - } - fn context_path(self, operation: &'static str, path: &Path) -> Self::R { - self.context_fn(|| format!("{operation} '{}'", path.display())) + self.context_fn(|err| OperationError { + operation, + kind: OperationErrorKind::WithPath(path.to_path_buf()), + err, + }) } fn context_two_path( @@ -1640,21 +1742,19 @@ impl MapErrContext for Result { oldpath: &Path, newpath: &Path, ) -> Self::R { - self.context_fn(|| { - format!( - "{operation} '{}' -> '{}'", - oldpath.display(), - newpath.display() - ) + self.context_fn(|err| OperationError { + operation, + kind: OperationErrorKind::WithTwoPaths( + oldpath.to_path_buf(), + newpath.to_path_buf(), + ), + err, }) } } -fn path_into_string(s: std::ffi::OsString) -> Result { - s.into_string().map_err(|s| { - let message = format!("File name or path {s:?} is not valid UTF-8"); - custom_error("InvalidData", message) - }) +fn path_into_string(s: std::ffi::OsString) -> Result { + s.into_string().map_err(FsOpsError::InvalidUtf8) } macro_rules! create_struct_writer { @@ -1695,6 +1795,8 @@ create_struct_writer! { atime: u64, birthtime_set: bool, birthtime: u64, + ctime_set: bool, + ctime: u64, // Following are only valid under Unix. dev: u64, ino: u64, @@ -1726,6 +1828,8 @@ impl From for SerializableStat { atime: stat.atime.unwrap_or(0), birthtime_set: stat.birthtime.is_some(), birthtime: stat.birthtime.unwrap_or(0), + ctime_set: stat.ctime.is_some(), + ctime: stat.ctime.unwrap_or(0), dev: stat.dev, ino: stat.ino, diff --git a/ext/fs/std_fs.rs b/ext/fs/std_fs.rs index d8d5f65027..73439d9bab 100644 --- a/ext/fs/std_fs.rs +++ b/ext/fs/std_fs.rs @@ -11,13 +11,13 @@ use std::path::Path; use std::path::PathBuf; use std::rc::Rc; -use deno_core::normalize_path; use deno_core::unsync::spawn_blocking; use deno_io::fs::File; use deno_io::fs::FsError; use deno_io::fs::FsResult; use deno_io::fs::FsStat; use deno_io::StdFileResourceInner; +use deno_path_util::normalize_path; use crate::interface::AccessCheckCb; use crate::interface::FsDirEntry; @@ -101,7 +101,7 @@ impl FileSystem for RealFs { &self, path: &Path, recursive: bool, - mode: u32, + mode: Option, ) -> FsResult<()> { mkdir(path, recursive, mode) } @@ -109,7 +109,7 @@ impl FileSystem for RealFs { &self, path: PathBuf, recursive: bool, - mode: u32, + mode: Option, ) -> FsResult<()> { spawn_blocking(move || mkdir(&path, recursive, mode)).await? } @@ -407,11 +407,11 @@ impl FileSystem for RealFs { } } -fn mkdir(path: &Path, recursive: bool, mode: u32) -> FsResult<()> { +fn mkdir(path: &Path, recursive: bool, mode: Option) -> FsResult<()> { let mut builder = fs::DirBuilder::new(); builder.recursive(recursive); #[cfg(unix)] - { + if let Some(mode) = mode { use std::os::unix::fs::DirBuilderExt; builder.mode(mode); } @@ -821,24 +821,46 @@ fn stat_extra( Ok(info.dwVolumeSerialNumber as u64) } + const WINDOWS_TICK: i64 = 10_000; // 100-nanosecond intervals in a millisecond + const SEC_TO_UNIX_EPOCH: i64 = 11_644_473_600; // Seconds between Windows epoch and Unix epoch + + fn windows_time_to_unix_time_msec(windows_time: &i64) -> i64 { + let milliseconds_since_windows_epoch = windows_time / WINDOWS_TICK; + milliseconds_since_windows_epoch - SEC_TO_UNIX_EPOCH * 1000 + } + use windows_sys::Wdk::Storage::FileSystem::FILE_ALL_INFORMATION; + use windows_sys::Win32::Foundation::NTSTATUS; unsafe fn query_file_information( handle: winapi::shared::ntdef::HANDLE, - ) -> std::io::Result { + ) -> Result { use windows_sys::Wdk::Storage::FileSystem::NtQueryInformationFile; + use windows_sys::Win32::Foundation::RtlNtStatusToDosError; + use windows_sys::Win32::Foundation::ERROR_MORE_DATA; + use windows_sys::Win32::System::IO::IO_STATUS_BLOCK; let mut info = std::mem::MaybeUninit::::zeroed(); + let mut io_status_block = + std::mem::MaybeUninit::::zeroed(); let status = NtQueryInformationFile( handle as _, - std::ptr::null_mut(), + io_status_block.as_mut_ptr(), info.as_mut_ptr() as *mut _, std::mem::size_of::() as _, 18, /* FileAllInformation */ ); if status < 0 { - return Err(std::io::Error::last_os_error()); + let converted_status = RtlNtStatusToDosError(status); + + // If error more data is returned, then it means that the buffer is too small to get full filename information + // to have that we should retry. However, since we only use BasicInformation and StandardInformation, it is fine to ignore it + // since struct is populated with other data anyway. + // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryinformationfile#remarksdd + if converted_status != ERROR_MORE_DATA { + return Err(converted_status as NTSTATUS); + } } Ok(info.assume_init()) @@ -862,10 +884,13 @@ fn stat_extra( } let result = get_dev(file_handle); - CloseHandle(file_handle); fsstat.dev = result?; if let Ok(file_info) = query_file_information(file_handle) { + fsstat.ctime = Some(windows_time_to_unix_time_msec( + &file_info.BasicInformation.ChangeTime, + ) as u64); + if file_info.BasicInformation.FileAttributes & winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT != 0 @@ -898,6 +923,7 @@ fn stat_extra( } } + CloseHandle(file_handle); Ok(()) } } @@ -929,7 +955,7 @@ fn exists(path: &Path) -> bool { } fn realpath(path: &Path) -> FsResult { - Ok(deno_core::strip_unc_prefix(path.canonicalize()?)) + Ok(deno_path_util::strip_unc_prefix(path.canonicalize()?)) } fn read_dir(path: &Path) -> FsResult> { diff --git a/ext/http/00_serve.ts b/ext/http/00_serve.ts index 3b9b085a22..fcdb87d092 100644 --- a/ext/http/00_serve.ts +++ b/ext/http/00_serve.ts @@ -14,6 +14,7 @@ import { op_http_get_request_headers, op_http_get_request_method_and_url, op_http_read_request_body, + op_http_request_on_cancel, op_http_serve, op_http_serve_on, op_http_set_promise_complete, @@ -41,6 +42,10 @@ const { Uint8Array, Promise, } = primordials; +const { + getAsyncContext, + setAsyncContext, +} = core; import { InnerBody } from "ext:deno_fetch/22_body.js"; import { Event } from "ext:deno_web/02_event.js"; @@ -76,7 +81,11 @@ import { ReadableStreamPrototype, resourceForReadableStream, } from "ext:deno_web/06_streams.js"; -import { listen, listenOptionApiName, TcpConn } from "ext:deno_net/01_net.js"; +import { + listen, + listenOptionApiName, + UpgradedConn, +} from "ext:deno_net/01_net.js"; import { hasTlsKeyPairOptions, listenTls } from "ext:deno_net/02_tls.js"; import { SymbolAsyncDispose } from "ext:deno_web/00_infra.js"; @@ -189,7 +198,7 @@ class InnerRequest { const upgradeRid = op_http_upgrade_raw(external); - const conn = new TcpConn( + const conn = new UpgradedConn( upgradeRid, underlyingConn?.remoteAddr, underlyingConn?.localAddr, @@ -369,6 +378,18 @@ class InnerRequest { get external() { return this.#external; } + + onCancel(callback) { + if (this.#external === null) { + callback(); + return; + } + + PromisePrototypeThen( + op_http_request_on_cancel(this.#external), + callback, + ); + } } class CallbackContext { @@ -380,8 +401,10 @@ class CallbackContext { /** @type {Promise | undefined} */ closing; listener; + asyncContext; constructor(signal, args, listener) { + this.asyncContext = getAsyncContext(); // The abort signal triggers a non-graceful shutdown signal?.addEventListener( "abort", @@ -491,82 +514,89 @@ function fastSyncResponseOrStream( */ function mapToCallback(context, callback, onError) { return async function (req) { - // Get the response from the user-provided callback. If that fails, use onError. If that fails, return a fallback - // 500 error. - let innerRequest; - let response; + const asyncContext = getAsyncContext(); + setAsyncContext(context.asyncContext); + try { - innerRequest = new InnerRequest(req, context); - const request = fromInnerRequest(innerRequest, "immutable"); - innerRequest.request = request; - response = await callback( - request, - new ServeHandlerInfo(innerRequest), - ); - - // Throwing Error if the handler return value is not a Response class - if (!ObjectPrototypeIsPrototypeOf(ResponsePrototype, response)) { - throw new TypeError( - "Return value from serve handler must be a response or a promise resolving to a response", - ); - } - - if (response.type === "error") { - throw new TypeError( - "Return value from serve handler must not be an error response (like Response.error())", - ); - } - - if (response.bodyUsed) { - throw new TypeError( - "The body of the Response returned from the serve handler has already been consumed", - ); - } - } catch (error) { + // Get the response from the user-provided callback. If that fails, use onError. If that fails, return a fallback + // 500 error. + let innerRequest; + let response; try { - response = await onError(error); + innerRequest = new InnerRequest(req, context); + const request = fromInnerRequest(innerRequest, "immutable"); + innerRequest.request = request; + response = await callback( + request, + new ServeHandlerInfo(innerRequest), + ); + + // Throwing Error if the handler return value is not a Response class if (!ObjectPrototypeIsPrototypeOf(ResponsePrototype, response)) { throw new TypeError( - "Return value from onError handler must be a response or a promise resolving to a response", + "Return value from serve handler must be a response or a promise resolving to a response", + ); + } + + if (response.type === "error") { + throw new TypeError( + "Return value from serve handler must not be an error response (like Response.error())", + ); + } + + if (response.bodyUsed) { + throw new TypeError( + "The body of the Response returned from the serve handler has already been consumed", ); } } catch (error) { - // deno-lint-ignore no-console - console.error("Exception in onError while handling exception", error); - response = internalServerError(); + try { + response = await onError(error); + if (!ObjectPrototypeIsPrototypeOf(ResponsePrototype, response)) { + throw new TypeError( + "Return value from onError handler must be a response or a promise resolving to a response", + ); + } + } catch (error) { + // deno-lint-ignore no-console + console.error("Exception in onError while handling exception", error); + response = internalServerError(); + } } - } - const inner = toInnerResponse(response); - if (innerRequest?.[_upgraded]) { - // We're done here as the connection has been upgraded during the callback and no longer requires servicing. - if (response !== UPGRADE_RESPONSE_SENTINEL) { - // deno-lint-ignore no-console - console.error("Upgrade response was not returned from callback"); - context.close(); + const inner = toInnerResponse(response); + if (innerRequest?.[_upgraded]) { + // We're done here as the connection has been upgraded during the callback and no longer requires servicing. + if (response !== UPGRADE_RESPONSE_SENTINEL) { + // deno-lint-ignore no-console + console.error("Upgrade response was not returned from callback"); + context.close(); + } + innerRequest?.[_upgraded](); + return; } - innerRequest?.[_upgraded](); - return; - } - // Did everything shut down while we were waiting? - if (context.closed) { - // We're shutting down, so this status shouldn't make it back to the client but "Service Unavailable" seems appropriate - innerRequest?.close(); - op_http_set_promise_complete(req, 503); - return; - } - - const status = inner.status; - const headers = inner.headerList; - if (headers && headers.length > 0) { - if (headers.length == 1) { - op_http_set_response_header(req, headers[0][0], headers[0][1]); - } else { - op_http_set_response_headers(req, headers); + // Did everything shut down while we were waiting? + if (context.closed) { + // We're shutting down, so this status shouldn't make it back to the client but "Service Unavailable" seems appropriate + innerRequest?.close(); + op_http_set_promise_complete(req, 503); + return; } - } - fastSyncResponseOrStream(req, inner.body, status, innerRequest); + const status = inner.status; + const headers = inner.headerList; + if (headers && headers.length > 0) { + if (headers.length == 1) { + op_http_set_response_header(req, headers[0][0], headers[0][1]); + } else { + op_http_set_response_headers(req, headers); + } + } + + fastSyncResponseOrStream(req, inner.body, status, innerRequest); + } finally { + setAsyncContext(asyncContext); + } }; } diff --git a/ext/http/Cargo.toml b/ext/http/Cargo.toml index ba21e3f30d..ed98fe349c 100644 --- a/ext/http/Cargo.toml +++ b/ext/http/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_http" -version = "0.166.0" +version = "0.175.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/http/fly_accept_encoding.rs b/ext/http/fly_accept_encoding.rs index 94e3368767..4d6fd2231e 100644 --- a/ext/http/fly_accept_encoding.rs +++ b/ext/http/fly_accept_encoding.rs @@ -119,7 +119,7 @@ fn encodings_iter_inner<'s>( }; Some(Ok((encoding, qval))) }) - .map(|r| r?) // flatten Result Result { +) -> Result { // SAFETY: external is deleted before calling this op. let http = unsafe { take_external!(external, "op_http_upgrade_raw") }; @@ -177,7 +196,7 @@ pub fn op_http_upgrade_raw( upgraded.write_all(&bytes).await?; break upgraded; } - Err(err) => return Err(err), + Err(err) => return Err(HttpNextError::WebSocketUpgrade(err)), } }; @@ -193,7 +212,7 @@ pub fn op_http_upgrade_raw( } read_tx.write_all(&buf[..read]).await?; } - Ok::<_, AnyError>(()) + Ok::<_, HttpNextError>(()) }); spawn(async move { let mut buf = [0; 1024]; @@ -204,7 +223,7 @@ pub fn op_http_upgrade_raw( } upgraded_tx.write_all(&buf[..read]).await?; } - Ok::<_, AnyError>(()) + Ok::<_, HttpNextError>(()) }); Ok(()) @@ -223,7 +242,7 @@ pub async fn op_http_upgrade_websocket_next( state: Rc>, external: *const c_void, #[serde] headers: Vec<(ByteString, ByteString)>, -) -> Result { +) -> Result { let http = // SAFETY: external is deleted before calling this op. unsafe { take_external!(external, "op_http_upgrade_websocket_next") }; @@ -246,7 +265,11 @@ pub async fn op_http_upgrade_websocket_next( // Stage 3: take the extracted raw network stream and upgrade it to a websocket, then return it let (stream, bytes) = extract_network_stream(upgraded); - ws_create_server_stream(&mut state.borrow_mut(), stream, bytes) + Ok(ws_create_server_stream( + &mut state.borrow_mut(), + stream, + bytes, + )) } #[op2(fast)] @@ -296,7 +319,7 @@ where let authority: v8::Local = match request_properties.authority { Some(authority) => v8::String::new_from_utf8( scope, - authority.as_ref(), + authority.as_bytes(), v8::NewStringType::Normal, ) .unwrap() @@ -305,15 +328,25 @@ where }; // Only extract the path part - we handle authority elsewhere - let path = match &request_parts.uri.path_and_query() { - Some(path_and_query) => path_and_query.to_string(), - None => "".to_owned(), + let path = match request_parts.uri.path_and_query() { + Some(path_and_query) => { + let path = path_and_query.as_str(); + if matches!(path.as_bytes().first(), Some(b'/' | b'*')) { + Cow::Borrowed(path) + } else { + Cow::Owned(format!("/{}", path)) + } + } + None => Cow::Borrowed(""), }; - let path: v8::Local = - v8::String::new_from_utf8(scope, path.as_ref(), v8::NewStringType::Normal) - .unwrap() - .into(); + let path: v8::Local = v8::String::new_from_utf8( + scope, + path.as_bytes(), + v8::NewStringType::Normal, + ) + .unwrap() + .into(); let peer_address: v8::Local = v8::String::new_from_utf8( scope, @@ -531,6 +564,7 @@ fn is_request_compressible( match accept_encoding.to_str() { // Firefox and Chrome send this -- no need to parse Ok("gzip, deflate, br") => return Compression::Brotli, + Ok("gzip, deflate, br, zstd") => return Compression::Brotli, Ok("gzip") => return Compression::GZip, Ok("br") => return Compression::Brotli, _ => (), @@ -667,6 +701,27 @@ fn set_response( http.complete(); } +#[op2(fast)] +pub fn op_http_get_request_cancelled(external: *const c_void) -> bool { + let http = + // SAFETY: op is called with external. + unsafe { clone_external!(external, "op_http_get_request_cancelled") }; + http.cancelled() +} + +#[op2(async)] +pub async fn op_http_request_on_cancel(external: *const c_void) { + let http = + // SAFETY: op is called with external. + unsafe { clone_external!(external, "op_http_request_on_cancel") }; + let (tx, rx) = tokio::sync::oneshot::channel(); + + http.on_cancel(tx); + drop(http); + + rx.await.ok(); +} + /// Returned promise resolves when body streaming finishes. /// Call [`op_http_close_after_finish`] when done with the external. #[op2(async)] @@ -676,7 +731,7 @@ pub async fn op_http_set_response_body_resource( #[smi] stream_rid: ResourceId, auto_close: bool, status: u16, -) -> Result { +) -> Result { let http = // SAFETY: op is called with external. unsafe { clone_external!(external, "op_http_set_response_body_resource") }; @@ -691,9 +746,15 @@ pub async fn op_http_set_response_body_resource( let resource = { let mut state = state.borrow_mut(); if auto_close { - state.resource_table.take_any(stream_rid)? + state + .resource_table + .take_any(stream_rid) + .map_err(HttpNextError::Resource)? } else { - state.resource_table.get_any(stream_rid)? + state + .resource_table + .get_any(stream_rid) + .map_err(HttpNextError::Resource)? } }; @@ -800,17 +861,17 @@ async fn serve_http2_autodetect( io: impl HttpServeStream, svc: impl HttpService + 'static, cancel: Rc, -) -> Result<(), AnyError> { +) -> Result<(), HttpNextError> { let prefix = NetworkStreamPrefixCheck::new(io, HTTP2_PREFIX); let (matches, io) = prefix.match_prefix().await?; if matches { serve_http2_unconditional(io, svc, cancel) .await - .map_err(|e| e.into()) + .map_err(HttpNextError::Hyper) } else { serve_http11_unconditional(io, svc, cancel) .await - .map_err(|e| e.into()) + .map_err(HttpNextError::Hyper) } } @@ -819,7 +880,7 @@ fn serve_https( request_info: HttpConnectionProperties, lifetime: HttpLifetime, tx: tokio::sync::mpsc::Sender>, -) -> JoinHandle> { +) -> JoinHandle> { let HttpLifetime { server_state, connection_cancel_handle, @@ -838,11 +899,11 @@ fn serve_https( if Some(TLS_ALPN_HTTP_2) == handshake.as_deref() { serve_http2_unconditional(io, svc, listen_cancel_handle) .await - .map_err(|e| e.into()) + .map_err(HttpNextError::Hyper) } else if Some(TLS_ALPN_HTTP_11) == handshake.as_deref() { serve_http11_unconditional(io, svc, listen_cancel_handle) .await - .map_err(|e| e.into()) + .map_err(HttpNextError::Hyper) } else { serve_http2_autodetect(io, svc, listen_cancel_handle).await } @@ -856,7 +917,7 @@ fn serve_http( request_info: HttpConnectionProperties, lifetime: HttpLifetime, tx: tokio::sync::mpsc::Sender>, -) -> JoinHandle> { +) -> JoinHandle> { let HttpLifetime { server_state, connection_cancel_handle, @@ -877,7 +938,7 @@ fn serve_http_on( listen_properties: &HttpListenProperties, lifetime: HttpLifetime, tx: tokio::sync::mpsc::Sender>, -) -> JoinHandle> +) -> JoinHandle> where HTTP: HttpPropertyExtractor, { @@ -908,7 +969,7 @@ struct HttpLifetime { } struct HttpJoinHandle { - join_handle: AsyncRefCell>>>, + join_handle: AsyncRefCell>>>, connection_cancel_handle: Rc, listen_cancel_handle: Rc, rx: AsyncRefCell>>, @@ -968,12 +1029,13 @@ impl Drop for HttpJoinHandle { pub fn op_http_serve( state: Rc>, #[smi] listener_rid: ResourceId, -) -> Result<(ResourceId, &'static str, String), AnyError> +) -> Result<(ResourceId, &'static str, String), HttpNextError> where HTTP: HttpPropertyExtractor, { let listener = - HTTP::get_listener_for_rid(&mut state.borrow_mut(), listener_rid)?; + HTTP::get_listener_for_rid(&mut state.borrow_mut(), listener_rid) + .map_err(HttpNextError::Resource)?; let listen_properties = HTTP::listen_properties_from_listener(&listener)?; @@ -988,7 +1050,8 @@ where loop { let conn = HTTP::accept_connection_from_listener(&listener) .try_or_cancel(listen_cancel_clone.clone()) - .await?; + .await + .map_err(HttpNextError::HttpPropertyExtractor)?; serve_http_on::( conn, &listen_properties_clone, @@ -997,7 +1060,7 @@ where ); } #[allow(unreachable_code)] - Ok::<_, AnyError>(()) + Ok::<_, HttpNextError>(()) }); // Set the handle after we start the future @@ -1017,25 +1080,25 @@ where pub fn op_http_serve_on( state: Rc>, #[smi] connection_rid: ResourceId, -) -> Result<(ResourceId, &'static str, String), AnyError> +) -> Result<(ResourceId, &'static str, String), HttpNextError> where HTTP: HttpPropertyExtractor, { let connection = - HTTP::get_connection_for_rid(&mut state.borrow_mut(), connection_rid)?; + HTTP::get_connection_for_rid(&mut state.borrow_mut(), connection_rid) + .map_err(HttpNextError::Resource)?; let listen_properties = HTTP::listen_properties_from_connection(&connection)?; let (tx, rx) = tokio::sync::mpsc::channel(10); let resource: Rc = Rc::new(HttpJoinHandle::new(rx)); - let handle: JoinHandle> = - serve_http_on::( - connection, - &listen_properties, - resource.lifetime(), - tx, - ); + let handle = serve_http_on::( + connection, + &listen_properties, + resource.lifetime(), + tx, + ); // Set the handle after we start the future *RcRef::map(&resource, |this| &this.join_handle) @@ -1081,12 +1144,13 @@ pub fn op_http_try_wait( pub async fn op_http_wait( state: Rc>, #[smi] rid: ResourceId, -) -> Result<*const c_void, AnyError> { +) -> Result<*const c_void, HttpNextError> { // We will get the join handle initially, as we might be consuming requests still let join_handle = state .borrow_mut() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(HttpNextError::Resource)?; let cancel = join_handle.listen_cancel_handle(); let next = async { @@ -1113,13 +1177,12 @@ pub async fn op_http_wait( // Filter out shutdown (ENOTCONN) errors if let Err(err) = res { - if let Some(err) = err.source() { - if let Some(err) = err.downcast_ref::() { - if err.kind() == io::ErrorKind::NotConnected { - return Ok(null()); - } + if let HttpNextError::Io(err) = &err { + if err.kind() == io::ErrorKind::NotConnected { + return Ok(null()); } } + return Err(err); } @@ -1132,7 +1195,7 @@ pub fn op_http_cancel( state: &mut OpState, #[smi] rid: ResourceId, graceful: bool, -) -> Result<(), AnyError> { +) -> Result<(), deno_core::error::AnyError> { let join_handle = state.resource_table.get::(rid)?; if graceful { @@ -1152,11 +1215,12 @@ pub async fn op_http_close( state: Rc>, #[smi] rid: ResourceId, graceful: bool, -) -> Result<(), AnyError> { +) -> Result<(), HttpNextError> { let join_handle = state .borrow_mut() .resource_table - .take::(rid)?; + .take::(rid) + .map_err(HttpNextError::Resource)?; if graceful { http_general_trace!("graceful shutdown"); @@ -1202,23 +1266,26 @@ impl UpgradeStream { } } - async fn read(self: Rc, buf: &mut [u8]) -> Result { + async fn read( + self: Rc, + buf: &mut [u8], + ) -> Result { let cancel_handle = RcRef::map(self.clone(), |this| &this.cancel_handle); async { let read = RcRef::map(self, |this| &this.read); let mut read = read.borrow_mut().await; - Ok(Pin::new(&mut *read).read(buf).await?) + Pin::new(&mut *read).read(buf).await } .try_or_cancel(cancel_handle) .await } - async fn write(self: Rc, buf: &[u8]) -> Result { + async fn write(self: Rc, buf: &[u8]) -> Result { let cancel_handle = RcRef::map(self.clone(), |this| &this.cancel_handle); async { let write = RcRef::map(self, |this| &this.write); let mut write = write.borrow_mut().await; - Ok(Pin::new(&mut *write).write(buf).await?) + Pin::new(&mut *write).write(buf).await } .try_or_cancel(cancel_handle) .await @@ -1228,7 +1295,7 @@ impl UpgradeStream { self: Rc, buf1: &[u8], buf2: &[u8], - ) -> Result { + ) -> Result { let mut wr = RcRef::map(self, |r| &r.write).borrow_mut().await; let total = buf1.len() + buf2.len(); @@ -1281,9 +1348,12 @@ pub async fn op_raw_write_vectored( #[smi] rid: ResourceId, #[buffer] buf1: JsBuffer, #[buffer] buf2: JsBuffer, -) -> Result { - let resource: Rc = - state.borrow().resource_table.get::(rid)?; +) -> Result { + let resource: Rc = state + .borrow() + .resource_table + .get::(rid) + .map_err(HttpNextError::Resource)?; let nwritten = resource.write_vectored(&buf1, &buf2).await?; Ok(nwritten) } diff --git a/ext/http/lib.rs b/ext/http/lib.rs index 934f8a0024..49893b1b92 100644 --- a/ext/http/lib.rs +++ b/ext/http/lib.rs @@ -6,8 +6,6 @@ use async_compression::Level; use base64::prelude::BASE64_STANDARD; use base64::Engine; use cache_control::CacheControl; -use deno_core::error::custom_error; -use deno_core::error::AnyError; use deno_core::futures::channel::mpsc; use deno_core::futures::channel::oneshot; use deno_core::futures::future::pending; @@ -89,11 +87,14 @@ mod service; mod websocket_upgrade; use fly_accept_encoding::Encoding; +pub use http_next::HttpNextError; pub use request_properties::DefaultHttpPropertyExtractor; pub use request_properties::HttpConnectionProperties; pub use request_properties::HttpListenProperties; pub use request_properties::HttpPropertyExtractor; pub use request_properties::HttpRequestProperties; +pub use service::UpgradeUnavailableError; +pub use websocket_upgrade::WebSocketUpgradeError; deno_core::extension!( deno_http, @@ -111,7 +112,9 @@ deno_core::extension!( http_next::op_http_close_after_finish, http_next::op_http_get_request_header, http_next::op_http_get_request_headers, + http_next::op_http_request_on_cancel, http_next::op_http_get_request_method_and_url, + http_next::op_http_get_request_cancelled, http_next::op_http_read_request_body, http_next::op_http_serve_on, http_next::op_http_serve, @@ -134,6 +137,38 @@ deno_core::extension!( esm = ["00_serve.ts", "01_http.js", "02_websocket.ts"], ); +#[derive(Debug, thiserror::Error)] +pub enum HttpError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + Canceled(#[from] deno_core::Canceled), + #[error("{0}")] + HyperV014(#[source] Arc), + #[error("{0}")] + InvalidHeaderName(#[from] hyper_v014::header::InvalidHeaderName), + #[error("{0}")] + InvalidHeaderValue(#[from] hyper_v014::header::InvalidHeaderValue), + #[error("{0}")] + Http(#[from] hyper_v014::http::Error), + #[error("response headers already sent")] + ResponseHeadersAlreadySent, + #[error("connection closed while sending response")] + ConnectionClosedWhileSendingResponse, + #[error("already in use")] + AlreadyInUse, + #[error("{0}")] + Io(#[from] std::io::Error), + #[error("no response headers")] + NoResponseHeaders, + #[error("response already completed")] + ResponseAlreadyCompleted, + #[error("cannot upgrade because request body was used")] + UpgradeBodyUsed, + #[error(transparent)] + Other(deno_core::error::AnyError), +} + pub enum HttpSocketAddr { IpSocket(std::net::SocketAddr), #[cfg(unix)] @@ -216,7 +251,7 @@ impl HttpConnResource { String, String, )>, - AnyError, + HttpError, > { let fut = async { let (request_tx, request_rx) = oneshot::channel(); @@ -259,8 +294,8 @@ impl HttpConnResource { } /// A future that completes when this HTTP connection is closed or errors. - async fn closed(&self) -> Result<(), AnyError> { - self.closed_fut.clone().map_err(AnyError::from).await + async fn closed(&self) -> Result<(), HttpError> { + self.closed_fut.clone().map_err(HttpError::HyperV014).await } } @@ -280,14 +315,13 @@ pub fn http_create_conn_resource( io: S, addr: A, scheme: &'static str, -) -> Result +) -> ResourceId where S: AsyncRead + AsyncWrite + Unpin + Send + 'static, A: Into, { let conn = HttpConnResource::new(io, scheme, addr.into()); - let rid = state.resource_table.add(conn); - Ok(rid) + state.resource_table.add(conn) } /// An object that implements the `hyper::Service` trait, through which Hyper @@ -423,7 +457,9 @@ impl Resource for HttpStreamReadResource { // safely call `await` on it without creating a race condition. Some(_) => match body.as_mut().next().await.unwrap() { Ok(chunk) => assert!(chunk.is_empty()), - Err(err) => break Err(AnyError::from(err)), + Err(err) => { + break Err(HttpError::HyperV014(Arc::new(err)).into()) + } }, None => break Ok(BufView::empty()), } @@ -545,8 +581,12 @@ struct NextRequestResponse( async fn op_http_accept( state: Rc>, #[smi] rid: ResourceId, -) -> Result, AnyError> { - let conn = state.borrow().resource_table.get::(rid)?; +) -> Result, HttpError> { + let conn = state + .borrow() + .resource_table + .get::(rid) + .map_err(HttpError::Resource)?; match conn.accept().await { Ok(Some((read_stream, write_stream, method, url))) => { @@ -657,11 +697,12 @@ async fn op_http_write_headers( #[smi] status: u16, #[serde] headers: Vec<(ByteString, ByteString)>, #[serde] data: Option, -) -> Result<(), AnyError> { +) -> Result<(), HttpError> { let stream = state .borrow_mut() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(HttpError::Resource)?; // Track supported encoding let encoding = stream.accept_encoding; @@ -708,14 +749,14 @@ async fn op_http_write_headers( let mut old_wr = RcRef::map(&stream, |r| &r.wr).borrow_mut().await; let response_tx = match replace(&mut *old_wr, new_wr) { HttpResponseWriter::Headers(response_tx) => response_tx, - _ => return Err(http_error("response headers already sent")), + _ => return Err(HttpError::ResponseHeadersAlreadySent), }; match response_tx.send(body) { Ok(_) => Ok(()), Err(_) => { stream.conn.closed().await?; - Err(http_error("connection closed while sending response")) + Err(HttpError::ConnectionClosedWhileSendingResponse) } } } @@ -725,11 +766,14 @@ async fn op_http_write_headers( fn op_http_headers( state: &mut OpState, #[smi] rid: u32, -) -> Result, AnyError> { - let stream = state.resource_table.get::(rid)?; +) -> Result, HttpError> { + let stream = state + .resource_table + .get::(rid) + .map_err(HttpError::Resource)?; let rd = RcRef::map(&stream, |r| &r.rd) .try_borrow() - .ok_or_else(|| http_error("already in use"))?; + .ok_or(HttpError::AlreadyInUse)?; match &*rd { HttpRequestReader::Headers(request) => Ok(req_headers(request.headers())), HttpRequestReader::Body(headers, _) => Ok(req_headers(headers)), @@ -741,7 +785,7 @@ fn http_response( data: Option, compressing: bool, encoding: Encoding, -) -> Result<(HttpResponseWriter, hyper_v014::Body), AnyError> { +) -> Result<(HttpResponseWriter, hyper_v014::Body), HttpError> { // Gzip, after level 1, doesn't produce significant size difference. // This default matches nginx default gzip compression level (1): // https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_comp_level @@ -878,25 +922,34 @@ async fn op_http_write_resource( state: Rc>, #[smi] rid: ResourceId, #[smi] stream: ResourceId, -) -> Result<(), AnyError> { +) -> Result<(), HttpError> { let http_stream = state .borrow() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(HttpError::Resource)?; let mut wr = RcRef::map(&http_stream, |r| &r.wr).borrow_mut().await; - let resource = state.borrow().resource_table.get_any(stream)?; + let resource = state + .borrow() + .resource_table + .get_any(stream) + .map_err(HttpError::Resource)?; loop { match *wr { HttpResponseWriter::Headers(_) => { - return Err(http_error("no response headers")) + return Err(HttpError::NoResponseHeaders) } HttpResponseWriter::Closed => { - return Err(http_error("response already completed")) + return Err(HttpError::ResponseAlreadyCompleted) } _ => {} }; - let view = resource.clone().read(64 * 1024).await?; // 64KB + let view = resource + .clone() + .read(64 * 1024) + .await + .map_err(HttpError::Other)?; // 64KB if view.is_empty() { break; } @@ -937,16 +990,17 @@ async fn op_http_write( state: Rc>, #[smi] rid: ResourceId, #[buffer] buf: JsBuffer, -) -> Result<(), AnyError> { +) -> Result<(), HttpError> { let stream = state .borrow() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(HttpError::Resource)?; let mut wr = RcRef::map(&stream, |r| &r.wr).borrow_mut().await; match &mut *wr { - HttpResponseWriter::Headers(_) => Err(http_error("no response headers")), - HttpResponseWriter::Closed => Err(http_error("response already completed")), + HttpResponseWriter::Headers(_) => Err(HttpError::NoResponseHeaders), + HttpResponseWriter::Closed => Err(HttpError::ResponseAlreadyCompleted), HttpResponseWriter::Body { writer, .. } => { let mut result = writer.write_all(&buf).await; if result.is_ok() { @@ -961,7 +1015,7 @@ async fn op_http_write( stream.conn.closed().await?; // If there was no connection error, drop body_tx. *wr = HttpResponseWriter::Closed; - Err(http_error("response already completed")) + Err(HttpError::ResponseAlreadyCompleted) } } } @@ -975,7 +1029,7 @@ async fn op_http_write( stream.conn.closed().await?; // If there was no connection error, drop body_tx. *wr = HttpResponseWriter::Closed; - Err(http_error("response already completed")) + Err(HttpError::ResponseAlreadyCompleted) } } } @@ -989,11 +1043,12 @@ async fn op_http_write( async fn op_http_shutdown( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { +) -> Result<(), HttpError> { let stream = state .borrow() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(HttpError::Resource)?; let mut wr = RcRef::map(&stream, |r| &r.wr).borrow_mut().await; let wr = take(&mut *wr); match wr { @@ -1022,14 +1077,12 @@ async fn op_http_shutdown( #[op2] #[string] -fn op_http_websocket_accept_header( - #[string] key: String, -) -> Result { +fn op_http_websocket_accept_header(#[string] key: String) -> String { let digest = ring::digest::digest( &ring::digest::SHA1_FOR_LEGACY_USE_ONLY, format!("{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11").as_bytes(), ); - Ok(BASE64_STANDARD.encode(digest)) + BASE64_STANDARD.encode(digest) } #[op2(async)] @@ -1037,25 +1090,29 @@ fn op_http_websocket_accept_header( async fn op_http_upgrade_websocket( state: Rc>, #[smi] rid: ResourceId, -) -> Result { +) -> Result { let stream = state .borrow_mut() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(HttpError::Resource)?; let mut rd = RcRef::map(&stream, |r| &r.rd).borrow_mut().await; let request = match &mut *rd { HttpRequestReader::Headers(request) => request, - _ => { - return Err(http_error("cannot upgrade because request body was used")) - } + _ => return Err(HttpError::UpgradeBodyUsed), }; - let (transport, bytes) = - extract_network_stream(hyper_v014::upgrade::on(request).await?); - let ws_rid = - ws_create_server_stream(&mut state.borrow_mut(), transport, bytes)?; - Ok(ws_rid) + let (transport, bytes) = extract_network_stream( + hyper_v014::upgrade::on(request) + .await + .map_err(|err| HttpError::HyperV014(Arc::new(err)))?, + ); + Ok(ws_create_server_stream( + &mut state.borrow_mut(), + transport, + bytes, + )) } // Needed so hyper can use non Send futures @@ -1082,10 +1139,6 @@ where } } -fn http_error(message: &'static str) -> AnyError { - custom_error("Http", message) -} - /// Filters out the ever-surprising 'shutdown ENOTCONN' errors. fn filter_enotconn( result: Result<(), hyper_v014::Error>, diff --git a/ext/http/request_body.rs b/ext/http/request_body.rs index 45df12457c..f1c3f358ea 100644 --- a/ext/http/request_body.rs +++ b/ext/http/request_body.rs @@ -1,9 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use bytes::Bytes; -use deno_core::error::AnyError; use deno_core::futures::stream::Peekable; use deno_core::futures::Stream; use deno_core::futures::StreamExt; +use deno_core::futures::TryFutureExt; use deno_core::AsyncRefCell; use deno_core::AsyncResult; use deno_core::BufView; @@ -22,7 +22,7 @@ use std::task::Poll; struct ReadFuture(Incoming); impl Stream for ReadFuture { - type Item = Result; + type Item = Result; fn poll_next( self: Pin<&mut Self>, @@ -37,13 +37,13 @@ impl Stream for ReadFuture { if let Ok(data) = frame.into_data() { // Ensure that we never yield an empty frame if !data.is_empty() { - break Poll::Ready(Some(Ok::<_, AnyError>(data))); + break Poll::Ready(Some(Ok(data))); } } // Loop again so we don't lose the waker continue; } - Some(Err(e)) => Poll::Ready(Some(Err(e.into()))), + Some(Err(e)) => Poll::Ready(Some(Err(e))), None => Poll::Ready(None), }; } @@ -58,7 +58,7 @@ impl HttpRequestBody { Self(AsyncRefCell::new(ReadFuture(body).peekable()), size_hint) } - async fn read(self: Rc, limit: usize) -> Result { + async fn read(self: Rc, limit: usize) -> Result { let peekable = RcRef::map(self, |this| &this.0); let mut peekable = peekable.borrow_mut().await; match Pin::new(&mut *peekable).peek_mut().await { @@ -82,7 +82,7 @@ impl Resource for HttpRequestBody { } fn read(self: Rc, limit: usize) -> AsyncResult { - Box::pin(HttpRequestBody::read(self, limit)) + Box::pin(HttpRequestBody::read(self, limit).map_err(Into::into)) } fn size_hint(&self) -> (u64, Option) { diff --git a/ext/http/request_properties.rs b/ext/http/request_properties.rs index 1422c7417d..39d35a79f1 100644 --- a/ext/http/request_properties.rs +++ b/ext/http/request_properties.rs @@ -34,8 +34,8 @@ pub struct HttpConnectionProperties { pub stream_type: NetworkStreamType, } -pub struct HttpRequestProperties { - pub authority: Option, +pub struct HttpRequestProperties<'a> { + pub authority: Option>, } /// Pluggable trait to determine listen, connection and request properties @@ -84,11 +84,11 @@ pub trait HttpPropertyExtractor { ) -> NetworkStream; /// Determines the request properties. - fn request_properties( - connection_properties: &HttpConnectionProperties, - uri: &Uri, - headers: &HeaderMap, - ) -> HttpRequestProperties; + fn request_properties<'a>( + connection_properties: &'a HttpConnectionProperties, + uri: &'a Uri, + headers: &'a HeaderMap, + ) -> HttpRequestProperties<'a>; } pub struct DefaultHttpPropertyExtractor {} @@ -180,18 +180,17 @@ impl HttpPropertyExtractor for DefaultHttpPropertyExtractor { } } - fn request_properties( - connection_properties: &HttpConnectionProperties, - uri: &Uri, - headers: &HeaderMap, - ) -> HttpRequestProperties { + fn request_properties<'a>( + connection_properties: &'a HttpConnectionProperties, + uri: &'a Uri, + headers: &'a HeaderMap, + ) -> HttpRequestProperties<'a> { let authority = req_host( uri, headers, connection_properties.stream_type, connection_properties.local_port.unwrap_or_default(), - ) - .map(|s| s.into_owned()); + ); HttpRequestProperties { authority } } diff --git a/ext/http/service.rs b/ext/http/service.rs index 787e9babf8..ce24dea43f 100644 --- a/ext/http/service.rs +++ b/ext/http/service.rs @@ -2,7 +2,6 @@ use crate::request_properties::HttpConnectionProperties; use crate::response_body::ResponseBytesInner; use crate::response_body::ResponseStreamResult; -use deno_core::error::AnyError; use deno_core::futures::ready; use deno_core::BufView; use deno_core::OpState; @@ -28,6 +27,7 @@ use std::rc::Rc; use std::task::Context; use std::task::Poll; use std::task::Waker; +use tokio::sync::oneshot; pub type Request = hyper::Request; pub type Response = hyper::Response; @@ -206,8 +206,13 @@ pub(crate) async fn handle_request( Ok(response) } +#[derive(Debug, thiserror::Error)] +#[error("upgrade unavailable")] +pub struct UpgradeUnavailableError; + struct HttpRecordInner { server_state: SignallingRc, + closed_channel: Option>, request_info: HttpConnectionProperties, request_parts: http::request::Parts, request_body: Option, @@ -273,6 +278,7 @@ impl HttpRecord { response_body_finished: false, response_body_waker: None, trailers: None, + closed_channel: None, been_dropped: false, finished: false, needs_close_after_finish: false, @@ -309,6 +315,10 @@ impl HttpRecord { RefMut::map(self.self_mut(), |inner| &mut inner.needs_close_after_finish) } + pub fn on_cancel(&self, sender: oneshot::Sender<()>) { + self.self_mut().closed_channel = Some(sender); + } + fn recycle(self: Rc) { assert!( Rc::strong_count(&self) == 1, @@ -344,14 +354,14 @@ impl HttpRecord { } /// Perform the Hyper upgrade on this record. - pub fn upgrade(&self) -> Result { + pub fn upgrade(&self) -> Result { // Manually perform the upgrade. We're peeking into hyper's underlying machinery here a bit self .self_mut() .request_parts .extensions .remove::() - .ok_or_else(|| AnyError::msg("upgrade unavailable")) + .ok_or(UpgradeUnavailableError) } /// Take the Hyper body from this record. @@ -387,6 +397,9 @@ impl HttpRecord { inner.been_dropped = true; // The request body might include actual resources. inner.request_body.take(); + if let Some(closed_channel) = inner.closed_channel.take() { + let _ = closed_channel.send(()); + } } /// Complete this record, potentially expunging it if it is fully complete (ie: cancelled as well). @@ -515,7 +528,7 @@ pub struct HttpRecordResponse(ManuallyDrop>); impl Body for HttpRecordResponse { type Data = BufView; - type Error = AnyError; + type Error = deno_core::error::AnyError; fn poll_frame( self: Pin<&mut Self>, @@ -640,7 +653,7 @@ mod tests { } #[tokio::test] - async fn test_handle_request() -> Result<(), AnyError> { + async fn test_handle_request() -> Result<(), deno_core::error::AnyError> { let (tx, mut rx) = tokio::sync::mpsc::channel(10); let server_state = HttpServerState::new(); let server_state_check = server_state.clone(); diff --git a/ext/http/websocket_upgrade.rs b/ext/http/websocket_upgrade.rs index 4dead767a3..af9504717e 100644 --- a/ext/http/websocket_upgrade.rs +++ b/ext/http/websocket_upgrade.rs @@ -4,7 +4,6 @@ use std::marker::PhantomData; use bytes::Bytes; use bytes::BytesMut; -use deno_core::error::AnyError; use httparse::Status; use hyper::header::HeaderName; use hyper::header::HeaderValue; @@ -13,12 +12,30 @@ use memmem::Searcher; use memmem::TwoWaySearcher; use once_cell::sync::OnceCell; -use crate::http_error; +#[derive(Debug, thiserror::Error)] +pub enum WebSocketUpgradeError { + #[error("invalid headers")] + InvalidHeaders, + #[error("{0}")] + HttpParse(#[from] httparse::Error), + #[error("{0}")] + Http(#[from] http::Error), + #[error("{0}")] + Utf8(#[from] std::str::Utf8Error), + #[error("{0}")] + InvalidHeaderName(#[from] http::header::InvalidHeaderName), + #[error("{0}")] + InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), + #[error("invalid HTTP status line")] + InvalidHttpStatusLine, + #[error("attempted to write to completed upgrade buffer")] + UpgradeBufferAlreadyCompleted, +} /// Given a buffer that ends in `\n\n` or `\r\n\r\n`, returns a parsed [`Request`]. fn parse_response( header_bytes: &[u8], -) -> Result<(usize, Response), AnyError> { +) -> Result<(usize, Response), WebSocketUpgradeError> { let mut headers = [httparse::EMPTY_HEADER; 16]; let status = httparse::parse_headers(header_bytes, &mut headers)?; match status { @@ -32,7 +49,7 @@ fn parse_response( } Ok((index, resp)) } - _ => Err(http_error("invalid headers")), + _ => Err(WebSocketUpgradeError::InvalidHeaders), } } @@ -69,11 +86,14 @@ pub struct WebSocketUpgrade { impl WebSocketUpgrade { /// Ensures that the status line starts with "HTTP/1.1 101 " which matches all of the node.js /// WebSocket libraries that are known. We don't care about the trailing status text. - fn validate_status(&self, status: &[u8]) -> Result<(), AnyError> { + fn validate_status( + &self, + status: &[u8], + ) -> Result<(), WebSocketUpgradeError> { if status.starts_with(b"HTTP/1.1 101 ") { Ok(()) } else { - Err(http_error("invalid HTTP status line")) + Err(WebSocketUpgradeError::InvalidHttpStatusLine) } } @@ -82,7 +102,7 @@ impl WebSocketUpgrade { pub fn write( &mut self, bytes: &[u8], - ) -> Result, Bytes)>, AnyError> { + ) -> Result, Bytes)>, WebSocketUpgradeError> { use WebSocketUpgradeState::*; match self.state { @@ -142,9 +162,7 @@ impl WebSocketUpgrade { Ok(None) } } - Complete => { - Err(http_error("attempted to write to completed upgrade buffer")) - } + Complete => Err(WebSocketUpgradeError::UpgradeBufferAlreadyCompleted), } } } @@ -157,8 +175,8 @@ mod tests { type ExpectedResponseAndHead = Option<(Response, &'static [u8])>; fn assert_response( - result: Result, Bytes)>, AnyError>, - expected: Result, + result: Result, Bytes)>, WebSocketUpgradeError>, + expected: Result, chunk_info: Option<(usize, usize)>, ) { let formatted = format!("{result:?}"); @@ -189,8 +207,8 @@ mod tests { "Expected Ok(None), was {formatted}", ), Err(e) => assert_eq!( - e, - result.err().map(|e| format!("{e:?}")).unwrap_or_default(), + format!("{e:?}"), + format!("{:?}", result.unwrap_err()), "Expected error, was {formatted}", ), } @@ -198,7 +216,7 @@ mod tests { fn validate_upgrade_all_at_once( s: &str, - expected: Result, + expected: Result, ) { let mut upgrade = WebSocketUpgrade::default(); let res = upgrade.write(s.as_bytes()); @@ -209,7 +227,7 @@ mod tests { fn validate_upgrade_chunks( s: &str, size: usize, - expected: Result, + expected: Result, ) { let chunk_info = Some((s.as_bytes().len(), size)); let mut upgrade = WebSocketUpgrade::default(); @@ -226,7 +244,7 @@ mod tests { fn validate_upgrade( s: &str, - expected: fn() -> Result, + expected: fn() -> Result, ) { validate_upgrade_all_at_once(s, expected()); validate_upgrade_chunks(s, 1, expected()); @@ -315,7 +333,7 @@ mod tests { #[test] fn upgrade_invalid_status() { validate_upgrade("HTTP/1.1 200 OK\nConnection: Upgrade\n\n", || { - Err("invalid HTTP status line") + Err(WebSocketUpgradeError::InvalidHttpStatusLine) }); } @@ -327,7 +345,11 @@ mod tests { .join("\n"); validate_upgrade( &format!("HTTP/1.1 101 Switching Protocols\n{headers}\n\n"), - || Err("too many headers"), + || { + Err(WebSocketUpgradeError::HttpParse( + httparse::Error::TooManyHeaders, + )) + }, ); } } diff --git a/ext/io/Cargo.toml b/ext/io/Cargo.toml index 22c346764b..6ef049ff9b 100644 --- a/ext/io/Cargo.toml +++ b/ext/io/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_io" -version = "0.78.0" +version = "0.87.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/io/bi_pipe.rs b/ext/io/bi_pipe.rs index 402e383ac9..3492e2f441 100644 --- a/ext/io/bi_pipe.rs +++ b/ext/io/bi_pipe.rs @@ -2,7 +2,6 @@ use std::rc::Rc; -use deno_core::error::AnyError; use deno_core::AsyncRefCell; use deno_core::AsyncResult; use deno_core::CancelHandle; @@ -71,13 +70,16 @@ impl BiPipeResource { pub async fn read( self: Rc, data: &mut [u8], - ) -> Result { + ) -> Result { let mut rd = RcRef::map(&self, |r| &r.read_half).borrow_mut().await; let cancel_handle = RcRef::map(&self, |r| &r.cancel); - Ok(rd.read(data).try_or_cancel(cancel_handle).await?) + rd.read(data).try_or_cancel(cancel_handle).await } - pub async fn write(self: Rc, data: &[u8]) -> Result { + pub async fn write( + self: Rc, + data: &[u8], + ) -> Result { let mut wr = RcRef::map(self, |r| &r.write_half).borrow_mut().await; let nwritten = wr.write(data).await?; wr.flush().await?; @@ -181,9 +183,10 @@ fn from_raw( ) -> Result<(BiPipeRead, BiPipeWrite), std::io::Error> { use std::os::fd::FromRawFd; // Safety: The fd is part of a pair of connected sockets - let unix_stream = tokio::net::UnixStream::from_std(unsafe { - std::os::unix::net::UnixStream::from_raw_fd(stream) - })?; + let unix_stream = + unsafe { std::os::unix::net::UnixStream::from_raw_fd(stream) }; + unix_stream.set_nonblocking(true)?; + let unix_stream = tokio::net::UnixStream::from_std(unix_stream)?; let (read, write) = unix_stream.into_split(); Ok((BiPipeRead { inner: read }, BiPipeWrite { inner: write })) } @@ -270,15 +273,15 @@ impl_async_write!(for BiPipe -> self.write_end); /// Creates both sides of a bidirectional pipe, returning the raw /// handles to the underlying OS resources. -pub fn bi_pipe_pair_raw() -> Result<(RawBiPipeHandle, RawBiPipeHandle), AnyError> -{ +pub fn bi_pipe_pair_raw( +) -> Result<(RawBiPipeHandle, RawBiPipeHandle), std::io::Error> { #[cfg(unix)] { // SockFlag is broken on macOS // https://github.com/nix-rust/nix/issues/861 let mut fds = [-1, -1]; #[cfg(not(target_os = "macos"))] - let flags = libc::SOCK_CLOEXEC | libc::SOCK_NONBLOCK; + let flags = libc::SOCK_CLOEXEC; #[cfg(target_os = "macos")] let flags = 0; @@ -293,19 +296,19 @@ pub fn bi_pipe_pair_raw() -> Result<(RawBiPipeHandle, RawBiPipeHandle), AnyError ) }; if ret != 0 { - return Err(std::io::Error::last_os_error().into()); + return Err(std::io::Error::last_os_error()); } if cfg!(target_os = "macos") { let fcntl = |fd: i32, flag: libc::c_int| -> Result<(), std::io::Error> { // SAFETY: libc call, fd is valid - let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) }; + let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; if flags == -1 { return Err(fail(fds)); } // SAFETY: libc call, fd is valid - let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | flag) }; + let ret = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | flag) }; if ret == -1 { return Err(fail(fds)); } @@ -321,13 +324,9 @@ pub fn bi_pipe_pair_raw() -> Result<(RawBiPipeHandle, RawBiPipeHandle), AnyError std::io::Error::last_os_error() } - // SOCK_NONBLOCK is not supported on macOS. - (fcntl)(fds[0], libc::O_NONBLOCK)?; - (fcntl)(fds[1], libc::O_NONBLOCK)?; - // SOCK_CLOEXEC is not supported on macOS. - (fcntl)(fds[0], libc::FD_CLOEXEC)?; - (fcntl)(fds[1], libc::FD_CLOEXEC)?; + fcntl(fds[0], libc::FD_CLOEXEC)?; + fcntl(fds[1], libc::FD_CLOEXEC)?; } let fd1 = fds[0]; @@ -389,7 +388,7 @@ pub fn bi_pipe_pair_raw() -> Result<(RawBiPipeHandle, RawBiPipeHandle), AnyError continue; } - return Err(err.into()); + return Err(err); } break (path, hd1); @@ -411,7 +410,7 @@ pub fn bi_pipe_pair_raw() -> Result<(RawBiPipeHandle, RawBiPipeHandle), AnyError 0, ); if hd2 == INVALID_HANDLE_VALUE { - return Err(io::Error::last_os_error().into()); + return Err(io::Error::last_os_error()); } // Will not block because we have create the pair. @@ -419,7 +418,7 @@ pub fn bi_pipe_pair_raw() -> Result<(RawBiPipeHandle, RawBiPipeHandle), AnyError let err = std::io::Error::last_os_error(); if err.raw_os_error() != Some(ERROR_PIPE_CONNECTED as i32) { CloseHandle(hd2); - return Err(err.into()); + return Err(err); } } diff --git a/ext/io/fs.rs b/ext/io/fs.rs index 3798c1429d..7ef02315ba 100644 --- a/ext/io/fs.rs +++ b/ext/io/fs.rs @@ -1,15 +1,12 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::borrow::Cow; +use std::fmt::Formatter; use std::io; use std::rc::Rc; use std::time::SystemTime; use std::time::UNIX_EPOCH; -use deno_core::error::custom_error; -use deno_core::error::not_supported; -use deno_core::error::resource_unavailable; -use deno_core::error::AnyError; use deno_core::BufMutView; use deno_core::BufView; use deno_core::OpState; @@ -25,6 +22,21 @@ pub enum FsError { NotCapable(&'static str), } +impl std::fmt::Display for FsError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FsError::Io(err) => std::fmt::Display::fmt(err, f), + FsError::FileBusy => f.write_str("file busy"), + FsError::NotSupported => f.write_str("not supported"), + FsError::NotCapable(err) => { + f.write_str(&format!("requires {err} access")) + } + } + } +} + +impl std::error::Error for FsError {} + impl FsError { pub fn kind(&self) -> io::ErrorKind { match self { @@ -59,19 +71,6 @@ impl From for FsError { } } -impl From for AnyError { - fn from(err: FsError) -> Self { - match err { - FsError::Io(err) => AnyError::from(err), - FsError::FileBusy => resource_unavailable(), - FsError::NotSupported => not_supported(), - FsError::NotCapable(err) => { - custom_error("NotCapable", format!("permission denied: {err}")) - } - } - } -} - impl From for FsError { fn from(err: JoinError) -> Self { if err.is_cancelled() { @@ -95,6 +94,7 @@ pub struct FsStat { pub mtime: Option, pub atime: Option, pub birthtime: Option, + pub ctime: Option, pub dev: u64, pub ino: u64, @@ -154,6 +154,16 @@ impl FsStat { } } + #[inline(always)] + fn get_ctime(ctime_or_0: i64) -> Option { + if ctime_or_0 > 0 { + // ctime return seconds since epoch, but we need milliseconds + return Some(ctime_or_0 as u64 * 1000); + } + + None + } + Self { is_file: metadata.is_file(), is_directory: metadata.is_dir(), @@ -163,6 +173,7 @@ impl FsStat { mtime: to_msec(metadata.modified()), atime: to_msec(metadata.accessed()), birthtime: to_msec(metadata.created()), + ctime: get_ctime(unix_or_zero!(ctime)), dev: unix_or_zero!(dev), ino: unix_or_zero!(ino), @@ -266,9 +277,9 @@ impl FileResource { state: &OpState, rid: ResourceId, f: F, - ) -> Result + ) -> Result where - F: FnOnce(Rc) -> Result, + F: FnOnce(Rc) -> Result, { let resource = state.resource_table.get::(rid)?; f(resource) @@ -277,7 +288,7 @@ impl FileResource { pub fn get_file( state: &OpState, rid: ResourceId, - ) -> Result, AnyError> { + ) -> Result, deno_core::error::AnyError> { let resource = state.resource_table.get::(rid)?; Ok(resource.file()) } @@ -286,9 +297,9 @@ impl FileResource { state: &OpState, rid: ResourceId, f: F, - ) -> Result + ) -> Result where - F: FnOnce(Rc) -> Result, + F: FnOnce(Rc) -> Result, { Self::with_resource(state, rid, |r| f(r.file.clone())) } @@ -303,10 +314,7 @@ impl deno_core::Resource for FileResource { Cow::Borrowed(&self.name) } - fn read( - self: Rc, - limit: usize, - ) -> deno_core::AsyncResult { + fn read(self: Rc, limit: usize) -> deno_core::AsyncResult { Box::pin(async move { self .file @@ -319,8 +327,8 @@ impl deno_core::Resource for FileResource { fn read_byob( self: Rc, - buf: deno_core::BufMutView, - ) -> deno_core::AsyncResult<(usize, deno_core::BufMutView)> { + buf: BufMutView, + ) -> deno_core::AsyncResult<(usize, BufMutView)> { Box::pin(async move { self .file @@ -333,17 +341,14 @@ impl deno_core::Resource for FileResource { fn write( self: Rc, - buf: deno_core::BufView, + buf: BufView, ) -> deno_core::AsyncResult { Box::pin(async move { self.file.clone().write(buf).await.map_err(|err| err.into()) }) } - fn write_all( - self: Rc, - buf: deno_core::BufView, - ) -> deno_core::AsyncResult<()> { + fn write_all(self: Rc, buf: BufView) -> deno_core::AsyncResult<()> { Box::pin(async move { self .file diff --git a/ext/io/lib.rs b/ext/io/lib.rs index a07d64ae37..5d183aa464 100644 --- a/ext/io/lib.rs +++ b/ext/io/lib.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use deno_core::unsync::spawn_blocking; use deno_core::unsync::TaskQueue; @@ -48,6 +47,7 @@ use winapi::um::processenv::GetStdHandle; #[cfg(windows)] use winapi::um::winbase; +use deno_core::futures::TryFutureExt; #[cfg(windows)] use parking_lot::Condvar; #[cfg(windows)] @@ -348,13 +348,13 @@ where RcRef::map(self, |r| &r.stream).borrow_mut() } - async fn write(self: Rc, data: &[u8]) -> Result { + async fn write(self: Rc, data: &[u8]) -> Result { let mut stream = self.borrow_mut().await; let nwritten = stream.write(data).await?; Ok(nwritten) } - async fn shutdown(self: Rc) -> Result<(), AnyError> { + async fn shutdown(self: Rc) -> Result<(), io::Error> { let mut stream = self.borrow_mut().await; stream.shutdown().await?; Ok(()) @@ -396,7 +396,7 @@ where self.cancel_handle.cancel() } - async fn read(self: Rc, data: &mut [u8]) -> Result { + async fn read(self: Rc, data: &mut [u8]) -> Result { let mut rd = self.borrow_mut().await; let nread = rd.read(data).try_or_cancel(self.cancel_handle()).await?; Ok(nread) @@ -417,7 +417,7 @@ impl Resource for ChildStdinResource { deno_core::impl_writable!(); fn shutdown(self: Rc) -> AsyncResult<()> { - Box::pin(self.shutdown()) + Box::pin(self.shutdown().map_err(|e| e.into())) } } @@ -1010,7 +1010,7 @@ pub fn op_print( state: &mut OpState, #[string] msg: &str, is_err: bool, -) -> Result<(), AnyError> { +) -> Result<(), deno_core::error::AnyError> { let rid = if is_err { 2 } else { 1 }; FileResource::with_file(state, rid, move |file| { Ok(file.write_all_sync(msg.as_bytes())?) diff --git a/ext/kv/Cargo.toml b/ext/kv/Cargo.toml index eee4762eb4..1d7b91770f 100644 --- a/ext/kv/Cargo.toml +++ b/ext/kv/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_kv" -version = "0.76.0" +version = "0.85.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -21,6 +21,7 @@ bytes.workspace = true chrono = { workspace = true, features = ["now"] } deno_core.workspace = true deno_fetch.workspace = true +deno_path_util.workspace = true deno_permissions.workspace = true deno_tls.workspace = true denokv_proto.workspace = true @@ -35,6 +36,7 @@ prost.workspace = true rand.workspace = true rusqlite.workspace = true serde.workspace = true +thiserror.workspace = true url.workspace = true [build-dependencies] diff --git a/ext/kv/config.rs b/ext/kv/config.rs index 6e2e2c3a1f..7166bcbcc2 100644 --- a/ext/kv/config.rs +++ b/ext/kv/config.rs @@ -1,16 +1,17 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#[derive(Clone, Copy, Debug)] pub struct KvConfig { - pub(crate) max_write_key_size_bytes: usize, - pub(crate) max_read_key_size_bytes: usize, - pub(crate) max_value_size_bytes: usize, - pub(crate) max_read_ranges: usize, - pub(crate) max_read_entries: usize, - pub(crate) max_checks: usize, - pub(crate) max_mutations: usize, - pub(crate) max_watched_keys: usize, - pub(crate) max_total_mutation_size_bytes: usize, - pub(crate) max_total_key_size_bytes: usize, + pub max_write_key_size_bytes: usize, + pub max_read_key_size_bytes: usize, + pub max_value_size_bytes: usize, + pub max_read_ranges: usize, + pub max_read_entries: usize, + pub max_checks: usize, + pub max_mutations: usize, + pub max_watched_keys: usize, + pub max_total_mutation_size_bytes: usize, + pub max_total_key_size_bytes: usize, } impl KvConfig { diff --git a/ext/kv/lib.rs b/ext/kv/lib.rs index 13e4f1662f..a4ccfe3d63 100644 --- a/ext/kv/lib.rs +++ b/ext/kv/lib.rs @@ -12,15 +12,11 @@ use std::num::NonZeroU32; use std::rc::Rc; use std::time::Duration; -use anyhow::bail; use base64::prelude::BASE64_URL_SAFE; use base64::Engine; use chrono::DateTime; use chrono::Utc; -use deno_core::anyhow::Context; use deno_core::error::get_custom_error_class; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::futures::StreamExt; use deno_core::op2; use deno_core::serde_v8::AnyValue; @@ -118,12 +114,72 @@ impl Resource for DatabaseWatcherResource { } } +#[derive(Debug, thiserror::Error)] +pub enum KvError { + #[error(transparent)] + DatabaseHandler(deno_core::error::AnyError), + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error("Too many ranges (max {0})")] + TooManyRanges(usize), + #[error("Too many entries (max {0})")] + TooManyEntries(usize), + #[error("Too many checks (max {0})")] + TooManyChecks(usize), + #[error("Too many mutations (max {0})")] + TooManyMutations(usize), + #[error("Too many keys (max {0})")] + TooManyKeys(usize), + #[error("limit must be greater than 0")] + InvalidLimit, + #[error("Invalid boundary key")] + InvalidBoundaryKey, + #[error("Key too large for read (max {0} bytes)")] + KeyTooLargeToRead(usize), + #[error("Key too large for write (max {0} bytes)")] + KeyTooLargeToWrite(usize), + #[error("Total mutation size too large (max {0} bytes)")] + TotalMutationTooLarge(usize), + #[error("Total key size too large (max {0} bytes)")] + TotalKeyTooLarge(usize), + #[error(transparent)] + Kv(deno_core::error::AnyError), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Queue message not found")] + QueueMessageNotFound, + #[error("Start key is not in the keyspace defined by prefix")] + StartKeyNotInKeyspace, + #[error("End key is not in the keyspace defined by prefix")] + EndKeyNotInKeyspace, + #[error("Start key is greater than end key")] + StartKeyGreaterThanEndKey, + #[error("Invalid check")] + InvalidCheck(#[source] KvCheckError), + #[error("Invalid mutation")] + InvalidMutation(#[source] KvMutationError), + #[error("Invalid enqueue")] + InvalidEnqueue(#[source] std::io::Error), + #[error("key cannot be empty")] + EmptyKey, // TypeError + #[error("Value too large (max {0} bytes)")] + ValueTooLarge(usize), // TypeError + #[error("enqueue payload too large (max {0} bytes)")] + EnqueuePayloadTooLarge(usize), // TypeError + #[error("invalid cursor")] + InvalidCursor, + #[error("cursor out of bounds")] + CursorOutOfBounds, + #[error("Invalid range")] + InvalidRange, +} + #[op2(async)] #[smi] async fn op_kv_database_open( state: Rc>, #[string] path: Option, -) -> Result +) -> Result where DBH: DatabaseHandler + 'static, { @@ -134,7 +190,10 @@ where .check_or_exit(UNSTABLE_FEATURE_NAME, "Deno.openKv"); state.borrow::>().clone() }; - let db = handler.open(state.clone(), path).await?; + let db = handler + .open(state.clone(), path) + .await + .map_err(KvError::DatabaseHandler)?; let rid = state.borrow_mut().resource_table.add(DatabaseResource { db, cancel_handle: CancelHandle::new_rc(), @@ -184,8 +243,8 @@ enum ToV8Value { } impl TryFrom for KvValue { - type Error = AnyError; - fn try_from(value: FromV8Value) -> Result { + type Error = num_bigint::TryFromBigIntError; + fn try_from(value: FromV8Value) -> Result { Ok(match value { FromV8Value::V8(buf) => KvValue::V8(buf.to_vec()), FromV8Value::Bytes(buf) => KvValue::Bytes(buf.to_vec()), @@ -214,8 +273,8 @@ struct ToV8KvEntry { } impl TryFrom for ToV8KvEntry { - type Error = AnyError; - fn try_from(entry: KvEntry) -> Result { + type Error = std::io::Error; + fn try_from(entry: KvEntry) -> Result { Ok(ToV8KvEntry { key: decode_key(&entry.key)? .0 @@ -261,14 +320,16 @@ async fn op_kv_snapshot_read( #[smi] rid: ResourceId, #[serde] ranges: Vec, #[serde] consistency: V8Consistency, -) -> Result>, AnyError> +) -> Result>, KvError> where DBH: DatabaseHandler + 'static, { let db = { let state = state.borrow(); - let resource = - state.resource_table.get::>(rid)?; + let resource = state + .resource_table + .get::>(rid) + .map_err(KvError::Resource)?; resource.db.clone() }; @@ -278,10 +339,7 @@ where }; if ranges.len() > config.max_read_ranges { - return Err(type_error(format!( - "Too many ranges (max {})", - config.max_read_ranges - ))); + return Err(KvError::TooManyRanges(config.max_read_ranges)); } let mut total_entries = 0usize; @@ -300,33 +358,32 @@ where Ok(ReadRange { start, end, - limit: NonZeroU32::new(limit) - .with_context(|| "limit must be greater than 0")?, + limit: NonZeroU32::new(limit).ok_or(KvError::InvalidLimit)?, reverse, }) }) - .collect::, AnyError>>()?; + .collect::, KvError>>()?; if total_entries > config.max_read_entries { - return Err(type_error(format!( - "Too many entries (max {})", - config.max_read_entries - ))); + return Err(KvError::TooManyEntries(config.max_read_entries)); } let opts = SnapshotReadOptions { consistency: consistency.into(), }; - let output_ranges = db.snapshot_read(read_ranges, opts).await?; + let output_ranges = db + .snapshot_read(read_ranges, opts) + .await + .map_err(KvError::Kv)?; let output_ranges = output_ranges .into_iter() .map(|x| { x.entries .into_iter() .map(TryInto::try_into) - .collect::, AnyError>>() + .collect::, std::io::Error>>() }) - .collect::, AnyError>>()?; + .collect::, std::io::Error>>()?; Ok(output_ranges) } @@ -345,7 +402,7 @@ impl Resource for QueueMessageResource { async fn op_kv_dequeue_next_message( state: Rc>, #[smi] rid: ResourceId, -) -> Result, AnyError> +) -> Result, KvError> where DBH: DatabaseHandler + 'static, { @@ -358,17 +415,19 @@ where if get_custom_error_class(&err) == Some("BadResource") { return Ok(None); } else { - return Err(err); + return Err(KvError::Resource(err)); } } }; resource.db.clone() }; - let Some(mut handle) = db.dequeue_next_message().await? else { + let Some(mut handle) = + db.dequeue_next_message().await.map_err(KvError::Kv)? + else { return Ok(None); }; - let payload = handle.take_payload().await?.into(); + let payload = handle.take_payload().await.map_err(KvError::Kv)?.into(); let handle_rid = { let mut state = state.borrow_mut(); state.resource_table.add(QueueMessageResource { handle }) @@ -382,18 +441,18 @@ fn op_kv_watch( state: &mut OpState, #[smi] rid: ResourceId, #[serde] keys: Vec, -) -> Result +) -> Result where DBH: DatabaseHandler + 'static, { - let resource = state.resource_table.get::>(rid)?; + let resource = state + .resource_table + .get::>(rid) + .map_err(KvError::Resource)?; let config = state.borrow::>().clone(); if keys.len() > config.max_watched_keys { - return Err(type_error(format!( - "Too many keys (max {})", - config.max_watched_keys - ))); + return Err(KvError::TooManyKeys(config.max_watched_keys)); } let keys: Vec> = keys @@ -428,10 +487,13 @@ enum WatchEntry { async fn op_kv_watch_next( state: Rc>, #[smi] rid: ResourceId, -) -> Result>, AnyError> { +) -> Result>, KvError> { let resource = { let state = state.borrow(); - let resource = state.resource_table.get::(rid)?; + let resource = state + .resource_table + .get::(rid) + .map_err(KvError::Resource)?; resource.clone() }; @@ -457,7 +519,7 @@ async fn op_kv_watch_next( return Ok(None); }; - let entries = res?; + let entries = res.map_err(KvError::Kv)?; let entries = entries .into_iter() .map(|entry| { @@ -468,7 +530,7 @@ async fn op_kv_watch_next( WatchKeyOutput::Unchanged => WatchEntry::Unchanged, }) }) - .collect::>()?; + .collect::>()?; Ok(Some(entries)) } @@ -478,7 +540,7 @@ async fn op_kv_finish_dequeued_message( state: Rc>, #[smi] handle_rid: ResourceId, success: bool, -) -> Result<(), AnyError> +) -> Result<(), KvError> where DBH: DatabaseHandler + 'static, { @@ -487,9 +549,9 @@ where let handle = state .resource_table .take::::DB as Database>::QMH>>(handle_rid) - .map_err(|_| type_error("Queue message not found"))?; + .map_err(|_| KvError::QueueMessageNotFound)?; Rc::try_unwrap(handle) - .map_err(|_| type_error("Queue message not found"))? + .map_err(|_| KvError::QueueMessageNotFound)? .handle }; // if we fail to finish the message, there is not much we can do and the @@ -500,32 +562,52 @@ where Ok(()) } +#[derive(Debug, thiserror::Error)] +pub enum KvCheckError { + #[error("invalid versionstamp")] + InvalidVersionstamp, + #[error(transparent)] + Io(std::io::Error), +} + type V8KvCheck = (KvKey, Option); -fn check_from_v8(value: V8KvCheck) -> Result { +fn check_from_v8(value: V8KvCheck) -> Result { let versionstamp = match value.1 { Some(data) => { let mut out = [0u8; 10]; if data.len() != out.len() * 2 { - bail!(type_error("invalid versionstamp")); + return Err(KvCheckError::InvalidVersionstamp); } faster_hex::hex_decode(&data, &mut out) - .map_err(|_| type_error("invalid versionstamp"))?; + .map_err(|_| KvCheckError::InvalidVersionstamp)?; Some(out) } None => None, }; Ok(Check { - key: encode_v8_key(value.0)?, + key: encode_v8_key(value.0).map_err(KvCheckError::Io)?, versionstamp, }) } +#[derive(Debug, thiserror::Error)] +pub enum KvMutationError { + #[error(transparent)] + BigInt(#[from] num_bigint::TryFromBigIntError), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Invalid mutation '{0}' with value")] + InvalidMutationWithValue(String), + #[error("Invalid mutation '{0}' without value")] + InvalidMutationWithoutValue(String), +} + type V8KvMutation = (KvKey, String, Option, Option); fn mutation_from_v8( (value, current_timstamp): (V8KvMutation, DateTime), -) -> Result { +) -> Result { let key = encode_v8_key(value.0)?; let kind = match (value.1.as_str(), value.2) { ("set", Some(value)) => MutationKind::Set(value.try_into()?), @@ -542,10 +624,10 @@ fn mutation_from_v8( MutationKind::SetSuffixVersionstampedKey(value.try_into()?) } (op, Some(_)) => { - return Err(type_error(format!("Invalid mutation '{op}' with value"))) + return Err(KvMutationError::InvalidMutationWithValue(op.to_string())) } (op, None) => { - return Err(type_error(format!("Invalid mutation '{op}' without value"))) + return Err(KvMutationError::InvalidMutationWithoutValue(op.to_string())) } }; Ok(Mutation { @@ -562,7 +644,7 @@ type V8Enqueue = (JsBuffer, u64, Vec, Option>); fn enqueue_from_v8( value: V8Enqueue, current_timestamp: DateTime, -) -> Result { +) -> Result { Ok(Enqueue { payload: value.0.to_vec(), deadline: current_timestamp @@ -597,7 +679,7 @@ impl RawSelector { prefix: Option, start: Option, end: Option, - ) -> Result { + ) -> Result { let prefix = prefix.map(encode_v8_key).transpose()?; let start = start.map(encode_v8_key).transpose()?; let end = end.map(encode_v8_key).transpose()?; @@ -610,9 +692,7 @@ impl RawSelector { }), (Some(prefix), Some(start), None) => { if !start.starts_with(&prefix) || start.len() == prefix.len() { - return Err(type_error( - "Start key is not in the keyspace defined by prefix", - )); + return Err(KvError::StartKeyNotInKeyspace); } Ok(Self::Prefixed { prefix, @@ -622,9 +702,7 @@ impl RawSelector { } (Some(prefix), None, Some(end)) => { if !end.starts_with(&prefix) || end.len() == prefix.len() { - return Err(type_error( - "End key is not in the keyspace defined by prefix", - )); + return Err(KvError::EndKeyNotInKeyspace); } Ok(Self::Prefixed { prefix, @@ -634,7 +712,7 @@ impl RawSelector { } (None, Some(start), Some(end)) => { if start > end { - return Err(type_error("Start key is greater than end key")); + return Err(KvError::StartKeyGreaterThanEndKey); } Ok(Self::Range { start, end }) } @@ -642,7 +720,7 @@ impl RawSelector { let end = start.iter().copied().chain(Some(0)).collect(); Ok(Self::Range { start, end }) } - _ => Err(type_error("Invalid range")), + _ => Err(KvError::InvalidRange), } } @@ -701,10 +779,10 @@ fn common_prefix_for_bytes<'a>(a: &'a [u8], b: &'a [u8]) -> &'a [u8] { fn encode_cursor( selector: &RawSelector, boundary_key: &[u8], -) -> Result { +) -> Result { let common_prefix = selector.common_prefix(); if !boundary_key.starts_with(common_prefix) { - return Err(type_error("Invalid boundary key")); + return Err(KvError::InvalidBoundaryKey); } Ok(BASE64_URL_SAFE.encode(&boundary_key[common_prefix.len()..])) } @@ -713,7 +791,7 @@ fn decode_selector_and_cursor( selector: &RawSelector, reverse: bool, cursor: Option<&ByteString>, -) -> Result<(Vec, Vec), AnyError> { +) -> Result<(Vec, Vec), KvError> { let Some(cursor) = cursor else { return Ok((selector.range_start_key(), selector.range_end_key())); }; @@ -721,7 +799,7 @@ fn decode_selector_and_cursor( let common_prefix = selector.common_prefix(); let cursor = BASE64_URL_SAFE .decode(cursor) - .map_err(|_| type_error("invalid cursor"))?; + .map_err(|_| KvError::InvalidCursor)?; let first_key: Vec; let last_key: Vec; @@ -746,13 +824,13 @@ fn decode_selector_and_cursor( // Defend against out-of-bounds reading if let Some(start) = selector.start() { if &first_key[..] < start { - return Err(type_error("cursor out of bounds")); + return Err(KvError::CursorOutOfBounds); } } if let Some(end) = selector.end() { if &last_key[..] > end { - return Err(type_error("cursor out of bounds")); + return Err(KvError::CursorOutOfBounds); } } @@ -767,15 +845,17 @@ async fn op_kv_atomic_write( #[serde] checks: Vec, #[serde] mutations: Vec, #[serde] enqueues: Vec, -) -> Result, AnyError> +) -> Result, KvError> where DBH: DatabaseHandler + 'static, { let current_timestamp = chrono::Utc::now(); let db = { let state = state.borrow(); - let resource = - state.resource_table.get::>(rid)?; + let resource = state + .resource_table + .get::>(rid) + .map_err(KvError::Resource)?; resource.db.clone() }; @@ -785,34 +865,28 @@ where }; if checks.len() > config.max_checks { - return Err(type_error(format!( - "Too many checks (max {})", - config.max_checks - ))); + return Err(KvError::TooManyChecks(config.max_checks)); } if mutations.len() + enqueues.len() > config.max_mutations { - return Err(type_error(format!( - "Too many mutations (max {})", - config.max_mutations - ))); + return Err(KvError::TooManyMutations(config.max_mutations)); } let checks = checks .into_iter() .map(check_from_v8) - .collect::, AnyError>>() - .with_context(|| "invalid check")?; + .collect::, KvCheckError>>() + .map_err(KvError::InvalidCheck)?; let mutations = mutations .into_iter() .map(|mutation| mutation_from_v8((mutation, current_timestamp))) - .collect::, AnyError>>() - .with_context(|| "Invalid mutation")?; + .collect::, KvMutationError>>() + .map_err(KvError::InvalidMutation)?; let enqueues = enqueues .into_iter() .map(|e| enqueue_from_v8(e, current_timestamp)) - .collect::, AnyError>>() - .with_context(|| "invalid enqueue")?; + .collect::, std::io::Error>>() + .map_err(KvError::InvalidEnqueue)?; let mut total_payload_size = 0usize; let mut total_key_size = 0usize; @@ -823,7 +897,7 @@ where .chain(mutations.iter().map(|m| &m.key)) { if key.is_empty() { - return Err(type_error("key cannot be empty")); + return Err(KvError::EmptyKey); } total_payload_size += check_write_key_size(key, &config)?; @@ -847,17 +921,13 @@ where } if total_payload_size > config.max_total_mutation_size_bytes { - return Err(type_error(format!( - "Total mutation size too large (max {} bytes)", - config.max_total_mutation_size_bytes - ))); + return Err(KvError::TotalMutationTooLarge( + config.max_total_mutation_size_bytes, + )); } if total_key_size > config.max_total_key_size_bytes { - return Err(type_error(format!( - "Total key size too large (max {} bytes)", - config.max_total_key_size_bytes - ))); + return Err(KvError::TotalKeyTooLarge(config.max_total_key_size_bytes)); } let atomic_write = AtomicWrite { @@ -866,7 +936,7 @@ where enqueues, }; - let result = db.atomic_write(atomic_write).await?; + let result = db.atomic_write(atomic_write).await.map_err(KvError::Kv)?; Ok(result.map(|res| faster_hex::hex_string(&res.versionstamp))) } @@ -879,19 +949,16 @@ type EncodeCursorRangeSelector = (Option, Option, Option); fn op_kv_encode_cursor( #[serde] (prefix, start, end): EncodeCursorRangeSelector, #[serde] boundary_key: KvKey, -) -> Result { +) -> Result { let selector = RawSelector::from_tuple(prefix, start, end)?; let boundary_key = encode_v8_key(boundary_key)?; let cursor = encode_cursor(&selector, &boundary_key)?; Ok(cursor) } -fn check_read_key_size(key: &[u8], config: &KvConfig) -> Result<(), AnyError> { +fn check_read_key_size(key: &[u8], config: &KvConfig) -> Result<(), KvError> { if key.len() > config.max_read_key_size_bytes { - Err(type_error(format!( - "Key too large for read (max {} bytes)", - config.max_read_key_size_bytes - ))) + Err(KvError::KeyTooLargeToRead(config.max_read_key_size_bytes)) } else { Ok(()) } @@ -900,12 +967,9 @@ fn check_read_key_size(key: &[u8], config: &KvConfig) -> Result<(), AnyError> { fn check_write_key_size( key: &[u8], config: &KvConfig, -) -> Result { +) -> Result { if key.len() > config.max_write_key_size_bytes { - Err(type_error(format!( - "Key too large for write (max {} bytes)", - config.max_write_key_size_bytes - ))) + Err(KvError::KeyTooLargeToWrite(config.max_write_key_size_bytes)) } else { Ok(key.len()) } @@ -914,7 +978,7 @@ fn check_write_key_size( fn check_value_size( value: &KvValue, config: &KvConfig, -) -> Result { +) -> Result { let payload = match value { KvValue::Bytes(x) => x, KvValue::V8(x) => x, @@ -922,10 +986,7 @@ fn check_value_size( }; if payload.len() > config.max_value_size_bytes { - Err(type_error(format!( - "Value too large (max {} bytes)", - config.max_value_size_bytes - ))) + Err(KvError::ValueTooLarge(config.max_value_size_bytes)) } else { Ok(payload.len()) } @@ -934,12 +995,9 @@ fn check_value_size( fn check_enqueue_payload_size( payload: &[u8], config: &KvConfig, -) -> Result { +) -> Result { if payload.len() > config.max_value_size_bytes { - Err(type_error(format!( - "enqueue payload too large (max {} bytes)", - config.max_value_size_bytes - ))) + Err(KvError::EnqueuePayloadTooLarge(config.max_value_size_bytes)) } else { Ok(payload.len()) } diff --git a/ext/kv/remote.rs b/ext/kv/remote.rs index 922853588a..4930aacfe3 100644 --- a/ext/kv/remote.rs +++ b/ext/kv/remote.rs @@ -15,6 +15,7 @@ use deno_core::futures::Stream; use deno_core::OpState; use deno_fetch::create_http_client; use deno_fetch::CreateHttpClientOptions; +use deno_permissions::PermissionCheckError; use deno_tls::rustls::RootCertStore; use deno_tls::Proxy; use deno_tls::RootCertStoreProvider; @@ -45,17 +46,17 @@ impl HttpOptions { } pub trait RemoteDbHandlerPermissions { - fn check_env(&mut self, var: &str) -> Result<(), AnyError>; + fn check_env(&mut self, var: &str) -> Result<(), PermissionCheckError>; fn check_net_url( &mut self, url: &Url, api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result<(), PermissionCheckError>; } impl RemoteDbHandlerPermissions for deno_permissions::PermissionsContainer { #[inline(always)] - fn check_env(&mut self, var: &str) -> Result<(), AnyError> { + fn check_env(&mut self, var: &str) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_env(self, var) } @@ -64,7 +65,7 @@ impl RemoteDbHandlerPermissions for deno_permissions::PermissionsContainer { &mut self, url: &Url, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_net_url(self, url, api_name) } } @@ -103,7 +104,9 @@ impl denokv_remote::RemotePermissions fn check_net_url(&self, url: &Url) -> Result<(), anyhow::Error> { let mut state = self.state.borrow_mut(); let permissions = state.borrow_mut::

(); - permissions.check_net_url(url, "Deno.openKv") + permissions + .check_net_url(url, "Deno.openKv") + .map_err(Into::into) } } diff --git a/ext/kv/sqlite.rs b/ext/kv/sqlite.rs index 8027ff03d4..9de5209275 100644 --- a/ext/kv/sqlite.rs +++ b/ext/kv/sqlite.rs @@ -13,20 +13,20 @@ use std::sync::Arc; use std::sync::Mutex; use std::sync::OnceLock; +use crate::DatabaseHandler; use async_trait::async_trait; use deno_core::error::type_error; use deno_core::error::AnyError; -use deno_core::normalize_path; use deno_core::unsync::spawn_blocking; use deno_core::OpState; +use deno_path_util::normalize_path; +use deno_permissions::PermissionCheckError; pub use denokv_sqlite::SqliteBackendError; use denokv_sqlite::SqliteConfig; use denokv_sqlite::SqliteNotifier; use rand::SeedableRng; use rusqlite::OpenFlags; -use crate::DatabaseHandler; - static SQLITE_NOTIFIERS_MAP: OnceLock>> = OnceLock::new(); @@ -42,13 +42,13 @@ pub trait SqliteDbHandlerPermissions { &mut self, p: &str, api_name: &str, - ) -> Result; + ) -> Result; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write<'a>( &mut self, p: &'a Path, api_name: &str, - ) -> Result, AnyError>; + ) -> Result, PermissionCheckError>; } impl SqliteDbHandlerPermissions for deno_permissions::PermissionsContainer { @@ -57,7 +57,7 @@ impl SqliteDbHandlerPermissions for deno_permissions::PermissionsContainer { &mut self, p: &str, api_name: &str, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_read(self, p, api_name) } @@ -66,7 +66,7 @@ impl SqliteDbHandlerPermissions for deno_permissions::PermissionsContainer { &mut self, p: &'a Path, api_name: &str, - ) -> Result, AnyError> { + ) -> Result, PermissionCheckError> { deno_permissions::PermissionsContainer::check_write_path(self, p, api_name) } } diff --git a/ext/napi/Cargo.toml b/ext/napi/Cargo.toml index aabf6d31f5..df3ec0287b 100644 --- a/ext/napi/Cargo.toml +++ b/ext/napi/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_napi" -version = "0.99.0" +version = "0.108.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -16,4 +16,14 @@ path = "lib.rs" [dependencies] deno_core.workspace = true deno_permissions.workspace = true +libc.workspace = true libloading = { version = "0.7" } +log.workspace = true +napi_sym.workspace = true +thiserror.workspace = true + +[target.'cfg(windows)'.dependencies] +windows-sys.workspace = true + +[dev-dependencies] +libuv-sys-lite = "=1.48.2" diff --git a/ext/napi/README.md b/ext/napi/README.md index e69de29bb2..b479295244 100644 --- a/ext/napi/README.md +++ b/ext/napi/README.md @@ -0,0 +1,114 @@ +# napi + +This directory contains source for Deno's Node-API implementation. It depends on +`napi_sym` and `deno_napi`. + +Files are generally organized the same as in Node.js's implementation to ease in +ensuring compatibility. + +## Adding a new function + +Add the symbol name to +[`cli/napi_sym/symbol_exports.json`](../napi_sym/symbol_exports.json). + +```diff +{ + "symbols": [ + ... + "napi_get_undefined", +- "napi_get_null" ++ "napi_get_null", ++ "napi_get_boolean" + ] +} +``` + +Determine where to place the implementation. `napi_get_boolean` is related to JS +values so we will place it in `js_native_api.rs`. If something is not clear, +just create a new file module. + +See [`napi_sym`](../napi_sym/) for writing the implementation: + +```rust +#[napi_sym::napi_sym] +fn napi_get_boolean( + env: *mut Env, + value: bool, + result: *mut napi_value, +) -> Result { + // ... + Ok(()) +} +``` + +Update the generated symbol lists using the script: + +``` +deno run --allow-write tools/napi/generate_symbols_lists.js +``` + +Add a test in [`/tests/napi`](../../tests/napi/). You can also refer to Node.js +test suite for Node-API. + +```js +// tests/napi/boolean_test.js +import { assertEquals, loadTestLibrary } from "./common.js"; +const lib = loadTestLibrary(); +Deno.test("napi get boolean", function () { + assertEquals(lib.test_get_boolean(true), true); + assertEquals(lib.test_get_boolean(false), false); +}); +``` + +```rust +// tests/napi/src/boolean.rs + +use napi_sys::Status::napi_ok; +use napi_sys::ValueType::napi_boolean; +use napi_sys::*; + +extern "C" fn test_boolean( + env: napi_env, + info: napi_callback_info, +) -> napi_value { + let (args, argc, _) = crate::get_callback_info!(env, info, 1); + assert_eq!(argc, 1); + + let mut ty = -1; + assert!(unsafe { napi_typeof(env, args[0], &mut ty) } == napi_ok); + assert_eq!(ty, napi_boolean); + + // Use napi_get_boolean here... + + value +} + +pub fn init(env: napi_env, exports: napi_value) { + let properties = &[crate::new_property!(env, "test_boolean\0", test_boolean)]; + + unsafe { + napi_define_properties(env, exports, properties.len(), properties.as_ptr()) + }; +} +``` + +```diff +// tests/napi/src/lib.rs + ++ mod boolean; + +... + +#[no_mangle] +unsafe extern "C" fn napi_register_module_v1( + env: napi_env, + exports: napi_value, +) -> napi_value { + ... ++ boolean::init(env, exports); + + exports +} +``` + +Run the test using `cargo test -p tests/napi`. diff --git a/ext/napi/build.rs b/ext/napi/build.rs new file mode 100644 index 0000000000..8705830a95 --- /dev/null +++ b/ext/napi/build.rs @@ -0,0 +1,22 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +fn main() { + let symbols_file_name = match std::env::consts::OS { + "android" | "freebsd" | "openbsd" => { + "generated_symbol_exports_list_linux.def".to_string() + } + os => format!("generated_symbol_exports_list_{}.def", os), + }; + let symbols_path = std::path::Path::new(".") + .join(symbols_file_name) + .canonicalize() + .expect( + "Missing symbols list! Generate using tools/napi/generate_symbols_lists.js", + ); + + println!("cargo:rustc-rerun-if-changed={}", symbols_path.display()); + + let path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()) + .join("napi_symbol_path.txt"); + std::fs::write(path, symbols_path.as_os_str().as_encoded_bytes()).unwrap(); +} diff --git a/cli/napi/generated_symbol_exports_list_linux.def b/ext/napi/generated_symbol_exports_list_linux.def similarity index 96% rename from cli/napi/generated_symbol_exports_list_linux.def rename to ext/napi/generated_symbol_exports_list_linux.def index 06e94f05bb..614880ebfa 100644 --- a/cli/napi/generated_symbol_exports_list_linux.def +++ b/ext/napi/generated_symbol_exports_list_linux.def @@ -1 +1 @@ -{ "node_api_create_syntax_error"; "napi_make_callback"; "napi_has_named_property"; "napi_async_destroy"; "napi_coerce_to_object"; "napi_get_arraybuffer_info"; "napi_detach_arraybuffer"; "napi_get_undefined"; "napi_reference_unref"; "napi_fatal_error"; "napi_open_callback_scope"; "napi_close_callback_scope"; "napi_get_value_uint32"; "napi_create_function"; "napi_create_arraybuffer"; "napi_get_value_int64"; "napi_get_all_property_names"; "napi_resolve_deferred"; "napi_is_detached_arraybuffer"; "napi_create_string_utf8"; "napi_create_threadsafe_function"; "node_api_throw_syntax_error"; "napi_create_bigint_int64"; "napi_wrap"; "napi_set_property"; "napi_get_value_bigint_int64"; "napi_open_handle_scope"; "napi_create_error"; "napi_create_buffer"; "napi_cancel_async_work"; "napi_is_exception_pending"; "napi_acquire_threadsafe_function"; "napi_create_external"; "napi_get_threadsafe_function_context"; "napi_get_null"; "napi_create_string_utf16"; "node_api_create_external_string_utf16"; "napi_get_value_bigint_uint64"; "napi_module_register"; "napi_is_typedarray"; "napi_create_external_buffer"; "napi_get_new_target"; "napi_get_instance_data"; "napi_close_handle_scope"; "napi_get_value_string_utf16"; "napi_get_property_names"; "napi_is_arraybuffer"; "napi_get_cb_info"; "napi_define_properties"; "napi_add_env_cleanup_hook"; "node_api_get_module_file_name"; "napi_get_node_version"; "napi_create_int64"; "napi_create_double"; "napi_get_and_clear_last_exception"; "napi_create_reference"; "napi_get_typedarray_info"; "napi_call_threadsafe_function"; "napi_get_last_error_info"; "napi_create_array_with_length"; "napi_coerce_to_number"; "napi_get_global"; "napi_is_error"; "napi_set_instance_data"; "napi_create_typedarray"; "napi_throw_type_error"; "napi_has_property"; "napi_get_value_external"; "napi_create_range_error"; "napi_typeof"; "napi_ref_threadsafe_function"; "napi_create_bigint_uint64"; "napi_get_prototype"; "napi_adjust_external_memory"; "napi_release_threadsafe_function"; "napi_delete_async_work"; "napi_create_string_latin1"; "node_api_create_external_string_latin1"; "napi_is_array"; "napi_unref_threadsafe_function"; "napi_throw_error"; "napi_has_own_property"; "napi_get_reference_value"; "napi_remove_env_cleanup_hook"; "napi_get_value_string_utf8"; "napi_is_promise"; "napi_get_boolean"; "napi_run_script"; "napi_get_element"; "napi_get_named_property"; "napi_get_buffer_info"; "napi_get_value_bool"; "napi_reference_ref"; "napi_create_object"; "napi_create_promise"; "napi_create_int32"; "napi_escape_handle"; "napi_open_escapable_handle_scope"; "napi_throw"; "napi_get_value_double"; "napi_set_named_property"; "napi_call_function"; "napi_create_date"; "napi_object_freeze"; "napi_get_uv_event_loop"; "napi_get_value_string_latin1"; "napi_reject_deferred"; "napi_add_finalizer"; "napi_create_array"; "napi_delete_reference"; "napi_get_date_value"; "napi_create_dataview"; "napi_get_version"; "napi_define_class"; "napi_is_date"; "napi_remove_wrap"; "napi_delete_property"; "napi_instanceof"; "napi_create_buffer_copy"; "napi_delete_element"; "napi_object_seal"; "napi_queue_async_work"; "napi_get_value_bigint_words"; "napi_is_buffer"; "napi_get_array_length"; "napi_get_property"; "napi_new_instance"; "napi_set_element"; "napi_create_bigint_words"; "napi_strict_equals"; "napi_is_dataview"; "napi_close_escapable_handle_scope"; "napi_get_dataview_info"; "napi_get_value_int32"; "napi_unwrap"; "napi_throw_range_error"; "napi_coerce_to_bool"; "napi_create_uint32"; "napi_has_element"; "napi_create_external_arraybuffer"; "napi_create_symbol"; "node_api_symbol_for"; "napi_coerce_to_string"; "napi_create_type_error"; "napi_fatal_exception"; "napi_create_async_work"; "napi_async_init"; "node_api_create_property_key_utf16"; "napi_type_tag_object"; "napi_check_object_type_tag"; "node_api_post_finalizer"; "napi_add_async_cleanup_hook"; "napi_remove_async_cleanup_hook"; }; \ No newline at end of file +{ "node_api_create_syntax_error"; "napi_make_callback"; "napi_has_named_property"; "napi_async_destroy"; "napi_coerce_to_object"; "napi_get_arraybuffer_info"; "napi_detach_arraybuffer"; "napi_get_undefined"; "napi_reference_unref"; "napi_fatal_error"; "napi_open_callback_scope"; "napi_close_callback_scope"; "napi_get_value_uint32"; "napi_create_function"; "napi_create_arraybuffer"; "napi_get_value_int64"; "napi_get_all_property_names"; "napi_resolve_deferred"; "napi_is_detached_arraybuffer"; "napi_create_string_utf8"; "napi_create_threadsafe_function"; "node_api_throw_syntax_error"; "napi_create_bigint_int64"; "napi_wrap"; "napi_set_property"; "napi_get_value_bigint_int64"; "napi_open_handle_scope"; "napi_create_error"; "napi_create_buffer"; "napi_cancel_async_work"; "napi_is_exception_pending"; "napi_acquire_threadsafe_function"; "napi_create_external"; "napi_get_threadsafe_function_context"; "napi_get_null"; "napi_create_string_utf16"; "node_api_create_external_string_utf16"; "napi_get_value_bigint_uint64"; "napi_module_register"; "napi_is_typedarray"; "napi_create_external_buffer"; "napi_get_new_target"; "napi_get_instance_data"; "napi_close_handle_scope"; "napi_get_value_string_utf16"; "napi_get_property_names"; "napi_is_arraybuffer"; "napi_get_cb_info"; "napi_define_properties"; "napi_add_env_cleanup_hook"; "node_api_get_module_file_name"; "napi_get_node_version"; "napi_create_int64"; "napi_create_double"; "napi_get_and_clear_last_exception"; "napi_create_reference"; "napi_get_typedarray_info"; "napi_call_threadsafe_function"; "napi_get_last_error_info"; "napi_create_array_with_length"; "napi_coerce_to_number"; "napi_get_global"; "napi_is_error"; "napi_set_instance_data"; "napi_create_typedarray"; "napi_throw_type_error"; "napi_has_property"; "napi_get_value_external"; "napi_create_range_error"; "napi_typeof"; "napi_ref_threadsafe_function"; "napi_create_bigint_uint64"; "napi_get_prototype"; "napi_adjust_external_memory"; "napi_release_threadsafe_function"; "napi_delete_async_work"; "napi_create_string_latin1"; "node_api_create_external_string_latin1"; "napi_is_array"; "napi_unref_threadsafe_function"; "napi_throw_error"; "napi_has_own_property"; "napi_get_reference_value"; "napi_remove_env_cleanup_hook"; "napi_get_value_string_utf8"; "napi_is_promise"; "napi_get_boolean"; "napi_run_script"; "napi_get_element"; "napi_get_named_property"; "napi_get_buffer_info"; "napi_get_value_bool"; "napi_reference_ref"; "napi_create_object"; "napi_create_promise"; "napi_create_int32"; "napi_escape_handle"; "napi_open_escapable_handle_scope"; "napi_throw"; "napi_get_value_double"; "napi_set_named_property"; "napi_call_function"; "napi_create_date"; "napi_object_freeze"; "napi_get_uv_event_loop"; "napi_get_value_string_latin1"; "napi_reject_deferred"; "napi_add_finalizer"; "napi_create_array"; "napi_delete_reference"; "napi_get_date_value"; "napi_create_dataview"; "napi_get_version"; "napi_define_class"; "napi_is_date"; "napi_remove_wrap"; "napi_delete_property"; "napi_instanceof"; "napi_create_buffer_copy"; "napi_delete_element"; "napi_object_seal"; "napi_queue_async_work"; "napi_get_value_bigint_words"; "napi_is_buffer"; "napi_get_array_length"; "napi_get_property"; "napi_new_instance"; "napi_set_element"; "napi_create_bigint_words"; "napi_strict_equals"; "napi_is_dataview"; "napi_close_escapable_handle_scope"; "napi_get_dataview_info"; "napi_get_value_int32"; "napi_unwrap"; "napi_throw_range_error"; "napi_coerce_to_bool"; "napi_create_uint32"; "napi_has_element"; "napi_create_external_arraybuffer"; "napi_create_symbol"; "node_api_symbol_for"; "napi_coerce_to_string"; "napi_create_type_error"; "napi_fatal_exception"; "napi_create_async_work"; "napi_async_init"; "node_api_create_property_key_utf16"; "napi_type_tag_object"; "napi_check_object_type_tag"; "node_api_post_finalizer"; "napi_add_async_cleanup_hook"; "napi_remove_async_cleanup_hook"; "uv_mutex_init"; "uv_mutex_lock"; "uv_mutex_unlock"; "uv_mutex_destroy"; "uv_async_init"; "uv_async_send"; "uv_close"; }; \ No newline at end of file diff --git a/cli/napi/generated_symbol_exports_list_macos.def b/ext/napi/generated_symbol_exports_list_macos.def similarity index 96% rename from cli/napi/generated_symbol_exports_list_macos.def rename to ext/napi/generated_symbol_exports_list_macos.def index cac7100c6f..36b2f37fa5 100644 --- a/cli/napi/generated_symbol_exports_list_macos.def +++ b/ext/napi/generated_symbol_exports_list_macos.def @@ -150,4 +150,11 @@ _napi_type_tag_object _napi_check_object_type_tag _node_api_post_finalizer _napi_add_async_cleanup_hook -_napi_remove_async_cleanup_hook \ No newline at end of file +_napi_remove_async_cleanup_hook +_uv_mutex_init +_uv_mutex_lock +_uv_mutex_unlock +_uv_mutex_destroy +_uv_async_init +_uv_async_send +_uv_close \ No newline at end of file diff --git a/cli/napi/generated_symbol_exports_list_windows.def b/ext/napi/generated_symbol_exports_list_windows.def similarity index 96% rename from cli/napi/generated_symbol_exports_list_windows.def rename to ext/napi/generated_symbol_exports_list_windows.def index 5386b46e54..b7355112e7 100644 --- a/cli/napi/generated_symbol_exports_list_windows.def +++ b/ext/napi/generated_symbol_exports_list_windows.def @@ -152,4 +152,11 @@ EXPORTS napi_check_object_type_tag node_api_post_finalizer napi_add_async_cleanup_hook - napi_remove_async_cleanup_hook \ No newline at end of file + napi_remove_async_cleanup_hook + uv_mutex_init + uv_mutex_lock + uv_mutex_unlock + uv_mutex_destroy + uv_async_init + uv_async_send + uv_close \ No newline at end of file diff --git a/cli/napi/js_native_api.rs b/ext/napi/js_native_api.rs similarity index 98% rename from cli/napi/js_native_api.rs rename to ext/napi/js_native_api.rs index e922d8c3f2..53a12d6eba 100644 --- a/cli/napi/js_native_api.rs +++ b/ext/napi/js_native_api.rs @@ -5,7 +5,7 @@ const NAPI_VERSION: u32 = 9; -use deno_runtime::deno_napi::*; +use crate::*; use libc::INT_MAX; use super::util::check_new_from_utf8; @@ -17,9 +17,9 @@ use super::util::napi_set_last_error; use super::util::v8_name_from_property_descriptor; use crate::check_arg; use crate::check_env; -use deno_runtime::deno_napi::function::create_function; -use deno_runtime::deno_napi::function::create_function_template; -use deno_runtime::deno_napi::function::CallbackInfo; +use crate::function::create_function; +use crate::function::create_function_template; +use crate::function::CallbackInfo; use napi_sym::napi_sym; use std::ptr::NonNull; @@ -264,6 +264,16 @@ fn napi_define_class<'s>( Err(status) => return status, }; + let mut accessor_property = v8::PropertyAttribute::NONE; + + if p.attributes & napi_enumerable == 0 { + accessor_property = accessor_property | v8::PropertyAttribute::DONT_ENUM; + } + if p.attributes & napi_configurable == 0 { + accessor_property = + accessor_property | v8::PropertyAttribute::DONT_DELETE; + } + if p.getter.is_some() || p.setter.is_some() { let getter = p.getter.map(|g| { create_function_template(&mut env.scope(), env_ptr, None, g, p.data) @@ -271,8 +281,6 @@ fn napi_define_class<'s>( let setter = p.setter.map(|s| { create_function_template(&mut env.scope(), env_ptr, None, s, p.data) }); - - let mut accessor_property = v8::PropertyAttribute::NONE; if getter.is_some() && setter.is_some() && (p.attributes & napi_writable) == 0 @@ -280,15 +288,6 @@ fn napi_define_class<'s>( accessor_property = accessor_property | v8::PropertyAttribute::READ_ONLY; } - if p.attributes & napi_enumerable == 0 { - accessor_property = - accessor_property | v8::PropertyAttribute::DONT_ENUM; - } - if p.attributes & napi_configurable == 0 { - accessor_property = - accessor_property | v8::PropertyAttribute::DONT_DELETE; - } - let proto = tpl.prototype_template(&mut env.scope()); proto.set_accessor_property(name, getter, setter, accessor_property); } else if let Some(method) = p.method { @@ -300,10 +299,14 @@ fn napi_define_class<'s>( p.data, ); let proto = tpl.prototype_template(&mut env.scope()); - proto.set(name, function.into()); + proto.set_with_attr(name, function.into(), accessor_property); } else { let proto = tpl.prototype_template(&mut env.scope()); - proto.set(name, p.value.unwrap().into()); + if (p.attributes & napi_writable) == 0 { + accessor_property = + accessor_property | v8::PropertyAttribute::READ_ONLY; + } + proto.set_with_attr(name, p.value.unwrap().into(), accessor_property); } } @@ -1080,7 +1083,7 @@ fn napi_create_string_latin1( } #[napi_sym] -fn napi_create_string_utf8( +pub(crate) fn napi_create_string_utf8( env_ptr: *mut Env, string: *const c_char, length: usize, @@ -1644,7 +1647,7 @@ fn napi_get_cb_info( check_arg!(env, argc); let argc = unsafe { *argc as usize }; for i in 0..argc { - let mut arg = args.get(i as _); + let arg = args.get(i as _); unsafe { *argv.add(i) = arg.into(); } @@ -1694,15 +1697,14 @@ fn napi_get_new_target( } #[napi_sym] -fn napi_call_function( - env_ptr: *mut Env, - recv: napi_value, - func: napi_value, +fn napi_call_function<'s>( + env: &'s mut Env, + recv: napi_value<'s>, + func: napi_value<'s>, argc: usize, - argv: *const napi_value, - result: *mut napi_value, + argv: *const napi_value<'s>, + result: *mut napi_value<'s>, ) -> napi_status { - let env = check_env!(env_ptr); check_arg!(env, recv); let args = if argc > 0 { check_arg!(env, argv); @@ -1716,11 +1718,11 @@ fn napi_call_function( let Some(func) = func.and_then(|f| v8::Local::::try_from(f).ok()) else { - return napi_set_last_error(env, napi_function_expected); + return napi_function_expected; }; let Some(v) = func.call(&mut env.scope(), recv.unwrap(), args) else { - return napi_set_last_error(env_ptr, napi_generic_failure); + return napi_generic_failure; }; if !result.is_null() { @@ -1729,7 +1731,7 @@ fn napi_call_function( } } - return napi_clear_last_error(env_ptr); + napi_ok } #[napi_sym] diff --git a/ext/napi/lib.rs b/ext/napi/lib.rs index 4500c66fd4..88b8c238df 100644 --- a/ext/napi/lib.rs +++ b/ext/napi/lib.rs @@ -5,9 +5,23 @@ #![allow(clippy::undocumented_unsafe_blocks)] #![deny(clippy::missing_safety_doc)] +//! Symbols to be exported are now defined in this JSON file. +//! The `#[napi_sym]` macro checks for missing entries and panics. +//! +//! `./tools/napi/generate_symbols_list.js` is used to generate the LINK `cli/exports.def` on Windows, +//! which is also checked into git. +//! +//! To add a new napi function: +//! 1. Place `#[napi_sym]` on top of your implementation. +//! 2. Add the function's identifier to this JSON list. +//! 3. Finally, run `tools/napi/generate_symbols_list.js` to update `ext/napi/generated_symbol_exports_list_*.def`. + +pub mod js_native_api; +pub mod node_api; +pub mod util; +pub mod uv; + use core::ptr::NonNull; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::parking_lot::RwLock; use deno_core::url::Url; @@ -20,6 +34,18 @@ use std::path::PathBuf; use std::rc::Rc; use std::thread_local; +#[derive(Debug, thiserror::Error)] +pub enum NApiError { + #[error("Invalid path")] + InvalidPath, + #[error(transparent)] + LibLoading(#[from] libloading::Error), + #[error("Unable to find register Node-API module at {}", .0.display())] + ModuleNotFound(PathBuf), + #[error(transparent)] + Permission(#[from] PermissionCheckError), +} + #[cfg(unix)] use libloading::os::unix::*; @@ -29,6 +55,7 @@ use libloading::os::windows::*; // Expose common stuff for ease of use. // `use deno_napi::*` pub use deno_core::v8; +use deno_permissions::PermissionCheckError; pub use std::ffi::CStr; pub use std::os::raw::c_char; pub use std::os::raw::c_void; @@ -482,14 +509,14 @@ deno_core::extension!(deno_napi, pub trait NapiPermissions { #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] - fn check(&mut self, path: &str) -> std::result::Result; + fn check(&mut self, path: &str) -> Result; } // NOTE(bartlomieju): for now, NAPI uses `--allow-ffi` flag, but that might // change in the future. impl NapiPermissions for deno_permissions::PermissionsContainer { #[inline(always)] - fn check(&mut self, path: &str) -> Result { + fn check(&mut self, path: &str) -> Result { deno_permissions::PermissionsContainer::check_ffi(self, path) } } @@ -512,7 +539,7 @@ fn op_napi_open( global: v8::Local<'scope, v8::Object>, buffer_constructor: v8::Local<'scope, v8::Function>, report_error: v8::Local<'scope, v8::Function>, -) -> std::result::Result, AnyError> +) -> Result, NApiError> where NP: NapiPermissions + 'static, { @@ -540,7 +567,7 @@ where let type_tag = v8::Global::new(scope, type_tag); let url_filename = - Url::from_file_path(&path).map_err(|_| type_error("Invalid path"))?; + Url::from_file_path(&path).map_err(|_| NApiError::InvalidPath)?; let env_shared = EnvShared::new(napi_wrap, type_tag, format!("{url_filename}\0")); @@ -565,17 +592,11 @@ where // SAFETY: opening a DLL calls dlopen #[cfg(unix)] - let library = match unsafe { Library::open(Some(&path), flags) } { - Ok(lib) => lib, - Err(e) => return Err(type_error(e.to_string())), - }; + let library = unsafe { Library::open(Some(&path), flags) }?; // SAFETY: opening a DLL calls dlopen #[cfg(not(unix))] - let library = match unsafe { Library::load_with_flags(&path, flags) } { - Ok(lib) => lib, - Err(e) => return Err(type_error(e.to_string())), - }; + let library = unsafe { Library::load_with_flags(&path, flags) }?; let maybe_module = MODULE_TO_REGISTER.with(|cell| { let mut slot = cell.borrow_mut(); @@ -610,10 +631,7 @@ where // SAFETY: we are going blind, calling the register function on the other side. unsafe { init(env_ptr, exports.into()) } } else { - return Err(type_error(format!( - "Unable to find register Node-API module at {}", - path.display() - ))); + return Err(NApiError::ModuleNotFound(path)); }; let exports = maybe_exports.unwrap_or(exports.into()); @@ -624,3 +642,34 @@ where Ok(exports) } + +#[allow(clippy::print_stdout)] +pub fn print_linker_flags(name: &str) { + let symbols_path = + include_str!(concat!(env!("OUT_DIR"), "/napi_symbol_path.txt")); + + #[cfg(target_os = "windows")] + println!("cargo:rustc-link-arg-bin={name}=/DEF:{}", symbols_path); + + #[cfg(target_os = "macos")] + println!( + "cargo:rustc-link-arg-bin={name}=-Wl,-exported_symbols_list,{}", + symbols_path, + ); + + #[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd" + ))] + println!( + "cargo:rustc-link-arg-bin={name}=-Wl,--export-dynamic-symbol-list={}", + symbols_path, + ); + + #[cfg(target_os = "android")] + println!( + "cargo:rustc-link-arg-bin={name}=-Wl,--export-dynamic-symbol-list={}", + symbols_path, + ); +} diff --git a/cli/napi/node_api.rs b/ext/napi/node_api.rs similarity index 97% rename from cli/napi/node_api.rs rename to ext/napi/node_api.rs index 81cb800a7d..2ca5c8d0b4 100644 --- a/cli/napi/node_api.rs +++ b/ext/napi/node_api.rs @@ -9,10 +9,10 @@ use super::util::napi_set_last_error; use super::util::SendPtr; use crate::check_arg; use crate::check_env; +use crate::*; use deno_core::parking_lot::Condvar; use deno_core::parking_lot::Mutex; use deno_core::V8CrossThreadTaskSpawner; -use deno_runtime::deno_napi::*; use napi_sym::napi_sym; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU8; @@ -140,7 +140,6 @@ fn napi_fatal_exception(env: &mut Env, err: napi_value) -> napi_status { } #[napi_sym] -#[allow(clippy::print_stderr)] fn napi_fatal_error( location: *const c_char, location_len: usize, @@ -173,9 +172,9 @@ fn napi_fatal_error( }; if let Some(location) = location { - eprintln!("NODE API FATAL ERROR: {} {}", location, message); + log::error!("NODE API FATAL ERROR: {} {}", location, message); } else { - eprintln!("NODE API FATAL ERROR: {}", message); + log::error!("NODE API FATAL ERROR: {}", message); } std::process::abort(); @@ -488,7 +487,7 @@ impl AsyncWork { } #[napi_sym] -fn napi_create_async_work( +pub(crate) fn napi_create_async_work( env: *mut Env, async_resource: napi_value, async_resource_name: napi_value, @@ -537,7 +536,10 @@ fn napi_create_async_work( } #[napi_sym] -fn napi_delete_async_work(env: *mut Env, work: napi_async_work) -> napi_status { +pub(crate) fn napi_delete_async_work( + env: *mut Env, + work: napi_async_work, +) -> napi_status { let env = check_env!(env); check_arg!(env, work); @@ -547,15 +549,23 @@ fn napi_delete_async_work(env: *mut Env, work: napi_async_work) -> napi_status { } #[napi_sym] -fn napi_get_uv_event_loop(env: *mut Env, uv_loop: *mut *mut ()) -> napi_status { - let env = check_env!(env); +fn napi_get_uv_event_loop( + env_ptr: *mut Env, + uv_loop: *mut *mut (), +) -> napi_status { + let env = check_env!(env_ptr); check_arg!(env, uv_loop); - // There is no uv_loop in Deno - napi_set_last_error(env, napi_generic_failure) + unsafe { + *uv_loop = env_ptr.cast(); + } + 0 } #[napi_sym] -fn napi_queue_async_work(env: *mut Env, work: napi_async_work) -> napi_status { +pub(crate) fn napi_queue_async_work( + env: *mut Env, + work: napi_async_work, +) -> napi_status { let env = check_env!(env); check_arg!(env, work); @@ -687,7 +697,7 @@ impl Drop for TsFn { if let Some(finalizer) = self.thread_finalize_cb { unsafe { - (finalizer)(self.env as _, self.thread_finalize_data, ptr::null_mut()); + (finalizer)(self.env as _, self.thread_finalize_data, self.context); } } } @@ -892,7 +902,7 @@ fn napi_create_threadsafe_function( }; let resource_name = resource_name.to_rust_string_lossy(&mut env.scope()); - let mut tsfn = Box::new(TsFn { + let tsfn = Box::new(TsFn { env, func, max_queue_size, diff --git a/cli/napi/sym/Cargo.toml b/ext/napi/sym/Cargo.toml similarity index 95% rename from cli/napi/sym/Cargo.toml rename to ext/napi/sym/Cargo.toml index eba5fa41cf..7c13a9165c 100644 --- a/cli/napi/sym/Cargo.toml +++ b/ext/napi/sym/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "napi_sym" -version = "0.98.0" +version = "0.107.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/cli/napi/sym/README.md b/ext/napi/sym/README.md similarity index 92% rename from cli/napi/sym/README.md rename to ext/napi/sym/README.md index de08a8e174..66eb4bff26 100644 --- a/cli/napi/sym/README.md +++ b/ext/napi/sym/README.md @@ -2,7 +2,8 @@ A proc_macro for Deno's Node-API implementation. It does the following things: -- Marks the symbol as `#[no_mangle]` and rewrites it as `pub extern "C" $name`. +- Marks the symbol as `#[no_mangle]` and rewrites it as + `unsafe extern "C" $name`. - Asserts that the function symbol is present in [`symbol_exports.json`](./symbol_exports.json). - Maps `deno_napi::Result` to raw `napi_result`. diff --git a/cli/napi/sym/lib.rs b/ext/napi/sym/lib.rs similarity index 100% rename from cli/napi/sym/lib.rs rename to ext/napi/sym/lib.rs diff --git a/cli/napi/sym/symbol_exports.json b/ext/napi/sym/symbol_exports.json similarity index 96% rename from cli/napi/sym/symbol_exports.json rename to ext/napi/sym/symbol_exports.json index 64b548d496..00946b8ed7 100644 --- a/cli/napi/sym/symbol_exports.json +++ b/ext/napi/sym/symbol_exports.json @@ -152,6 +152,13 @@ "napi_check_object_type_tag", "node_api_post_finalizer", "napi_add_async_cleanup_hook", - "napi_remove_async_cleanup_hook" + "napi_remove_async_cleanup_hook", + "uv_mutex_init", + "uv_mutex_lock", + "uv_mutex_unlock", + "uv_mutex_destroy", + "uv_async_init", + "uv_async_send", + "uv_close" ] } diff --git a/cli/napi/util.rs b/ext/napi/util.rs similarity index 84% rename from cli/napi/util.rs rename to ext/napi/util.rs index 63d8effbf2..21e9d433aa 100644 --- a/cli/napi/util.rs +++ b/ext/napi/util.rs @@ -1,9 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_runtime::deno_napi::*; +use crate::*; use libc::INT_MAX; #[repr(transparent)] -pub struct SendPtr(pub *const T); +pub(crate) struct SendPtr(pub *const T); impl SendPtr { // silly function to get around `clippy::redundant_locals` @@ -37,7 +37,7 @@ impl Drop for BufferFinalizer { } } -pub extern "C" fn backing_store_deleter_callback( +pub(crate) extern "C" fn backing_store_deleter_callback( data: *mut c_void, _byte_length: usize, deleter_data: *mut c_void, @@ -50,7 +50,7 @@ pub extern "C" fn backing_store_deleter_callback( drop(finalizer); } -pub fn make_external_backing_store( +pub(crate) fn make_external_backing_store( env: *mut Env, data: *mut c_void, byte_length: usize, @@ -90,9 +90,7 @@ macro_rules! check_env { macro_rules! return_error_status_if_false { ($env: expr, $condition: expr, $status: ident) => { if !$condition { - return Err( - $crate::napi::util::napi_set_last_error($env, $status).into(), - ); + return Err($crate::util::napi_set_last_error($env, $status).into()); } }; } @@ -101,7 +99,7 @@ macro_rules! return_error_status_if_false { macro_rules! return_status_if_false { ($env: expr, $condition: expr, $status: ident) => { if !$condition { - return $crate::napi::util::napi_set_last_error($env, $status); + return $crate::util::napi_set_last_error($env, $status); } }; } @@ -222,7 +220,7 @@ macro_rules! check_arg { ($env: expr, $ptr: expr) => { $crate::return_status_if_false!( $env, - !$crate::napi::util::Nullable::is_null(&$ptr), + !$crate::util::Nullable::is_null(&$ptr), napi_invalid_arg ); }; @@ -230,17 +228,17 @@ macro_rules! check_arg { #[macro_export] macro_rules! napi_wrap { - ( $( # $attr:tt )* fn $name:ident $( < $( $x:lifetime ),* > )? ( $env:ident : & $( $lt:lifetime )? mut Env $( , $ident:ident : $ty:ty )* $(,)? ) -> napi_status $body:block ) => { - $( # $attr )* + ( $( # [ $attr:meta ] )* $vis:vis fn $name:ident $( < $( $x:lifetime ),* > )? ( $env:ident : & $( $lt:lifetime )? mut Env $( , $ident:ident : $ty:ty )* $(,)? ) -> napi_status $body:block ) => { + $( # [ $attr ] )* #[no_mangle] - pub unsafe extern "C" fn $name $( < $( $x ),* > )? ( env_ptr : *mut Env , $( $ident : $ty ),* ) -> napi_status { + $vis unsafe extern "C" fn $name $( < $( $x ),* > )? ( env_ptr : *mut Env , $( $ident : $ty ),* ) -> napi_status { let env: & $( $lt )? mut Env = $crate::check_env!(env_ptr); if env.last_exception.is_some() { return napi_pending_exception; } - $crate::napi::util::napi_clear_last_error(env); + $crate::util::napi_clear_last_error(env); let scope_env = unsafe { &mut *env_ptr }; let scope = &mut scope_env.scope(); @@ -259,21 +257,21 @@ macro_rules! napi_wrap { let env = unsafe { &mut *env_ptr }; let global = v8::Global::new(env.isolate(), exception); env.last_exception = Some(global); - return $crate::napi::util::napi_set_last_error(env_ptr, napi_pending_exception); + return $crate::util::napi_set_last_error(env_ptr, napi_pending_exception); } if result != napi_ok { - return $crate::napi::util::napi_set_last_error(env_ptr, result); + return $crate::util::napi_set_last_error(env_ptr, result); } return result; } }; - ( $( # $attr:tt )* fn $name:ident $( < $( $x:lifetime ),* > )? ( $( $ident:ident : $ty:ty ),* $(,)? ) -> napi_status $body:block ) => { - $( # $attr )* + ( $( # [ $attr:meta ] )* $vis:vis fn $name:ident $( < $( $x:lifetime ),* > )? ( $( $ident:ident : $ty:ty ),* $(,)? ) -> napi_status $body:block ) => { + $( # [ $attr ] )* #[no_mangle] - pub unsafe extern "C" fn $name $( < $( $x ),* > )? ( $( $ident : $ty ),* ) -> napi_status { + $vis unsafe extern "C" fn $name $( < $( $x ),* > )? ( $( $ident : $ty ),* ) -> napi_status { #[inline(always)] fn inner $( < $( $x ),* > )? ( $( $ident : $ty ),* ) -> napi_status $body diff --git a/ext/napi/uv.rs b/ext/napi/uv.rs new file mode 100644 index 0000000000..ea6b539665 --- /dev/null +++ b/ext/napi/uv.rs @@ -0,0 +1,230 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use crate::*; +use deno_core::parking_lot::Mutex; +use std::mem::MaybeUninit; +use std::ptr::addr_of_mut; + +fn assert_ok(res: c_int) -> c_int { + if res != 0 { + log::error!("bad result in uv polyfill: {res}"); + // don't panic because that might unwind into + // c/c++ + std::process::abort(); + } + res +} + +use js_native_api::napi_create_string_utf8; +use node_api::napi_create_async_work; +use node_api::napi_delete_async_work; +use node_api::napi_queue_async_work; +use std::ffi::c_int; + +const UV_MUTEX_SIZE: usize = { + #[cfg(unix)] + { + std::mem::size_of::() + } + #[cfg(windows)] + { + std::mem::size_of::( + ) + } +}; + +#[repr(C)] +struct uv_mutex_t { + mutex: Mutex<()>, + _padding: [MaybeUninit; const { + (UV_MUTEX_SIZE - size_of::>()) / size_of::() + }], +} + +#[no_mangle] +unsafe extern "C" fn uv_mutex_init(lock: *mut uv_mutex_t) -> c_int { + unsafe { + addr_of_mut!((*lock).mutex).write(Mutex::new(())); + 0 + } +} + +#[no_mangle] +unsafe extern "C" fn uv_mutex_lock(lock: *mut uv_mutex_t) { + unsafe { + let guard = (*lock).mutex.lock(); + // forget the guard so it doesn't unlock when it goes out of scope. + // we're going to unlock it manually + std::mem::forget(guard); + } +} + +#[no_mangle] +unsafe extern "C" fn uv_mutex_unlock(lock: *mut uv_mutex_t) { + unsafe { + (*lock).mutex.force_unlock(); + } +} + +#[no_mangle] +unsafe extern "C" fn uv_mutex_destroy(_lock: *mut uv_mutex_t) { + // no cleanup required +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +enum uv_handle_type { + UV_UNKNOWN_HANDLE = 0, + UV_ASYNC, + UV_CHECK, + UV_FS_EVENT, + UV_FS_POLL, + UV_HANDLE, + UV_IDLE, + UV_NAMED_PIPE, + UV_POLL, + UV_PREPARE, + UV_PROCESS, + UV_STREAM, + UV_TCP, + UV_TIMER, + UV_TTY, + UV_UDP, + UV_SIGNAL, + UV_FILE, + UV_HANDLE_TYPE_MAX, +} + +const UV_HANDLE_SIZE: usize = 96; + +#[repr(C)] +struct uv_handle_t { + // public members + pub data: *mut c_void, + pub r#loop: *mut uv_loop_t, + pub r#type: uv_handle_type, + + _padding: [MaybeUninit; const { + (UV_HANDLE_SIZE + - size_of::<*mut c_void>() + - size_of::<*mut uv_loop_t>() + - size_of::()) + / size_of::() + }], +} + +#[cfg(unix)] +const UV_ASYNC_SIZE: usize = 128; + +#[cfg(windows)] +const UV_ASYNC_SIZE: usize = 224; + +#[repr(C)] +struct uv_async_t { + // public members + pub data: *mut c_void, + pub r#loop: *mut uv_loop_t, + pub r#type: uv_handle_type, + // private + async_cb: uv_async_cb, + work: napi_async_work, + _padding: [MaybeUninit; const { + (UV_ASYNC_SIZE + - size_of::<*mut c_void>() + - size_of::<*mut uv_loop_t>() + - size_of::() + - size_of::() + - size_of::()) + / size_of::() + }], +} + +type uv_loop_t = Env; +type uv_async_cb = extern "C" fn(handle: *mut uv_async_t); +#[no_mangle] +unsafe extern "C" fn uv_async_init( + r#loop: *mut uv_loop_t, + // probably uninitialized + r#async: *mut uv_async_t, + async_cb: uv_async_cb, +) -> c_int { + unsafe { + addr_of_mut!((*r#async).r#loop).write(r#loop); + addr_of_mut!((*r#async).r#type).write(uv_handle_type::UV_ASYNC); + addr_of_mut!((*r#async).async_cb).write(async_cb); + + let mut resource_name: MaybeUninit = MaybeUninit::uninit(); + assert_ok(napi_create_string_utf8( + r#loop, + c"uv_async".as_ptr(), + usize::MAX, + resource_name.as_mut_ptr(), + )); + let resource_name = resource_name.assume_init(); + + let res = napi_create_async_work( + r#loop, + None::>.into(), + resource_name, + Some(async_exec_wrap), + None, + r#async.cast(), + addr_of_mut!((*r#async).work), + ); + -res + } +} + +#[no_mangle] +unsafe extern "C" fn uv_async_send(handle: *mut uv_async_t) -> c_int { + unsafe { -napi_queue_async_work((*handle).r#loop, (*handle).work) } +} + +type uv_close_cb = unsafe extern "C" fn(*mut uv_handle_t); + +#[no_mangle] +unsafe extern "C" fn uv_close(handle: *mut uv_handle_t, close: uv_close_cb) { + unsafe { + if handle.is_null() { + close(handle); + return; + } + if let uv_handle_type::UV_ASYNC = (*handle).r#type { + let handle: *mut uv_async_t = handle.cast(); + napi_delete_async_work((*handle).r#loop, (*handle).work); + } + close(handle); + } +} + +unsafe extern "C" fn async_exec_wrap(_env: napi_env, data: *mut c_void) { + let data: *mut uv_async_t = data.cast(); + unsafe { + ((*data).async_cb)(data); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sizes() { + assert_eq!( + std::mem::size_of::(), + UV_MUTEX_SIZE + ); + assert_eq!( + std::mem::size_of::(), + UV_HANDLE_SIZE + ); + assert_eq!( + std::mem::size_of::(), + UV_ASYNC_SIZE + ); + assert_eq!(std::mem::size_of::(), UV_MUTEX_SIZE); + assert_eq!(std::mem::size_of::(), UV_HANDLE_SIZE); + assert_eq!(std::mem::size_of::(), UV_ASYNC_SIZE); + } +} diff --git a/ext/net/01_net.js b/ext/net/01_net.js index 5b894947ee..c3e5f9e5ca 100644 --- a/ext/net/01_net.js +++ b/ext/net/01_net.js @@ -194,6 +194,20 @@ class Conn { } } +class UpgradedConn extends Conn { + #rid = 0; + + constructor(rid, remoteAddr, localAddr) { + super(rid, remoteAddr, localAddr); + ObjectDefineProperty(this, internalRidSymbol, { + __proto__: null, + enumerable: false, + value: rid, + }); + this.#rid = rid; + } +} + class TcpConn extends Conn { #rid = 0; @@ -601,5 +615,6 @@ export { resolveDns, TcpConn, UnixConn, + UpgradedConn, validatePort, }; diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 11d19440f7..6dad965590 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -124,17 +124,19 @@ function loadTlsKeyPair(api, { // Check for "pem" format if (keyFormat !== undefined && keyFormat !== "pem") { - throw new TypeError('If `keyFormat` is specified, it must be "pem"'); + throw new TypeError( + `If "keyFormat" is specified, it must be "pem": received "${keyFormat}"`, + ); } if (cert !== undefined && key === undefined) { throw new TypeError( - `If \`cert\` is specified, \`key\` must be specified as well for \`${api}\`.`, + `If \`cert\` is specified, \`key\` must be specified as well for \`${api}\``, ); } if (cert === undefined && key !== undefined) { throw new TypeError( - `If \`key\` is specified, \`cert\` must be specified as well for \`${api}\`.`, + `If \`key\` is specified, \`cert\` must be specified as well for \`${api}\``, ); } diff --git a/ext/net/Cargo.toml b/ext/net/Cargo.toml index 02e7a16818..245deedd2d 100644 --- a/ext/net/Cargo.toml +++ b/ext/net/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_net" -version = "0.160.0" +version = "0.169.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -17,10 +17,11 @@ path = "lib.rs" deno_core.workspace = true deno_permissions.workspace = true deno_tls.workspace = true +hickory-proto = "0.24" +hickory-resolver = { version = "0.24", features = ["tokio-runtime", "serde-config"] } pin-project.workspace = true rustls-tokio-stream.workspace = true serde.workspace = true socket2.workspace = true +thiserror.workspace = true tokio.workspace = true -trust-dns-proto = "0.23" -trust-dns-resolver = { version = "0.23", features = ["tokio-runtime", "serde-config"] } diff --git a/ext/net/io.rs b/ext/net/io.rs index f3aed3fcbd..2907fa398b 100644 --- a/ext/net/io.rs +++ b/ext/net/io.rs @@ -1,7 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::generic_error; -use deno_core::error::AnyError; +use deno_core::futures::TryFutureExt; use deno_core::AsyncMutFuture; use deno_core::AsyncRefCell; use deno_core::AsyncResult; @@ -69,25 +68,36 @@ where pub async fn read( self: Rc, data: &mut [u8], - ) -> Result { + ) -> Result { let mut rd = self.rd_borrow_mut().await; let nread = rd.read(data).try_or_cancel(self.cancel_handle()).await?; Ok(nread) } - pub async fn write(self: Rc, data: &[u8]) -> Result { + pub async fn write( + self: Rc, + data: &[u8], + ) -> Result { let mut wr = self.wr_borrow_mut().await; let nwritten = wr.write(data).await?; Ok(nwritten) } - pub async fn shutdown(self: Rc) -> Result<(), AnyError> { + pub async fn shutdown(self: Rc) -> Result<(), std::io::Error> { let mut wr = self.wr_borrow_mut().await; wr.shutdown().await?; Ok(()) } } +#[derive(Debug, thiserror::Error)] +pub enum MapError { + #[error("{0}")] + Io(std::io::Error), + #[error("Unable to get resources")] + NoResources, +} + pub type TcpStreamResource = FullDuplexResource; @@ -100,7 +110,7 @@ impl Resource for TcpStreamResource { } fn shutdown(self: Rc) -> AsyncResult<()> { - Box::pin(self.shutdown()) + Box::pin(self.shutdown().map_err(Into::into)) } fn close(self: Rc) { @@ -109,31 +119,30 @@ impl Resource for TcpStreamResource { } impl TcpStreamResource { - pub fn set_nodelay(self: Rc, nodelay: bool) -> Result<(), AnyError> { - self.map_socket(Box::new(move |socket| Ok(socket.set_nodelay(nodelay)?))) + pub fn set_nodelay(self: Rc, nodelay: bool) -> Result<(), MapError> { + self.map_socket(Box::new(move |socket| socket.set_nodelay(nodelay))) } pub fn set_keepalive( self: Rc, keepalive: bool, - ) -> Result<(), AnyError> { - self - .map_socket(Box::new(move |socket| Ok(socket.set_keepalive(keepalive)?))) + ) -> Result<(), MapError> { + self.map_socket(Box::new(move |socket| socket.set_keepalive(keepalive))) } #[allow(clippy::type_complexity)] fn map_socket( self: Rc, - map: Box Result<(), AnyError>>, - ) -> Result<(), AnyError> { + map: Box Result<(), std::io::Error>>, + ) -> Result<(), MapError> { if let Some(wr) = RcRef::map(self, |r| &r.wr).try_borrow() { let stream = wr.as_ref().as_ref(); let socket = socket2::SockRef::from(stream); - return map(socket); + return map(socket).map_err(MapError::Io); } - Err(generic_error("Unable to get resources")) + Err(MapError::NoResources) } } @@ -153,7 +162,9 @@ impl UnixStreamResource { unreachable!() } #[allow(clippy::unused_async)] - pub async fn shutdown(self: Rc) -> Result<(), AnyError> { + pub async fn shutdown( + self: Rc, + ) -> Result<(), deno_core::error::AnyError> { unreachable!() } pub fn cancel_read_ops(&self) { @@ -170,7 +181,7 @@ impl Resource for UnixStreamResource { } fn shutdown(self: Rc) -> AsyncResult<()> { - Box::pin(self.shutdown()) + Box::pin(self.shutdown().map_err(Into::into)) } fn close(self: Rc) { diff --git a/ext/net/lib.rs b/ext/net/lib.rs index b039965d4c..bf8f58aa27 100644 --- a/ext/net/lib.rs +++ b/ext/net/lib.rs @@ -11,6 +11,7 @@ mod tcp; use deno_core::error::AnyError; use deno_core::OpState; +use deno_permissions::PermissionCheckError; use deno_tls::rustls::RootCertStore; use deno_tls::RootCertStoreProvider; use std::borrow::Cow; @@ -25,25 +26,25 @@ pub trait NetPermissions { &mut self, host: &(T, Option), api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result<(), PermissionCheckError>; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_read( &mut self, p: &str, api_name: &str, - ) -> Result; + ) -> Result; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write( &mut self, p: &str, api_name: &str, - ) -> Result; + ) -> Result; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write_path<'a>( &mut self, p: &'a Path, api_name: &str, - ) -> Result, AnyError>; + ) -> Result, PermissionCheckError>; } impl NetPermissions for deno_permissions::PermissionsContainer { @@ -52,7 +53,7 @@ impl NetPermissions for deno_permissions::PermissionsContainer { &mut self, host: &(T, Option), api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_net(self, host, api_name) } @@ -61,7 +62,7 @@ impl NetPermissions for deno_permissions::PermissionsContainer { &mut self, path: &str, api_name: &str, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_read(self, path, api_name) } @@ -70,7 +71,7 @@ impl NetPermissions for deno_permissions::PermissionsContainer { &mut self, path: &str, api_name: &str, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_write(self, path, api_name) } @@ -79,7 +80,7 @@ impl NetPermissions for deno_permissions::PermissionsContainer { &mut self, path: &'a Path, api_name: &str, - ) -> Result, AnyError> { + ) -> Result, PermissionCheckError> { deno_permissions::PermissionsContainer::check_write_path( self, path, api_name, ) diff --git a/ext/net/ops.rs b/ext/net/ops.rs index 5248493f41..9a8b70f0f6 100644 --- a/ext/net/ops.rs +++ b/ext/net/ops.rs @@ -6,10 +6,6 @@ use crate::resolve_addr::resolve_addr; use crate::resolve_addr::resolve_addr_sync; use crate::tcp::TcpListener; use crate::NetPermissions; -use deno_core::error::bad_resource; -use deno_core::error::custom_error; -use deno_core::error::generic_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::CancelFuture; @@ -22,6 +18,16 @@ use deno_core::OpState; use deno_core::RcRef; use deno_core::Resource; use deno_core::ResourceId; +use hickory_proto::rr::rdata::caa::Value; +use hickory_proto::rr::record_data::RData; +use hickory_proto::rr::record_type::RecordType; +use hickory_resolver::config::NameServerConfigGroup; +use hickory_resolver::config::ResolverConfig; +use hickory_resolver::config::ResolverOpts; +use hickory_resolver::error::ResolveError; +use hickory_resolver::error::ResolveErrorKind; +use hickory_resolver::system_conf; +use hickory_resolver::AsyncResolver; use serde::Deserialize; use serde::Serialize; use socket2::Domain; @@ -37,15 +43,6 @@ use std::rc::Rc; use std::str::FromStr; use tokio::net::TcpStream; use tokio::net::UdpSocket; -use trust_dns_proto::rr::rdata::caa::Value; -use trust_dns_proto::rr::record_data::RData; -use trust_dns_proto::rr::record_type::RecordType; -use trust_dns_resolver::config::NameServerConfigGroup; -use trust_dns_resolver::config::ResolverConfig; -use trust_dns_resolver::config::ResolverOpts; -use trust_dns_resolver::error::ResolveErrorKind; -use trust_dns_resolver::system_conf; -use trust_dns_resolver::AsyncResolver; #[derive(Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] @@ -68,11 +65,69 @@ impl From for IpAddr { } } -pub(crate) fn accept_err(e: std::io::Error) -> AnyError { +#[derive(Debug, thiserror::Error)] +pub enum NetError { + #[error("Listener has been closed")] + ListenerClosed, + #[error("Listener already in use")] + ListenerBusy, + #[error("Socket has been closed")] + SocketClosed, + #[error("Socket has been closed")] + SocketClosedNotConnected, + #[error("Socket already in use")] + SocketBusy, + #[error("{0}")] + Io(#[from] std::io::Error), + #[error("Another accept task is ongoing")] + AcceptTaskOngoing, + #[error(transparent)] + Permission(#[from] deno_permissions::PermissionCheckError), + #[error("{0}")] + Resource(deno_core::error::AnyError), + #[error("No resolved address found")] + NoResolvedAddress, + #[error("{0}")] + AddrParse(#[from] std::net::AddrParseError), + #[error("{0}")] + Map(crate::io::MapError), + #[error("{0}")] + Canceled(#[from] deno_core::Canceled), + #[error("{0}")] + DnsNotFound(ResolveError), + #[error("{0}")] + DnsNotConnected(ResolveError), + #[error("{0}")] + DnsTimedOut(ResolveError), + #[error("{0}")] + Dns(#[from] ResolveError), + #[error("Provided record type is not supported")] + UnsupportedRecordType, + #[error("File name or path {0:?} is not valid UTF-8")] + InvalidUtf8(std::ffi::OsString), + #[error("unexpected key type")] + UnexpectedKeyType, + #[error("Invalid hostname: '{0}'")] + InvalidHostname(String), // TypeError + #[error("TCP stream is currently in use")] + TcpStreamBusy, + #[error("{0}")] + Rustls(#[from] deno_tls::rustls::Error), + #[error("{0}")] + Tls(#[from] deno_tls::TlsError), + #[error("Error creating TLS certificate: Deno.listenTls requires a key")] + ListenTlsRequiresKey, // InvalidData + #[error("{0}")] + RootCertStore(deno_core::anyhow::Error), + #[error("{0}")] + Reunite(tokio::net::tcp::ReuniteError), +} + +pub(crate) fn accept_err(e: std::io::Error) -> NetError { if let std::io::ErrorKind::Interrupted = e.kind() { - bad_resource("Listener has been closed") + NetError::ListenerClosed } else { - e.into() + NetError::Io(e) } } @@ -81,15 +136,15 @@ pub(crate) fn accept_err(e: std::io::Error) -> AnyError { pub async fn op_net_accept_tcp( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(ResourceId, IpAddr, IpAddr), AnyError> { +) -> Result<(ResourceId, IpAddr, IpAddr), NetError> { let resource = state .borrow() .resource_table .get::>(rid) - .map_err(|_| bad_resource("Listener has been closed"))?; + .map_err(|_| NetError::ListenerClosed)?; let listener = RcRef::map(&resource, |r| &r.listener) .try_borrow_mut() - .ok_or_else(|| custom_error("Busy", "Another accept task is ongoing"))?; + .ok_or_else(|| NetError::AcceptTaskOngoing)?; let cancel = RcRef::map(resource, |r| &r.cancel); let (tcp_stream, _socket_addr) = listener .accept() @@ -112,12 +167,12 @@ pub async fn op_net_recv_udp( state: Rc>, #[smi] rid: ResourceId, #[buffer] mut buf: JsBuffer, -) -> Result<(usize, IpAddr), AnyError> { +) -> Result<(usize, IpAddr), NetError> { let resource = state .borrow_mut() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket).borrow().await; let cancel_handle = RcRef::map(&resource, |r| &r.cancel); let (nread, remote_addr) = socket @@ -134,7 +189,7 @@ pub async fn op_net_send_udp( #[smi] rid: ResourceId, #[serde] addr: IpAddr, #[buffer] zero_copy: JsBuffer, -) -> Result +) -> Result where NP: NetPermissions + 'static, { @@ -148,13 +203,13 @@ where let addr = resolve_addr(&addr.hostname, addr.port) .await? .next() - .ok_or_else(|| generic_error("No resolved address found"))?; + .ok_or(NetError::NoResolvedAddress)?; let resource = state .borrow_mut() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket).borrow().await; let nwritten = socket.send_to(&zero_copy, &addr).await?; @@ -167,12 +222,12 @@ pub async fn op_net_join_multi_v4_udp( #[smi] rid: ResourceId, #[string] address: String, #[string] multi_interface: String, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { let resource = state .borrow_mut() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket).borrow().await; let addr = Ipv4Addr::from_str(address.as_str())?; @@ -189,12 +244,12 @@ pub async fn op_net_join_multi_v6_udp( #[smi] rid: ResourceId, #[string] address: String, #[smi] multi_interface: u32, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { let resource = state .borrow_mut() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket).borrow().await; let addr = Ipv6Addr::from_str(address.as_str())?; @@ -210,12 +265,12 @@ pub async fn op_net_leave_multi_v4_udp( #[smi] rid: ResourceId, #[string] address: String, #[string] multi_interface: String, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { let resource = state .borrow_mut() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket).borrow().await; let addr = Ipv4Addr::from_str(address.as_str())?; @@ -232,12 +287,12 @@ pub async fn op_net_leave_multi_v6_udp( #[smi] rid: ResourceId, #[string] address: String, #[smi] multi_interface: u32, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { let resource = state .borrow_mut() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket).borrow().await; let addr = Ipv6Addr::from_str(address.as_str())?; @@ -253,16 +308,16 @@ pub async fn op_net_set_multi_loopback_udp( #[smi] rid: ResourceId, is_v4_membership: bool, loopback: bool, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { let resource = state .borrow_mut() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket).borrow().await; if is_v4_membership { - socket.set_multicast_loop_v4(loopback)? + socket.set_multicast_loop_v4(loopback)?; } else { socket.set_multicast_loop_v6(loopback)?; } @@ -275,12 +330,12 @@ pub async fn op_net_set_multi_ttl_udp( state: Rc>, #[smi] rid: ResourceId, #[smi] ttl: u32, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { let resource = state .borrow_mut() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket).borrow().await; socket.set_multicast_ttl_v4(ttl)?; @@ -293,7 +348,7 @@ pub async fn op_net_set_multi_ttl_udp( pub async fn op_net_connect_tcp( state: Rc>, #[serde] addr: IpAddr, -) -> Result<(ResourceId, IpAddr, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -304,7 +359,7 @@ where pub async fn op_net_connect_tcp_inner( state: Rc>, addr: IpAddr, -) -> Result<(ResourceId, IpAddr, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -318,7 +373,7 @@ where let addr = resolve_addr(&addr.hostname, addr.port) .await? .next() - .ok_or_else(|| generic_error("No resolved address found"))?; + .ok_or_else(|| NetError::NoResolvedAddress)?; let tcp_stream = TcpStream::connect(&addr).await?; let local_addr = tcp_stream.local_addr()?; let remote_addr = tcp_stream.peer_addr()?; @@ -353,7 +408,7 @@ pub fn op_net_listen_tcp( #[serde] addr: IpAddr, reuse_port: bool, load_balanced: bool, -) -> Result<(ResourceId, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -365,7 +420,7 @@ where .check_net(&(&addr.hostname, Some(addr.port)), "Deno.listen()")?; let addr = resolve_addr_sync(&addr.hostname, addr.port)? .next() - .ok_or_else(|| generic_error("No resolved address found"))?; + .ok_or_else(|| NetError::NoResolvedAddress)?; let listener = if load_balanced { TcpListener::bind_load_balanced(addr) @@ -384,7 +439,7 @@ fn net_listen_udp( addr: IpAddr, reuse_address: bool, loopback: bool, -) -> Result<(ResourceId, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -393,7 +448,7 @@ where .check_net(&(&addr.hostname, Some(addr.port)), "Deno.listenDatagram()")?; let addr = resolve_addr_sync(&addr.hostname, addr.port)? .next() - .ok_or_else(|| generic_error("No resolved address found"))?; + .ok_or_else(|| NetError::NoResolvedAddress)?; let domain = if addr.is_ipv4() { Domain::IPV4 @@ -453,7 +508,7 @@ pub fn op_net_listen_udp( #[serde] addr: IpAddr, reuse_address: bool, loopback: bool, -) -> Result<(ResourceId, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -468,7 +523,7 @@ pub fn op_node_unstable_net_listen_udp( #[serde] addr: IpAddr, reuse_address: bool, loopback: bool, -) -> Result<(ResourceId, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -551,7 +606,7 @@ pub struct NameServer { pub async fn op_dns_resolve( state: Rc>, #[serde] args: ResolveAddrArgs, -) -> Result, AnyError> +) -> Result, NetError> where NP: NetPermissions + 'static, { @@ -618,22 +673,17 @@ where }; lookup - .map_err(|e| { - let message = format!("{e}"); - match e.kind() { - ResolveErrorKind::NoRecordsFound { .. } => { - custom_error("NotFound", message) - } - ResolveErrorKind::Message("No connections available") => { - custom_error("NotConnected", message) - } - ResolveErrorKind::Timeout => custom_error("TimedOut", message), - _ => generic_error(message), + .map_err(|e| match e.kind() { + ResolveErrorKind::NoRecordsFound { .. } => NetError::DnsNotFound(e), + ResolveErrorKind::Message("No connections available") => { + NetError::DnsNotConnected(e) } + ResolveErrorKind::Timeout => NetError::DnsTimedOut(e), + _ => NetError::Dns(e), })? .iter() .filter_map(|rdata| rdata_to_return_record(record_type)(rdata).transpose()) - .collect::, AnyError>>() + .collect::, NetError>>() } #[op2(fast)] @@ -641,7 +691,7 @@ pub fn op_set_nodelay( state: &mut OpState, #[smi] rid: ResourceId, nodelay: bool, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { op_set_nodelay_inner(state, rid, nodelay) } @@ -650,10 +700,12 @@ pub fn op_set_nodelay_inner( state: &mut OpState, rid: ResourceId, nodelay: bool, -) -> Result<(), AnyError> { - let resource: Rc = - state.resource_table.get::(rid)?; - resource.set_nodelay(nodelay) +) -> Result<(), NetError> { + let resource: Rc = state + .resource_table + .get::(rid) + .map_err(NetError::Resource)?; + resource.set_nodelay(nodelay).map_err(NetError::Map) } #[op2(fast)] @@ -661,7 +713,7 @@ pub fn op_set_keepalive( state: &mut OpState, #[smi] rid: ResourceId, keepalive: bool, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { op_set_keepalive_inner(state, rid, keepalive) } @@ -670,17 +722,19 @@ pub fn op_set_keepalive_inner( state: &mut OpState, rid: ResourceId, keepalive: bool, -) -> Result<(), AnyError> { - let resource: Rc = - state.resource_table.get::(rid)?; - resource.set_keepalive(keepalive) +) -> Result<(), NetError> { + let resource: Rc = state + .resource_table + .get::(rid) + .map_err(NetError::Resource)?; + resource.set_keepalive(keepalive).map_err(NetError::Map) } fn rdata_to_return_record( ty: RecordType, -) -> impl Fn(&RData) -> Result, AnyError> { +) -> impl Fn(&RData) -> Result, NetError> { use RecordType::*; - move |r: &RData| -> Result, AnyError> { + move |r: &RData| -> Result, NetError> { let record = match ty { A => r.as_a().map(ToString::to_string).map(DnsReturnRecord::A), AAAA => r @@ -761,12 +815,7 @@ fn rdata_to_return_record( .collect(); DnsReturnRecord::Txt(texts) }), - _ => { - return Err(custom_error( - "NotSupported", - "Provided record type is not supported", - )) - } + _ => return Err(NetError::UnsupportedRecordType), }; Ok(record) } @@ -778,6 +827,22 @@ mod tests { use deno_core::futures::FutureExt; use deno_core::JsRuntime; use deno_core::RuntimeOptions; + use deno_permissions::PermissionCheckError; + use hickory_proto::rr::rdata::a::A; + use hickory_proto::rr::rdata::aaaa::AAAA; + use hickory_proto::rr::rdata::caa::KeyValue; + use hickory_proto::rr::rdata::caa::CAA; + use hickory_proto::rr::rdata::mx::MX; + use hickory_proto::rr::rdata::name::ANAME; + use hickory_proto::rr::rdata::name::CNAME; + use hickory_proto::rr::rdata::name::NS; + use hickory_proto::rr::rdata::name::PTR; + use hickory_proto::rr::rdata::naptr::NAPTR; + use hickory_proto::rr::rdata::srv::SRV; + use hickory_proto::rr::rdata::txt::TXT; + use hickory_proto::rr::rdata::SOA; + use hickory_proto::rr::record_data::RData; + use hickory_proto::rr::Name; use socket2::SockRef; use std::net::Ipv4Addr; use std::net::Ipv6Addr; @@ -786,21 +851,6 @@ mod tests { use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; - use trust_dns_proto::rr::rdata::a::A; - use trust_dns_proto::rr::rdata::aaaa::AAAA; - use trust_dns_proto::rr::rdata::caa::KeyValue; - use trust_dns_proto::rr::rdata::caa::CAA; - use trust_dns_proto::rr::rdata::mx::MX; - use trust_dns_proto::rr::rdata::name::ANAME; - use trust_dns_proto::rr::rdata::name::CNAME; - use trust_dns_proto::rr::rdata::name::NS; - use trust_dns_proto::rr::rdata::name::PTR; - use trust_dns_proto::rr::rdata::naptr::NAPTR; - use trust_dns_proto::rr::rdata::srv::SRV; - use trust_dns_proto::rr::rdata::txt::TXT; - use trust_dns_proto::rr::rdata::SOA; - use trust_dns_proto::rr::record_data::RData; - use trust_dns_proto::rr::Name; #[test] fn rdata_to_return_record_a() { @@ -985,7 +1035,7 @@ mod tests { &mut self, _host: &(T, Option), _api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result<(), PermissionCheckError> { Ok(()) } @@ -993,7 +1043,7 @@ mod tests { &mut self, p: &str, _api_name: &str, - ) -> Result { + ) -> Result { Ok(PathBuf::from(p)) } @@ -1001,7 +1051,7 @@ mod tests { &mut self, p: &str, _api_name: &str, - ) -> Result { + ) -> Result { Ok(PathBuf::from(p)) } @@ -1009,7 +1059,7 @@ mod tests { &mut self, p: &'a Path, _api_name: &str, - ) -> Result, AnyError> { + ) -> Result, PermissionCheckError> { Ok(Cow::Borrowed(p)) } } @@ -1091,7 +1141,7 @@ mod tests { let vals = result.unwrap(); rid = rid.or(Some(vals.0)); } - }; + } let rid = rid.unwrap(); let state = runtime.op_state(); diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index 5bc04ceb51..c7d65dd85e 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -2,6 +2,7 @@ use crate::io::TcpStreamResource; use crate::ops::IpAddr; +use crate::ops::NetError; use crate::ops::TlsHandshakeInfo; use crate::raw::NetworkListenerResource; use crate::resolve_addr::resolve_addr; @@ -10,13 +11,7 @@ use crate::tcp::TcpListener; use crate::DefaultTlsOptions; use crate::NetPermissions; use crate::UnsafelyIgnoreCertificateErrors; -use deno_core::anyhow::anyhow; -use deno_core::anyhow::bail; -use deno_core::error::bad_resource; -use deno_core::error::custom_error; -use deno_core::error::generic_error; -use deno_core::error::invalid_hostname; -use deno_core::error::AnyError; +use deno_core::futures::TryFutureExt; use deno_core::op2; use deno_core::v8; use deno_core::AsyncRefCell; @@ -118,20 +113,23 @@ impl TlsStreamResource { pub async fn read( self: Rc, data: &mut [u8], - ) -> Result { + ) -> Result { let mut rd = RcRef::map(&self, |r| &r.rd).borrow_mut().await; let cancel_handle = RcRef::map(&self, |r| &r.cancel_handle); - Ok(rd.read(data).try_or_cancel(cancel_handle).await?) + rd.read(data).try_or_cancel(cancel_handle).await } - pub async fn write(self: Rc, data: &[u8]) -> Result { + pub async fn write( + self: Rc, + data: &[u8], + ) -> Result { let mut wr = RcRef::map(self, |r| &r.wr).borrow_mut().await; let nwritten = wr.write(data).await?; wr.flush().await?; Ok(nwritten) } - pub async fn shutdown(self: Rc) -> Result<(), AnyError> { + pub async fn shutdown(self: Rc) -> Result<(), std::io::Error> { let mut wr = RcRef::map(self, |r| &r.wr).borrow_mut().await; wr.shutdown().await?; Ok(()) @@ -139,7 +137,7 @@ impl TlsStreamResource { pub async fn handshake( self: &Rc, - ) -> Result { + ) -> Result { if let Some(tls_info) = &*self.handshake_info.borrow() { return Ok(tls_info.clone()); } @@ -164,7 +162,7 @@ impl Resource for TlsStreamResource { } fn shutdown(self: Rc) -> AsyncResult<()> { - Box::pin(self.shutdown()) + Box::pin(self.shutdown().map_err(Into::into)) } fn close(self: Rc) { @@ -201,7 +199,7 @@ pub fn op_tls_key_null() -> TlsKeysHolder { pub fn op_tls_key_static( #[string] cert: &str, #[string] key: &str, -) -> Result { +) -> Result { let cert = load_certs(&mut BufReader::new(cert.as_bytes()))?; let key = load_private_keys(key.as_bytes())? .into_iter() @@ -236,9 +234,9 @@ pub fn op_tls_cert_resolver_resolve( #[cppgc] lookup: &TlsKeyLookup, #[string] sni: String, #[cppgc] key: &TlsKeysHolder, -) -> Result<(), AnyError> { +) -> Result<(), NetError> { let TlsKeys::Static(key) = key.take() else { - bail!("unexpected key type"); + return Err(NetError::UnexpectedKeyType); }; lookup.resolve(sni, Ok(key)); Ok(()) @@ -250,7 +248,7 @@ pub fn op_tls_cert_resolver_resolve_error( #[string] sni: String, #[string] error: String, ) { - lookup.resolve(sni, Err(anyhow!(error))) + lookup.resolve(sni, Err(error)) } #[op2] @@ -258,7 +256,7 @@ pub fn op_tls_cert_resolver_resolve_error( pub fn op_tls_start( state: Rc>, #[serde] args: StartTlsArgs, -) -> Result<(ResourceId, IpAddr, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -271,7 +269,9 @@ where { let mut s = state.borrow_mut(); let permissions = s.borrow_mut::(); - permissions.check_net(&(&hostname, Some(0)), "Deno.startTls()")?; + permissions + .check_net(&(&hostname, Some(0)), "Deno.startTls()") + .map_err(NetError::Permission)?; } let ca_certs = args @@ -281,7 +281,7 @@ where .collect::>(); let hostname_dns = ServerName::try_from(hostname.to_string()) - .map_err(|_| invalid_hostname(&hostname))?; + .map_err(|_| NetError::InvalidHostname(hostname))?; let unsafely_ignore_certificate_errors = state .borrow() @@ -291,19 +291,21 @@ where let root_cert_store = state .borrow() .borrow::() - .root_cert_store()?; + .root_cert_store() + .map_err(NetError::RootCertStore)?; let resource_rc = state .borrow_mut() .resource_table - .take::(rid)?; + .take::(rid) + .map_err(NetError::Resource)?; // This TCP connection might be used somewhere else. If it's the case, we cannot proceed with the // process of starting a TLS connection on top of this TCP connection, so we just return a Busy error. // See also: https://github.com/denoland/deno/pull/16242 - let resource = Rc::try_unwrap(resource_rc) - .map_err(|_| custom_error("Busy", "TCP stream is currently in use"))?; + let resource = + Rc::try_unwrap(resource_rc).map_err(|_| NetError::TcpStreamBusy)?; let (read_half, write_half) = resource.into_inner(); - let tcp_stream = read_half.reunite(write_half)?; + let tcp_stream = read_half.reunite(write_half).map_err(NetError::Reunite)?; let local_addr = tcp_stream.local_addr()?; let remote_addr = tcp_stream.peer_addr()?; @@ -345,7 +347,7 @@ pub async fn op_net_connect_tls( #[serde] addr: IpAddr, #[serde] args: ConnectTlsArgs, #[cppgc] key_pair: &TlsKeysHolder, -) -> Result<(ResourceId, IpAddr, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -359,9 +361,14 @@ where let mut s = state.borrow_mut(); let permissions = s.borrow_mut::(); permissions - .check_net(&(&addr.hostname, Some(addr.port)), "Deno.connectTls()")?; + .check_net(&(&addr.hostname, Some(addr.port)), "Deno.connectTls()") + .map_err(NetError::Permission)?; if let Some(path) = cert_file { - Some(permissions.check_read(path, "Deno.connectTls()")?) + Some( + permissions + .check_read(path, "Deno.connectTls()") + .map_err(NetError::Permission)?, + ) } else { None } @@ -382,17 +389,18 @@ where let root_cert_store = state .borrow() .borrow::() - .root_cert_store()?; + .root_cert_store() + .map_err(NetError::RootCertStore)?; let hostname_dns = if let Some(server_name) = args.server_name { ServerName::try_from(server_name) } else { ServerName::try_from(addr.hostname.clone()) } - .map_err(|_| invalid_hostname(&addr.hostname))?; + .map_err(|_| NetError::InvalidHostname(addr.hostname.clone()))?; let connect_addr = resolve_addr(&addr.hostname, addr.port) .await? .next() - .ok_or_else(|| generic_error("No resolved address found"))?; + .ok_or_else(|| NetError::NoResolvedAddress)?; let tcp_stream = TcpStream::connect(connect_addr).await?; let local_addr = tcp_stream.local_addr()?; let remote_addr = tcp_stream.peer_addr()?; @@ -444,7 +452,7 @@ pub fn op_net_listen_tls( #[serde] addr: IpAddr, #[serde] args: ListenTlsArgs, #[cppgc] keys: &TlsKeysHolder, -) -> Result<(ResourceId, IpAddr), AnyError> +) -> Result<(ResourceId, IpAddr), NetError> where NP: NetPermissions + 'static, { @@ -455,12 +463,13 @@ where { let permissions = state.borrow_mut::(); permissions - .check_net(&(&addr.hostname, Some(addr.port)), "Deno.listenTls()")?; + .check_net(&(&addr.hostname, Some(addr.port)), "Deno.listenTls()") + .map_err(NetError::Permission)?; } let bind_addr = resolve_addr_sync(&addr.hostname, addr.port)? .next() - .ok_or_else(|| generic_error("No resolved address found"))?; + .ok_or(NetError::NoResolvedAddress)?; let tcp_listener = if args.load_balanced { TcpListener::bind_load_balanced(bind_addr) @@ -475,28 +484,24 @@ where .map(|s| s.into_bytes()) .collect(); let listener = match keys.take() { - TlsKeys::Null => Err(anyhow!("Deno.listenTls requires a key")), + TlsKeys::Null => return Err(NetError::ListenTlsRequiresKey), TlsKeys::Static(TlsKey(cert, key)) => { let mut tls_config = ServerConfig::builder() .with_no_client_auth() - .with_single_cert(cert, key) - .map_err(|e| anyhow!(e))?; + .with_single_cert(cert, key)?; tls_config.alpn_protocols = alpn; - Ok(TlsListener { + TlsListener { tcp_listener, tls_config: Some(tls_config.into()), server_config_provider: None, - }) + } } - TlsKeys::Resolver(resolver) => Ok(TlsListener { + TlsKeys::Resolver(resolver) => TlsListener { tcp_listener, tls_config: None, server_config_provider: Some(resolver.into_server_config_provider(alpn)), - }), - } - .map_err(|e| { - custom_error("InvalidData", "Error creating TLS certificate").context(e) - })?; + }, + }; let tls_listener_resource = NetworkListenerResource::new(listener); @@ -510,23 +515,23 @@ where pub async fn op_net_accept_tls( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(ResourceId, IpAddr, IpAddr), AnyError> { +) -> Result<(ResourceId, IpAddr, IpAddr), NetError> { let resource = state .borrow() .resource_table .get::>(rid) - .map_err(|_| bad_resource("Listener has been closed"))?; + .map_err(|_| NetError::ListenerClosed)?; let cancel_handle = RcRef::map(&resource, |r| &r.cancel); let listener = RcRef::map(&resource, |r| &r.listener) .try_borrow_mut() - .ok_or_else(|| custom_error("Busy", "Another accept task is ongoing"))?; + .ok_or_else(|| NetError::AcceptTaskOngoing)?; let (tls_stream, remote_addr) = match listener.accept().try_or_cancel(&cancel_handle).await { Ok(tuple) => tuple, Err(err) if err.kind() == ErrorKind::Interrupted => { - return Err(bad_resource("Listener has been closed")); + return Err(NetError::ListenerClosed); } Err(err) => return Err(err.into()), }; @@ -547,11 +552,11 @@ pub async fn op_net_accept_tls( pub async fn op_tls_handshake( state: Rc>, #[smi] rid: ResourceId, -) -> Result { +) -> Result { let resource = state .borrow() .resource_table .get::(rid) - .map_err(|_| bad_resource("Listener has been closed"))?; - resource.handshake().await + .map_err(|_| NetError::ListenerClosed)?; + resource.handshake().await.map_err(Into::into) } diff --git a/ext/net/ops_unix.rs b/ext/net/ops_unix.rs index 95293284f0..04ae84906f 100644 --- a/ext/net/ops_unix.rs +++ b/ext/net/ops_unix.rs @@ -1,11 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::io::UnixStreamResource; +use crate::ops::NetError; use crate::raw::NetworkListenerResource; use crate::NetPermissions; -use deno_core::error::bad_resource; -use deno_core::error::custom_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::AsyncRefCell; use deno_core::CancelHandle; @@ -26,11 +24,8 @@ use tokio::net::UnixListener; pub use tokio::net::UnixStream; /// A utility function to map OsStrings to Strings -pub fn into_string(s: std::ffi::OsString) -> Result { - s.into_string().map_err(|s| { - let message = format!("File name or path {s:?} is not valid UTF-8"); - custom_error("InvalidData", message) - }) +pub fn into_string(s: std::ffi::OsString) -> Result { + s.into_string().map_err(NetError::InvalidUtf8) } pub struct UnixDatagramResource { @@ -63,15 +58,15 @@ pub struct UnixListenArgs { pub async fn op_net_accept_unix( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(ResourceId, Option, Option), AnyError> { +) -> Result<(ResourceId, Option, Option), NetError> { let resource = state .borrow() .resource_table .get::>(rid) - .map_err(|_| bad_resource("Listener has been closed"))?; + .map_err(|_| NetError::ListenerClosed)?; let listener = RcRef::map(&resource, |r| &r.listener) .try_borrow_mut() - .ok_or_else(|| custom_error("Busy", "Listener already in use"))?; + .ok_or(NetError::ListenerBusy)?; let cancel = RcRef::map(resource, |r| &r.cancel); let (unix_stream, _socket_addr) = listener .accept() @@ -95,7 +90,7 @@ pub async fn op_net_accept_unix( pub async fn op_net_connect_unix( state: Rc>, #[string] address_path: String, -) -> Result<(ResourceId, Option, Option), AnyError> +) -> Result<(ResourceId, Option, Option), NetError> where NP: NetPermissions + 'static, { @@ -103,10 +98,12 @@ where let mut state_ = state.borrow_mut(); let address_path = state_ .borrow_mut::() - .check_read(&address_path, "Deno.connect()")?; + .check_read(&address_path, "Deno.connect()") + .map_err(NetError::Permission)?; _ = state_ .borrow_mut::() - .check_write_path(&address_path, "Deno.connect()")?; + .check_write_path(&address_path, "Deno.connect()") + .map_err(NetError::Permission)?; address_path }; let unix_stream = UnixStream::connect(&address_path).await?; @@ -127,15 +124,15 @@ pub async fn op_net_recv_unixpacket( state: Rc>, #[smi] rid: ResourceId, #[buffer] mut buf: JsBuffer, -) -> Result<(usize, Option), AnyError> { +) -> Result<(usize, Option), NetError> { let resource = state .borrow() .resource_table .get::(rid) - .map_err(|_| bad_resource("Socket has been closed"))?; + .map_err(|_| NetError::SocketClosed)?; let socket = RcRef::map(&resource, |r| &r.socket) .try_borrow_mut() - .ok_or_else(|| custom_error("Busy", "Socket already in use"))?; + .ok_or(NetError::SocketBusy)?; let cancel = RcRef::map(resource, |r| &r.cancel); let (nread, remote_addr) = socket.recv_from(&mut buf).try_or_cancel(cancel).await?; @@ -150,24 +147,25 @@ pub async fn op_net_send_unixpacket( #[smi] rid: ResourceId, #[string] address_path: String, #[buffer] zero_copy: JsBuffer, -) -> Result +) -> Result where NP: NetPermissions + 'static, { let address_path = { let mut s = state.borrow_mut(); s.borrow_mut::() - .check_write(&address_path, "Deno.DatagramConn.send()")? + .check_write(&address_path, "Deno.DatagramConn.send()") + .map_err(NetError::Permission)? }; let resource = state .borrow() .resource_table .get::(rid) - .map_err(|_| custom_error("NotConnected", "Socket has been closed"))?; + .map_err(|_| NetError::SocketClosedNotConnected)?; let socket = RcRef::map(&resource, |r| &r.socket) .try_borrow_mut() - .ok_or_else(|| custom_error("Busy", "Socket already in use"))?; + .ok_or(NetError::SocketBusy)?; let nwritten = socket.send_to(&zero_copy, address_path).await?; Ok(nwritten) @@ -179,14 +177,18 @@ pub fn op_net_listen_unix( state: &mut OpState, #[string] address_path: String, #[string] api_name: String, -) -> Result<(ResourceId, Option), AnyError> +) -> Result<(ResourceId, Option), NetError> where NP: NetPermissions + 'static, { let permissions = state.borrow_mut::(); let api_call_expr = format!("{}()", api_name); - let address_path = permissions.check_read(&address_path, &api_call_expr)?; - _ = permissions.check_write_path(&address_path, &api_call_expr)?; + let address_path = permissions + .check_read(&address_path, &api_call_expr) + .map_err(NetError::Permission)?; + _ = permissions + .check_write_path(&address_path, &api_call_expr) + .map_err(NetError::Permission)?; let listener = UnixListener::bind(address_path)?; let local_addr = listener.local_addr()?; let pathname = local_addr.as_pathname().map(pathstring).transpose()?; @@ -198,14 +200,17 @@ where pub fn net_listen_unixpacket( state: &mut OpState, address_path: String, -) -> Result<(ResourceId, Option), AnyError> +) -> Result<(ResourceId, Option), NetError> where NP: NetPermissions + 'static, { let permissions = state.borrow_mut::(); - let address_path = - permissions.check_read(&address_path, "Deno.listenDatagram()")?; - _ = permissions.check_write_path(&address_path, "Deno.listenDatagram()")?; + let address_path = permissions + .check_read(&address_path, "Deno.listenDatagram()") + .map_err(NetError::Permission)?; + _ = permissions + .check_write_path(&address_path, "Deno.listenDatagram()") + .map_err(NetError::Permission)?; let socket = UnixDatagram::bind(address_path)?; let local_addr = socket.local_addr()?; let pathname = local_addr.as_pathname().map(pathstring).transpose()?; @@ -222,7 +227,7 @@ where pub fn op_net_listen_unixpacket( state: &mut OpState, #[string] path: String, -) -> Result<(ResourceId, Option), AnyError> +) -> Result<(ResourceId, Option), NetError> where NP: NetPermissions + 'static, { @@ -235,13 +240,13 @@ where pub fn op_node_unstable_net_listen_unixpacket( state: &mut OpState, #[string] path: String, -) -> Result<(ResourceId, Option), AnyError> +) -> Result<(ResourceId, Option), NetError> where NP: NetPermissions + 'static, { net_listen_unixpacket::(state, path) } -pub fn pathstring(pathname: &Path) -> Result { +pub fn pathstring(pathname: &Path) -> Result { into_string(pathname.into()) } diff --git a/ext/net/resolve_addr.rs b/ext/net/resolve_addr.rs index 8bbdd5192c..3a97081eac 100644 --- a/ext/net/resolve_addr.rs +++ b/ext/net/resolve_addr.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use std::net::SocketAddr; use std::net::ToSocketAddrs; use tokio::net::lookup_host; @@ -9,7 +8,7 @@ use tokio::net::lookup_host; pub async fn resolve_addr( hostname: &str, port: u16, -) -> Result + '_, AnyError> { +) -> Result + '_, std::io::Error> { let addr_port_pair = make_addr_port_pair(hostname, port); let result = lookup_host(addr_port_pair).await?; Ok(result) @@ -19,7 +18,7 @@ pub async fn resolve_addr( pub fn resolve_addr_sync( hostname: &str, port: u16, -) -> Result, AnyError> { +) -> Result, std::io::Error> { let addr_port_pair = make_addr_port_pair(hostname, port); let result = addr_port_pair.to_socket_addrs()?; Ok(result) diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 17da17cd81..9e1a3495b5 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_node" -version = "0.105.0" +version = "0.114.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -34,6 +34,7 @@ deno_io.workspace = true deno_media_type.workspace = true deno_net.workspace = true deno_package_json.workspace = true +deno_path_util.workspace = true deno_permissions.workspace = true deno_whoami = "0.1.0" der = { version = "0.7.9", features = ["derive"] } diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 0c821ecf8b..84f39552c1 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -9,25 +9,23 @@ use std::path::Path; use std::path::PathBuf; use deno_core::error::AnyError; -use deno_core::located_script_name; use deno_core::op2; use deno_core::url::Url; #[allow(unused_imports)] use deno_core::v8; use deno_core::v8::ExternalReference; -use deno_core::JsRuntime; -use deno_fs::sync::MaybeSend; -use deno_fs::sync::MaybeSync; -use node_resolver::NpmResolverRc; +use node_resolver::errors::ClosestPkgJsonError; +use node_resolver::NpmPackageFolderResolverRc; use once_cell::sync::Lazy; extern crate libz_sys as zlib; mod global; -mod ops; +pub mod ops; mod polyfill; pub use deno_package_json::PackageJson; +use deno_permissions::PermissionCheckError; pub use node_resolver::PathClean; pub use ops::ipc::ChildPipeFd; pub use ops::ipc::IpcJsonStreamResource; @@ -49,10 +47,18 @@ pub trait NodePermissions { &mut self, url: &Url, api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result<(), PermissionCheckError>; + fn check_net( + &mut self, + host: (&str, Option), + api_name: &str, + ) -> Result<(), PermissionCheckError>; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] - fn check_read(&mut self, path: &str) -> Result { + fn check_read( + &mut self, + path: &str, + ) -> Result { self.check_read_with_api_name(path, None) } #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] @@ -60,19 +66,24 @@ pub trait NodePermissions { &mut self, path: &str, api_name: Option<&str>, - ) -> Result; + ) -> Result; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_read_path<'a>( &mut self, path: &'a Path, - ) -> Result, AnyError>; - fn check_sys(&mut self, kind: &str, api_name: &str) -> Result<(), AnyError>; + ) -> Result, PermissionCheckError>; + fn query_read_all(&mut self) -> bool; + fn check_sys( + &mut self, + kind: &str, + api_name: &str, + ) -> Result<(), PermissionCheckError>; #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write_with_api_name( &mut self, path: &str, api_name: Option<&str>, - ) -> Result; + ) -> Result; } impl NodePermissions for deno_permissions::PermissionsContainer { @@ -81,16 +92,24 @@ impl NodePermissions for deno_permissions::PermissionsContainer { &mut self, url: &Url, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_net_url(self, url, api_name) } + fn check_net( + &mut self, + host: (&str, Option), + api_name: &str, + ) -> Result<(), PermissionCheckError> { + deno_permissions::PermissionsContainer::check_net(self, &host, api_name) + } + #[inline(always)] fn check_read_with_api_name( &mut self, path: &str, api_name: Option<&str>, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_read_with_api_name( self, path, api_name, ) @@ -99,36 +118,50 @@ impl NodePermissions for deno_permissions::PermissionsContainer { fn check_read_path<'a>( &mut self, path: &'a Path, - ) -> Result, AnyError> { + ) -> Result, PermissionCheckError> { deno_permissions::PermissionsContainer::check_read_path(self, path, None) } + fn query_read_all(&mut self) -> bool { + deno_permissions::PermissionsContainer::query_read_all(self) + } + #[inline(always)] fn check_write_with_api_name( &mut self, path: &str, api_name: Option<&str>, - ) -> Result { + ) -> Result { deno_permissions::PermissionsContainer::check_write_with_api_name( self, path, api_name, ) } - fn check_sys(&mut self, kind: &str, api_name: &str) -> Result<(), AnyError> { + fn check_sys( + &mut self, + kind: &str, + api_name: &str, + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_sys(self, kind, api_name) } } #[allow(clippy::disallowed_types)] -pub type NodeRequireResolverRc = - deno_fs::sync::MaybeArc; +pub type NodeRequireLoaderRc = std::rc::Rc; -pub trait NodeRequireResolver: std::fmt::Debug + MaybeSend + MaybeSync { - fn ensure_read_permission( +pub trait NodeRequireLoader { + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn ensure_read_permission<'a>( &self, permissions: &mut dyn NodePermissions, - path: &Path, - ) -> Result<(), AnyError>; + path: &'a Path, + ) -> Result, AnyError>; + + fn load_text_file_lossy(&self, path: &Path) -> Result; + + /// Get if the module kind is maybe CJS and loading should determine + /// if its CJS or ESM. + fn is_maybe_cjs(&self, specifier: &Url) -> Result; } pub static NODE_ENV_VAR_ALLOWLIST: Lazy> = Lazy::new(|| { @@ -146,10 +179,12 @@ fn op_node_build_os() -> String { env!("TARGET").split('-').nth(2).unwrap().to_string() } +#[derive(Clone)] pub struct NodeExtInitServices { - pub node_require_resolver: NodeRequireResolverRc, + pub node_require_loader: NodeRequireLoaderRc, pub node_resolver: NodeResolverRc, - pub npm_resolver: NpmResolverRc, + pub npm_resolver: NpmPackageFolderResolverRc, + pub pkg_json_resolver: PackageJsonResolverRc, } deno_core::extension!(deno_node, @@ -167,6 +202,7 @@ deno_core::extension!(deno_node, ops::buffer::op_is_ascii, ops::buffer::op_is_utf8, + ops::buffer::op_transcode, ops::crypto::op_node_check_prime_async, ops::crypto::op_node_check_prime_bytes_async, ops::crypto::op_node_check_prime_bytes, @@ -314,6 +350,7 @@ deno_core::extension!(deno_node, ops::zlib::op_zlib_write, ops::zlib::op_zlib_init, ops::zlib::op_zlib_reset, + ops::zlib::op_zlib_crc32, ops::zlib::brotli::op_brotli_compress, ops::zlib::brotli::op_brotli_compress_async, ops::zlib::brotli::op_create_brotli_compress, @@ -341,7 +378,7 @@ deno_core::extension!(deno_node, 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_node_os_user_info

, ops::os::op_geteuid

, ops::os::op_getegid

, ops::os::op_cpus

, @@ -353,6 +390,7 @@ deno_core::extension!(deno_node, ops::require::op_require_proxy_path, ops::require::op_require_is_deno_dir_package, ops::require::op_require_resolve_deno_dir, + ops::require::op_require_is_maybe_cjs, ops::require::op_require_is_request_relative, ops::require::op_require_resolve_lookup_paths, ops::require::op_require_try_self_parent_path

, @@ -366,7 +404,6 @@ deno_core::extension!(deno_node, ops::require::op_require_read_file

, ops::require::op_require_as_file_path, ops::require::op_require_resolve_exports

, - ops::require::op_require_read_closest_package_json

, ops::require::op_require_read_package_scope

, ops::require::op_require_package_imports_resolve

, ops::require::op_require_break_on_next_statement, @@ -380,6 +417,15 @@ deno_core::extension!(deno_node, ops::process::op_node_process_kill, ops::process::op_process_abort, ops::tls::op_get_root_certificates, + ops::inspector::op_inspector_open

, + ops::inspector::op_inspector_close, + ops::inspector::op_inspector_url, + ops::inspector::op_inspector_wait, + ops::inspector::op_inspector_connect

, + ops::inspector::op_inspector_dispatch, + ops::inspector::op_inspector_disconnect, + ops::inspector::op_inspector_emit_protocol_event, + ops::inspector::op_inspector_enabled, ], esm_entry_point = "ext:deno_node/02_init.js", esm = [ @@ -462,6 +508,7 @@ deno_core::extension!(deno_node, "internal_binding/constants.ts", "internal_binding/crypto.ts", "internal_binding/handle_wrap.ts", + "internal_binding/http_parser.ts", "internal_binding/mod.ts", "internal_binding/node_file.ts", "internal_binding/node_options.ts", @@ -587,8 +634,8 @@ deno_core::extension!(deno_node, "node:http" = "http.ts", "node:http2" = "http2.ts", "node:https" = "https.ts", - "node:inspector" = "inspector.ts", - "node:inspector/promises" = "inspector.ts", + "node:inspector" = "inspector.js", + "node:inspector/promises" = "inspector/promises.js", "node:module" = "01_require.js", "node:net" = "net.ts", "node:os" = "os.ts", @@ -631,9 +678,10 @@ deno_core::extension!(deno_node, state.put(options.fs.clone()); if let Some(init) = &options.maybe_init { - state.put(init.node_require_resolver.clone()); + state.put(init.node_require_loader.clone()); state.put(init.node_resolver.clone()); state.put(init.npm_resolver.clone()); + state.put(init.pkg_json_resolver.clone()); } }, global_template_middleware = global_template_middleware, @@ -753,33 +801,16 @@ deno_core::extension!(deno_node, }, ); -pub fn load_cjs_module( - js_runtime: &mut JsRuntime, - module: &str, - main: bool, - inspect_brk: bool, -) -> Result<(), AnyError> { - fn escape_for_single_quote_string(text: &str) -> String { - text.replace('\\', r"\\").replace('\'', r"\'") - } - - let source_code = format!( - r#"(function loadCjsModule(moduleName, isMain, inspectBrk) {{ - Deno[Deno.internal].node.loadCjsModule(moduleName, isMain, inspectBrk); - }})('{module}', {main}, {inspect_brk});"#, - main = main, - module = escape_for_single_quote_string(module), - inspect_brk = inspect_brk, - ); - - js_runtime.execute_script(located_script_name!(), source_code)?; - Ok(()) -} - pub type NodeResolver = node_resolver::NodeResolver; #[allow(clippy::disallowed_types)] pub type NodeResolverRc = deno_fs::sync::MaybeArc>; +pub type PackageJsonResolver = + node_resolver::PackageJsonResolver; +#[allow(clippy::disallowed_types)] +pub type PackageJsonResolverRc = deno_fs::sync::MaybeArc< + node_resolver::PackageJsonResolver, +>; #[derive(Debug)] pub struct DenoFsNodeResolverEnv { diff --git a/ext/node/ops/blocklist.rs b/ext/node/ops/blocklist.rs index 332cdda8f9..6c64d68eca 100644 --- a/ext/node/ops/blocklist.rs +++ b/ext/node/ops/blocklist.rs @@ -7,9 +7,6 @@ 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; @@ -27,13 +24,25 @@ impl deno_core::GarbageCollected for BlockListResource {} #[derive(Serialize)] struct SocketAddressSerialization(String, String); +#[derive(Debug, thiserror::Error)] +pub enum BlocklistError { + #[error("{0}")] + AddrParse(#[from] std::net::AddrParseError), + #[error("{0}")] + IpNetwork(#[from] ipnetwork::IpNetworkError), + #[error("Invalid address")] + InvalidAddress, + #[error("IP version mismatch between start and end addresses")] + IpVersionMismatch, +} + #[op2(fast)] pub fn op_socket_address_parse( state: &mut OpState, #[string] addr: &str, #[smi] port: u16, #[string] family: &str, -) -> Result { +) -> Result { let ip = addr.parse::()?; let parsed: SocketAddr = SocketAddr::new(ip, port); let parsed_ip_str = parsed.ip().to_string(); @@ -52,7 +61,7 @@ pub fn op_socket_address_parse( Ok(false) } } else { - Err(anyhow!("Invalid address")) + Err(BlocklistError::InvalidAddress) } } @@ -60,8 +69,8 @@ pub fn op_socket_address_parse( #[serde] pub fn op_socket_address_get_serialization( state: &mut OpState, -) -> Result { - Ok(state.take::()) +) -> SocketAddressSerialization { + state.take::() } #[op2] @@ -77,7 +86,7 @@ pub fn op_blocklist_new() -> BlockListResource { pub fn op_blocklist_add_address( #[cppgc] wrap: &BlockListResource, #[string] addr: &str, -) -> Result<(), AnyError> { +) -> Result<(), BlocklistError> { wrap.blocklist.borrow_mut().add_address(addr) } @@ -86,7 +95,7 @@ pub fn op_blocklist_add_range( #[cppgc] wrap: &BlockListResource, #[string] start: &str, #[string] end: &str, -) -> Result { +) -> Result { wrap.blocklist.borrow_mut().add_range(start, end) } @@ -95,7 +104,7 @@ pub fn op_blocklist_add_subnet( #[cppgc] wrap: &BlockListResource, #[string] addr: &str, #[smi] prefix: u8, -) -> Result<(), AnyError> { +) -> Result<(), BlocklistError> { wrap.blocklist.borrow_mut().add_subnet(addr, prefix) } @@ -104,7 +113,7 @@ pub fn op_blocklist_check( #[cppgc] wrap: &BlockListResource, #[string] addr: &str, #[string] r#type: &str, -) -> Result { +) -> Result { wrap.blocklist.borrow().check(addr, r#type) } @@ -123,7 +132,7 @@ impl BlockList { &mut self, addr: IpAddr, prefix: Option, - ) -> Result<(), AnyError> { + ) -> Result<(), BlocklistError> { match addr { IpAddr::V4(addr) => { let ipv4_prefix = prefix.unwrap_or(32); @@ -154,7 +163,7 @@ impl BlockList { Ok(()) } - pub fn add_address(&mut self, address: &str) -> Result<(), AnyError> { + pub fn add_address(&mut self, address: &str) -> Result<(), BlocklistError> { let ip: IpAddr = address.parse()?; self.map_addr_add_network(ip, None)?; Ok(()) @@ -164,7 +173,7 @@ impl BlockList { &mut self, start: &str, end: &str, - ) -> Result { + ) -> Result { let start_ip: IpAddr = start.parse()?; let end_ip: IpAddr = end.parse()?; @@ -193,25 +202,33 @@ impl BlockList { self.map_addr_add_network(IpAddr::V6(addr), None)?; } } - _ => bail!("IP version mismatch between start and end addresses"), + _ => return Err(BlocklistError::IpVersionMismatch), } Ok(true) } - pub fn add_subnet(&mut self, addr: &str, prefix: u8) -> Result<(), AnyError> { + pub fn add_subnet( + &mut self, + addr: &str, + prefix: u8, + ) -> Result<(), BlocklistError> { let ip: IpAddr = addr.parse()?; self.map_addr_add_network(ip, Some(prefix))?; Ok(()) } - pub fn check(&self, addr: &str, r#type: &str) -> Result { + 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")) + Err(BlocklistError::InvalidAddress) } } } diff --git a/ext/node/ops/buffer.rs b/ext/node/ops/buffer.rs index 74a011ab80..01f878ec15 100644 --- a/ext/node/ops/buffer.rs +++ b/ext/node/ops/buffer.rs @@ -1,5 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use deno_core::anyhow::anyhow; +use deno_core::anyhow::Result; use deno_core::op2; #[op2(fast)] @@ -11,3 +13,107 @@ pub fn op_is_ascii(#[buffer] buf: &[u8]) -> bool { pub fn op_is_utf8(#[buffer] buf: &[u8]) -> bool { std::str::from_utf8(buf).is_ok() } + +#[op2] +#[buffer] +pub fn op_transcode( + #[buffer] source: &[u8], + #[string] from_encoding: &str, + #[string] to_encoding: &str, +) -> Result> { + match (from_encoding, to_encoding) { + ("utf8", "ascii") => Ok(utf8_to_ascii(source)), + ("utf8", "latin1") => Ok(utf8_to_latin1(source)), + ("utf8", "utf16le") => utf8_to_utf16le(source), + ("utf16le", "utf8") => utf16le_to_utf8(source), + ("latin1", "utf16le") | ("ascii", "utf16le") => { + Ok(latin1_ascii_to_utf16le(source)) + } + (from, to) => Err(anyhow!("Unable to transcode Buffer {from}->{to}")), + } +} + +fn latin1_ascii_to_utf16le(source: &[u8]) -> Vec { + let mut result = Vec::with_capacity(source.len() * 2); + for &byte in source { + result.push(byte); + result.push(0); + } + result +} + +fn utf16le_to_utf8(source: &[u8]) -> Result> { + let ucs2_vec: Vec = source + .chunks(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .collect(); + String::from_utf16(&ucs2_vec) + .map(|utf8_string| utf8_string.into_bytes()) + .map_err(|e| anyhow!("Invalid UTF-16 sequence: {}", e)) +} + +fn utf8_to_utf16le(source: &[u8]) -> Result> { + let utf8_string = std::str::from_utf8(source)?; + let ucs2_vec: Vec = utf8_string.encode_utf16().collect(); + let bytes: Vec = ucs2_vec.iter().flat_map(|&x| x.to_le_bytes()).collect(); + Ok(bytes) +} + +fn utf8_to_latin1(source: &[u8]) -> Vec { + let mut latin1_bytes = Vec::with_capacity(source.len()); + let mut i = 0; + while i < source.len() { + match source[i] { + byte if byte <= 0x7F => { + // ASCII character + latin1_bytes.push(byte); + i += 1; + } + byte if (0xC2..=0xDF).contains(&byte) && i + 1 < source.len() => { + // 2-byte UTF-8 sequence + let codepoint = + ((byte as u16 & 0x1F) << 6) | (source[i + 1] as u16 & 0x3F); + latin1_bytes.push(if codepoint <= 0xFF { + codepoint as u8 + } else { + b'?' + }); + i += 2; + } + _ => { + // 3-byte or 4-byte UTF-8 sequence, or invalid UTF-8 + latin1_bytes.push(b'?'); + // Skip to the next valid UTF-8 start byte + i += 1; + while i < source.len() && (source[i] & 0xC0) == 0x80 { + i += 1; + } + } + } + } + latin1_bytes +} + +fn utf8_to_ascii(source: &[u8]) -> Vec { + let mut ascii_bytes = Vec::with_capacity(source.len()); + let mut i = 0; + while i < source.len() { + match source[i] { + byte if byte <= 0x7F => { + // ASCII character + ascii_bytes.push(byte); + i += 1; + } + _ => { + // Non-ASCII character + ascii_bytes.push(b'?'); + // Skip to the next valid UTF-8 start byte + i += 1; + while i < source.len() && (source[i] & 0xC0) == 0x80 { + i += 1; + } + } + } + } + ascii_bytes +} diff --git a/ext/node/ops/crypto/cipher.rs b/ext/node/ops/crypto/cipher.rs index b80aa33fe8..ec45146b49 100644 --- a/ext/node/ops/crypto/cipher.rs +++ b/ext/node/ops/crypto/cipher.rs @@ -4,9 +4,6 @@ use aes::cipher::block_padding::Pkcs7; use aes::cipher::BlockDecryptMut; use aes::cipher::BlockEncryptMut; use aes::cipher::KeyIvInit; -use deno_core::error::range_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::Resource; use digest::generic_array::GenericArray; use digest::KeyInit; @@ -50,8 +47,22 @@ pub struct DecipherContext { decipher: Rc>, } +#[derive(Debug, thiserror::Error)] +pub enum CipherContextError { + #[error("Cipher context is already in use")] + ContextInUse, + #[error("{0}")] + Resource(deno_core::error::AnyError), + #[error(transparent)] + Cipher(#[from] CipherError), +} + impl CipherContext { - pub fn new(algorithm: &str, key: &[u8], iv: &[u8]) -> Result { + pub fn new( + algorithm: &str, + key: &[u8], + iv: &[u8], + ) -> Result { Ok(Self { cipher: Rc::new(RefCell::new(Cipher::new(algorithm, key, iv)?)), }) @@ -74,16 +85,31 @@ impl CipherContext { auto_pad: bool, input: &[u8], output: &mut [u8], - ) -> Result { + ) -> Result { Rc::try_unwrap(self.cipher) - .map_err(|_| type_error("Cipher context is already in use"))? + .map_err(|_| CipherContextError::ContextInUse)? .into_inner() .r#final(auto_pad, input, output) + .map_err(Into::into) } } +#[derive(Debug, thiserror::Error)] +pub enum DecipherContextError { + #[error("Decipher context is already in use")] + ContextInUse, + #[error("{0}")] + Resource(deno_core::error::AnyError), + #[error(transparent)] + Decipher(#[from] DecipherError), +} + impl DecipherContext { - pub fn new(algorithm: &str, key: &[u8], iv: &[u8]) -> Result { + pub fn new( + algorithm: &str, + key: &[u8], + iv: &[u8], + ) -> Result { Ok(Self { decipher: Rc::new(RefCell::new(Decipher::new(algorithm, key, iv)?)), }) @@ -103,11 +129,12 @@ impl DecipherContext { input: &[u8], output: &mut [u8], auth_tag: &[u8], - ) -> Result<(), AnyError> { + ) -> Result<(), DecipherContextError> { Rc::try_unwrap(self.decipher) - .map_err(|_| type_error("Decipher context is already in use"))? + .map_err(|_| DecipherContextError::ContextInUse)? .into_inner() .r#final(auto_pad, input, output, auth_tag) + .map_err(Into::into) } } @@ -123,12 +150,26 @@ impl Resource for DecipherContext { } } +#[derive(Debug, thiserror::Error)] +pub enum CipherError { + #[error("IV length must be 12 bytes")] + InvalidIvLength, + #[error("Invalid key length")] + InvalidKeyLength, + #[error("Invalid initialization vector")] + InvalidInitializationVector, + #[error("Cannot pad the input data")] + CannotPadInputData, + #[error("Unknown cipher {0}")] + UnknownCipher(String), +} + impl Cipher { fn new( algorithm_name: &str, key: &[u8], iv: &[u8], - ) -> Result { + ) -> Result { use Cipher::*; Ok(match algorithm_name { "aes-128-cbc" => { @@ -139,7 +180,7 @@ impl Cipher { "aes-256-ecb" => Aes256Ecb(Box::new(ecb::Encryptor::new(key.into()))), "aes-128-gcm" => { if iv.len() != 12 { - return Err(type_error("IV length must be 12 bytes")); + return Err(CipherError::InvalidIvLength); } let cipher = @@ -149,7 +190,7 @@ impl Cipher { } "aes-256-gcm" => { if iv.len() != 12 { - return Err(type_error("IV length must be 12 bytes")); + return Err(CipherError::InvalidIvLength); } let cipher = @@ -159,15 +200,15 @@ impl Cipher { } "aes256" | "aes-256-cbc" => { if key.len() != 32 { - return Err(range_error("Invalid key length")); + return Err(CipherError::InvalidKeyLength); } if iv.len() != 16 { - return Err(type_error("Invalid initialization vector")); + return Err(CipherError::InvalidInitializationVector); } Aes256Cbc(Box::new(cbc::Encryptor::new(key.into(), iv.into()))) } - _ => return Err(type_error(format!("Unknown cipher {algorithm_name}"))), + _ => return Err(CipherError::UnknownCipher(algorithm_name.to_string())), }) } @@ -235,14 +276,14 @@ impl Cipher { auto_pad: bool, input: &[u8], output: &mut [u8], - ) -> Result { + ) -> Result { assert!(input.len() < 16); use Cipher::*; match (self, auto_pad) { (Aes128Cbc(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot pad the input data"))?; + .map_err(|_| CipherError::CannotPadInputData)?; Ok(None) } (Aes128Cbc(mut encryptor), false) => { @@ -255,7 +296,7 @@ impl Cipher { (Aes128Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot pad the input data"))?; + .map_err(|_| CipherError::CannotPadInputData)?; Ok(None) } (Aes128Ecb(mut encryptor), false) => { @@ -268,7 +309,7 @@ impl Cipher { (Aes192Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot pad the input data"))?; + .map_err(|_| CipherError::CannotPadInputData)?; Ok(None) } (Aes192Ecb(mut encryptor), false) => { @@ -281,7 +322,7 @@ impl Cipher { (Aes256Ecb(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot pad the input data"))?; + .map_err(|_| CipherError::CannotPadInputData)?; Ok(None) } (Aes256Ecb(mut encryptor), false) => { @@ -296,7 +337,7 @@ impl Cipher { (Aes256Cbc(encryptor), true) => { let _ = (*encryptor) .encrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot pad the input data"))?; + .map_err(|_| CipherError::CannotPadInputData)?; Ok(None) } (Aes256Cbc(mut encryptor), false) => { @@ -319,12 +360,32 @@ impl Cipher { } } +#[derive(Debug, thiserror::Error)] +pub enum DecipherError { + #[error("IV length must be 12 bytes")] + InvalidIvLength, + #[error("Invalid key length")] + InvalidKeyLength, + #[error("Invalid initialization vector")] + InvalidInitializationVector, + #[error("Cannot unpad the input data")] + CannotUnpadInputData, + #[error("Failed to authenticate data")] + DataAuthenticationFailed, + #[error("setAutoPadding(false) not supported for Aes128Gcm yet")] + SetAutoPaddingFalseAes128GcmUnsupported, + #[error("setAutoPadding(false) not supported for Aes256Gcm yet")] + SetAutoPaddingFalseAes256GcmUnsupported, + #[error("Unknown cipher {0}")] + UnknownCipher(String), +} + impl Decipher { fn new( algorithm_name: &str, key: &[u8], iv: &[u8], - ) -> Result { + ) -> Result { use Decipher::*; Ok(match algorithm_name { "aes-128-cbc" => { @@ -335,7 +396,7 @@ impl Decipher { "aes-256-ecb" => Aes256Ecb(Box::new(ecb::Decryptor::new(key.into()))), "aes-128-gcm" => { if iv.len() != 12 { - return Err(type_error("IV length must be 12 bytes")); + return Err(DecipherError::InvalidIvLength); } let decipher = @@ -345,7 +406,7 @@ impl Decipher { } "aes-256-gcm" => { if iv.len() != 12 { - return Err(type_error("IV length must be 12 bytes")); + return Err(DecipherError::InvalidIvLength); } let decipher = @@ -355,15 +416,17 @@ impl Decipher { } "aes256" | "aes-256-cbc" => { if key.len() != 32 { - return Err(range_error("Invalid key length")); + return Err(DecipherError::InvalidKeyLength); } if iv.len() != 16 { - return Err(type_error("Invalid initialization vector")); + return Err(DecipherError::InvalidInitializationVector); } Aes256Cbc(Box::new(cbc::Decryptor::new(key.into(), iv.into()))) } - _ => return Err(type_error(format!("Unknown cipher {algorithm_name}"))), + _ => { + return Err(DecipherError::UnknownCipher(algorithm_name.to_string())) + } }) } @@ -432,14 +495,14 @@ impl Decipher { input: &[u8], output: &mut [u8], auth_tag: &[u8], - ) -> Result<(), AnyError> { + ) -> Result<(), DecipherError> { use Decipher::*; match (self, auto_pad) { (Aes128Cbc(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot unpad the input data"))?; + .map_err(|_| DecipherError::CannotUnpadInputData)?; Ok(()) } (Aes128Cbc(mut decryptor), false) => { @@ -453,7 +516,7 @@ impl Decipher { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot unpad the input data"))?; + .map_err(|_| DecipherError::CannotUnpadInputData)?; Ok(()) } (Aes128Ecb(mut decryptor), false) => { @@ -467,7 +530,7 @@ impl Decipher { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot unpad the input data"))?; + .map_err(|_| DecipherError::CannotUnpadInputData)?; Ok(()) } (Aes192Ecb(mut decryptor), false) => { @@ -481,7 +544,7 @@ impl Decipher { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot unpad the input data"))?; + .map_err(|_| DecipherError::CannotUnpadInputData)?; Ok(()) } (Aes256Ecb(mut decryptor), false) => { @@ -496,28 +559,28 @@ impl Decipher { if tag.as_slice() == auth_tag { Ok(()) } else { - Err(type_error("Failed to authenticate data")) + Err(DecipherError::DataAuthenticationFailed) } } - (Aes128Gcm(_), false) => Err(type_error( - "setAutoPadding(false) not supported for Aes256Gcm yet", - )), + (Aes128Gcm(_), false) => { + Err(DecipherError::SetAutoPaddingFalseAes128GcmUnsupported) + } (Aes256Gcm(decipher), true) => { let tag = decipher.finish(); if tag.as_slice() == auth_tag { Ok(()) } else { - Err(type_error("Failed to authenticate data")) + Err(DecipherError::DataAuthenticationFailed) } } - (Aes256Gcm(_), false) => Err(type_error( - "setAutoPadding(false) not supported for Aes256Gcm yet", - )), + (Aes256Gcm(_), false) => { + Err(DecipherError::SetAutoPaddingFalseAes256GcmUnsupported) + } (Aes256Cbc(decryptor), true) => { assert!(input.len() == 16); let _ = (*decryptor) .decrypt_padded_b2b_mut::(input, output) - .map_err(|_| type_error("Cannot unpad the input data"))?; + .map_err(|_| DecipherError::CannotUnpadInputData)?; Ok(()) } (Aes256Cbc(mut decryptor), false) => { diff --git a/ext/node/ops/crypto/digest.rs b/ext/node/ops/crypto/digest.rs index 293e8e0637..a7d8fb51f1 100644 --- a/ext/node/ops/crypto/digest.rs +++ b/ext/node/ops/crypto/digest.rs @@ -1,6 +1,4 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::generic_error; -use deno_core::error::AnyError; use deno_core::GarbageCollected; use digest::Digest; use digest::DynDigest; @@ -19,7 +17,7 @@ impl Hasher { pub fn new( algorithm: &str, output_length: Option, - ) -> Result { + ) -> Result { let hash = Hash::new(algorithm, output_length)?; Ok(Self { @@ -44,7 +42,7 @@ impl Hasher { pub fn clone_inner( &self, output_length: Option, - ) -> Result, AnyError> { + ) -> Result, HashError> { let hash = self.hash.borrow(); let Some(hash) = hash.as_ref() else { return Ok(None); @@ -184,11 +182,19 @@ pub enum Hash { use Hash::*; +#[derive(Debug, thiserror::Error)] +pub enum HashError { + #[error("Output length mismatch for non-extendable algorithm")] + OutputLengthMismatch, + #[error("Digest method not supported: {0}")] + DigestMethodUnsupported(String), +} + impl Hash { pub fn new( algorithm_name: &str, output_length: Option, - ) -> Result { + ) -> Result { match algorithm_name { "shake128" => return Ok(Shake128(Default::default(), output_length)), "shake256" => return Ok(Shake256(Default::default(), output_length)), @@ -201,17 +207,13 @@ impl Hash { 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", - )); + return Err(HashError::OutputLengthMismatch); } } FixedSize(Box::new(digest)) }, _ => { - return Err(generic_error(format!( - "Digest method not supported: {algorithm_name}" - ))) + return Err(HashError::DigestMethodUnsupported(algorithm_name.to_string())) } ); @@ -243,14 +245,12 @@ impl Hash { pub fn clone_hash( &self, output_length: Option, - ) -> Result { + ) -> 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", - )); + return Err(HashError::OutputLengthMismatch); } } FixedSize(context.box_clone()) diff --git a/ext/node/ops/crypto/keys.rs b/ext/node/ops/crypto/keys.rs index 867b34e044..f164972d48 100644 --- a/ext/node/ops/crypto/keys.rs +++ b/ext/node/ops/crypto/keys.rs @@ -4,9 +4,7 @@ use std::borrow::Cow; use std::cell::RefCell; use base64::Engine; -use deno_core::error::generic_error; use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::serde_v8::BigInt as V8BigInt; use deno_core::unsync::spawn_blocking; @@ -46,6 +44,7 @@ use spki::der::Reader as _; use spki::DecodePublicKey as _; use spki::EncodePublicKey as _; use spki::SubjectPublicKeyInfoRef; +use x509_parser::error::X509Error; use x509_parser::x509; use super::dh; @@ -236,9 +235,11 @@ impl RsaPssPrivateKey { } impl EcPublicKey { - pub fn to_jwk(&self) -> Result { + pub fn to_jwk(&self) -> Result { match self { - EcPublicKey::P224(_) => Err(type_error("Unsupported JWK EC curve: P224")), + EcPublicKey::P224(_) => { + Err(AsymmetricPublicKeyJwkError::UnsupportedJwkEcCurveP224) + } EcPublicKey::P256(key) => Ok(key.to_jwk()), EcPublicKey::P384(key) => Ok(key.to_jwk()), } @@ -363,49 +364,201 @@ impl<'a> TryFrom> for RsaPssParameters<'a> { } } +#[derive(Debug, thiserror::Error)] +pub enum X509PublicKeyError { + #[error(transparent)] + X509(#[from] x509_parser::error::X509Error), + #[error(transparent)] + Rsa(#[from] rsa::Error), + #[error(transparent)] + Asn1(#[from] x509_parser::der_parser::asn1_rs::Error), + #[error(transparent)] + Ec(#[from] elliptic_curve::Error), + #[error("unsupported ec named curve")] + UnsupportedEcNamedCurve, + #[error("missing ec parameters")] + MissingEcParameters, + #[error("malformed DSS public key")] + MalformedDssPublicKey, + #[error("unsupported x509 public key type")] + UnsupportedX509KeyType, +} + +#[derive(Debug, thiserror::Error)] +pub enum RsaJwkError { + #[error(transparent)] + Base64(#[from] base64::DecodeError), + #[error(transparent)] + Rsa(#[from] rsa::Error), + #[error("missing RSA private component")] + MissingRsaPrivateComponent, +} + +#[derive(Debug, thiserror::Error)] +pub enum EcJwkError { + #[error(transparent)] + Ec(#[from] elliptic_curve::Error), + #[error("unsupported curve: {0}")] + UnsupportedCurve(String), +} + +#[derive(Debug, thiserror::Error)] +pub enum EdRawError { + #[error(transparent)] + Ed25519Signature(#[from] ed25519_dalek::SignatureError), + #[error("invalid Ed25519 key")] + InvalidEd25519Key, + #[error("unsupported curve")] + UnsupportedCurve, +} + +#[derive(Debug, thiserror::Error)] +pub enum AsymmetricPrivateKeyError { + #[error("invalid PEM private key: not valid utf8 starting at byte {0}")] + InvalidPemPrivateKeyInvalidUtf8(usize), + #[error("invalid encrypted PEM private key")] + InvalidEncryptedPemPrivateKey, + #[error("invalid PEM private key")] + InvalidPemPrivateKey, + #[error("encrypted private key requires a passphrase to decrypt")] + EncryptedPrivateKeyRequiresPassphraseToDecrypt, + #[error("invalid PKCS#1 private key")] + InvalidPkcs1PrivateKey, + #[error("invalid SEC1 private key")] + InvalidSec1PrivateKey, + #[error("unsupported PEM label: {0}")] + UnsupportedPemLabel(String), + #[error(transparent)] + RsaPssParamsParse(#[from] RsaPssParamsParseError), + #[error("invalid encrypted PKCS#8 private key")] + InvalidEncryptedPkcs8PrivateKey, + #[error("invalid PKCS#8 private key")] + InvalidPkcs8PrivateKey, + #[error("PKCS#1 private key does not support encryption with passphrase")] + Pkcs1PrivateKeyDoesNotSupportEncryptionWithPassphrase, + #[error("SEC1 private key does not support encryption with passphrase")] + Sec1PrivateKeyDoesNotSupportEncryptionWithPassphrase, + #[error("unsupported ec named curve")] + UnsupportedEcNamedCurve, + #[error("invalid private key")] + InvalidPrivateKey, + #[error("invalid DSA private key")] + InvalidDsaPrivateKey, + #[error("malformed or missing named curve in ec parameters")] + MalformedOrMissingNamedCurveInEcParameters, + #[error("unsupported key type: {0}")] + UnsupportedKeyType(String), + #[error("unsupported key format: {0}")] + UnsupportedKeyFormat(String), + #[error("invalid x25519 private key")] + InvalidX25519PrivateKey, + #[error("x25519 private key is the wrong length")] + X25519PrivateKeyIsWrongLength, + #[error("invalid Ed25519 private key")] + InvalidEd25519PrivateKey, + #[error("missing dh parameters")] + MissingDhParameters, + #[error("unsupported private key oid")] + UnsupportedPrivateKeyOid, +} + +#[derive(Debug, thiserror::Error)] +pub enum AsymmetricPublicKeyError { + #[error("invalid PEM private key: not valid utf8 starting at byte {0}")] + InvalidPemPrivateKeyInvalidUtf8(usize), + #[error("invalid PEM public key")] + InvalidPemPublicKey, + #[error("invalid PKCS#1 public key")] + InvalidPkcs1PublicKey, + #[error(transparent)] + AsymmetricPrivateKey(#[from] AsymmetricPrivateKeyError), + #[error("invalid x509 certificate")] + InvalidX509Certificate, + #[error(transparent)] + X509(#[from] x509_parser::nom::Err), + #[error(transparent)] + X509PublicKey(#[from] X509PublicKeyError), + #[error("unsupported PEM label: {0}")] + UnsupportedPemLabel(String), + #[error("invalid SPKI public key")] + InvalidSpkiPublicKey, + #[error("unsupported key type: {0}")] + UnsupportedKeyType(String), + #[error("unsupported key format: {0}")] + UnsupportedKeyFormat(String), + #[error(transparent)] + Spki(#[from] spki::Error), + #[error(transparent)] + Pkcs1(#[from] rsa::pkcs1::Error), + #[error(transparent)] + RsaPssParamsParse(#[from] RsaPssParamsParseError), + #[error("malformed DSS public key")] + MalformedDssPublicKey, + #[error("malformed or missing named curve in ec parameters")] + MalformedOrMissingNamedCurveInEcParameters, + #[error("malformed or missing public key in ec spki")] + MalformedOrMissingPublicKeyInEcSpki, + #[error(transparent)] + Ec(#[from] elliptic_curve::Error), + #[error("unsupported ec named curve")] + UnsupportedEcNamedCurve, + #[error("malformed or missing public key in x25519 spki")] + MalformedOrMissingPublicKeyInX25519Spki, + #[error("x25519 public key is too short")] + X25519PublicKeyIsTooShort, + #[error("invalid Ed25519 public key")] + InvalidEd25519PublicKey, + #[error("missing dh parameters")] + MissingDhParameters, + #[error("malformed dh parameters")] + MalformedDhParameters, + #[error("malformed or missing public key in dh spki")] + MalformedOrMissingPublicKeyInDhSpki, + #[error("unsupported private key oid")] + UnsupportedPrivateKeyOid, +} + impl KeyObjectHandle { pub fn new_asymmetric_private_key_from_js( key: &[u8], format: &str, typ: &str, passphrase: Option<&[u8]>, - ) -> Result { + ) -> Result { let document = match format { "pem" => { 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() - )) + AsymmetricPrivateKeyError::InvalidPemPrivateKeyInvalidUtf8( + err.valid_up_to(), + ) })?; if let Some(passphrase) = passphrase { - SecretDocument::from_pkcs8_encrypted_pem(pem, passphrase) - .map_err(|_| type_error("invalid encrypted PEM private key"))? + SecretDocument::from_pkcs8_encrypted_pem(pem, passphrase).map_err( + |_| AsymmetricPrivateKeyError::InvalidEncryptedPemPrivateKey, + )? } else { let (label, doc) = SecretDocument::from_pem(pem) - .map_err(|_| type_error("invalid PEM private key"))?; + .map_err(|_| AsymmetricPrivateKeyError::InvalidPemPrivateKey)?; match label { EncryptedPrivateKeyInfo::PEM_LABEL => { - return Err(type_error( - "encrypted private key requires a passphrase to decrypt", - )) + return Err(AsymmetricPrivateKeyError::EncryptedPrivateKeyRequiresPassphraseToDecrypt); } PrivateKeyInfo::PEM_LABEL => doc, rsa::pkcs1::RsaPrivateKey::PEM_LABEL => { - SecretDocument::from_pkcs1_der(doc.as_bytes()) - .map_err(|_| type_error("invalid PKCS#1 private key"))? + SecretDocument::from_pkcs1_der(doc.as_bytes()).map_err(|_| { + AsymmetricPrivateKeyError::InvalidPkcs1PrivateKey + })? } sec1::EcPrivateKey::PEM_LABEL => { SecretDocument::from_sec1_der(doc.as_bytes()) - .map_err(|_| type_error("invalid SEC1 private key"))? + .map_err(|_| AsymmetricPrivateKeyError::InvalidSec1PrivateKey)? } _ => { - return Err(type_error(format!( - "unsupported PEM label: {}", - label - ))) + return Err(AsymmetricPrivateKeyError::UnsupportedPemLabel( + label.to_string(), + )) } } } @@ -413,54 +566,57 @@ impl KeyObjectHandle { "der" => match typ { "pkcs8" => { if let Some(passphrase) = passphrase { - SecretDocument::from_pkcs8_encrypted_der(key, passphrase) - .map_err(|_| type_error("invalid encrypted PKCS#8 private key"))? + SecretDocument::from_pkcs8_encrypted_der(key, passphrase).map_err( + |_| AsymmetricPrivateKeyError::InvalidEncryptedPkcs8PrivateKey, + )? } else { SecretDocument::from_pkcs8_der(key) - .map_err(|_| type_error("invalid PKCS#8 private key"))? + .map_err(|_| AsymmetricPrivateKeyError::InvalidPkcs8PrivateKey)? } } "pkcs1" => { if passphrase.is_some() { - return Err(type_error( - "PKCS#1 private key does not support encryption with passphrase", - )); + return Err(AsymmetricPrivateKeyError::Pkcs1PrivateKeyDoesNotSupportEncryptionWithPassphrase); } SecretDocument::from_pkcs1_der(key) - .map_err(|_| type_error("invalid PKCS#1 private key"))? + .map_err(|_| AsymmetricPrivateKeyError::InvalidPkcs1PrivateKey)? } "sec1" => { if passphrase.is_some() { - return Err(type_error( - "SEC1 private key does not support encryption with passphrase", - )); + return Err(AsymmetricPrivateKeyError::Sec1PrivateKeyDoesNotSupportEncryptionWithPassphrase); } SecretDocument::from_sec1_der(key) - .map_err(|_| type_error("invalid SEC1 private key"))? + .map_err(|_| AsymmetricPrivateKeyError::InvalidSec1PrivateKey)? + } + _ => { + return Err(AsymmetricPrivateKeyError::UnsupportedKeyType( + typ.to_string(), + )) } - _ => return Err(type_error(format!("unsupported key type: {}", typ))), }, _ => { - return Err(type_error(format!("unsupported key format: {}", format))) + return Err(AsymmetricPrivateKeyError::UnsupportedKeyFormat( + format.to_string(), + )) } }; let pk_info = PrivateKeyInfo::try_from(document.as_bytes()) - .map_err(|_| type_error("invalid private key"))?; + .map_err(|_| AsymmetricPrivateKeyError::InvalidPrivateKey)?; let alg = pk_info.algorithm.oid; let private_key = match alg { RSA_ENCRYPTION_OID => { let private_key = rsa::RsaPrivateKey::from_pkcs1_der(pk_info.private_key) - .map_err(|_| type_error("invalid PKCS#1 private key"))?; + .map_err(|_| AsymmetricPrivateKeyError::InvalidPkcs1PrivateKey)?; AsymmetricPrivateKey::Rsa(private_key) } RSASSA_PSS_OID => { let details = parse_rsa_pss_params(pk_info.algorithm.parameters)?; let private_key = rsa::RsaPrivateKey::from_pkcs1_der(pk_info.private_key) - .map_err(|_| type_error("invalid PKCS#1 private key"))?; + .map_err(|_| AsymmetricPrivateKeyError::InvalidPkcs1PrivateKey)?; AsymmetricPrivateKey::RsaPss(RsaPssPrivateKey { key: private_key, details, @@ -468,40 +624,43 @@ impl KeyObjectHandle { } DSA_OID => { let private_key = dsa::SigningKey::try_from(pk_info) - .map_err(|_| type_error("invalid DSA private key"))?; + .map_err(|_| AsymmetricPrivateKeyError::InvalidDsaPrivateKey)?; AsymmetricPrivateKey::Dsa(private_key) } EC_OID => { let named_curve = pk_info.algorithm.parameters_oid().map_err(|_| { - type_error("malformed or missing named curve in ec parameters") + AsymmetricPrivateKeyError::MalformedOrMissingNamedCurveInEcParameters })?; match named_curve { ID_SECP224R1_OID => { - let secret_key = - p224::SecretKey::from_sec1_der(pk_info.private_key) - .map_err(|_| type_error("invalid SEC1 private key"))?; + let secret_key = p224::SecretKey::from_sec1_der( + pk_info.private_key, + ) + .map_err(|_| AsymmetricPrivateKeyError::InvalidSec1PrivateKey)?; AsymmetricPrivateKey::Ec(EcPrivateKey::P224(secret_key)) } ID_SECP256R1_OID => { - let secret_key = - p256::SecretKey::from_sec1_der(pk_info.private_key) - .map_err(|_| type_error("invalid SEC1 private key"))?; + let secret_key = p256::SecretKey::from_sec1_der( + pk_info.private_key, + ) + .map_err(|_| AsymmetricPrivateKeyError::InvalidSec1PrivateKey)?; AsymmetricPrivateKey::Ec(EcPrivateKey::P256(secret_key)) } ID_SECP384R1_OID => { - let secret_key = - p384::SecretKey::from_sec1_der(pk_info.private_key) - .map_err(|_| type_error("invalid SEC1 private key"))?; + let secret_key = p384::SecretKey::from_sec1_der( + pk_info.private_key, + ) + .map_err(|_| AsymmetricPrivateKeyError::InvalidSec1PrivateKey)?; AsymmetricPrivateKey::Ec(EcPrivateKey::P384(secret_key)) } - _ => return Err(type_error("unsupported ec named curve")), + _ => return Err(AsymmetricPrivateKeyError::UnsupportedEcNamedCurve), } } X25519_OID => { let string_ref = OctetStringRef::from_der(pk_info.private_key) - .map_err(|_| type_error("invalid x25519 private key"))?; + .map_err(|_| AsymmetricPrivateKeyError::InvalidX25519PrivateKey)?; if string_ref.as_bytes().len() != 32 { - return Err(type_error("x25519 private key is the wrong length")); + return Err(AsymmetricPrivateKeyError::X25519PrivateKeyIsWrongLength); } let mut bytes = [0; 32]; bytes.copy_from_slice(string_ref.as_bytes()); @@ -509,22 +668,22 @@ impl KeyObjectHandle { } ED25519_OID => { let signing_key = ed25519_dalek::SigningKey::try_from(pk_info) - .map_err(|_| type_error("invalid Ed25519 private key"))?; + .map_err(|_| AsymmetricPrivateKeyError::InvalidEd25519PrivateKey)?; AsymmetricPrivateKey::Ed25519(signing_key) } DH_KEY_AGREEMENT_OID => { let params = pk_info .algorithm .parameters - .ok_or_else(|| type_error("missing dh parameters"))?; + .ok_or(AsymmetricPrivateKeyError::MissingDhParameters)?; let params = pkcs3::DhParameter::from_der(¶ms.to_der().unwrap()) - .map_err(|_| type_error("malformed dh parameters"))?; + .map_err(|_| AsymmetricPrivateKeyError::MissingDhParameters)?; AsymmetricPrivateKey::Dh(DhPrivateKey { key: dh::PrivateKey::from_bytes(pk_info.private_key), params, }) } - _ => return Err(type_error("unsupported private key oid")), + _ => return Err(AsymmetricPrivateKeyError::UnsupportedPrivateKeyOid), }; Ok(KeyObjectHandle::AsymmetricPrivate(private_key)) @@ -532,7 +691,7 @@ impl KeyObjectHandle { pub fn new_x509_public_key( spki: &x509::SubjectPublicKeyInfo, - ) -> Result { + ) -> Result { use x509_parser::der_parser::asn1_rs::oid; use x509_parser::public_key::PublicKey; @@ -565,18 +724,18 @@ impl KeyObjectHandle { let public_key = p384::PublicKey::from_sec1_bytes(data)?; AsymmetricPublicKey::Ec(EcPublicKey::P384(public_key)) } - _ => return Err(type_error("unsupported ec named curve")), + _ => return Err(X509PublicKeyError::UnsupportedEcNamedCurve), } } else { - return Err(type_error("missing ec parameters")); + return Err(X509PublicKeyError::MissingEcParameters); } } PublicKey::DSA(_) => { let verifying_key = dsa::VerifyingKey::from_public_key_der(spki.raw) - .map_err(|_| type_error("malformed DSS public key"))?; + .map_err(|_| X509PublicKeyError::MalformedDssPublicKey)?; AsymmetricPublicKey::Dsa(verifying_key) } - _ => return Err(type_error("unsupported x509 public key type")), + _ => return Err(X509PublicKeyError::UnsupportedX509KeyType), }; Ok(KeyObjectHandle::AsymmetricPublic(key)) @@ -585,7 +744,7 @@ impl KeyObjectHandle { pub fn new_rsa_jwk( jwk: RsaJwkKey, is_public: bool, - ) -> Result { + ) -> Result { use base64::prelude::BASE64_URL_SAFE_NO_PAD; let n = BASE64_URL_SAFE_NO_PAD.decode(jwk.n.as_bytes())?; @@ -604,19 +763,19 @@ impl KeyObjectHandle { let d = BASE64_URL_SAFE_NO_PAD.decode( jwk .d - .ok_or_else(|| type_error("missing RSA private component"))? + .ok_or(RsaJwkError::MissingRsaPrivateComponent)? .as_bytes(), )?; let p = BASE64_URL_SAFE_NO_PAD.decode( jwk .p - .ok_or_else(|| type_error("missing RSA private component"))? + .ok_or(RsaJwkError::MissingRsaPrivateComponent)? .as_bytes(), )?; let q = BASE64_URL_SAFE_NO_PAD.decode( jwk .q - .ok_or_else(|| type_error("missing RSA private component"))? + .ok_or(RsaJwkError::MissingRsaPrivateComponent)? .as_bytes(), )?; @@ -640,7 +799,7 @@ impl KeyObjectHandle { pub fn new_ec_jwk( jwk: &JwkEcKey, is_public: bool, - ) -> Result { + ) -> Result { // https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.1 let handle = match jwk.crv() { "P-256" if is_public => { @@ -660,7 +819,7 @@ impl KeyObjectHandle { EcPrivateKey::P384(p384::SecretKey::from_jwk(jwk)?), )), _ => { - return Err(type_error(format!("unsupported curve: {}", jwk.crv()))); + return Err(EcJwkError::UnsupportedCurve(jwk.crv().to_string())); } }; @@ -671,12 +830,11 @@ impl KeyObjectHandle { curve: &str, data: &[u8], is_public: bool, - ) -> Result { + ) -> Result { match curve { "Ed25519" => { - let data = data - .try_into() - .map_err(|_| type_error("invalid Ed25519 key"))?; + let data = + data.try_into().map_err(|_| EdRawError::InvalidEd25519Key)?; if !is_public { Ok(KeyObjectHandle::AsymmetricPrivate( AsymmetricPrivateKey::Ed25519( @@ -692,9 +850,8 @@ impl KeyObjectHandle { } } "X25519" => { - let data: [u8; 32] = data - .try_into() - .map_err(|_| type_error("invalid x25519 key"))?; + let data: [u8; 32] = + data.try_into().map_err(|_| EdRawError::InvalidEd25519Key)?; if !is_public { Ok(KeyObjectHandle::AsymmetricPrivate( AsymmetricPrivateKey::X25519(x25519_dalek::StaticSecret::from( @@ -707,7 +864,7 @@ impl KeyObjectHandle { )) } } - _ => Err(type_error("unsupported curve")), + _ => Err(EdRawError::UnsupportedCurve), } } @@ -716,24 +873,23 @@ impl KeyObjectHandle { format: &str, typ: &str, passphrase: Option<&[u8]>, - ) -> Result { + ) -> Result { let document = match format { "pem" => { let pem = std::str::from_utf8(key).map_err(|err| { - type_error(format!( - "invalid PEM public key: not valid utf8 starting at byte {}", - err.valid_up_to() - )) + AsymmetricPublicKeyError::InvalidPemPrivateKeyInvalidUtf8( + err.valid_up_to(), + ) })?; let (label, document) = Document::from_pem(pem) - .map_err(|_| type_error("invalid PEM public key"))?; + .map_err(|_| AsymmetricPublicKeyError::InvalidPemPublicKey)?; match label { SubjectPublicKeyInfoRef::PEM_LABEL => document, rsa::pkcs1::RsaPublicKey::PEM_LABEL => { Document::from_pkcs1_der(document.as_bytes()) - .map_err(|_| type_error("invalid PKCS#1 public key"))? + .map_err(|_| AsymmetricPublicKeyError::InvalidPkcs1PublicKey)? } EncryptedPrivateKeyInfo::PEM_LABEL | PrivateKeyInfo::PEM_LABEL @@ -754,27 +910,36 @@ impl KeyObjectHandle { } "CERTIFICATE" => { let (_, pem) = x509_parser::pem::parse_x509_pem(pem.as_bytes()) - .map_err(|_| type_error("invalid x509 certificate"))?; + .map_err(|_| AsymmetricPublicKeyError::InvalidX509Certificate)?; let cert = pem.parse_x509()?; let public_key = cert.tbs_certificate.subject_pki; - return KeyObjectHandle::new_x509_public_key(&public_key); + return KeyObjectHandle::new_x509_public_key(&public_key) + .map_err(Into::into); } _ => { - return Err(type_error(format!("unsupported PEM label: {}", label))) + return Err(AsymmetricPublicKeyError::UnsupportedPemLabel( + label.to_string(), + )) } } } "der" => match typ { "pkcs1" => Document::from_pkcs1_der(key) - .map_err(|_| type_error("invalid PKCS#1 public key"))?, + .map_err(|_| AsymmetricPublicKeyError::InvalidPkcs1PublicKey)?, "spki" => Document::from_public_key_der(key) - .map_err(|_| type_error("invalid SPKI public key"))?, - _ => return Err(type_error(format!("unsupported key type: {}", typ))), + .map_err(|_| AsymmetricPublicKeyError::InvalidSpkiPublicKey)?, + _ => { + return Err(AsymmetricPublicKeyError::UnsupportedKeyType( + typ.to_string(), + )) + } }, _ => { - return Err(type_error(format!("unsupported key format: {}", format))) + return Err(AsymmetricPublicKeyError::UnsupportedKeyType( + format.to_string(), + )) } }; @@ -799,16 +964,16 @@ impl KeyObjectHandle { } DSA_OID => { let verifying_key = dsa::VerifyingKey::try_from(spki) - .map_err(|_| type_error("malformed DSS public key"))?; + .map_err(|_| AsymmetricPublicKeyError::MalformedDssPublicKey)?; AsymmetricPublicKey::Dsa(verifying_key) } EC_OID => { let named_curve = spki.algorithm.parameters_oid().map_err(|_| { - type_error("malformed or missing named curve in ec parameters") - })?; - let data = spki.subject_public_key.as_bytes().ok_or_else(|| { - type_error("malformed or missing public key in ec spki") + AsymmetricPublicKeyError::MalformedOrMissingNamedCurveInEcParameters })?; + let data = spki.subject_public_key.as_bytes().ok_or( + AsymmetricPublicKeyError::MalformedOrMissingPublicKeyInEcSpki, + )?; match named_curve { ID_SECP224R1_OID => { @@ -823,54 +988,68 @@ impl KeyObjectHandle { let public_key = p384::PublicKey::from_sec1_bytes(data)?; AsymmetricPublicKey::Ec(EcPublicKey::P384(public_key)) } - _ => return Err(type_error("unsupported ec named curve")), + _ => return Err(AsymmetricPublicKeyError::UnsupportedEcNamedCurve), } } X25519_OID => { let mut bytes = [0; 32]; - let data = spki.subject_public_key.as_bytes().ok_or_else(|| { - type_error("malformed or missing public key in x25519 spki") - })?; + let data = spki.subject_public_key.as_bytes().ok_or( + AsymmetricPublicKeyError::MalformedOrMissingPublicKeyInX25519Spki, + )?; if data.len() < 32 { - return Err(type_error("x25519 public key is too short")); + return Err(AsymmetricPublicKeyError::X25519PublicKeyIsTooShort); } bytes.copy_from_slice(&data[0..32]); AsymmetricPublicKey::X25519(x25519_dalek::PublicKey::from(bytes)) } ED25519_OID => { let verifying_key = ed25519_dalek::VerifyingKey::try_from(spki) - .map_err(|_| type_error("invalid Ed25519 private key"))?; + .map_err(|_| AsymmetricPublicKeyError::InvalidEd25519PublicKey)?; AsymmetricPublicKey::Ed25519(verifying_key) } DH_KEY_AGREEMENT_OID => { let params = spki .algorithm .parameters - .ok_or_else(|| type_error("missing dh parameters"))?; + .ok_or(AsymmetricPublicKeyError::MissingDhParameters)?; let params = pkcs3::DhParameter::from_der(¶ms.to_der().unwrap()) - .map_err(|_| type_error("malformed dh parameters"))?; + .map_err(|_| AsymmetricPublicKeyError::MalformedDhParameters)?; let Some(subject_public_key) = spki.subject_public_key.as_bytes() else { - return Err(type_error("malformed or missing public key in dh spki")); + return Err( + AsymmetricPublicKeyError::MalformedOrMissingPublicKeyInDhSpki, + ); }; AsymmetricPublicKey::Dh(DhPublicKey { key: dh::PublicKey::from_bytes(subject_public_key), params, }) } - _ => return Err(type_error("unsupported public key oid")), + _ => return Err(AsymmetricPublicKeyError::UnsupportedPrivateKeyOid), }; Ok(KeyObjectHandle::AsymmetricPublic(public_key)) } } +#[derive(Debug, thiserror::Error)] +pub enum RsaPssParamsParseError { + #[error("malformed pss private key parameters")] + MalformedPssPrivateKeyParameters, + #[error("unsupported pss hash algorithm")] + UnsupportedPssHashAlgorithm, + #[error("unsupported pss mask gen algorithm")] + UnsupportedPssMaskGenAlgorithm, + #[error("malformed or missing pss mask gen algorithm parameters")] + MalformedOrMissingPssMaskGenAlgorithm, +} + fn parse_rsa_pss_params( parameters: Option>, -) -> Result, deno_core::anyhow::Error> { +) -> Result, RsaPssParamsParseError> { let details = if let Some(parameters) = parameters { let params = RsaPssParameters::try_from(parameters) - .map_err(|_| type_error("malformed pss private key parameters"))?; + .map_err(|_| RsaPssParamsParseError::MalformedPssPrivateKeyParameters)?; let hash_algorithm = match params.hash_algorithm.map(|k| k.oid) { Some(ID_SHA1_OID) => RsaPssHashAlgorithm::Sha1, @@ -881,16 +1060,16 @@ fn parse_rsa_pss_params( Some(ID_SHA512_224_OID) => RsaPssHashAlgorithm::Sha512_224, Some(ID_SHA512_256_OID) => RsaPssHashAlgorithm::Sha512_256, None => RsaPssHashAlgorithm::Sha1, - _ => return Err(type_error("unsupported pss hash algorithm")), + _ => return Err(RsaPssParamsParseError::UnsupportedPssHashAlgorithm), }; let mf1_hash_algorithm = match params.mask_gen_algorithm { Some(alg) => { if alg.oid != ID_MFG1 { - return Err(type_error("unsupported pss mask gen algorithm")); + return Err(RsaPssParamsParseError::UnsupportedPssMaskGenAlgorithm); } let params = alg.parameters_oid().map_err(|_| { - type_error("malformed or missing pss mask gen algorithm parameters") + RsaPssParamsParseError::MalformedOrMissingPssMaskGenAlgorithm })?; match params { ID_SHA1_OID => RsaPssHashAlgorithm::Sha1, @@ -900,7 +1079,9 @@ fn parse_rsa_pss_params( ID_SHA512_OID => RsaPssHashAlgorithm::Sha512, ID_SHA512_224_OID => RsaPssHashAlgorithm::Sha512_224, ID_SHA512_256_OID => RsaPssHashAlgorithm::Sha512_256, - _ => return Err(type_error("unsupported pss mask gen algorithm")), + _ => { + return Err(RsaPssParamsParseError::UnsupportedPssMaskGenAlgorithm) + } } } None => hash_algorithm, @@ -921,14 +1102,49 @@ fn parse_rsa_pss_params( Ok(details) } -use base64::prelude::BASE64_URL_SAFE_NO_PAD; - fn bytes_to_b64(bytes: &[u8]) -> String { + use base64::prelude::BASE64_URL_SAFE_NO_PAD; BASE64_URL_SAFE_NO_PAD.encode(bytes) } +#[derive(Debug, thiserror::Error)] +pub enum AsymmetricPublicKeyJwkError { + #[error("key is not an asymmetric public key")] + KeyIsNotAsymmetricPublicKey, + #[error("Unsupported JWK EC curve: P224")] + UnsupportedJwkEcCurveP224, + #[error("jwk export not implemented for this key type")] + JwkExportNotImplementedForKeyType, +} + +#[derive(Debug, thiserror::Error)] +pub enum AsymmetricPublicKeyDerError { + #[error("key is not an asymmetric public key")] + KeyIsNotAsymmetricPublicKey, + #[error("invalid RSA public key")] + InvalidRsaPublicKey, + #[error("exporting non-RSA public key as PKCS#1 is not supported")] + ExportingNonRsaPublicKeyAsPkcs1Unsupported, + #[error("invalid EC public key")] + InvalidEcPublicKey, + #[error("exporting RSA-PSS public key as SPKI is not supported yet")] + ExportingNonRsaPssPublicKeyAsSpkiUnsupported, + #[error("invalid DSA public key")] + InvalidDsaPublicKey, + #[error("invalid X25519 public key")] + InvalidX25519PublicKey, + #[error("invalid Ed25519 public key")] + InvalidEd25519PublicKey, + #[error("invalid DH public key")] + InvalidDhPublicKey, + #[error("unsupported key type: {0}")] + UnsupportedKeyType(String), +} + impl AsymmetricPublicKey { - fn export_jwk(&self) -> Result { + fn export_jwk( + &self, + ) -> Result { match self { AsymmetricPublicKey::Ec(key) => { let jwk = key.to_jwk()?; @@ -974,40 +1190,39 @@ impl AsymmetricPublicKey { }); Ok(jwk) } - _ => Err(type_error("jwk export not implemented for this key type")), + _ => Err(AsymmetricPublicKeyJwkError::JwkExportNotImplementedForKeyType), } } - fn export_der(&self, typ: &str) -> Result, AnyError> { + fn export_der( + &self, + typ: &str, + ) -> Result, AsymmetricPublicKeyDerError> { match typ { "pkcs1" => match self { AsymmetricPublicKey::Rsa(key) => { let der = key .to_pkcs1_der() - .map_err(|_| type_error("invalid RSA public key"))? + .map_err(|_| AsymmetricPublicKeyDerError::InvalidRsaPublicKey)? .into_vec() .into_boxed_slice(); Ok(der) } - _ => Err(type_error( - "exporting non-RSA public key as PKCS#1 is not supported", - )), + _ => Err(AsymmetricPublicKeyDerError::ExportingNonRsaPublicKeyAsPkcs1Unsupported), }, "spki" => { let der = match self { AsymmetricPublicKey::Rsa(key) => key .to_public_key_der() - .map_err(|_| type_error("invalid RSA public key"))? + .map_err(|_| AsymmetricPublicKeyDerError::InvalidRsaPublicKey)? .into_vec() .into_boxed_slice(), AsymmetricPublicKey::RsaPss(_key) => { - return Err(generic_error( - "exporting RSA-PSS public key as SPKI is not supported yet", - )) + return Err(AsymmetricPublicKeyDerError::ExportingNonRsaPssPublicKeyAsSpkiUnsupported) } AsymmetricPublicKey::Dsa(key) => key .to_public_key_der() - .map_err(|_| type_error("invalid DSA public key"))? + .map_err(|_| AsymmetricPublicKeyDerError::InvalidDsaPublicKey)? .into_vec() .into_boxed_slice(), AsymmetricPublicKey::Ec(key) => { @@ -1023,12 +1238,12 @@ impl AsymmetricPublicKey { parameters: Some(asn1::AnyRef::from(&oid)), }, subject_public_key: BitStringRef::from_bytes(&sec1) - .map_err(|_| type_error("invalid EC public key"))?, + .map_err(|_| AsymmetricPublicKeyDerError::InvalidEcPublicKey)?, }; spki .to_der() - .map_err(|_| type_error("invalid EC public key"))? + .map_err(|_| AsymmetricPublicKeyDerError::InvalidEcPublicKey)? .into_boxed_slice() } AsymmetricPublicKey::X25519(key) => { @@ -1038,12 +1253,12 @@ impl AsymmetricPublicKey { parameters: None, }, subject_public_key: BitStringRef::from_bytes(key.as_bytes()) - .map_err(|_| type_error("invalid X25519 public key"))?, + .map_err(|_| AsymmetricPublicKeyDerError::InvalidX25519PublicKey)?, }; spki .to_der() - .map_err(|_| type_error("invalid X25519 public key"))? + .map_err(|_| AsymmetricPublicKeyDerError::InvalidX25519PublicKey)? .into_boxed_slice() } AsymmetricPublicKey::Ed25519(key) => { @@ -1053,12 +1268,12 @@ impl AsymmetricPublicKey { parameters: None, }, subject_public_key: BitStringRef::from_bytes(key.as_bytes()) - .map_err(|_| type_error("invalid Ed25519 public key"))?, + .map_err(|_| AsymmetricPublicKeyDerError::InvalidEd25519PublicKey)?, }; spki .to_der() - .map_err(|_| type_error("invalid Ed25519 public key"))? + .map_err(|_| AsymmetricPublicKeyDerError::InvalidEd25519PublicKey)? .into_boxed_slice() } AsymmetricPublicKey::Dh(key) => { @@ -1071,43 +1286,67 @@ impl AsymmetricPublicKey { }, subject_public_key: BitStringRef::from_bytes(&public_key_bytes) .map_err(|_| { - type_error("invalid DH public key") + AsymmetricPublicKeyDerError::InvalidDhPublicKey })?, }; spki .to_der() - .map_err(|_| type_error("invalid DH public key"))? + .map_err(|_| AsymmetricPublicKeyDerError::InvalidDhPublicKey)? .into_boxed_slice() } }; Ok(der) } - _ => Err(type_error(format!("unsupported key type: {}", typ))), + _ => Err(AsymmetricPublicKeyDerError::UnsupportedKeyType(typ.to_string())), } } } +#[derive(Debug, thiserror::Error)] +pub enum AsymmetricPrivateKeyDerError { + #[error("key is not an asymmetric private key")] + KeyIsNotAsymmetricPrivateKey, + #[error("invalid RSA private key")] + InvalidRsaPrivateKey, + #[error("exporting non-RSA private key as PKCS#1 is not supported")] + ExportingNonRsaPrivateKeyAsPkcs1Unsupported, + #[error("invalid EC private key")] + InvalidEcPrivateKey, + #[error("exporting non-EC private key as SEC1 is not supported")] + ExportingNonEcPrivateKeyAsSec1Unsupported, + #[error("exporting RSA-PSS private key as PKCS#8 is not supported yet")] + ExportingNonRsaPssPrivateKeyAsPkcs8Unsupported, + #[error("invalid DSA private key")] + InvalidDsaPrivateKey, + #[error("invalid X25519 private key")] + InvalidX25519PrivateKey, + #[error("invalid Ed25519 private key")] + InvalidEd25519PrivateKey, + #[error("invalid DH private key")] + InvalidDhPrivateKey, + #[error("unsupported key type: {0}")] + UnsupportedKeyType(String), +} + impl AsymmetricPrivateKey { fn export_der( &self, typ: &str, // cipher: Option<&str>, // passphrase: Option<&str>, - ) -> Result, AnyError> { + ) -> Result, AsymmetricPrivateKeyDerError> { match typ { "pkcs1" => match self { AsymmetricPrivateKey::Rsa(key) => { let der = key .to_pkcs1_der() - .map_err(|_| type_error("invalid RSA private key"))? + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidRsaPrivateKey)? .to_bytes() .to_vec() .into_boxed_slice(); Ok(der) } - _ => Err(type_error( - "exporting non-RSA private key as PKCS#1 is not supported", - )), + _ => Err(AsymmetricPrivateKeyDerError::ExportingNonRsaPrivateKeyAsPkcs1Unsupported), }, "sec1" => match self { AsymmetricPrivateKey::Ec(key) => { @@ -1116,30 +1355,26 @@ impl AsymmetricPrivateKey { EcPrivateKey::P256(key) => key.to_sec1_der(), EcPrivateKey::P384(key) => key.to_sec1_der(), } - .map_err(|_| type_error("invalid EC private key"))?; + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidEcPrivateKey)?; Ok(sec1.to_vec().into_boxed_slice()) } - _ => Err(type_error( - "exporting non-EC private key as SEC1 is not supported", - )), + _ => Err(AsymmetricPrivateKeyDerError::ExportingNonEcPrivateKeyAsSec1Unsupported), }, "pkcs8" => { let der = match self { AsymmetricPrivateKey::Rsa(key) => { let document = key .to_pkcs8_der() - .map_err(|_| type_error("invalid RSA private key"))?; + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidRsaPrivateKey)?; document.to_bytes().to_vec().into_boxed_slice() } AsymmetricPrivateKey::RsaPss(_key) => { - return Err(generic_error( - "exporting RSA-PSS private key as PKCS#8 is not supported yet", - )) + return Err(AsymmetricPrivateKeyDerError::ExportingNonRsaPssPrivateKeyAsPkcs8Unsupported) } AsymmetricPrivateKey::Dsa(key) => { let document = key .to_pkcs8_der() - .map_err(|_| type_error("invalid DSA private key"))?; + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidDsaPrivateKey)?; document.to_bytes().to_vec().into_boxed_slice() } AsymmetricPrivateKey::Ec(key) => { @@ -1148,14 +1383,14 @@ impl AsymmetricPrivateKey { EcPrivateKey::P256(key) => key.to_pkcs8_der(), EcPrivateKey::P384(key) => key.to_pkcs8_der(), } - .map_err(|_| type_error("invalid EC private key"))?; + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidEcPrivateKey)?; document.to_bytes().to_vec().into_boxed_slice() } AsymmetricPrivateKey::X25519(key) => { let private_key = OctetStringRef::new(key.as_bytes()) - .map_err(|_| type_error("invalid X25519 private key"))? + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidX25519PrivateKey)? .to_der() - .map_err(|_| type_error("invalid X25519 private key"))?; + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidX25519PrivateKey)?; let private_key = PrivateKeyInfo { algorithm: rsa::pkcs8::AlgorithmIdentifierRef { @@ -1168,15 +1403,15 @@ impl AsymmetricPrivateKey { let der = private_key .to_der() - .map_err(|_| type_error("invalid X25519 private key"))? + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidX25519PrivateKey)? .into_boxed_slice(); return Ok(der); } AsymmetricPrivateKey::Ed25519(key) => { let private_key = OctetStringRef::new(key.as_bytes()) - .map_err(|_| type_error("invalid Ed25519 private key"))? + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidEd25519PrivateKey)? .to_der() - .map_err(|_| type_error("invalid Ed25519 private key"))?; + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidEd25519PrivateKey)?; let private_key = PrivateKeyInfo { algorithm: rsa::pkcs8::AlgorithmIdentifierRef { @@ -1189,7 +1424,7 @@ impl AsymmetricPrivateKey { private_key .to_der() - .map_err(|_| type_error("invalid ED25519 private key"))? + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidEd25519PrivateKey)? .into_boxed_slice() } AsymmetricPrivateKey::Dh(key) => { @@ -1206,14 +1441,14 @@ impl AsymmetricPrivateKey { private_key .to_der() - .map_err(|_| type_error("invalid DH private key"))? + .map_err(|_| AsymmetricPrivateKeyDerError::InvalidDhPrivateKey)? .into_boxed_slice() } }; Ok(der) } - _ => Err(type_error(format!("unsupported key type: {}", typ))), + _ => Err(AsymmetricPrivateKeyDerError::UnsupportedKeyType(typ.to_string())), } } } @@ -1225,7 +1460,7 @@ pub fn op_node_create_private_key( #[string] format: &str, #[string] typ: &str, #[buffer] passphrase: Option<&[u8]>, -) -> Result { +) -> Result { KeyObjectHandle::new_asymmetric_private_key_from_js( key, format, typ, passphrase, ) @@ -1237,7 +1472,7 @@ pub fn op_node_create_ed_raw( #[string] curve: &str, #[buffer] key: &[u8], is_public: bool, -) -> Result { +) -> Result { KeyObjectHandle::new_ed_raw(curve, key, is_public) } @@ -1255,16 +1490,16 @@ pub struct RsaJwkKey { pub fn op_node_create_rsa_jwk( #[serde] jwk: RsaJwkKey, is_public: bool, -) -> Result { +) -> Result { KeyObjectHandle::new_rsa_jwk(jwk, is_public) } #[op2] #[cppgc] pub fn op_node_create_ec_jwk( - #[serde] jwk: elliptic_curve::JwkEcKey, + #[serde] jwk: JwkEcKey, is_public: bool, -) -> Result { +) -> Result { KeyObjectHandle::new_ec_jwk(&jwk, is_public) } @@ -1275,7 +1510,7 @@ pub fn op_node_create_public_key( #[string] format: &str, #[string] typ: &str, #[buffer] passphrase: Option<&[u8]>, -) -> Result { +) -> Result { KeyObjectHandle::new_asymmetric_public_key_from_js( key, format, typ, passphrase, ) @@ -1293,7 +1528,7 @@ pub fn op_node_create_secret_key( #[string] pub fn op_node_get_asymmetric_key_type( #[cppgc] handle: &KeyObjectHandle, -) -> Result<&'static str, AnyError> { +) -> Result<&'static str, deno_core::error::AnyError> { match handle { KeyObjectHandle::AsymmetricPrivate(AsymmetricPrivateKey::Rsa(_)) | KeyObjectHandle::AsymmetricPublic(AsymmetricPublicKey::Rsa(_)) => { @@ -1364,7 +1599,7 @@ pub enum AsymmetricKeyDetails { #[serde] pub fn op_node_get_asymmetric_key_details( #[cppgc] handle: &KeyObjectHandle, -) -> Result { +) -> Result { match handle { KeyObjectHandle::AsymmetricPrivate(private_key) => match private_key { AsymmetricPrivateKey::Rsa(key) => { @@ -1482,12 +1717,10 @@ pub fn op_node_get_asymmetric_key_details( #[smi] pub fn op_node_get_symmetric_key_size( #[cppgc] handle: &KeyObjectHandle, -) -> Result { +) -> Result { match handle { - KeyObjectHandle::AsymmetricPrivate(_) => { - Err(type_error("asymmetric key is not a symmetric key")) - } - KeyObjectHandle::AsymmetricPublic(_) => { + KeyObjectHandle::AsymmetricPrivate(_) + | KeyObjectHandle::AsymmetricPublic(_) => { Err(type_error("asymmetric key is not a symmetric key")) } KeyObjectHandle::Secret(key) => Ok(key.len() * 8), @@ -1592,13 +1825,17 @@ pub async fn op_node_generate_rsa_key_async( .unwrap() } +#[derive(Debug, thiserror::Error)] +#[error("digest not allowed for RSA-PSS keys{}", .0.as_ref().map(|digest| format!(": {digest}")).unwrap_or_default())] +pub struct GenerateRsaPssError(Option); + fn generate_rsa_pss( modulus_length: usize, public_exponent: usize, hash_algorithm: Option<&str>, mf1_hash_algorithm: Option<&str>, salt_length: Option, -) -> Result { +) -> Result { let key = RsaPrivateKey::new_with_exp( &mut thread_rng(), modulus_length, @@ -1617,25 +1854,19 @@ fn generate_rsa_pss( let hash_algorithm = match_fixed_digest_with_oid!( hash_algorithm, fn (algorithm: Option) { - algorithm.ok_or_else(|| type_error("digest not allowed for RSA-PSS keys: {}"))? + algorithm.ok_or(GenerateRsaPssError(None))? }, _ => { - return Err(type_error(format!( - "digest not allowed for RSA-PSS keys: {}", - hash_algorithm - ))) + return Err(GenerateRsaPssError(Some(hash_algorithm.to_string()))) } ); let mf1_hash_algorithm = match_fixed_digest_with_oid!( mf1_hash_algorithm, fn (algorithm: Option) { - algorithm.ok_or_else(|| type_error("digest not allowed for RSA-PSS keys: {}"))? + algorithm.ok_or(GenerateRsaPssError(None))? }, _ => { - return Err(type_error(format!( - "digest not allowed for RSA-PSS keys: {}", - mf1_hash_algorithm - ))) + return Err(GenerateRsaPssError(Some(mf1_hash_algorithm.to_string()))) } ); let salt_length = @@ -1663,7 +1894,7 @@ pub fn op_node_generate_rsa_pss_key( #[string] hash_algorithm: Option, // todo: Option<&str> not supproted in ops yet #[string] mf1_hash_algorithm: Option, // todo: Option<&str> not supproted in ops yet #[smi] salt_length: Option, -) -> Result { +) -> Result { generate_rsa_pss( modulus_length, public_exponent, @@ -1681,7 +1912,7 @@ pub async fn op_node_generate_rsa_pss_key_async( #[string] hash_algorithm: Option, // todo: Option<&str> not supproted in ops yet #[string] mf1_hash_algorithm: Option, // todo: Option<&str> not supproted in ops yet #[smi] salt_length: Option, -) -> Result { +) -> Result { spawn_blocking(move || { generate_rsa_pss( modulus_length, @@ -1698,7 +1929,7 @@ pub async fn op_node_generate_rsa_pss_key_async( fn dsa_generate( modulus_length: usize, divisor_length: usize, -) -> Result { +) -> Result { let mut rng = rand::thread_rng(); use dsa::Components; use dsa::KeySize; @@ -1729,7 +1960,7 @@ fn dsa_generate( pub fn op_node_generate_dsa_key( #[smi] modulus_length: usize, #[smi] divisor_length: usize, -) -> Result { +) -> Result { dsa_generate(modulus_length, divisor_length) } @@ -1738,13 +1969,15 @@ pub fn op_node_generate_dsa_key( pub async fn op_node_generate_dsa_key_async( #[smi] modulus_length: usize, #[smi] divisor_length: usize, -) -> Result { +) -> Result { spawn_blocking(move || dsa_generate(modulus_length, divisor_length)) .await .unwrap() } -fn ec_generate(named_curve: &str) -> Result { +fn ec_generate( + named_curve: &str, +) -> Result { let mut rng = rand::thread_rng(); // TODO(@littledivy): Support public key point encoding. // Default is uncompressed. @@ -1776,7 +2009,7 @@ fn ec_generate(named_curve: &str) -> Result { #[cppgc] pub fn op_node_generate_ec_key( #[string] named_curve: &str, -) -> Result { +) -> Result { ec_generate(named_curve) } @@ -1784,7 +2017,7 @@ pub fn op_node_generate_ec_key( #[cppgc] pub async fn op_node_generate_ec_key_async( #[string] named_curve: String, -) -> Result { +) -> Result { spawn_blocking(move || ec_generate(&named_curve)) .await .unwrap() @@ -1840,7 +2073,7 @@ fn u32_slice_to_u8_slice(slice: &[u32]) -> &[u8] { fn dh_group_generate( group_name: &str, -) -> Result { +) -> Result { let (dh, prime, generator) = match group_name { "modp5" => ( dh::DiffieHellman::group::(), @@ -1895,7 +2128,7 @@ fn dh_group_generate( #[cppgc] pub fn op_node_generate_dh_group_key( #[string] group_name: &str, -) -> Result { +) -> Result { dh_group_generate(group_name) } @@ -1903,7 +2136,7 @@ pub fn op_node_generate_dh_group_key( #[cppgc] pub async fn op_node_generate_dh_group_key_async( #[string] group_name: String, -) -> Result { +) -> Result { spawn_blocking(move || dh_group_generate(&group_name)) .await .unwrap() @@ -1913,7 +2146,7 @@ fn dh_generate( prime: Option<&[u8]>, prime_len: usize, generator: usize, -) -> Result { +) -> KeyObjectHandlePair { let prime = prime .map(|p| p.into()) .unwrap_or_else(|| Prime::generate(prime_len)); @@ -1923,7 +2156,7 @@ fn dh_generate( base: asn1::Int::new(generator.to_be_bytes().as_slice()).unwrap(), private_value_length: None, }; - Ok(KeyObjectHandlePair::new( + KeyObjectHandlePair::new( AsymmetricPrivateKey::Dh(DhPrivateKey { key: dh.private_key, params: params.clone(), @@ -1932,7 +2165,7 @@ fn dh_generate( key: dh.public_key, params, }), - )) + ) } #[op2] @@ -1941,7 +2174,7 @@ pub fn op_node_generate_dh_key( #[buffer] prime: Option<&[u8]>, #[smi] prime_len: usize, #[smi] generator: usize, -) -> Result { +) -> KeyObjectHandlePair { dh_generate(prime, prime_len, generator) } @@ -1951,7 +2184,7 @@ pub async fn op_node_generate_dh_key_async( #[buffer(copy)] prime: Option>, #[smi] prime_len: usize, #[smi] generator: usize, -) -> Result { +) -> KeyObjectHandlePair { spawn_blocking(move || dh_generate(prime.as_deref(), prime_len, generator)) .await .unwrap() @@ -1963,21 +2196,21 @@ pub fn op_node_dh_keys_generate_and_export( #[buffer] prime: Option<&[u8]>, #[smi] prime_len: usize, #[smi] generator: usize, -) -> Result<(ToJsBuffer, ToJsBuffer), AnyError> { +) -> (ToJsBuffer, ToJsBuffer) { let prime = prime .map(|p| p.into()) .unwrap_or_else(|| Prime::generate(prime_len)); let dh = dh::DiffieHellman::new(prime, generator); let private_key = dh.private_key.into_vec().into_boxed_slice(); let public_key = dh.public_key.into_vec().into_boxed_slice(); - Ok((private_key.into(), public_key.into())) + (private_key.into(), public_key.into()) } #[op2] #[buffer] pub fn op_node_export_secret_key( #[cppgc] handle: &KeyObjectHandle, -) -> Result, AnyError> { +) -> Result, deno_core::error::AnyError> { let key = handle .as_secret_key() .ok_or_else(|| type_error("key is not a secret key"))?; @@ -1988,7 +2221,7 @@ pub fn op_node_export_secret_key( #[string] pub fn op_node_export_secret_key_b64url( #[cppgc] handle: &KeyObjectHandle, -) -> Result { +) -> Result { let key = handle .as_secret_key() .ok_or_else(|| type_error("key is not a secret key"))?; @@ -1999,23 +2232,33 @@ pub fn op_node_export_secret_key_b64url( #[serde] pub fn op_node_export_public_key_jwk( #[cppgc] handle: &KeyObjectHandle, -) -> Result { +) -> Result { let public_key = handle .as_public_key() - .ok_or_else(|| type_error("key is not an asymmetric public key"))?; + .ok_or(AsymmetricPublicKeyJwkError::KeyIsNotAsymmetricPublicKey)?; public_key.export_jwk() } +#[derive(Debug, thiserror::Error)] +pub enum ExportPublicKeyPemError { + #[error(transparent)] + AsymmetricPublicKeyDer(#[from] AsymmetricPublicKeyDerError), + #[error("very large data")] + VeryLargeData, + #[error(transparent)] + Der(#[from] der::Error), +} + #[op2] #[string] pub fn op_node_export_public_key_pem( #[cppgc] handle: &KeyObjectHandle, #[string] typ: &str, -) -> Result { +) -> Result { let public_key = handle .as_public_key() - .ok_or_else(|| type_error("key is not an asymmetric public key"))?; + .ok_or(AsymmetricPublicKeyDerError::KeyIsNotAsymmetricPublicKey)?; let data = public_key.export_der(typ)?; let label = match typ { @@ -2024,7 +2267,9 @@ pub fn op_node_export_public_key_pem( _ => unreachable!("export_der would have errored"), }; - let mut out = vec![0; 2048]; + let pem_len = der::pem::encapsulated_len(label, LineEnding::LF, data.len()) + .map_err(|_| ExportPublicKeyPemError::VeryLargeData)?; + let mut out = vec![0; pem_len]; let mut writer = PemWriter::new(label, LineEnding::LF, &mut out)?; writer.write(&data)?; let len = writer.finish()?; @@ -2038,22 +2283,32 @@ pub fn op_node_export_public_key_pem( pub fn op_node_export_public_key_der( #[cppgc] handle: &KeyObjectHandle, #[string] typ: &str, -) -> Result, AnyError> { +) -> Result, AsymmetricPublicKeyDerError> { let public_key = handle .as_public_key() - .ok_or_else(|| type_error("key is not an asymmetric public key"))?; + .ok_or(AsymmetricPublicKeyDerError::KeyIsNotAsymmetricPublicKey)?; public_key.export_der(typ) } +#[derive(Debug, thiserror::Error)] +pub enum ExportPrivateKeyPemError { + #[error(transparent)] + AsymmetricPublicKeyDer(#[from] AsymmetricPrivateKeyDerError), + #[error("very large data")] + VeryLargeData, + #[error(transparent)] + Der(#[from] der::Error), +} + #[op2] #[string] pub fn op_node_export_private_key_pem( #[cppgc] handle: &KeyObjectHandle, #[string] typ: &str, -) -> Result { +) -> Result { let private_key = handle .as_private_key() - .ok_or_else(|| type_error("key is not an asymmetric private key"))?; + .ok_or(AsymmetricPrivateKeyDerError::KeyIsNotAsymmetricPrivateKey)?; let data = private_key.export_der(typ)?; let label = match typ { @@ -2063,7 +2318,9 @@ pub fn op_node_export_private_key_pem( _ => unreachable!("export_der would have errored"), }; - let mut out = vec![0; 2048]; + let pem_len = der::pem::encapsulated_len(label, LineEnding::LF, data.len()) + .map_err(|_| ExportPrivateKeyPemError::VeryLargeData)?; + let mut out = vec![0; pem_len]; let mut writer = PemWriter::new(label, LineEnding::LF, &mut out)?; writer.write(&data)?; let len = writer.finish()?; @@ -2077,10 +2334,10 @@ pub fn op_node_export_private_key_pem( pub fn op_node_export_private_key_der( #[cppgc] handle: &KeyObjectHandle, #[string] typ: &str, -) -> Result, AnyError> { +) -> Result, AsymmetricPrivateKeyDerError> { let private_key = handle .as_private_key() - .ok_or_else(|| type_error("key is not an asymmetric private key"))?; + .ok_or(AsymmetricPrivateKeyDerError::KeyIsNotAsymmetricPrivateKey)?; private_key.export_der(typ) } @@ -2098,7 +2355,7 @@ pub fn op_node_key_type(#[cppgc] handle: &KeyObjectHandle) -> &'static str { #[cppgc] pub fn op_node_derive_public_key_from_private_key( #[cppgc] handle: &KeyObjectHandle, -) -> Result { +) -> Result { let Some(private_key) = handle.as_private_key() else { return Err(type_error("expected private key")); }; diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index 600d315587..e90e820909 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -1,7 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use deno_core::error::generic_error; use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::unsync::spawn_blocking; use deno_core::JsBuffer; @@ -34,14 +33,14 @@ use rsa::Pkcs1v15Encrypt; use rsa::RsaPrivateKey; use rsa::RsaPublicKey; -mod cipher; +pub mod cipher; mod dh; -mod digest; +pub mod digest; pub mod keys; mod md5_sha1; mod pkcs3; mod primes; -mod sign; +pub mod sign; pub mod x509; use self::digest::match_fixed_digest_with_eager_block_buffer; @@ -58,38 +57,31 @@ pub fn op_node_check_prime( pub fn op_node_check_prime_bytes( #[anybuffer] bytes: &[u8], #[number] checks: usize, -) -> Result { +) -> bool { let candidate = BigInt::from_bytes_be(num_bigint::Sign::Plus, bytes); - Ok(primes::is_probably_prime(&candidate, checks)) + primes::is_probably_prime(&candidate, checks) } #[op2(async)] pub async fn op_node_check_prime_async( #[bigint] num: i64, #[number] checks: usize, -) -> Result { +) -> Result { // TODO(@littledivy): use rayon for CPU-bound tasks - Ok( - spawn_blocking(move || { - primes::is_probably_prime(&BigInt::from(num), checks) - }) - .await?, - ) + spawn_blocking(move || primes::is_probably_prime(&BigInt::from(num), checks)) + .await } #[op2(async)] pub fn op_node_check_prime_bytes_async( #[anybuffer] bytes: &[u8], #[number] checks: usize, -) -> Result>, AnyError> { +) -> impl Future> { let candidate = BigInt::from_bytes_be(num_bigint::Sign::Plus, bytes); // TODO(@littledivy): use rayon for CPU-bound tasks - Ok(async move { - Ok( - spawn_blocking(move || primes::is_probably_prime(&candidate, checks)) - .await?, - ) - }) + async move { + spawn_blocking(move || primes::is_probably_prime(&candidate, checks)).await + } } #[op2] @@ -97,7 +89,7 @@ pub fn op_node_check_prime_bytes_async( pub fn op_node_create_hash( #[string] algorithm: &str, output_length: Option, -) -> Result { +) -> Result { digest::Hasher::new(algorithm, output_length.map(|l| l as usize)) } @@ -145,17 +137,31 @@ pub fn op_node_hash_digest_hex( pub fn op_node_hash_clone( #[cppgc] hasher: &digest::Hasher, output_length: Option, -) -> Result, AnyError> { +) -> Result, digest::HashError> { hasher.clone_inner(output_length.map(|l| l as usize)) } +#[derive(Debug, thiserror::Error)] +pub enum PrivateEncryptDecryptError { + #[error(transparent)] + Pkcs8(#[from] pkcs8::Error), + #[error(transparent)] + Spki(#[from] spki::Error), + #[error(transparent)] + Utf8(#[from] std::str::Utf8Error), + #[error(transparent)] + Rsa(#[from] rsa::Error), + #[error("Unknown padding")] + UnknownPadding, +} + #[op2] #[serde] pub fn op_node_private_encrypt( #[serde] key: StringOrBuffer, #[serde] msg: StringOrBuffer, #[smi] padding: u32, -) -> Result { +) -> Result { let key = RsaPrivateKey::from_pkcs8_pem((&key).try_into()?)?; let mut rng = rand::thread_rng(); @@ -172,7 +178,7 @@ pub fn op_node_private_encrypt( .encrypt(&mut rng, Oaep::new::(), &msg)? .into(), ), - _ => Err(type_error("Unknown padding")), + _ => Err(PrivateEncryptDecryptError::UnknownPadding), } } @@ -182,13 +188,13 @@ pub fn op_node_private_decrypt( #[serde] key: StringOrBuffer, #[serde] msg: StringOrBuffer, #[smi] padding: u32, -) -> Result { +) -> Result { let key = RsaPrivateKey::from_pkcs8_pem((&key).try_into()?)?; match padding { 1 => Ok(key.decrypt(Pkcs1v15Encrypt, &msg)?.into()), 4 => Ok(key.decrypt(Oaep::new::(), &msg)?.into()), - _ => Err(type_error("Unknown padding")), + _ => Err(PrivateEncryptDecryptError::UnknownPadding), } } @@ -198,7 +204,7 @@ pub fn op_node_public_encrypt( #[serde] key: StringOrBuffer, #[serde] msg: StringOrBuffer, #[smi] padding: u32, -) -> Result { +) -> Result { let key = RsaPublicKey::from_public_key_pem((&key).try_into()?)?; let mut rng = rand::thread_rng(); @@ -209,7 +215,7 @@ pub fn op_node_public_encrypt( .encrypt(&mut rng, Oaep::new::(), &msg)? .into(), ), - _ => Err(type_error("Unknown padding")), + _ => Err(PrivateEncryptDecryptError::UnknownPadding), } } @@ -220,7 +226,7 @@ pub fn op_node_create_cipheriv( #[string] algorithm: &str, #[buffer] key: &[u8], #[buffer] iv: &[u8], -) -> Result { +) -> Result { let context = cipher::CipherContext::new(algorithm, key, iv)?; Ok(state.resource_table.add(context)) } @@ -262,11 +268,14 @@ pub fn op_node_cipheriv_final( auto_pad: bool, #[buffer] input: &[u8], #[anybuffer] output: &mut [u8], -) -> Result>, AnyError> { - let context = state.resource_table.take::(rid)?; +) -> Result>, cipher::CipherContextError> { + let context = state + .resource_table + .take::(rid) + .map_err(cipher::CipherContextError::Resource)?; let context = Rc::try_unwrap(context) - .map_err(|_| type_error("Cipher context is already in use"))?; - context.r#final(auto_pad, input, output) + .map_err(|_| cipher::CipherContextError::ContextInUse)?; + context.r#final(auto_pad, input, output).map_err(Into::into) } #[op2] @@ -274,10 +283,13 @@ pub fn op_node_cipheriv_final( pub fn op_node_cipheriv_take( state: &mut OpState, #[smi] rid: u32, -) -> Result>, AnyError> { - let context = state.resource_table.take::(rid)?; +) -> Result>, cipher::CipherContextError> { + let context = state + .resource_table + .take::(rid) + .map_err(cipher::CipherContextError::Resource)?; let context = Rc::try_unwrap(context) - .map_err(|_| type_error("Cipher context is already in use"))?; + .map_err(|_| cipher::CipherContextError::ContextInUse)?; Ok(context.take_tag()) } @@ -288,7 +300,7 @@ pub fn op_node_create_decipheriv( #[string] algorithm: &str, #[buffer] key: &[u8], #[buffer] iv: &[u8], -) -> Result { +) -> Result { let context = cipher::DecipherContext::new(algorithm, key, iv)?; Ok(state.resource_table.add(context)) } @@ -326,10 +338,13 @@ pub fn op_node_decipheriv_decrypt( pub fn op_node_decipheriv_take( state: &mut OpState, #[smi] rid: u32, -) -> Result<(), AnyError> { - let context = state.resource_table.take::(rid)?; +) -> Result<(), cipher::DecipherContextError> { + let context = state + .resource_table + .take::(rid) + .map_err(cipher::DecipherContextError::Resource)?; Rc::try_unwrap(context) - .map_err(|_| type_error("Cipher context is already in use"))?; + .map_err(|_| cipher::DecipherContextError::ContextInUse)?; Ok(()) } @@ -341,11 +356,16 @@ pub fn op_node_decipheriv_final( #[buffer] input: &[u8], #[anybuffer] output: &mut [u8], #[buffer] auth_tag: &[u8], -) -> Result<(), AnyError> { - let context = state.resource_table.take::(rid)?; +) -> Result<(), cipher::DecipherContextError> { + let context = state + .resource_table + .take::(rid) + .map_err(cipher::DecipherContextError::Resource)?; let context = Rc::try_unwrap(context) - .map_err(|_| type_error("Cipher context is already in use"))?; - context.r#final(auto_pad, input, output, auth_tag) + .map_err(|_| cipher::DecipherContextError::ContextInUse)?; + context + .r#final(auto_pad, input, output, auth_tag) + .map_err(Into::into) } #[op2] @@ -356,7 +376,7 @@ pub fn op_node_sign( #[string] digest_type: &str, #[smi] pss_salt_length: Option, #[smi] dsa_signature_encoding: u32, -) -> Result, AnyError> { +) -> Result, sign::KeyObjectHandlePrehashedSignAndVerifyError> { handle.sign_prehashed( digest_type, digest, @@ -373,7 +393,7 @@ pub fn op_node_verify( #[buffer] signature: &[u8], #[smi] pss_salt_length: Option, #[smi] dsa_signature_encoding: u32, -) -> Result { +) -> Result { handle.verify_prehashed( digest_type, digest, @@ -383,13 +403,21 @@ pub fn op_node_verify( ) } +#[derive(Debug, thiserror::Error)] +pub enum Pbkdf2Error { + #[error("unsupported digest: {0}")] + UnsupportedDigest(String), + #[error(transparent)] + Join(#[from] tokio::task::JoinError), +} + fn pbkdf2_sync( password: &[u8], salt: &[u8], iterations: u32, algorithm_name: &str, derived_key: &mut [u8], -) -> Result<(), AnyError> { +) -> Result<(), Pbkdf2Error> { match_fixed_digest_with_eager_block_buffer!( algorithm_name, fn () { @@ -397,10 +425,7 @@ fn pbkdf2_sync( Ok(()) }, _ => { - Err(type_error(format!( - "unsupported digest: {}", - algorithm_name - ))) + Err(Pbkdf2Error::UnsupportedDigest(algorithm_name.to_string())) } ) } @@ -424,7 +449,7 @@ pub async fn op_node_pbkdf2_async( #[smi] iterations: u32, #[string] digest: String, #[number] keylen: usize, -) -> Result { +) -> Result { spawn_blocking(move || { let mut derived_key = vec![0; keylen]; pbkdf2_sync(&password, &salt, iterations, &digest, &mut derived_key) @@ -450,15 +475,27 @@ pub async fn op_node_fill_random_async(#[smi] len: i32) -> ToJsBuffer { .unwrap() } +#[derive(Debug, thiserror::Error)] +pub enum HkdfError { + #[error("expected secret key")] + ExpectedSecretKey, + #[error("HKDF-Expand failed")] + HkdfExpandFailed, + #[error("Unsupported digest: {0}")] + UnsupportedDigest(String), + #[error(transparent)] + Join(#[from] tokio::task::JoinError), +} + fn hkdf_sync( digest_algorithm: &str, handle: &KeyObjectHandle, salt: &[u8], info: &[u8], okm: &mut [u8], -) -> Result<(), AnyError> { +) -> Result<(), HkdfError> { let Some(ikm) = handle.as_secret_key() else { - return Err(type_error("expected secret key")); + return Err(HkdfError::ExpectedSecretKey); }; match_fixed_digest_with_eager_block_buffer!( @@ -466,10 +503,10 @@ fn hkdf_sync( fn () { let hk = Hkdf::::new(Some(salt), ikm); hk.expand(info, okm) - .map_err(|_| type_error("HKDF-Expand failed")) + .map_err(|_| HkdfError::HkdfExpandFailed) }, _ => { - Err(type_error(format!("Unsupported digest: {}", digest_algorithm))) + Err(HkdfError::UnsupportedDigest(digest_algorithm.to_string())) } ) } @@ -481,7 +518,7 @@ pub fn op_node_hkdf( #[buffer] salt: &[u8], #[buffer] info: &[u8], #[buffer] okm: &mut [u8], -) -> Result<(), AnyError> { +) -> Result<(), HkdfError> { hkdf_sync(digest_algorithm, handle, salt, info, okm) } @@ -493,7 +530,7 @@ pub async fn op_node_hkdf_async( #[buffer] salt: JsBuffer, #[buffer] info: JsBuffer, #[number] okm_len: usize, -) -> Result { +) -> Result { let handle = handle.clone(); spawn_blocking(move || { let mut okm = vec![0u8; okm_len]; @@ -509,27 +546,24 @@ pub fn op_node_dh_compute_secret( #[buffer] prime: JsBuffer, #[buffer] private_key: JsBuffer, #[buffer] their_public_key: JsBuffer, -) -> Result { +) -> ToJsBuffer { let pubkey: BigUint = BigUint::from_bytes_be(their_public_key.as_ref()); let privkey: BigUint = BigUint::from_bytes_be(private_key.as_ref()); let primei: BigUint = BigUint::from_bytes_be(prime.as_ref()); let shared_secret: BigUint = pubkey.modpow(&privkey, &primei); - Ok(shared_secret.to_bytes_be().into()) + shared_secret.to_bytes_be().into() } #[op2(fast)] -#[smi] -pub fn op_node_random_int( - #[smi] min: i32, - #[smi] max: i32, -) -> Result { +#[number] +pub fn op_node_random_int(#[number] min: i64, #[number] max: i64) -> i64 { let mut rng = rand::thread_rng(); // Uniform distribution is required to avoid Modulo Bias // https://en.wikipedia.org/wiki/Fisher–Yates_shuffle#Modulo_bias let dist = Uniform::from(min..max); - Ok(dist.sample(&mut rng)) + dist.sample(&mut rng) } #[allow(clippy::too_many_arguments)] @@ -542,7 +576,7 @@ fn scrypt( parallelization: u32, _maxmem: u32, output_buffer: &mut [u8], -) -> Result<(), AnyError> { +) -> Result<(), deno_core::error::AnyError> { // Construct Params let params = scrypt::Params::new( cost as u8, @@ -573,7 +607,7 @@ pub fn op_node_scrypt_sync( #[smi] parallelization: u32, #[smi] maxmem: u32, #[anybuffer] output_buffer: &mut [u8], -) -> Result<(), AnyError> { +) -> Result<(), deno_core::error::AnyError> { scrypt( password, salt, @@ -586,6 +620,14 @@ pub fn op_node_scrypt_sync( ) } +#[derive(Debug, thiserror::Error)] +pub enum ScryptAsyncError { + #[error(transparent)] + Join(#[from] tokio::task::JoinError), + #[error(transparent)] + Other(deno_core::error::AnyError), +} + #[op2(async)] #[serde] pub async fn op_node_scrypt_async( @@ -596,10 +638,11 @@ pub async fn op_node_scrypt_async( #[smi] block_size: u32, #[smi] parallelization: u32, #[smi] maxmem: u32, -) -> Result { +) -> Result { spawn_blocking(move || { let mut output_buffer = vec![0u8; keylen as usize]; - let res = scrypt( + + scrypt( password, salt, keylen, @@ -608,25 +651,30 @@ pub async fn op_node_scrypt_async( parallelization, maxmem, &mut output_buffer, - ); - - if res.is_ok() { - Ok(output_buffer.into()) - } else { - // TODO(lev): rethrow the error? - Err(generic_error("scrypt failure")) - } + ) + .map(|_| output_buffer.into()) + .map_err(ScryptAsyncError::Other) }) .await? } +#[derive(Debug, thiserror::Error)] +pub enum EcdhEncodePubKey { + #[error("Invalid public key")] + InvalidPublicKey, + #[error("Unsupported curve")] + UnsupportedCurve, + #[error(transparent)] + Sec1(#[from] sec1::Error), +} + #[op2] #[buffer] pub fn op_node_ecdh_encode_pubkey( #[string] curve: &str, #[buffer] pubkey: &[u8], compress: bool, -) -> Result, AnyError> { +) -> Result, EcdhEncodePubKey> { use elliptic_curve::sec1::FromEncodedPoint; match curve { @@ -639,7 +687,7 @@ pub fn op_node_ecdh_encode_pubkey( ); // CtOption does not expose its variants. if pubkey.is_none().into() { - return Err(type_error("Invalid public key")); + return Err(EcdhEncodePubKey::InvalidPublicKey); } let pubkey = pubkey.unwrap(); @@ -652,7 +700,7 @@ pub fn op_node_ecdh_encode_pubkey( ); // CtOption does not expose its variants. if pubkey.is_none().into() { - return Err(type_error("Invalid public key")); + return Err(EcdhEncodePubKey::InvalidPublicKey); } let pubkey = pubkey.unwrap(); @@ -665,7 +713,7 @@ pub fn op_node_ecdh_encode_pubkey( ); // CtOption does not expose its variants. if pubkey.is_none().into() { - return Err(type_error("Invalid public key")); + return Err(EcdhEncodePubKey::InvalidPublicKey); } let pubkey = pubkey.unwrap(); @@ -678,14 +726,14 @@ pub fn op_node_ecdh_encode_pubkey( ); // CtOption does not expose its variants. if pubkey.is_none().into() { - return Err(type_error("Invalid public key")); + return Err(EcdhEncodePubKey::InvalidPublicKey); } let pubkey = pubkey.unwrap(); Ok(pubkey.to_encoded_point(compress).as_ref().to_vec()) } - &_ => Err(type_error("Unsupported curve")), + &_ => Err(EcdhEncodePubKey::UnsupportedCurve), } } @@ -695,7 +743,7 @@ pub fn op_node_ecdh_generate_keys( #[buffer] pubbuf: &mut [u8], #[buffer] privbuf: &mut [u8], #[string] format: &str, -) -> Result<(), AnyError> { +) -> Result<(), deno_core::error::AnyError> { let mut rng = rand::thread_rng(); let compress = format == "compressed"; match curve { @@ -742,7 +790,7 @@ pub fn op_node_ecdh_compute_secret( #[buffer] this_priv: Option, #[buffer] their_pub: &mut [u8], #[buffer] secret: &mut [u8], -) -> Result<(), AnyError> { +) { match curve { "secp256k1" => { let their_public_key = @@ -760,8 +808,6 @@ pub fn op_node_ecdh_compute_secret( their_public_key.as_affine(), ); secret.copy_from_slice(shared_secret.raw_secret_bytes()); - - Ok(()) } "prime256v1" | "secp256r1" => { let their_public_key = @@ -776,8 +822,6 @@ pub fn op_node_ecdh_compute_secret( their_public_key.as_affine(), ); secret.copy_from_slice(shared_secret.raw_secret_bytes()); - - Ok(()) } "secp384r1" => { let their_public_key = @@ -792,8 +836,6 @@ pub fn op_node_ecdh_compute_secret( their_public_key.as_affine(), ); secret.copy_from_slice(shared_secret.raw_secret_bytes()); - - Ok(()) } "secp224r1" => { let their_public_key = @@ -808,8 +850,6 @@ pub fn op_node_ecdh_compute_secret( their_public_key.as_affine(), ); secret.copy_from_slice(shared_secret.raw_secret_bytes()); - - Ok(()) } &_ => todo!(), } @@ -820,7 +860,7 @@ pub fn op_node_ecdh_compute_public_key( #[string] curve: &str, #[buffer] privkey: &[u8], #[buffer] pubkey: &mut [u8], -) -> Result<(), AnyError> { +) { match curve { "secp256k1" => { let this_private_key = @@ -828,8 +868,6 @@ pub fn op_node_ecdh_compute_public_key( .expect("bad private key"); let public_key = this_private_key.public_key(); pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref()); - - Ok(()) } "prime256v1" | "secp256r1" => { let this_private_key = @@ -837,7 +875,6 @@ pub fn op_node_ecdh_compute_public_key( .expect("bad private key"); let public_key = this_private_key.public_key(); pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref()); - Ok(()) } "secp384r1" => { let this_private_key = @@ -845,7 +882,6 @@ pub fn op_node_ecdh_compute_public_key( .expect("bad private key"); let public_key = this_private_key.public_key(); pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref()); - Ok(()) } "secp224r1" => { let this_private_key = @@ -853,7 +889,6 @@ pub fn op_node_ecdh_compute_public_key( .expect("bad private key"); let public_key = this_private_key.public_key(); pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref()); - Ok(()) } &_ => todo!(), } @@ -874,8 +909,20 @@ pub fn op_node_gen_prime(#[number] size: usize) -> ToJsBuffer { #[serde] pub async fn op_node_gen_prime_async( #[number] size: usize, -) -> Result { - Ok(spawn_blocking(move || gen_prime(size)).await?) +) -> Result { + spawn_blocking(move || gen_prime(size)).await +} + +#[derive(Debug, thiserror::Error)] +pub enum DiffieHellmanError { + #[error("Expected private key")] + ExpectedPrivateKey, + #[error("Expected public key")] + ExpectedPublicKey, + #[error("DH parameters mismatch")] + DhParametersMismatch, + #[error("Unsupported key type for diffie hellman, or key type mismatch")] + UnsupportedKeyTypeForDiffieHellmanOrKeyTypeMismatch, } #[op2] @@ -883,117 +930,134 @@ pub async fn op_node_gen_prime_async( pub fn op_node_diffie_hellman( #[cppgc] private: &KeyObjectHandle, #[cppgc] public: &KeyObjectHandle, -) -> Result, AnyError> { +) -> Result, DiffieHellmanError> { let private = private .as_private_key() - .ok_or_else(|| type_error("Expected private key"))?; + .ok_or(DiffieHellmanError::ExpectedPrivateKey)?; let public = public .as_public_key() - .ok_or_else(|| type_error("Expected public key"))?; + .ok_or(DiffieHellmanError::ExpectedPublicKey)?; - let res = match (private, &*public) { - ( - AsymmetricPrivateKey::Ec(EcPrivateKey::P224(private)), - AsymmetricPublicKey::Ec(EcPublicKey::P224(public)), - ) => p224::ecdh::diffie_hellman( - private.to_nonzero_scalar(), - public.as_affine(), - ) - .raw_secret_bytes() - .to_vec() - .into_boxed_slice(), - ( - AsymmetricPrivateKey::Ec(EcPrivateKey::P256(private)), - AsymmetricPublicKey::Ec(EcPublicKey::P256(public)), - ) => p256::ecdh::diffie_hellman( - private.to_nonzero_scalar(), - public.as_affine(), - ) - .raw_secret_bytes() - .to_vec() - .into_boxed_slice(), - ( - AsymmetricPrivateKey::Ec(EcPrivateKey::P384(private)), - AsymmetricPublicKey::Ec(EcPublicKey::P384(public)), - ) => p384::ecdh::diffie_hellman( - private.to_nonzero_scalar(), - public.as_affine(), - ) - .raw_secret_bytes() - .to_vec() - .into_boxed_slice(), - ( - AsymmetricPrivateKey::X25519(private), - AsymmetricPublicKey::X25519(public), - ) => private - .diffie_hellman(public) - .to_bytes() - .into_iter() - .collect(), - (AsymmetricPrivateKey::Dh(private), AsymmetricPublicKey::Dh(public)) => { - if private.params.prime != public.params.prime - || private.params.base != public.params.base - { - return Err(type_error("DH parameters mismatch")); + let res = + match (private, &*public) { + ( + AsymmetricPrivateKey::Ec(EcPrivateKey::P224(private)), + AsymmetricPublicKey::Ec(EcPublicKey::P224(public)), + ) => p224::ecdh::diffie_hellman( + private.to_nonzero_scalar(), + public.as_affine(), + ) + .raw_secret_bytes() + .to_vec() + .into_boxed_slice(), + ( + AsymmetricPrivateKey::Ec(EcPrivateKey::P256(private)), + AsymmetricPublicKey::Ec(EcPublicKey::P256(public)), + ) => p256::ecdh::diffie_hellman( + private.to_nonzero_scalar(), + public.as_affine(), + ) + .raw_secret_bytes() + .to_vec() + .into_boxed_slice(), + ( + AsymmetricPrivateKey::Ec(EcPrivateKey::P384(private)), + AsymmetricPublicKey::Ec(EcPublicKey::P384(public)), + ) => p384::ecdh::diffie_hellman( + private.to_nonzero_scalar(), + public.as_affine(), + ) + .raw_secret_bytes() + .to_vec() + .into_boxed_slice(), + ( + AsymmetricPrivateKey::X25519(private), + AsymmetricPublicKey::X25519(public), + ) => private + .diffie_hellman(public) + .to_bytes() + .into_iter() + .collect(), + (AsymmetricPrivateKey::Dh(private), AsymmetricPublicKey::Dh(public)) => { + if private.params.prime != public.params.prime + || private.params.base != public.params.base + { + return Err(DiffieHellmanError::DhParametersMismatch); + } + + // OSIP - Octet-String-to-Integer primitive + let public_key = public.key.clone().into_vec(); + let pubkey = BigUint::from_bytes_be(&public_key); + + // Exponentiation (z = y^x mod p) + let prime = BigUint::from_bytes_be(private.params.prime.as_bytes()); + let private_key = private.key.clone().into_vec(); + let private_key = BigUint::from_bytes_be(&private_key); + let shared_secret = pubkey.modpow(&private_key, &prime); + + shared_secret.to_bytes_be().into() } - - // OSIP - Octet-String-to-Integer primitive - let public_key = public.key.clone().into_vec(); - let pubkey = BigUint::from_bytes_be(&public_key); - - // Exponentiation (z = y^x mod p) - let prime = BigUint::from_bytes_be(private.params.prime.as_bytes()); - let private_key = private.key.clone().into_vec(); - let private_key = BigUint::from_bytes_be(&private_key); - let shared_secret = pubkey.modpow(&private_key, &prime); - - shared_secret.to_bytes_be().into() - } - _ => { - return Err(type_error( - "Unsupported key type for diffie hellman, or key type mismatch", - )) - } - }; + _ => return Err( + DiffieHellmanError::UnsupportedKeyTypeForDiffieHellmanOrKeyTypeMismatch, + ), + }; Ok(res) } +#[derive(Debug, thiserror::Error)] +pub enum SignEd25519Error { + #[error("Expected private key")] + ExpectedPrivateKey, + #[error("Expected Ed25519 private key")] + ExpectedEd25519PrivateKey, + #[error("Invalid Ed25519 private key")] + InvalidEd25519PrivateKey, +} + #[op2(fast)] pub fn op_node_sign_ed25519( #[cppgc] key: &KeyObjectHandle, #[buffer] data: &[u8], #[buffer] signature: &mut [u8], -) -> Result<(), AnyError> { +) -> Result<(), SignEd25519Error> { let private = key .as_private_key() - .ok_or_else(|| type_error("Expected private key"))?; + .ok_or(SignEd25519Error::ExpectedPrivateKey)?; let ed25519 = match private { AsymmetricPrivateKey::Ed25519(private) => private, - _ => return Err(type_error("Expected Ed25519 private key")), + _ => return Err(SignEd25519Error::ExpectedEd25519PrivateKey), }; let pair = Ed25519KeyPair::from_seed_unchecked(ed25519.as_bytes().as_slice()) - .map_err(|_| type_error("Invalid Ed25519 private key"))?; + .map_err(|_| SignEd25519Error::InvalidEd25519PrivateKey)?; signature.copy_from_slice(pair.sign(data).as_ref()); Ok(()) } +#[derive(Debug, thiserror::Error)] +pub enum VerifyEd25519Error { + #[error("Expected public key")] + ExpectedPublicKey, + #[error("Expected Ed25519 public key")] + ExpectedEd25519PublicKey, +} + #[op2(fast)] pub fn op_node_verify_ed25519( #[cppgc] key: &KeyObjectHandle, #[buffer] data: &[u8], #[buffer] signature: &[u8], -) -> Result { +) -> Result { let public = key .as_public_key() - .ok_or_else(|| type_error("Expected public key"))?; + .ok_or(VerifyEd25519Error::ExpectedPublicKey)?; let ed25519 = match &*public { AsymmetricPublicKey::Ed25519(public) => public, - _ => return Err(type_error("Expected Ed25519 public key")), + _ => return Err(VerifyEd25519Error::ExpectedEd25519PublicKey), }; let verified = ring::signature::UnparsedPublicKey::new( diff --git a/ext/node/ops/crypto/sign.rs b/ext/node/ops/crypto/sign.rs index b7779a5d80..30094c0765 100644 --- a/ext/node/ops/crypto/sign.rs +++ b/ext/node/ops/crypto/sign.rs @@ -1,7 +1,4 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::generic_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; use rand::rngs::OsRng; use rsa::signature::hazmat::PrehashSigner as _; use rsa::signature::hazmat::PrehashVerifier as _; @@ -26,7 +23,7 @@ use elliptic_curve::FieldBytesSize; fn dsa_signature( encoding: u32, signature: ecdsa::Signature, -) -> Result, AnyError> +) -> Result, KeyObjectHandlePrehashedSignAndVerifyError> where MaxSize: ArrayLength, as Add>::Output: Add + ArrayLength, @@ -36,10 +33,54 @@ where 0 => Ok(signature.to_der().to_bytes().to_vec().into_boxed_slice()), // IEEE P1363 1 => Ok(signature.to_bytes().to_vec().into_boxed_slice()), - _ => Err(type_error("invalid DSA signature encoding")), + _ => Err( + KeyObjectHandlePrehashedSignAndVerifyError::InvalidDsaSignatureEncoding, + ), } } +#[derive(Debug, thiserror::Error)] +pub enum KeyObjectHandlePrehashedSignAndVerifyError { + #[error("invalid DSA signature encoding")] + InvalidDsaSignatureEncoding, + #[error("key is not a private key")] + KeyIsNotPrivate, + #[error("digest not allowed for RSA signature: {0}")] + DigestNotAllowedForRsaSignature(String), + #[error("failed to sign digest with RSA")] + FailedToSignDigestWithRsa, + #[error("digest not allowed for RSA-PSS signature: {0}")] + DigestNotAllowedForRsaPssSignature(String), + #[error("failed to sign digest with RSA-PSS")] + FailedToSignDigestWithRsaPss, + #[error("failed to sign digest with DSA")] + FailedToSignDigestWithDsa, + #[error("rsa-pss with different mf1 hash algorithm and hash algorithm is not supported")] + RsaPssHashAlgorithmUnsupported, + #[error( + "private key does not allow {actual} to be used, expected {expected}" + )] + PrivateKeyDisallowsUsage { actual: String, expected: String }, + #[error("failed to sign digest")] + FailedToSignDigest, + #[error("x25519 key cannot be used for signing")] + X25519KeyCannotBeUsedForSigning, + #[error("Ed25519 key cannot be used for prehashed signing")] + Ed25519KeyCannotBeUsedForPrehashedSigning, + #[error("DH key cannot be used for signing")] + DhKeyCannotBeUsedForSigning, + #[error("key is not a public or private key")] + KeyIsNotPublicOrPrivate, + #[error("Invalid DSA signature")] + InvalidDsaSignature, + #[error("x25519 key cannot be used for verification")] + X25519KeyCannotBeUsedForVerification, + #[error("Ed25519 key cannot be used for prehashed verification")] + Ed25519KeyCannotBeUsedForPrehashedVerification, + #[error("DH key cannot be used for verification")] + DhKeyCannotBeUsedForVerification, +} + impl KeyObjectHandle { pub fn sign_prehashed( &self, @@ -47,10 +88,10 @@ impl KeyObjectHandle { digest: &[u8], pss_salt_length: Option, dsa_signature_encoding: u32, - ) -> Result, AnyError> { + ) -> Result, KeyObjectHandlePrehashedSignAndVerifyError> { let private_key = self .as_private_key() - .ok_or_else(|| type_error("key is not a private key"))?; + .ok_or(KeyObjectHandlePrehashedSignAndVerifyError::KeyIsNotPrivate)?; match private_key { AsymmetricPrivateKey::Rsa(key) => { @@ -63,34 +104,26 @@ impl KeyObjectHandle { rsa::pkcs1v15::Pkcs1v15Sign::new::() }, _ => { - return Err(type_error(format!( - "digest not allowed for RSA signature: {}", - digest_type - ))) + return Err(KeyObjectHandlePrehashedSignAndVerifyError::DigestNotAllowedForRsaSignature(digest_type.to_string())) } ) }; let signature = signer .sign(Some(&mut OsRng), key, digest) - .map_err(|_| generic_error("failed to sign digest with RSA"))?; + .map_err(|_| KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigestWithRsa)?; Ok(signature.into()) } AsymmetricPrivateKey::RsaPss(key) => { let mut hash_algorithm = None; let mut salt_length = None; - match &key.details { - Some(details) => { - if details.hash_algorithm != details.mf1_hash_algorithm { - return Err(type_error( - "rsa-pss with different mf1 hash algorithm and hash algorithm is not supported", - )); - } - hash_algorithm = Some(details.hash_algorithm); - salt_length = Some(details.salt_length as usize); + if let Some(details) = &key.details { + if details.hash_algorithm != details.mf1_hash_algorithm { + return Err(KeyObjectHandlePrehashedSignAndVerifyError::RsaPssHashAlgorithmUnsupported); } - None => {} - }; + hash_algorithm = Some(details.hash_algorithm); + salt_length = Some(details.salt_length as usize); + } if let Some(s) = pss_salt_length { salt_length = Some(s as usize); } @@ -99,10 +132,10 @@ impl KeyObjectHandle { fn (algorithm: Option) { if let Some(hash_algorithm) = hash_algorithm.take() { if Some(hash_algorithm) != algorithm { - return Err(type_error(format!( - "private key does not allow {} to be used, expected {}", - digest_type, hash_algorithm.as_str() - ))); + return Err(KeyObjectHandlePrehashedSignAndVerifyError::PrivateKeyDisallowsUsage { + actual: digest_type.to_string(), + expected: hash_algorithm.as_str().to_string(), + }); } } if let Some(salt_length) = salt_length { @@ -112,15 +145,12 @@ impl KeyObjectHandle { } }, _ => { - return Err(type_error(format!( - "digest not allowed for RSA-PSS signature: {}", - digest_type - ))) + return Err(KeyObjectHandlePrehashedSignAndVerifyError::DigestNotAllowedForRsaPssSignature(digest_type.to_string())); } ); let signature = pss .sign(Some(&mut OsRng), &key.key, digest) - .map_err(|_| generic_error("failed to sign digest with RSA-PSS"))?; + .map_err(|_| KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigestWithRsaPss)?; Ok(signature.into()) } AsymmetricPrivateKey::Dsa(key) => { @@ -130,15 +160,12 @@ impl KeyObjectHandle { key.sign_prehashed_rfc6979::(digest) }, _ => { - return Err(type_error(format!( - "digest not allowed for RSA signature: {}", - digest_type - ))) + return Err(KeyObjectHandlePrehashedSignAndVerifyError::DigestNotAllowedForRsaSignature(digest_type.to_string())) } ); let signature = - res.map_err(|_| generic_error("failed to sign digest with DSA"))?; + res.map_err(|_| KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigestWithDsa)?; Ok(signature.into()) } AsymmetricPrivateKey::Ec(key) => match key { @@ -146,7 +173,7 @@ impl KeyObjectHandle { let signing_key = p224::ecdsa::SigningKey::from(key); let signature: p224::ecdsa::Signature = signing_key .sign_prehash(digest) - .map_err(|_| type_error("failed to sign digest"))?; + .map_err(|_| KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigest)?; dsa_signature(dsa_signature_encoding, signature) } @@ -154,7 +181,7 @@ impl KeyObjectHandle { let signing_key = p256::ecdsa::SigningKey::from(key); let signature: p256::ecdsa::Signature = signing_key .sign_prehash(digest) - .map_err(|_| type_error("failed to sign digest"))?; + .map_err(|_| KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigest)?; dsa_signature(dsa_signature_encoding, signature) } @@ -162,19 +189,17 @@ impl KeyObjectHandle { let signing_key = p384::ecdsa::SigningKey::from(key); let signature: p384::ecdsa::Signature = signing_key .sign_prehash(digest) - .map_err(|_| type_error("failed to sign digest"))?; + .map_err(|_| KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigest)?; dsa_signature(dsa_signature_encoding, signature) } }, AsymmetricPrivateKey::X25519(_) => { - Err(type_error("x25519 key cannot be used for signing")) + Err(KeyObjectHandlePrehashedSignAndVerifyError::X25519KeyCannotBeUsedForSigning) } - AsymmetricPrivateKey::Ed25519(_) => Err(type_error( - "Ed25519 key cannot be used for prehashed signing", - )), + AsymmetricPrivateKey::Ed25519(_) => Err(KeyObjectHandlePrehashedSignAndVerifyError::Ed25519KeyCannotBeUsedForPrehashedSigning), AsymmetricPrivateKey::Dh(_) => { - Err(type_error("DH key cannot be used for signing")) + Err(KeyObjectHandlePrehashedSignAndVerifyError::DhKeyCannotBeUsedForSigning) } } } @@ -186,10 +211,10 @@ impl KeyObjectHandle { signature: &[u8], pss_salt_length: Option, dsa_signature_encoding: u32, - ) -> Result { - let public_key = self - .as_public_key() - .ok_or_else(|| type_error("key is not a public or private key"))?; + ) -> Result { + let public_key = self.as_public_key().ok_or( + KeyObjectHandlePrehashedSignAndVerifyError::KeyIsNotPublicOrPrivate, + )?; match &*public_key { AsymmetricPublicKey::Rsa(key) => { @@ -202,10 +227,7 @@ impl KeyObjectHandle { rsa::pkcs1v15::Pkcs1v15Sign::new::() }, _ => { - return Err(type_error(format!( - "digest not allowed for RSA signature: {}", - digest_type - ))) + return Err(KeyObjectHandlePrehashedSignAndVerifyError::DigestNotAllowedForRsaSignature(digest_type.to_string())) } ) }; @@ -215,18 +237,13 @@ impl KeyObjectHandle { AsymmetricPublicKey::RsaPss(key) => { let mut hash_algorithm = None; let mut salt_length = None; - match &key.details { - Some(details) => { - if details.hash_algorithm != details.mf1_hash_algorithm { - return Err(type_error( - "rsa-pss with different mf1 hash algorithm and hash algorithm is not supported", - )); - } - hash_algorithm = Some(details.hash_algorithm); - salt_length = Some(details.salt_length as usize); + if let Some(details) = &key.details { + if details.hash_algorithm != details.mf1_hash_algorithm { + return Err(KeyObjectHandlePrehashedSignAndVerifyError::RsaPssHashAlgorithmUnsupported); } - None => {} - }; + hash_algorithm = Some(details.hash_algorithm); + salt_length = Some(details.salt_length as usize); + } if let Some(s) = pss_salt_length { salt_length = Some(s as usize); } @@ -235,10 +252,10 @@ impl KeyObjectHandle { fn (algorithm: Option) { if let Some(hash_algorithm) = hash_algorithm.take() { if Some(hash_algorithm) != algorithm { - return Err(type_error(format!( - "private key does not allow {} to be used, expected {}", - digest_type, hash_algorithm.as_str() - ))); + return Err(KeyObjectHandlePrehashedSignAndVerifyError::PrivateKeyDisallowsUsage { + actual: digest_type.to_string(), + expected: hash_algorithm.as_str().to_string(), + }); } } if let Some(salt_length) = salt_length { @@ -248,17 +265,14 @@ impl KeyObjectHandle { } }, _ => { - return Err(type_error(format!( - "digest not allowed for RSA-PSS signature: {}", - digest_type - ))) + return Err(KeyObjectHandlePrehashedSignAndVerifyError::DigestNotAllowedForRsaPssSignature(digest_type.to_string())); } ); Ok(pss.verify(&key.key, digest, signature).is_ok()) } AsymmetricPublicKey::Dsa(key) => { let signature = dsa::Signature::from_der(signature) - .map_err(|_| type_error("Invalid DSA signature"))?; + .map_err(|_| KeyObjectHandlePrehashedSignAndVerifyError::InvalidDsaSignature)?; Ok(key.verify_prehash(digest, &signature).is_ok()) } AsymmetricPublicKey::Ec(key) => match key { @@ -300,13 +314,11 @@ impl KeyObjectHandle { } }, AsymmetricPublicKey::X25519(_) => { - Err(type_error("x25519 key cannot be used for verification")) + Err(KeyObjectHandlePrehashedSignAndVerifyError::X25519KeyCannotBeUsedForVerification) } - AsymmetricPublicKey::Ed25519(_) => Err(type_error( - "Ed25519 key cannot be used for prehashed verification", - )), + AsymmetricPublicKey::Ed25519(_) => Err(KeyObjectHandlePrehashedSignAndVerifyError::Ed25519KeyCannotBeUsedForPrehashedVerification), AsymmetricPublicKey::Dh(_) => { - Err(type_error("DH key cannot be used for verification")) + Err(KeyObjectHandlePrehashedSignAndVerifyError::DhKeyCannotBeUsedForVerification) } } } diff --git a/ext/node/ops/crypto/x509.rs b/ext/node/ops/crypto/x509.rs index b44ff3a4b3..ab8e52f703 100644 --- a/ext/node/ops/crypto/x509.rs +++ b/ext/node/ops/crypto/x509.rs @@ -1,11 +1,11 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use x509_parser::der_parser::asn1_rs::Any; use x509_parser::der_parser::asn1_rs::Tag; use x509_parser::der_parser::oid::Oid; +pub use x509_parser::error::X509Error; use x509_parser::extensions; use x509_parser::pem; use x509_parser::prelude::*; @@ -65,7 +65,7 @@ impl<'a> Deref for CertificateView<'a> { #[cppgc] pub fn op_node_x509_parse( #[buffer] buf: &[u8], -) -> Result { +) -> Result { let source = match pem::parse_x509_pem(buf) { Ok((_, pem)) => CertificateSources::Pem(pem), Err(_) => CertificateSources::Der(buf.to_vec().into_boxed_slice()), @@ -81,7 +81,7 @@ pub fn op_node_x509_parse( X509Certificate::from_der(buf).map(|(_, cert)| cert)? } }; - Ok::<_, AnyError>(CertificateView { cert }) + Ok::<_, X509Error>(CertificateView { cert }) }, )?; @@ -89,23 +89,23 @@ pub fn op_node_x509_parse( } #[op2(fast)] -pub fn op_node_x509_ca(#[cppgc] cert: &Certificate) -> Result { +pub fn op_node_x509_ca(#[cppgc] cert: &Certificate) -> bool { let cert = cert.inner.get().deref(); - Ok(cert.is_ca()) + cert.is_ca() } #[op2(fast)] pub fn op_node_x509_check_email( #[cppgc] cert: &Certificate, #[string] email: &str, -) -> Result { +) -> bool { let cert = cert.inner.get().deref(); let subject = cert.subject(); if subject .iter_email() .any(|e| e.as_str().unwrap_or("") == email) { - return Ok(true); + return true; } let subject_alt = cert @@ -121,62 +121,60 @@ pub fn op_node_x509_check_email( for name in &subject_alt.general_names { if let extensions::GeneralName::RFC822Name(n) = name { if *n == email { - return Ok(true); + return true; } } } } - Ok(false) + false } #[op2] #[string] -pub fn op_node_x509_fingerprint( - #[cppgc] cert: &Certificate, -) -> Result, AnyError> { - Ok(cert.fingerprint::()) +pub fn op_node_x509_fingerprint(#[cppgc] cert: &Certificate) -> Option { + cert.fingerprint::() } #[op2] #[string] pub fn op_node_x509_fingerprint256( #[cppgc] cert: &Certificate, -) -> Result, AnyError> { - Ok(cert.fingerprint::()) +) -> Option { + cert.fingerprint::() } #[op2] #[string] pub fn op_node_x509_fingerprint512( #[cppgc] cert: &Certificate, -) -> Result, AnyError> { - Ok(cert.fingerprint::()) +) -> Option { + cert.fingerprint::() } #[op2] #[string] pub fn op_node_x509_get_issuer( #[cppgc] cert: &Certificate, -) -> Result { +) -> Result { let cert = cert.inner.get().deref(); - Ok(x509name_to_string(cert.issuer(), oid_registry())?) + x509name_to_string(cert.issuer(), oid_registry()) } #[op2] #[string] pub fn op_node_x509_get_subject( #[cppgc] cert: &Certificate, -) -> Result { +) -> Result { let cert = cert.inner.get().deref(); - Ok(x509name_to_string(cert.subject(), oid_registry())?) + x509name_to_string(cert.subject(), oid_registry()) } #[op2] #[cppgc] pub fn op_node_x509_public_key( #[cppgc] cert: &Certificate, -) -> Result { +) -> Result { let cert = cert.inner.get().deref(); let public_key = &cert.tbs_certificate.subject_pki; @@ -245,37 +243,29 @@ fn x509name_to_string( #[op2] #[string] -pub fn op_node_x509_get_valid_from( - #[cppgc] cert: &Certificate, -) -> Result { +pub fn op_node_x509_get_valid_from(#[cppgc] cert: &Certificate) -> String { let cert = cert.inner.get().deref(); - Ok(cert.validity().not_before.to_string()) + cert.validity().not_before.to_string() } #[op2] #[string] -pub fn op_node_x509_get_valid_to( - #[cppgc] cert: &Certificate, -) -> Result { +pub fn op_node_x509_get_valid_to(#[cppgc] cert: &Certificate) -> String { let cert = cert.inner.get().deref(); - Ok(cert.validity().not_after.to_string()) + cert.validity().not_after.to_string() } #[op2] #[string] -pub fn op_node_x509_get_serial_number( - #[cppgc] cert: &Certificate, -) -> Result { +pub fn op_node_x509_get_serial_number(#[cppgc] cert: &Certificate) -> String { let cert = cert.inner.get().deref(); let mut s = cert.serial.to_str_radix(16); s.make_ascii_uppercase(); - Ok(s) + s } #[op2(fast)] -pub fn op_node_x509_key_usage( - #[cppgc] cert: &Certificate, -) -> Result { +pub fn op_node_x509_key_usage(#[cppgc] cert: &Certificate) -> u16 { let cert = cert.inner.get().deref(); let key_usage = cert .extensions() @@ -286,5 +276,5 @@ pub fn op_node_x509_key_usage( _ => None, }); - Ok(key_usage.map(|k| k.flags).unwrap_or(0)) + key_usage.map(|k| k.flags).unwrap_or(0) } diff --git a/ext/node/ops/fs.rs b/ext/node/ops/fs.rs index 6253f32d05..9c0e4e1ccf 100644 --- a/ext/node/ops/fs.rs +++ b/ext/node/ops/fs.rs @@ -3,7 +3,6 @@ use std::cell::RefCell; use std::rc::Rc; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_fs::FileSystemRc; @@ -11,11 +10,27 @@ use serde::Serialize; use crate::NodePermissions; +#[derive(Debug, thiserror::Error)] +pub enum FsError { + #[error(transparent)] + Permission(#[from] deno_permissions::PermissionCheckError), + #[error("{0}")] + Io(#[from] std::io::Error), + #[cfg(windows)] + #[error("Path has no root.")] + PathHasNoRoot, + #[cfg(not(any(unix, windows)))] + #[error("Unsupported platform.")] + UnsupportedPlatform, + #[error(transparent)] + Fs(#[from] deno_io::fs::FsError), +} + #[op2(fast)] pub fn op_node_fs_exists_sync

( state: &mut OpState, #[string] path: String, -) -> Result +) -> Result where P: NodePermissions + 'static, { @@ -30,7 +45,7 @@ where pub async fn op_node_fs_exists

( state: Rc>, #[string] path: String, -) -> Result +) -> Result where P: NodePermissions + 'static, { @@ -50,7 +65,7 @@ pub fn op_node_cp_sync

( state: &mut OpState, #[string] path: &str, #[string] new_path: &str, -) -> Result<(), AnyError> +) -> Result<(), FsError> where P: NodePermissions + 'static, { @@ -71,7 +86,7 @@ pub async fn op_node_cp

( state: Rc>, #[string] path: String, #[string] new_path: String, -) -> Result<(), AnyError> +) -> Result<(), FsError> where P: NodePermissions + 'static, { @@ -108,7 +123,7 @@ pub fn op_node_statfs

( state: Rc>, #[string] path: String, bigint: bool, -) -> Result +) -> Result where P: NodePermissions + 'static, { @@ -130,13 +145,21 @@ where let mut cpath = path.as_bytes().to_vec(); cpath.push(0); if bigint { - #[cfg(not(target_os = "macos"))] + #[cfg(not(any( + target_os = "macos", + target_os = "freebsd", + target_os = "openbsd" + )))] // 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")] + #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "openbsd" + ))] // 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(); @@ -146,7 +169,10 @@ where return Err(std::io::Error::last_os_error().into()); } Ok(StatFs { + #[cfg(not(target_os = "openbsd"))] typ: result.f_type as _, + #[cfg(target_os = "openbsd")] + typ: 0 as _, bsize: result.f_bsize as _, blocks: result.f_blocks as _, bfree: result.f_bfree as _, @@ -164,7 +190,10 @@ where return Err(std::io::Error::last_os_error().into()); } Ok(StatFs { + #[cfg(not(target_os = "openbsd"))] typ: result.f_type as _, + #[cfg(target_os = "openbsd")] + typ: 0 as _, bsize: result.f_bsize as _, blocks: result.f_blocks as _, bfree: result.f_bfree as _, @@ -176,7 +205,6 @@ where } #[cfg(windows)] { - use deno_core::anyhow::anyhow; use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceW; @@ -186,10 +214,7 @@ where // call below. #[allow(clippy::disallowed_methods)] let path = path.canonicalize()?; - let root = path - .ancestors() - .last() - .ok_or(anyhow!("Path has no root."))?; + let root = path.ancestors().last().ok_or(FsError::PathHasNoRoot)?; let mut root = OsStr::new(root).encode_wide().collect::>(); root.push(0); let mut sectors_per_cluster = 0; @@ -229,7 +254,7 @@ where { let _ = path; let _ = bigint; - Err(anyhow!("Unsupported platform.")) + Err(FsError::UnsupportedPlatform) } } @@ -241,7 +266,7 @@ pub fn op_node_lutimes_sync

( #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, -) -> Result<(), AnyError> +) -> Result<(), FsError> where P: NodePermissions + 'static, { @@ -262,7 +287,7 @@ pub async fn op_node_lutimes

( #[smi] atime_nanos: u32, #[number] mtime_secs: i64, #[smi] mtime_nanos: u32, -) -> Result<(), AnyError> +) -> Result<(), FsError> where P: NodePermissions + 'static, { @@ -286,7 +311,7 @@ pub fn op_node_lchown_sync

( #[string] path: String, uid: Option, gid: Option, -) -> Result<(), AnyError> +) -> Result<(), FsError> where P: NodePermissions + 'static, { @@ -304,7 +329,7 @@ pub async fn op_node_lchown

( #[string] path: String, uid: Option, gid: Option, -) -> Result<(), AnyError> +) -> Result<(), FsError> where P: NodePermissions + 'static, { diff --git a/ext/node/ops/http.rs b/ext/node/ops/http.rs index 773902dedf..69571078fe 100644 --- a/ext/node/ops/http.rs +++ b/ext/node/ops/http.rs @@ -8,14 +8,12 @@ use std::task::Context; use std::task::Poll; use bytes::Bytes; -use deno_core::anyhow; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::futures::stream::Peekable; use deno_core::futures::Future; use deno_core::futures::FutureExt; use deno_core::futures::Stream; use deno_core::futures::StreamExt; +use deno_core::futures::TryFutureExt; use deno_core::op2; use deno_core::serde::Serialize; use deno_core::unsync::spawn; @@ -33,6 +31,7 @@ use deno_core::Resource; use deno_core::ResourceId; use deno_fetch::get_or_create_client_from_state; use deno_fetch::FetchCancelHandle; +use deno_fetch::FetchError; use deno_fetch::FetchRequestResource; use deno_fetch::FetchReturn; use deno_fetch::HttpClientResource; @@ -59,12 +58,15 @@ pub fn op_node_http_request

( #[serde] headers: Vec<(ByteString, ByteString)>, #[smi] client_rid: Option, #[smi] body: Option, -) -> Result +) -> Result where P: crate::NodePermissions + 'static, { let client = if let Some(rid) = client_rid { - let r = state.resource_table.get::(rid)?; + let r = state + .resource_table + .get::(rid) + .map_err(FetchError::Resource)?; r.client.clone() } else { get_or_create_client_from_state(state)? @@ -81,10 +83,8 @@ where let mut header_map = HeaderMap::new(); for (key, value) in headers { - let name = HeaderName::from_bytes(&key) - .map_err(|err| type_error(err.to_string()))?; - let v = HeaderValue::from_bytes(&value) - .map_err(|err| type_error(err.to_string()))?; + let name = HeaderName::from_bytes(&key)?; + let v = HeaderValue::from_bytes(&value)?; header_map.append(name, v); } @@ -92,7 +92,10 @@ where let (body, con_len) = if let Some(body) = body { ( BodyExt::boxed(NodeHttpResourceToBodyAdapter::new( - state.resource_table.take_any(body)?, + state + .resource_table + .take_any(body) + .map_err(FetchError::Resource)?, )), None, ) @@ -117,7 +120,7 @@ where *request.uri_mut() = url .as_str() .parse() - .map_err(|_| type_error("Invalid URL"))?; + .map_err(|_| FetchError::InvalidUrl(url.clone()))?; *request.headers_mut() = header_map; if let Some((username, password)) = maybe_authority { @@ -136,9 +139,9 @@ where let fut = async move { client .send(request) + .map_err(Into::into) .or_cancel(cancel_handle_) .await - .map(|res| res.map_err(|err| type_error(err.to_string()))) }; let request_rid = state.resource_table.add(FetchRequestResource { @@ -174,11 +177,12 @@ pub struct NodeHttpFetchResponse { pub async fn op_node_http_fetch_send( state: Rc>, #[smi] rid: ResourceId, -) -> Result { +) -> Result { let request = state .borrow_mut() .resource_table - .take::(rid)?; + .take::(rid) + .map_err(FetchError::Resource)?; let request = Rc::try_unwrap(request) .ok() @@ -191,22 +195,23 @@ pub async fn op_node_http_fetch_send( // If any error in the chain is a hyper body error, return that as a special result we can use to // reconstruct an error chain (eg: `new TypeError(..., { cause: new Error(...) })`). // TODO(mmastrac): it would be a lot easier if we just passed a v8::Global through here instead - let mut err_ref: &dyn std::error::Error = err.as_ref(); - while let Some(err) = std::error::Error::source(err_ref) { - if let Some(err) = err.downcast_ref::() { - if let Some(err) = std::error::Error::source(err) { - return Ok(NodeHttpFetchResponse { - error: Some(err.to_string()), - ..Default::default() - }); + + if let FetchError::ClientSend(err_src) = &err { + if let Some(client_err) = std::error::Error::source(&err_src.source) { + if let Some(err_src) = client_err.downcast_ref::() { + if let Some(err_src) = std::error::Error::source(err_src) { + return Ok(NodeHttpFetchResponse { + error: Some(err_src.to_string()), + ..Default::default() + }); + } } } - err_ref = err; } - return Err(type_error(err.to_string())); + return Err(err); } - Err(_) => return Err(type_error("request was cancelled")), + Err(_) => return Err(FetchError::RequestCanceled), }; let status = res.status(); @@ -250,11 +255,12 @@ pub async fn op_node_http_fetch_send( pub async fn op_node_http_fetch_response_upgrade( state: Rc>, #[smi] rid: ResourceId, -) -> Result { +) -> Result { let raw_response = state .borrow_mut() .resource_table - .take::(rid)?; + .take::(rid) + .map_err(FetchError::Resource)?; let raw_response = Rc::try_unwrap(raw_response) .expect("Someone is holding onto NodeHttpFetchResponseResource"); @@ -277,7 +283,7 @@ pub async fn op_node_http_fetch_response_upgrade( } read_tx.write_all(&buf[..read]).await?; } - Ok::<_, AnyError>(()) + Ok::<_, FetchError>(()) }); spawn(async move { let mut buf = [0; 1024]; @@ -288,7 +294,7 @@ pub async fn op_node_http_fetch_response_upgrade( } upgraded_tx.write_all(&buf[..read]).await?; } - Ok::<_, AnyError>(()) + Ok::<_, FetchError>(()) }); } @@ -318,23 +324,26 @@ impl UpgradeStream { } } - async fn read(self: Rc, buf: &mut [u8]) -> Result { + async fn read( + self: Rc, + buf: &mut [u8], + ) -> Result { let cancel_handle = RcRef::map(self.clone(), |this| &this.cancel_handle); async { let read = RcRef::map(self, |this| &this.read); let mut read = read.borrow_mut().await; - Ok(Pin::new(&mut *read).read(buf).await?) + Pin::new(&mut *read).read(buf).await } .try_or_cancel(cancel_handle) .await } - async fn write(self: Rc, buf: &[u8]) -> Result { + async fn write(self: Rc, buf: &[u8]) -> Result { let cancel_handle = RcRef::map(self.clone(), |this| &this.cancel_handle); async { let write = RcRef::map(self, |this| &this.write); let mut write = write.borrow_mut().await; - Ok(Pin::new(&mut *write).write(buf).await?) + Pin::new(&mut *write).write(buf).await } .try_or_cancel(cancel_handle) .await @@ -387,7 +396,7 @@ impl NodeHttpFetchResponseResource { } } - pub async fn upgrade(self) -> Result { + pub async fn upgrade(self) -> Result { let reader = self.response_reader.into_inner(); match reader { NodeHttpFetchResponseReader::Start(resp) => { @@ -445,7 +454,9 @@ impl Resource for NodeHttpFetchResponseResource { // safely call `await` on it without creating a race condition. Some(_) => match reader.as_mut().next().await.unwrap() { Ok(chunk) => assert!(chunk.is_empty()), - Err(err) => break Err(type_error(err.to_string())), + Err(err) => { + break Err(deno_core::error::type_error(err.to_string())) + } }, None => break Ok(BufView::empty()), } @@ -453,7 +464,7 @@ impl Resource for NodeHttpFetchResponseResource { }; let cancel_handle = RcRef::map(self, |r| &r.cancel); - fut.try_or_cancel(cancel_handle).await + fut.try_or_cancel(cancel_handle).await.map_err(Into::into) }) } @@ -469,7 +480,9 @@ impl Resource for NodeHttpFetchResponseResource { #[allow(clippy::type_complexity)] pub struct NodeHttpResourceToBodyAdapter( Rc, - Option>>>>, + Option< + Pin>>>, + >, ); impl NodeHttpResourceToBodyAdapter { @@ -485,7 +498,7 @@ unsafe impl Send for NodeHttpResourceToBodyAdapter {} unsafe impl Sync for NodeHttpResourceToBodyAdapter {} impl Stream for NodeHttpResourceToBodyAdapter { - type Item = Result; + type Item = Result; fn poll_next( self: Pin<&mut Self>, @@ -515,7 +528,7 @@ impl Stream for NodeHttpResourceToBodyAdapter { impl hyper::body::Body for NodeHttpResourceToBodyAdapter { type Data = Bytes; - type Error = anyhow::Error; + type Error = deno_core::anyhow::Error; fn poll_frame( self: Pin<&mut Self>, diff --git a/ext/node/ops/http2.rs b/ext/node/ops/http2.rs index 9595cb33d6..53dada9f41 100644 --- a/ext/node/ops/http2.rs +++ b/ext/node/ops/http2.rs @@ -7,7 +7,6 @@ use std::rc::Rc; use std::task::Poll; use bytes::Bytes; -use deno_core::error::AnyError; use deno_core::futures::future::poll_fn; use deno_core::op2; use deno_core::serde::Serialize; @@ -110,17 +109,28 @@ impl Resource for Http2ServerSendResponse { } } +#[derive(Debug, thiserror::Error)] +pub enum Http2Error { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + UrlParse(#[from] url::ParseError), + #[error(transparent)] + H2(#[from] h2::Error), +} + #[op2(async)] #[serde] pub async fn op_http2_connect( state: Rc>, #[smi] rid: ResourceId, #[string] url: String, -) -> Result<(ResourceId, ResourceId), AnyError> { +) -> Result<(ResourceId, ResourceId), Http2Error> { // No permission check necessary because we're using an existing connection let network_stream = { let mut state = state.borrow_mut(); - take_network_stream_resource(&mut state.resource_table, rid)? + take_network_stream_resource(&mut state.resource_table, rid) + .map_err(Http2Error::Resource)? }; let url = Url::parse(&url)?; @@ -144,9 +154,10 @@ pub async fn op_http2_connect( pub async fn op_http2_listen( state: Rc>, #[smi] rid: ResourceId, -) -> Result { +) -> Result { let stream = - take_network_stream_resource(&mut state.borrow_mut().resource_table, rid)?; + take_network_stream_resource(&mut state.borrow_mut().resource_table, rid) + .map_err(Http2Error::Resource)?; let conn = h2::server::Builder::new().handshake(stream).await?; Ok( @@ -166,12 +177,13 @@ pub async fn op_http2_accept( #[smi] rid: ResourceId, ) -> Result< Option<(Vec<(ByteString, ByteString)>, ResourceId, ResourceId)>, - AnyError, + Http2Error, > { let resource = state .borrow() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(Http2Error::Resource)?; let mut conn = RcRef::map(&resource, |r| &r.conn).borrow_mut().await; if let Some(res) = conn.accept().await { let (req, resp) = res?; @@ -233,11 +245,12 @@ pub async fn op_http2_send_response( #[smi] rid: ResourceId, #[smi] status: u16, #[serde] headers: Vec<(ByteString, ByteString)>, -) -> Result<(ResourceId, u32), AnyError> { +) -> Result<(ResourceId, u32), Http2Error> { let resource = state .borrow() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(Http2Error::Resource)?; let mut send_response = RcRef::map(resource, |r| &r.send_response) .borrow_mut() .await; @@ -262,8 +275,12 @@ pub async fn op_http2_send_response( pub async fn op_http2_poll_client_connection( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { - let resource = state.borrow().resource_table.get::(rid)?; +) -> Result<(), Http2Error> { + let resource = state + .borrow() + .resource_table + .get::(rid) + .map_err(Http2Error::Resource)?; let cancel_handle = RcRef::map(resource.clone(), |this| &this.cancel_handle); let mut conn = RcRef::map(resource, |this| &this.conn).borrow_mut().await; @@ -289,11 +306,12 @@ pub async fn op_http2_client_request( // 4 strings of keys? #[serde] mut pseudo_headers: HashMap, #[serde] headers: Vec<(ByteString, ByteString)>, -) -> Result<(ResourceId, u32), AnyError> { +) -> Result<(ResourceId, u32), Http2Error> { let resource = state .borrow() .resource_table - .get::(client_rid)?; + .get::(client_rid) + .map_err(Http2Error::Resource)?; let url = resource.url.clone(); @@ -326,7 +344,10 @@ pub async fn op_http2_client_request( let resource = { let state = state.borrow(); - state.resource_table.get::(client_rid)? + state + .resource_table + .get::(client_rid) + .map_err(Http2Error::Resource)? }; let mut client = RcRef::map(&resource, |r| &r.client).borrow_mut().await; poll_fn(|cx| client.poll_ready(cx)).await?; @@ -345,11 +366,12 @@ pub async fn op_http2_client_send_data( #[smi] stream_rid: ResourceId, #[buffer] data: JsBuffer, end_of_stream: bool, -) -> Result<(), AnyError> { +) -> Result<(), Http2Error> { let resource = state .borrow() .resource_table - .get::(stream_rid)?; + .get::(stream_rid) + .map_err(Http2Error::Resource)?; let mut stream = RcRef::map(&resource, |r| &r.stream).borrow_mut().await; stream.send_data(data.to_vec().into(), end_of_stream)?; @@ -361,7 +383,7 @@ pub async fn op_http2_client_reset_stream( state: Rc>, #[smi] stream_rid: ResourceId, #[smi] code: u32, -) -> Result<(), AnyError> { +) -> Result<(), deno_core::error::AnyError> { let resource = state .borrow() .resource_table @@ -376,11 +398,12 @@ pub async fn op_http2_client_send_trailers( state: Rc>, #[smi] stream_rid: ResourceId, #[serde] trailers: Vec<(ByteString, ByteString)>, -) -> Result<(), AnyError> { +) -> Result<(), Http2Error> { let resource = state .borrow() .resource_table - .get::(stream_rid)?; + .get::(stream_rid) + .map_err(Http2Error::Resource)?; let mut stream = RcRef::map(&resource, |r| &r.stream).borrow_mut().await; let mut trailers_map = http::HeaderMap::new(); @@ -408,11 +431,12 @@ pub struct Http2ClientResponse { pub async fn op_http2_client_get_response( state: Rc>, #[smi] stream_rid: ResourceId, -) -> Result<(Http2ClientResponse, bool), AnyError> { +) -> Result<(Http2ClientResponse, bool), Http2Error> { let resource = state .borrow() .resource_table - .get::(stream_rid)?; + .get::(stream_rid) + .map_err(Http2Error::Resource)?; let mut response_future = RcRef::map(&resource, |r| &r.response).borrow_mut().await; @@ -478,23 +502,22 @@ fn poll_data_or_trailers( pub async fn op_http2_client_get_response_body_chunk( state: Rc>, #[smi] body_rid: ResourceId, -) -> Result<(Option>, bool, bool), AnyError> { +) -> Result<(Option>, bool, bool), Http2Error> { let resource = state .borrow() .resource_table - .get::(body_rid)?; + .get::(body_rid) + .map_err(Http2Error::Resource)?; let mut body = RcRef::map(&resource, |r| &r.body).borrow_mut().await; loop { 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)); - } + match err.reason() { + Some(Reason::NO_ERROR) => return Ok((None, true, false)), + Some(Reason::CANCEL) => return Ok((None, false, true)), + _ => return Err(err.into()), } - return Err(err.into()); } match result.unwrap() { DataOrTrailers::Data(data) => { @@ -527,7 +550,7 @@ pub async fn op_http2_client_get_response_body_chunk( pub async fn op_http2_client_get_response_trailers( state: Rc>, #[smi] body_rid: ResourceId, -) -> Result>, AnyError> { +) -> Result>, deno_core::error::AnyError> { let resource = state .borrow() .resource_table diff --git a/ext/node/ops/idna.rs b/ext/node/ops/idna.rs index 9c9450c70f..a3d85e77c2 100644 --- a/ext/node/ops/idna.rs +++ b/ext/node/ops/idna.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::anyhow::Error; -use deno_core::error::range_error; use deno_core::op2; use std::borrow::Cow; @@ -11,19 +9,21 @@ use std::borrow::Cow; const PUNY_PREFIX: &str = "xn--"; -fn invalid_input_err() -> Error { - range_error("Invalid input") -} - -fn not_basic_err() -> Error { - range_error("Illegal input >= 0x80 (not a basic code point)") +#[derive(Debug, thiserror::Error)] +pub enum IdnaError { + #[error("Invalid input")] + InvalidInput, + #[error("Input would take more than 63 characters to encode")] + InputTooLong, + #[error("Illegal input >= 0x80 (not a basic code point)")] + IllegalInput, } /// map a domain by mapping each label with the given function -fn map_domain( +fn map_domain( domain: &str, - f: impl Fn(&str) -> Result, E>, -) -> Result { + f: impl Fn(&str) -> Result, IdnaError>, +) -> Result { let mut result = String::with_capacity(domain.len()); let mut domain = domain; @@ -48,7 +48,7 @@ fn map_domain( /// Maps a unicode domain to ascii by punycode encoding each label /// /// Note this is not IDNA2003 or IDNA2008 compliant, rather it matches node.js's punycode implementation -fn to_ascii(input: &str) -> Result { +fn to_ascii(input: &str) -> Result { if input.is_ascii() { return Ok(input.into()); } @@ -61,9 +61,7 @@ fn to_ascii(input: &str) -> Result { } else { idna::punycode::encode_str(label) .map(|encoded| [PUNY_PREFIX, &encoded].join("").into()) // add the prefix - .ok_or_else(|| { - Error::msg("Input would take more than 63 characters to encode") // only error possible per the docs - }) + .ok_or(IdnaError::InputTooLong) // only error possible per the docs } })?; @@ -74,13 +72,13 @@ fn to_ascii(input: &str) -> Result { /// Maps an ascii domain to unicode by punycode decoding each label /// /// Note this is not IDNA2003 or IDNA2008 compliant, rather it matches node.js's punycode implementation -fn to_unicode(input: &str) -> Result { +fn to_unicode(input: &str) -> Result { map_domain(input, |s| { if let Some(puny) = s.strip_prefix(PUNY_PREFIX) { // it's a punycode encoded label Ok( idna::punycode::decode_to_string(&puny.to_lowercase()) - .ok_or_else(invalid_input_err)? + .ok_or(IdnaError::InvalidInput)? .into(), ) } else { @@ -95,7 +93,7 @@ fn to_unicode(input: &str) -> Result { #[string] pub fn op_node_idna_punycode_to_ascii( #[string] domain: String, -) -> Result { +) -> Result { to_ascii(&domain) } @@ -105,7 +103,7 @@ pub fn op_node_idna_punycode_to_ascii( #[string] pub fn op_node_idna_punycode_to_unicode( #[string] domain: String, -) -> Result { +) -> Result { to_unicode(&domain) } @@ -115,8 +113,8 @@ pub fn op_node_idna_punycode_to_unicode( #[string] pub fn op_node_idna_domain_to_ascii( #[string] domain: String, -) -> Result { - idna::domain_to_ascii(&domain).map_err(|e| e.into()) +) -> Result { + idna::domain_to_ascii(&domain) } /// Converts a domain to Unicode as per the IDNA spec @@ -131,7 +129,7 @@ pub fn op_node_idna_domain_to_unicode(#[string] domain: String) -> String { #[string] pub fn op_node_idna_punycode_decode( #[string] domain: String, -) -> Result { +) -> Result { if domain.is_empty() { return Ok(domain); } @@ -147,11 +145,10 @@ pub fn op_node_idna_punycode_decode( .unwrap_or(domain.len() - 1); if !domain[..last_dash].is_ascii() { - return Err(not_basic_err()); + return Err(IdnaError::IllegalInput); } - idna::punycode::decode_to_string(&domain) - .ok_or_else(|| deno_core::error::range_error("Invalid input")) + idna::punycode::decode_to_string(&domain).ok_or(IdnaError::InvalidInput) } #[op2] diff --git a/ext/node/ops/inspector.rs b/ext/node/ops/inspector.rs new file mode 100644 index 0000000000..34a7e004c1 --- /dev/null +++ b/ext/node/ops/inspector.rs @@ -0,0 +1,161 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use crate::NodePermissions; +use deno_core::anyhow::Error; +use deno_core::error::generic_error; +use deno_core::futures::channel::mpsc; +use deno_core::op2; +use deno_core::v8; +use deno_core::GarbageCollected; +use deno_core::InspectorSessionKind; +use deno_core::InspectorSessionOptions; +use deno_core::JsRuntimeInspector; +use deno_core::OpState; +use std::cell::RefCell; +use std::rc::Rc; + +#[op2(fast)] +pub fn op_inspector_enabled() -> bool { + // TODO: hook up to InspectorServer + false +} + +#[op2] +pub fn op_inspector_open

( + _state: &mut OpState, + _port: Option, + #[string] _host: Option, +) -> Result<(), Error> +where + P: NodePermissions + 'static, +{ + // TODO: hook up to InspectorServer + /* + let server = state.borrow_mut::(); + if let Some(host) = host { + server.set_host(host); + } + if let Some(port) = port { + server.set_port(port); + } + state + .borrow_mut::

() + .check_net((server.host(), Some(server.port())), "inspector.open")?; + */ + + Ok(()) +} + +#[op2(fast)] +pub fn op_inspector_close() { + // TODO: hook up to InspectorServer +} + +#[op2] +#[string] +pub fn op_inspector_url() -> Option { + // TODO: hook up to InspectorServer + None +} + +#[op2(fast)] +pub fn op_inspector_wait(state: &OpState) -> bool { + match state.try_borrow::>>() { + Some(inspector) => { + inspector + .borrow_mut() + .wait_for_session_and_break_on_next_statement(); + true + } + None => false, + } +} + +#[op2(fast)] +pub fn op_inspector_emit_protocol_event( + #[string] _event_name: String, + #[string] _params: String, +) { + // TODO: inspector channel & protocol notifications +} + +struct JSInspectorSession { + tx: RefCell>>, +} + +impl GarbageCollected for JSInspectorSession {} + +#[op2] +#[cppgc] +pub fn op_inspector_connect<'s, P>( + isolate: *mut v8::Isolate, + scope: &mut v8::HandleScope<'s>, + state: &mut OpState, + connect_to_main_thread: bool, + callback: v8::Local<'s, v8::Function>, +) -> Result +where + P: NodePermissions + 'static, +{ + state + .borrow_mut::

() + .check_sys("inspector", "inspector.Session.connect")?; + + if connect_to_main_thread { + return Err(generic_error("connectToMainThread not supported")); + } + + let context = scope.get_current_context(); + let context = v8::Global::new(scope, context); + let callback = v8::Global::new(scope, callback); + + let inspector = state + .borrow::>>() + .borrow_mut(); + + let tx = inspector.create_raw_session( + InspectorSessionOptions { + kind: InspectorSessionKind::NonBlocking { + wait_for_disconnect: false, + }, + }, + // The inspector connection does not keep the event loop alive but + // when the inspector sends a message to the frontend, the JS that + // that runs may keep the event loop alive so we have to call back + // synchronously, instead of using the usual LocalInspectorSession + // UnboundedReceiver API. + Box::new(move |message| { + // SAFETY: This function is called directly by the inspector, so + // 1) The isolate is still valid + // 2) We are on the same thread as the Isolate + let scope = unsafe { &mut v8::CallbackScope::new(&mut *isolate) }; + let context = v8::Local::new(scope, context.clone()); + let scope = &mut v8::ContextScope::new(scope, context); + let scope = &mut v8::TryCatch::new(scope); + let recv = v8::undefined(scope); + if let Some(message) = v8::String::new(scope, &message.content) { + let callback = v8::Local::new(scope, callback.clone()); + callback.call(scope, recv.into(), &[message.into()]); + } + }), + ); + + Ok(JSInspectorSession { + tx: RefCell::new(Some(tx)), + }) +} + +#[op2(fast)] +pub fn op_inspector_dispatch( + #[cppgc] session: &JSInspectorSession, + #[string] message: String, +) { + if let Some(tx) = &*session.tx.borrow() { + let _ = tx.unbounded_send(message); + } +} + +#[op2(fast)] +pub fn op_inspector_disconnect(#[cppgc] session: &JSInspectorSession) { + drop(session.tx.borrow_mut().take()); +} diff --git a/ext/node/ops/ipc.rs b/ext/node/ops/ipc.rs index 59b6fece14..672cf0d707 100644 --- a/ext/node/ops/ipc.rs +++ b/ext/node/ops/ipc.rs @@ -17,8 +17,6 @@ mod impl_ { use std::task::Context; use std::task::Poll; - use deno_core::error::bad_resource_id; - use deno_core::error::AnyError; use deno_core::op2; use deno_core::serde; use deno_core::serde::Serializer; @@ -167,7 +165,7 @@ mod impl_ { #[smi] pub fn op_node_child_ipc_pipe( state: &mut OpState, - ) -> Result, AnyError> { + ) -> Result, io::Error> { let fd = match state.try_borrow_mut::() { Some(child_pipe_fd) => child_pipe_fd.0, None => return Ok(None), @@ -180,6 +178,18 @@ mod impl_ { )) } + #[derive(Debug, thiserror::Error)] + pub enum IpcError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + IpcJsonStream(#[from] IpcJsonStreamError), + #[error(transparent)] + Canceled(#[from] deno_core::Canceled), + #[error("failed to serialize json value: {0}")] + SerdeJson(serde_json::Error), + } + #[op2(async)] pub fn op_node_ipc_write<'a>( scope: &mut v8::HandleScope<'a>, @@ -192,34 +202,37 @@ mod impl_ { // ideally we would just return `Result<(impl Future, bool), ..>`, but that's not // supported by `op2` currently. queue_ok: v8::Local<'a, v8::Array>, - ) -> Result>, AnyError> { + ) -> Result>, IpcError> { let mut serialized = Vec::with_capacity(64); let mut ser = serde_json::Serializer::new(&mut serialized); - serialize_v8_value(scope, value, &mut ser).map_err(|e| { - deno_core::error::type_error(format!( - "failed to serialize json value: {e}" - )) - })?; + serialize_v8_value(scope, value, &mut ser).map_err(IpcError::SerdeJson)?; serialized.push(b'\n'); let stream = state .borrow() .resource_table .get::(rid) - .map_err(|_| bad_resource_id())?; + .map_err(IpcError::Resource)?; let old = stream .queued_bytes .fetch_add(serialized.len(), std::sync::atomic::Ordering::Relaxed); if old + serialized.len() > 2 * INITIAL_CAPACITY { // sending messages too fast - let v = false.to_v8(scope)?; + let v = false.to_v8(scope).unwrap(); // Infallible queue_ok.set_index(scope, 0, v); } Ok(async move { - stream.clone().write_msg_bytes(&serialized).await?; + let cancel = stream.cancel.clone(); + let result = stream + .clone() + .write_msg_bytes(&serialized) + .or_cancel(cancel) + .await; + // adjust count even on error stream .queued_bytes .fetch_sub(serialized.len(), std::sync::atomic::Ordering::Relaxed); + result??; Ok(()) }) } @@ -239,12 +252,12 @@ mod impl_ { pub async fn op_node_ipc_read( state: Rc>, #[smi] rid: ResourceId, - ) -> Result { + ) -> Result { let stream = state .borrow() .resource_table .get::(rid) - .map_err(|_| bad_resource_id())?; + .map_err(IpcError::Resource)?; let cancel = stream.cancel.clone(); let mut stream = RcRef::map(stream, |r| &r.read_half).borrow_mut().await; @@ -400,7 +413,7 @@ mod impl_ { async fn write_msg_bytes( self: Rc, msg: &[u8], - ) -> Result<(), AnyError> { + ) -> Result<(), io::Error> { let mut write_half = RcRef::map(self, |r| &r.write_half).borrow_mut().await; write_half.write_all(msg).await?; @@ -455,6 +468,14 @@ mod impl_ { } } + #[derive(Debug, thiserror::Error)] + pub enum IpcJsonStreamError { + #[error("{0}")] + Io(#[source] std::io::Error), + #[error("{0}")] + SimdJson(#[source] simd_json::Error), + } + // JSON serialization stream over IPC pipe. // // `\n` is used as a delimiter between messages. @@ -475,7 +496,7 @@ mod impl_ { async fn read_msg( &mut self, - ) -> Result, AnyError> { + ) -> Result, IpcJsonStreamError> { let mut json = None; let nread = read_msg_inner( &mut self.pipe, @@ -483,7 +504,8 @@ mod impl_ { &mut json, &mut self.read_buffer, ) - .await?; + .await + .map_err(IpcJsonStreamError::Io)?; if nread == 0 { // EOF. return Ok(None); @@ -493,7 +515,8 @@ mod impl_ { Some(v) => v, None => { // Took more than a single read and some buffering. - simd_json::from_slice(&mut self.buffer[..nread])? + simd_json::from_slice(&mut self.buffer[..nread]) + .map_err(IpcJsonStreamError::SimdJson)? } }; diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index b562261f39..b53f19dc23 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -7,6 +7,7 @@ pub mod fs; pub mod http; pub mod http2; pub mod idna; +pub mod inspector; pub mod ipc; pub mod os; pub mod process; diff --git a/ext/node/ops/os/mod.rs b/ext/node/ops/os/mod.rs index ca91895f22..d291277ad4 100644 --- a/ext/node/ops/os/mod.rs +++ b/ext/node/ops/os/mod.rs @@ -1,19 +1,31 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::mem::MaybeUninit; + use crate::NodePermissions; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; mod cpus; -mod priority; +pub mod priority; + +#[derive(Debug, thiserror::Error)] +pub enum OsError { + #[error(transparent)] + Priority(priority::PriorityError), + #[error(transparent)] + Permission(#[from] deno_permissions::PermissionCheckError), + #[error("Failed to get cpu info")] + FailedToGetCpuInfo, + #[error("Failed to get user info")] + FailedToGetUserInfo(#[source] std::io::Error), +} #[op2(fast)] pub fn op_node_os_get_priority

( state: &mut OpState, pid: u32, -) -> Result +) -> Result where P: NodePermissions + 'static, { @@ -22,7 +34,7 @@ where permissions.check_sys("getPriority", "node:os.getPriority()")?; } - priority::get_priority(pid) + priority::get_priority(pid).map_err(OsError::Priority) } #[op2(fast)] @@ -30,7 +42,7 @@ pub fn op_node_os_set_priority

( state: &mut OpState, pid: u32, priority: i32, -) -> Result<(), AnyError> +) -> Result<(), OsError> where P: NodePermissions + 'static, { @@ -39,25 +51,171 @@ where permissions.check_sys("setPriority", "node:os.setPriority()")?; } - priority::set_priority(pid, priority) + priority::set_priority(pid, priority).map_err(OsError::Priority) +} + +#[derive(serde::Serialize)] +pub struct UserInfo { + username: String, + homedir: String, + shell: Option, +} + +#[cfg(unix)] +fn get_user_info(uid: u32) -> Result { + use std::ffi::CStr; + let mut pw: MaybeUninit = MaybeUninit::uninit(); + let mut result: *mut libc::passwd = std::ptr::null_mut(); + // SAFETY: libc call, no invariants + let max_buf_size = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) }; + let buf_size = if max_buf_size < 0 { + // from the man page + 16_384 + } else { + max_buf_size as usize + }; + let mut buf = { + let mut b = Vec::>::with_capacity(buf_size); + // SAFETY: MaybeUninit has no initialization invariants, and len == cap + unsafe { + b.set_len(buf_size); + } + b + }; + // SAFETY: libc call, args are correct + let s = unsafe { + libc::getpwuid_r( + uid, + pw.as_mut_ptr(), + buf.as_mut_ptr().cast(), + buf_size, + std::ptr::addr_of_mut!(result), + ) + }; + if result.is_null() { + if s != 0 { + return Err( + OsError::FailedToGetUserInfo(std::io::Error::last_os_error()), + ); + } else { + return Err(OsError::FailedToGetUserInfo(std::io::Error::from( + std::io::ErrorKind::NotFound, + ))); + } + } + // SAFETY: pw was initialized by the call to `getpwuid_r` above + let pw = unsafe { pw.assume_init() }; + // SAFETY: initialized above, pw alive until end of function, nul terminated + let username = unsafe { CStr::from_ptr(pw.pw_name) }; + // SAFETY: initialized above, pw alive until end of function, nul terminated + let homedir = unsafe { CStr::from_ptr(pw.pw_dir) }; + // SAFETY: initialized above, pw alive until end of function, nul terminated + let shell = unsafe { CStr::from_ptr(pw.pw_shell) }; + Ok(UserInfo { + username: username.to_string_lossy().into_owned(), + homedir: homedir.to_string_lossy().into_owned(), + shell: Some(shell.to_string_lossy().into_owned()), + }) +} + +#[cfg(windows)] +fn get_user_info(_uid: u32) -> Result { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + + use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Win32::Foundation::GetLastError; + use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER; + use windows_sys::Win32::Foundation::HANDLE; + use windows_sys::Win32::System::Threading::GetCurrentProcess; + use windows_sys::Win32::System::Threading::OpenProcessToken; + use windows_sys::Win32::UI::Shell::GetUserProfileDirectoryW; + struct Handle(HANDLE); + impl Drop for Handle { + fn drop(&mut self) { + // SAFETY: win32 call + unsafe { + CloseHandle(self.0); + } + } + } + let mut token: MaybeUninit = MaybeUninit::uninit(); + + // Get a handle to the current process + // SAFETY: win32 call + unsafe { + if OpenProcessToken( + GetCurrentProcess(), + windows_sys::Win32::Security::TOKEN_READ, + token.as_mut_ptr(), + ) == 0 + { + return Err( + OsError::FailedToGetUserInfo(std::io::Error::last_os_error()), + ); + } + } + + // SAFETY: initialized by call above + let token = Handle(unsafe { token.assume_init() }); + + let mut bufsize = 0; + // get the size for the homedir buf (it'll end up in `bufsize`) + // SAFETY: win32 call + unsafe { + GetUserProfileDirectoryW(token.0, std::ptr::null_mut(), &mut bufsize); + let err = GetLastError(); + if err != ERROR_INSUFFICIENT_BUFFER { + return Err(OsError::FailedToGetUserInfo( + std::io::Error::from_raw_os_error(err as i32), + )); + } + } + let mut path = vec![0; bufsize as usize]; + // Actually get the homedir + // SAFETY: path is `bufsize` elements + unsafe { + if GetUserProfileDirectoryW(token.0, path.as_mut_ptr(), &mut bufsize) == 0 { + return Err( + OsError::FailedToGetUserInfo(std::io::Error::last_os_error()), + ); + } + } + // remove trailing nul + path.pop(); + let homedir_wide = OsString::from_wide(&path); + let homedir = homedir_wide.to_string_lossy().into_owned(); + + Ok(UserInfo { + username: deno_whoami::username(), + homedir, + shell: None, + }) } #[op2] -#[string] -pub fn op_node_os_username

(state: &mut OpState) -> Result +#[serde] +pub fn op_node_os_user_info

( + state: &mut OpState, + #[smi] uid: u32, +) -> Result where P: NodePermissions + 'static, { { let permissions = state.borrow_mut::

(); - permissions.check_sys("username", "node:os.userInfo()")?; + permissions + .check_sys("userInfo", "node:os.userInfo()") + .map_err(OsError::Permission)?; } - Ok(deno_whoami::username()) + get_user_info(uid) } #[op2(fast)] -pub fn op_geteuid

(state: &mut OpState) -> Result +pub fn op_geteuid

( + state: &mut OpState, +) -> Result where P: NodePermissions + 'static, { @@ -76,7 +234,9 @@ where } #[op2(fast)] -pub fn op_getegid

(state: &mut OpState) -> Result +pub fn op_getegid

( + state: &mut OpState, +) -> Result where P: NodePermissions + 'static, { @@ -96,7 +256,7 @@ where #[op2] #[serde] -pub fn op_cpus

(state: &mut OpState) -> Result, AnyError> +pub fn op_cpus

(state: &mut OpState) -> Result, OsError> where P: NodePermissions + 'static, { @@ -105,12 +265,14 @@ where permissions.check_sys("cpus", "node:os.cpus()")?; } - cpus::cpu_info().ok_or_else(|| type_error("Failed to get cpu info")) + cpus::cpu_info().ok_or(OsError::FailedToGetCpuInfo) } #[op2] #[string] -pub fn op_homedir

(state: &mut OpState) -> Result, AnyError> +pub fn op_homedir

( + state: &mut OpState, +) -> Result, deno_core::error::AnyError> where P: NodePermissions + 'static, { diff --git a/ext/node/ops/os/priority.rs b/ext/node/ops/os/priority.rs index 043928e2a6..9a1ebcca70 100644 --- a/ext/node/ops/os/priority.rs +++ b/ext/node/ops/os/priority.rs @@ -1,12 +1,18 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; - pub use impl_::*; +#[derive(Debug, thiserror::Error)] +pub enum PriorityError { + #[error("{0}")] + Io(#[from] std::io::Error), + #[cfg(windows)] + #[error("Invalid priority")] + InvalidPriority, +} + #[cfg(unix)] mod impl_ { - use super::*; use errno::errno; use errno::set_errno; use errno::Errno; @@ -16,7 +22,7 @@ mod impl_ { const PRIORITY_HIGH: i32 = -14; // Ref: https://github.com/libuv/libuv/blob/55376b044b74db40772e8a6e24d67a8673998e02/src/unix/core.c#L1533-L1547 - pub fn get_priority(pid: u32) -> Result { + pub fn get_priority(pid: u32) -> Result { set_errno(Errno(0)); match ( // SAFETY: libc::getpriority is unsafe @@ -29,7 +35,10 @@ mod impl_ { } } - pub fn set_priority(pid: u32, priority: i32) -> Result<(), AnyError> { + pub fn set_priority( + pid: u32, + priority: i32, + ) -> Result<(), super::PriorityError> { // SAFETY: libc::setpriority is unsafe match unsafe { libc::setpriority(PRIO_PROCESS, pid as id_t, priority) } { -1 => Err(std::io::Error::last_os_error().into()), @@ -40,8 +49,6 @@ mod impl_ { #[cfg(windows)] mod impl_ { - use super::*; - use deno_core::error::type_error; use winapi::shared::minwindef::DWORD; use winapi::shared::minwindef::FALSE; use winapi::shared::ntdef::NULL; @@ -67,7 +74,7 @@ mod impl_ { const PRIORITY_HIGHEST: i32 = -20; // Ported from: https://github.com/libuv/libuv/blob/a877ca2435134ef86315326ef4ef0c16bdbabf17/src/win/util.c#L1649-L1685 - pub fn get_priority(pid: u32) -> Result { + pub fn get_priority(pid: u32) -> Result { // SAFETY: Windows API calls unsafe { let handle = if pid == 0 { @@ -95,7 +102,10 @@ mod impl_ { } // Ported from: https://github.com/libuv/libuv/blob/a877ca2435134ef86315326ef4ef0c16bdbabf17/src/win/util.c#L1688-L1719 - pub fn set_priority(pid: u32, priority: i32) -> Result<(), AnyError> { + pub fn set_priority( + pid: u32, + priority: i32, + ) -> Result<(), super::PriorityError> { // SAFETY: Windows API calls unsafe { let handle = if pid == 0 { @@ -109,7 +119,7 @@ mod impl_ { #[allow(clippy::manual_range_contains)] let priority_class = if priority < PRIORITY_HIGHEST || priority > PRIORITY_LOW { - return Err(type_error("Invalid priority")); + return Err(super::PriorityError::InvalidPriority); } else if priority < PRIORITY_HIGH { REALTIME_PRIORITY_CLASS } else if priority < PRIORITY_ABOVE_NORMAL { diff --git a/ext/node/ops/process.rs b/ext/node/ops/process.rs index 0992c46c62..282567226e 100644 --- a/ext/node/ops/process.rs +++ b/ext/node/ops/process.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_permissions::PermissionsContainer; @@ -51,7 +50,7 @@ pub fn op_node_process_kill( state: &mut OpState, #[smi] pid: i32, #[smi] sig: i32, -) -> Result { +) -> Result { state .borrow_mut::() .check_run_all("process.kill")?; diff --git a/ext/node/ops/require.rs b/ext/node/ops/require.rs index 3578719d0c..e381ee91d4 100644 --- a/ext/node/ops/require.rs +++ b/ext/node/ops/require.rs @@ -1,40 +1,73 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::anyhow::Context; -use deno_core::error::generic_error; use deno_core::error::AnyError; -use deno_core::normalize_path; use deno_core::op2; use deno_core::url::Url; use deno_core::v8; use deno_core::JsRuntimeInspector; -use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_fs::FileSystemRc; +use deno_package_json::NodeModuleKind; use deno_package_json::PackageJsonRc; -use node_resolver::NodeModuleKind; +use deno_path_util::normalize_path; +use deno_path_util::url_from_file_path; +use deno_path_util::url_to_file_path; +use node_resolver::errors::ClosestPkgJsonError; use node_resolver::NodeResolutionMode; use node_resolver::REQUIRE_CONDITIONS; +use std::borrow::Cow; use std::cell::RefCell; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; use crate::NodePermissions; -use crate::NodeRequireResolverRc; +use crate::NodeRequireLoaderRc; use crate::NodeResolverRc; -use crate::NpmResolverRc; +use crate::NpmPackageFolderResolverRc; +use crate::PackageJsonResolverRc; -fn ensure_read_permission

( +#[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] +fn ensure_read_permission<'a, P>( state: &mut OpState, - file_path: &Path, -) -> Result<(), AnyError> + file_path: &'a Path, +) -> Result, deno_core::error::AnyError> where P: NodePermissions + 'static, { - let resolver = state.borrow::().clone(); + let loader = state.borrow::().clone(); let permissions = state.borrow_mut::

(); - resolver.ensure_read_permission(permissions, file_path) + loader.ensure_read_permission(permissions, file_path) +} + +#[derive(Debug, thiserror::Error)] +pub enum RequireError { + #[error(transparent)] + UrlParse(#[from] url::ParseError), + #[error(transparent)] + Permission(deno_core::error::AnyError), + #[error(transparent)] + PackageExportsResolve( + #[from] node_resolver::errors::PackageExportsResolveError, + ), + #[error(transparent)] + PackageJsonLoad(#[from] node_resolver::errors::PackageJsonLoadError), + #[error(transparent)] + ClosestPkgJson(#[from] node_resolver::errors::ClosestPkgJsonError), + #[error(transparent)] + PackageImportsResolve( + #[from] node_resolver::errors::PackageImportsResolveError, + ), + #[error(transparent)] + FilePathConversion(#[from] deno_path_util::UrlToFilePathError), + #[error(transparent)] + UrlConversion(#[from] deno_path_util::PathToUrlError), + #[error(transparent)] + Fs(#[from] deno_io::fs::FsError), + #[error(transparent)] + ReadModule(deno_core::error::AnyError), + #[error("Unable to get CWD: {0}")] + UnableToGetCwd(deno_io::fs::FsError), } #[op2] @@ -93,7 +126,7 @@ pub fn op_require_init_paths() -> Vec { pub fn op_require_node_module_paths

( state: &mut OpState, #[string] from: String, -) -> Result, AnyError> +) -> Result, RequireError> where P: NodePermissions + 'static, { @@ -102,13 +135,10 @@ where let from = if from.starts_with("file:///") { url_to_file_path(&Url::parse(&from)?)? } else { - let current_dir = - &(fs.cwd().map_err(AnyError::from)).context("Unable to get CWD")?; - deno_core::normalize_path(current_dir.join(from)) + let current_dir = &fs.cwd().map_err(RequireError::UnableToGetCwd)?; + normalize_path(current_dir.join(from)) }; - ensure_read_permission::

(state, &from)?; - if cfg!(windows) { // return root node_modules when path is 'D:\\'. let from_str = from.to_str().unwrap(); @@ -189,17 +219,17 @@ pub fn op_require_resolve_deno_dir( state: &mut OpState, #[string] request: String, #[string] parent_filename: String, -) -> Option { - let resolver = state.borrow::(); - resolver - .resolve_package_folder_from_package( - &request, - &ModuleSpecifier::from_file_path(&parent_filename).unwrap_or_else(|_| { - panic!("Url::from_file_path: [{:?}]", parent_filename) - }), - ) - .ok() - .map(|p| p.to_string_lossy().into_owned()) +) -> Result, AnyError> { + let resolver = state.borrow::(); + Ok( + resolver + .resolve_package_folder_from_package( + &request, + &url_from_file_path(&PathBuf::from(parent_filename))?, + ) + .ok() + .map(|p| p.to_string_lossy().into_owned()), + ) } #[op2(fast)] @@ -207,8 +237,11 @@ pub fn op_require_is_deno_dir_package( state: &mut OpState, #[string] path: String, ) -> bool { - let resolver = state.borrow::(); - resolver.in_npm_package_at_file_path(&PathBuf::from(path)) + let resolver = state.borrow::(); + match deno_path_util::url_from_file_path(&PathBuf::from(path)) { + Ok(specifier) => resolver.in_npm_package(&specifier), + Err(_) => false, + } } #[op2] @@ -262,12 +295,12 @@ pub fn op_require_path_is_absolute(#[string] p: String) -> bool { pub fn op_require_stat

( state: &mut OpState, #[string] path: String, -) -> Result +) -> Result where P: NodePermissions + 'static, { let path = PathBuf::from(path); - ensure_read_permission::

(state, &path)?; + let path = ensure_read_permission::

(state, &path)?; let fs = state.borrow::(); if let Ok(metadata) = fs.stat_sync(&path) { if metadata.is_file { @@ -285,15 +318,16 @@ where pub fn op_require_real_path

( state: &mut OpState, #[string] request: String, -) -> Result +) -> Result where P: NodePermissions + 'static, { let path = PathBuf::from(request); - ensure_read_permission::

(state, &path)?; + let path = ensure_read_permission::

(state, &path) + .map_err(RequireError::Permission)?; let fs = state.borrow::(); let canonicalized_path = - deno_core::strip_unc_prefix(fs.realpath_sync(&path)?); + deno_path_util::strip_unc_prefix(fs.realpath_sync(&path)?); Ok(canonicalized_path.to_string_lossy().into_owned()) } @@ -317,12 +351,14 @@ pub fn op_require_path_resolve(#[serde] parts: Vec) -> String { #[string] pub fn op_require_path_dirname( #[string] request: String, -) -> Result { +) -> Result { let p = PathBuf::from(request); if let Some(parent) = p.parent() { Ok(parent.to_string_lossy().into_owned()) } else { - Err(generic_error("Path doesn't have a parent")) + Err(deno_core::error::generic_error( + "Path doesn't have a parent", + )) } } @@ -330,12 +366,14 @@ pub fn op_require_path_dirname( #[string] pub fn op_require_path_basename( #[string] request: String, -) -> Result { +) -> Result { let p = PathBuf::from(request); if let Some(path) = p.file_name() { Ok(path.to_string_lossy().into_owned()) } else { - Err(generic_error("Path doesn't have a file name")) + Err(deno_core::error::generic_error( + "Path doesn't have a file name", + )) } } @@ -346,7 +384,7 @@ pub fn op_require_try_self_parent_path

( has_parent: bool, #[string] maybe_parent_filename: Option, #[string] maybe_parent_id: Option, -) -> Result, AnyError> +) -> Result, deno_core::error::AnyError> where P: NodePermissions + 'static, { @@ -362,7 +400,7 @@ where if parent_id == "" || parent_id == "internal/preload" { let fs = state.borrow::(); if let Ok(cwd) = fs.cwd() { - ensure_read_permission::

(state, &cwd)?; + let cwd = ensure_read_permission::

(state, &cwd)?; return Ok(Some(cwd.to_string_lossy().into_owned())); } } @@ -376,7 +414,7 @@ pub fn op_require_try_self

( state: &mut OpState, #[string] parent_path: Option, #[string] request: String, -) -> Result, AnyError> +) -> Result, RequireError> where P: NodePermissions + 'static, { @@ -384,8 +422,8 @@ where return Ok(None); } - let node_resolver = state.borrow::(); - let pkg = node_resolver + let pkg_json_resolver = state.borrow::(); + let pkg = pkg_json_resolver .get_closest_package_json_from_path(&PathBuf::from(parent_path.unwrap())) .ok() .flatten(); @@ -414,6 +452,7 @@ where let referrer = deno_core::url::Url::from_file_path(&pkg.path).unwrap(); if let Some(exports) = &pkg.exports { + let node_resolver = state.borrow::(); let r = node_resolver.package_exports_resolve( &pkg.path, &expansion, @@ -438,14 +477,18 @@ where pub fn op_require_read_file

( state: &mut OpState, #[string] file_path: String, -) -> Result +) -> Result where P: NodePermissions + 'static, { let file_path = PathBuf::from(file_path); - ensure_read_permission::

(state, &file_path)?; - let fs = state.borrow::(); - Ok(fs.read_text_file_lossy_sync(&file_path, None)?) + // todo(dsherret): there's multiple borrows to NodeRequireLoaderRc here + let file_path = ensure_read_permission::

(state, &file_path) + .map_err(RequireError::Permission)?; + let loader = state.borrow::(); + loader + .load_text_file_lossy(&file_path) + .map_err(RequireError::ReadModule) } #[op2] @@ -470,16 +513,17 @@ pub fn op_require_resolve_exports

( #[string] name: String, #[string] expansion: String, #[string] parent_path: String, -) -> Result, AnyError> +) -> Result, RequireError> where P: NodePermissions + 'static, { let fs = state.borrow::(); - let npm_resolver = state.borrow::(); let node_resolver = state.borrow::(); + let pkg_json_resolver = state.borrow::(); let modules_path = PathBuf::from(&modules_path_str); - let pkg_path = if npm_resolver.in_npm_package_at_file_path(&modules_path) + let modules_specifier = deno_path_util::url_from_file_path(&modules_path)?; + let pkg_path = if node_resolver.in_npm_package(&modules_specifier) && !uses_local_node_modules_dir { modules_path @@ -493,7 +537,7 @@ where } }; let Some(pkg) = - node_resolver.load_package_json(&pkg_path.join("package.json"))? + pkg_json_resolver.load_package_json(&pkg_path.join("package.json"))? else { return Ok(None); }; @@ -501,12 +545,16 @@ where return Ok(None); }; - let referrer = Url::from_file_path(parent_path).unwrap(); + let referrer = if parent_path.is_empty() { + None + } else { + Some(Url::from_file_path(parent_path).unwrap()) + }; let r = node_resolver.package_exports_resolve( &pkg.path, &format!(".{expansion}"), exports, - Some(&referrer), + referrer.as_ref(), NodeModuleKind::Cjs, REQUIRE_CONDITIONS, NodeResolutionMode::Execution, @@ -518,21 +566,17 @@ where })) } -#[op2] -#[serde] -pub fn op_require_read_closest_package_json

( +#[op2(fast)] +pub fn op_require_is_maybe_cjs( state: &mut OpState, #[string] filename: String, -) -> Result, AnyError> -where - P: NodePermissions + 'static, -{ +) -> Result { let filename = PathBuf::from(filename); - ensure_read_permission::

(state, filename.parent().unwrap())?; - let node_resolver = state.borrow::().clone(); - node_resolver - .get_closest_package_json_from_path(&filename) - .map_err(AnyError::from) + let Ok(url) = url_from_file_path(&filename) else { + return Ok(false); + }; + let loader = state.borrow::(); + loader.is_maybe_cjs(&url) } #[op2] @@ -544,13 +588,13 @@ pub fn op_require_read_package_scope

( where P: NodePermissions + 'static, { - let node_resolver = state.borrow::().clone(); + let pkg_json_resolver = state.borrow::(); 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 + pkg_json_resolver .load_package_json(&package_json_path) .ok() .flatten() @@ -562,22 +606,23 @@ pub fn op_require_package_imports_resolve

( state: &mut OpState, #[string] referrer_filename: String, #[string] request: String, -) -> Result, AnyError> +) -> Result, RequireError> where P: NodePermissions + 'static, { let referrer_path = PathBuf::from(&referrer_filename); - ensure_read_permission::

(state, &referrer_path)?; - let node_resolver = state.borrow::(); + let referrer_path = ensure_read_permission::

(state, &referrer_path) + .map_err(RequireError::Permission)?; + let pkg_json_resolver = state.borrow::(); let Some(pkg) = - node_resolver.get_closest_package_json_from_path(&referrer_path)? + pkg_json_resolver.get_closest_package_json_from_path(&referrer_path)? else { return Ok(None); }; if pkg.imports.is_some() { - let referrer_url = - deno_core::url::Url::from_file_path(&referrer_filename).unwrap(); + let node_resolver = state.borrow::(); + let referrer_url = Url::from_file_path(&referrer_filename).unwrap(); let url = node_resolver.package_imports_resolve( &request, Some(&referrer_url), @@ -602,20 +647,11 @@ pub fn op_require_break_on_next_statement(state: Rc>) { inspector.wait_for_session_and_break_on_next_statement() } -fn url_to_file_path_string(url: &Url) -> Result { +fn url_to_file_path_string(url: &Url) -> Result { let file_path = url_to_file_path(url)?; Ok(file_path.to_string_lossy().into_owned()) } -fn url_to_file_path(url: &Url) -> Result { - match url.to_file_path() { - Ok(file_path) => Ok(file_path), - Err(()) => { - deno_core::anyhow::bail!("failed to convert '{}' to file path", url) - } - } -} - #[op2(fast)] pub fn op_require_can_parse_as_esm( scope: &mut v8::HandleScope, diff --git a/ext/node/ops/util.rs b/ext/node/ops/util.rs index 533d51c92c..1c177ac043 100644 --- a/ext/node/ops/util.rs +++ b/ext/node/ops/util.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::ResourceHandle; @@ -22,7 +21,7 @@ enum HandleType { pub fn op_node_guess_handle_type( state: &mut OpState, rid: u32, -) -> Result { +) -> Result { let handle = state.resource_table.get_handle(rid)?; let handle_type = match handle { diff --git a/ext/node/ops/v8.rs b/ext/node/ops/v8.rs index 8813d2e18e..61f67f11f7 100644 --- a/ext/node/ops/v8.rs +++ b/ext/node/ops/v8.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::generic_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; + use deno_core::op2; use deno_core::v8; use deno_core::FastString; @@ -206,10 +204,9 @@ pub fn op_v8_write_value( scope: &mut v8::HandleScope, #[cppgc] ser: &Serializer, value: v8::Local, -) -> Result<(), AnyError> { +) { let context = scope.get_current_context(); ser.inner.write_value(context, value); - Ok(()) } struct DeserBuffer { @@ -271,11 +268,13 @@ pub fn op_v8_new_deserializer( scope: &mut v8::HandleScope, obj: v8::Local, buffer: v8::Local, -) -> Result, AnyError> { +) -> Result, deno_core::error::AnyError> { let offset = buffer.byte_offset(); let len = buffer.byte_length(); let backing_store = buffer.get_backing_store().ok_or_else(|| { - generic_error("deserialization buffer has no backing store") + deno_core::error::generic_error( + "deserialization buffer has no backing store", + ) })?; let (buf_slice, buf_ptr) = if let Some(data) = backing_store.data() { // SAFETY: the offset is valid for the underlying buffer because we're getting it directly from v8 @@ -317,10 +316,10 @@ pub fn op_v8_transfer_array_buffer_de( #[op2(fast)] pub fn op_v8_read_double( #[cppgc] deser: &Deserializer, -) -> Result { +) -> Result { let mut double = 0f64; if !deser.inner.read_double(&mut double) { - return Err(type_error("ReadDouble() failed")); + return Err(deno_core::error::type_error("ReadDouble() failed")); } Ok(double) } @@ -355,10 +354,10 @@ pub fn op_v8_read_raw_bytes( #[op2(fast)] pub fn op_v8_read_uint32( #[cppgc] deser: &Deserializer, -) -> Result { +) -> Result { let mut value = 0; if !deser.inner.read_uint32(&mut value) { - return Err(type_error("ReadUint32() failed")); + return Err(deno_core::error::type_error("ReadUint32() failed")); } Ok(value) @@ -368,10 +367,10 @@ pub fn op_v8_read_uint32( #[serde] pub fn op_v8_read_uint64( #[cppgc] deser: &Deserializer, -) -> Result<(u32, u32), AnyError> { +) -> Result<(u32, u32), deno_core::error::AnyError> { let mut val = 0; if !deser.inner.read_uint64(&mut val) { - return Err(type_error("ReadUint64() failed")); + return Err(deno_core::error::type_error("ReadUint64() failed")); } Ok(((val >> 32) as u32, val as u32)) diff --git a/ext/node/ops/winerror.rs b/ext/node/ops/winerror.rs index c0d66f7d0b..cb053774ef 100644 --- a/ext/node/ops/winerror.rs +++ b/ext/node/ops/winerror.rs @@ -62,10 +62,11 @@ pub fn op_node_sys_to_uv_error(err: i32) -> String { WSAEHOSTUNREACH => "EHOSTUNREACH", ERROR_INSUFFICIENT_BUFFER => "EINVAL", ERROR_INVALID_DATA => "EINVAL", - ERROR_INVALID_NAME => "EINVAL", + ERROR_INVALID_NAME => "ENOENT", ERROR_INVALID_PARAMETER => "EINVAL", WSAEINVAL => "EINVAL", WSAEPFNOSUPPORT => "EINVAL", + ERROR_NOT_A_REPARSE_POINT => "EINVAL", ERROR_BEGINNING_OF_MEDIA => "EIO", ERROR_BUS_RESET => "EIO", ERROR_CRC => "EIO", diff --git a/ext/node/ops/worker_threads.rs b/ext/node/ops/worker_threads.rs index c7ea4c52c2..d2e5758826 100644 --- a/ext/node/ops/worker_threads.rs +++ b/ext/node/ops/worker_threads.rs @@ -1,37 +1,56 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::generic_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::url::Url; use deno_core::OpState; use deno_fs::FileSystemRc; -use node_resolver::NodeResolution; +use std::borrow::Cow; use std::path::Path; use std::path::PathBuf; use crate::NodePermissions; -use crate::NodeRequireResolverRc; -use crate::NodeResolverRc; +use crate::NodeRequireLoaderRc; -fn ensure_read_permission

( +#[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] +fn ensure_read_permission<'a, P>( state: &mut OpState, - file_path: &Path, -) -> Result<(), AnyError> + file_path: &'a Path, +) -> Result, deno_core::error::AnyError> where P: NodePermissions + 'static, { - let resolver = state.borrow::().clone(); + let loader = state.borrow::().clone(); let permissions = state.borrow_mut::

(); - resolver.ensure_read_permission(permissions, file_path) + loader.ensure_read_permission(permissions, file_path) } +#[derive(Debug, thiserror::Error)] +pub enum WorkerThreadsFilenameError { + #[error(transparent)] + Permission(deno_core::error::AnyError), + #[error("{0}")] + UrlParse(#[from] url::ParseError), + #[error("Relative path entries must start with '.' or '..'")] + InvalidRelativeUrl, + #[error("URL from Path-String")] + UrlFromPathString, + #[error("URL to Path-String")] + UrlToPathString, + #[error("URL to Path")] + UrlToPath, + #[error("File not found [{0:?}]")] + FileNotFound(PathBuf), + #[error(transparent)] + Fs(#[from] deno_io::fs::FsError), +} + +// todo(dsherret): we should remove this and do all this work inside op_create_worker #[op2] #[string] pub fn op_worker_threads_filename

( state: &mut OpState, #[string] specifier: String, -) -> Result +) -> Result where P: NodePermissions + 'static, { @@ -43,44 +62,26 @@ where } else { let path = PathBuf::from(&specifier); if path.is_relative() && !specifier.starts_with('.') { - return Err(generic_error( - "Relative path entries must start with '.' or '..'", - )); + return Err(WorkerThreadsFilenameError::InvalidRelativeUrl); } - ensure_read_permission::

(state, &path)?; + let path = ensure_read_permission::

(state, &path) + .map_err(WorkerThreadsFilenameError::Permission)?; let fs = state.borrow::(); let canonicalized_path = - deno_core::strip_unc_prefix(fs.realpath_sync(&path)?); + deno_path_util::strip_unc_prefix(fs.realpath_sync(&path)?); Url::from_file_path(canonicalized_path) - .map_err(|e| generic_error(format!("URL from Path-String: {:#?}", e)))? + .map_err(|_| WorkerThreadsFilenameError::UrlFromPathString)? }; let url_path = url .to_file_path() - .map_err(|e| generic_error(format!("URL to Path-String: {:#?}", e)))?; - ensure_read_permission::

(state, &url_path)?; + .map_err(|_| WorkerThreadsFilenameError::UrlToPathString)?; + let url_path = ensure_read_permission::

(state, &url_path) + .map_err(WorkerThreadsFilenameError::Permission)?; let fs = state.borrow::(); if !fs.exists_sync(&url_path) { - return Err(generic_error(format!("File not found [{:?}]", url_path))); - } - let node_resolver = state.borrow::(); - match node_resolver.url_to_node_resolution(url)? { - NodeResolution::Esm(u) => Ok(u.to_string()), - NodeResolution::CommonJs(u) => wrap_cjs(u), - NodeResolution::BuiltIn(_) => Err(generic_error("Neither ESM nor CJS")), + return Err(WorkerThreadsFilenameError::FileNotFound( + url_path.to_path_buf(), + )); } -} - -/// -/// Wrap a CJS file-URL and the required setup in a stringified `data:`-URL -/// -fn wrap_cjs(url: Url) -> Result { - let path = url - .to_file_path() - .map_err(|e| generic_error(format!("URL to Path: {:#?}", e)))?; - let filename = path.file_name().unwrap().to_string_lossy(); - Ok(format!( - "data:text/javascript,import {{ createRequire }} from \"node:module\";\ - const require = createRequire(\"{}\"); require(\"./{}\");", - url, filename, - )) + Ok(url.to_string()) } diff --git a/ext/node/ops/zlib/brotli.rs b/ext/node/ops/zlib/brotli.rs index 3e3905fc3d..1a681ff7f7 100644 --- a/ext/node/ops/zlib/brotli.rs +++ b/ext/node/ops/zlib/brotli.rs @@ -9,8 +9,6 @@ use brotli::BrotliDecompressStream; use brotli::BrotliResult; use brotli::BrotliState; use brotli::Decompressor; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::JsBuffer; use deno_core::OpState; @@ -19,7 +17,23 @@ use deno_core::ToJsBuffer; use std::cell::RefCell; use std::io::Read; -fn encoder_mode(mode: u32) -> Result { +#[derive(Debug, thiserror::Error)] +pub enum BrotliError { + #[error("Invalid encoder mode")] + InvalidEncoderMode, + #[error("Failed to compress")] + CompressFailed, + #[error("Failed to decompress")] + DecompressFailed, + #[error(transparent)] + Join(#[from] tokio::task::JoinError), + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error("{0}")] + Io(std::io::Error), +} + +fn encoder_mode(mode: u32) -> Result { Ok(match mode { 0 => BrotliEncoderMode::BROTLI_MODE_GENERIC, 1 => BrotliEncoderMode::BROTLI_MODE_TEXT, @@ -28,7 +42,7 @@ fn encoder_mode(mode: u32) -> Result { 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")), + _ => return Err(BrotliError::InvalidEncoderMode), }) } @@ -40,7 +54,7 @@ pub fn op_brotli_compress( #[smi] quality: i32, #[smi] lgwin: i32, #[smi] mode: u32, -) -> Result { +) -> Result { let mode = encoder_mode(mode)?; let mut out_size = out.len(); @@ -57,7 +71,7 @@ pub fn op_brotli_compress( &mut |_, _, _, _| (), ); if result != 1 { - return Err(type_error("Failed to compress")); + return Err(BrotliError::CompressFailed); } Ok(out_size) @@ -87,7 +101,7 @@ pub async fn op_brotli_compress_async( #[smi] quality: i32, #[smi] lgwin: i32, #[smi] mode: u32, -) -> Result { +) -> Result { let mode = encoder_mode(mode)?; tokio::task::spawn_blocking(move || { let input = &*input; @@ -107,7 +121,7 @@ pub async fn op_brotli_compress_async( &mut |_, _, _, _| (), ); if result != 1 { - return Err(type_error("Failed to compress")); + return Err(BrotliError::CompressFailed); } out.truncate(out_size); @@ -151,8 +165,11 @@ pub fn op_brotli_compress_stream( #[smi] rid: u32, #[buffer] input: &[u8], #[buffer] output: &mut [u8], -) -> Result { - let ctx = state.resource_table.get::(rid)?; +) -> Result { + let ctx = state + .resource_table + .get::(rid) + .map_err(BrotliError::Resource)?; let mut inst = ctx.inst.borrow_mut(); let mut output_offset = 0; @@ -168,7 +185,7 @@ pub fn op_brotli_compress_stream( &mut |_, _, _, _| (), ); if !result { - return Err(type_error("Failed to compress")); + return Err(BrotliError::CompressFailed); } Ok(output_offset) @@ -180,8 +197,11 @@ pub fn op_brotli_compress_stream_end( state: &mut OpState, #[smi] rid: u32, #[buffer] output: &mut [u8], -) -> Result { - let ctx = state.resource_table.get::(rid)?; +) -> Result { + let ctx = state + .resource_table + .get::(rid) + .map_err(BrotliError::Resource)?; let mut inst = ctx.inst.borrow_mut(); let mut output_offset = 0; @@ -197,13 +217,13 @@ pub fn op_brotli_compress_stream_end( &mut |_, _, _, _| (), ); if !result { - return Err(type_error("Failed to compress")); + return Err(BrotliError::CompressFailed); } Ok(output_offset) } -fn brotli_decompress(buffer: &[u8]) -> Result { +fn brotli_decompress(buffer: &[u8]) -> Result { let mut output = Vec::with_capacity(4096); let mut decompressor = Decompressor::new(buffer, buffer.len()); decompressor.read_to_end(&mut output)?; @@ -214,7 +234,7 @@ fn brotli_decompress(buffer: &[u8]) -> Result { #[serde] pub fn op_brotli_decompress( #[buffer] buffer: &[u8], -) -> Result { +) -> Result { brotli_decompress(buffer) } @@ -222,8 +242,11 @@ pub fn op_brotli_decompress( #[serde] pub async fn op_brotli_decompress_async( #[buffer] buffer: JsBuffer, -) -> Result { - tokio::task::spawn_blocking(move || brotli_decompress(&buffer)).await? +) -> Result { + tokio::task::spawn_blocking(move || { + brotli_decompress(&buffer).map_err(BrotliError::Io) + }) + .await? } struct BrotliDecompressCtx { @@ -252,8 +275,11 @@ pub fn op_brotli_decompress_stream( #[smi] rid: u32, #[buffer] input: &[u8], #[buffer] output: &mut [u8], -) -> Result { - let ctx = state.resource_table.get::(rid)?; +) -> Result { + let ctx = state + .resource_table + .get::(rid) + .map_err(BrotliError::Resource)?; let mut inst = ctx.inst.borrow_mut(); let mut output_offset = 0; @@ -268,7 +294,7 @@ pub fn op_brotli_decompress_stream( &mut inst, ); if matches!(result, BrotliResult::ResultFailure) { - return Err(type_error("Failed to decompress")); + return Err(BrotliError::DecompressFailed); } Ok(output_offset) @@ -280,8 +306,11 @@ pub fn op_brotli_decompress_stream_end( state: &mut OpState, #[smi] rid: u32, #[buffer] output: &mut [u8], -) -> Result { - let ctx = state.resource_table.get::(rid)?; +) -> Result { + let ctx = state + .resource_table + .get::(rid) + .map_err(BrotliError::Resource)?; let mut inst = ctx.inst.borrow_mut(); let mut output_offset = 0; @@ -296,7 +325,7 @@ pub fn op_brotli_decompress_stream_end( &mut inst, ); if matches!(result, BrotliResult::ResultFailure) { - return Err(type_error("Failed to decompress")); + return Err(BrotliError::DecompressFailed); } Ok(output_offset) diff --git a/ext/node/ops/zlib/mod.rs b/ext/node/ops/zlib/mod.rs index b1d6d21d22..991c0925d2 100644 --- a/ext/node/ops/zlib/mod.rs +++ b/ext/node/ops/zlib/mod.rs @@ -1,14 +1,14 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; -use deno_core::error::AnyError; + use deno_core::op2; +use libc::c_ulong; use std::borrow::Cow; use std::cell::RefCell; use zlib::*; mod alloc; pub mod brotli; -mod mode; +pub mod mode; mod stream; use mode::Flush; @@ -17,11 +17,11 @@ use mode::Mode; use self::stream::StreamWrapper; #[inline] -fn check(condition: bool, msg: &str) -> Result<(), AnyError> { +fn check(condition: bool, msg: &str) -> Result<(), deno_core::error::AnyError> { if condition { Ok(()) } else { - Err(type_error(msg.to_string())) + Err(deno_core::error::type_error(msg.to_string())) } } @@ -56,7 +56,7 @@ impl ZlibInner { out_off: u32, out_len: u32, flush: Flush, - ) -> Result<(), AnyError> { + ) -> Result<(), deno_core::error::AnyError> { check(self.init_done, "write before init")?; check(!self.write_in_progress, "write already in progress")?; check(!self.pending_close, "close already in progress")?; @@ -65,11 +65,11 @@ impl ZlibInner { let next_in = input .get(in_off as usize..in_off as usize + in_len as usize) - .ok_or_else(|| type_error("invalid input range"))? + .ok_or_else(|| deno_core::error::type_error("invalid input range"))? .as_ptr() as *mut _; let next_out = out .get_mut(out_off as usize..out_off as usize + out_len as usize) - .ok_or_else(|| type_error("invalid output range"))? + .ok_or_else(|| deno_core::error::type_error("invalid output range"))? .as_mut_ptr(); self.strm.avail_in = in_len; @@ -81,7 +81,10 @@ impl ZlibInner { Ok(()) } - fn do_write(&mut self, flush: Flush) -> Result<(), AnyError> { + fn do_write( + &mut self, + flush: Flush, + ) -> Result<(), deno_core::error::AnyError> { self.flush = flush; match self.mode { Mode::Deflate | Mode::Gzip | Mode::DeflateRaw => { @@ -127,7 +130,7 @@ impl ZlibInner { self.mode = Mode::Inflate; } } else if next_expected_header_byte.is_some() { - return Err(type_error( + return Err(deno_core::error::type_error( "invalid number of gzip magic number bytes read", )); } @@ -181,7 +184,7 @@ impl ZlibInner { Ok(()) } - fn init_stream(&mut self) -> Result<(), AnyError> { + fn init_stream(&mut self) -> Result<(), deno_core::error::AnyError> { match self.mode { Mode::Gzip | Mode::Gunzip => self.window_bits += 16, Mode::Unzip => self.window_bits += 32, @@ -199,7 +202,7 @@ impl ZlibInner { Mode::Inflate | Mode::Gunzip | Mode::InflateRaw | Mode::Unzip => { self.strm.inflate_init(self.window_bits) } - Mode::None => return Err(type_error("Unknown mode")), + Mode::None => return Err(deno_core::error::type_error("Unknown mode")), }; self.write_in_progress = false; @@ -208,7 +211,7 @@ impl ZlibInner { Ok(()) } - fn close(&mut self) -> Result { + fn close(&mut self) -> Result { if self.write_in_progress { self.pending_close = true; return Ok(false); @@ -222,10 +225,8 @@ impl ZlibInner { Ok(true) } - fn reset_stream(&mut self) -> Result<(), AnyError> { + fn reset_stream(&mut self) { self.err = self.strm.reset(self.mode); - - Ok(()) } } @@ -243,7 +244,7 @@ impl deno_core::Resource for Zlib { #[op2] #[cppgc] -pub fn op_zlib_new(#[smi] mode: i32) -> Result { +pub fn op_zlib_new(#[smi] mode: i32) -> Result { let mode = Mode::try_from(mode)?; let inner = ZlibInner { @@ -256,12 +257,20 @@ pub fn op_zlib_new(#[smi] mode: i32) -> Result { }) } +#[derive(Debug, thiserror::Error)] +pub enum ZlibError { + #[error("zlib not initialized")] + NotInitialized, + #[error(transparent)] + Mode(#[from] mode::ModeError), + #[error(transparent)] + Other(#[from] deno_core::error::AnyError), +} + #[op2(fast)] -pub fn op_zlib_close(#[cppgc] resource: &Zlib) -> Result<(), AnyError> { +pub fn op_zlib_close(#[cppgc] resource: &Zlib) -> Result<(), ZlibError> { let mut resource = resource.inner.borrow_mut(); - let zlib = resource - .as_mut() - .ok_or_else(|| type_error("zlib not initialized"))?; + let zlib = resource.as_mut().ok_or(ZlibError::NotInitialized)?; // If there is a pending write, defer the close until the write is done. zlib.close()?; @@ -282,11 +291,9 @@ pub fn op_zlib_write( #[smi] out_off: u32, #[smi] out_len: u32, #[buffer] result: &mut [u32], -) -> Result { +) -> Result { let mut zlib = resource.inner.borrow_mut(); - let zlib = zlib - .as_mut() - .ok_or_else(|| type_error("zlib not initialized"))?; + let zlib = zlib.as_mut().ok_or(ZlibError::NotInitialized)?; let flush = Flush::try_from(flush)?; zlib.start_write(input, in_off, in_len, out, out_off, out_len, flush)?; @@ -307,11 +314,9 @@ pub fn op_zlib_init( #[smi] mem_level: i32, #[smi] strategy: i32, #[buffer] dictionary: &[u8], -) -> Result { +) -> Result { let mut zlib = resource.inner.borrow_mut(); - let zlib = zlib - .as_mut() - .ok_or_else(|| type_error("zlib not initialized"))?; + let zlib = zlib.as_mut().ok_or(ZlibError::NotInitialized)?; check((8..=15).contains(&window_bits), "invalid windowBits")?; check((-1..=9).contains(&level), "invalid level")?; @@ -348,13 +353,11 @@ pub fn op_zlib_init( #[op2(fast)] #[smi] -pub fn op_zlib_reset(#[cppgc] resource: &Zlib) -> Result { +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"))?; + let zlib = zlib.as_mut().ok_or(ZlibError::NotInitialized)?; - zlib.reset_stream()?; + zlib.reset_stream(); Ok(zlib.err) } @@ -362,12 +365,10 @@ pub fn op_zlib_reset(#[cppgc] resource: &Zlib) -> Result { #[op2(fast)] pub fn op_zlib_close_if_pending( #[cppgc] resource: &Zlib, -) -> Result<(), AnyError> { +) -> Result<(), ZlibError> { let pending_close = { let mut zlib = resource.inner.borrow_mut(); - let zlib = zlib - .as_mut() - .ok_or_else(|| type_error("zlib not initialized"))?; + let zlib = zlib.as_mut().ok_or(ZlibError::NotInitialized)?; zlib.write_in_progress = false; zlib.pending_close @@ -381,6 +382,15 @@ pub fn op_zlib_close_if_pending( Ok(()) } +#[op2(fast)] +#[smi] +pub fn op_zlib_crc32(#[buffer] data: &[u8], #[smi] value: u32) -> u32 { + // SAFETY: `data` is a valid buffer. + unsafe { + zlib::crc32(value as c_ulong, data.as_ptr(), data.len() as u32) as u32 + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ext/node/ops/zlib/mode.rs b/ext/node/ops/zlib/mode.rs index 753300cc49..41565f9b11 100644 --- a/ext/node/ops/zlib/mode.rs +++ b/ext/node/ops/zlib/mode.rs @@ -1,19 +1,8 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -#[derive(Debug)] -pub enum Error { - BadArgument, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::BadArgument => write!(f, "bad argument"), - } - } -} - -impl std::error::Error for Error {} +#[derive(Debug, thiserror::Error)] +#[error("bad argument")] +pub struct ModeError; macro_rules! repr_i32 { ($(#[$meta:meta])* $vis:vis enum $name:ident { @@ -25,12 +14,12 @@ macro_rules! repr_i32 { } impl core::convert::TryFrom for $name { - type Error = Error; + type Error = ModeError; fn try_from(v: i32) -> Result { match v { $(x if x == $name::$vname as i32 => Ok($name::$vname),)* - _ => Err(Error::BadArgument), + _ => Err(ModeError), } } } diff --git a/ext/node/polyfills/01_require.js b/ext/node/polyfills/01_require.js index 5b0980c310..083d4e49be 100644 --- a/ext/node/polyfills/01_require.js +++ b/ext/node/polyfills/01_require.js @@ -11,6 +11,7 @@ import { op_require_can_parse_as_esm, op_require_init_paths, op_require_is_deno_dir_package, + op_require_is_maybe_cjs, op_require_is_request_relative, op_require_node_module_paths, op_require_package_imports_resolve, @@ -19,7 +20,6 @@ import { op_require_path_is_absolute, op_require_path_resolve, op_require_proxy_path, - op_require_read_closest_package_json, op_require_read_file, op_require_read_package_scope, op_require_real_path, @@ -523,17 +523,13 @@ function resolveExports( return; } - if (!parentPath) { - return false; - } - return op_require_resolve_exports( usesLocalNodeModulesDir, modulesPath, request, name, expansion, - parentPath, + parentPath ?? "", ) ?? false; } @@ -1064,23 +1060,22 @@ Module.prototype._compile = function (content, filename, format) { return result; }; -Module._extensions[".js"] = function (module, filename) { - const content = op_require_read_file(filename); +Module._extensions[".js"] = + Module._extensions[".ts"] = + Module._extensions[".jsx"] = + Module._extensions[".tsx"] = + function (module, filename) { + const content = op_require_read_file(filename); + const format = op_require_is_maybe_cjs(filename) ? undefined : "module"; + module._compile(content, filename, format); + }; - let format; - if (StringPrototypeEndsWith(filename, ".js")) { - const pkg = op_require_read_closest_package_json(filename); - if (pkg?.type === "module") { - format = "module"; - } else if (pkg?.type === "commonjs") { - format = "commonjs"; - } - } else if (StringPrototypeEndsWith(filename, ".cjs")) { - format = "commonjs"; - } - - module._compile(content, filename, format); -}; +Module._extensions[".cjs"] = + Module._extensions[".cts"] = + function (module, filename) { + const content = op_require_read_file(filename); + module._compile(content, filename, "commonjs"); + }; function loadESMFromCJS(module, filename, code) { const namespace = op_import_sync( @@ -1091,7 +1086,10 @@ function loadESMFromCJS(module, filename, code) { module.exports = namespace; } -Module._extensions[".mjs"] = function (module, filename) { +Module._extensions[".mjs"] = Module._extensions[".mts"] = function ( + module, + filename, +) { loadESMFromCJS(module, filename); }; @@ -1212,6 +1210,24 @@ function isBuiltin(moduleName) { !StringPrototypeStartsWith(moduleName, "internal/"); } +function getBuiltinModule(id) { + if (!isBuiltin(id)) { + return undefined; + } + + if (StringPrototypeStartsWith(id, "node:")) { + // Slice 'node:' prefix + id = StringPrototypeSlice(id, 5); + } + + const mod = loadNativeModule(id, id); + if (mod) { + return mod.exports; + } + + return undefined; +} + Module.isBuiltin = isBuiltin; Module.createRequire = createRequire; @@ -1291,6 +1307,8 @@ export function findSourceMap(_path) { return undefined; } +Module.findSourceMap = findSourceMap; + /** * @param {string | URL} _specifier * @param {string | URL} _parentUrl @@ -1304,7 +1322,7 @@ export function register(_specifier, _parentUrl, _options) { return undefined; } -export { builtinModules, createRequire, isBuiltin, Module }; +export { builtinModules, createRequire, getBuiltinModule, isBuiltin, Module }; export const _cache = Module._cache; export const _extensions = Module._extensions; export const _findPath = Module._findPath; diff --git a/ext/node/polyfills/_fs/_fs_common.ts b/ext/node/polyfills/_fs/_fs_common.ts index ac0bf5a551..a29548bb36 100644 --- a/ext/node/polyfills/_fs/_fs_common.ts +++ b/ext/node/polyfills/_fs/_fs_common.ts @@ -20,6 +20,7 @@ import { notImplemented, TextEncodings, } from "ext:deno_node/_utils.ts"; +import { type Buffer } from "node:buffer"; export type CallbackWithError = (err: ErrnoException | null) => void; diff --git a/ext/node/polyfills/_fs/_fs_copy.ts b/ext/node/polyfills/_fs/_fs_copy.ts index 2f8ddf4fcd..0434bff4d1 100644 --- a/ext/node/polyfills/_fs/_fs_copy.ts +++ b/ext/node/polyfills/_fs/_fs_copy.ts @@ -53,8 +53,9 @@ export function copyFile( }, (e) => { if (e instanceof Deno.errors.NotFound) { Deno.copyFile(srcStr, destStr).then(() => cb(null), cb); + } else { + cb(e); } - cb(e); }); } else { Deno.copyFile(srcStr, destStr).then(() => cb(null), cb); @@ -83,8 +84,9 @@ export function copyFileSync( } catch (e) { if (e instanceof Deno.errors.NotFound) { Deno.copyFileSync(srcStr, destStr); + } else { + throw e; } - throw e; } } else { Deno.copyFileSync(srcStr, destStr); diff --git a/ext/node/polyfills/_fs/_fs_open.ts b/ext/node/polyfills/_fs/_fs_open.ts index 8bd989790b..31ca4bb619 100644 --- a/ext/node/polyfills/_fs/_fs_open.ts +++ b/ext/node/polyfills/_fs/_fs_open.ts @@ -147,8 +147,8 @@ export function open( export function openPromise( path: string | Buffer | URL, - flags?: openFlags = "r", - mode? = 0o666, + flags: openFlags = "r", + mode = 0o666, ): Promise { return new Promise((resolve, reject) => { open(path, flags, mode, (err, fd) => { diff --git a/ext/node/polyfills/_fs/_fs_read.ts b/ext/node/polyfills/_fs/_fs_read.ts index dec3a8bbdf..df4f5e375d 100644 --- a/ext/node/polyfills/_fs/_fs_read.ts +++ b/ext/node/polyfills/_fs/_fs_read.ts @@ -173,7 +173,7 @@ export function readSync( validateBuffer(buffer); if (length == null) { - length = 0; + length = buffer.byteLength; } if (typeof offsetOrOpt === "number") { diff --git a/ext/node/polyfills/_fs/_fs_readFile.ts b/ext/node/polyfills/_fs/_fs_readFile.ts index 0f05ee1678..cf7e0305d8 100644 --- a/ext/node/polyfills/_fs/_fs_readFile.ts +++ b/ext/node/polyfills/_fs/_fs_readFile.ts @@ -19,6 +19,7 @@ import { TextEncodings, } from "ext:deno_node/_utils.ts"; import { FsFile } from "ext:deno_fs/30_fs.js"; +import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; function maybeDecode(data: Uint8Array, encoding: TextEncodings): string; function maybeDecode( @@ -87,7 +88,7 @@ export function readFile( } const buffer = maybeDecode(data, encoding); (cb as BinaryCallback)(null, buffer); - }, (err) => cb && cb(err)); + }, (err) => cb && cb(denoErrorToNodeError(err))); } } @@ -117,7 +118,12 @@ export function readFileSync( opt?: FileOptionsArgument, ): string | Buffer { path = path instanceof URL ? pathFromURL(path) : path; - const data = Deno.readFileSync(path); + let data; + try { + data = Deno.readFileSync(path); + } catch (err) { + throw denoErrorToNodeError(err); + } const encoding = getEncoding(opt); if (encoding && encoding !== "binary") { const text = maybeDecode(data, encoding); diff --git a/ext/node/polyfills/_fs/_fs_readlink.ts b/ext/node/polyfills/_fs/_fs_readlink.ts index 5f23127982..08bea843fa 100644 --- a/ext/node/polyfills/_fs/_fs_readlink.ts +++ b/ext/node/polyfills/_fs/_fs_readlink.ts @@ -4,13 +4,10 @@ // deno-lint-ignore-file prefer-primordials import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; -import { - intoCallbackAPIWithIntercept, - MaybeEmpty, - notImplemented, -} from "ext:deno_node/_utils.ts"; +import { MaybeEmpty, notImplemented } from "ext:deno_node/_utils.ts"; import { pathFromURL } from "ext:deno_web/00_infra.js"; import { promisify } from "ext:deno_node/internal/util.mjs"; +import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; type ReadlinkCallback = ( err: MaybeEmpty, @@ -69,12 +66,17 @@ export function readlink( const encoding = getEncoding(optOrCallback); - intoCallbackAPIWithIntercept( - Deno.readLink, - (data: string): string | Uint8Array => maybeEncode(data, encoding), - cb, - path, - ); + Deno.readLink(path).then((data: string) => { + const res = maybeEncode(data, encoding); + if (cb) cb(null, res); + }, (err: Error) => { + if (cb) { + (cb as (e: Error) => void)(denoErrorToNodeError(err, { + syscall: "readlink", + path, + })); + } + }); } export const readlinkPromise = promisify(readlink) as ( @@ -88,5 +90,12 @@ export function readlinkSync( ): string | Uint8Array { path = path instanceof URL ? pathFromURL(path) : path; - return maybeEncode(Deno.readLinkSync(path), getEncoding(opt)); + try { + return maybeEncode(Deno.readLinkSync(path), getEncoding(opt)); + } catch (error) { + throw denoErrorToNodeError(error, { + syscall: "readlink", + path, + }); + } } diff --git a/ext/node/polyfills/_fs/_fs_readv.ts b/ext/node/polyfills/_fs/_fs_readv.ts index 384f5e319a..2259f029ae 100644 --- a/ext/node/polyfills/_fs/_fs_readv.ts +++ b/ext/node/polyfills/_fs/_fs_readv.ts @@ -15,6 +15,7 @@ import { maybeCallback } from "ext:deno_node/_fs/_fs_common.ts"; import { validateInteger } from "ext:deno_node/internal/validators.mjs"; import * as io from "ext:deno_io/12_io.js"; import { op_fs_seek_async, op_fs_seek_sync } from "ext:core/ops"; +import process from "node:process"; type Callback = ( err: ErrnoException | null, diff --git a/ext/node/polyfills/_fs/_fs_stat.ts b/ext/node/polyfills/_fs/_fs_stat.ts index c4ed82d577..507cb05eaf 100644 --- a/ext/node/polyfills/_fs/_fs_stat.ts +++ b/ext/node/polyfills/_fs/_fs_stat.ts @@ -290,8 +290,8 @@ export function convertFileInfoToStats(origin: Deno.FileInfo): Stats { isFIFO: () => false, isCharacterDevice: () => false, isSocket: () => false, - ctime: origin.mtime, - ctimeMs: origin.mtime?.getTime() || null, + ctime: origin.ctime, + ctimeMs: origin.ctime?.getTime() || null, }); return stats; @@ -336,9 +336,9 @@ export function convertFileInfoToBigIntStats( isFIFO: () => false, isCharacterDevice: () => false, isSocket: () => false, - ctime: origin.mtime, - ctimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null, - ctimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null, + ctime: origin.ctime, + ctimeMs: origin.ctime ? BigInt(origin.ctime.getTime()) : null, + ctimeNs: origin.ctime ? BigInt(origin.ctime.getTime()) * 1000000n : null, }); return stats; } @@ -383,7 +383,10 @@ export function stat( Deno.stat(path).then( (stat) => callback(null, CFISBIS(stat, options.bigint)), - (err) => callback(denoErrorToNodeError(err, { syscall: "stat" })), + (err) => + callback( + denoErrorToNodeError(err, { syscall: "stat", path: getPathname(path) }), + ), ); } @@ -417,9 +420,16 @@ export function statSync( return; } if (err instanceof Error) { - throw denoErrorToNodeError(err, { syscall: "stat" }); + throw denoErrorToNodeError(err, { + syscall: "stat", + path: getPathname(path), + }); } else { throw err; } } } + +function getPathname(path: string | URL) { + return typeof path === "string" ? path : path.pathname; +} diff --git a/ext/node/polyfills/_next_tick.ts b/ext/node/polyfills/_next_tick.ts index 5ee27728d0..af306a29c8 100644 --- a/ext/node/polyfills/_next_tick.ts +++ b/ext/node/polyfills/_next_tick.ts @@ -62,6 +62,8 @@ export function processTicksAndRejections() { callback(...args); } } + } catch (e) { + reportError(e); } finally { // FIXME(bartlomieju): Deno currently doesn't support async hooks // if (destroyHooksExist()) @@ -87,8 +89,7 @@ export function runNextTicks() { // runMicrotasks(); // if (!hasTickScheduled() && !hasRejectionToWarn()) // return; - if (!core.hasTickScheduled()) { - core.runMicrotasks(); + if (queue.isEmpty() || !core.hasTickScheduled()) { return true; } diff --git a/ext/node/polyfills/_process/streams.mjs b/ext/node/polyfills/_process/streams.mjs index 19c1c9c182..3573956c9d 100644 --- a/ext/node/polyfills/_process/streams.mjs +++ b/ext/node/polyfills/_process/streams.mjs @@ -63,13 +63,22 @@ export function createWritableStdioStream(writer, name, warmup = false) { stream.destroySoon = stream.destroy; stream._isStdio = true; stream.once("close", () => writer?.close()); + + // We cannot call `writer?.isTerminal()` eagerly here + let getIsTTY = () => writer?.isTerminal(); + const getColumns = () => + stream._columns || + (writer?.isTerminal() ? Deno.consoleSize?.().columns : undefined); + ObjectDefineProperties(stream, { columns: { __proto__: null, enumerable: true, configurable: true, - get: () => - writer?.isTerminal() ? Deno.consoleSize?.().columns : undefined, + get: () => getColumns(), + set: (value) => { + stream._columns = value; + }, }, rows: { __proto__: null, @@ -81,7 +90,11 @@ export function createWritableStdioStream(writer, name, warmup = false) { __proto__: null, enumerable: true, configurable: true, - get: () => writer?.isTerminal(), + // Allow users to overwrite it + get: () => getIsTTY(), + set: (value) => { + getIsTTY = () => value; + }, }, getWindowSize: { __proto__: null, diff --git a/ext/node/polyfills/_tls_wrap.ts b/ext/node/polyfills/_tls_wrap.ts index a614b45df0..e36fc637e7 100644 --- a/ext/node/polyfills/_tls_wrap.ts +++ b/ext/node/polyfills/_tls_wrap.ts @@ -68,6 +68,7 @@ export class TLSSocket extends net.Socket { secureConnecting: boolean; _SNICallback: any; servername: string | null; + alpnProtocol: string | boolean | null; alpnProtocols: string[] | null; authorized: boolean; authorizationError: any; @@ -114,6 +115,7 @@ export class TLSSocket extends net.Socket { this.secureConnecting = true; this._SNICallback = null; this.servername = null; + this.alpnProtocol = null; this.alpnProtocols = tlsOptions.ALPNProtocols; this.authorized = false; this.authorizationError = null; @@ -151,10 +153,21 @@ export class TLSSocket extends net.Socket { handle.afterConnect = async (req: any, status: number) => { try { const conn = await Deno.startTls(handle[kStreamBaseField], options); + try { + const hs = await conn.handshake(); + if (hs.alpnProtocol) { + tlssock.alpnProtocol = hs.alpnProtocol; + } else { + tlssock.alpnProtocol = false; + } + } catch { + // Don't interrupt "secure" event to let the first read/write + // operation emit the error. + } handle[kStreamBaseField] = conn; tlssock.emit("secure"); tlssock.removeListener("end", onConnectEnd); - } catch { + } catch (_) { // TODO(kt3k): Handle this } return afterConnect.call(handle, req, status); @@ -269,6 +282,7 @@ export class ServerImpl extends EventEmitter { // Creates TCP handle and socket directly from Deno.TlsConn. // This works as TLS socket. We don't use TLSSocket class for doing // this because Deno.startTls only supports client side tcp connection. + // TODO(@satyarohith): set TLSSocket.alpnProtocol when we use TLSSocket class. const handle = new TCP(TCPConstants.SOCKET, await listener.accept()); const socket = new net.Socket({ handle }); this.emit("secureConnection", socket); diff --git a/ext/node/polyfills/_utils.ts b/ext/node/polyfills/_utils.ts index b50c113e14..79d84e00f0 100644 --- a/ext/node/polyfills/_utils.ts +++ b/ext/node/polyfills/_utils.ts @@ -17,6 +17,7 @@ const { 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"; +import { ERR_NOT_IMPLEMENTED } from "ext:deno_node/internal/errors.ts"; export type BinaryEncodings = "binary"; @@ -34,8 +35,7 @@ export type TextEncodings = export type Encodings = BinaryEncodings | TextEncodings; export function notImplemented(msg: string): never { - const message = msg ? `Not implemented: ${msg}` : "Not implemented"; - throw new Error(message); + throw new ERR_NOT_IMPLEMENTED(msg); } export function warnNotImplemented(msg?: string) { diff --git a/ext/node/polyfills/_zlib.mjs b/ext/node/polyfills/_zlib.mjs index 851bd602f2..07fc440ef5 100644 --- a/ext/node/polyfills/_zlib.mjs +++ b/ext/node/polyfills/_zlib.mjs @@ -14,6 +14,7 @@ import { nextTick } from "ext:deno_node/_next_tick.ts"; import { isAnyArrayBuffer, isArrayBufferView, + isUint8Array, } from "ext:deno_node/internal/util/types.ts"; var kRangeErrorMessage = "Cannot create final Buffer. It would be larger " + @@ -158,6 +159,12 @@ export const inflateRawSync = function (buffer, opts) { function sanitizeInput(input) { if (typeof input === "string") input = Buffer.from(input); + if (isArrayBufferView(input) && !isUint8Array(input)) { + input = Buffer.from(input.buffer, input.byteOffset, input.byteLength); + } else if (isAnyArrayBuffer(input)) { + input = Buffer.from(input); + } + if ( !Buffer.isBuffer(input) && (input.buffer && !input.buffer.constructor === ArrayBuffer) diff --git a/ext/node/polyfills/buffer.ts b/ext/node/polyfills/buffer.ts index 8986cf53d7..efe3b07a97 100644 --- a/ext/node/polyfills/buffer.ts +++ b/ext/node/polyfills/buffer.ts @@ -13,4 +13,5 @@ export { kMaxLength, kStringMaxLength, SlowBuffer, + transcode, } from "ext:deno_node/internal/buffer.mjs"; diff --git a/ext/node/polyfills/child_process.ts b/ext/node/polyfills/child_process.ts index c37dfc410a..eda718ff34 100644 --- a/ext/node/polyfills/child_process.ts +++ b/ext/node/polyfills/child_process.ts @@ -132,6 +132,8 @@ export function fork( rm = 2; } execArgv.splice(index, rm); + } else if (flag.startsWith("--no-warnings")) { + execArgv[index] = "--quiet"; } else { index++; } diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts index f3f6f86ed8..9a920adeee 100644 --- a/ext/node/polyfills/http.ts +++ b/ext/node/polyfills/http.ts @@ -34,6 +34,7 @@ import { finished, Readable as NodeReadable, Writable as NodeWritable, + WritableOptions as NodeWritableOptions, } from "node:stream"; import { kUniqueHeaders, @@ -66,12 +67,13 @@ import { headersEntries } from "ext:deno_fetch/20_headers.js"; import { timerId } from "ext:deno_web/03_abort_signal.js"; import { clearTimeout as webClearTimeout } from "ext:deno_web/02_timers.js"; import { resourceForReadableStream } from "ext:deno_web/06_streams.js"; -import { TcpConn } from "ext:deno_net/01_net.js"; +import { UpgradedConn } from "ext:deno_net/01_net.js"; import { STATUS_CODES } from "node:_http_server"; import { methods as METHODS } from "node:_http_common"; +import { deprecate } from "node:util"; const { internalRidSymbol } = core; -const { ArrayIsArray } = primordials; +const { ArrayIsArray, StringPrototypeToLowerCase } = primordials; type Chunk = string | Buffer | Uint8Array; @@ -516,7 +518,7 @@ class ClientRequest extends OutgoingMessage { ); assert(typeof res.remoteAddrIp !== "undefined"); assert(typeof res.remoteAddrIp !== "undefined"); - const conn = new TcpConn( + const conn = new UpgradedConn( upgradeRid, { transport: "tcp", @@ -1183,49 +1185,95 @@ function onError(self, error, cb) { } } -export class ServerResponse extends NodeWritable { - statusCode = 200; - statusMessage?: string = undefined; - #headers: Record = { __proto__: null }; - #hasNonStringHeaders: boolean = false; - #readable: ReadableStream; - override writable = true; - // used by `npm:on-finished` - finished = false; - headersSent = false; - #resolve: (value: Response | PromiseLike) => void; +export type ServerResponse = { + statusCode: number; + statusMessage?: string; + + _headers: Record; + _hasNonStringHeaders: boolean; + + _readable: ReadableStream; + finished: boolean; + headersSent: boolean; + _resolve: (value: Response | PromiseLike) => void; // deno-lint-ignore no-explicit-any - #socketOverride: any | null = null; + _socketOverride: any | null; + // deno-lint-ignore no-explicit-any + socket: any | null; - static #enqueue(controller: ReadableStreamDefaultController, chunk: 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. - } - } + setHeader(name: string, value: string | string[]): void; + appendHeader(name: string, value: string | string[]): void; + getHeader(name: string): string | string[]; + removeHeader(name: string): void; + getHeaderNames(): string[]; + getHeaders(): Record; + hasHeader(name: string): boolean; - /** Returns true if the response body should be null with the given - * http status code */ - static #bodyShouldBeNull(status: number) { - return status === 101 || status === 204 || status === 205 || status === 304; - } + writeHead( + status: number, + statusMessage?: string, + headers?: + | Record + | Array<[string, string]>, + ): void; + writeHead( + status: number, + headers?: + | Record + | Array<[string, string]>, + ): void; - constructor( + _ensureHeaders(singleChunk?: Chunk): void; + + respond(final: boolean, singleChunk?: Chunk): void; + // deno-lint-ignore no-explicit-any + end(chunk?: any, encoding?: any, cb?: any): void; + + flushHeaders(): void; + _implicitHeader(): void; + + // Undocumented field used by `npm:light-my-request`. + _header: string; + + assignSocket(socket): void; + detachSocket(socket): void; +} & { -readonly [K in keyof NodeWritable]: NodeWritable[K] }; + +type ServerResponseStatic = { + new ( resolve: (value: Response | PromiseLike) => void, socket: FakeSocket, - ) { - let controller: ReadableByteStreamController; - const readable = new ReadableStream({ - start(c) { - controller = c as ReadableByteStreamController; - }, - }); - super({ + ): ServerResponse; + _enqueue(controller: ReadableStreamDefaultController, chunk: Chunk): void; + _bodyShouldBeNull(statusCode: number): boolean; +}; + +export const ServerResponse = function ( + this: ServerResponse, + resolve: (value: Response | PromiseLike) => void, + socket: FakeSocket, +) { + this.statusCode = 200; + this.statusMessage = undefined; + this._headers = { __proto__: null }; + this._hasNonStringHeaders = false; + this.writable = true; + + // used by `npm:on-finished` + this.finished = false; + this.headersSent = false; + this._socketOverride = null; + + let controller: ReadableByteStreamController; + const readable = new ReadableStream({ + start(c) { + controller = c as ReadableByteStreamController; + }, + }); + + NodeWritable.call( + this, + { autoDestroy: true, defaultEncoding: "utf-8", emitClose: true, @@ -1234,16 +1282,16 @@ export class ServerResponse extends NodeWritable { 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); + if (this._socketOverride && this._socketOverride.writable) { + this._socketOverride.write(chunk, encoding); return cb(); } if (!this.headersSent) { - ServerResponse.#enqueue(controller, chunk); + ServerResponse._enqueue(controller, chunk); this.respond(false); return cb(); } - ServerResponse.#enqueue(controller, chunk); + ServerResponse._enqueue(controller, chunk); return cb(); }, final: (cb) => { @@ -1259,192 +1307,269 @@ export class ServerResponse extends NodeWritable { } return cb(null); }, - }); - this.#readable = readable; - this.#resolve = resolve; - this.socket = socket; - } + } satisfies NodeWritableOptions, + ); - setHeader(name: string, value: string | string[]) { - if (Array.isArray(value)) { - this.#hasNonStringHeaders = true; - } - this.#headers[name] = value; - return this; - } + this._readable = readable; + this._resolve = resolve; + this.socket = socket; - appendHeader(name: string, value: string | string[]) { - if (this.#headers[name] === undefined) { - if (Array.isArray(value)) this.#hasNonStringHeaders = true; - this.#headers[name] = value; + this._header = ""; +} as unknown as ServerResponseStatic; + +Object.setPrototypeOf(ServerResponse.prototype, NodeWritable.prototype); +Object.setPrototypeOf(ServerResponse, NodeWritable); + +ServerResponse._enqueue = function ( + this: ServerResponse, + controller: ReadableStreamDefaultController, + chunk: Chunk, +) { + try { + if (typeof chunk === "string") { + controller.enqueue(ENCODER.encode(chunk)); } else { - this.#hasNonStringHeaders = true; - if (!Array.isArray(this.#headers[name])) { - this.#headers[name] = [this.#headers[name]]; + controller.enqueue(chunk); + } + } catch (_) { + // The stream might have been closed. Ignore the error. + } +}; + +/** Returns true if the response body should be null with the given + * http status code */ +ServerResponse._bodyShouldBeNull = function ( + this: ServerResponse, + status: number, +) { + return status === 101 || status === 204 || status === 205 || status === 304; +}; + +ServerResponse.prototype.setHeader = function ( + this: ServerResponse, + name: string, + value: string | string[], +) { + if (Array.isArray(value)) { + this._hasNonStringHeaders = true; + } + this._headers[StringPrototypeToLowerCase(name)] = value; + return this; +}; + +ServerResponse.prototype.appendHeader = function ( + this: ServerResponse, + name: string, + value: string | string[], +) { + const key = StringPrototypeToLowerCase(name); + if (this._headers[key] === undefined) { + if (Array.isArray(value)) this._hasNonStringHeaders = true; + this._headers[key] = value; + } else { + this._hasNonStringHeaders = true; + if (!Array.isArray(this._headers[key])) { + this._headers[key] = [this._headers[key]]; + } + const header = this._headers[key]; + if (Array.isArray(value)) { + header.push(...value); + } else { + header.push(value); + } + } + return this; +}; + +ServerResponse.prototype.getHeader = function ( + this: ServerResponse, + name: string, +) { + return this._headers[StringPrototypeToLowerCase(name)]; +}; + +ServerResponse.prototype.removeHeader = function ( + this: ServerResponse, + name: string, +) { + delete this._headers[StringPrototypeToLowerCase(name)]; +}; + +ServerResponse.prototype.getHeaderNames = function (this: ServerResponse) { + return Object.keys(this._headers); +}; + +ServerResponse.prototype.getHeaders = function ( + this: ServerResponse, +): Record { + return { __proto__: null, ...this._headers }; +}; + +ServerResponse.prototype.hasHeader = function ( + this: ServerResponse, + name: string, +) { + return Object.hasOwn(this._headers, name); +}; + +ServerResponse.prototype.writeHead = function ( + this: ServerResponse, + status: number, + statusMessageOrHeaders?: + | string + | Record + | Array<[string, string]>, + maybeHeaders?: + | Record + | Array<[string, string]>, +) { + this.statusCode = status; + + 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]); } - const header = this.#headers[name]; - if (Array.isArray(value)) { - header.push(...value); - } else { - header.push(value); + } else { + headers = headers as Record; + for (const k in headers) { + if (Object.hasOwn(headers, k)) { + this.setHeader(k, headers[k]); + } } } - return this; } - getHeader(name: string) { - return this.#headers[name]; - } - removeHeader(name: string) { - delete this.#headers[name]; - } - getHeaderNames() { - return Object.keys(this.#headers); - } - getHeaders(): Record { - // @ts-ignore Ignore null __proto__ - return { __proto__: null, ...this.#headers }; - } - hasHeader(name: string) { - return Object.hasOwn(this.#headers, name); - } + return this; +}; - 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; +ServerResponse.prototype._ensureHeaders = function ( + this: ServerResponse, + singleChunk?: Chunk, +) { + if (this.statusCode === 200 && this.statusMessage === undefined) { + this.statusMessage = "OK"; + } + if (typeof singleChunk === "string" && !this.hasHeader("content-type")) { + this.setHeader("content-type", "text/plain;charset=UTF-8"); + } +}; - 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]); +ServerResponse.prototype.respond = function ( + this: ServerResponse, + final: boolean, + singleChunk?: Chunk, +) { + this.headersSent = true; + this._ensureHeaders(singleChunk); + let body = singleChunk ?? (final ? null : this._readable); + 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 = headers as Record; - for (const k in headers) { - if (Object.hasOwn(headers, k)) { - this.setHeader(k, headers[k]); - } - } + headers.push([key, entry]); } } - - return this; - } - - #ensureHeaders(singleChunk?: Chunk) { - if (this.statusCode === 200 && this.statusMessage === undefined) { - this.statusMessage = "OK"; - } - if ( - typeof singleChunk === "string" && - !this.hasHeader("content-type") - ) { - this.setHeader("content-type", "text/plain;charset=UTF-8"); - } - } - - respond(final: boolean, singleChunk?: Chunk) { - this.headersSent = true; - this.#ensureHeaders(singleChunk); - let body = singleChunk ?? (final ? null : this.#readable); - 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, - status: this.statusCode, - statusText: this.statusMessage, - }), - ); } + this._resolve( + new Response(body, { + headers, + status: this.statusCode, + statusText: this.statusMessage, + }), + ); +}; +ServerResponse.prototype.end = function ( + this: ServerResponse, // deno-lint-ignore no-explicit-any - override end(chunk?: any, encoding?: any, cb?: any): this { - this.finished = true; - 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["content-length"] = "0"; - delete this.#headers["transfer-encoding"]; - } - - // @ts-expect-error The signature for cb is stricter than the one implemented here - return super.end(chunk, encoding, cb); + chunk?: any, + // deno-lint-ignore no-explicit-any + encoding?: any, + // deno-lint-ignore no-explicit-any + cb?: any, +) { + this.finished = true; + 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["content-length"] = "0"; + delete this._headers["transfer-encoding"]; } - flushHeaders() { - // no-op - } + // @ts-expect-error The signature for cb is stricter than the one implemented here + NodeWritable.prototype.end.call(this, chunk, encoding, cb); +}; - // Undocumented API used by `npm:compression`. - _implicitHeader() { - this.writeHead(this.statusCode); - } +ServerResponse.prototype.flushHeaders = function (this: ServerResponse) { + // no-op +}; - assignSocket(socket) { - if (socket._httpMessage) { - throw new ERR_HTTP_SOCKET_ASSIGNED(); - } - socket._httpMessage = this; - this.#socketOverride = socket; - } +// Undocumented API used by `npm:compression`. +ServerResponse.prototype._implicitHeader = function (this: ServerResponse) { + this.writeHead(this.statusCode); +}; - detachSocket(socket) { - assert(socket._httpMessage === this); - socket._httpMessage = null; - this.#socketOverride = null; +ServerResponse.prototype.assignSocket = function ( + this: ServerResponse, + socket, +) { + if (socket._httpMessage) { + throw new ERR_HTTP_SOCKET_ASSIGNED(); } -} + socket._httpMessage = this; + this._socketOverride = socket; +}; + +ServerResponse.prototype.detachSocket = function ( + this: ServerResponse, + socket, +) { + assert(socket._httpMessage === this); + socket._httpMessage = null; + this._socketOverride = null; +}; + +Object.defineProperty(ServerResponse.prototype, "connection", { + get: deprecate( + function (this: ServerResponse) { + return this._socketOverride; + }, + "ServerResponse.prototype.connection is deprecated", + "DEP0066", + ), + set: deprecate( + // deno-lint-ignore no-explicit-any + function (this: ServerResponse, socket: any) { + this._socketOverride = socket; + }, + "ServerResponse.prototype.connection is deprecated", + "DEP0066", + ), +}); // TODO(@AaronO): optimize export class IncomingMessageForServer extends NodeReadable { @@ -1677,6 +1802,8 @@ export class ServerImpl extends EventEmitter { this.#server.ref(); } this.#unref = false; + + return this; } unref() { @@ -1684,6 +1811,8 @@ export class ServerImpl extends EventEmitter { this.#server.unref(); } this.#unref = true; + + return this; } close(cb?: (err?: Error) => void): this { diff --git a/ext/node/polyfills/http2.ts b/ext/node/polyfills/http2.ts index a9ced2bd9e..dc2379aebb 100644 --- a/ext/node/polyfills/http2.ts +++ b/ext/node/polyfills/http2.ts @@ -882,6 +882,7 @@ export class ClientHttp2Stream extends Duplex { trailersReady: false, endAfterHeaders: false, shutdownWritableCalled: false, + serverEndedCall: false, }; this[kDenoResponse] = undefined; this[kDenoRid] = undefined; @@ -1109,7 +1110,9 @@ export class ClientHttp2Stream extends Duplex { } debugHttp2(">>> chunk", chunk, finished, this[kDenoResponse].bodyRid); - if (chunk === null) { + if (finished || chunk === null) { + this[kState].serverEndedCall = true; + const trailerList = await op_http2_client_get_response_trailers( this[kDenoResponse].bodyRid, ); @@ -1237,7 +1240,9 @@ export class ClientHttp2Stream extends Duplex { this[kSession] = undefined; session[kMaybeDestroy](); - callback(err); + if (callback) { + callback(err); + } } [kMaybeDestroy](code = constants.NGHTTP2_NO_ERROR) { @@ -1280,6 +1285,9 @@ function shutdownWritable(stream, callback, streamRid) { if (state.flags & STREAM_FLAGS_HAS_TRAILERS) { onStreamTrailers(stream); callback(); + } else if (state.serverEndedCall) { + debugHttp2(">>> stream finished"); + callback(); } else { op_http2_client_send_data(streamRid, new Uint8Array(), true) .then(() => { diff --git a/ext/node/polyfills/inspector.js b/ext/node/polyfills/inspector.js new file mode 100644 index 0000000000..7eb15ce917 --- /dev/null +++ b/ext/node/polyfills/inspector.js @@ -0,0 +1,210 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +import process from "node:process"; +import { EventEmitter } from "node:events"; +import { primordials } from "ext:core/mod.js"; +import { + op_get_extras_binding_object, + op_inspector_close, + op_inspector_connect, + op_inspector_disconnect, + op_inspector_dispatch, + op_inspector_emit_protocol_event, + op_inspector_enabled, + op_inspector_open, + op_inspector_url, + op_inspector_wait, +} from "ext:core/ops"; +import { + isUint32, + validateFunction, + validateInt32, + validateObject, + validateString, +} from "ext:deno_node/internal/validators.mjs"; +import { + ERR_INSPECTOR_ALREADY_ACTIVATED, + ERR_INSPECTOR_ALREADY_CONNECTED, + ERR_INSPECTOR_CLOSED, + ERR_INSPECTOR_COMMAND, + ERR_INSPECTOR_NOT_ACTIVE, + ERR_INSPECTOR_NOT_CONNECTED, + ERR_INSPECTOR_NOT_WORKER, +} from "ext:deno_node/internal/errors.ts"; + +const { + SymbolDispose, + JSONParse, + JSONStringify, + SafeMap, +} = primordials; + +class Session extends EventEmitter { + #connection = null; + #nextId = 1; + #messageCallbacks = new SafeMap(); + + connect() { + if (this.#connection) { + throw new ERR_INSPECTOR_ALREADY_CONNECTED("The inspector session"); + } + this.#connection = op_inspector_connect(false, (m) => this.#onMessage(m)); + } + + connectToMainThread() { + if (isMainThread) { + throw new ERR_INSPECTOR_NOT_WORKER(); + } + if (this.#connection) { + throw new ERR_INSPECTOR_ALREADY_CONNECTED("The inspector session"); + } + this.#connection = op_inspector_connect(true, (m) => this.#onMessage(m)); + } + + #onMessage(message) { + const parsed = JSONParse(message); + try { + if (parsed.id) { + const callback = this.#messageCallbacks.get(parsed.id); + this.#messageCallbacks.delete(parsed.id); + if (callback) { + if (parsed.error) { + return callback( + new ERR_INSPECTOR_COMMAND( + parsed.error.code, + parsed.error.message, + ), + ); + } + + callback(null, parsed.result); + } + } else { + this.emit(parsed.method, parsed); + this.emit("inspectorNotification", parsed); + } + } catch (error) { + process.emitWarning(error); + } + } + + post(method, params, callback) { + validateString(method, "method"); + if (!callback && typeof params === "function") { + callback = params; + params = null; + } + if (params) { + validateObject(params, "params"); + } + if (callback) { + validateFunction(callback, "callback"); + } + + if (!this.#connection) { + throw new ERR_INSPECTOR_NOT_CONNECTED(); + } + const id = this.#nextId++; + const message = { id, method }; + if (params) { + message.params = params; + } + if (callback) { + this.#messageCallbacks.set(id, callback); + } + op_inspector_dispatch(this.#connection, JSONStringify(message)); + } + + disconnect() { + if (!this.#connection) { + return; + } + op_inspector_disconnect(this.#connection); + this.#connection = null; + // deno-lint-ignore prefer-primordials + for (const callback of this.#messageCallbacks.values()) { + process.nextTick(callback, new ERR_INSPECTOR_CLOSED()); + } + this.#messageCallbacks.clear(); + this.#nextId = 1; + } +} + +function open(port, host, wait) { + if (op_inspector_enabled()) { + throw new ERR_INSPECTOR_ALREADY_ACTIVATED(); + } + // inspectorOpen() currently does not typecheck its arguments and adding + // such checks would be a potentially breaking change. However, the native + // open() function requires the port to fit into a 16-bit unsigned integer, + // causing an integer overflow otherwise, so we at least need to prevent that. + if (isUint32(port)) { + validateInt32(port, "port", 0, 65535); + } else { + // equiv of handling args[0]->IsUint32() + port = undefined; + } + if (typeof host !== "string") { + // equiv of handling args[1]->IsString() + host = undefined; + } + op_inspector_open(port, host); + if (wait) { + op_inspector_wait(); + } + + return { + __proto__: null, + [SymbolDispose]() { + _debugEnd(); + }, + }; +} + +function close() { + op_inspector_close(); +} + +function url() { + return op_inspector_url(); +} + +function waitForDebugger() { + if (!op_inspector_wait()) { + throw new ERR_INSPECTOR_NOT_ACTIVE(); + } +} + +function broadcastToFrontend(eventName, params) { + validateString(eventName, "eventName"); + if (params) { + validateObject(params, "params"); + } + op_inspector_emit_protocol_event(eventName, JSONStringify(params ?? {})); +} + +const Network = { + requestWillBeSent: (params) => + broadcastToFrontend("Network.requestWillBeSent", params), + responseReceived: (params) => + broadcastToFrontend("Network.responseReceived", params), + loadingFinished: (params) => + broadcastToFrontend("Network.loadingFinished", params), + loadingFailed: (params) => + broadcastToFrontend("Network.loadingFailed", params), +}; + +const console = op_get_extras_binding_object().console; + +export { close, console, Network, open, Session, url, waitForDebugger }; + +export default { + open, + close, + url, + waitForDebugger, + console, + Session, + Network, +}; diff --git a/ext/node/polyfills/inspector.ts b/ext/node/polyfills/inspector.ts deleted file mode 100644 index 9de86ab14f..0000000000 --- a/ext/node/polyfills/inspector.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// Copyright Joyent and Node contributors. All rights reserved. MIT license. - -import { EventEmitter } from "node:events"; -import { notImplemented } from "ext:deno_node/_utils.ts"; -import { primordials } from "ext:core/mod.js"; - -const { - SafeMap, -} = primordials; - -class Session extends EventEmitter { - #connection = null; - #nextId = 1; - #messageCallbacks = new SafeMap(); - - /** Connects the session to the inspector back-end. */ - connect() { - notImplemented("inspector.Session.prototype.connect"); - } - - /** Connects the session to the main thread - * inspector back-end. */ - connectToMainThread() { - notImplemented("inspector.Session.prototype.connectToMainThread"); - } - - /** Posts a message to the inspector back-end. */ - post( - _method: string, - _params?: Record, - _callback?: (...args: unknown[]) => void, - ) { - notImplemented("inspector.Session.prototype.post"); - } - - /** Immediately closes the session, all pending - * message callbacks will be called with an - * error. - */ - disconnect() { - notImplemented("inspector.Session.prototype.disconnect"); - } -} - -/** Activates inspector on host and port. - * See https://nodejs.org/api/inspector.html#inspectoropenport-host-wait */ -function open(_port?: number, _host?: string, _wait?: boolean) { - notImplemented("inspector.Session.prototype.open"); -} - -/** Deactivate the inspector. Blocks until there are no active connections. - * See https://nodejs.org/api/inspector.html#inspectorclose */ -function close() { - notImplemented("inspector.Session.prototype.close"); -} - -/** Return the URL of the active inspector, or undefined if there is none. - * See https://nodejs.org/api/inspector.html#inspectorurl */ -function url() { - // TODO(kt3k): returns undefined for now, which means the inspector is not activated. - return undefined; -} - -/** Blocks until a client (existing or connected later) has sent Runtime.runIfWaitingForDebugger command. - * See https://nodejs.org/api/inspector.html#inspectorwaitfordebugger */ -function waitForDebugger() { - notImplemented("inspector.wairForDebugger"); -} - -const console = globalThis.console; - -export { close, console, open, Session, url, waitForDebugger }; - -export default { - close, - console, - open, - Session, - url, - waitForDebugger, -}; diff --git a/ext/node/polyfills/inspector/promises.js b/ext/node/polyfills/inspector/promises.js new file mode 100644 index 0000000000..3483e53f5e --- /dev/null +++ b/ext/node/polyfills/inspector/promises.js @@ -0,0 +1,20 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +import inspector from "node:inspector"; +import { promisify } from "ext:deno_node/internal/util.mjs"; + +class Session extends inspector.Session { + constructor() { + super(); + } +} +Session.prototype.post = promisify(inspector.Session.prototype.post); + +export * from "node:inspector"; +export { Session }; + +export default { + ...inspector, + Session, +}; diff --git a/ext/node/polyfills/internal/buffer.mjs b/ext/node/polyfills/internal/buffer.mjs index 6e43a49031..dd549221fa 100644 --- a/ext/node/polyfills/internal/buffer.mjs +++ b/ext/node/polyfills/internal/buffer.mjs @@ -2,11 +2,60 @@ // Copyright Joyent and Node contributors. All rights reserved. MIT license. // Copyright Feross Aboukhadijeh, and other contributors. 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 { op_is_ascii, op_is_utf8 } from "ext:core/ops"; +import { core, primordials } from "ext:core/mod.js"; +const { + isAnyArrayBuffer, + isArrayBuffer, + isDataView, + isSharedArrayBuffer, + isTypedArray, +} = core; +const { + ArrayBufferPrototypeGetByteLength, + ArrayBufferPrototypeGetDetached, + ArrayIsArray, + ArrayPrototypeSlice, + BigInt, + DataViewPrototypeGetByteLength, + Float32Array, + Float64Array, + MathFloor, + MathMin, + Number, + NumberIsInteger, + NumberIsNaN, + NumberMAX_SAFE_INTEGER, + NumberMIN_SAFE_INTEGER, + NumberPrototypeToString, + ObjectCreate, + ObjectDefineProperty, + ObjectPrototypeIsPrototypeOf, + ObjectSetPrototypeOf, + RangeError, + SafeRegExp, + String, + StringFromCharCode, + StringPrototypeCharCodeAt, + StringPrototypeIncludes, + StringPrototypeReplace, + StringPrototypeToLowerCase, + StringPrototypeTrim, + SymbolFor, + SymbolToPrimitive, + TypeError, + TypeErrorPrototype, + TypedArrayPrototypeCopyWithin, + TypedArrayPrototypeFill, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, + TypedArrayPrototypeSet, + TypedArrayPrototypeSlice, + TypedArrayPrototypeSubarray, + Uint8Array, + Uint8ArrayPrototype, +} = primordials; +import { op_is_ascii, op_is_utf8, op_transcode } 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"; @@ -24,15 +73,14 @@ import { hexToBytes, utf16leToBytes, } from "ext:deno_node/internal_binding/_utils.ts"; -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 { + ERR_INVALID_STATE, + genericNodeError, + NodeError, +} from "ext:deno_node/internal/errors.ts"; import { forgivingBase64Encode, forgivingBase64UrlEncode, @@ -46,9 +94,13 @@ const utf8Encoder = new TextEncoder(); // Temporary buffers to convert numbers. const float32Array = new Float32Array(1); -const uInt8Float32Array = new Uint8Array(float32Array.buffer); +const uInt8Float32Array = new Uint8Array( + TypedArrayPrototypeGetBuffer(float32Array), +); const float64Array = new Float64Array(1); -const uInt8Float64Array = new Uint8Array(float64Array.buffer); +const uInt8Float64Array = new Uint8Array( + TypedArrayPrototypeGetBuffer(float64Array), +); // Check endianness. float32Array[0] = -1; // 0xBF800000 @@ -60,10 +112,7 @@ export const kMaxLength = 2147483647; export const kStringMaxLength = 536870888; const MAX_UINT32 = 2 ** 32; -const customInspectSymbol = - typeof Symbol === "function" && typeof Symbol["for"] === "function" - ? Symbol["for"]("nodejs.util.inspect.custom") - : null; +const customInspectSymbol = SymbolFor("nodejs.util.inspect.custom"); export const INSPECT_MAX_BYTES = 50; @@ -72,23 +121,25 @@ export const constants = { MAX_STRING_LENGTH: kStringMaxLength, }; -Object.defineProperty(Buffer.prototype, "parent", { +ObjectDefineProperty(Buffer.prototype, "parent", { + __proto__: null, enumerable: true, get: function () { - if (!Buffer.isBuffer(this)) { + if (!BufferIsBuffer(this)) { return void 0; } - return this.buffer; + return TypedArrayPrototypeGetBuffer(this); }, }); -Object.defineProperty(Buffer.prototype, "offset", { +ObjectDefineProperty(Buffer.prototype, "offset", { + __proto__: null, enumerable: true, get: function () { - if (!Buffer.isBuffer(this)) { + if (!BufferIsBuffer(this)) { return void 0; } - return this.byteOffset; + return TypedArrayPrototypeGetByteOffset(this); }, }); @@ -99,10 +150,21 @@ function createBuffer(length) { ); } const buf = new Uint8Array(length); - Object.setPrototypeOf(buf, Buffer.prototype); + ObjectSetPrototypeOf(buf, BufferPrototype); return buf; } +/** + * @param {ArrayBufferLike} O + * @returns {boolean} + */ +function isDetachedBuffer(O) { + if (isSharedArrayBuffer(O)) { + return false; + } + return ArrayBufferPrototypeGetDetached(O); +} + export function Buffer(arg, encodingOrOffset, length) { if (typeof arg === "number") { if (typeof encodingOrOffset === "string") { @@ -129,6 +191,7 @@ function _from(value, encodingOrOffset, length) { return fromArrayBuffer(value, encodingOrOffset, length); } + // deno-lint-ignore prefer-primordials const valueOf = value.valueOf && value.valueOf(); if ( valueOf != null && @@ -143,8 +206,8 @@ function _from(value, encodingOrOffset, length) { return b; } - if (typeof value[Symbol.toPrimitive] === "function") { - const primitive = value[Symbol.toPrimitive]("string"); + if (typeof value[SymbolToPrimitive] === "function") { + const primitive = value[SymbolToPrimitive]("string"); if (typeof primitive === "string") { return fromString(primitive, encodingOrOffset); } @@ -158,13 +221,19 @@ function _from(value, encodingOrOffset, length) { ); } -Buffer.from = function from(value, encodingOrOffset, length) { +const BufferFrom = Buffer.from = function from( + value, + encodingOrOffset, + length, +) { return _from(value, encodingOrOffset, length); }; -Object.setPrototypeOf(Buffer.prototype, Uint8Array.prototype); +const BufferPrototype = Buffer.prototype; -Object.setPrototypeOf(Buffer, Uint8Array); +ObjectSetPrototypeOf(Buffer.prototype, Uint8ArrayPrototype); + +ObjectSetPrototypeOf(Buffer, Uint8Array); function assertSize(size) { validateNumber(size, "size", 0, kMaxLength); @@ -182,6 +251,7 @@ function _alloc(size, fill, encoding) { encoding, ); } + // deno-lint-ignore prefer-primordials return buffer.fill(fill, encoding); } return buffer; @@ -208,13 +278,14 @@ function fromString(string, encoding) { if (typeof encoding !== "string" || encoding === "") { encoding = "utf8"; } - if (!Buffer.isEncoding(encoding)) { + if (!BufferIsEncoding(encoding)) { throw new codes.ERR_UNKNOWN_ENCODING(encoding); } const length = byteLength(string, encoding) | 0; let buf = createBuffer(length); const actual = buf.write(string, encoding); if (actual !== length) { + // deno-lint-ignore prefer-primordials buf = buf.slice(0, actual); } return buf; @@ -222,11 +293,12 @@ function fromString(string, encoding) { function fromArrayLike(obj) { const buf = new Uint8Array(obj); - Object.setPrototypeOf(buf, Buffer.prototype); + ObjectSetPrototypeOf(buf, BufferPrototype); return buf; } function fromObject(obj) { + // deno-lint-ignore prefer-primordials if (obj.length !== undefined || isAnyArrayBuffer(obj.buffer)) { if (typeof obj.length !== "number") { return createBuffer(0); @@ -235,7 +307,7 @@ function fromObject(obj) { return fromArrayLike(obj); } - if (obj.type === "Buffer" && Array.isArray(obj.data)) { + if (obj.type === "Buffer" && ArrayIsArray(obj.data)) { return fromArrayLike(obj.data); } } @@ -244,7 +316,7 @@ function checked(length) { if (length >= kMaxLength) { throw new RangeError( "Attempt to allocate Buffer larger than maximum size: 0x" + - kMaxLength.toString(16) + " bytes", + NumberPrototypeToString(kMaxLength, 16) + " bytes", ); } return length | 0; @@ -252,25 +324,33 @@ function checked(length) { export function SlowBuffer(length) { assertSize(length); - return Buffer.alloc(+length); + return _alloc(+length); } -Object.setPrototypeOf(SlowBuffer.prototype, Uint8Array.prototype); +ObjectSetPrototypeOf(SlowBuffer.prototype, Uint8ArrayPrototype); -Object.setPrototypeOf(SlowBuffer, Uint8Array); +ObjectSetPrototypeOf(SlowBuffer, Uint8Array); -Buffer.isBuffer = function isBuffer(b) { - return b != null && b._isBuffer === true && b !== Buffer.prototype; +const BufferIsBuffer = Buffer.isBuffer = function isBuffer(b) { + return b != null && b._isBuffer === true && b !== BufferPrototype; }; -Buffer.compare = function compare(a, b) { - if (isInstance(a, Uint8Array)) { - a = Buffer.from(a, a.offset, a.byteLength); +const BufferCompare = Buffer.compare = function compare(a, b) { + if (isUint8Array(a)) { + a = BufferFrom( + a, + TypedArrayPrototypeGetByteOffset(a), + TypedArrayPrototypeGetByteLength(a), + ); } - if (isInstance(b, Uint8Array)) { - b = Buffer.from(b, b.offset, b.byteLength); + if (isUint8Array(b)) { + b = BufferFrom( + b, + TypedArrayPrototypeGetByteOffset(b), + TypedArrayPrototypeGetByteLength(b), + ); } - if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { + if (!BufferIsBuffer(a) || !BufferIsBuffer(b)) { throw new TypeError( 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array', ); @@ -280,7 +360,7 @@ Buffer.compare = function compare(a, b) { } let x = a.length; let y = b.length; - for (let i = 0, len = Math.min(x, y); i < len; ++i) { + for (let i = 0, len = MathMin(x, y); i < len; ++i) { if (a[i] !== b[i]) { x = a[i]; y = b[i]; @@ -296,18 +376,18 @@ Buffer.compare = function compare(a, b) { return 0; }; -Buffer.isEncoding = function isEncoding(encoding) { +const BufferIsEncoding = Buffer.isEncoding = function isEncoding(encoding) { return typeof encoding === "string" && encoding.length !== 0 && normalizeEncoding(encoding) !== undefined; }; Buffer.concat = function concat(list, length) { - if (!Array.isArray(list)) { + if (!ArrayIsArray(list)) { throw new codes.ERR_INVALID_ARG_TYPE("list", "Array", list); } if (list.length === 0) { - return Buffer.alloc(0); + return _alloc(0); } if (length === undefined) { @@ -321,7 +401,7 @@ Buffer.concat = function concat(list, length) { validateOffset(length, "length"); } - const buffer = Buffer.allocUnsafe(length); + const buffer = _allocUnsafe(length); let pos = 0; for (let i = 0; i < list.length; i++) { const buf = list[i]; @@ -342,7 +422,7 @@ Buffer.concat = function concat(list, length) { // Zero-fill the remaining bytes if the specified `length` was more than // the actual total length, i.e. if we have some remaining allocated bytes // there were not initialized. - buffer.fill(0, pos, length); + TypedArrayPrototypeFill(buffer, 0, pos, length); } return buffer; @@ -350,7 +430,18 @@ Buffer.concat = function concat(list, length) { function byteLength(string, encoding) { if (typeof string !== "string") { - if (isArrayBufferView(string) || isAnyArrayBuffer(string)) { + if (isTypedArray(string)) { + return TypedArrayPrototypeGetByteLength(string); + } + if (isDataView(string)) { + return DataViewPrototypeGetByteLength(string); + } + if (isArrayBuffer(string)) { + return ArrayBufferPrototypeGetByteLength(string); + } + if (isSharedArrayBuffer(string)) { + // TODO(petamoriken): add SharedArayBuffer to primordials + // deno-lint-ignore prefer-primordials return string.byteLength; } @@ -459,6 +550,7 @@ Buffer.prototype.toString = function toString(encoding, start, end) { throw new codes.ERR_UNKNOWN_ENCODING(encoding); } + // deno-lint-ignore prefer-primordials return ops.slice(this, start, end); }; @@ -475,22 +567,29 @@ Buffer.prototype.equals = function equals(b) { if (this === b) { return true; } - return Buffer.compare(this, b) === 0; + return BufferCompare(this, b) === 0; }; -Buffer.prototype.inspect = function inspect() { - let str = ""; - const max = INSPECT_MAX_BYTES; - str = this.toString("hex", 0, max).replace(/(.{2})/g, "$1 ").trim(); - if (this.length > max) { - str += " ... "; - } - return ""; -}; +const SPACER_PATTERN = new SafeRegExp(/(.{2})/g); -if (customInspectSymbol) { - Buffer.prototype[customInspectSymbol] = Buffer.prototype.inspect; -} +Buffer.prototype[customInspectSymbol] = + Buffer.prototype.inspect = + function inspect() { + let str = ""; + const max = INSPECT_MAX_BYTES; + str = StringPrototypeTrim( + StringPrototypeReplace( + // deno-lint-ignore prefer-primordials + this.toString("hex", 0, max), + SPACER_PATTERN, + "$1 ", + ), + ); + if (this.length > max) { + str += " ... "; + } + return ""; + }; Buffer.prototype.compare = function compare( target, @@ -499,10 +598,14 @@ Buffer.prototype.compare = function compare( thisStart, thisEnd, ) { - if (isInstance(target, Uint8Array)) { - target = Buffer.from(target, target.offset, target.byteLength); + if (isUint8Array(target)) { + target = BufferFrom( + target, + TypedArrayPrototypeGetByteOffset(target), + TypedArrayPrototypeGetByteLength(target), + ); } - if (!Buffer.isBuffer(target)) { + if (!BufferIsBuffer(target)) { throw new codes.ERR_INVALID_ARG_TYPE( "target", ["Buffer", "Uint8Array"], @@ -559,8 +662,9 @@ Buffer.prototype.compare = function compare( } let x = thisEnd - thisStart; let y = end - start; - const len = Math.min(x, y); - const thisCopy = this.slice(thisStart, thisEnd); + const len = MathMin(x, y); + const thisCopy = TypedArrayPrototypeSlice(this, thisStart, thisEnd); + // deno-lint-ignore prefer-primordials const targetCopy = target.slice(start, end); for (let i = 0; i < len; ++i) { if (thisCopy[i] !== targetCopy[i]) { @@ -590,7 +694,8 @@ function bidirectionalIndexOf(buffer, val, byteOffset, encoding, dir) { byteOffset = -0x80000000; } byteOffset = +byteOffset; - if (Number.isNaN(byteOffset)) { + if (NumberIsNaN(byteOffset)) { + // deno-lint-ignore prefer-primordials byteOffset = dir ? 0 : (buffer.length || buffer.byteLength); } dir = !!dir; @@ -610,6 +715,7 @@ function bidirectionalIndexOf(buffer, val, byteOffset, encoding, dir) { if (ops === undefined) { throw new codes.ERR_UNKNOWN_ENCODING(encoding); } + // deno-lint-ignore prefer-primordials return ops.indexOf(buffer, val, byteOffset, dir); } @@ -626,6 +732,7 @@ function bidirectionalIndexOf(buffer, val, byteOffset, encoding, dir) { } Buffer.prototype.includes = function includes(val, byteOffset, encoding) { + // deno-lint-ignore prefer-primordials return this.indexOf(val, byteOffset, encoding) !== -1; }; @@ -645,7 +752,7 @@ Buffer.prototype.asciiSlice = function asciiSlice(offset, length) { if (offset === 0 && length === this.length) { return bytesToAscii(this); } else { - return bytesToAscii(this.slice(offset, length)); + return bytesToAscii(TypedArrayPrototypeSlice(this, offset, length)); } }; @@ -660,7 +767,9 @@ Buffer.prototype.base64Slice = function base64Slice( if (offset === 0 && length === this.length) { return forgivingBase64Encode(this); } else { - return forgivingBase64Encode(this.slice(offset, length)); + return forgivingBase64Encode( + TypedArrayPrototypeSlice(this, offset, length), + ); } }; @@ -679,7 +788,9 @@ Buffer.prototype.base64urlSlice = function base64urlSlice( if (offset === 0 && length === this.length) { return forgivingBase64UrlEncode(this); } else { - return forgivingBase64UrlEncode(this.slice(offset, length)); + return forgivingBase64UrlEncode( + TypedArrayPrototypeSlice(this, offset, length), + ); } }; @@ -724,7 +835,7 @@ Buffer.prototype.ucs2Slice = function ucs2Slice(offset, length) { if (offset === 0 && length === this.length) { return bytesToUtf16le(this); } else { - return bytesToUtf16le(this.slice(offset, length)); + return bytesToUtf16le(TypedArrayPrototypeSlice(this, offset, length)); } }; @@ -743,9 +854,9 @@ Buffer.prototype.utf8Slice = function utf8Slice(string, offset, length) { Buffer.prototype.utf8Write = function utf8Write(string, offset, length) { offset = offset || 0; - const maxLength = Math.min(length || Infinity, this.length - offset); + const maxLength = MathMin(length || Infinity, this.length - offset); const buf = offset || maxLength < this.length - ? this.subarray(offset, maxLength + offset) + ? TypedArrayPrototypeSubarray(this, offset, maxLength + offset) : this; return utf8Encoder.encodeInto(string, buf).written; }; @@ -797,7 +908,7 @@ Buffer.prototype.write = function write(string, offset, length, encoding) { Buffer.prototype.toJSON = function toJSON() { return { type: "Buffer", - data: Array.prototype.slice.call(this._arr || this, 0), + data: ArrayPrototypeSlice(this._arr || this, 0), }; }; function fromArrayBuffer(obj, byteOffset, length) { @@ -806,11 +917,12 @@ function fromArrayBuffer(obj, byteOffset, length) { byteOffset = 0; } else { byteOffset = +byteOffset; - if (Number.isNaN(byteOffset)) { + if (NumberIsNaN(byteOffset)) { byteOffset = 0; } } + // deno-lint-ignore prefer-primordials const maxLength = obj.byteLength - byteOffset; if (maxLength < 0) { @@ -832,7 +944,7 @@ function fromArrayBuffer(obj, byteOffset, length) { } const buffer = new Uint8Array(obj, byteOffset, length); - Object.setPrototypeOf(buffer, Buffer.prototype); + ObjectSetPrototypeOf(buffer, BufferPrototype); return buffer; } @@ -840,6 +952,7 @@ function _base64Slice(buf, start, end) { if (start === 0 && end === buf.length) { return forgivingBase64Encode(buf); } else { + // deno-lint-ignore prefer-primordials return forgivingBase64Encode(buf.slice(start, end)); } } @@ -848,9 +961,10 @@ const decoder = new TextDecoder(); function _utf8Slice(buf, start, end) { try { + // deno-lint-ignore prefer-primordials return decoder.decode(buf.slice(start, end)); } catch (err) { - if (err instanceof TypeError) { + if (ObjectPrototypeIsPrototypeOf(TypeErrorPrototype, err)) { throw new NodeError("ERR_STRING_TOO_LONG", "String too long"); } throw err; @@ -859,9 +973,9 @@ function _utf8Slice(buf, start, end) { function _latin1Slice(buf, start, end) { let ret = ""; - end = Math.min(buf.length, end); + end = MathMin(buf.length, end); for (let i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i]); + ret += StringFromCharCode(buf[i]); } return ret; } @@ -990,42 +1104,38 @@ Buffer.prototype.readUint32BE = Buffer.prototype.readUInt32BE = readUInt32BE; Buffer.prototype.readBigUint64LE = Buffer.prototype.readBigUInt64LE = - defineBigIntMethod( - function readBigUInt64LE(offset) { - offset = offset >>> 0; - validateNumber(offset, "offset"); - const first = this[offset]; - const last = this[offset + 7]; - if (first === void 0 || last === void 0) { - boundsError(offset, this.length - 8); - } - const lo = first + this[++offset] * 2 ** 8 + - this[++offset] * 2 ** 16 + - this[++offset] * 2 ** 24; - const hi = this[++offset] + this[++offset] * 2 ** 8 + - this[++offset] * 2 ** 16 + last * 2 ** 24; - return BigInt(lo) + (BigInt(hi) << BigInt(32)); - }, - ); + function readBigUInt64LE(offset) { + offset = offset >>> 0; + validateNumber(offset, "offset"); + const first = this[offset]; + const last = this[offset + 7]; + if (first === void 0 || last === void 0) { + boundsError(offset, this.length - 8); + } + const lo = first + this[++offset] * 2 ** 8 + + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 24; + const hi = this[++offset] + this[++offset] * 2 ** 8 + + this[++offset] * 2 ** 16 + last * 2 ** 24; + return BigInt(lo) + (BigInt(hi) << 32n); + }; Buffer.prototype.readBigUint64BE = Buffer.prototype.readBigUInt64BE = - defineBigIntMethod( - function readBigUInt64BE(offset) { - offset = offset >>> 0; - validateNumber(offset, "offset"); - const first = this[offset]; - const last = this[offset + 7]; - if (first === void 0 || last === void 0) { - boundsError(offset, this.length - 8); - } - const hi = first * 2 ** 24 + this[++offset] * 2 ** 16 + - this[++offset] * 2 ** 8 + this[++offset]; - const lo = this[++offset] * 2 ** 24 + this[++offset] * 2 ** 16 + - this[++offset] * 2 ** 8 + last; - return (BigInt(hi) << BigInt(32)) + BigInt(lo); - }, - ); + function readBigUInt64BE(offset) { + offset = offset >>> 0; + validateNumber(offset, "offset"); + const first = this[offset]; + const last = this[offset + 7]; + if (first === void 0 || last === void 0) { + boundsError(offset, this.length - 8); + } + const hi = first * 2 ** 24 + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 8 + this[++offset]; + const lo = this[++offset] * 2 ** 24 + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 8 + last; + return (BigInt(hi) << 32n) + BigInt(lo); + }; Buffer.prototype.readIntLE = function readIntLE( offset, @@ -1144,43 +1254,39 @@ Buffer.prototype.readInt32BE = function readInt32BE(offset = 0) { last; }; -Buffer.prototype.readBigInt64LE = defineBigIntMethod( - function readBigInt64LE(offset) { - offset = offset >>> 0; - validateNumber(offset, "offset"); - const first = this[offset]; - const last = this[offset + 7]; - if (first === void 0 || last === void 0) { - boundsError(offset, this.length - 8); - } - const val = this[offset + 4] + this[offset + 5] * 2 ** 8 + - this[offset + 6] * 2 ** 16 + (last << 24); - return (BigInt(val) << BigInt(32)) + - BigInt( - first + this[++offset] * 2 ** 8 + this[++offset] * 2 ** 16 + - this[++offset] * 2 ** 24, - ); - }, -); +Buffer.prototype.readBigInt64LE = function readBigInt64LE(offset) { + offset = offset >>> 0; + validateNumber(offset, "offset"); + const first = this[offset]; + const last = this[offset + 7]; + if (first === void 0 || last === void 0) { + boundsError(offset, this.length - 8); + } + const val = this[offset + 4] + this[offset + 5] * 2 ** 8 + + this[offset + 6] * 2 ** 16 + (last << 24); + return (BigInt(val) << 32n) + + BigInt( + first + this[++offset] * 2 ** 8 + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 24, + ); +}; -Buffer.prototype.readBigInt64BE = defineBigIntMethod( - function readBigInt64BE(offset) { - offset = offset >>> 0; - validateNumber(offset, "offset"); - const first = this[offset]; - const last = this[offset + 7]; - if (first === void 0 || last === void 0) { - boundsError(offset, this.length - 8); - } - const val = (first << 24) + this[++offset] * 2 ** 16 + - this[++offset] * 2 ** 8 + this[++offset]; - return (BigInt(val) << BigInt(32)) + - BigInt( - this[++offset] * 2 ** 24 + this[++offset] * 2 ** 16 + - this[++offset] * 2 ** 8 + last, - ); - }, -); +Buffer.prototype.readBigInt64BE = function readBigInt64BE(offset) { + offset = offset >>> 0; + validateNumber(offset, "offset"); + const first = this[offset]; + const last = this[offset + 7]; + if (first === void 0 || last === void 0) { + boundsError(offset, this.length - 8); + } + const val = (first << 24) + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 8 + this[++offset]; + return (BigInt(val) << 32n) + + BigInt( + this[++offset] * 2 ** 24 + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 8 + last, + ); +}; Buffer.prototype.readFloatLE = function readFloatLE(offset) { return bigEndian @@ -1289,7 +1395,7 @@ Buffer.prototype.writeUint32BE = function wrtBigUInt64LE(buf, value, offset, min, max) { checkIntBI(value, min, max, buf, offset, 7); - let lo = Number(value & BigInt(4294967295)); + let lo = Number(value & 4294967295n); buf[offset++] = lo; lo = lo >> 8; buf[offset++] = lo; @@ -1297,7 +1403,7 @@ function wrtBigUInt64LE(buf, value, offset, min, max) { buf[offset++] = lo; lo = lo >> 8; buf[offset++] = lo; - let hi = Number(value >> BigInt(32) & BigInt(4294967295)); + let hi = Number(value >> 32n & 4294967295n); buf[offset++] = hi; hi = hi >> 8; buf[offset++] = hi; @@ -1310,7 +1416,7 @@ function wrtBigUInt64LE(buf, value, offset, min, max) { function wrtBigUInt64BE(buf, value, offset, min, max) { checkIntBI(value, min, max, buf, offset, 7); - let lo = Number(value & BigInt(4294967295)); + let lo = Number(value & 4294967295n); buf[offset + 7] = lo; lo = lo >> 8; buf[offset + 6] = lo; @@ -1318,7 +1424,7 @@ function wrtBigUInt64BE(buf, value, offset, min, max) { buf[offset + 5] = lo; lo = lo >> 8; buf[offset + 4] = lo; - let hi = Number(value >> BigInt(32) & BigInt(4294967295)); + let hi = Number(value >> 32n & 4294967295n); buf[offset + 3] = hi; hi = hi >> 8; buf[offset + 2] = hi; @@ -1331,31 +1437,27 @@ function wrtBigUInt64BE(buf, value, offset, min, max) { Buffer.prototype.writeBigUint64LE = Buffer.prototype.writeBigUInt64LE = - defineBigIntMethod( - function writeBigUInt64LE(value, offset = 0) { - return wrtBigUInt64LE( - this, - value, - offset, - BigInt(0), - BigInt("0xffffffffffffffff"), - ); - }, - ); + function writeBigUInt64LE(value, offset = 0) { + return wrtBigUInt64LE( + this, + value, + offset, + 0n, + 0xffffffffffffffffn, + ); + }; Buffer.prototype.writeBigUint64BE = Buffer.prototype.writeBigUInt64BE = - defineBigIntMethod( - function writeBigUInt64BE(value, offset = 0) { - return wrtBigUInt64BE( - this, - value, - offset, - BigInt(0), - BigInt("0xffffffffffffffff"), - ); - }, - ); + function writeBigUInt64BE(value, offset = 0) { + return wrtBigUInt64BE( + this, + value, + offset, + 0n, + 0xffffffffffffffffn, + ); + }; Buffer.prototype.writeIntLE = function writeIntLE( value, @@ -1446,29 +1548,25 @@ Buffer.prototype.writeInt32BE = function writeInt32BE(value, offset = 0) { return writeU_Int32BE(this, value, offset, -0x80000000, 0x7fffffff); }; -Buffer.prototype.writeBigInt64LE = defineBigIntMethod( - function writeBigInt64LE(value, offset = 0) { - return wrtBigUInt64LE( - this, - value, - offset, - -BigInt("0x8000000000000000"), - BigInt("0x7fffffffffffffff"), - ); - }, -); +Buffer.prototype.writeBigInt64LE = function writeBigInt64LE(value, offset = 0) { + return wrtBigUInt64LE( + this, + value, + offset, + -0x8000000000000000n, + 0x7fffffffffffffffn, + ); +}; -Buffer.prototype.writeBigInt64BE = defineBigIntMethod( - function writeBigInt64BE(value, offset = 0) { - return wrtBigUInt64BE( - this, - value, - offset, - -BigInt("0x8000000000000000"), - BigInt("0x7fffffffffffffff"), - ); - }, -); +Buffer.prototype.writeBigInt64BE = function writeBigInt64BE(value, offset = 0) { + return wrtBigUInt64BE( + this, + value, + offset, + -0x8000000000000000n, + 0x7fffffffffffffffn, + ); +}; Buffer.prototype.writeFloatLE = function writeFloatLE( value, @@ -1596,14 +1694,12 @@ Buffer.prototype.copy = function copy( } const len = sourceEnd - sourceStart; - if ( - this === target && typeof Uint8Array.prototype.copyWithin === "function" - ) { - this.copyWithin(targetStart, sourceStart, sourceEnd); + if (this === target) { + TypedArrayPrototypeCopyWithin(this, targetStart, sourceStart, sourceEnd); } else { - Uint8Array.prototype.set.call( + TypedArrayPrototypeSet( target, - this.subarray(sourceStart, sourceEnd), + TypedArrayPrototypeSubarray(this, sourceStart, sourceEnd), targetStart, ); } @@ -1623,11 +1719,11 @@ Buffer.prototype.fill = function fill(val, start, end, encoding) { if (encoding !== void 0 && typeof encoding !== "string") { throw new TypeError("encoding must be a string"); } - if (typeof encoding === "string" && !Buffer.isEncoding(encoding)) { + if (typeof encoding === "string" && !BufferIsEncoding(encoding)) { throw new TypeError("Unknown encoding: " + encoding); } if (val.length === 1) { - const code = val.charCodeAt(0); + const code = StringPrototypeCharCodeAt(val, 0); if (encoding === "utf8" && code < 128 || encoding === "latin1") { val = code; } @@ -1654,7 +1750,7 @@ Buffer.prototype.fill = function fill(val, start, end, encoding) { this[i] = val; } } else { - const bytes = Buffer.isBuffer(val) ? val : Buffer.from(val, encoding); + const bytes = BufferIsBuffer(val) ? val : BufferFrom(val, encoding); const len = bytes.length; if (len === 0) { throw new codes.ERR_INVALID_ARG_VALUE( @@ -1681,7 +1777,7 @@ function checkIntBI(value, min, max, buf, offset, byteLength2) { const n = typeof min === "bigint" ? "n" : ""; let range; if (byteLength2 > 3) { - if (min === 0 || min === BigInt(0)) { + if (min === 0 || min === 0n) { range = `>= 0${n} and < 2${n} ** ${(byteLength2 + 1) * 8}${n}`; } else { range = `>= -(2${n} ** ${(byteLength2 + 1) * 8 - 1}${n}) and < 2 ** ${ @@ -1706,7 +1802,7 @@ function checkIntBI(value, min, max, buf, offset, byteLength2) { function blitBuffer(src, dst, offset, byteLength = Infinity) { const srcLength = src.length; // Establish the number of bytes to be written - const bytesToWrite = Math.min( + const bytesToWrite = MathMin( // If byte length is defined in the call, then it sets an upper bound, // otherwise it is Infinity and is never chosen. byteLength, @@ -1726,15 +1822,9 @@ function blitBuffer(src, dst, offset, byteLength = Infinity) { return bytesToWrite; } -function isInstance(obj, type) { - return obj instanceof type || - obj != null && obj.constructor != null && - obj.constructor.name != null && obj.constructor.name === type.name; -} - const hexSliceLookupTable = function () { const alphabet = "0123456789abcdef"; - const table = new Array(256); + const table = []; for (let i = 0; i < 16; ++i) { const i16 = i * 16; for (let j = 0; j < 16; ++j) { @@ -1744,14 +1834,6 @@ const hexSliceLookupTable = function () { return table; }(); -function defineBigIntMethod(fn) { - return typeof BigInt === "undefined" ? BufferBigIntNotDefined : fn; -} - -function BufferBigIntNotDefined() { - throw new Error("BigInt not supported"); -} - export function readUInt48LE(buf, offset = 0) { validateNumber(offset, "offset"); const first = buf[offset]; @@ -2075,10 +2157,10 @@ export function byteLengthUtf8(str) { function base64ByteLength(str, bytes) { // Handle padding - if (str.charCodeAt(bytes - 1) === 0x3D) { + if (StringPrototypeCharCodeAt(str, bytes - 1) === 0x3D) { bytes--; } - if (bytes > 1 && str.charCodeAt(bytes - 1) === 0x3D) { + if (bytes > 1 && StringPrototypeCharCodeAt(str, bytes - 1) === 0x3D) { bytes--; } @@ -2086,7 +2168,7 @@ function base64ByteLength(str, bytes) { return (bytes * 3) >>> 2; } -export const encodingsMap = Object.create(null); +export const encodingsMap = ObjectCreate(null); for (let i = 0; i < encodings.length; ++i) { encodingsMap[encodings[i]] = i; } @@ -2216,7 +2298,7 @@ export const encodingOps = { }; export function getEncodingOps(encoding) { - encoding = String(encoding).toLowerCase(); + encoding = StringPrototypeToLowerCase(String(encoding)); switch (encoding.length) { case 4: if (encoding === "utf8") return encodingOps.utf8; @@ -2256,6 +2338,14 @@ export function getEncodingOps(encoding) { } } +/** + * @param {Buffer} source + * @param {Buffer} target + * @param {number} targetStart + * @param {number} sourceStart + * @param {number} sourceEnd + * @returns {number} + */ export function _copyActual( source, target, @@ -2274,6 +2364,7 @@ export function _copyActual( } if (sourceStart !== 0 || sourceEnd < source.length) { + // deno-lint-ignore prefer-primordials source = new Uint8Array(source.buffer, source.byteOffset + sourceStart, nb); } @@ -2283,7 +2374,7 @@ export function _copyActual( } export function boundsError(value, length, type) { - if (Math.floor(value) !== value) { + if (MathFloor(value) !== value) { validateNumber(value, type); throw new codes.ERR_OUT_OF_RANGE(type || "offset", "an integer", value); } @@ -2306,7 +2397,7 @@ export function validateNumber(value, name, min = undefined, max) { if ( (min != null && value < min) || (max != null && value > max) || - ((min != null || max != null) && Number.isNaN(value)) + ((min != null || max != null) && NumberIsNaN(value)) ) { throw new codes.ERR_OUT_OF_RANGE( name, @@ -2340,11 +2431,11 @@ function checkInt(value, min, max, buf, offset, byteLength) { export function toInteger(n, defaultVal) { n = +n; if ( - !Number.isNaN(n) && - n >= Number.MIN_SAFE_INTEGER && - n <= Number.MAX_SAFE_INTEGER + !NumberIsNaN(n) && + n >= NumberMIN_SAFE_INTEGER && + n <= NumberMAX_SAFE_INTEGER ) { - return ((n % 1) === 0 ? n : Math.floor(n)); + return ((n % 1) === 0 ? n : MathFloor(n)); } return defaultVal; } @@ -2417,7 +2508,7 @@ export function writeU_Int48BE(buf, value, offset, min, max) { value = +value; checkInt(value, min, max, buf, offset, 5); - const newVal = Math.floor(value * 2 ** -32); + const newVal = MathFloor(value * 2 ** -32); buf[offset++] = newVal >>> 8; buf[offset++] = newVal; buf[offset + 3] = value; @@ -2435,7 +2526,7 @@ export function writeU_Int40BE(buf, value, offset, min, max) { value = +value; checkInt(value, min, max, buf, offset, 4); - buf[offset++] = Math.floor(value * 2 ** -32); + buf[offset++] = MathFloor(value * 2 ** -32); buf[offset + 3] = value; value = value >>> 8; buf[offset + 2] = value; @@ -2478,12 +2569,12 @@ export function validateOffset( value, name, min = 0, - max = Number.MAX_SAFE_INTEGER, + max = NumberMAX_SAFE_INTEGER, ) { if (typeof value !== "number") { throw new codes.ERR_INVALID_ARG_TYPE(name, "number", value); } - if (!Number.isInteger(value)) { + if (!NumberIsInteger(value)) { throw new codes.ERR_OUT_OF_RANGE(name, "an integer", value); } if (value < min || value > max) { @@ -2496,7 +2587,7 @@ export function writeU_Int48LE(buf, value, offset, min, max) { value = +value; checkInt(value, min, max, buf, offset, 5); - const newVal = Math.floor(value * 2 ** -32); + const newVal = MathFloor(value * 2 ** -32); buf[offset++] = value; value = value >>> 8; buf[offset++] = value; @@ -2522,7 +2613,7 @@ export function writeU_Int40LE(buf, value, offset, min, max) { buf[offset++] = value; value = value >>> 8; buf[offset++] = value; - buf[offset++] = Math.floor(newVal * 2 ** -32); + buf[offset++] = MathFloor(newVal * 2 ** -32); return offset; } @@ -2556,14 +2647,14 @@ export function writeU_Int24LE(buf, value, offset, min, max) { export function isUtf8(input) { if (isTypedArray(input)) { - if (input.buffer.detached) { + if (isDetachedBuffer(TypedArrayPrototypeGetBuffer(input))) { throw new ERR_INVALID_STATE("Cannot validate on a detached buffer"); } return op_is_utf8(input); } if (isAnyArrayBuffer(input)) { - if (input.detached) { + if (isDetachedBuffer(input)) { throw new ERR_INVALID_STATE("Cannot validate on a detached buffer"); } return op_is_utf8(new Uint8Array(input)); @@ -2578,14 +2669,14 @@ export function isUtf8(input) { export function isAscii(input) { if (isTypedArray(input)) { - if (input.buffer.detached) { + if (isDetachedBuffer(TypedArrayPrototypeGetBuffer(input))) { throw new ERR_INVALID_STATE("Cannot validate on a detached buffer"); } return op_is_ascii(input); } if (isAnyArrayBuffer(input)) { - if (input.detached) { + if (isDetachedBuffer(input)) { throw new ERR_INVALID_STATE("Cannot validate on a detached buffer"); } return op_is_ascii(new Uint8Array(input)); @@ -2598,6 +2689,48 @@ export function isAscii(input) { ], input); } +export function transcode(source, fromEnco, toEnco) { + if (!isUint8Array(source)) { + throw new codes.ERR_INVALID_ARG_TYPE( + "source", + ["Buffer", "Uint8Array"], + source, + ); + } + if (source.length === 0) { + return Buffer.alloc(0); + } + const code = "U_ILLEGAL_ARGUMENT_ERROR"; + const illegalArgumentError = genericNodeError( + `Unable to transcode Buffer [${code}]`, + { code: code, errno: 1 }, + ); + fromEnco = normalizeEncoding(fromEnco); + toEnco = normalizeEncoding(toEnco); + if (!fromEnco || !toEnco) { + throw illegalArgumentError; + } + // Return the provided source when transcode is not required + // for the from/to encoding pair. + const returnSource = fromEnco === toEnco || + fromEnco === "ascii" && toEnco === "utf8" || + fromEnco === "ascii" && toEnco === "latin1"; + if (returnSource) { + return Buffer.from(source); + } + + try { + const result = op_transcode(new Uint8Array(source), fromEnco, toEnco); + return Buffer.from(result, toEnco); + } catch (err) { + if (StringPrototypeIncludes(err.message, "Unable to transcode Buffer")) { + throw illegalArgumentError; + } else { + throw err; + } + } +} + export default { atob, btoa, @@ -2610,4 +2743,5 @@ export default { kMaxLength, kStringMaxLength, SlowBuffer, + transcode, }; diff --git a/ext/node/polyfills/internal/child_process.ts b/ext/node/polyfills/internal/child_process.ts index 6f209b7192..cfff1079ff 100644 --- a/ext/node/polyfills/internal/child_process.ts +++ b/ext/node/polyfills/internal/child_process.ts @@ -1191,8 +1191,12 @@ function toDenoArgs(args: string[]): string[] { } if (flagInfo === undefined) { - // Not a known flag that expects a value. Just copy it to the output. - denoArgs.push(arg); + if (arg === "--no-warnings") { + denoArgs.push("--quiet"); + } else { + // Not a known flag that expects a value. Just copy it to the output. + denoArgs.push(arg); + } continue; } @@ -1335,7 +1339,7 @@ export function setupChannel(target: any, ipc: number) { } } - process.nextTick(handleMessage, msg); + nextTick(handleMessage, msg); } } catch (err) { if ( @@ -1396,7 +1400,7 @@ export function setupChannel(target: any, ipc: number) { if (!target.connected) { const err = new ERR_IPC_CHANNEL_CLOSED(); if (typeof callback === "function") { - process.nextTick(callback, err); + nextTick(callback, err); } else { nextTick(() => target.emit("error", err)); } @@ -1412,7 +1416,18 @@ export function setupChannel(target: any, ipc: number) { .then(() => { control.unrefCounted(); if (callback) { - process.nextTick(callback, null); + nextTick(callback, null); + } + }, (err: Error) => { + control.unrefCounted(); + if (err instanceof Deno.errors.Interrupted) { + // Channel closed on us mid-write. + } else { + if (typeof callback === "function") { + nextTick(callback, err); + } else { + nextTick(() => target.emit("error", err)); + } } }); return queueOk[0]; @@ -1429,7 +1444,7 @@ export function setupChannel(target: any, ipc: number) { target.connected = false; target[kCanDisconnect] = false; control[kControlDisconnect](); - process.nextTick(() => { + nextTick(() => { target.channel = null; core.close(ipc); target.emit("disconnect"); diff --git a/ext/node/polyfills/internal/crypto/_randomInt.ts b/ext/node/polyfills/internal/crypto/_randomInt.ts index 7f4d703ad4..e08b3e9639 100644 --- a/ext/node/polyfills/internal/crypto/_randomInt.ts +++ b/ext/node/polyfills/internal/crypto/_randomInt.ts @@ -1,9 +1,15 @@ // 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 { op_node_random_int } from "ext:core/ops"; +import { primordials } from "ext:core/mod.js"; +const { + Error, + MathCeil, + MathFloor, + MathPow, + NumberIsSafeInteger, + RangeError, +} = primordials; export default function randomInt(max: number): number; export default function randomInt(min: number, max: number): number; @@ -23,7 +29,9 @@ export default function randomInt( cb?: (err: Error | null, n?: number) => void, ): number | void { if (typeof max === "number" && typeof min === "number") { - [max, min] = [min, max]; + const temp = max; + max = min; + min = temp; } if (min === undefined) min = 0; else if (typeof min === "function") { @@ -32,13 +40,13 @@ export default function randomInt( } if ( - !Number.isSafeInteger(min) || - typeof max === "number" && !Number.isSafeInteger(max) + !NumberIsSafeInteger(min) || + typeof max === "number" && !NumberIsSafeInteger(max) ) { throw new Error("max or min is not a Safe Number"); } - if (max - min > Math.pow(2, 48)) { + if (max - min > MathPow(2, 48)) { throw new RangeError("max - min should be less than 2^48!"); } @@ -46,8 +54,8 @@ export default function randomInt( throw new Error("Min is bigger than Max!"); } - min = Math.ceil(min); - max = Math.floor(max); + min = MathCeil(min); + max = MathFloor(max); const result = op_node_random_int(min, max); if (cb) { diff --git a/ext/node/polyfills/internal/crypto/keygen.ts b/ext/node/polyfills/internal/crypto/keygen.ts index a40c76c0d7..44bfd83277 100644 --- a/ext/node/polyfills/internal/crypto/keygen.ts +++ b/ext/node/polyfills/internal/crypto/keygen.ts @@ -29,6 +29,7 @@ import { } from "ext:deno_node/internal/validators.mjs"; import { Buffer } from "node:buffer"; import { KeyFormat, KeyType } from "ext:deno_node/internal/crypto/types.ts"; +import process from "node:process"; import { op_node_generate_dh_group_key, diff --git a/ext/node/polyfills/internal/crypto/random.ts b/ext/node/polyfills/internal/crypto/random.ts index 4219414dc1..a41b868190 100644 --- a/ext/node/polyfills/internal/crypto/random.ts +++ b/ext/node/polyfills/internal/crypto/random.ts @@ -38,6 +38,7 @@ import { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE, } from "ext:deno_node/internal/errors.ts"; +import { Buffer } from "node:buffer"; export { default as randomBytes } from "ext:deno_node/internal/crypto/_randomBytes.ts"; export { diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts index 51bd7a0250..61b53fa968 100644 --- a/ext/node/polyfills/internal/errors.ts +++ b/ext/node/polyfills/internal/errors.ts @@ -18,7 +18,7 @@ */ import { primordials } from "ext:core/mod.js"; -const { JSONStringify, SymbolFor } = primordials; +const { JSONStringify, SafeArrayIterator, SymbolFor } = primordials; import { format, inspect } from "ext:deno_node/internal/util/inspect.mjs"; import { codes } from "ext:deno_node/internal/error_codes.ts"; import { @@ -1874,6 +1874,11 @@ export class ERR_SOCKET_CLOSED extends NodeError { super("ERR_SOCKET_CLOSED", `Socket is closed`); } } +export class ERR_SOCKET_CONNECTION_TIMEOUT extends NodeError { + constructor() { + super("ERR_SOCKET_CONNECTION_TIMEOUT", `Socket connection timeout`); + } +} export class ERR_SOCKET_DGRAM_IS_CONNECTED extends NodeError { constructor() { super("ERR_SOCKET_DGRAM_IS_CONNECTED", `Already connected`); @@ -2385,6 +2390,15 @@ export class ERR_INVALID_RETURN_VALUE extends NodeTypeError { } } +export class ERR_NOT_IMPLEMENTED extends NodeError { + constructor(message?: string) { + super( + "ERR_NOT_IMPLEMENTED", + message ? `Not implemented: ${message}` : "Not implemented", + ); + } +} + export class ERR_INVALID_URL extends NodeTypeError { input: string; constructor(input: string) { @@ -2558,19 +2572,6 @@ export class ERR_FS_RMDIR_ENOTDIR extends NodeSystemError { } } -export class ERR_OS_NO_HOMEDIR extends NodeSystemError { - constructor() { - const code = isWindows ? "ENOENT" : "ENOTDIR"; - const ctx: NodeSystemErrorCtx = { - message: "not a directory", - syscall: "home", - code, - errno: isWindows ? osConstants.errno.ENOENT : osConstants.errno.ENOTDIR, - }; - super(code, ctx, "Path is not a directory"); - } -} - export class ERR_HTTP_SOCKET_ASSIGNED extends NodeError { constructor() { super( @@ -2646,11 +2647,30 @@ export function aggregateTwoErrors( } return innerError || outerError; } + +export class NodeAggregateError extends AggregateError { + code: string; + constructor(errors, message) { + super(new SafeArrayIterator(errors), message); + this.code = errors[0]?.code; + } + + get [kIsNodeError]() { + return true; + } + + // deno-lint-ignore adjacent-overload-signatures + get ["constructor"]() { + return AggregateError; + } +} + codes.ERR_IPC_CHANNEL_CLOSED = ERR_IPC_CHANNEL_CLOSED; codes.ERR_INVALID_ARG_TYPE = ERR_INVALID_ARG_TYPE; codes.ERR_INVALID_ARG_VALUE = ERR_INVALID_ARG_VALUE; codes.ERR_OUT_OF_RANGE = ERR_OUT_OF_RANGE; codes.ERR_SOCKET_BAD_PORT = ERR_SOCKET_BAD_PORT; +codes.ERR_SOCKET_CONNECTION_TIMEOUT = ERR_SOCKET_CONNECTION_TIMEOUT; codes.ERR_BUFFER_OUT_OF_BOUNDS = ERR_BUFFER_OUT_OF_BOUNDS; codes.ERR_UNKNOWN_ENCODING = ERR_UNKNOWN_ENCODING; codes.ERR_PARSE_ARGS_INVALID_OPTION_VALUE = ERR_PARSE_ARGS_INVALID_OPTION_VALUE; @@ -2851,6 +2871,7 @@ export default { ERR_INVALID_SYNC_FORK_INPUT, ERR_INVALID_THIS, ERR_INVALID_TUPLE, + ERR_NOT_IMPLEMENTED, ERR_INVALID_URI, ERR_INVALID_URL, ERR_INVALID_URL_SCHEME, diff --git a/ext/node/polyfills/internal/net.ts b/ext/node/polyfills/internal/net.ts index 144612626f..a3dcb3ed21 100644 --- a/ext/node/polyfills/internal/net.ts +++ b/ext/node/polyfills/internal/net.ts @@ -95,4 +95,5 @@ export function makeSyncWrite(fd: number) { }; } +export const kReinitializeHandle = Symbol("kReinitializeHandle"); export const normalizedArgsSymbol = Symbol("normalizedArgs"); diff --git a/ext/node/polyfills/internal/util/inspect.mjs b/ext/node/polyfills/internal/util/inspect.mjs index 3a61c387c0..ae797449bf 100644 --- a/ext/node/polyfills/internal/util/inspect.mjs +++ b/ext/node/polyfills/internal/util/inspect.mjs @@ -565,6 +565,19 @@ export function stripVTControlCharacters(str) { export function styleText(format, text) { validateString(text, "text"); + + if (Array.isArray(format)) { + for (let i = 0; i < format.length; i++) { + const item = format[i]; + const formatCodes = inspect.colors[item]; + if (formatCodes == null) { + validateOneOf(item, "format", Object.keys(inspect.colors)); + } + text = `\u001b[${formatCodes[0]}m${text}\u001b[${formatCodes[1]}m`; + } + return text; + } + const formatCodes = inspect.colors[format]; if (formatCodes == null) { validateOneOf(format, "format", Object.keys(inspect.colors)); diff --git a/ext/node/polyfills/internal_binding/_timingSafeEqual.ts b/ext/node/polyfills/internal_binding/_timingSafeEqual.ts index ff141fdbfc..559b7685b8 100644 --- a/ext/node/polyfills/internal_binding/_timingSafeEqual.ts +++ b/ext/node/polyfills/internal_binding/_timingSafeEqual.ts @@ -5,10 +5,11 @@ import { Buffer } from "node:buffer"; -function assert(cond) { - if (!cond) { - throw new Error("assertion failed"); +function toDataView(ab: ArrayBufferLike | ArrayBufferView): DataView { + if (ArrayBuffer.isView(ab)) { + return new DataView(ab.buffer, ab.byteOffset, ab.byteLength); } + return new DataView(ab); } /** Compare to array buffers or data views in a way that timing based attacks @@ -21,13 +22,11 @@ function stdTimingSafeEqual( return false; } if (!(a instanceof DataView)) { - a = new DataView(ArrayBuffer.isView(a) ? a.buffer : a); + a = toDataView(a); } if (!(b instanceof DataView)) { - b = new DataView(ArrayBuffer.isView(b) ? b.buffer : b); + b = toDataView(b); } - assert(a instanceof DataView); - assert(b instanceof DataView); const length = a.byteLength; let out = 0; let i = -1; @@ -41,7 +40,11 @@ export const timingSafeEqual = ( a: Buffer | DataView | ArrayBuffer, b: Buffer | DataView | ArrayBuffer, ): boolean => { - if (a instanceof Buffer) a = new DataView(a.buffer); - if (a instanceof Buffer) b = new DataView(a.buffer); + if (a instanceof Buffer) { + a = new DataView(a.buffer, a.byteOffset, a.byteLength); + } + if (b instanceof Buffer) { + b = new DataView(b.buffer, b.byteOffset, b.byteLength); + } return stdTimingSafeEqual(a, b); }; diff --git a/ext/node/polyfills/internal_binding/http_parser.ts b/ext/node/polyfills/internal_binding/http_parser.ts new file mode 100644 index 0000000000..bad10d9851 --- /dev/null +++ b/ext/node/polyfills/internal_binding/http_parser.ts @@ -0,0 +1,160 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { primordials } from "ext:core/mod.js"; +import { AsyncWrap } from "ext:deno_node/internal_binding/async_wrap.ts"; + +const { + ObjectDefineProperty, + ObjectEntries, + ObjectSetPrototypeOf, + SafeArrayIterator, +} = primordials; + +export const methods = [ + "DELETE", + "GET", + "HEAD", + "POST", + "PUT", + "CONNECT", + "OPTIONS", + "TRACE", + "COPY", + "LOCK", + "MKCOL", + "MOVE", + "PROPFIND", + "PROPPATCH", + "SEARCH", + "UNLOCK", + "BIND", + "REBIND", + "UNBIND", + "ACL", + "REPORT", + "MKACTIVITY", + "CHECKOUT", + "MERGE", + "M-SEARCH", + "NOTIFY", + "SUBSCRIBE", + "UNSUBSCRIBE", + "PATCH", + "PURGE", + "MKCALENDAR", + "LINK", + "UNLINK", + "SOURCE", + "QUERY", +]; + +export const allMethods = [ + "DELETE", + "GET", + "HEAD", + "POST", + "PUT", + "CONNECT", + "OPTIONS", + "TRACE", + "COPY", + "LOCK", + "MKCOL", + "MOVE", + "PROPFIND", + "PROPPATCH", + "SEARCH", + "UNLOCK", + "BIND", + "REBIND", + "UNBIND", + "ACL", + "REPORT", + "MKACTIVITY", + "CHECKOUT", + "MERGE", + "M-SEARCH", + "NOTIFY", + "SUBSCRIBE", + "UNSUBSCRIBE", + "PATCH", + "PURGE", + "MKCALENDAR", + "LINK", + "UNLINK", + "SOURCE", + "PRI", + "DESCRIBE", + "ANNOUNCE", + "SETUP", + "PLAY", + "PAUSE", + "TEARDOWN", + "GET_PARAMETER", + "SET_PARAMETER", + "REDIRECT", + "RECORD", + "FLUSH", + "QUERY", +]; + +export function HTTPParser() { +} + +ObjectSetPrototypeOf(HTTPParser.prototype, AsyncWrap.prototype); + +function defineProps(obj: object, props: Record) { + for (const entry of new SafeArrayIterator(ObjectEntries(props))) { + ObjectDefineProperty(obj, entry[0], { + __proto__: null, + value: entry[1], + enumerable: true, + writable: true, + configurable: true, + }); + } +} + +defineProps(HTTPParser, { + REQUEST: 1, + RESPONSE: 2, + kOnMessageBegin: 0, + kOnHeaders: 1, + kOnHeadersComplete: 2, + kOnBody: 3, + kOnMessageComplete: 4, + kOnExecute: 5, + kOnTimeout: 6, + kLenientNone: 0, + kLenientHeaders: 1, + kLenientChunkedLength: 2, + kLenientKeepAlive: 4, + kLenientTransferEncoding: 8, + kLenientVersion: 16, + kLenientDataAfterClose: 32, + kLenientOptionalLFAfterCR: 64, + kLenientOptionalCRLFAfterChunk: 128, + kLenientOptionalCRBeforeLF: 256, + kLenientSpacesAfterChunkSize: 512, + kLenientAll: 1023, +}); diff --git a/ext/node/polyfills/internal_binding/mod.ts b/ext/node/polyfills/internal_binding/mod.ts index f2d7f55bc7..ebbfc629f1 100644 --- a/ext/node/polyfills/internal_binding/mod.ts +++ b/ext/node/polyfills/internal_binding/mod.ts @@ -17,6 +17,7 @@ import * as types from "ext:deno_node/internal_binding/types.ts"; import * as udpWrap from "ext:deno_node/internal_binding/udp_wrap.ts"; import * as util from "ext:deno_node/internal_binding/util.ts"; import * as uv from "ext:deno_node/internal_binding/uv.ts"; +import * as httpParser from "ext:deno_node/internal_binding/http_parser.ts"; const modules = { "async_wrap": asyncWrap, @@ -32,7 +33,7 @@ const modules = { "fs_dir": {}, "fs_event_wrap": {}, "heap_utils": {}, - "http_parser": {}, + "http_parser": httpParser, icu: {}, inspector: {}, "js_stream": {}, diff --git a/ext/node/polyfills/internal_binding/tcp_wrap.ts b/ext/node/polyfills/internal_binding/tcp_wrap.ts index 973a1d1c01..d9f1c5356a 100644 --- a/ext/node/polyfills/internal_binding/tcp_wrap.ts +++ b/ext/node/polyfills/internal_binding/tcp_wrap.ts @@ -299,8 +299,10 @@ export class TCP extends ConnectionWrap { * @param noDelay * @return An error status code. */ - setNoDelay(_noDelay: boolean): number { - // TODO(bnoordhuis) https://github.com/denoland/deno/pull/13103 + setNoDelay(noDelay: boolean): number { + if (this[kStreamBaseField] && "setNoDelay" in this[kStreamBaseField]) { + this[kStreamBaseField].setNoDelay(noDelay); + } return 0; } diff --git a/ext/node/polyfills/internal_binding/uv.ts b/ext/node/polyfills/internal_binding/uv.ts index aa468a0a58..6cd70a7e85 100644 --- a/ext/node/polyfills/internal_binding/uv.ts +++ b/ext/node/polyfills/internal_binding/uv.ts @@ -530,10 +530,12 @@ export function mapSysErrnoToUvErrno(sysErrno: number): number { export const UV_EAI_MEMORY = codeMap.get("EAI_MEMORY")!; export const UV_EBADF = codeMap.get("EBADF")!; +export const UV_ECANCELED = codeMap.get("ECANCELED")!; export const UV_EEXIST = codeMap.get("EEXIST"); export const UV_EINVAL = codeMap.get("EINVAL")!; export const UV_ENOENT = codeMap.get("ENOENT"); export const UV_ENOTSOCK = codeMap.get("ENOTSOCK")!; +export const UV_ETIMEDOUT = codeMap.get("ETIMEDOUT")!; export const UV_UNKNOWN = codeMap.get("UNKNOWN")!; export function errname(errno: number): string { diff --git a/ext/node/polyfills/net.ts b/ext/node/polyfills/net.ts index 48e1d0de87..2b01125190 100644 --- a/ext/node/polyfills/net.ts +++ b/ext/node/polyfills/net.ts @@ -31,6 +31,7 @@ import { isIP, isIPv4, isIPv6, + kReinitializeHandle, normalizedArgsSymbol, } from "ext:deno_node/internal/net.ts"; import { Duplex } from "node:stream"; @@ -50,9 +51,11 @@ import { ERR_SERVER_ALREADY_LISTEN, ERR_SERVER_NOT_RUNNING, ERR_SOCKET_CLOSED, + ERR_SOCKET_CONNECTION_TIMEOUT, errnoException, exceptionWithHostPort, genericNodeError, + NodeAggregateError, uvExceptionWithHostPort, } from "ext:deno_node/internal/errors.ts"; import type { ErrnoException } from "ext:deno_node/internal/errors.ts"; @@ -80,6 +83,7 @@ import { Buffer } from "node:buffer"; import type { LookupOneOptions } from "ext:deno_node/internal/dns/utils.ts"; import { validateAbortSignal, + validateBoolean, validateFunction, validateInt32, validateNumber, @@ -100,13 +104,25 @@ import { ShutdownWrap } from "ext:deno_node/internal_binding/stream_wrap.ts"; import { assert } from "ext:deno_node/_util/asserts.ts"; import { isWindows } from "ext:deno_node/_util/os.ts"; import { ADDRCONFIG, lookup as dnsLookup } from "node:dns"; -import { codeMap } from "ext:deno_node/internal_binding/uv.ts"; +import { + codeMap, + UV_ECANCELED, + UV_ETIMEDOUT, +} from "ext:deno_node/internal_binding/uv.ts"; import { guessHandleType } from "ext:deno_node/internal_binding/util.ts"; import { debuglog } from "ext:deno_node/internal/util/debuglog.ts"; import type { DuplexOptions } from "ext:deno_node/_stream.d.ts"; import type { BufferEncoding } from "ext:deno_node/_global.d.ts"; import type { Abortable } from "ext:deno_node/_events.d.ts"; import { channel } from "node:diagnostics_channel"; +import { primordials } from "ext:core/mod.js"; + +const { + ArrayPrototypeIncludes, + ArrayPrototypePush, + FunctionPrototypeBind, + MathMax, +} = primordials; let debug = debuglog("net", (fn) => { debug = fn; @@ -120,6 +136,9 @@ const kBytesWritten = Symbol("kBytesWritten"); const DEFAULT_IPV4_ADDR = "0.0.0.0"; const DEFAULT_IPV6_ADDR = "::"; +let autoSelectFamilyDefault = true; +let autoSelectFamilyAttemptTimeoutDefault = 250; + type Handle = TCP | Pipe; interface HandleOptions { @@ -214,6 +233,8 @@ interface TcpSocketConnectOptions extends ConnectOptions { hints?: number; family?: number; lookup?: LookupFunction; + autoSelectFamily?: boolean | undefined; + autoSelectFamilyAttemptTimeout?: number | undefined; } interface IpcSocketConnectOptions extends ConnectOptions { @@ -316,12 +337,6 @@ export function _normalizeArgs(args: unknown[]): NormalizedArgs { return arr; } -function _isTCPConnectWrap( - req: TCPConnectWrap | PipeConnectWrap, -): req is TCPConnectWrap { - return "localAddress" in req && "localPort" in req; -} - function _afterConnect( status: number, // deno-lint-ignore no-explicit-any @@ -372,7 +387,7 @@ function _afterConnect( socket.connecting = false; let details; - if (_isTCPConnectWrap(req)) { + if (req.localAddress && req.localPort) { details = req.localAddress + ":" + req.localPort; } @@ -384,7 +399,7 @@ function _afterConnect( details, ); - if (_isTCPConnectWrap(req)) { + if (details) { ex.localAddress = req.localAddress; ex.localPort = req.localPort; } @@ -393,6 +408,107 @@ function _afterConnect( } } +function _createConnectionError(req, status) { + let details; + + if (req.localAddress && req.localPort) { + details = req.localAddress + ":" + req.localPort; + } + + const ex = exceptionWithHostPort( + status, + "connect", + req.address, + req.port, + details, + ); + if (details) { + ex.localAddress = req.localAddress; + ex.localPort = req.localPort; + } + + return ex; +} + +function _afterConnectMultiple( + context, + current, + status, + handle, + req, + readable, + writable, +) { + debug( + "connect/multiple: connection attempt to %s:%s completed with status %s", + req.address, + req.port, + status, + ); + + // Make sure another connection is not spawned + clearTimeout(context[kTimeout]); + + // One of the connection has completed and correctly dispatched but after timeout, ignore this one + if (status === 0 && current !== context.current - 1) { + debug( + "connect/multiple: ignoring successful but timedout connection to %s:%s", + req.address, + req.port, + ); + handle.close(); + return; + } + + const self = context.socket; + + // Some error occurred, add to the list of exceptions + if (status !== 0) { + const ex = _createConnectionError(req, status); + ArrayPrototypePush(context.errors, ex); + + self.emit( + "connectionAttemptFailed", + req.address, + req.port, + req.addressType, + ex, + ); + + // Try the next address, unless we were aborted + if (context.socket.connecting) { + _internalConnectMultiple(context, status === UV_ECANCELED); + } + + return; + } + + _afterConnect(status, self._handle, req, readable, writable); +} + +function _internalConnectMultipleTimeout(context, req, handle) { + debug( + "connect/multiple: connection to %s:%s timed out", + req.address, + req.port, + ); + context.socket.emit( + "connectionAttemptTimeout", + req.address, + req.port, + req.addressType, + ); + + req.oncomplete = undefined; + ArrayPrototypePush(context.errors, _createConnectionError(req, UV_ETIMEDOUT)); + handle.close(); + + // Try the next address, unless we were aborted + if (context.socket.connecting) { + _internalConnectMultiple(context); + } +} + function _checkBindError(err: number, port: number, handle: TCP) { // EADDRINUSE may not be reported until we call `listen()` or `connect()`. // To complicate matters, a failed `bind()` followed by `listen()` or `connect()` @@ -495,6 +611,131 @@ function _internalConnect( } } +function _internalConnectMultiple(context, canceled?: boolean) { + clearTimeout(context[kTimeout]); + const self = context.socket; + + // We were requested to abort. Stop all operations + if (self._aborted) { + return; + } + + // All connections have been tried without success, destroy with error + if (canceled || context.current === context.addresses.length) { + if (context.errors.length === 0) { + self.destroy(new ERR_SOCKET_CONNECTION_TIMEOUT()); + return; + } + + self.destroy(new NodeAggregateError(context.errors)); + return; + } + + assert(self.connecting); + + const current = context.current++; + + if (current > 0) { + self[kReinitializeHandle](new TCP(TCPConstants.SOCKET)); + } + + const { localPort, port, flags } = context; + const { address, family: addressType } = context.addresses[current]; + let localAddress; + let err; + + if (localPort) { + if (addressType === 4) { + localAddress = DEFAULT_IPV4_ADDR; + err = self._handle.bind(localAddress, localPort); + } else { // addressType === 6 + localAddress = DEFAULT_IPV6_ADDR; + err = self._handle.bind6(localAddress, localPort, flags); + } + + debug( + "connect/multiple: binding to localAddress: %s and localPort: %d (addressType: %d)", + localAddress, + localPort, + addressType, + ); + + err = _checkBindError(err, localPort, self._handle); + if (err) { + ArrayPrototypePush( + context.errors, + exceptionWithHostPort(err, "bind", localAddress, localPort), + ); + _internalConnectMultiple(context); + return; + } + } + + debug( + "connect/multiple: attempting to connect to %s:%d (addressType: %d)", + address, + port, + addressType, + ); + self.emit("connectionAttempt", address, port, addressType); + + const req = new TCPConnectWrap(); + req.oncomplete = FunctionPrototypeBind( + _afterConnectMultiple, + undefined, + context, + current, + ); + req.address = address; + req.port = port; + req.localAddress = localAddress; + req.localPort = localPort; + req.addressType = addressType; + + ArrayPrototypePush( + self.autoSelectFamilyAttemptedAddresses, + `${address}:${port}`, + ); + + if (addressType === 4) { + err = self._handle.connect(req, address, port); + } else { + err = self._handle.connect6(req, address, port); + } + + if (err) { + const sockname = self._getsockname(); + let details; + + if (sockname) { + details = sockname.address + ":" + sockname.port; + } + + const ex = exceptionWithHostPort(err, "connect", address, port, details); + ArrayPrototypePush(context.errors, ex); + + self.emit("connectionAttemptFailed", address, port, addressType, ex); + _internalConnectMultiple(context); + return; + } + + if (current < context.addresses.length - 1) { + debug( + "connect/multiple: setting the attempt timeout to %d ms", + context.timeout, + ); + + // If the attempt has not returned an error, start the connection timer + context[kTimeout] = setTimeout( + _internalConnectMultipleTimeout, + context.timeout, + context, + req, + self._handle, + ); + } +} + // Provide a better error message when we call end() as a result // of the other side sending a FIN. The standard "write after end" // is overly vague, and makes it seem like the user's code is to blame. @@ -597,7 +838,7 @@ function _lookupAndConnect( ) { const { localAddress, localPort } = options; const host = options.host || "localhost"; - let { port } = options; + let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options; if (localAddress && !isIP(localAddress)) { throw new ERR_INVALID_IP_ADDRESS(localAddress); @@ -621,6 +862,22 @@ function _lookupAndConnect( port |= 0; + if (autoSelectFamily != null) { + validateBoolean(autoSelectFamily, "options.autoSelectFamily"); + } else { + autoSelectFamily = autoSelectFamilyDefault; + } + + if (autoSelectFamilyAttemptTimeout !== undefined) { + validateInt32(autoSelectFamilyAttemptTimeout); + + if (autoSelectFamilyAttemptTimeout < 10) { + autoSelectFamilyAttemptTimeout = 10; + } + } else { + autoSelectFamilyAttemptTimeout = autoSelectFamilyAttemptTimeoutDefault; + } + // If host is an IP, skip performing a lookup const addressType = isIP(host); if (addressType) { @@ -649,6 +906,7 @@ function _lookupAndConnect( const dnsOpts = { family: options.family, hints: options.hints || 0, + all: false, }; if ( @@ -665,6 +923,31 @@ function _lookupAndConnect( self._host = host; const lookup = options.lookup || dnsLookup; + if ( + dnsOpts.family !== 4 && dnsOpts.family !== 6 && !localAddress && + autoSelectFamily + ) { + debug("connect: autodetecting"); + + dnsOpts.all = true; + defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () { + _lookupAndConnectMultiple( + self, + asyncIdSymbol, + lookup, + host, + options, + dnsOpts, + port, + localAddress, + localPort, + autoSelectFamilyAttemptTimeout, + ); + }); + + return; + } + defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () { lookup( host, @@ -719,6 +1002,143 @@ function _lookupAndConnect( }); } +function _lookupAndConnectMultiple( + self: Socket, + asyncIdSymbol: number, + // deno-lint-ignore no-explicit-any + lookup: any, + host: string, + options: TcpSocketConnectOptions, + dnsopts, + port: number, + localAddress: string, + localPort: number, + timeout: number | undefined, +) { + defaultTriggerAsyncIdScope(self[asyncIdSymbol], function emitLookup() { + lookup(host, dnsopts, function emitLookup(err, addresses) { + // It's possible we were destroyed while looking this up. + // XXX it would be great if we could cancel the promise returned by + // the look up. + if (!self.connecting) { + return; + } else if (err) { + self.emit("lookup", err, undefined, undefined, host); + + // net.createConnection() creates a net.Socket object and immediately + // calls net.Socket.connect() on it (that's us). There are no event + // listeners registered yet so defer the error event to the next tick. + nextTick(_connectErrorNT, self, err); + return; + } + + // Filter addresses by only keeping the one which are either IPv4 or IPV6. + // The first valid address determines which group has preference on the + // alternate family sorting which happens later. + const validAddresses = [[], []]; + const validIps = [[], []]; + let destinations; + for (let i = 0, l = addresses.length; i < l; i++) { + const address = addresses[i]; + const { address: ip, family: addressType } = address; + self.emit("lookup", err, ip, addressType, host); + // It's possible we were destroyed while looking this up. + if (!self.connecting) { + return; + } + if (isIP(ip) && (addressType === 4 || addressType === 6)) { + destinations ||= addressType === 6 ? { 6: 0, 4: 1 } : { 4: 0, 6: 1 }; + + const destination = destinations[addressType]; + + // Only try an address once + if (!ArrayPrototypeIncludes(validIps[destination], ip)) { + ArrayPrototypePush(validAddresses[destination], address); + ArrayPrototypePush(validIps[destination], ip); + } + } + } + + // When no AAAA or A records are available, fail on the first one + if (!validAddresses[0].length && !validAddresses[1].length) { + const { address: firstIp, family: firstAddressType } = addresses[0]; + + if (!isIP(firstIp)) { + err = new ERR_INVALID_IP_ADDRESS(firstIp); + nextTick(_connectErrorNT, self, err); + } else if (firstAddressType !== 4 && firstAddressType !== 6) { + err = new ERR_INVALID_ADDRESS_FAMILY( + firstAddressType, + options.host, + options.port, + ); + nextTick(_connectErrorNT, self, err); + } + + return; + } + + // Sort addresses alternating families + const toAttempt = []; + for ( + let i = 0, + l = MathMax(validAddresses[0].length, validAddresses[1].length); + i < l; + i++ + ) { + if (i in validAddresses[0]) { + ArrayPrototypePush(toAttempt, validAddresses[0][i]); + } + if (i in validAddresses[1]) { + ArrayPrototypePush(toAttempt, validAddresses[1][i]); + } + } + + if (toAttempt.length === 1) { + debug( + "connect/multiple: only one address found, switching back to single connection", + ); + const { address: ip, family: addressType } = toAttempt[0]; + + self._unrefTimer(); + defaultTriggerAsyncIdScope( + self[asyncIdSymbol], + _internalConnect, + self, + ip, + port, + addressType, + localAddress, + localPort, + ); + + return; + } + + self.autoSelectFamilyAttemptedAddresses = []; + debug("connect/multiple: will try the following addresses", toAttempt); + + const context = { + socket: self, + addresses: toAttempt, + current: 0, + port, + localPort, + timeout, + [kTimeout]: null, + errors: [], + }; + + self._unrefTimer(); + defaultTriggerAsyncIdScope( + self[asyncIdSymbol], + _internalConnectMultiple, + context, + ); + }); + }); +} + function _afterShutdown(this: ShutdownWrap) { // deno-lint-ignore no-explicit-any const self: any = this.handle[ownerSymbol]; @@ -777,6 +1197,7 @@ export class Socket extends Duplex { _host: string | null = null; // deno-lint-ignore no-explicit-any _parent: any = null; + autoSelectFamilyAttemptedAddresses: AddressInfo[] | undefined = undefined; constructor(options: SocketOptions | number) { if (typeof options === "number") { @@ -1546,6 +1967,16 @@ export class Socket extends Duplex { set _handle(v: Handle | null) { this[kHandle] = v; } + + // deno-lint-ignore no-explicit-any + [kReinitializeHandle](handle: any) { + this._handle?.close(); + + this._handle = handle; + this._handle[ownerSymbol] = this; + + _initSocketHandle(this); + } } export const Stream = Socket; @@ -1593,6 +2024,33 @@ export function connect(...args: unknown[]) { export const createConnection = connect; +/** https://docs.deno.com/api/node/net/#namespace_getdefaultautoselectfamily */ +export function getDefaultAutoSelectFamily() { + return autoSelectFamilyDefault; +} + +/** https://docs.deno.com/api/node/net/#namespace_setdefaultautoselectfamily */ +export function setDefaultAutoSelectFamily(value: boolean) { + validateBoolean(value, "value"); + autoSelectFamilyDefault = value; +} + +/** https://docs.deno.com/api/node/net/#namespace_getdefaultautoselectfamilyattempttimeout */ +export function getDefaultAutoSelectFamilyAttemptTimeout() { + return autoSelectFamilyAttemptTimeoutDefault; +} + +/** https://docs.deno.com/api/node/net/#namespace_setdefaultautoselectfamilyattempttimeout */ +export function setDefaultAutoSelectFamilyAttemptTimeout(value: number) { + validateInt32(value, "value", 1); + + if (value < 10) { + value = 10; + } + + autoSelectFamilyAttemptTimeoutDefault = value; +} + export interface ListenOptions extends Abortable { fd?: number; port?: number | undefined; @@ -2478,15 +2936,19 @@ export { BlockList, isIP, isIPv4, isIPv6, SocketAddress }; export default { _createServerHandle, _normalizeArgs, - isIP, - isIPv4, - isIPv6, BlockList, - SocketAddress, connect, createConnection, createServer, + getDefaultAutoSelectFamily, + getDefaultAutoSelectFamilyAttemptTimeout, + isIP, + isIPv4, + isIPv6, Server, + setDefaultAutoSelectFamily, + setDefaultAutoSelectFamilyAttemptTimeout, Socket, + SocketAddress, Stream, }; diff --git a/ext/node/polyfills/os.ts b/ext/node/polyfills/os.ts index e47e8679ec..edc89ed2c3 100644 --- a/ext/node/polyfills/os.ts +++ b/ext/node/polyfills/os.ts @@ -28,16 +28,17 @@ import { op_homedir, op_node_os_get_priority, op_node_os_set_priority, - op_node_os_username, + op_node_os_user_info, } from "ext:core/ops"; import { validateIntegerRange } from "ext:deno_node/_utils.ts"; import process from "node:process"; 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 { osUptime } from "ext:runtime/30_os.js"; import { Buffer } from "ext:deno_node/internal/buffer.mjs"; +import { primordials } from "ext:core/mod.js"; +const { StringPrototypeEndsWith, StringPrototypeSlice } = primordials; export const constants = os; @@ -136,6 +137,8 @@ export function arch(): string { (uptime as any)[Symbol.toPrimitive] = (): number => uptime(); // deno-lint-ignore no-explicit-any (machine as any)[Symbol.toPrimitive] = (): string => machine(); +// deno-lint-ignore no-explicit-any +(tmpdir as any)[Symbol.toPrimitive] = (): string | null => tmpdir(); export function cpus(): CPUCoreInfo[] { return op_cpus(); @@ -268,26 +271,27 @@ export function setPriority(pid: number, priority?: number) { export function tmpdir(): string | null { /* This follows the node js implementation, but has a few differences: - * On windows, if none of the environment variables are defined, - we return null. - * On unix we use a plain Deno.env.get, instead of safeGetenv, + * We use a plain Deno.env.get, instead of safeGetenv, which special cases setuid binaries. - * Node removes a single trailing / or \, we remove all. */ if (isWindows) { - const temp = Deno.env.get("TEMP") || Deno.env.get("TMP"); - if (temp) { - return temp.replace(/(? 1 && StringPrototypeEndsWith(temp, "\\") && + !StringPrototypeEndsWith(temp, ":\\") + ) { + temp = StringPrototypeSlice(temp, 0, -1); } - const base = Deno.env.get("SYSTEMROOT") || Deno.env.get("WINDIR"); - if (base) { - return base + "\\temp"; - } - return null; + + return temp; } else { // !isWindows - const temp = Deno.env.get("TMPDIR") || Deno.env.get("TMP") || + let temp = Deno.env.get("TMPDIR") || Deno.env.get("TMP") || Deno.env.get("TEMP") || "/tmp"; - return temp.replace(/(? 1 && StringPrototypeEndsWith(temp, "/")) { + temp = StringPrototypeSlice(temp, 0, -1); + } + return temp; } } @@ -320,7 +324,6 @@ export function uptime(): number { return osUptime(); } -/** Not yet implemented */ export function userInfo( options: UserInfoOptions = { encoding: "utf-8" }, ): UserInfo { @@ -331,20 +334,10 @@ export function userInfo( uid = -1; gid = -1; } - - // TODO(@crowlKats): figure out how to do this correctly: - // The value of homedir returned by os.userInfo() is provided by the operating system. - // This differs from the result of os.homedir(), which queries environment - // variables for the home directory before falling back to the operating system response. - let _homedir = homedir(); - if (!_homedir) { - throw new ERR_OS_NO_HOMEDIR(); - } - let shell = isWindows ? null : (Deno.env.get("SHELL") || null); - let username = op_node_os_username(); + let { username, homedir, shell } = op_node_os_user_info(uid); if (options?.encoding === "buffer") { - _homedir = _homedir ? Buffer.from(_homedir) : _homedir; + homedir = homedir ? Buffer.from(homedir) : homedir; shell = shell ? Buffer.from(shell) : shell; username = Buffer.from(username); } @@ -352,7 +345,7 @@ export function userInfo( return { uid, gid, - homedir: _homedir, + homedir, shell, username, }; diff --git a/ext/node/polyfills/process.ts b/ext/node/polyfills/process.ts index 3dc6ce61aa..647376d5cf 100644 --- a/ext/node/polyfills/process.ts +++ b/ext/node/polyfills/process.ts @@ -15,7 +15,7 @@ import { import { warnNotImplemented } from "ext:deno_node/_utils.ts"; import { EventEmitter } from "node:events"; -import Module from "node:module"; +import Module, { getBuiltinModule } from "node:module"; import { report } from "ext:deno_node/internal/process/report.ts"; import { validateString } from "ext:deno_node/internal/validators.mjs"; import { @@ -38,7 +38,15 @@ import { versions, } from "ext:deno_node/_process/process.ts"; import { _exiting } from "ext:deno_node/_process/exiting.ts"; -export { _nextTick as nextTick, chdir, cwd, env, version, versions }; +export { + _nextTick as nextTick, + chdir, + cwd, + env, + getBuiltinModule, + version, + versions, +}; import { createWritableStdioStream, initStdin, @@ -520,9 +528,7 @@ Process.prototype.on = function ( } else if ( event !== "SIGBREAK" && event !== "SIGINT" && Deno.build.os === "windows" ) { - // Ignores all signals except SIGBREAK and SIGINT on windows. - // deno-lint-ignore no-console - console.warn(`Ignoring signal "${event}" on Windows`); + // TODO(#26331): Ignores all signals except SIGBREAK and SIGINT on windows. } else { EventEmitter.prototype.on.call(this, event, listener); Deno.addSignalListener(event as Deno.Signal, listener); @@ -730,6 +736,8 @@ Process.prototype.getegid = getegid; /** This method is removed on Windows */ Process.prototype.geteuid = geteuid; +Process.prototype.getBuiltinModule = getBuiltinModule; + // TODO(kt3k): Implement this when we added -e option to node compat mode Process.prototype._eval = undefined; @@ -911,7 +919,7 @@ Object.defineProperty(argv, "1", { if (Deno.mainModule?.startsWith("file:")) { return pathFromURL(new URL(Deno.mainModule)); } else { - return join(Deno.cwd(), "$deno$node.js"); + return join(Deno.cwd(), "$deno$node.mjs"); } }, }); diff --git a/ext/node/polyfills/util.ts b/ext/node/polyfills/util.ts index 586fae17e9..d82b288b03 100644 --- a/ext/node/polyfills/util.ts +++ b/ext/node/polyfills/util.ts @@ -39,6 +39,7 @@ import { formatWithOptions, inspect, stripVTControlCharacters, + styleText, } from "ext:deno_node/internal/util/inspect.mjs"; import { codes } from "ext:deno_node/internal/error_codes.ts"; import types from "node:util/types"; @@ -63,6 +64,7 @@ export { parseArgs, promisify, stripVTControlCharacters, + styleText, types, }; @@ -354,4 +356,5 @@ export default { debuglog, debug: debuglog, isDeepStrictEqual, + styleText, }; diff --git a/ext/node/polyfills/v8.ts b/ext/node/polyfills/v8.ts index 5849f3ccc9..9df199865e 100644 --- a/ext/node/polyfills/v8.ts +++ b/ext/node/polyfills/v8.ts @@ -313,20 +313,6 @@ export class DefaultDeserializer extends Deserializer { ); } } -export const promiseHooks = { - onInit() { - notImplemented("v8.promiseHooks.onInit"); - }, - onSettled() { - notImplemented("v8.promiseHooks.onSetttled"); - }, - onBefore() { - notImplemented("v8.promiseHooks.onBefore"); - }, - createHook() { - notImplemented("v8.promiseHooks.createHook"); - }, -}; export default { cachedDataVersionTag, getHeapCodeStatistics, @@ -343,5 +329,4 @@ export default { Deserializer, DefaultSerializer, DefaultDeserializer, - promiseHooks, }; diff --git a/ext/node/polyfills/vm.js b/ext/node/polyfills/vm.js index 183ddad2f4..b64c847c58 100644 --- a/ext/node/polyfills/vm.js +++ b/ext/node/polyfills/vm.js @@ -182,6 +182,7 @@ function getContextOptions(options) { let defaultContextNameIndex = 1; export function createContext( + // deno-lint-ignore prefer-primordials contextObject = {}, options = { __proto__: null }, ) { diff --git a/ext/node/polyfills/worker_threads.ts b/ext/node/polyfills/worker_threads.ts index 5ff4446f73..d4b75fb30c 100644 --- a/ext/node/polyfills/worker_threads.ts +++ b/ext/node/polyfills/worker_threads.ts @@ -302,8 +302,8 @@ class NodeWorker extends EventEmitter { if (this.#status !== "TERMINATED") { this.#status = "TERMINATED"; op_host_terminate_worker(this.#id); + this.emit("exit", 0); } - this.emit("exit", 0); return PromiseResolve(0); } @@ -422,7 +422,11 @@ internals.__initWorkerThreads = ( parentPort.once = function (this: ParentPort, name, listener) { // deno-lint-ignore no-explicit-any - const _listener = (ev: any) => listener(ev.data); + const _listener = (ev: any) => { + const message = ev.data; + patchMessagePortIfFound(message); + return listener(message); + }; listeners.set(listener, _listener); this.addEventListener(name, _listener); return this; @@ -494,7 +498,9 @@ export function receiveMessageOnPort(port: MessagePort): object | undefined { port[MessagePortReceiveMessageOnPortSymbol] = true; const data = op_message_port_recv_message_sync(port[MessagePortIdSymbol]); if (data === null) return undefined; - return { message: deserializeJsMessageData(data)[0] }; + const message = deserializeJsMessageData(data)[0]; + patchMessagePortIfFound(message); + return { message }; } class NodeMessageChannel { diff --git a/ext/node/polyfills/zlib.ts b/ext/node/polyfills/zlib.ts index 3fe5f8bbd9..6e5d02b5be 100644 --- a/ext/node/polyfills/zlib.ts +++ b/ext/node/polyfills/zlib.ts @@ -40,6 +40,58 @@ import { createBrotliCompress, createBrotliDecompress, } from "ext:deno_node/_brotli.js"; +import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; +import { validateUint32 } from "ext:deno_node/internal/validators.mjs"; +import { op_zlib_crc32 } from "ext:core/ops"; +import { core, primordials } from "ext:core/mod.js"; +import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; +const { + Uint8Array, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteOffset, + DataViewPrototypeGetBuffer, + DataViewPrototypeGetByteLength, + DataViewPrototypeGetByteOffset, +} = primordials; +const { isTypedArray, isDataView } = core; + +const enc = new TextEncoder(); +const toU8 = (input) => { + if (typeof input === "string") { + return enc.encode(input); + } + + if (isTypedArray(input)) { + return new Uint8Array( + TypedArrayPrototypeGetBuffer(input), + TypedArrayPrototypeGetByteOffset(input), + TypedArrayPrototypeGetByteLength(input), + ); + } else if (isDataView(input)) { + return new Uint8Array( + DataViewPrototypeGetBuffer(input), + DataViewPrototypeGetByteOffset(input), + DataViewPrototypeGetByteLength(input), + ); + } + + return input; +}; + +export function crc32(data, value = 0) { + if (typeof data !== "string" && !isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE("data", [ + "Buffer", + "TypedArray", + "DataView", + "string", + ], data); + } + validateUint32(value, "value"); + + return op_zlib_crc32(toU8(data), value); +} export class Options { constructor() { @@ -87,6 +139,7 @@ export default { BrotliOptions, codes, constants, + crc32, createBrotliCompress, createBrotliDecompress, createDeflate, diff --git a/ext/node_resolver/package_json.rs b/ext/node_resolver/package_json.rs deleted file mode 100644 index de750f1d7e..0000000000 --- a/ext/node_resolver/package_json.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -use deno_package_json::PackageJson; -use deno_package_json::PackageJsonRc; -use std::cell::RefCell; -use std::collections::HashMap; -use std::io::ErrorKind; -use std::path::Path; -use std::path::PathBuf; - -use crate::errors::PackageJsonLoadError; - -// use a thread local cache so that workers have their own distinct cache -thread_local! { - static CACHE: RefCell> = RefCell::new(HashMap::new()); -} - -pub struct PackageJsonThreadLocalCache; - -impl PackageJsonThreadLocalCache { - pub fn clear() { - CACHE.with(|cache| cache.borrow_mut().clear()); - } -} - -impl deno_package_json::PackageJsonCache for PackageJsonThreadLocalCache { - fn get(&self, path: &Path) -> Option { - CACHE.with(|cache| cache.borrow().get(path).cloned()) - } - - fn set(&self, path: PathBuf, package_json: PackageJsonRc) { - CACHE.with(|cache| cache.borrow_mut().insert(path, package_json)); - } -} - -/// Helper to load a package.json file using the thread local cache -/// in node_resolver. -pub fn load_pkg_json( - fs: &dyn deno_package_json::fs::DenoPkgJsonFs, - path: &Path, -) -> Result, PackageJsonLoadError> { - let result = - PackageJson::load_from_path(path, fs, Some(&PackageJsonThreadLocalCache)); - match result { - Ok(pkg_json) => Ok(Some(pkg_json)), - Err(deno_package_json::PackageJsonLoadError::Io { source, .. }) - if source.kind() == ErrorKind::NotFound => - { - Ok(None) - } - Err(err) => Err(PackageJsonLoadError(err)), - } -} diff --git a/ext/node_resolver/path.rs b/ext/node_resolver/path.rs deleted file mode 100644 index ece270cd91..0000000000 --- a/ext/node_resolver/path.rs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; - -use url::Url; - -/// Extension to path_clean::PathClean -pub trait PathClean { - fn clean(&self) -> T; -} - -impl PathClean for PathBuf { - fn clean(&self) -> PathBuf { - fn is_clean_path(path: &Path) -> bool { - let path = path.to_string_lossy(); - let mut current_index = 0; - while let Some(index) = path[current_index..].find("\\.") { - let trailing_index = index + current_index + 2; - let mut trailing_chars = path[trailing_index..].chars(); - match trailing_chars.next() { - Some('.') => match trailing_chars.next() { - Some('/') | Some('\\') | None => { - return false; - } - _ => {} - }, - Some('/') | Some('\\') => { - return false; - } - _ => {} - } - current_index = trailing_index; - } - true - } - - let path = path_clean::PathClean::clean(self); - if cfg!(windows) && !is_clean_path(&path) { - // temporary workaround because path_clean::PathClean::clean is - // not good enough on windows - let mut components = Vec::new(); - - for component in path.components() { - match component { - Component::CurDir => { - // skip - } - Component::ParentDir => { - let maybe_last_component = components.pop(); - if !matches!(maybe_last_component, Some(Component::Normal(_))) { - panic!("Error normalizing: {}", path.display()); - } - } - Component::Normal(_) | Component::RootDir | Component::Prefix(_) => { - components.push(component); - } - } - } - components.into_iter().collect::() - } else { - path - } - } -} - -pub(crate) fn to_file_specifier(path: &Path) -> Url { - match Url::from_file_path(path) { - Ok(url) => url, - Err(_) => panic!("Invalid path: {}", path.display()), - } -} - -// todo(dsherret): we have the below code also in deno_core and it -// would be good to somehow re-use it in both places (we don't want -// to create a dependency on deno_core here) - -#[cfg(not(windows))] -#[inline] -pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { - path -} - -/// Strips the unc prefix (ex. \\?\) from Windows paths. -#[cfg(windows)] -pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { - use std::path::Component; - use std::path::Prefix; - - let mut components = path.components(); - match components.next() { - Some(Component::Prefix(prefix)) => { - match prefix.kind() { - // \\?\device - Prefix::Verbatim(device) => { - let mut path = PathBuf::new(); - path.push(format!(r"\\{}\", device.to_string_lossy())); - path.extend(components.filter(|c| !matches!(c, Component::RootDir))); - path - } - // \\?\c:\path - Prefix::VerbatimDisk(_) => { - let mut path = PathBuf::new(); - path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", "")); - path.extend(components); - path - } - // \\?\UNC\hostname\share_name\path - Prefix::VerbatimUNC(hostname, share_name) => { - let mut path = PathBuf::new(); - path.push(format!( - r"\\{}\{}\", - hostname.to_string_lossy(), - share_name.to_string_lossy() - )); - path.extend(components.filter(|c| !matches!(c, Component::RootDir))); - path - } - _ => path, - } - } - _ => path, - } -} - -#[cfg(test)] -mod test { - #[cfg(windows)] - #[test] - fn test_path_clean() { - use super::*; - - run_test("C:\\test\\./file.txt", "C:\\test\\file.txt"); - run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt"); - run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt"); - - fn run_test(input: &str, expected: &str) { - assert_eq!(PathBuf::from(input).clean(), PathBuf::from(expected)); - } - } - - #[cfg(windows)] - #[test] - fn test_strip_unc_prefix() { - use std::path::PathBuf; - - run_test(r"C:\", r"C:\"); - run_test(r"C:\test\file.txt", r"C:\test\file.txt"); - - run_test(r"\\?\C:\", r"C:\"); - run_test(r"\\?\C:\test\file.txt", r"C:\test\file.txt"); - - run_test(r"\\.\C:\", r"\\.\C:\"); - run_test(r"\\.\C:\Test\file.txt", r"\\.\C:\Test\file.txt"); - - run_test(r"\\?\UNC\localhost\", r"\\localhost"); - run_test(r"\\?\UNC\localhost\c$\", r"\\localhost\c$"); - run_test( - r"\\?\UNC\localhost\c$\Windows\file.txt", - r"\\localhost\c$\Windows\file.txt", - ); - run_test(r"\\?\UNC\wsl$\deno.json", r"\\wsl$\deno.json"); - - run_test(r"\\?\server1", r"\\server1"); - run_test(r"\\?\server1\e$\", r"\\server1\e$\"); - run_test( - r"\\?\server1\e$\test\file.txt", - r"\\server1\e$\test\file.txt", - ); - - fn run_test(input: &str, expected: &str) { - assert_eq!( - super::strip_unc_prefix(PathBuf::from(input)), - PathBuf::from(expected) - ); - } - } -} diff --git a/ext/tls/Cargo.toml b/ext/tls/Cargo.toml index 08a9802df5..943fc8413e 100644 --- a/ext/tls/Cargo.toml +++ b/ext/tls/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_tls" -version = "0.155.0" +version = "0.164.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -21,5 +21,6 @@ rustls-pemfile.workspace = true rustls-tokio-stream.workspace = true rustls-webpki.workspace = true serde.workspace = true +thiserror.workspace = true tokio.workspace = true webpki-roots.workspace = true diff --git a/ext/tls/lib.rs b/ext/tls/lib.rs index c4d548ccf2..883d2995e4 100644 --- a/ext/tls/lib.rs +++ b/ext/tls/lib.rs @@ -9,17 +9,12 @@ pub use rustls_tokio_stream::*; pub use webpki; pub use webpki_roots; -use deno_core::anyhow::anyhow; -use deno_core::error::custom_error; -use deno_core::error::AnyError; - use rustls::client::danger::HandshakeSignatureValid; use rustls::client::danger::ServerCertVerified; use rustls::client::danger::ServerCertVerifier; use rustls::client::WebPkiServerVerifier; use rustls::ClientConfig; use rustls::DigitallySignedStruct; -use rustls::Error; use rustls::RootCertStore; use rustls_pemfile::certs; use rustls_pemfile::ec_private_keys; @@ -35,12 +30,30 @@ use std::sync::Arc; mod tls_key; pub use tls_key::*; +#[derive(Debug, thiserror::Error)] +pub enum TlsError { + #[error(transparent)] + Rustls(#[from] rustls::Error), + #[error("Unable to add pem file to certificate store: {0}")] + UnableAddPemFileToCert(std::io::Error), + #[error("Unable to decode certificate")] + CertInvalid, + #[error("No certificates found in certificate data")] + CertsNotFound, + #[error("No keys found in key data")] + KeysNotFound, + #[error("Unable to decode key")] + KeyDecode, +} + /// Lazily resolves the root cert store. /// /// This was done because the root cert store is not needed in all cases /// and takes a bit of time to initialize. pub trait RootCertStoreProvider: Send + Sync { - fn get_or_try_init(&self) -> Result<&RootCertStore, AnyError>; + fn get_or_try_init( + &self, + ) -> Result<&RootCertStore, deno_core::error::AnyError>; } // This extension has no runtime apis, it only exports some shared native functions. @@ -77,7 +90,7 @@ impl ServerCertVerifier for NoCertificateVerification { server_name: &rustls::pki_types::ServerName<'_>, ocsp_response: &[u8], now: rustls::pki_types::UnixTime, - ) -> Result { + ) -> Result { if self.ic_allowlist.is_empty() { return Ok(ServerCertVerified::assertion()); } @@ -89,7 +102,9 @@ impl ServerCertVerifier for NoCertificateVerification { _ => { // NOTE(bartlomieju): `ServerName` is a non-exhaustive enum // so we have this catch all errors here. - return Err(Error::General("Unknown `ServerName` variant".to_string())); + return Err(rustls::Error::General( + "Unknown `ServerName` variant".to_string(), + )); } }; if self.ic_allowlist.contains(&dns_name_or_ip_address) { @@ -110,7 +125,7 @@ impl ServerCertVerifier for NoCertificateVerification { message: &[u8], cert: &rustls::pki_types::CertificateDer, dss: &DigitallySignedStruct, - ) -> Result { + ) -> Result { if self.ic_allowlist.is_empty() { return Ok(HandshakeSignatureValid::assertion()); } @@ -126,7 +141,7 @@ impl ServerCertVerifier for NoCertificateVerification { message: &[u8], cert: &rustls::pki_types::CertificateDer, dss: &DigitallySignedStruct, - ) -> Result { + ) -> Result { if self.ic_allowlist.is_empty() { return Ok(HandshakeSignatureValid::assertion()); } @@ -178,7 +193,7 @@ pub fn create_client_config( unsafely_ignore_certificate_errors: Option>, maybe_cert_chain_and_key: TlsKeys, socket_use: SocketUse, -) -> Result { +) -> Result { if let Some(ic_allowlist) = unsafely_ignore_certificate_errors { let client_config = ClientConfig::builder() .dangerous() @@ -214,10 +229,7 @@ pub fn create_client_config( root_cert_store.add(cert)?; } Err(e) => { - return Err(anyhow!( - "Unable to add pem file to certificate store: {}", - e - )); + return Err(TlsError::UnableAddPemFileToCert(e)); } } } @@ -255,74 +267,61 @@ fn add_alpn(client: &mut ClientConfig, socket_use: SocketUse) { pub fn load_certs( reader: &mut dyn BufRead, -) -> Result>, AnyError> { +) -> Result>, TlsError> { let certs: Result, _> = certs(reader).collect(); - let certs = certs - .map_err(|_| custom_error("InvalidData", "Unable to decode certificate"))?; + let certs = certs.map_err(|_| TlsError::CertInvalid)?; if certs.is_empty() { - return Err(cert_not_found_err()); + return Err(TlsError::CertsNotFound); } Ok(certs) } -fn key_decode_err() -> AnyError { - custom_error("InvalidData", "Unable to decode key") -} - -fn key_not_found_err() -> AnyError { - custom_error("InvalidData", "No keys found in key data") -} - -fn cert_not_found_err() -> AnyError { - custom_error("InvalidData", "No certificates found in certificate data") -} - /// Starts with -----BEGIN RSA PRIVATE KEY----- fn load_rsa_keys( mut bytes: &[u8], -) -> Result>, AnyError> { +) -> Result>, TlsError> { let keys: Result, _> = rsa_private_keys(&mut bytes).collect(); - let keys = keys.map_err(|_| key_decode_err())?; + let keys = keys.map_err(|_| TlsError::KeyDecode)?; Ok(keys.into_iter().map(PrivateKeyDer::Pkcs1).collect()) } /// Starts with -----BEGIN EC PRIVATE KEY----- fn load_ec_keys( mut bytes: &[u8], -) -> Result>, AnyError> { +) -> Result>, TlsError> { let keys: Result, std::io::Error> = ec_private_keys(&mut bytes).collect(); - let keys2 = keys.map_err(|_| key_decode_err())?; + let keys2 = keys.map_err(|_| TlsError::KeyDecode)?; Ok(keys2.into_iter().map(PrivateKeyDer::Sec1).collect()) } /// Starts with -----BEGIN PRIVATE KEY----- fn load_pkcs8_keys( mut bytes: &[u8], -) -> Result>, AnyError> { +) -> Result>, TlsError> { let keys: Result, std::io::Error> = pkcs8_private_keys(&mut bytes).collect(); - let keys2 = keys.map_err(|_| key_decode_err())?; + let keys2 = keys.map_err(|_| TlsError::KeyDecode)?; Ok(keys2.into_iter().map(PrivateKeyDer::Pkcs8).collect()) } fn filter_invalid_encoding_err( - to_be_filtered: Result, -) -> Result { + to_be_filtered: Result, +) -> Result { match to_be_filtered { - Err(Error::InvalidCertificate(rustls::CertificateError::BadEncoding)) => { - Ok(HandshakeSignatureValid::assertion()) - } + Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::BadEncoding, + )) => Ok(HandshakeSignatureValid::assertion()), res => res, } } pub fn load_private_keys( bytes: &[u8], -) -> Result>, AnyError> { +) -> Result>, TlsError> { let mut keys = load_rsa_keys(bytes)?; if keys.is_empty() { @@ -334,7 +333,7 @@ pub fn load_private_keys( } if keys.is_empty() { - return Err(key_not_found_err()); + return Err(TlsError::KeysNotFound); } Ok(keys) diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs index 66fac86f87..b7baa604b9 100644 --- a/ext/tls/tls_key.rs +++ b/ext/tls/tls_key.rs @@ -11,8 +11,6 @@ //! key lookup can handle closing one end of the pair, in which case they will just //! attempt to clean up the associated resources. -use deno_core::anyhow::anyhow; -use deno_core::error::AnyError; use deno_core::futures::future::poll_fn; use deno_core::futures::future::Either; use deno_core::futures::FutureExt; @@ -33,7 +31,19 @@ use tokio::sync::oneshot; use webpki::types::CertificateDer; use webpki::types::PrivateKeyDer; -type ErrorType = Rc; +#[derive(Debug, thiserror::Error)] +pub enum TlsKeyError { + #[error(transparent)] + Rustls(#[from] rustls::Error), + #[error("Failed: {0}")] + Failed(ErrorType), + #[error(transparent)] + JoinError(#[from] tokio::task::JoinError), + #[error(transparent)] + RecvError(#[from] tokio::sync::broadcast::error::RecvError), +} + +type ErrorType = Arc>; /// A TLS certificate/private key pair. /// see https://docs.rs/rustls-pki-types/latest/rustls_pki_types/#cloning-private-keys @@ -114,7 +124,7 @@ impl TlsKeyResolver { &self, sni: String, alpn: Vec>, - ) -> Result, AnyError> { + ) -> Result, TlsKeyError> { let key = self.resolve(sni).await?; let mut tls_config = ServerConfig::builder() @@ -183,7 +193,7 @@ impl TlsKeyResolver { pub fn resolve( &self, sni: String, - ) -> impl Future> { + ) -> impl Future> { let mut cache = self.inner.cache.borrow_mut(); let mut recv = match cache.get(&sni) { None => { @@ -194,7 +204,7 @@ impl TlsKeyResolver { } Some(TlsKeyState::Resolving(recv)) => recv.resubscribe(), Some(TlsKeyState::Resolved(res)) => { - return Either::Left(ready(res.clone().map_err(|_| anyhow!("Failed")))); + return Either::Left(ready(res.clone().map_err(TlsKeyError::Failed))); } }; drop(cache); @@ -212,7 +222,7 @@ impl TlsKeyResolver { // Someone beat us to it } } - res.map_err(|_| anyhow!("Failed")) + res.map_err(TlsKeyError::Failed) }); Either::Right(async move { handle.await? }) } @@ -247,13 +257,13 @@ impl TlsKeyLookup { } /// Resolve a previously polled item. - pub fn resolve(&self, sni: String, res: Result) { + pub fn resolve(&self, sni: String, res: Result) { _ = self .pending .borrow_mut() .remove(&sni) .unwrap() - .send(res.map_err(Rc::new)); + .send(res.map_err(|e| Arc::new(e.into_boxed_str()))); } } diff --git a/ext/url/00_url.js b/ext/url/00_url.js index 577caba902..ec875da768 100644 --- a/ext/url/00_url.js +++ b/ext/url/00_url.js @@ -139,7 +139,7 @@ class URLSearchParams { throw new TypeError( `${prefix}: Item ${ i + 0 - } in the parameter list does have length 2 exactly.`, + } in the parameter list does have length 2 exactly`, ); } return [pair[0], pair[1]]; diff --git a/ext/url/01_urlpattern.js b/ext/url/01_urlpattern.js index 58a6d6bce0..6e27563089 100644 --- a/ext/url/01_urlpattern.js +++ b/ext/url/01_urlpattern.js @@ -31,6 +31,7 @@ import * as webidl from "ext:deno_webidl/00_webidl.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; const _components = Symbol("components"); +const urlPatternSettings = { groupStringFallback: false }; /** * @typedef Components @@ -349,7 +350,11 @@ class URLPattern { const groups = res.groups; for (let i = 0; i < groupList.length; ++i) { // TODO(lucacasonato): this is vulnerable to override mistake - groups[groupList[i]] = match[i + 1] ?? ""; // TODO(@crowlKats): remove fallback for 2.0 + if (urlPatternSettings.groupStringFallback) { + groups[groupList[i]] = match[i + 1] ?? ""; + } else { + groups[groupList[i]] = match[i + 1]; + } } break; } @@ -422,4 +427,4 @@ webidl.converters.URLPatternOptions = webidl }, ]); -export { URLPattern }; +export { URLPattern, urlPatternSettings }; diff --git a/ext/url/Cargo.toml b/ext/url/Cargo.toml index 7f86e14456..557a4669e6 100644 --- a/ext/url/Cargo.toml +++ b/ext/url/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_url" -version = "0.168.0" +version = "0.177.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -15,6 +15,7 @@ path = "lib.rs" [dependencies] deno_core.workspace = true +thiserror.workspace = true urlpattern = "0.3.0" [dev-dependencies] diff --git a/ext/url/lib.rs b/ext/url/lib.rs index 6869d656b5..f8946532ae 100644 --- a/ext/url/lib.rs +++ b/ext/url/lib.rs @@ -15,6 +15,8 @@ use std::path::PathBuf; use crate::urlpattern::op_urlpattern_parse; use crate::urlpattern::op_urlpattern_process_match_input; +pub use urlpattern::UrlPatternError; + deno_core::extension!( deno_url, deps = [deno_webidl], diff --git a/ext/url/urlpattern.rs b/ext/url/urlpattern.rs index b6d9a13828..7d4e8ee71b 100644 --- a/ext/url/urlpattern.rs +++ b/ext/url/urlpattern.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use urlpattern::quirks; @@ -9,21 +7,23 @@ use urlpattern::quirks::MatchInput; use urlpattern::quirks::StringOrInit; use urlpattern::quirks::UrlPattern; +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct UrlPatternError(urlpattern::Error); + #[op2] #[serde] pub fn op_urlpattern_parse( #[serde] input: StringOrInit, #[string] base_url: Option, #[serde] options: urlpattern::UrlPatternOptions, -) -> Result { - let init = urlpattern::quirks::process_construct_pattern_input( - input, - base_url.as_deref(), - ) - .map_err(|e| type_error(e.to_string()))?; +) -> Result { + let init = + quirks::process_construct_pattern_input(input, base_url.as_deref()) + .map_err(UrlPatternError)?; - let pattern = urlpattern::quirks::parse_pattern(init, options) - .map_err(|e| type_error(e.to_string()))?; + let pattern = + quirks::parse_pattern(init, options).map_err(UrlPatternError)?; Ok(pattern) } @@ -33,14 +33,14 @@ pub fn op_urlpattern_parse( pub fn op_urlpattern_process_match_input( #[serde] input: StringOrInit, #[string] base_url: Option, -) -> Result, AnyError> { - let res = urlpattern::quirks::process_match_input(input, base_url.as_deref()) - .map_err(|e| type_error(e.to_string()))?; +) -> Result, UrlPatternError> { + let res = quirks::process_match_input(input, base_url.as_deref()) + .map_err(UrlPatternError)?; let (input, inputs) = match res { Some((input, inputs)) => (input, inputs), None => return Ok(None), }; - Ok(urlpattern::quirks::parse_match_input(input).map(|input| (input, inputs))) + Ok(quirks::parse_match_input(input).map(|input| (input, inputs))) } diff --git a/ext/web/02_timers.js b/ext/web/02_timers.js index 89acaca42b..6058febd59 100644 --- a/ext/web/02_timers.js +++ b/ext/web/02_timers.js @@ -1,12 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { core, primordials } from "ext:core/mod.js"; -import { op_defer, op_now } from "ext:core/ops"; +import { op_defer } from "ext:core/ops"; const { - Uint8Array, - Uint32Array, PromisePrototypeThen, - TypedArrayPrototypeGetBuffer, TypeError, indirectEval, ReflectApply, @@ -18,13 +15,6 @@ const { import * as webidl from "ext:deno_webidl/00_webidl.js"; -const hrU8 = new Uint8Array(8); -const hr = new Uint32Array(TypedArrayPrototypeGetBuffer(hrU8)); -function opNow() { - op_now(hrU8); - return (hr[0] * 1000 + hr[1] / 1e6); -} - // --------------------------------------------------------------------------- function checkThis(thisArg) { @@ -151,7 +141,6 @@ export { clearInterval, clearTimeout, defer, - opNow, refTimer, setImmediate, setInterval, diff --git a/ext/web/13_message_port.js b/ext/web/13_message_port.js index 04697d6aa8..cf72c43e6f 100644 --- a/ext/web/13_message_port.js +++ b/ext/web/13_message_port.js @@ -22,6 +22,7 @@ const { Symbol, SymbolFor, SymbolIterator, + PromiseResolve, SafeArrayIterator, TypeError, } = primordials; @@ -41,7 +42,10 @@ import { import { isDetachedBuffer } from "./06_streams.js"; import { DOMException } from "./01_dom_exception.js"; -let messageEventListenerCount = 0; +// counter of how many message ports are actively refed +// either due to the existence of "message" event listeners or +// explicit calls to ref/unref (in the case of node message ports) +let refedMessagePortsCount = 0; class MessageChannel { /** @type {MessagePort} */ @@ -93,6 +97,7 @@ const MessagePortReceiveMessageOnPortSymbol = Symbol( ); const _enabled = Symbol("enabled"); const _refed = Symbol("refed"); +const _messageEventListenerCount = Symbol("messageEventListenerCount"); const nodeWorkerThreadCloseCb = Symbol("nodeWorkerThreadCloseCb"); const nodeWorkerThreadCloseCbInvoked = Symbol("nodeWorkerThreadCloseCbInvoked"); export const refMessagePort = Symbol("refMessagePort"); @@ -109,6 +114,9 @@ function createMessagePort(id) { port[core.hostObjectBrand] = core.hostObjectBrand; setEventTargetData(port); port[_id] = id; + port[_enabled] = false; + port[_messageEventListenerCount] = 0; + port[_refed] = false; return port; } @@ -122,12 +130,18 @@ function nodeWorkerThreadMaybeInvokeCloseCb(port) { } } +const _isRefed = Symbol("isRefed"); +const _dataPromise = Symbol("dataPromise"); + class MessagePort extends EventTarget { /** @type {number | null} */ [_id] = null; /** @type {boolean} */ [_enabled] = false; [_refed] = false; + /** @type {Promise | undefined} */ + [_dataPromise] = undefined; + [_messageEventListenerCount] = 0; constructor() { super(); @@ -193,24 +207,21 @@ class MessagePort extends EventTarget { this[_enabled] = true; while (true) { if (this[_id] === null) break; - // Exit if no message event listeners are present in Node compat mode. - if ( - typeof this[nodeWorkerThreadCloseCb] == "function" && - messageEventListenerCount === 0 - ) break; let data; try { - data = await op_message_port_recv_message( + this[_dataPromise] = op_message_port_recv_message( this[_id], ); + if ( + typeof this[nodeWorkerThreadCloseCb] === "function" && + !this[_refed] + ) { + core.unrefOpPromise(this[_dataPromise]); + } + data = await this[_dataPromise]; + this[_dataPromise] = undefined; } catch (err) { if (ObjectPrototypeIsPrototypeOf(InterruptedPrototype, err)) { - // If we were interrupted, check if the interruption is coming - // from `receiveMessageOnPort` API from Node compat, if so, continue. - if (this[MessagePortReceiveMessageOnPortSymbol]) { - this[MessagePortReceiveMessageOnPortSymbol] = false; - continue; - } break; } nodeWorkerThreadMaybeInvokeCloseCb(this); @@ -246,12 +257,26 @@ class MessagePort extends EventTarget { } [refMessagePort](ref) { - if (ref && !this[_refed]) { - this[_refed] = true; - messageEventListenerCount++; - } else if (!ref && this[_refed]) { - this[_refed] = false; - messageEventListenerCount = 0; + if (ref) { + if (!this[_refed]) { + refedMessagePortsCount++; + if ( + this[_dataPromise] + ) { + core.refOpPromise(this[_dataPromise]); + } + this[_refed] = true; + } + } else if (!ref) { + if (this[_refed]) { + refedMessagePortsCount--; + if ( + this[_dataPromise] + ) { + core.unrefOpPromise(this[_dataPromise]); + } + this[_refed] = false; + } } } @@ -266,15 +291,20 @@ class MessagePort extends EventTarget { removeEventListener(...args) { if (args[0] == "message") { - messageEventListenerCount--; + if (--this[_messageEventListenerCount] === 0 && this[_refed]) { + refedMessagePortsCount--; + this[_refed] = false; + } } super.removeEventListener(...new SafeArrayIterator(args)); } addEventListener(...args) { if (args[0] == "message") { - messageEventListenerCount++; - if (!this[_refed]) this[_refed] = true; + if (++this[_messageEventListenerCount] === 1 && !this[_refed]) { + refedMessagePortsCount++; + this[_refed] = true; + } } super.addEventListener(...new SafeArrayIterator(args)); } @@ -295,7 +325,17 @@ class MessagePort extends EventTarget { } defineEventHandler(MessagePort.prototype, "message", function (self) { - self.start(); + if (self[nodeWorkerThreadCloseCb]) { + (async () => { + // delay `start()` until he end of this event loop turn, to give `receiveMessageOnPort` + // a chance to receive a message first. this is primarily to resolve an issue with + // a pattern used in `npm:piscina` that results in an indefinite hang + await PromiseResolve(); + self.start(); + })(); + } else { + self.start(); + } }); defineEventHandler(MessagePort.prototype, "messageerror"); @@ -463,12 +503,12 @@ function structuredClone(value, options) { export { deserializeJsMessageData, MessageChannel, - messageEventListenerCount, MessagePort, MessagePortIdSymbol, MessagePortPrototype, MessagePortReceiveMessageOnPortSymbol, nodeWorkerThreadCloseCb, + refedMessagePortsCount, serializeJsMessageData, structuredClone, }; diff --git a/ext/web/15_performance.js b/ext/web/15_performance.js index ea55572781..9e0e310a57 100644 --- a/ext/web/15_performance.js +++ b/ext/web/15_performance.js @@ -1,6 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { primordials } from "ext:core/mod.js"; +import { op_now, op_time_origin } from "ext:core/ops"; const { ArrayPrototypeFilter, ArrayPrototypePush, @@ -10,19 +11,34 @@ const { Symbol, SymbolFor, TypeError, + TypedArrayPrototypeGetBuffer, + Uint8Array, + Uint32Array, } = primordials; import * as webidl from "ext:deno_webidl/00_webidl.js"; import { structuredClone } from "./02_structured_clone.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; import { EventTarget } from "./02_event.js"; -import { opNow } from "./02_timers.js"; import { DOMException } from "./01_dom_exception.js"; const illegalConstructorKey = Symbol("illegalConstructorKey"); let performanceEntries = []; let timeOrigin; +const hrU8 = new Uint8Array(8); +const hr = new Uint32Array(TypedArrayPrototypeGetBuffer(hrU8)); + +function setTimeOrigin() { + op_time_origin(hrU8); + timeOrigin = hr[0] * 1000 + hr[1] / 1e6; +} + +function now() { + op_now(hrU8); + return hr[0] * 1000 + hr[1] / 1e6; +} + webidl.converters["PerformanceMarkOptions"] = webidl .createDictionaryConverter( "PerformanceMarkOptions", @@ -90,10 +106,6 @@ webidl.converters["DOMString or PerformanceMeasureOptions"] = ( return webidl.converters.DOMString(V, prefix, context, opts); }; -function setTimeOrigin(origin) { - timeOrigin = origin; -} - function findMostRecent( name, type, @@ -135,8 +147,6 @@ function filterByNameType( ); } -const now = opNow; - const _name = Symbol("[[name]]"); const _entryType = Symbol("[[entryType]]"); const _startTime = Symbol("[[startTime]]"); diff --git a/ext/web/Cargo.toml b/ext/web/Cargo.toml index 7fd12632d7..db28d0e578 100644 --- a/ext/web/Cargo.toml +++ b/ext/web/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_web" -version = "0.199.0" +version = "0.208.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -23,6 +23,7 @@ encoding_rs.workspace = true flate2 = { workspace = true, features = ["default"] } futures.workspace = true serde = "1.0.149" +thiserror.workspace = true tokio.workspace = true uuid = { workspace = true, features = ["serde"] } diff --git a/ext/web/blob.rs b/ext/web/blob.rs index 392f36acb8..bc64a0f27e 100644 --- a/ext/web/blob.rs +++ b/ext/web/blob.rs @@ -7,8 +7,6 @@ use std::rc::Rc; use std::sync::Arc; use async_trait::async_trait; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::parking_lot::Mutex; use deno_core::url::Url; @@ -19,6 +17,18 @@ use serde::Deserialize; use serde::Serialize; use uuid::Uuid; +#[derive(Debug, thiserror::Error)] +pub enum BlobError { + #[error("Blob part not found")] + BlobPartNotFound, + #[error("start + len can not be larger than blob part size")] + SizeLargerThanBlobPart, + #[error("Blob URLs are not supported in this context")] + BlobURLsNotSupported, + #[error(transparent)] + Url(#[from] deno_core::url::ParseError), +} + use crate::Location; pub type PartMap = HashMap>; @@ -96,18 +106,18 @@ pub struct Blob { impl Blob { // TODO(lucacsonato): this should be a stream! - pub async fn read_all(&self) -> Result, AnyError> { + pub async fn read_all(&self) -> Vec { let size = self.size(); let mut bytes = Vec::with_capacity(size); for part in &self.parts { - let chunk = part.read().await?; + let chunk = part.read().await; bytes.extend_from_slice(chunk); } assert_eq!(bytes.len(), size); - Ok(bytes) + bytes } fn size(&self) -> usize { @@ -122,7 +132,7 @@ impl Blob { #[async_trait] pub trait BlobPart: Debug { // TODO(lucacsonato): this should be a stream! - async fn read(&self) -> Result<&[u8], AnyError>; + async fn read(&self) -> &[u8]; fn size(&self) -> usize; } @@ -137,8 +147,8 @@ impl From> for InMemoryBlobPart { #[async_trait] impl BlobPart for InMemoryBlobPart { - async fn read(&self) -> Result<&[u8], AnyError> { - Ok(&self.0) + async fn read(&self) -> &[u8] { + &self.0 } fn size(&self) -> usize { @@ -155,9 +165,9 @@ pub struct SlicedBlobPart { #[async_trait] impl BlobPart for SlicedBlobPart { - async fn read(&self) -> Result<&[u8], AnyError> { - let original = self.part.read().await?; - Ok(&original[self.start..self.start + self.len]) + async fn read(&self) -> &[u8] { + let original = self.part.read().await; + &original[self.start..self.start + self.len] } fn size(&self) -> usize { @@ -189,19 +199,17 @@ pub fn op_blob_slice_part( state: &mut OpState, #[serde] id: Uuid, #[serde] options: SliceOptions, -) -> Result { +) -> Result { let blob_store = state.borrow::>(); let part = blob_store .get_part(&id) - .ok_or_else(|| type_error("Blob part not found"))?; + .ok_or(BlobError::BlobPartNotFound)?; let SliceOptions { start, len } = options; let size = part.size(); if start + len > size { - return Err(type_error( - "start + len can not be larger than blob part size", - )); + return Err(BlobError::SizeLargerThanBlobPart); } let sliced_part = SlicedBlobPart { part, start, len }; @@ -215,14 +223,14 @@ pub fn op_blob_slice_part( pub async fn op_blob_read_part( state: Rc>, #[serde] id: Uuid, -) -> Result { +) -> Result { let part = { let state = state.borrow(); let blob_store = state.borrow::>(); blob_store.get_part(&id) } - .ok_or_else(|| type_error("Blob part not found"))?; - let buf = part.read().await?; + .ok_or(BlobError::BlobPartNotFound)?; + let buf = part.read().await; Ok(ToJsBuffer::from(buf.to_vec())) } @@ -238,13 +246,13 @@ pub fn op_blob_create_object_url( state: &mut OpState, #[string] media_type: String, #[serde] part_ids: Vec, -) -> Result { +) -> Result { let mut parts = Vec::with_capacity(part_ids.len()); let blob_store = state.borrow::>(); for part_id in part_ids { let part = blob_store .get_part(&part_id) - .ok_or_else(|| type_error("Blob part not found"))?; + .ok_or(BlobError::BlobPartNotFound)?; parts.push(part); } @@ -263,7 +271,7 @@ pub fn op_blob_create_object_url( pub fn op_blob_revoke_object_url( state: &mut OpState, #[string] url: &str, -) -> Result<(), AnyError> { +) -> Result<(), BlobError> { let url = Url::parse(url)?; let blob_store = state.borrow::>(); blob_store.remove_object_url(&url); @@ -287,15 +295,15 @@ pub struct ReturnBlobPart { pub fn op_blob_from_object_url( state: &mut OpState, #[string] url: String, -) -> Result, AnyError> { +) -> Result, BlobError> { let url = Url::parse(&url)?; if url.scheme() != "blob" { return Ok(None); } - let blob_store = state.try_borrow::>().ok_or_else(|| { - type_error("Blob URLs are not supported in this context.") - })?; + let blob_store = state + .try_borrow::>() + .ok_or(BlobError::BlobURLsNotSupported)?; if let Some(blob) = blob_store.get_object_url(url) { let parts = blob .parts diff --git a/ext/web/compression.rs b/ext/web/compression.rs index b9ae12ef17..6967009915 100644 --- a/ext/web/compression.rs +++ b/ext/web/compression.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use flate2::write::DeflateDecoder; use flate2::write::DeflateEncoder; @@ -13,6 +11,18 @@ use flate2::Compression; use std::cell::RefCell; use std::io::Write; +#[derive(Debug, thiserror::Error)] +pub enum CompressionError { + #[error("Unsupported format")] + UnsupportedFormat, + #[error("resource is closed")] + ResourceClosed, + #[error(transparent)] + IoTypeError(std::io::Error), + #[error(transparent)] + Io(std::io::Error), +} + #[derive(Debug)] struct CompressionResource(RefCell>); @@ -34,7 +44,7 @@ enum Inner { pub fn op_compression_new( #[string] format: &str, is_decoder: bool, -) -> Result { +) -> Result { let w = Vec::new(); let inner = match (format, is_decoder) { ("deflate", true) => Inner::DeflateDecoder(ZlibDecoder::new(w)), @@ -49,7 +59,7 @@ pub fn op_compression_new( ("gzip", false) => { Inner::GzEncoder(GzEncoder::new(w, Compression::default())) } - _ => return Err(type_error("Unsupported format")), + _ => return Err(CompressionError::UnsupportedFormat), }; Ok(CompressionResource(RefCell::new(Some(inner)))) } @@ -59,40 +69,38 @@ pub fn op_compression_new( pub fn op_compression_write( #[cppgc] resource: &CompressionResource, #[anybuffer] input: &[u8], -) -> Result, AnyError> { +) -> Result, CompressionError> { let mut inner = resource.0.borrow_mut(); - let inner = inner - .as_mut() - .ok_or_else(|| type_error("resource is closed"))?; + let inner = inner.as_mut().ok_or(CompressionError::ResourceClosed)?; let out: Vec = match &mut *inner { Inner::DeflateDecoder(d) => { - d.write_all(input).map_err(|e| type_error(e.to_string()))?; - d.flush()?; + d.write_all(input).map_err(CompressionError::IoTypeError)?; + d.flush().map_err(CompressionError::Io)?; d.get_mut().drain(..) } Inner::DeflateEncoder(d) => { - d.write_all(input).map_err(|e| type_error(e.to_string()))?; - d.flush()?; + d.write_all(input).map_err(CompressionError::IoTypeError)?; + d.flush().map_err(CompressionError::Io)?; d.get_mut().drain(..) } Inner::DeflateRawDecoder(d) => { - d.write_all(input).map_err(|e| type_error(e.to_string()))?; - d.flush()?; + d.write_all(input).map_err(CompressionError::IoTypeError)?; + d.flush().map_err(CompressionError::Io)?; d.get_mut().drain(..) } Inner::DeflateRawEncoder(d) => { - d.write_all(input).map_err(|e| type_error(e.to_string()))?; - d.flush()?; + d.write_all(input).map_err(CompressionError::IoTypeError)?; + d.flush().map_err(CompressionError::Io)?; d.get_mut().drain(..) } Inner::GzDecoder(d) => { - d.write_all(input).map_err(|e| type_error(e.to_string()))?; - d.flush()?; + d.write_all(input).map_err(CompressionError::IoTypeError)?; + d.flush().map_err(CompressionError::Io)?; d.get_mut().drain(..) } Inner::GzEncoder(d) => { - d.write_all(input).map_err(|e| type_error(e.to_string()))?; - d.flush()?; + d.write_all(input).map_err(CompressionError::IoTypeError)?; + d.flush().map_err(CompressionError::Io)?; d.get_mut().drain(..) } } @@ -105,27 +113,27 @@ pub fn op_compression_write( pub fn op_compression_finish( #[cppgc] resource: &CompressionResource, report_errors: bool, -) -> Result, AnyError> { +) -> Result, CompressionError> { let inner = resource .0 .borrow_mut() .take() - .ok_or_else(|| type_error("resource is closed"))?; + .ok_or(CompressionError::ResourceClosed)?; let out = match inner { Inner::DeflateDecoder(d) => { - d.finish().map_err(|e| type_error(e.to_string())) + d.finish().map_err(CompressionError::IoTypeError) } Inner::DeflateEncoder(d) => { - d.finish().map_err(|e| type_error(e.to_string())) + d.finish().map_err(CompressionError::IoTypeError) } Inner::DeflateRawDecoder(d) => { - d.finish().map_err(|e| type_error(e.to_string())) + d.finish().map_err(CompressionError::IoTypeError) } Inner::DeflateRawEncoder(d) => { - d.finish().map_err(|e| type_error(e.to_string())) + d.finish().map_err(CompressionError::IoTypeError) } - Inner::GzDecoder(d) => d.finish().map_err(|e| type_error(e.to_string())), - Inner::GzEncoder(d) => d.finish().map_err(|e| type_error(e.to_string())), + Inner::GzDecoder(d) => d.finish().map_err(CompressionError::IoTypeError), + Inner::GzEncoder(d) => d.finish().map_err(CompressionError::IoTypeError), }; match out { Err(err) => { diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 3977379a5d..af0fc2c276 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -6,9 +6,6 @@ mod message_port; mod stream_resource; mod timers; -use deno_core::error::range_error; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::url::Url; use deno_core::v8; @@ -22,10 +19,14 @@ use encoding_rs::DecoderResult; use encoding_rs::Encoding; use std::borrow::Cow; use std::cell::RefCell; -use std::fmt; use std::path::PathBuf; use std::sync::Arc; +pub use blob::BlobError; +pub use compression::CompressionError; +pub use message_port::MessagePortError; +pub use stream_resource::StreamResourceError; + use crate::blob::op_blob_create_object_url; use crate::blob::op_blob_create_part; use crate::blob::op_blob_from_object_url; @@ -51,7 +52,8 @@ pub use crate::message_port::Transferable; use crate::timers::op_defer; use crate::timers::op_now; -use crate::timers::StartTime; +use crate::timers::op_time_origin; +pub use crate::timers::StartTime; pub use crate::timers::TimersPermission; deno_core::extension!(deno_web, @@ -83,6 +85,7 @@ deno_core::extension!(deno_web, compression::op_compression_write, compression::op_compression_finish, op_now

, + op_time_origin

, op_defer, stream_resource::op_readable_stream_resource_allocate, stream_resource::op_readable_stream_resource_allocate_sized, @@ -122,13 +125,31 @@ deno_core::extension!(deno_web, if let Some(location) = options.maybe_location { state.put(Location(location)); } - state.put(StartTime::now()); + state.put(StartTime::default()); } ); +#[derive(Debug, thiserror::Error)] +pub enum WebError { + #[error("Failed to decode base64")] + Base64Decode, + #[error("The encoding label provided ('{0}') is invalid.")] + InvalidEncodingLabel(String), + #[error("buffer exceeds maximum length")] + BufferTooLong, + #[error("Value too large to decode")] + ValueTooLarge, + #[error("Provided buffer too small")] + BufferTooSmall, + #[error("The encoded data is not valid")] + DataInvalid, + #[error(transparent)] + DataError(#[from] v8::DataError), +} + #[op2] #[serde] -fn op_base64_decode(#[string] input: String) -> Result { +fn op_base64_decode(#[string] input: String) -> Result { let mut s = input.into_bytes(); let decoded_len = forgiving_base64_decode_inplace(&mut s)?; s.truncate(decoded_len); @@ -137,7 +158,7 @@ fn op_base64_decode(#[string] input: String) -> Result { #[op2] #[serde] -fn op_base64_atob(#[serde] mut s: ByteString) -> Result { +fn op_base64_atob(#[serde] mut s: ByteString) -> Result { let decoded_len = forgiving_base64_decode_inplace(&mut s)?; s.truncate(decoded_len); Ok(s) @@ -147,11 +168,9 @@ fn op_base64_atob(#[serde] mut s: ByteString) -> Result { #[inline] fn forgiving_base64_decode_inplace( input: &mut [u8], -) -> Result { - let error = - || DomExceptionInvalidCharacterError::new("Failed to decode base64"); - let decoded = - base64_simd::forgiving_decode_inplace(input).map_err(|_| error())?; +) -> Result { + let decoded = base64_simd::forgiving_decode_inplace(input) + .map_err(|_| WebError::Base64Decode)?; Ok(decoded.len()) } @@ -177,13 +196,9 @@ fn forgiving_base64_encode(s: &[u8]) -> String { #[string] fn op_encoding_normalize_label( #[string] label: String, -) -> Result { +) -> Result { let encoding = Encoding::for_label_no_replacement(label.as_bytes()) - .ok_or_else(|| { - range_error(format!( - "The encoding label provided ('{label}') is invalid." - )) - })?; + .ok_or(WebError::InvalidEncodingLabel(label))?; Ok(encoding.name().to_lowercase()) } @@ -192,7 +207,7 @@ fn op_encoding_decode_utf8<'a>( scope: &mut v8::HandleScope<'a>, #[anybuffer] zero_copy: &[u8], ignore_bom: bool, -) -> Result, AnyError> { +) -> Result, WebError> { let buf = &zero_copy; let buf = if !ignore_bom @@ -216,7 +231,7 @@ fn op_encoding_decode_utf8<'a>( // - https://github.com/v8/v8/blob/d68fb4733e39525f9ff0a9222107c02c28096e2a/include/v8.h#L3277-L3278 match v8::String::new_from_utf8(scope, buf, v8::NewStringType::Normal) { Some(text) => Ok(text), - None => Err(type_error("buffer exceeds maximum length")), + None => Err(WebError::BufferTooLong), } } @@ -227,12 +242,9 @@ fn op_encoding_decode_single( #[string] label: String, fatal: bool, ignore_bom: bool, -) -> Result { - let encoding = Encoding::for_label(label.as_bytes()).ok_or_else(|| { - range_error(format!( - "The encoding label provided ('{label}') is invalid." - )) - })?; +) -> Result { + let encoding = Encoding::for_label(label.as_bytes()) + .ok_or(WebError::InvalidEncodingLabel(label))?; let mut decoder = if ignore_bom { encoding.new_decoder_without_bom_handling() @@ -242,7 +254,7 @@ fn op_encoding_decode_single( let max_buffer_length = decoder .max_utf16_buffer_length(data.len()) - .ok_or_else(|| range_error("Value too large to decode."))?; + .ok_or(WebError::ValueTooLarge)?; let mut output = vec![0; max_buffer_length]; @@ -254,12 +266,8 @@ fn op_encoding_decode_single( output.truncate(written); Ok(output.into()) } - DecoderResult::OutputFull => { - Err(range_error("Provided buffer too small.")) - } - DecoderResult::Malformed(_, _) => { - Err(type_error("The encoded data is not valid.")) - } + DecoderResult::OutputFull => Err(WebError::BufferTooSmall), + DecoderResult::Malformed(_, _) => Err(WebError::DataInvalid), } } else { let (result, _, written, _) = @@ -269,7 +277,7 @@ fn op_encoding_decode_single( output.truncate(written); Ok(output.into()) } - CoderResult::OutputFull => Err(range_error("Provided buffer too small.")), + CoderResult::OutputFull => Err(WebError::BufferTooSmall), } } } @@ -280,12 +288,9 @@ fn op_encoding_new_decoder( #[string] label: &str, fatal: bool, ignore_bom: bool, -) -> Result { - let encoding = Encoding::for_label(label.as_bytes()).ok_or_else(|| { - range_error(format!( - "The encoding label provided ('{label}') is invalid." - )) - })?; +) -> Result { + let encoding = Encoding::for_label(label.as_bytes()) + .ok_or_else(|| WebError::InvalidEncodingLabel(label.to_string()))?; let decoder = if ignore_bom { encoding.new_decoder_without_bom_handling() @@ -305,13 +310,13 @@ fn op_encoding_decode( #[anybuffer] data: &[u8], #[cppgc] resource: &TextDecoderResource, stream: bool, -) -> Result { +) -> Result { let mut decoder = resource.decoder.borrow_mut(); let fatal = resource.fatal; let max_buffer_length = decoder .max_utf16_buffer_length(data.len()) - .ok_or_else(|| range_error("Value too large to decode."))?; + .ok_or(WebError::ValueTooLarge)?; let mut output = vec![0; max_buffer_length]; @@ -323,12 +328,8 @@ fn op_encoding_decode( output.truncate(written); Ok(output.into()) } - DecoderResult::OutputFull => { - Err(range_error("Provided buffer too small.")) - } - DecoderResult::Malformed(_, _) => { - Err(type_error("The encoded data is not valid.")) - } + DecoderResult::OutputFull => Err(WebError::BufferTooSmall), + DecoderResult::Malformed(_, _) => Err(WebError::DataInvalid), } } else { let (result, _, written, _) = @@ -338,7 +339,7 @@ fn op_encoding_decode( output.truncate(written); Ok(output.into()) } - CoderResult::OutputFull => Err(range_error("Provided buffer too small.")), + CoderResult::OutputFull => Err(WebError::BufferTooSmall), } } } @@ -356,7 +357,7 @@ fn op_encoding_encode_into( input: v8::Local, #[buffer] buffer: &mut [u8], #[buffer] out_buf: &mut [u32], -) -> Result<(), AnyError> { +) -> Result<(), WebError> { let s = v8::Local::::try_from(input)?; let mut nchars = 0; @@ -414,53 +415,4 @@ pub fn get_declaration() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_web.d.ts") } -#[derive(Debug)] -pub struct DomExceptionQuotaExceededError { - pub msg: String, -} - -impl DomExceptionQuotaExceededError { - pub fn new(msg: &str) -> Self { - DomExceptionQuotaExceededError { - msg: msg.to_string(), - } - } -} - -#[derive(Debug)] -pub struct DomExceptionInvalidCharacterError { - pub msg: String, -} - -impl DomExceptionInvalidCharacterError { - pub fn new(msg: &str) -> Self { - DomExceptionInvalidCharacterError { - msg: msg.to_string(), - } - } -} - -impl fmt::Display for DomExceptionQuotaExceededError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.pad(&self.msg) - } -} -impl fmt::Display for DomExceptionInvalidCharacterError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.pad(&self.msg) - } -} - -impl std::error::Error for DomExceptionQuotaExceededError {} - -impl std::error::Error for DomExceptionInvalidCharacterError {} - -pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { - e.downcast_ref::() - .map(|_| "DOMExceptionQuotaExceededError") - .or_else(|| { - e.downcast_ref::() - .map(|_| "DOMExceptionInvalidCharacterError") - }) -} pub struct Location(pub Url); diff --git a/ext/web/message_port.rs b/ext/web/message_port.rs index c069037f81..1a4a09073d 100644 --- a/ext/web/message_port.rs +++ b/ext/web/message_port.rs @@ -4,8 +4,6 @@ use std::borrow::Cow; use std::cell::RefCell; use std::rc::Rc; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::CancelFuture; @@ -23,6 +21,20 @@ use tokio::sync::mpsc::unbounded_channel; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedSender; +#[derive(Debug, thiserror::Error)] +pub enum MessagePortError { + #[error("Invalid message port transfer")] + InvalidTransfer, + #[error("Message port is not ready for transfer")] + NotReady, + #[error("Can not transfer self message port")] + TransferSelf, + #[error(transparent)] + Canceled(#[from] deno_core::Canceled), + #[error(transparent)] + Resource(deno_core::error::AnyError), +} + pub enum Transferable { MessagePort(MessagePort), ArrayBuffer(u32), @@ -40,7 +52,7 @@ impl MessagePort { &self, state: &mut OpState, data: JsMessageData, - ) -> Result<(), AnyError> { + ) -> Result<(), MessagePortError> { let transferables = deserialize_js_transferables(state, data.transferables)?; @@ -56,7 +68,7 @@ impl MessagePort { pub async fn recv( &self, state: Rc>, - ) -> Result, AnyError> { + ) -> Result, MessagePortError> { let rx = &self.rx; let maybe_data = poll_fn(|cx| { @@ -147,7 +159,7 @@ pub enum JsTransferable { pub fn deserialize_js_transferables( state: &mut OpState, js_transferables: Vec, -) -> Result, AnyError> { +) -> Result, MessagePortError> { let mut transferables = Vec::with_capacity(js_transferables.len()); for js_transferable in js_transferables { match js_transferable { @@ -155,10 +167,10 @@ pub fn deserialize_js_transferables( let resource = state .resource_table .take::(id) - .map_err(|_| type_error("Invalid message port transfer"))?; + .map_err(|_| MessagePortError::InvalidTransfer)?; resource.cancel.cancel(); - let resource = Rc::try_unwrap(resource) - .map_err(|_| type_error("Message port is not ready for transfer"))?; + let resource = + Rc::try_unwrap(resource).map_err(|_| MessagePortError::NotReady)?; transferables.push(Transferable::MessagePort(resource.port)); } JsTransferable::ArrayBuffer(id) => { @@ -202,16 +214,19 @@ pub fn op_message_port_post_message( state: &mut OpState, #[smi] rid: ResourceId, #[serde] data: JsMessageData, -) -> Result<(), AnyError> { +) -> Result<(), MessagePortError> { for js_transferable in &data.transferables { if let JsTransferable::MessagePort(id) = js_transferable { if *id == rid { - return Err(type_error("Can not transfer self message port")); + return Err(MessagePortError::TransferSelf); } } } - let resource = state.resource_table.get::(rid)?; + let resource = state + .resource_table + .get::(rid) + .map_err(MessagePortError::Resource)?; resource.port.send(state, data) } @@ -220,7 +235,7 @@ pub fn op_message_port_post_message( pub async fn op_message_port_recv_message( state: Rc>, #[smi] rid: ResourceId, -) -> Result, AnyError> { +) -> Result, MessagePortError> { let resource = { let state = state.borrow(); match state.resource_table.get::(rid) { @@ -237,9 +252,11 @@ pub async fn op_message_port_recv_message( pub fn op_message_port_recv_message_sync( state: &mut OpState, // Rc>, #[smi] rid: ResourceId, -) -> Result, AnyError> { - let resource = state.resource_table.get::(rid)?; - resource.cancel.cancel(); +) -> Result, MessagePortError> { + let resource = state + .resource_table + .get::(rid) + .map_err(MessagePortError::Resource)?; let mut rx = resource.port.rx.borrow_mut(); match rx.try_recv() { diff --git a/ext/web/stream_resource.rs b/ext/web/stream_resource.rs index 78487883b6..c44a385ea9 100644 --- a/ext/web/stream_resource.rs +++ b/ext/web/stream_resource.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use bytes::BytesMut; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::external; use deno_core::op2; use deno_core::serde_v8::V8Slice; @@ -18,6 +16,7 @@ use deno_core::RcRef; use deno_core::Resource; use deno_core::ResourceId; use futures::future::poll_fn; +use futures::TryFutureExt; use std::borrow::Cow; use std::cell::RefCell; use std::cell::RefMut; @@ -31,6 +30,14 @@ use std::task::Context; use std::task::Poll; use std::task::Waker; +#[derive(Debug, thiserror::Error)] +pub enum StreamResourceError { + #[error(transparent)] + Canceled(#[from] deno_core::Canceled), + #[error("{0}")] + Js(String), +} + // How many buffers we'll allow in the channel before we stop allowing writes. const BUFFER_CHANNEL_SIZE: u16 = 1024; @@ -48,7 +55,7 @@ struct BoundedBufferChannelInner { buffers: [MaybeUninit>; BUFFER_CHANNEL_SIZE as _], ring_producer: u16, ring_consumer: u16, - error: Option, + error: Option, current_size: usize, // TODO(mmastrac): we can math this field instead of accounting for it len: usize, @@ -141,7 +148,10 @@ impl BoundedBufferChannelInner { self.len = 0; } - pub fn read(&mut self, limit: usize) -> Result, AnyError> { + pub fn read( + &mut self, + limit: usize, + ) -> Result, StreamResourceError> { // Empty buffers will return the error, if one exists, or None if self.len == 0 { if let Some(error) = self.error.take() { @@ -230,7 +240,7 @@ impl BoundedBufferChannelInner { Ok(()) } - pub fn write_error(&mut self, error: AnyError) { + pub fn write_error(&mut self, error: StreamResourceError) { self.error = Some(error); if let Some(waker) = self.read_waker.take() { waker.wake(); @@ -306,7 +316,10 @@ impl BoundedBufferChannel { self.inner.borrow_mut() } - pub fn read(&self, limit: usize) -> Result, AnyError> { + pub fn read( + &self, + limit: usize, + ) -> Result, StreamResourceError> { self.inner().read(limit) } @@ -314,7 +327,7 @@ impl BoundedBufferChannel { self.inner().write(buffer) } - pub fn write_error(&self, error: AnyError) { + pub fn write_error(&self, error: StreamResourceError) { self.inner().write_error(error) } @@ -358,7 +371,10 @@ impl ReadableStreamResource { RcRef::map(self, |s| &s.cancel_handle).clone() } - async fn read(self: Rc, limit: usize) -> Result { + async fn read( + self: Rc, + limit: usize, + ) -> Result { let cancel_handle = self.cancel_handle(); // Serialize all the reads using a task queue. let _read_permit = self.read_queue.acquire().await; @@ -387,7 +403,7 @@ impl Resource for ReadableStreamResource { } fn read(self: Rc, limit: usize) -> AsyncResult { - Box::pin(ReadableStreamResource::read(self, limit)) + Box::pin(ReadableStreamResource::read(self, limit).map_err(|e| e.into())) } fn close(self: Rc) { @@ -550,7 +566,7 @@ pub fn op_readable_stream_resource_write_error( ) -> bool { let sender = get_sender(sender); // We can always write an error, no polling required - sender.write_error(type_error(Cow::Owned(error))); + sender.write_error(StreamResourceError::Js(error)); !sender.closed() } diff --git a/ext/web/timers.rs b/ext/web/timers.rs index a9ab7c97e4..06444ed34f 100644 --- a/ext/web/timers.rs +++ b/ext/web/timers.rs @@ -4,7 +4,10 @@ use deno_core::op2; use deno_core::OpState; +use std::time::Duration; use std::time::Instant; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; pub trait TimersPermission { fn allow_hrtime(&mut self) -> bool; @@ -17,21 +20,28 @@ impl TimersPermission for deno_permissions::PermissionsContainer { } } -pub type StartTime = Instant; +pub struct StartTime(Instant); -// Returns a milliseconds and nanoseconds subsec -// since the start time of the deno runtime. -// If the High precision flag is not set, the -// nanoseconds are rounded on 2ms. -#[op2(fast)] -pub fn op_now(state: &mut OpState, #[buffer] buf: &mut [u8]) +impl Default for StartTime { + fn default() -> Self { + Self(Instant::now()) + } +} + +impl std::ops::Deref for StartTime { + type Target = Instant; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +fn expose_time(state: &mut OpState, duration: Duration, out: &mut [u8]) where TP: TimersPermission + 'static, { - let start_time = state.borrow::(); - let elapsed = start_time.elapsed(); - let seconds = elapsed.as_secs(); - let mut subsec_nanos = elapsed.subsec_nanos(); + let seconds = duration.as_secs() as u32; + let mut subsec_nanos = duration.subsec_nanos(); // If the permission is not enabled // Round the nano result on 2 milliseconds @@ -40,14 +50,33 @@ where let reduced_time_precision = 2_000_000; // 2ms in nanoseconds subsec_nanos -= subsec_nanos % reduced_time_precision; } - if buf.len() < 8 { - return; + + if out.len() >= 8 { + out[0..4].copy_from_slice(&seconds.to_ne_bytes()); + out[4..8].copy_from_slice(&subsec_nanos.to_ne_bytes()); } - let buf: &mut [u32] = - // SAFETY: buffer is at least 8 bytes long. - unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr() as _, 2) }; - buf[0] = seconds as u32; - buf[1] = subsec_nanos; +} + +#[op2(fast)] +pub fn op_now(state: &mut OpState, #[buffer] buf: &mut [u8]) +where + TP: TimersPermission + 'static, +{ + let start_time = state.borrow::(); + let elapsed = start_time.elapsed(); + expose_time::(state, elapsed, buf); +} + +#[op2(fast)] +pub fn op_time_origin(state: &mut OpState, #[buffer] buf: &mut [u8]) +where + TP: TimersPermission + 'static, +{ + // https://w3c.github.io/hr-time/#dfn-estimated-monotonic-time-of-the-unix-epoch + let wall_time = SystemTime::now(); + let monotonic_time = state.borrow::().elapsed(); + let epoch = wall_time.duration_since(UNIX_EPOCH).unwrap() - monotonic_time; + expose_time::(state, epoch, buf); } #[allow(clippy::unused_async)] diff --git a/ext/webgpu/Cargo.toml b/ext/webgpu/Cargo.toml index b5cff59855..f23bb8371e 100644 --- a/ext/webgpu/Cargo.toml +++ b/ext/webgpu/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_webgpu" -version = "0.135.0" +version = "0.144.0" authors = ["the Deno authors"] edition.workspace = true license = "MIT" @@ -25,6 +25,7 @@ serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["full"] } wgpu-types = { workspace = true, features = ["serde"] } raw-window-handle = { workspace = true } +thiserror.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies.wgpu-core] workspace = true diff --git a/ext/webgpu/buffer.rs b/ext/webgpu/buffer.rs index c6cd6f0a74..c2b53890e0 100644 --- a/ext/webgpu/buffer.rs +++ b/ext/webgpu/buffer.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; @@ -13,9 +11,18 @@ use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; -use super::error::DomExceptionOperationError; use super::error::WebGpuResult; +#[derive(Debug, thiserror::Error)] +pub enum BufferError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error("usage is not valid")] + InvalidUsage, + #[error(transparent)] + Access(wgpu_core::resource::BufferAccessError), +} + pub(crate) struct WebGpuBuffer( pub(crate) super::Instance, pub(crate) wgpu_core::id::BufferId, @@ -46,18 +53,19 @@ pub fn op_webgpu_create_buffer( #[number] size: u64, usage: u32, mapped_at_creation: bool, -) -> Result { +) -> Result { let instance = state.borrow::(); let device_resource = state .resource_table - .get::(device_rid)?; + .get::(device_rid) + .map_err(BufferError::Resource)?; let device = device_resource.1; let descriptor = wgpu_core::resource::BufferDescriptor { label: Some(label), size, usage: wgpu_types::BufferUsages::from_bits(usage) - .ok_or_else(|| type_error("usage is not valid"))?, + .ok_or(BufferError::InvalidUsage)?, mapped_at_creation, }; @@ -77,18 +85,21 @@ pub async fn op_webgpu_buffer_get_map_async( mode: u32, #[number] offset: u64, #[number] size: u64, -) -> Result { +) -> Result { let device; let done = Arc::new(Mutex::new(None)); { let state_ = state.borrow(); let instance = state_.borrow::(); - let buffer_resource = - state_.resource_table.get::(buffer_rid)?; + let buffer_resource = state_ + .resource_table + .get::(buffer_rid) + .map_err(BufferError::Resource)?; let buffer = buffer_resource.1; let device_resource = state_ .resource_table - .get::(device_rid)?; + .get::(device_rid) + .map_err(BufferError::Resource)?; device = device_resource.1; let done_ = done.clone(); @@ -120,9 +131,7 @@ pub async fn op_webgpu_buffer_get_map_async( let result = done.lock().unwrap().take(); match result { Some(Ok(())) => return Ok(WebGpuResult::empty()), - Some(Err(e)) => { - return Err(DomExceptionOperationError::new(&e.to_string()).into()) - } + Some(Err(e)) => return Err(BufferError::Access(e)), None => { { let state = state.borrow(); @@ -143,9 +152,12 @@ pub fn op_webgpu_buffer_get_mapped_range( #[number] offset: u64, #[number] size: Option, #[buffer] buf: &mut [u8], -) -> Result { +) -> Result { let instance = state.borrow::(); - let buffer_resource = state.resource_table.get::(buffer_rid)?; + let buffer_resource = state + .resource_table + .get::(buffer_rid) + .map_err(BufferError::Resource)?; let buffer = buffer_resource.1; let (slice_pointer, range_size) = @@ -154,7 +166,7 @@ pub fn op_webgpu_buffer_get_mapped_range( offset, size )) - .map_err(|e| DomExceptionOperationError::new(&e.to_string()))?; + .map_err(BufferError::Access)?; // SAFETY: guarantee to be safe from wgpu let slice = unsafe { @@ -176,12 +188,16 @@ pub fn op_webgpu_buffer_unmap( #[smi] buffer_rid: ResourceId, #[smi] mapped_rid: ResourceId, #[buffer] buf: Option<&[u8]>, -) -> Result { +) -> Result { let mapped_resource = state .resource_table - .take::(mapped_rid)?; + .take::(mapped_rid) + .map_err(BufferError::Resource)?; let instance = state.borrow::(); - let buffer_resource = state.resource_table.get::(buffer_rid)?; + let buffer_resource = state + .resource_table + .get::(buffer_rid) + .map_err(BufferError::Resource)?; let buffer = buffer_resource.1; if let Some(buf) = buf { diff --git a/ext/webgpu/bundle.rs b/ext/webgpu/bundle.rs index 57158271cc..d9a5b29539 100644 --- a/ext/webgpu/bundle.rs +++ b/ext/webgpu/bundle.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; @@ -13,6 +11,14 @@ use std::rc::Rc; use super::error::WebGpuResult; +#[derive(Debug, thiserror::Error)] +pub enum BundleError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error("size must be larger than 0")] + InvalidSize, +} + struct WebGpuRenderBundleEncoder( RefCell, ); @@ -53,7 +59,7 @@ pub struct CreateRenderBundleEncoderArgs { pub fn op_webgpu_create_render_bundle_encoder( state: &mut OpState, #[serde] args: CreateRenderBundleEncoderArgs, -) -> Result { +) -> Result { let device_resource = state .resource_table .get::(args.device_rid)?; @@ -100,7 +106,7 @@ pub fn op_webgpu_render_bundle_encoder_finish( state: &mut OpState, #[smi] render_bundle_encoder_rid: ResourceId, #[string] label: Cow, -) -> Result { +) -> Result { let render_bundle_encoder_resource = state .resource_table @@ -131,7 +137,7 @@ pub fn op_webgpu_render_bundle_encoder_set_bind_group( #[buffer] dynamic_offsets_data: &[u32], #[number] dynamic_offsets_data_start: usize, #[number] dynamic_offsets_data_length: usize, -) -> Result { +) -> Result { let bind_group_resource = state .resource_table @@ -171,7 +177,7 @@ pub fn op_webgpu_render_bundle_encoder_push_debug_group( state: &mut OpState, #[smi] render_bundle_encoder_rid: ResourceId, #[string] group_label: &str, -) -> Result { +) -> Result { let render_bundle_encoder_resource = state .resource_table @@ -195,7 +201,7 @@ pub fn op_webgpu_render_bundle_encoder_push_debug_group( pub fn op_webgpu_render_bundle_encoder_pop_debug_group( state: &mut OpState, #[smi] render_bundle_encoder_rid: ResourceId, -) -> Result { +) -> Result { let render_bundle_encoder_resource = state .resource_table @@ -214,7 +220,7 @@ pub fn op_webgpu_render_bundle_encoder_insert_debug_marker( state: &mut OpState, #[smi] render_bundle_encoder_rid: ResourceId, #[string] marker_label: &str, -) -> Result { +) -> Result { let render_bundle_encoder_resource = state .resource_table @@ -239,7 +245,7 @@ pub fn op_webgpu_render_bundle_encoder_set_pipeline( state: &mut OpState, #[smi] render_bundle_encoder_rid: ResourceId, #[smi] pipeline: ResourceId, -) -> Result { +) -> Result { let render_pipeline_resource = state .resource_table @@ -266,18 +272,17 @@ pub fn op_webgpu_render_bundle_encoder_set_index_buffer( #[serde] index_format: wgpu_types::IndexFormat, #[number] offset: u64, #[number] size: u64, -) -> Result { +) -> Result { let buffer_resource = state .resource_table - .get::(buffer)?; - let render_bundle_encoder_resource = - state - .resource_table - .get::(render_bundle_encoder_rid)?; - let size = Some( - std::num::NonZeroU64::new(size) - .ok_or_else(|| type_error("size must be larger than 0"))?, - ); + .get::(buffer) + .map_err(BundleError::Resource)?; + let render_bundle_encoder_resource = state + .resource_table + .get::(render_bundle_encoder_rid) + .map_err(BundleError::Resource)?; + let size = + Some(std::num::NonZeroU64::new(size).ok_or(BundleError::InvalidSize)?); render_bundle_encoder_resource .0 @@ -296,19 +301,17 @@ pub fn op_webgpu_render_bundle_encoder_set_vertex_buffer( #[smi] buffer: ResourceId, #[number] offset: u64, #[number] size: Option, -) -> Result { +) -> Result { let buffer_resource = state .resource_table - .get::(buffer)?; - let render_bundle_encoder_resource = - state - .resource_table - .get::(render_bundle_encoder_rid)?; + .get::(buffer) + .map_err(BundleError::Resource)?; + let render_bundle_encoder_resource = state + .resource_table + .get::(render_bundle_encoder_rid) + .map_err(BundleError::Resource)?; let size = if let Some(size) = size { - Some( - std::num::NonZeroU64::new(size) - .ok_or_else(|| type_error("size must be larger than 0"))?, - ) + Some(std::num::NonZeroU64::new(size).ok_or(BundleError::InvalidSize)?) } else { None }; @@ -333,7 +336,7 @@ pub fn op_webgpu_render_bundle_encoder_draw( instance_count: u32, first_vertex: u32, first_instance: u32, -) -> Result { +) -> Result { let render_bundle_encoder_resource = state .resource_table @@ -360,7 +363,7 @@ pub fn op_webgpu_render_bundle_encoder_draw_indexed( first_index: u32, base_vertex: i32, first_instance: u32, -) -> Result { +) -> Result { let render_bundle_encoder_resource = state .resource_table @@ -385,7 +388,7 @@ pub fn op_webgpu_render_bundle_encoder_draw_indirect( #[smi] render_bundle_encoder_rid: ResourceId, #[smi] indirect_buffer: ResourceId, #[number] indirect_offset: u64, -) -> Result { +) -> Result { let buffer_resource = state .resource_table .get::(indirect_buffer)?; diff --git a/ext/webgpu/byow.rs b/ext/webgpu/byow.rs index 3a43f416e5..c9e1177b1e 100644 --- a/ext/webgpu/byow.rs +++ b/ext/webgpu/byow.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::ResourceId; @@ -16,6 +14,47 @@ use std::ptr::NonNull; use crate::surface::WebGpuSurface; +#[derive(Debug, thiserror::Error)] +pub enum ByowError { + #[error("Cannot create surface outside of WebGPU context. Did you forget to call `navigator.gpu.requestAdapter()`?")] + WebGPUNotInitiated, + #[error("Invalid parameters")] + InvalidParameters, + #[error(transparent)] + CreateSurface(wgpu_core::instance::CreateSurfaceError), + #[cfg(target_os = "windows")] + #[error("Invalid system on Windows")] + InvalidSystem, + #[cfg(target_os = "macos")] + #[error("Invalid system on macOS")] + InvalidSystem, + #[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd" + ))] + #[error("Invalid system on Linux/BSD")] + InvalidSystem, + #[cfg(any( + target_os = "windows", + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd" + ))] + #[error("window is null")] + NullWindow, + #[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd" + ))] + #[error("display is null")] + NullDisplay, + #[cfg(target_os = "macos")] + #[error("ns_view is null")] + NSViewDisplay, +} + #[op2(fast)] #[smi] pub fn op_webgpu_surface_create( @@ -23,10 +62,10 @@ pub fn op_webgpu_surface_create( #[string] system: &str, p1: *const c_void, p2: *const c_void, -) -> Result { - let instance = state.try_borrow::().ok_or_else(|| { - type_error("Cannot create surface outside of WebGPU context. Did you forget to call `navigator.gpu.requestAdapter()`?") - })?; +) -> Result { + let instance = state + .try_borrow::() + .ok_or(ByowError::WebGPUNotInitiated)?; // Security note: // // The `p1` and `p2` parameters are pointers to platform-specific window @@ -41,13 +80,15 @@ pub fn op_webgpu_surface_create( // // - Only FFI can export v8::External to user code. if p1.is_null() { - return Err(type_error("Invalid parameters")); + return Err(ByowError::InvalidParameters); } let (win_handle, display_handle) = raw_window(system, p1, p2)?; // SAFETY: see above comment let surface = unsafe { - instance.instance_create_surface(display_handle, win_handle, None)? + instance + .instance_create_surface(display_handle, win_handle, None) + .map_err(ByowError::CreateSurface)? }; let rid = state @@ -66,15 +107,14 @@ fn raw_window( system: &str, _ns_window: *const c_void, ns_view: *const c_void, -) -> Result { +) -> Result { if system != "cocoa" { - return Err(type_error("Invalid system on macOS")); + return Err(ByowError::InvalidSystem); } let win_handle = raw_window_handle::RawWindowHandle::AppKit( raw_window_handle::AppKitWindowHandle::new( - NonNull::new(ns_view as *mut c_void) - .ok_or(type_error("ns_view is null"))?, + NonNull::new(ns_view as *mut c_void).ok_or(ByowError::NSViewDisplay)?, ), ); @@ -89,16 +129,16 @@ fn raw_window( system: &str, window: *const c_void, hinstance: *const c_void, -) -> Result { +) -> Result { use raw_window_handle::WindowsDisplayHandle; if system != "win32" { - return Err(type_error("Invalid system on Windows")); + return Err(ByowError::InvalidSystem); } let win_handle = { let mut handle = raw_window_handle::Win32WindowHandle::new( std::num::NonZeroIsize::new(window as isize) - .ok_or(type_error("window is null"))?, + .ok_or(ByowError::NullWindow)?, ); handle.hinstance = std::num::NonZeroIsize::new(hinstance as isize); @@ -115,7 +155,7 @@ fn raw_window( system: &str, window: *const c_void, display: *const c_void, -) -> Result { +) -> Result { let (win_handle, display_handle); if system == "x11" { win_handle = raw_window_handle::RawWindowHandle::Xlib( @@ -131,19 +171,17 @@ fn raw_window( } else if system == "wayland" { win_handle = raw_window_handle::RawWindowHandle::Wayland( raw_window_handle::WaylandWindowHandle::new( - NonNull::new(window as *mut c_void) - .ok_or(type_error("window is null"))?, + NonNull::new(window as *mut c_void).ok_or(ByowError::NullWindow)?, ), ); display_handle = raw_window_handle::RawDisplayHandle::Wayland( raw_window_handle::WaylandDisplayHandle::new( - NonNull::new(display as *mut c_void) - .ok_or(type_error("display is null"))?, + NonNull::new(display as *mut c_void).ok_or(ByowError::NullDisplay)?, ), ); } else { - return Err(type_error("Invalid system on Linux/BSD")); + return Err(ByowError::InvalidSystem); } Ok((win_handle, display_handle)) @@ -160,6 +198,6 @@ fn raw_window( _system: &str, _window: *const c_void, _display: *const c_void, -) -> Result { - Err(type_error("Unsupported platform")) +) -> Result { + Err(deno_core::error::type_error("Unsupported platform")) } diff --git a/ext/webgpu/error.rs b/ext/webgpu/error.rs index 5b55d506ad..f08f765386 100644 --- a/ext/webgpu/error.rs +++ b/ext/webgpu/error.rs @@ -1,11 +1,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::ResourceId; use serde::Serialize; use std::convert::From; use std::error::Error; -use std::fmt; use wgpu_core::binding_model::CreateBindGroupError; use wgpu_core::binding_model::CreateBindGroupLayoutError; use wgpu_core::binding_model::CreatePipelineLayoutError; @@ -286,29 +284,3 @@ impl From for WebGpuError { WebGpuError::Validation(fmt_err(&err)) } } - -#[derive(Debug)] -pub struct DomExceptionOperationError { - pub msg: String, -} - -impl DomExceptionOperationError { - pub fn new(msg: &str) -> Self { - DomExceptionOperationError { - msg: msg.to_string(), - } - } -} - -impl fmt::Display for DomExceptionOperationError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.pad(&self.msg) - } -} - -impl std::error::Error for DomExceptionOperationError {} - -pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { - e.downcast_ref::() - .map(|_| "DOMExceptionOperationError") -} diff --git a/ext/webgpu/lib.rs b/ext/webgpu/lib.rs index df2ab323af..5dc8278e41 100644 --- a/ext/webgpu/lib.rs +++ b/ext/webgpu/lib.rs @@ -2,7 +2,6 @@ #![cfg(not(target_arch = "wasm32"))] #![warn(unsafe_op_in_unsafe_fn)] -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; @@ -16,7 +15,6 @@ use std::rc::Rc; pub use wgpu_core; pub use wgpu_types; -use error::DomExceptionOperationError; use error::WebGpuResult; pub const UNSTABLE_FEATURE_NAME: &str = "webgpu"; @@ -44,7 +42,7 @@ mod macros { #[cfg(all(not(target_arch = "wasm32"), windows))] wgpu_types::Backend::Dx12 => $($c)*.$method:: $params, #[cfg(any( - all(unix, not(target_os = "macos"), not(target_os = "ios")), + all(not(target_os = "macos"), not(target_os = "ios")), feature = "angle", target_arch = "wasm32" ))] @@ -85,6 +83,18 @@ pub mod shader; pub mod surface; pub mod texture; +#[derive(Debug, thiserror::Error)] +pub enum InitError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + InvalidAdapter(wgpu_core::instance::InvalidAdapter), + #[error(transparent)] + RequestDevice(wgpu_core::instance::RequestDeviceError), + #[error(transparent)] + InvalidDevice(wgpu_core::device::InvalidDevice), +} + pub type Instance = std::sync::Arc; struct WebGpuAdapter(Instance, wgpu_core::id::AdapterId); @@ -400,7 +410,7 @@ pub fn op_webgpu_request_adapter( state: Rc>, #[serde] power_preference: Option, force_fallback_adapter: bool, -) -> Result { +) -> Result { let mut state = state.borrow_mut(); let backends = std::env::var("DENO_WEBGPU_BACKEND").map_or_else( @@ -441,10 +451,11 @@ pub fn op_webgpu_request_adapter( } }; let adapter_features = - gfx_select!(adapter => instance.adapter_features(adapter))?; + gfx_select!(adapter => instance.adapter_features(adapter)) + .map_err(InitError::InvalidAdapter)?; let features = deserialize_features(&adapter_features); - let adapter_limits = - gfx_select!(adapter => instance.adapter_limits(adapter))?; + let adapter_limits = gfx_select!(adapter => instance.adapter_limits(adapter)) + .map_err(InitError::InvalidAdapter)?; let instance = instance.clone(); @@ -663,10 +674,12 @@ pub fn op_webgpu_request_device( #[string] label: String, #[serde] required_features: GpuRequiredFeatures, #[serde] required_limits: Option, -) -> Result { +) -> Result { let mut state = state.borrow_mut(); - let adapter_resource = - state.resource_table.take::(adapter_rid)?; + let adapter_resource = state + .resource_table + .take::(adapter_rid) + .map_err(InitError::Resource)?; let adapter = adapter_resource.1; let instance = state.borrow::(); @@ -685,13 +698,14 @@ pub fn op_webgpu_request_device( )); adapter_resource.close(); if let Some(err) = maybe_err { - return Err(DomExceptionOperationError::new(&err.to_string()).into()); + return Err(InitError::RequestDevice(err)); } - let device_features = - gfx_select!(device => instance.device_features(device))?; + let device_features = gfx_select!(device => instance.device_features(device)) + .map_err(InitError::InvalidDevice)?; let features = deserialize_features(&device_features); - let limits = gfx_select!(device => instance.device_limits(device))?; + let limits = gfx_select!(device => instance.device_limits(device)) + .map_err(InitError::InvalidDevice)?; let instance = instance.clone(); let instance2 = instance.clone(); @@ -722,14 +736,17 @@ pub struct GPUAdapterInfo { pub fn op_webgpu_request_adapter_info( state: Rc>, #[smi] adapter_rid: ResourceId, -) -> Result { +) -> Result { let state = state.borrow_mut(); - let adapter_resource = - state.resource_table.get::(adapter_rid)?; + let adapter_resource = state + .resource_table + .get::(adapter_rid) + .map_err(InitError::Resource)?; let adapter = adapter_resource.1; let instance = state.borrow::(); - let info = gfx_select!(adapter => instance.adapter_get_info(adapter))?; + let info = gfx_select!(adapter => instance.adapter_get_info(adapter)) + .map_err(InitError::InvalidAdapter)?; Ok(GPUAdapterInfo { vendor: info.vendor.to_string(), @@ -770,9 +787,11 @@ impl From for wgpu_types::QueryType { pub fn op_webgpu_create_query_set( state: &mut OpState, #[serde] args: CreateQuerySetArgs, -) -> Result { - let device_resource = - state.resource_table.get::(args.device_rid)?; +) -> Result { + let device_resource = state + .resource_table + .get::(args.device_rid) + .map_err(InitError::Resource)?; let device = device_resource.1; let instance = state.borrow::(); diff --git a/ext/webgpu/render_pass.rs b/ext/webgpu/render_pass.rs index c68be3d99a..9b9d87d9fc 100644 --- a/ext/webgpu/render_pass.rs +++ b/ext/webgpu/render_pass.rs @@ -1,7 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; @@ -12,6 +10,14 @@ use std::cell::RefCell; use super::error::WebGpuResult; +#[derive(Debug, thiserror::Error)] +pub enum RenderPassError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error("size must be larger than 0")] + InvalidSize, +} + pub(crate) struct WebGpuRenderPass( pub(crate) RefCell, ); @@ -38,7 +44,7 @@ pub struct RenderPassSetViewportArgs { pub fn op_webgpu_render_pass_set_viewport( state: &mut OpState, #[serde] args: RenderPassSetViewportArgs, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(args.render_pass_rid)?; @@ -65,7 +71,7 @@ pub fn op_webgpu_render_pass_set_scissor_rect( y: u32, width: u32, height: u32, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -87,7 +93,7 @@ pub fn op_webgpu_render_pass_set_blend_constant( state: &mut OpState, #[smi] render_pass_rid: ResourceId, #[serde] color: wgpu_types::Color, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -106,7 +112,7 @@ pub fn op_webgpu_render_pass_set_stencil_reference( state: &mut OpState, #[smi] render_pass_rid: ResourceId, reference: u32, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -125,7 +131,7 @@ pub fn op_webgpu_render_pass_begin_occlusion_query( state: &mut OpState, #[smi] render_pass_rid: ResourceId, query_index: u32, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -143,7 +149,7 @@ pub fn op_webgpu_render_pass_begin_occlusion_query( pub fn op_webgpu_render_pass_end_occlusion_query( state: &mut OpState, #[smi] render_pass_rid: ResourceId, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -161,7 +167,7 @@ pub fn op_webgpu_render_pass_execute_bundles( state: &mut OpState, #[smi] render_pass_rid: ResourceId, #[serde] bundles: Vec, -) -> Result { +) -> Result { let bundles = bundles .iter() .map(|rid| { @@ -171,7 +177,7 @@ pub fn op_webgpu_render_pass_execute_bundles( .get::(*rid)?; Ok(render_bundle_resource.1) }) - .collect::, AnyError>>()?; + .collect::, deno_core::error::AnyError>>()?; let render_pass_resource = state .resource_table @@ -191,7 +197,7 @@ pub fn op_webgpu_render_pass_end( state: &mut OpState, #[smi] command_encoder_rid: ResourceId, #[smi] render_pass_rid: ResourceId, -) -> Result { +) -> Result { let command_encoder_resource = state .resource_table .get::( @@ -217,7 +223,7 @@ pub fn op_webgpu_render_pass_set_bind_group( #[buffer] dynamic_offsets_data: &[u32], #[number] dynamic_offsets_data_start: usize, #[number] dynamic_offsets_data_length: usize, -) -> Result { +) -> Result { let bind_group_resource = state .resource_table @@ -251,7 +257,7 @@ pub fn op_webgpu_render_pass_push_debug_group( state: &mut OpState, #[smi] render_pass_rid: ResourceId, #[string] group_label: &str, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -270,7 +276,7 @@ pub fn op_webgpu_render_pass_push_debug_group( pub fn op_webgpu_render_pass_pop_debug_group( state: &mut OpState, #[smi] render_pass_rid: ResourceId, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -288,7 +294,7 @@ pub fn op_webgpu_render_pass_insert_debug_marker( state: &mut OpState, #[smi] render_pass_rid: ResourceId, #[string] marker_label: &str, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -308,7 +314,7 @@ pub fn op_webgpu_render_pass_set_pipeline( state: &mut OpState, #[smi] render_pass_rid: ResourceId, pipeline: u32, -) -> Result { +) -> Result { let render_pipeline_resource = state .resource_table @@ -334,19 +340,18 @@ pub fn op_webgpu_render_pass_set_index_buffer( #[serde] index_format: wgpu_types::IndexFormat, #[number] offset: u64, #[number] size: Option, -) -> Result { +) -> Result { let buffer_resource = state .resource_table - .get::(buffer)?; + .get::(buffer) + .map_err(RenderPassError::Resource)?; let render_pass_resource = state .resource_table - .get::(render_pass_rid)?; + .get::(render_pass_rid) + .map_err(RenderPassError::Resource)?; let size = if let Some(size) = size { - Some( - std::num::NonZeroU64::new(size) - .ok_or_else(|| type_error("size must be larger than 0"))?, - ) + Some(std::num::NonZeroU64::new(size).ok_or(RenderPassError::InvalidSize)?) } else { None }; @@ -370,19 +375,18 @@ pub fn op_webgpu_render_pass_set_vertex_buffer( buffer: u32, #[number] offset: u64, #[number] size: Option, -) -> Result { +) -> Result { let buffer_resource = state .resource_table - .get::(buffer)?; + .get::(buffer) + .map_err(RenderPassError::Resource)?; let render_pass_resource = state .resource_table - .get::(render_pass_rid)?; + .get::(render_pass_rid) + .map_err(RenderPassError::Resource)?; let size = if let Some(size) = size { - Some( - std::num::NonZeroU64::new(size) - .ok_or_else(|| type_error("size must be larger than 0"))?, - ) + Some(std::num::NonZeroU64::new(size).ok_or(RenderPassError::InvalidSize)?) } else { None }; @@ -407,7 +411,7 @@ pub fn op_webgpu_render_pass_draw( instance_count: u32, first_vertex: u32, first_instance: u32, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -433,7 +437,7 @@ pub fn op_webgpu_render_pass_draw_indexed( first_index: u32, base_vertex: i32, first_instance: u32, -) -> Result { +) -> Result { let render_pass_resource = state .resource_table .get::(render_pass_rid)?; @@ -457,7 +461,7 @@ pub fn op_webgpu_render_pass_draw_indirect( #[smi] render_pass_rid: ResourceId, indirect_buffer: u32, #[number] indirect_offset: u64, -) -> Result { +) -> Result { let buffer_resource = state .resource_table .get::(indirect_buffer)?; @@ -481,7 +485,7 @@ pub fn op_webgpu_render_pass_draw_indexed_indirect( #[smi] render_pass_rid: ResourceId, indirect_buffer: u32, #[number] indirect_offset: u64, -) -> Result { +) -> Result { let buffer_resource = state .resource_table .get::(indirect_buffer)?; diff --git a/ext/webgpu/sampler.rs b/ext/webgpu/sampler.rs index 27c36802e6..9fc1269ea7 100644 --- a/ext/webgpu/sampler.rs +++ b/ext/webgpu/sampler.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; @@ -47,7 +46,7 @@ pub struct CreateSamplerArgs { pub fn op_webgpu_create_sampler( state: &mut OpState, #[serde] args: CreateSamplerArgs, -) -> Result { +) -> Result { let instance = state.borrow::(); let device_resource = state .resource_table diff --git a/ext/webgpu/shader.rs b/ext/webgpu/shader.rs index 0b3991c5de..4653bd85bf 100644 --- a/ext/webgpu/shader.rs +++ b/ext/webgpu/shader.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; @@ -31,7 +30,7 @@ pub fn op_webgpu_create_shader_module( #[smi] device_rid: ResourceId, #[string] label: Cow, #[string] code: Cow, -) -> Result { +) -> Result { let instance = state.borrow::(); let device_resource = state .resource_table diff --git a/ext/webgpu/surface.rs b/ext/webgpu/surface.rs index 1f6d2c87d2..297eaeb008 100644 --- a/ext/webgpu/surface.rs +++ b/ext/webgpu/surface.rs @@ -1,7 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use super::WebGpuResult; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; @@ -11,6 +10,16 @@ use std::borrow::Cow; use std::rc::Rc; use wgpu_types::SurfaceStatus; +#[derive(Debug, thiserror::Error)] +pub enum SurfaceError { + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error("Invalid Surface Status")] + InvalidStatus, + #[error(transparent)] + Surface(wgpu_core::present::SurfaceError), +} + pub struct WebGpuSurface(pub crate::Instance, pub wgpu_core::id::SurfaceId); impl Resource for WebGpuSurface { fn name(&self) -> Cow { @@ -41,7 +50,7 @@ pub struct SurfaceConfigureArgs { pub fn op_webgpu_surface_configure( state: &mut OpState, #[serde] args: SurfaceConfigureArgs, -) -> Result { +) -> Result { let instance = state.borrow::(); let device_resource = state .resource_table @@ -75,18 +84,22 @@ pub fn op_webgpu_surface_get_current_texture( state: &mut OpState, #[smi] device_rid: ResourceId, #[smi] surface_rid: ResourceId, -) -> Result { +) -> Result { let instance = state.borrow::(); let device_resource = state .resource_table - .get::(device_rid)?; + .get::(device_rid) + .map_err(SurfaceError::Resource)?; let device = device_resource.1; - let surface_resource = - state.resource_table.get::(surface_rid)?; + let surface_resource = state + .resource_table + .get::(surface_rid) + .map_err(SurfaceError::Resource)?; let surface = surface_resource.1; let output = - gfx_select!(device => instance.surface_get_current_texture(surface, None))?; + gfx_select!(device => instance.surface_get_current_texture(surface, None)) + .map_err(SurfaceError::Surface)?; match output.status { SurfaceStatus::Good | SurfaceStatus::Suboptimal => { @@ -98,7 +111,7 @@ pub fn op_webgpu_surface_get_current_texture( }); Ok(WebGpuResult::rid(rid)) } - _ => Err(AnyError::msg("Invalid Surface Status")), + _ => Err(SurfaceError::InvalidStatus), } } @@ -107,17 +120,21 @@ pub fn op_webgpu_surface_present( state: &mut OpState, #[smi] device_rid: ResourceId, #[smi] surface_rid: ResourceId, -) -> Result<(), AnyError> { +) -> Result<(), SurfaceError> { let instance = state.borrow::(); let device_resource = state .resource_table - .get::(device_rid)?; + .get::(device_rid) + .map_err(SurfaceError::Resource)?; let device = device_resource.1; - let surface_resource = - state.resource_table.get::(surface_rid)?; + let surface_resource = state + .resource_table + .get::(surface_rid) + .map_err(SurfaceError::Resource)?; let surface = surface_resource.1; - let _ = gfx_select!(device => instance.surface_present(surface))?; + let _ = gfx_select!(device => instance.surface_present(surface)) + .map_err(SurfaceError::Surface)?; Ok(()) } diff --git a/ext/webgpu/texture.rs b/ext/webgpu/texture.rs index 44edd1a887..f8a5e05a3e 100644 --- a/ext/webgpu/texture.rs +++ b/ext/webgpu/texture.rs @@ -1,6 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use deno_core::Resource; @@ -62,7 +61,7 @@ pub struct CreateTextureArgs { pub fn op_webgpu_create_texture( state: &mut OpState, #[serde] args: CreateTextureArgs, -) -> Result { +) -> Result { let instance = state.borrow::(); let device_resource = state .resource_table @@ -111,7 +110,7 @@ pub struct CreateTextureViewArgs { pub fn op_webgpu_create_texture_view( state: &mut OpState, #[serde] args: CreateTextureViewArgs, -) -> Result { +) -> Result { let instance = state.borrow::(); let texture_resource = state .resource_table diff --git a/ext/webidl/Cargo.toml b/ext/webidl/Cargo.toml index 2f87dc2197..8c3f6f6128 100644 --- a/ext/webidl/Cargo.toml +++ b/ext/webidl/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_webidl" -version = "0.168.0" +version = "0.177.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/ext/websocket/01_websocket.js b/ext/websocket/01_websocket.js index 58f4773101..468999b95d 100644 --- a/ext/websocket/01_websocket.js +++ b/ext/websocket/01_websocket.js @@ -28,6 +28,7 @@ const { ArrayPrototypePush, ArrayPrototypeShift, ArrayPrototypeSome, + Error, ErrorPrototypeToString, ObjectDefineProperties, ObjectPrototypeIsPrototypeOf, @@ -488,8 +489,11 @@ class WebSocket extends EventTarget { /* error */ this[_readyState] = CLOSED; + const message = op_ws_get_error(rid); + const error = new Error(message); const errorEv = new ErrorEvent("error", { - message: op_ws_get_error(rid), + error, + message, }); this.dispatchEvent(errorEv); diff --git a/ext/websocket/Cargo.toml b/ext/websocket/Cargo.toml index 3fc9025466..61f1f5959c 100644 --- a/ext/websocket/Cargo.toml +++ b/ext/websocket/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_websocket" -version = "0.173.0" +version = "0.182.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -28,4 +28,5 @@ hyper-util.workspace = true once_cell.workspace = true rustls-tokio-stream.workspace = true serde.workspace = true +thiserror.workspace = true tokio.workspace = true diff --git a/ext/websocket/lib.rs b/ext/websocket/lib.rs index 9e320040b9..a5734271cf 100644 --- a/ext/websocket/lib.rs +++ b/ext/websocket/lib.rs @@ -1,10 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::stream::WebSocketStream; use bytes::Bytes; -use deno_core::anyhow::bail; -use deno_core::error::invalid_hostname; -use deno_core::error::type_error; -use deno_core::error::AnyError; use deno_core::futures::TryFutureExt; use deno_core::op2; use deno_core::unsync::spawn; @@ -43,7 +39,6 @@ use serde::Serialize; use std::borrow::Cow; use std::cell::Cell; use std::cell::RefCell; -use std::fmt; use std::future::Future; use std::num::NonZeroUsize; use std::path::PathBuf; @@ -55,6 +50,7 @@ use tokio::io::ReadHalf; use tokio::io::WriteHalf; use tokio::net::TcpStream; +use deno_permissions::PermissionCheckError; use fastwebsockets::CloseCode; use fastwebsockets::FragmentCollectorRead; use fastwebsockets::Frame; @@ -75,11 +71,33 @@ static USE_WRITEV: Lazy = Lazy::new(|| { false }); +#[derive(Debug, thiserror::Error)] +pub enum WebsocketError { + #[error(transparent)] + Url(url::ParseError), + #[error(transparent)] + Permission(#[from] PermissionCheckError), + #[error(transparent)] + Resource(deno_core::error::AnyError), + #[error(transparent)] + Uri(#[from] http::uri::InvalidUri), + #[error("{0}")] + Io(#[from] std::io::Error), + #[error(transparent)] + WebSocket(#[from] fastwebsockets::WebSocketError), + #[error("failed to connect to WebSocket: {0}")] + ConnectionFailed(#[from] HandshakeError), + #[error(transparent)] + Canceled(#[from] deno_core::Canceled), +} + #[derive(Clone)] pub struct WsRootStoreProvider(Option>); impl WsRootStoreProvider { - pub fn get_or_try_init(&self) -> Result, AnyError> { + pub fn get_or_try_init( + &self, + ) -> Result, deno_core::error::AnyError> { Ok(match &self.0 { Some(provider) => Some(provider.get_or_try_init()?.clone()), None => None, @@ -95,7 +113,7 @@ pub trait WebSocketPermissions { &mut self, _url: &url::Url, _api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result<(), PermissionCheckError>; } impl WebSocketPermissions for deno_permissions::PermissionsContainer { @@ -104,7 +122,7 @@ impl WebSocketPermissions for deno_permissions::PermissionsContainer { &mut self, url: &url::Url, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result<(), PermissionCheckError> { deno_permissions::PermissionsContainer::check_net_url(self, url, api_name) } } @@ -137,13 +155,14 @@ pub fn op_ws_check_permission_and_cancel_handle( #[string] api_name: String, #[string] url: String, cancel_handle: bool, -) -> Result, AnyError> +) -> Result, WebsocketError> where WP: WebSocketPermissions + 'static, { - state - .borrow_mut::() - .check_net_url(&url::Url::parse(&url)?, &api_name)?; + state.borrow_mut::().check_net_url( + &url::Url::parse(&url).map_err(WebsocketError::Url)?, + &api_name, + )?; if cancel_handle { let rid = state @@ -163,16 +182,46 @@ pub struct CreateResponse { extensions: String, } +#[derive(Debug, thiserror::Error)] +pub enum HandshakeError { + #[error("Missing path in url")] + MissingPath, + #[error("Invalid status code {0}")] + InvalidStatusCode(StatusCode), + #[error(transparent)] + Http(#[from] http::Error), + #[error(transparent)] + WebSocket(#[from] fastwebsockets::WebSocketError), + #[error("Didn't receive h2 alpn, aborting connection")] + NoH2Alpn, + #[error(transparent)] + Rustls(#[from] deno_tls::rustls::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + H2(#[from] h2::Error), + #[error("Invalid hostname: '{0}'")] + InvalidHostname(String), + #[error(transparent)] + RootStoreError(deno_core::error::AnyError), + #[error(transparent)] + Tls(deno_tls::TlsError), + #[error(transparent)] + HeaderName(#[from] http::header::InvalidHeaderName), + #[error(transparent)] + HeaderValue(#[from] http::header::InvalidHeaderValue), +} + async fn handshake_websocket( state: &Rc>, uri: &Uri, protocols: &str, headers: Option>, -) -> Result<(WebSocket, http::HeaderMap), AnyError> { +) -> Result<(WebSocket, http::HeaderMap), HandshakeError> { let mut request = Request::builder().method(Method::GET).uri( uri .path_and_query() - .ok_or(type_error("Missing path in url".to_string()))? + .ok_or(HandshakeError::MissingPath)? .as_str(), ); @@ -194,7 +243,9 @@ async fn handshake_websocket( request = populate_common_request_headers(request, &user_agent, protocols, &headers)?; - let request = request.body(http_body_util::Empty::new())?; + let request = request + .body(http_body_util::Empty::new()) + .map_err(HandshakeError::Http)?; let domain = &uri.host().unwrap().to_string(); let port = &uri.port_u16().unwrap_or(match uri.scheme_str() { Some("wss") => 443, @@ -231,7 +282,7 @@ async fn handshake_websocket( async fn handshake_http1_ws( request: Request>, addr: &String, -) -> Result<(WebSocket, http::HeaderMap), AnyError> { +) -> Result<(WebSocket, http::HeaderMap), HandshakeError> { let tcp_socket = TcpStream::connect(addr).await?; handshake_connection(request, tcp_socket).await } @@ -241,11 +292,11 @@ async fn handshake_http1_wss( request: Request>, domain: &str, addr: &str, -) -> Result<(WebSocket, http::HeaderMap), AnyError> { +) -> Result<(WebSocket, http::HeaderMap), HandshakeError> { let tcp_socket = TcpStream::connect(addr).await?; let tls_config = create_ws_client_config(state, SocketUse::Http1Only)?; let dnsname = ServerName::try_from(domain.to_string()) - .map_err(|_| invalid_hostname(domain))?; + .map_err(|_| HandshakeError::InvalidHostname(domain.to_string()))?; let mut tls_connector = TlsStream::new_client_side( tcp_socket, ClientConnection::new(tls_config.into(), dnsname)?, @@ -266,11 +317,11 @@ async fn handshake_http2_wss( domain: &str, headers: &Option>, addr: &str, -) -> Result<(WebSocket, http::HeaderMap), AnyError> { +) -> Result<(WebSocket, http::HeaderMap), HandshakeError> { let tcp_socket = TcpStream::connect(addr).await?; let tls_config = create_ws_client_config(state, SocketUse::Http2Only)?; let dnsname = ServerName::try_from(domain.to_string()) - .map_err(|_| invalid_hostname(domain))?; + .map_err(|_| HandshakeError::InvalidHostname(domain.to_string()))?; // We need to better expose the underlying errors here let mut tls_connector = TlsStream::new_client_side( tcp_socket, @@ -279,7 +330,7 @@ async fn handshake_http2_wss( ); let handshake = tls_connector.handshake().await?; if handshake.alpn.is_none() { - bail!("Didn't receive h2 alpn, aborting connection"); + return Err(HandshakeError::NoH2Alpn); } let h2 = h2::client::Builder::new(); let (mut send, conn) = h2.handshake::<_, Bytes>(tls_connector).await?; @@ -298,7 +349,7 @@ async fn handshake_http2_wss( let (resp, send) = send.send_request(request.body(())?, false)?; let resp = resp.await?; if resp.status() != StatusCode::OK { - bail!("Invalid status code: {}", resp.status()); + return Err(HandshakeError::InvalidStatusCode(resp.status())); } let (http::response::Parts { headers, .. }, recv) = resp.into_parts(); let mut stream = WebSocket::after_handshake( @@ -317,7 +368,7 @@ async fn handshake_connection< >( request: Request>, socket: S, -) -> Result<(WebSocket, http::HeaderMap), AnyError> { +) -> Result<(WebSocket, http::HeaderMap), HandshakeError> { let (upgraded, response) = fastwebsockets::handshake::client(&LocalExecutor, request, socket).await?; @@ -332,7 +383,7 @@ async fn handshake_connection< pub fn create_ws_client_config( state: &Rc>, socket_use: SocketUse, -) -> Result { +) -> Result { let unsafely_ignore_certificate_errors: Option> = state .borrow() .try_borrow::() @@ -340,7 +391,8 @@ pub fn create_ws_client_config( let root_cert_store = state .borrow() .borrow::() - .get_or_try_init()?; + .get_or_try_init() + .map_err(HandshakeError::RootStoreError)?; create_client_config( root_cert_store, @@ -349,6 +401,7 @@ pub fn create_ws_client_config( TlsKeys::Null, socket_use, ) + .map_err(HandshakeError::Tls) } /// Headers common to both http/1.1 and h2 requests. @@ -357,7 +410,7 @@ fn populate_common_request_headers( user_agent: &str, protocols: &str, headers: &Option>, -) -> Result { +) -> Result { request = request .header("User-Agent", user_agent) .header("Sec-WebSocket-Version", "13"); @@ -368,10 +421,8 @@ fn populate_common_request_headers( if let Some(headers) = headers { for (key, value) in headers { - let name = HeaderName::from_bytes(key) - .map_err(|err| type_error(err.to_string()))?; - let v = HeaderValue::from_bytes(value) - .map_err(|err| type_error(err.to_string()))?; + let name = HeaderName::from_bytes(key)?; + let v = HeaderValue::from_bytes(value)?; let is_disallowed_header = matches!( name, @@ -401,14 +452,17 @@ pub async fn op_ws_create( #[string] protocols: String, #[smi] cancel_handle: Option, #[serde] headers: Option>, -) -> Result +) -> Result where WP: WebSocketPermissions + 'static, { { let mut s = state.borrow_mut(); s.borrow_mut::() - .check_net_url(&url::Url::parse(&url)?, &api_name) + .check_net_url( + &url::Url::parse(&url).map_err(WebsocketError::Url)?, + &api_name, + ) .expect( "Permission check should have been done in op_ws_check_permission", ); @@ -418,7 +472,8 @@ where let r = state .borrow_mut() .resource_table - .get::(cancel_rid)?; + .get::(cancel_rid) + .map_err(WebsocketError::Resource)?; Some(r.0.clone()) } else { None @@ -427,15 +482,11 @@ where let uri: Uri = url.parse()?; let handshake = handshake_websocket(&state, &uri, &protocols, headers) - .map_err(|err| { - AnyError::from(DomExceptionNetworkError::new(&format!( - "failed to connect to WebSocket: {err}" - ))) - }); + .map_err(WebsocketError::ConnectionFailed); let (stream, response) = match cancel_resource { - Some(rc) => handshake.try_or_cancel(rc).await, - None => handshake.await, - }?; + Some(rc) => handshake.try_or_cancel(rc).await?, + None => handshake.await?, + }; if let Some(cancel_rid) = cancel_handle { if let Ok(res) = state.borrow_mut().resource_table.take_any(cancel_rid) { @@ -520,14 +571,12 @@ impl ServerWebSocket { self: &Rc, lock: AsyncMutFuture>>, frame: Frame<'_>, - ) -> Result<(), AnyError> { + ) -> Result<(), WebsocketError> { let mut ws = lock.await; if ws.is_closed() { return Ok(()); } - ws.write_frame(frame) - .await - .map_err(|err| type_error(err.to_string()))?; + ws.write_frame(frame).await?; Ok(()) } } @@ -542,7 +591,7 @@ pub fn ws_create_server_stream( state: &mut OpState, transport: NetworkStream, read_buf: Bytes, -) -> Result { +) -> ResourceId { let mut ws = WebSocket::after_handshake( WebSocketStream::new( stream::WsStreamKind::Network(transport), @@ -554,8 +603,7 @@ pub fn ws_create_server_stream( ws.set_auto_close(true); ws.set_auto_pong(true); - let rid = state.resource_table.add(ServerWebSocket::new(ws)); - Ok(rid) + state.resource_table.add(ServerWebSocket::new(ws)) } fn send_binary(state: &mut OpState, rid: ResourceId, data: &[u8]) { @@ -625,11 +673,12 @@ pub async fn op_ws_send_binary_async( state: Rc>, #[smi] rid: ResourceId, #[buffer] data: JsBuffer, -) -> Result<(), AnyError> { +) -> Result<(), WebsocketError> { let resource = state .borrow_mut() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(WebsocketError::Resource)?; let data = data.to_vec(); let lock = resource.reserve_lock(); resource @@ -643,11 +692,12 @@ pub async fn op_ws_send_text_async( state: Rc>, #[smi] rid: ResourceId, #[string] data: String, -) -> Result<(), AnyError> { +) -> Result<(), WebsocketError> { let resource = state .borrow_mut() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(WebsocketError::Resource)?; let lock = resource.reserve_lock(); resource .write_frame( @@ -677,11 +727,12 @@ pub fn op_ws_get_buffered_amount( pub async fn op_ws_send_ping( state: Rc>, #[smi] rid: ResourceId, -) -> Result<(), AnyError> { +) -> Result<(), WebsocketError> { let resource = state .borrow_mut() .resource_table - .get::(rid)?; + .get::(rid) + .map_err(WebsocketError::Resource)?; let lock = resource.reserve_lock(); resource .write_frame( @@ -697,7 +748,7 @@ pub async fn op_ws_close( #[smi] rid: ResourceId, #[smi] code: Option, #[string] reason: Option, -) -> Result<(), AnyError> { +) -> Result<(), WebsocketError> { let Ok(resource) = state .borrow_mut() .resource_table @@ -712,8 +763,7 @@ pub async fn op_ws_close( resource.closed.set(true); let lock = resource.reserve_lock(); - resource.write_frame(lock, frame).await?; - Ok(()) + resource.write_frame(lock, frame).await } #[op2] @@ -867,32 +917,6 @@ pub fn get_declaration() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_websocket.d.ts") } -#[derive(Debug)] -pub struct DomExceptionNetworkError { - pub msg: String, -} - -impl DomExceptionNetworkError { - pub fn new(msg: &str) -> Self { - DomExceptionNetworkError { - msg: msg.to_string(), - } - } -} - -impl fmt::Display for DomExceptionNetworkError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.pad(&self.msg) - } -} - -impl std::error::Error for DomExceptionNetworkError {} - -pub fn get_network_error_class_name(e: &AnyError) -> Option<&'static str> { - e.downcast_ref::() - .map(|_| "DOMExceptionNetworkError") -} - // Needed so hyper can use non Send futures #[derive(Clone)] struct LocalExecutor; diff --git a/ext/webstorage/01_webstorage.js b/ext/webstorage/01_webstorage.js index 3cbdd708dd..9e86366563 100644 --- a/ext/webstorage/01_webstorage.js +++ b/ext/webstorage/01_webstorage.js @@ -143,6 +143,9 @@ function createStorage(persistent) { if (ReflectHas(target, key)) { return undefined; } + if (typeof key === "symbol") { + return undefined; + } const value = target.getItem(key); if (value === null) { return undefined; diff --git a/ext/webstorage/Cargo.toml b/ext/webstorage/Cargo.toml index fbcd17edf9..01e23ab839 100644 --- a/ext/webstorage/Cargo.toml +++ b/ext/webstorage/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_webstorage" -version = "0.163.0" +version = "0.172.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -17,3 +17,4 @@ path = "lib.rs" deno_core.workspace = true deno_web.workspace = true rusqlite.workspace = true +thiserror.workspace = true diff --git a/ext/webstorage/lib.rs b/ext/webstorage/lib.rs index 99e61a1804..40946f05a7 100644 --- a/ext/webstorage/lib.rs +++ b/ext/webstorage/lib.rs @@ -2,10 +2,8 @@ // NOTE to all: use **cached** prepared statements when interfacing with SQLite. -use std::fmt; use std::path::PathBuf; -use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; use rusqlite::params; @@ -14,6 +12,18 @@ use rusqlite::OptionalExtension; pub use rusqlite; +#[derive(Debug, thiserror::Error)] +pub enum WebStorageError { + #[error("LocalStorage is not supported in this context.")] + ContextNotSupported, + #[error(transparent)] + Sqlite(#[from] rusqlite::Error), + #[error(transparent)] + Io(std::io::Error), + #[error("Exceeded maximum storage size")] + StorageExceeded, +} + #[derive(Clone)] struct OriginStorageDir(PathBuf); @@ -51,15 +61,13 @@ struct SessionStorage(Connection); fn get_webstorage( state: &mut OpState, persistent: bool, -) -> Result<&Connection, AnyError> { +) -> Result<&Connection, WebStorageError> { let conn = if persistent { if state.try_borrow::().is_none() { - let path = state.try_borrow::().ok_or_else(|| { - DomExceptionNotSupportedError::new( - "LocalStorage is not supported in this context.", - ) - })?; - std::fs::create_dir_all(&path.0)?; + let path = state + .try_borrow::() + .ok_or(WebStorageError::ContextNotSupported)?; + std::fs::create_dir_all(&path.0).map_err(WebStorageError::Io)?; let conn = Connection::open(path.0.join("local_storage"))?; // Enable write-ahead-logging and tweak some other stuff. let initial_pragmas = " @@ -106,7 +114,7 @@ fn get_webstorage( pub fn op_webstorage_length( state: &mut OpState, persistent: bool, -) -> Result { +) -> Result { let conn = get_webstorage(state, persistent)?; let mut stmt = conn.prepare_cached("SELECT COUNT(*) FROM data")?; @@ -121,7 +129,7 @@ pub fn op_webstorage_key( state: &mut OpState, #[smi] index: u32, persistent: bool, -) -> Result, AnyError> { +) -> Result, WebStorageError> { let conn = get_webstorage(state, persistent)?; let mut stmt = @@ -135,14 +143,9 @@ pub fn op_webstorage_key( } #[inline] -fn size_check(input: usize) -> Result<(), AnyError> { +fn size_check(input: usize) -> Result<(), WebStorageError> { if input >= MAX_STORAGE_BYTES { - return Err( - deno_web::DomExceptionQuotaExceededError::new( - "Exceeded maximum storage size", - ) - .into(), - ); + return Err(WebStorageError::StorageExceeded); } Ok(()) @@ -154,7 +157,7 @@ pub fn op_webstorage_set( #[string] key: &str, #[string] value: &str, persistent: bool, -) -> Result<(), AnyError> { +) -> Result<(), WebStorageError> { let conn = get_webstorage(state, persistent)?; size_check(key.len() + value.len())?; @@ -178,7 +181,7 @@ pub fn op_webstorage_get( state: &mut OpState, #[string] key_name: String, persistent: bool, -) -> Result, AnyError> { +) -> Result, WebStorageError> { let conn = get_webstorage(state, persistent)?; let mut stmt = conn.prepare_cached("SELECT value FROM data WHERE key = ?")?; @@ -194,7 +197,7 @@ pub fn op_webstorage_remove( state: &mut OpState, #[string] key_name: &str, persistent: bool, -) -> Result<(), AnyError> { +) -> Result<(), WebStorageError> { let conn = get_webstorage(state, persistent)?; let mut stmt = conn.prepare_cached("DELETE FROM data WHERE key = ?")?; @@ -207,7 +210,7 @@ pub fn op_webstorage_remove( pub fn op_webstorage_clear( state: &mut OpState, persistent: bool, -) -> Result<(), AnyError> { +) -> Result<(), WebStorageError> { let conn = get_webstorage(state, persistent)?; let mut stmt = conn.prepare_cached("DELETE FROM data")?; @@ -221,7 +224,7 @@ pub fn op_webstorage_clear( pub fn op_webstorage_iterate_keys( state: &mut OpState, persistent: bool, -) -> Result, AnyError> { +) -> Result, WebStorageError> { let conn = get_webstorage(state, persistent)?; let mut stmt = conn.prepare_cached("SELECT key FROM data")?; @@ -232,31 +235,3 @@ pub fn op_webstorage_iterate_keys( Ok(keys) } - -#[derive(Debug)] -pub struct DomExceptionNotSupportedError { - pub msg: String, -} - -impl DomExceptionNotSupportedError { - pub fn new(msg: &str) -> Self { - DomExceptionNotSupportedError { - msg: msg.to_string(), - } - } -} - -impl fmt::Display for DomExceptionNotSupportedError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.pad(&self.msg) - } -} - -impl std::error::Error for DomExceptionNotSupportedError {} - -pub fn get_not_supported_error_class_name( - e: &AnyError, -) -> Option<&'static str> { - e.downcast_ref::() - .map(|_| "DOMExceptionNotSupportedError") -} diff --git a/resolvers/deno/Cargo.toml b/resolvers/deno/Cargo.toml new file mode 100644 index 0000000000..89c0232dc9 --- /dev/null +++ b/resolvers/deno/Cargo.toml @@ -0,0 +1,31 @@ +# Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_resolver" +version = "0.9.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true +description = "Deno resolution algorithm" + +[lib] +path = "lib.rs" + +[dependencies] +anyhow.workspace = true +base32.workspace = true +dashmap.workspace = true +deno_config.workspace = true +deno_media_type.workspace = true +deno_package_json.workspace = true +deno_package_json.features = ["sync"] +deno_path_util.workspace = true +deno_semver.workspace = true +node_resolver.workspace = true +thiserror.workspace = true +url.workspace = true + +[dev-dependencies] +test_util.workspace = true diff --git a/resolvers/deno/README.md b/resolvers/deno/README.md new file mode 100644 index 0000000000..f51619a314 --- /dev/null +++ b/resolvers/deno/README.md @@ -0,0 +1,3 @@ +# deno_resolver + +Deno resolution algorithm. diff --git a/resolvers/deno/cjs.rs b/resolvers/deno/cjs.rs new file mode 100644 index 0000000000..dbcbd8b6bf --- /dev/null +++ b/resolvers/deno/cjs.rs @@ -0,0 +1,272 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::sync::Arc; + +use dashmap::DashMap; +use deno_media_type::MediaType; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::ClosestPkgJsonError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::PackageJsonResolver; +use url::Url; + +/// Keeps track of what module specifiers were resolved as CJS. +/// +/// Modules that are `.js`, `.ts`, `.jsx`, and `tsx` are only known to +/// be CJS or ESM after they're loaded based on their contents. So these +/// files will be "maybe CJS" until they're loaded. +#[derive(Debug)] +pub struct CjsTracker { + is_cjs_resolver: IsCjsResolver, + known: DashMap, +} + +impl CjsTracker { + pub fn new( + in_npm_pkg_checker: Arc, + pkg_json_resolver: Arc>, + options: IsCjsResolverOptions, + ) -> Self { + Self { + is_cjs_resolver: IsCjsResolver::new( + in_npm_pkg_checker, + pkg_json_resolver, + options, + ), + known: Default::default(), + } + } + + /// Checks whether the file might be treated as CJS, but it's not for sure + /// yet because the source hasn't been loaded to see whether it contains + /// imports or exports. + pub fn is_maybe_cjs( + &self, + specifier: &Url, + media_type: MediaType, + ) -> Result { + self.treat_as_cjs_with_is_script(specifier, media_type, None) + } + + /// Gets whether the file is CJS. If true, this is for sure + /// cjs because `is_script` is provided. + /// + /// `is_script` should be `true` when the contents of the file at the + /// provided specifier are known to be a script and not an ES module. + pub fn is_cjs_with_known_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: bool, + ) -> Result { + self.treat_as_cjs_with_is_script(specifier, media_type, Some(is_script)) + } + + fn treat_as_cjs_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option, + ) -> Result { + let kind = match self + .get_known_kind_with_is_script(specifier, media_type, is_script) + { + Some(kind) => kind, + None => self.is_cjs_resolver.check_based_on_pkg_json(specifier)?, + }; + Ok(kind == NodeModuleKind::Cjs) + } + + /// Gets the referrer for the specified module specifier. + /// + /// Generally the referrer should already be tracked by calling + /// `is_cjs_with_known_is_script` before calling this method. + pub fn get_referrer_kind(&self, specifier: &Url) -> NodeModuleKind { + if specifier.scheme() != "file" { + return NodeModuleKind::Esm; + } + self + .get_known_kind(specifier, MediaType::from_specifier(specifier)) + .unwrap_or(NodeModuleKind::Esm) + } + + fn get_known_kind( + &self, + specifier: &Url, + media_type: MediaType, + ) -> Option { + self.get_known_kind_with_is_script(specifier, media_type, None) + } + + fn get_known_kind_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option, + ) -> Option { + self.is_cjs_resolver.get_known_kind_with_is_script( + specifier, + media_type, + is_script, + &self.known, + ) + } +} + +#[derive(Debug)] +pub struct IsCjsResolverOptions { + pub detect_cjs: bool, + pub is_node_main: bool, +} + +/// Resolves whether a module is CJS or ESM. +#[derive(Debug)] +pub struct IsCjsResolver { + in_npm_pkg_checker: Arc, + pkg_json_resolver: Arc>, + options: IsCjsResolverOptions, +} + +impl IsCjsResolver { + pub fn new( + in_npm_pkg_checker: Arc, + pkg_json_resolver: Arc>, + options: IsCjsResolverOptions, + ) -> Self { + Self { + in_npm_pkg_checker, + pkg_json_resolver, + options, + } + } + + /// Gets the referrer kind for a script in the LSP. + pub fn get_lsp_referrer_kind( + &self, + specifier: &Url, + is_script: Option, + ) -> NodeModuleKind { + if specifier.scheme() != "file" { + return NodeModuleKind::Esm; + } + match MediaType::from_specifier(specifier) { + MediaType::Mts | MediaType::Mjs | MediaType::Dmts => NodeModuleKind::Esm, + MediaType::Cjs | MediaType::Cts | MediaType::Dcts => NodeModuleKind::Cjs, + MediaType::Dts => { + // dts files are always determined based on the package.json because + // they contain imports/exports even when considered CJS + self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm) + } + MediaType::Wasm | + MediaType::Json => NodeModuleKind::Esm, + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + // treat these as unknown + | MediaType::Css + | MediaType::SourceMap + | MediaType::Unknown => { + match is_script { + Some(true) => self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm), + Some(false) | None => NodeModuleKind::Esm, + } + } + } + } + + fn get_known_kind_with_is_script( + &self, + specifier: &Url, + media_type: MediaType, + is_script: Option, + known_cache: &DashMap, + ) -> Option { + if specifier.scheme() != "file" { + return Some(NodeModuleKind::Esm); + } + + match media_type { + MediaType::Mts | MediaType::Mjs | MediaType::Dmts => Some(NodeModuleKind::Esm), + MediaType::Cjs | MediaType::Cts | MediaType::Dcts => Some(NodeModuleKind::Cjs), + MediaType::Dts => { + // dts files are always determined based on the package.json because + // they contain imports/exports even when considered CJS + if let Some(value) = known_cache.get(specifier).map(|v| *v) { + Some(value) + } else { + let value = self.check_based_on_pkg_json(specifier).ok(); + if let Some(value) = value { + known_cache.insert(specifier.clone(), value); + } + Some(value.unwrap_or(NodeModuleKind::Esm)) + } + } + MediaType::Wasm | + MediaType::Json => Some(NodeModuleKind::Esm), + MediaType::JavaScript + | MediaType::Jsx + | MediaType::TypeScript + | MediaType::Tsx + // treat these as unknown + | MediaType::Css + | MediaType::SourceMap + | MediaType::Unknown => { + if let Some(value) = known_cache.get(specifier).map(|v| *v) { + if value == NodeModuleKind::Cjs && is_script == Some(false) { + // we now know this is actually esm + known_cache.insert(specifier.clone(), NodeModuleKind::Esm); + Some(NodeModuleKind::Esm) + } else { + Some(value) + } + } else if is_script == Some(false) { + // we know this is esm + known_cache.insert(specifier.clone(), NodeModuleKind::Esm); + Some(NodeModuleKind::Esm) + } else { + None + } + } + } + } + + fn check_based_on_pkg_json( + &self, + specifier: &Url, + ) -> Result { + if self.in_npm_pkg_checker.in_npm_package(specifier) { + if let Some(pkg_json) = + self.pkg_json_resolver.get_closest_package_json(specifier)? + { + let is_file_location_cjs = pkg_json.typ != "module"; + Ok(if is_file_location_cjs { + NodeModuleKind::Cjs + } else { + NodeModuleKind::Esm + }) + } else { + Ok(NodeModuleKind::Cjs) + } + } else if self.options.detect_cjs || self.options.is_node_main { + if let Some(pkg_json) = + self.pkg_json_resolver.get_closest_package_json(specifier)? + { + let is_cjs_type = pkg_json.typ == "commonjs" + || self.options.is_node_main && pkg_json.typ == "none"; + Ok(if is_cjs_type { + NodeModuleKind::Cjs + } else { + NodeModuleKind::Esm + }) + } else if self.options.is_node_main { + Ok(NodeModuleKind::Cjs) + } else { + Ok(NodeModuleKind::Esm) + } + } else { + Ok(NodeModuleKind::Esm) + } + } +} diff --git a/resolvers/deno/clippy.toml b/resolvers/deno/clippy.toml new file mode 100644 index 0000000000..733ac83da1 --- /dev/null +++ b/resolvers/deno/clippy.toml @@ -0,0 +1,52 @@ +disallowed-methods = [ + { path = "std::env::current_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::canonicalize", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::is_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::is_file", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::is_symlink", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::metadata", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::read_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::read_link", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::symlink_metadata", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::try_exists", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::exists", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::canonicalize", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::is_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::is_file", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::is_symlink", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::metadata", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::read_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::read_link", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::symlink_metadata", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::PathBuf::try_exists", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::env::set_current_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::env::temp_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::canonicalize", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::copy", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::create_dir_all", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::create_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::DirBuilder::new", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::hard_link", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::metadata", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::OpenOptions::new", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::read_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::read_link", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::read_to_string", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::read", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::remove_dir_all", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::remove_dir", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::remove_file", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::rename", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::set_permissions", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::symlink_metadata", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::fs::write", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::canonicalize", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "std::path::Path::exists", reason = "File system operations should be done using DenoResolverFs trait" }, + { path = "url::Url::to_file_path", reason = "Use deno_path_util instead so it works in Wasm" }, + { path = "url::Url::from_file_path", reason = "Use deno_path_util instead so it works in Wasm" }, + { path = "url::Url::from_directory_path", reason = "Use deno_path_util instead so it works in Wasm" }, +] +disallowed-types = [ + # todo(dsherret): consider for the future + # { path = "std::sync::Arc", reason = "use crate::sync::MaybeArc instead" }, +] diff --git a/resolvers/deno/fs.rs b/resolvers/deno/fs.rs new file mode 100644 index 0000000000..4929f4508e --- /dev/null +++ b/resolvers/deno/fs.rs @@ -0,0 +1,18 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::path::Path; +use std::path::PathBuf; + +pub struct DirEntry { + pub name: String, + pub is_file: bool, + pub is_directory: bool, +} + +pub trait DenoResolverFs { + fn read_to_string_lossy(&self, path: &Path) -> std::io::Result; + fn realpath_sync(&self, path: &Path) -> std::io::Result; + fn exists_sync(&self, path: &Path) -> bool; + fn is_dir_sync(&self, path: &Path) -> bool; + fn read_dir_sync(&self, dir_path: &Path) -> std::io::Result>; +} diff --git a/resolvers/deno/lib.rs b/resolvers/deno/lib.rs new file mode 100644 index 0000000000..a2b6b642fc --- /dev/null +++ b/resolvers/deno/lib.rs @@ -0,0 +1,439 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +#![deny(clippy::print_stderr)] +#![deny(clippy::print_stdout)] + +use std::path::PathBuf; +use std::sync::Arc; + +use deno_config::workspace::MappedResolution; +use deno_config::workspace::MappedResolutionDiagnostic; +use deno_config::workspace::MappedResolutionError; +use deno_config::workspace::WorkspaceResolvePkgJsonFolderError; +use deno_config::workspace::WorkspaceResolver; +use deno_package_json::PackageJsonDepValue; +use deno_package_json::PackageJsonDepValueParseError; +use deno_semver::npm::NpmPackageReqReference; +use fs::DenoResolverFs; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::NodeResolveError; +use node_resolver::errors::PackageSubpathResolveError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::NodeResolution; +use node_resolver::NodeResolutionMode; +use node_resolver::NodeResolver; +use npm::MissingPackageNodeModulesFolderError; +use npm::NodeModulesOutOfDateError; +use npm::NpmReqResolver; +use npm::ResolveIfForNpmPackageError; +use npm::ResolvePkgFolderFromDenoReqError; +use npm::ResolveReqWithSubPathError; +use sloppy_imports::SloppyImportResolverFs; +use sloppy_imports::SloppyImportsResolutionMode; +use sloppy_imports::SloppyImportsResolver; +use thiserror::Error; +use url::Url; + +pub mod cjs; +pub mod fs; +pub mod npm; +pub mod sloppy_imports; + +#[derive(Debug, Clone)] +pub struct DenoResolution { + pub url: Url, + pub maybe_diagnostic: Option>, + pub found_package_json_dep: bool, +} + +#[derive(Debug, Error)] +pub enum DenoResolveErrorKind { + #[error("Importing from the vendor directory is not permitted. Use a remote specifier instead or disable vendoring.")] + InvalidVendorFolderImport, + #[error(transparent)] + MappedResolution(#[from] MappedResolutionError), + #[error(transparent)] + MissingPackageNodeModulesFolder(#[from] MissingPackageNodeModulesFolderError), + #[error(transparent)] + Node(#[from] NodeResolveError), + #[error(transparent)] + NodeModulesOutOfDate(#[from] NodeModulesOutOfDateError), + #[error(transparent)] + PackageJsonDepValueParse(#[from] PackageJsonDepValueParseError), + #[error(transparent)] + PackageJsonDepValueUrlParse(url::ParseError), + #[error(transparent)] + PackageSubpathResolve(#[from] PackageSubpathResolveError), + #[error(transparent)] + ResolvePkgFolderFromDenoReq(#[from] ResolvePkgFolderFromDenoReqError), + #[error(transparent)] + WorkspaceResolvePkgJsonFolder(#[from] WorkspaceResolvePkgJsonFolderError), +} + +impl DenoResolveErrorKind { + pub fn into_box(self) -> DenoResolveError { + DenoResolveError(Box::new(self)) + } +} + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct DenoResolveError(pub Box); + +impl DenoResolveError { + pub fn as_kind(&self) -> &DenoResolveErrorKind { + &self.0 + } + + pub fn into_kind(self) -> DenoResolveErrorKind { + *self.0 + } +} + +impl From for DenoResolveError +where + DenoResolveErrorKind: From, +{ + fn from(err: E) -> Self { + DenoResolveError(Box::new(DenoResolveErrorKind::from(err))) + } +} + +#[derive(Debug)] +pub struct NodeAndNpmReqResolver< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, +> { + pub node_resolver: Arc>, + pub npm_req_resolver: Arc>, +} + +pub struct DenoResolverOptions< + 'a, + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, +> { + pub in_npm_pkg_checker: Arc, + pub node_and_req_resolver: + Option>, + pub sloppy_imports_resolver: + Option>>, + pub workspace_resolver: Arc, + /// Whether "bring your own node_modules" is enabled where Deno does not + /// setup the node_modules directories automatically, but instead uses + /// what already exists on the file system. + pub is_byonm: bool, + pub maybe_vendor_dir: Option<&'a PathBuf>, +} + +/// A resolver that takes care of resolution, taking into account loaded +/// import map, JSX settings. +#[derive(Debug)] +pub struct DenoResolver< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, +> { + in_npm_pkg_checker: Arc, + node_and_npm_resolver: Option>, + sloppy_imports_resolver: + Option>>, + workspace_resolver: Arc, + is_byonm: bool, + maybe_vendor_specifier: Option, +} + +impl< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, + TSloppyImportResolverFs: SloppyImportResolverFs, + > DenoResolver +{ + pub fn new( + options: DenoResolverOptions, + ) -> Self { + Self { + in_npm_pkg_checker: options.in_npm_pkg_checker, + node_and_npm_resolver: options.node_and_req_resolver, + sloppy_imports_resolver: options.sloppy_imports_resolver, + workspace_resolver: options.workspace_resolver, + is_byonm: options.is_byonm, + maybe_vendor_specifier: options + .maybe_vendor_dir + .and_then(|v| deno_path_util::url_from_directory_path(v).ok()), + } + } + + pub fn resolve( + &self, + raw_specifier: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result { + let mut found_package_json_dep = false; + let mut maybe_diagnostic = None; + // Use node resolution if we're in an npm package + if let Some(node_and_npm_resolver) = self.node_and_npm_resolver.as_ref() { + let node_resolver = &node_and_npm_resolver.node_resolver; + if referrer.scheme() == "file" + && self.in_npm_pkg_checker.in_npm_package(referrer) + { + return node_resolver + .resolve(raw_specifier, referrer, referrer_kind, mode) + .map(|res| DenoResolution { + url: res.into_url(), + found_package_json_dep, + maybe_diagnostic, + }) + .map_err(|e| e.into()); + } + } + + // Attempt to resolve with the workspace resolver + let result: Result<_, DenoResolveError> = self + .workspace_resolver + .resolve(raw_specifier, referrer) + .map_err(|err| err.into()); + let result = match result { + Ok(resolution) => match resolution { + MappedResolution::Normal { + specifier, + maybe_diagnostic: current_diagnostic, + } + | MappedResolution::ImportMap { + specifier, + maybe_diagnostic: current_diagnostic, + } => { + maybe_diagnostic = current_diagnostic; + // do sloppy imports resolution if enabled + if let Some(sloppy_imports_resolver) = &self.sloppy_imports_resolver { + Ok( + sloppy_imports_resolver + .resolve( + &specifier, + match mode { + NodeResolutionMode::Execution => { + SloppyImportsResolutionMode::Execution + } + NodeResolutionMode::Types => { + SloppyImportsResolutionMode::Types + } + }, + ) + .map(|s| s.into_specifier()) + .unwrap_or(specifier), + ) + } else { + Ok(specifier) + } + } + MappedResolution::WorkspaceJsrPackage { specifier, .. } => { + Ok(specifier) + } + MappedResolution::WorkspaceNpmPackage { + target_pkg_json: pkg_json, + sub_path, + .. + } => self + .node_and_npm_resolver + .as_ref() + .unwrap() + .node_resolver + .resolve_package_subpath_from_deno_module( + pkg_json.dir_path(), + sub_path.as_deref(), + Some(referrer), + referrer_kind, + mode, + ) + .map_err(|e| e.into()), + 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 + found_package_json_dep = true; + + dep_result + .as_ref() + .map_err(|e| { + DenoResolveErrorKind::PackageJsonDepValueParse(e.clone()) + .into_box() + }) + .and_then(|dep| match dep { + // todo(dsherret): it seems bad that we're converting this + // to a url because the req might not be a valid url. + PackageJsonDepValue::Req(req) => Url::parse(&format!( + "npm:{}{}", + req, + sub_path.map(|s| format!("/{}", s)).unwrap_or_default() + )) + .map_err(|e| { + DenoResolveErrorKind::PackageJsonDepValueUrlParse(e).into_box() + }), + PackageJsonDepValue::Workspace(version_req) => self + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_pkg_json_dep( + alias, + version_req, + ) + .map_err(|e| { + DenoResolveErrorKind::WorkspaceResolvePkgJsonFolder(e) + .into_box() + }) + .and_then(|pkg_folder| { + self + .node_and_npm_resolver + .as_ref() + .unwrap() + .node_resolver + .resolve_package_subpath_from_deno_module( + pkg_folder, + sub_path.as_deref(), + Some(referrer), + referrer_kind, + mode, + ) + .map_err(|e| { + DenoResolveErrorKind::PackageSubpathResolve(e).into_box() + }) + }), + }) + } + }, + Err(err) => Err(err), + }; + + // 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( + DenoResolveErrorKind::InvalidVendorFolderImport.into_box(), + ); + } + } + } + + let Some(NodeAndNpmReqResolver { + node_resolver, + npm_req_resolver, + }) = &self.node_and_npm_resolver + else { + return Ok(DenoResolution { + url: result?, + maybe_diagnostic, + found_package_json_dep, + }); + }; + + match result { + Ok(specifier) => { + if let Ok(npm_req_ref) = + NpmPackageReqReference::from_specifier(&specifier) + { + // check if the npm specifier resolves to a workspace member + if let Some(pkg_folder) = self + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_npm_specifier( + npm_req_ref.req(), + ) + { + return node_resolver + .resolve_package_subpath_from_deno_module( + pkg_folder, + npm_req_ref.sub_path(), + Some(referrer), + referrer_kind, + mode, + ) + .map(|url| DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + .map_err(|e| e.into()); + } + + // do npm resolution for byonm + if self.is_byonm { + return npm_req_resolver + .resolve_req_reference( + &npm_req_ref, + referrer, + referrer_kind, + mode, + ) + .map(|url| DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + .map_err(|err| match err { + ResolveReqWithSubPathError::MissingPackageNodeModulesFolder( + err, + ) => err.into(), + ResolveReqWithSubPathError::ResolvePkgFolderFromDenoReq( + err, + ) => err.into(), + ResolveReqWithSubPathError::PackageSubpathResolve(err) => { + err.into() + } + }); + } + } + + Ok(DenoResolution { + url: node_resolver + .handle_if_in_node_modules(&specifier) + .unwrap_or(specifier), + maybe_diagnostic, + found_package_json_dep, + }) + } + Err(err) => { + // If byonm, check if the bare specifier resolves to an npm package + if self.is_byonm && referrer.scheme() == "file" { + let maybe_resolution = npm_req_resolver + .resolve_if_for_npm_pkg( + raw_specifier, + referrer, + referrer_kind, + mode, + ) + .map_err(|e| match e { + ResolveIfForNpmPackageError::NodeResolve(e) => { + DenoResolveErrorKind::Node(e).into_box() + } + ResolveIfForNpmPackageError::NodeModulesOutOfDate(e) => e.into(), + })?; + if let Some(res) = maybe_resolution { + match res { + NodeResolution::Module(url) => { + return Ok(DenoResolution { + url, + maybe_diagnostic, + found_package_json_dep, + }) + } + NodeResolution::BuiltIn(_) => { + // don't resolve bare specifiers for built-in modules via node resolution + } + } + } + } + + Err(err) + } + } + } +} diff --git a/resolvers/deno/npm/byonm.rs b/resolvers/deno/npm/byonm.rs new file mode 100644 index 0000000000..e9182d47a1 --- /dev/null +++ b/resolvers/deno/npm/byonm.rs @@ -0,0 +1,398 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; + +use deno_package_json::PackageJson; +use deno_package_json::PackageJsonDepValue; +use deno_path_util::url_to_file_path; +use deno_semver::package::PackageReq; +use deno_semver::Version; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::PackageFolderResolveError; +use node_resolver::errors::PackageFolderResolveIoError; +use node_resolver::errors::PackageJsonLoadError; +use node_resolver::errors::PackageNotFoundError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NpmPackageFolderResolver; +use node_resolver::PackageJsonResolverRc; +use thiserror::Error; +use url::Url; + +use crate::fs::DenoResolverFs; + +use super::local::normalize_pkg_name_for_node_modules_deno_folder; +use super::CliNpmReqResolver; +use super::ResolvePkgFolderFromDenoReqError; + +#[derive(Debug, Error)] +pub enum ByonmResolvePkgFolderFromDenoReqError { + #[error("Could not find \"{}\" in a node_modules folder. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", .0)] + MissingAlias(String), + #[error(transparent)] + PackageJson(#[from] PackageJsonLoadError), + #[error("Could not find a matching package for 'npm:{}' in the node_modules directory. Ensure you have all your JSR and npm dependencies listed in your deno.json or package.json, then run `deno install`. Alternatively, turn on auto-install by specifying `\"nodeModulesDir\": \"auto\"` in your deno.json file.", .0)] + UnmatchedReq(PackageReq), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +pub struct ByonmNpmResolverCreateOptions< + Fs: DenoResolverFs, + TEnv: NodeResolverEnv, +> { + // todo(dsherret): investigate removing this + pub root_node_modules_dir: Option, + pub fs: Fs, + pub pkg_json_resolver: PackageJsonResolverRc, +} + +#[derive(Debug)] +pub struct ByonmNpmResolver { + fs: Fs, + pkg_json_resolver: PackageJsonResolverRc, + root_node_modules_dir: Option, +} + +impl Clone + for ByonmNpmResolver +{ + fn clone(&self) -> Self { + Self { + fs: self.fs.clone(), + pkg_json_resolver: self.pkg_json_resolver.clone(), + root_node_modules_dir: self.root_node_modules_dir.clone(), + } + } +} + +impl ByonmNpmResolver { + pub fn new(options: ByonmNpmResolverCreateOptions) -> Self { + Self { + root_node_modules_dir: options.root_node_modules_dir, + fs: options.fs, + pkg_json_resolver: options.pkg_json_resolver, + } + } + + pub fn root_node_modules_dir(&self) -> Option<&Path> { + self.root_node_modules_dir.as_deref() + } + + fn load_pkg_json( + &self, + path: &Path, + ) -> Result>, PackageJsonLoadError> { + self.pkg_json_resolver.load_package_json(path) + } + + /// Finds the ancestor package.json that contains the specified dependency. + pub fn find_ancestor_package_json_with_dep( + &self, + dep_name: &str, + referrer: &Url, + ) -> Option> { + let referrer_path = url_to_file_path(referrer).ok()?; + let mut current_folder = referrer_path.parent()?; + loop { + let pkg_json_path = current_folder.join("package.json"); + if let Ok(Some(pkg_json)) = self.load_pkg_json(&pkg_json_path) { + if let Some(deps) = &pkg_json.dependencies { + if deps.contains_key(dep_name) { + return Some(pkg_json); + } + } + if let Some(deps) = &pkg_json.dev_dependencies { + if deps.contains_key(dep_name) { + return Some(pkg_json); + } + } + } + + if let Some(parent) = current_folder.parent() { + current_folder = parent; + } else { + return None; + } + } + } + + pub fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + referrer: &Url, + ) -> Result { + fn node_resolve_dir( + fs: &Fs, + alias: &str, + start_dir: &Path, + ) -> std::io::Result> { + for ancestor in start_dir.ancestors() { + let node_modules_folder = ancestor.join("node_modules"); + let sub_dir = join_package_name(&node_modules_folder, alias); + if fs.is_dir_sync(&sub_dir) { + return Ok(Some(deno_path_util::canonicalize_path_maybe_not_exists( + &sub_dir, + &|path| fs.realpath_sync(path), + )?)); + } + } + Ok(None) + } + + // now attempt to resolve if it's found in any package.json + let maybe_pkg_json_and_alias = + self.resolve_pkg_json_and_alias_for_req(req, referrer)?; + match maybe_pkg_json_and_alias { + Some((pkg_json, alias)) => { + // now try node resolution + if let Some(resolved) = + node_resolve_dir(&self.fs, &alias, pkg_json.dir_path())? + { + return Ok(resolved); + } + + Err(ByonmResolvePkgFolderFromDenoReqError::MissingAlias(alias)) + } + None => { + // now check if node_modules/.deno/ matches this constraint + if let Some(folder) = self.resolve_folder_in_root_node_modules(req) { + return Ok(folder); + } + + Err(ByonmResolvePkgFolderFromDenoReqError::UnmatchedReq( + req.clone(), + )) + } + } + } + + fn resolve_pkg_json_and_alias_for_req( + &self, + req: &PackageReq, + referrer: &Url, + ) -> Result, String)>, PackageJsonLoadError> { + 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) = url_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) = self.load_pkg_json(&package_json_path)? { + if let Some(alias) = + resolve_alias_from_pkg_json(req, pkg_json.as_ref()) + { + return Ok(Some((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) = self.load_pkg_json(&root_pkg_json_path)? { + if let Some(alias) = resolve_alias_from_pkg_json(req, pkg_json.as_ref()) + { + return Ok(Some((pkg_json, alias))); + } + } + } + + Ok(None) + } + + fn resolve_folder_in_root_node_modules( + &self, + req: &PackageReq, + ) -> Option { + // now check if node_modules/.deno/ matches this constraint + let root_node_modules_dir = self.root_node_modules_dir.as_ref()?; + let node_modules_deno_dir = root_node_modules_dir.join(".deno"); + let Ok(entries) = self.fs.read_dir_sync(&node_modules_deno_dir) else { + return None; + }; + let search_prefix = format!( + "{}@", + normalize_pkg_name_for_node_modules_deno_folder(&req.name) + ); + let mut best_version = None; + + // example entries: + // - @denotest+add@1.0.0 + // - @denotest+add@1.0.0_1 + for entry in entries { + if !entry.is_directory { + continue; + } + let Some(version_and_copy_idx) = entry.name.strip_prefix(&search_prefix) + else { + continue; + }; + let version = version_and_copy_idx + .rsplit_once('_') + .map(|(v, _)| v) + .unwrap_or(version_and_copy_idx); + let Ok(version) = Version::parse_from_npm(version) else { + continue; + }; + if let Some(tag) = req.version_req.tag() { + let initialized_file = + node_modules_deno_dir.join(&entry.name).join(".initialized"); + let Ok(contents) = self.fs.read_to_string_lossy(&initialized_file) + else { + continue; + }; + let mut tags = contents.split(',').map(str::trim); + if tags.any(|t| t == tag) { + if let Some((best_version_version, _)) = &best_version { + if version > *best_version_version { + best_version = Some((version, entry.name)); + } + } else { + best_version = Some((version, entry.name)); + } + } + } else if req.version_req.matches(&version) { + if let Some((best_version_version, _)) = &best_version { + if version > *best_version_version { + best_version = Some((version, entry.name)); + } + } else { + best_version = Some((version, entry.name)); + } + } + } + + best_version.map(|(_version, entry_name)| { + join_package_name( + &node_modules_deno_dir.join(entry_name).join("node_modules"), + &req.name, + ) + }) + } +} + +impl< + Fs: DenoResolverFs + Send + Sync + std::fmt::Debug, + TEnv: NodeResolverEnv, + > CliNpmReqResolver for ByonmNpmResolver +{ + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + referrer: &Url, + ) -> Result { + ByonmNpmResolver::resolve_pkg_folder_from_deno_module_req( + self, req, referrer, + ) + .map_err(ResolvePkgFolderFromDenoReqError::Byonm) + } +} + +impl< + Fs: DenoResolverFs + Send + Sync + std::fmt::Debug, + TEnv: NodeResolverEnv, + > NpmPackageFolderResolver for ByonmNpmResolver +{ + fn resolve_package_folder_from_package( + &self, + name: &str, + referrer: &Url, + ) -> Result { + fn inner( + fs: &Fs, + name: &str, + referrer: &Url, + ) -> Result { + let maybe_referrer_file = url_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")) + }; + + let sub_dir = join_package_name(&node_modules_folder, name); + if fs.is_dir_sync(&sub_dir) { + return Ok(sub_dir); + } + } + } + + Err( + PackageNotFoundError { + package_name: name.to_string(), + referrer: referrer.clone(), + referrer_extra: None, + } + .into(), + ) + } + + let path = inner(&self.fs, name, referrer)?; + self.fs.realpath_sync(&path).map_err(|err| { + PackageFolderResolveIoError { + package_name: name.to_string(), + referrer: referrer.clone(), + source: err, + } + .into() + }) + } +} + +#[derive(Debug)] +pub struct ByonmInNpmPackageChecker; + +impl InNpmPackageChecker for ByonmInNpmPackageChecker { + fn in_npm_package(&self, specifier: &Url) -> bool { + specifier.scheme() == "file" + && specifier + .path() + .to_ascii_lowercase() + .contains("/node_modules/") + } +} + +fn join_package_name(path: &Path, package_name: &str) -> PathBuf { + let mut path = path.to_path_buf(); + // ensure backslashes are used on windows + for part in package_name.split('/') { + path = path.join(part); + } + path +} diff --git a/resolvers/deno/npm/local.rs b/resolvers/deno/npm/local.rs new file mode 100644 index 0000000000..aef476ad94 --- /dev/null +++ b/resolvers/deno/npm/local.rs @@ -0,0 +1,27 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; + +/// Normalizes a package name for use at `node_modules/.deno/@[_]` +pub fn normalize_pkg_name_for_node_modules_deno_folder(name: &str) -> Cow { + let name = if name.to_lowercase() == name { + Cow::Borrowed(name) + } else { + Cow::Owned(format!("_{}", mixed_case_package_name_encode(name))) + }; + if name.starts_with('@') { + name.replace('/', "+").into() + } else { + name + } +} + +fn mixed_case_package_name_encode(name: &str) -> String { + // use base32 encoding because it's reversible and the character set + // only includes the characters within 0-9 and A-Z so it can be lower cased + base32::encode( + base32::Alphabet::Rfc4648Lower { padding: false }, + name.as_bytes(), + ) + .to_lowercase() +} diff --git a/resolvers/deno/npm/mod.rs b/resolvers/deno/npm/mod.rs new file mode 100644 index 0000000000..b0aec71b08 --- /dev/null +++ b/resolvers/deno/npm/mod.rs @@ -0,0 +1,256 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::fmt::Debug; +use std::path::PathBuf; +use std::sync::Arc; + +use deno_semver::npm::NpmPackageReqReference; +use deno_semver::package::PackageReq; +use node_resolver::env::NodeResolverEnv; +use node_resolver::errors::NodeResolveError; +use node_resolver::errors::NodeResolveErrorKind; +use node_resolver::errors::PackageFolderResolveErrorKind; +use node_resolver::errors::PackageFolderResolveIoError; +use node_resolver::errors::PackageNotFoundError; +use node_resolver::errors::PackageResolveErrorKind; +use node_resolver::errors::PackageSubpathResolveError; +use node_resolver::InNpmPackageChecker; +use node_resolver::NodeModuleKind; +use node_resolver::NodeResolution; +use node_resolver::NodeResolutionMode; +use node_resolver::NodeResolver; +use thiserror::Error; +use url::Url; + +use crate::fs::DenoResolverFs; + +pub use byonm::ByonmInNpmPackageChecker; +pub use byonm::ByonmNpmResolver; +pub use byonm::ByonmNpmResolverCreateOptions; +pub use byonm::ByonmResolvePkgFolderFromDenoReqError; +pub use local::normalize_pkg_name_for_node_modules_deno_folder; + +mod byonm; +mod local; + +#[derive(Debug, Error)] +#[error("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 `deno install`?", specifier)] +pub struct NodeModulesOutOfDateError { + pub specifier: String, +} + +#[derive(Debug, Error)] +#[error("Could not find '{}'. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", package_json_path.display())] +pub struct MissingPackageNodeModulesFolderError { + pub package_json_path: PathBuf, +} + +#[derive(Debug, Error)] +pub enum ResolveIfForNpmPackageError { + #[error(transparent)] + NodeResolve(#[from] NodeResolveError), + #[error(transparent)] + NodeModulesOutOfDate(#[from] NodeModulesOutOfDateError), +} + +#[derive(Debug, Error)] +pub enum ResolveReqWithSubPathError { + #[error(transparent)] + MissingPackageNodeModulesFolder(#[from] MissingPackageNodeModulesFolderError), + #[error(transparent)] + ResolvePkgFolderFromDenoReq(#[from] ResolvePkgFolderFromDenoReqError), + #[error(transparent)] + PackageSubpathResolve(#[from] PackageSubpathResolveError), +} + +#[derive(Debug, Error)] +pub enum ResolvePkgFolderFromDenoReqError { + // todo(dsherret): don't use anyhow here + #[error(transparent)] + Managed(anyhow::Error), + #[error(transparent)] + Byonm(#[from] ByonmResolvePkgFolderFromDenoReqError), +} + +// todo(dsherret): a temporary trait until we extract +// out the CLI npm resolver into here +pub trait CliNpmReqResolver: Debug + Send + Sync { + fn resolve_pkg_folder_from_deno_module_req( + &self, + req: &PackageReq, + referrer: &Url, + ) -> Result; +} + +pub struct NpmReqResolverOptions< + Fs: DenoResolverFs, + TNodeResolverEnv: NodeResolverEnv, +> { + /// The resolver when "bring your own node_modules" is enabled where Deno + /// does not setup the node_modules directories automatically, but instead + /// uses what already exists on the file system. + pub byonm_resolver: Option>>, + pub fs: Fs, + pub in_npm_pkg_checker: Arc, + pub node_resolver: Arc>, + pub npm_req_resolver: Arc, +} + +#[derive(Debug)] +pub struct NpmReqResolver +{ + byonm_resolver: Option>>, + fs: Fs, + in_npm_pkg_checker: Arc, + node_resolver: Arc>, + npm_resolver: Arc, +} + +impl + NpmReqResolver +{ + pub fn new(options: NpmReqResolverOptions) -> Self { + Self { + byonm_resolver: options.byonm_resolver, + fs: options.fs, + in_npm_pkg_checker: options.in_npm_pkg_checker, + node_resolver: options.node_resolver, + npm_resolver: options.npm_req_resolver, + } + } + + pub fn resolve_req_reference( + &self, + req_ref: &NpmPackageReqReference, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result { + self.resolve_req_with_sub_path( + req_ref.req(), + req_ref.sub_path(), + referrer, + referrer_kind, + mode, + ) + } + + pub fn resolve_req_with_sub_path( + &self, + req: &PackageReq, + sub_path: Option<&str>, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result { + let package_folder = self + .npm_resolver + .resolve_pkg_folder_from_deno_module_req(req, referrer)?; + let resolution_result = + self.node_resolver.resolve_package_subpath_from_deno_module( + &package_folder, + sub_path, + Some(referrer), + referrer_kind, + mode, + ); + match resolution_result { + Ok(url) => Ok(url), + Err(err) => { + if self.byonm_resolver.is_some() { + let package_json_path = package_folder.join("package.json"); + if !self.fs.exists_sync(&package_json_path) { + return Err( + MissingPackageNodeModulesFolderError { package_json_path }.into(), + ); + } + } + Err(err.into()) + } + } + } + + pub fn resolve_if_for_npm_pkg( + &self, + specifier: &str, + referrer: &Url, + referrer_kind: NodeModuleKind, + mode: NodeResolutionMode, + ) -> Result, ResolveIfForNpmPackageError> { + let resolution_result = + self + .node_resolver + .resolve(specifier, referrer, referrer_kind, mode); + match resolution_result { + Ok(res) => Ok(Some(res)), + Err(err) => { + let err = err.into_kind(); + match err { + NodeResolveErrorKind::RelativeJoin(_) + | NodeResolveErrorKind::PackageImportsResolve(_) + | NodeResolveErrorKind::UnsupportedEsmUrlScheme(_) + | NodeResolveErrorKind::DataUrlReferrer(_) + | NodeResolveErrorKind::TypesNotFound(_) + | NodeResolveErrorKind::FinalizeResolution(_) => { + Err(ResolveIfForNpmPackageError::NodeResolve(err.into())) + } + NodeResolveErrorKind::PackageResolve(err) => { + let err = err.into_kind(); + match err { + PackageResolveErrorKind::ClosestPkgJson(_) + | PackageResolveErrorKind::InvalidModuleSpecifier(_) + | PackageResolveErrorKind::ExportsResolve(_) + | PackageResolveErrorKind::SubpathResolve(_) => { + Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )) + } + PackageResolveErrorKind::PackageFolderResolve(err) => { + match err.as_kind() { + PackageFolderResolveErrorKind::Io( + PackageFolderResolveIoError { package_name, .. }, + ) + | PackageFolderResolveErrorKind::PackageNotFound( + PackageNotFoundError { package_name, .. }, + ) => { + if self.in_npm_pkg_checker.in_npm_package(referrer) { + return Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )); + } + if let Some(byonm_npm_resolver) = &self.byonm_resolver { + if byonm_npm_resolver + .find_ancestor_package_json_with_dep( + package_name, + referrer, + ) + .is_some() + { + return Err( + ResolveIfForNpmPackageError::NodeModulesOutOfDate( + NodeModulesOutOfDateError { + specifier: specifier.to_string(), + }, + ), + ); + } + } + Ok(None) + } + PackageFolderResolveErrorKind::ReferrerNotFound(_) => { + if self.in_npm_pkg_checker.in_npm_package(referrer) { + return Err(ResolveIfForNpmPackageError::NodeResolve( + NodeResolveErrorKind::PackageResolve(err.into()).into(), + )); + } + Ok(None) + } + } + } + } + } + } + } + } + } +} diff --git a/resolvers/deno/sloppy_imports.rs b/resolvers/deno/sloppy_imports.rs new file mode 100644 index 0000000000..7aba5b771a --- /dev/null +++ b/resolvers/deno/sloppy_imports.rs @@ -0,0 +1,513 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; +use std::path::Path; +use std::path::PathBuf; + +use deno_media_type::MediaType; +use deno_path_util::url_from_file_path; +use deno_path_util::url_to_file_path; +use url::Url; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SloppyImportsFsEntry { + File, + Dir, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SloppyImportsResolution { + /// Ex. `./file.js` to `./file.ts` + JsToTs(Url), + /// Ex. `./file` to `./file.ts` + NoExtension(Url), + /// Ex. `./dir` to `./dir/index.ts` + Directory(Url), +} + +impl SloppyImportsResolution { + pub fn as_specifier(&self) -> &Url { + match self { + Self::JsToTs(specifier) => specifier, + Self::NoExtension(specifier) => specifier, + Self::Directory(specifier) => specifier, + } + } + + pub fn into_specifier(self) -> Url { + match self { + Self::JsToTs(specifier) => specifier, + Self::NoExtension(specifier) => specifier, + Self::Directory(specifier) => specifier, + } + } + + pub fn as_suggestion_message(&self) -> String { + format!("Maybe {}", self.as_base_message()) + } + + pub fn as_quick_fix_message(&self) -> String { + let message = self.as_base_message(); + let mut chars = message.chars(); + format!( + "{}{}.", + chars.next().unwrap().to_uppercase(), + chars.as_str() + ) + } + + fn as_base_message(&self) -> String { + match self { + SloppyImportsResolution::JsToTs(specifier) => { + let media_type = MediaType::from_specifier(specifier); + format!("change the extension to '{}'", media_type.as_ts_extension()) + } + SloppyImportsResolution::NoExtension(specifier) => { + let media_type = MediaType::from_specifier(specifier); + format!("add a '{}' extension", media_type.as_ts_extension()) + } + SloppyImportsResolution::Directory(specifier) => { + let file_name = specifier + .path() + .rsplit_once('/') + .map(|(_, file_name)| file_name) + .unwrap_or(specifier.path()); + format!("specify path to '{}' file in directory instead", file_name) + } + } + } +} + +/// The kind of resolution currently being done. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SloppyImportsResolutionMode { + /// Resolving for code that will be executed. + Execution, + /// Resolving for code that will be used for type information. + Types, +} + +impl SloppyImportsResolutionMode { + pub fn is_types(&self) -> bool { + *self == SloppyImportsResolutionMode::Types + } +} + +pub trait SloppyImportResolverFs { + fn stat_sync(&self, path: &Path) -> Option; + + fn is_file(&self, path: &Path) -> bool { + self.stat_sync(path) == Some(SloppyImportsFsEntry::File) + } +} + +#[derive(Debug)] +pub struct SloppyImportsResolver { + fs: Fs, +} + +impl SloppyImportsResolver { + pub fn new(fs: Fs) -> Self { + Self { fs } + } + + pub fn resolve( + &self, + specifier: &Url, + mode: SloppyImportsResolutionMode, + ) -> Option { + fn path_without_ext( + path: &Path, + media_type: MediaType, + ) -> Option> { + let old_path_str = path.to_string_lossy(); + match media_type { + MediaType::Unknown => Some(old_path_str), + _ => old_path_str + .strip_suffix(media_type.as_ts_extension()) + .map(|s| Cow::Owned(s.to_string())), + } + } + + fn media_types_to_paths( + path_no_ext: &str, + original_media_type: MediaType, + probe_media_type_types: Vec, + reason: SloppyImportsResolutionReason, + ) -> Vec<(PathBuf, SloppyImportsResolutionReason)> { + probe_media_type_types + .into_iter() + .filter(|media_type| *media_type != original_media_type) + .map(|media_type| { + ( + PathBuf::from(format!( + "{}{}", + path_no_ext, + media_type.as_ts_extension() + )), + reason, + ) + }) + .collect::>() + } + + if specifier.scheme() != "file" { + return None; + } + + let path = url_to_file_path(specifier).ok()?; + + #[derive(Clone, Copy)] + enum SloppyImportsResolutionReason { + JsToTs, + NoExtension, + Directory, + } + + let probe_paths: Vec<(PathBuf, SloppyImportsResolutionReason)> = + match self.fs.stat_sync(&path) { + Some(SloppyImportsFsEntry::File) => { + if mode.is_types() { + let media_type = MediaType::from_specifier(specifier); + // attempt to resolve the .d.ts file before the .js file + let probe_media_type_types = match media_type { + MediaType::JavaScript => { + vec![(MediaType::Dts), MediaType::JavaScript] + } + MediaType::Mjs => { + vec![MediaType::Dmts, MediaType::Dts, MediaType::Mjs] + } + MediaType::Cjs => { + vec![MediaType::Dcts, MediaType::Dts, MediaType::Cjs] + } + _ => return None, + }; + let path_no_ext = path_without_ext(&path, media_type)?; + media_types_to_paths( + &path_no_ext, + media_type, + probe_media_type_types, + SloppyImportsResolutionReason::JsToTs, + ) + } else { + return None; + } + } + entry @ None | entry @ Some(SloppyImportsFsEntry::Dir) => { + let media_type = MediaType::from_specifier(specifier); + let probe_media_type_types = match media_type { + MediaType::JavaScript => ( + if mode.is_types() { + vec![MediaType::TypeScript, MediaType::Tsx, MediaType::Dts] + } else { + vec![MediaType::TypeScript, MediaType::Tsx] + }, + SloppyImportsResolutionReason::JsToTs, + ), + MediaType::Jsx => { + (vec![MediaType::Tsx], SloppyImportsResolutionReason::JsToTs) + } + MediaType::Mjs => ( + if mode.is_types() { + vec![MediaType::Mts, MediaType::Dmts, MediaType::Dts] + } else { + vec![MediaType::Mts] + }, + SloppyImportsResolutionReason::JsToTs, + ), + MediaType::Cjs => ( + if mode.is_types() { + vec![MediaType::Cts, MediaType::Dcts, MediaType::Dts] + } else { + vec![MediaType::Cts] + }, + SloppyImportsResolutionReason::JsToTs, + ), + MediaType::TypeScript + | MediaType::Mts + | MediaType::Cts + | MediaType::Dts + | MediaType::Dmts + | MediaType::Dcts + | MediaType::Tsx + | MediaType::Json + | MediaType::Wasm + | MediaType::Css + | MediaType::SourceMap => { + return None; + } + MediaType::Unknown => ( + if mode.is_types() { + vec![ + MediaType::TypeScript, + MediaType::Tsx, + MediaType::Mts, + MediaType::Dts, + MediaType::Dmts, + MediaType::Dcts, + MediaType::JavaScript, + MediaType::Jsx, + MediaType::Mjs, + ] + } else { + vec![ + MediaType::TypeScript, + MediaType::JavaScript, + MediaType::Tsx, + MediaType::Jsx, + MediaType::Mts, + MediaType::Mjs, + ] + }, + SloppyImportsResolutionReason::NoExtension, + ), + }; + let mut probe_paths = match path_without_ext(&path, media_type) { + Some(path_no_ext) => media_types_to_paths( + &path_no_ext, + media_type, + probe_media_type_types.0, + probe_media_type_types.1, + ), + None => vec![], + }; + + if matches!(entry, Some(SloppyImportsFsEntry::Dir)) { + // try to resolve at the index file + if mode.is_types() { + probe_paths.push(( + path.join("index.ts"), + SloppyImportsResolutionReason::Directory, + )); + + probe_paths.push(( + path.join("index.mts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.d.ts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.d.mts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.js"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.mjs"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.tsx"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.jsx"), + SloppyImportsResolutionReason::Directory, + )); + } else { + probe_paths.push(( + path.join("index.ts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.mts"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.tsx"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.js"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.mjs"), + SloppyImportsResolutionReason::Directory, + )); + probe_paths.push(( + path.join("index.jsx"), + SloppyImportsResolutionReason::Directory, + )); + } + } + if probe_paths.is_empty() { + return None; + } + probe_paths + } + }; + + for (probe_path, reason) in probe_paths { + if self.fs.is_file(&probe_path) { + if let Ok(specifier) = url_from_file_path(&probe_path) { + match reason { + SloppyImportsResolutionReason::JsToTs => { + return Some(SloppyImportsResolution::JsToTs(specifier)); + } + SloppyImportsResolutionReason::NoExtension => { + return Some(SloppyImportsResolution::NoExtension(specifier)); + } + SloppyImportsResolutionReason::Directory => { + return Some(SloppyImportsResolution::Directory(specifier)); + } + } + } + } + } + + None + } +} + +#[cfg(test)] +mod test { + use test_util::TestContext; + + use super::*; + + #[test] + fn test_unstable_sloppy_imports() { + fn resolve(specifier: &Url) -> Option { + resolve_with_mode(specifier, SloppyImportsResolutionMode::Execution) + } + + fn resolve_types(specifier: &Url) -> Option { + resolve_with_mode(specifier, SloppyImportsResolutionMode::Types) + } + + fn resolve_with_mode( + specifier: &Url, + mode: SloppyImportsResolutionMode, + ) -> Option { + struct RealSloppyImportsResolverFs; + impl SloppyImportResolverFs for RealSloppyImportsResolverFs { + fn stat_sync(&self, path: &Path) -> Option { + #[allow(clippy::disallowed_methods)] + let stat = std::fs::metadata(path).ok()?; + if stat.is_dir() { + Some(SloppyImportsFsEntry::Dir) + } else if stat.is_file() { + Some(SloppyImportsFsEntry::File) + } else { + None + } + } + } + + SloppyImportsResolver::new(RealSloppyImportsResolverFs) + .resolve(specifier, mode) + } + + let context = TestContext::default(); + let temp_dir = context.temp_dir().path(); + + // scenarios like resolving ./example.js to ./example.ts + for (ext_from, ext_to) in [("js", "ts"), ("js", "tsx"), ("mjs", "mts")] { + let ts_file = temp_dir.join(format!("file.{}", ext_to)); + ts_file.write(""); + assert_eq!(resolve(&ts_file.url_file()), None); + assert_eq!( + resolve( + &temp_dir + .url_dir() + .join(&format!("file.{}", ext_from)) + .unwrap() + ), + Some(SloppyImportsResolution::JsToTs(ts_file.url_file())), + ); + ts_file.remove_file(); + } + + // no extension scenarios + for ext in ["js", "ts", "js", "tsx", "jsx", "mjs", "mts"] { + let file = temp_dir.join(format!("file.{}", ext)); + file.write(""); + assert_eq!( + resolve( + &temp_dir + .url_dir() + .join("file") // no ext + .unwrap() + ), + Some(SloppyImportsResolution::NoExtension(file.url_file())) + ); + file.remove_file(); + } + + // .ts and .js exists, .js specified (goes to specified) + { + let ts_file = temp_dir.join("file.ts"); + ts_file.write(""); + let js_file = temp_dir.join("file.js"); + js_file.write(""); + assert_eq!(resolve(&js_file.url_file()), None); + } + + // only js exists, .js specified + { + let js_only_file = temp_dir.join("js_only.js"); + js_only_file.write(""); + assert_eq!(resolve(&js_only_file.url_file()), None); + assert_eq!(resolve_types(&js_only_file.url_file()), None); + } + + // resolving a directory to an index file + { + let routes_dir = temp_dir.join("routes"); + routes_dir.create_dir_all(); + let index_file = routes_dir.join("index.ts"); + index_file.write(""); + assert_eq!( + resolve(&routes_dir.url_file()), + Some(SloppyImportsResolution::Directory(index_file.url_file())), + ); + } + + // both a directory and a file with specifier is present + { + let api_dir = temp_dir.join("api"); + api_dir.create_dir_all(); + let bar_file = api_dir.join("bar.ts"); + bar_file.write(""); + let api_file = temp_dir.join("api.ts"); + api_file.write(""); + assert_eq!( + resolve(&api_dir.url_file()), + Some(SloppyImportsResolution::NoExtension(api_file.url_file())), + ); + } + } + + #[test] + fn test_sloppy_import_resolution_suggestion_message() { + // directory + assert_eq!( + SloppyImportsResolution::Directory( + Url::parse("file:///dir/index.js").unwrap() + ) + .as_suggestion_message(), + "Maybe specify path to 'index.js' file in directory instead" + ); + // no ext + assert_eq!( + SloppyImportsResolution::NoExtension( + Url::parse("file:///dir/index.mjs").unwrap() + ) + .as_suggestion_message(), + "Maybe add a '.mjs' extension" + ); + // js to ts + assert_eq!( + SloppyImportsResolution::JsToTs( + Url::parse("file:///dir/index.mts").unwrap() + ) + .as_suggestion_message(), + "Maybe change the extension to '.mts'" + ); + } +} diff --git a/ext/node_resolver/Cargo.toml b/resolvers/node/Cargo.toml similarity index 93% rename from ext/node_resolver/Cargo.toml rename to resolvers/node/Cargo.toml index 1042045692..6c2407ad9f 100644 --- a/ext/node_resolver/Cargo.toml +++ b/resolvers/node/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "node_resolver" -version = "0.7.0" +version = "0.16.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -21,6 +21,7 @@ anyhow.workspace = true async-trait.workspace = true deno_media_type.workspace = true deno_package_json.workspace = true +deno_path_util.workspace = true futures.workspace = true lazy-regex.workspace = true once_cell.workspace = true diff --git a/ext/node_resolver/README.md b/resolvers/node/README.md similarity index 100% rename from ext/node_resolver/README.md rename to resolvers/node/README.md diff --git a/ext/node_resolver/analyze.rs b/resolvers/node/analyze.rs similarity index 87% rename from ext/node_resolver/analyze.rs rename to resolvers/node/analyze.rs index deb56d064e..9126890805 100644 --- a/ext/node_resolver/analyze.rs +++ b/resolvers/node/analyze.rs @@ -6,6 +6,8 @@ use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; +use deno_path_util::url_from_file_path; +use deno_path_util::url_to_file_path; use futures::future::LocalBoxFuture; use futures::stream::FuturesUnordered; use futures::FutureExt; @@ -17,19 +19,19 @@ use anyhow::Error as AnyError; use url::Url; use crate::env::NodeResolverEnv; -use crate::package_json::load_pkg_json; -use crate::path::to_file_specifier; +use crate::npm::InNpmPackageCheckerRc; use crate::resolution::NodeResolverRc; use crate::NodeModuleKind; use crate::NodeResolutionMode; -use crate::NpmResolverRc; +use crate::NpmPackageFolderResolverRc; +use crate::PackageJsonResolverRc; use crate::PathClean; #[derive(Debug, Clone)] -pub enum CjsAnalysis { +pub enum CjsAnalysis<'a> { /// File was found to be an ES module and the translator should /// load the code as ESM. - Esm(String), + Esm(Cow<'a, str>), Cjs(CjsAnalysisExports), } @@ -49,11 +51,11 @@ 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. - async fn analyze_cjs( + async fn analyze_cjs<'a>( &self, specifier: &Url, - maybe_source: Option, - ) -> Result; + maybe_source: Option>, + ) -> Result, AnyError>; } pub struct NodeCodeTranslator< @@ -62,8 +64,10 @@ pub struct NodeCodeTranslator< > { cjs_code_analyzer: TCjsCodeAnalyzer, env: TNodeResolverEnv, + in_npm_pkg_checker: InNpmPackageCheckerRc, node_resolver: NodeResolverRc, - npm_resolver: NpmResolverRc, + npm_resolver: NpmPackageFolderResolverRc, + pkg_json_resolver: PackageJsonResolverRc, } impl @@ -72,14 +76,18 @@ impl pub fn new( cjs_code_analyzer: TCjsCodeAnalyzer, env: TNodeResolverEnv, + in_npm_pkg_checker: InNpmPackageCheckerRc, node_resolver: NodeResolverRc, - npm_resolver: NpmResolverRc, + npm_resolver: NpmPackageFolderResolverRc, + pkg_json_resolver: PackageJsonResolverRc, ) -> Self { Self { cjs_code_analyzer, env, + in_npm_pkg_checker, node_resolver, npm_resolver, + pkg_json_resolver, } } @@ -89,11 +97,11 @@ impl /// For all discovered reexports the analysis will be performed recursively. /// /// If successful a source code for equivalent ES module is returned. - pub async fn translate_cjs_to_esm( + pub async fn translate_cjs_to_esm<'a>( &self, entry_specifier: &Url, - source: Option, - ) -> Result { + source: Option>, + ) -> Result, AnyError> { let mut temp_var_count = 0; let analysis = self @@ -107,7 +115,7 @@ impl }; let mut source = vec![ - r#"import {createRequire as __internalCreateRequire} from "node:module"; + r#"import {createRequire as __internalCreateRequire, Module as __internalModule } from "node:module"; const require = __internalCreateRequire(import.meta.url);"# .to_string(), ]; @@ -134,9 +142,13 @@ impl } source.push(format!( - "const mod = require(\"{}\");", - entry_specifier - .to_file_path() + r#"let mod; + if (import.meta.main) {{ + mod = __internalModule._load("{0}", null, true) + }} else {{ + mod = require("{0}"); + }}"#, + url_to_file_path(entry_specifier) .unwrap() .to_str() .unwrap() @@ -159,7 +171,7 @@ impl source.push("export default mod;".to_string()); let translated_source = source.join("\n"); - Ok(translated_source) + Ok(Cow::Owned(translated_source)) } async fn analyze_reexports<'a>( @@ -174,7 +186,7 @@ impl struct Analysis { reexport_specifier: url::Url, referrer: url::Url, - analysis: CjsAnalysis, + analysis: CjsAnalysis<'static>, } type AnalysisFuture<'a> = LocalBoxFuture<'a, Result>; @@ -297,15 +309,13 @@ impl todo!(); } - let referrer_path = referrer.to_file_path().unwrap(); + let referrer_path = url_to_file_path(referrer).unwrap(); if specifier.starts_with("./") || specifier.starts_with("../") { if let Some(parent) = referrer_path.parent() { - return Some( - self - .file_extension_probe(parent.join(specifier), &referrer_path) - .map(|p| to_file_specifier(&p)), - ) - .transpose(); + return self + .file_extension_probe(parent.join(specifier), &referrer_path) + .and_then(|p| url_from_file_path(&p).map_err(AnyError::from)) + .map(Some); } else { todo!(); } @@ -331,8 +341,9 @@ impl }?; let package_json_path = module_dir.join("package.json"); - let maybe_package_json = - load_pkg_json(self.env.pkg_json_fs(), &package_json_path)?; + let maybe_package_json = self + .pkg_json_resolver + .load_package_json(&package_json_path)?; if let Some(package_json) = maybe_package_json { if let Some(exports) = &package_json.exports { return Some( @@ -358,35 +369,34 @@ impl if self.env.is_dir_sync(&d) { // subdir might have a package.json that specifies the entrypoint let package_json_path = d.join("package.json"); - let maybe_package_json = - load_pkg_json(self.env.pkg_json_fs(), &package_json_path)?; + let maybe_package_json = self + .pkg_json_resolver + .load_package_json(&package_json_path)?; if let Some(package_json) = maybe_package_json { if let Some(main) = package_json.main(NodeModuleKind::Cjs) { - return Ok(Some(to_file_specifier(&d.join(main).clean()))); + return Ok(Some(url_from_file_path(&d.join(main).clean())?)); } } - return Ok(Some(to_file_specifier(&d.join("index.js").clean()))); + return Ok(Some(url_from_file_path(&d.join("index.js").clean())?)); } - return Some( - self - .file_extension_probe(d, &referrer_path) - .map(|p| to_file_specifier(&p)), - ) - .transpose(); + return self + .file_extension_probe(d, &referrer_path) + .and_then(|p| url_from_file_path(&p).map_err(AnyError::from)) + .map(Some); } else if let Some(main) = package_json.main(NodeModuleKind::Cjs) { - return Ok(Some(to_file_specifier(&module_dir.join(main).clean()))); + return Ok(Some(url_from_file_path(&module_dir.join(main).clean())?)); } else { - return Ok(Some(to_file_specifier( + return Ok(Some(url_from_file_path( &module_dir.join("index.js").clean(), - ))); + )?)); } } // as a fallback, attempt to resolve it via the ancestor directories let mut last = referrer_path.as_path(); while let Some(parent) = last.parent() { - if !self.npm_resolver.in_npm_package_at_dir_path(parent) { + if !self.in_npm_pkg_checker.in_npm_package_at_dir_path(parent) { break; } let path = if parent.ends_with("node_modules") { @@ -395,7 +405,7 @@ impl parent.join("node_modules").join(specifier) }; if let Ok(path) = self.file_extension_probe(path, &referrer_path) { - return Ok(Some(to_file_specifier(&path))); + return Ok(Some(url_from_file_path(&path)?)); } last = parent; } diff --git a/ext/node_resolver/clippy.toml b/resolvers/node/clippy.toml similarity index 94% rename from ext/node_resolver/clippy.toml rename to resolvers/node/clippy.toml index 86150781bb..90eaba3fae 100644 --- a/ext/node_resolver/clippy.toml +++ b/resolvers/node/clippy.toml @@ -42,6 +42,9 @@ disallowed-methods = [ { path = "std::fs::write", reason = "File system operations should be done using NodeResolverFs trait" }, { path = "std::path::Path::canonicalize", reason = "File system operations should be done using NodeResolverFs trait" }, { path = "std::path::Path::exists", reason = "File system operations should be done using NodeResolverFs trait" }, + { path = "url::Url::to_file_path", reason = "Use deno_path_util instead so it works in Wasm" }, + { path = "url::Url::from_file_path", reason = "Use deno_path_util instead so it works in Wasm" }, + { path = "url::Url::from_directory_path", reason = "Use deno_path_util instead so it works in Wasm" }, ] disallowed-types = [ { path = "std::sync::Arc", reason = "use crate::sync::MaybeArc instead" }, diff --git a/ext/node_resolver/env.rs b/resolvers/node/env.rs similarity index 100% rename from ext/node_resolver/env.rs rename to resolvers/node/env.rs diff --git a/ext/node_resolver/errors.rs b/resolvers/node/errors.rs similarity index 92% rename from ext/node_resolver/errors.rs rename to resolvers/node/errors.rs index 4ba829eda5..aacbecefb4 100644 --- a/ext/node_resolver/errors.rs +++ b/resolvers/node/errors.rs @@ -81,29 +81,6 @@ pub trait NodeJsErrorCoded { fn code(&self) -> NodeJsErrorCode; } -kinded_err!( - ResolvePkgSubpathFromDenoModuleError, - ResolvePkgSubpathFromDenoModuleErrorKind -); - -impl NodeJsErrorCoded for ResolvePkgSubpathFromDenoModuleError { - fn code(&self) -> NodeJsErrorCode { - use ResolvePkgSubpathFromDenoModuleErrorKind::*; - match self.as_kind() { - PackageSubpathResolve(e) => e.code(), - UrlToNodeResolution(e) => e.code(), - } - } -} - -#[derive(Debug, Error)] -pub enum ResolvePkgSubpathFromDenoModuleErrorKind { - #[error(transparent)] - PackageSubpathResolve(#[from] PackageSubpathResolveError), - #[error(transparent)] - UrlToNodeResolution(#[from] UrlToNodeResolutionError), -} - // todo(https://github.com/denoland/deno_core/issues/810): make this a TypeError #[derive(Debug, Clone, Error)] #[error( @@ -394,37 +371,6 @@ impl NodeJsErrorCoded for CanonicalizingPkgJsonDirError { } } -#[derive(Debug, Error)] -#[error("TypeScript files are not supported in npm packages: {specifier}")] -pub struct TypeScriptNotSupportedInNpmError { - pub specifier: Url, -} - -impl NodeJsErrorCoded for TypeScriptNotSupportedInNpmError { - fn code(&self) -> NodeJsErrorCode { - NodeJsErrorCode::ERR_UNKNOWN_FILE_EXTENSION - } -} - -kinded_err!(UrlToNodeResolutionError, UrlToNodeResolutionErrorKind); - -impl NodeJsErrorCoded for UrlToNodeResolutionError { - fn code(&self) -> NodeJsErrorCode { - match self.as_kind() { - UrlToNodeResolutionErrorKind::TypeScriptNotSupported(e) => e.code(), - UrlToNodeResolutionErrorKind::ClosestPkgJson(e) => e.code(), - } - } -} - -#[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( @@ -533,8 +479,6 @@ pub enum NodeResolveErrorKind { TypesNotFound(#[from] TypesNotFoundError), #[error(transparent)] FinalizeResolution(#[from] FinalizeResolutionError), - #[error(transparent)] - UrlToNodeResolution(#[from] UrlToNodeResolutionError), } kinded_err!(FinalizeResolutionError, FinalizeResolutionErrorKind); @@ -728,8 +672,6 @@ pub enum ResolvePkgJsonBinExportError { MissingPkgJson { pkg_json_path: PathBuf }, #[error("Failed resolving binary export. {message}")] InvalidBinProperty { message: String }, - #[error(transparent)] - UrlToNodeResolution(#[from] UrlToNodeResolutionError), } #[derive(Debug, Error)] diff --git a/ext/node_resolver/lib.rs b/resolvers/node/lib.rs similarity index 68% rename from ext/node_resolver/lib.rs rename to resolvers/node/lib.rs index f03f770486..87bd629946 100644 --- a/ext/node_resolver/lib.rs +++ b/resolvers/node/lib.rs @@ -13,12 +13,16 @@ mod resolution; mod sync; pub use deno_package_json::PackageJson; -pub use npm::NpmResolver; -pub use npm::NpmResolverRc; -pub use package_json::load_pkg_json; +pub use npm::InNpmPackageChecker; +pub use npm::InNpmPackageCheckerRc; +pub use npm::NpmPackageFolderResolver; +pub use npm::NpmPackageFolderResolverRc; +pub use package_json::PackageJsonResolver; +pub use package_json::PackageJsonResolverRc; pub use package_json::PackageJsonThreadLocalCache; pub use path::PathClean; pub use resolution::parse_npm_pkg_name; +pub use resolution::resolve_specifier_into_node_modules; pub use resolution::NodeModuleKind; pub use resolution::NodeResolution; pub use resolution::NodeResolutionMode; diff --git a/ext/node_resolver/npm.rs b/resolvers/node/npm.rs similarity index 54% rename from ext/node_resolver/npm.rs rename to resolvers/node/npm.rs index 77df57c489..ab3a179426 100644 --- a/ext/node_resolver/npm.rs +++ b/resolvers/node/npm.rs @@ -3,6 +3,8 @@ use std::path::Path; use std::path::PathBuf; +use deno_path_util::url_from_directory_path; +use deno_path_util::url_from_file_path; use url::Url; use crate::errors; @@ -11,20 +13,29 @@ use crate::sync::MaybeSend; use crate::sync::MaybeSync; #[allow(clippy::disallowed_types)] -pub type NpmResolverRc = crate::sync::MaybeArc; +pub type NpmPackageFolderResolverRc = + crate::sync::MaybeArc; -pub trait NpmResolver: std::fmt::Debug + MaybeSend + MaybeSync { - /// Resolves an npm package folder path from an npm package referrer. +pub trait NpmPackageFolderResolver: + std::fmt::Debug + MaybeSend + MaybeSync +{ + /// Resolves an npm package folder path from the specified referrer. fn resolve_package_folder_from_package( &self, specifier: &str, referrer: &Url, ) -> Result; +} +#[allow(clippy::disallowed_types)] +pub type InNpmPackageCheckerRc = crate::sync::MaybeArc; + +/// Checks if a provided specifier is in an npm package. +pub trait InNpmPackageChecker: std::fmt::Debug + MaybeSend + MaybeSync { fn in_npm_package(&self, specifier: &Url) -> bool; fn in_npm_package_at_dir_path(&self, path: &Path) -> bool { - let specifier = match Url::from_directory_path(path.to_path_buf().clean()) { + let specifier = match url_from_directory_path(&path.to_path_buf().clean()) { Ok(p) => p, Err(_) => return false, }; @@ -32,7 +43,7 @@ pub trait NpmResolver: std::fmt::Debug + MaybeSend + MaybeSync { } fn in_npm_package_at_file_path(&self, path: &Path) -> bool { - let specifier = match Url::from_file_path(path.to_path_buf().clean()) { + let specifier = match url_from_file_path(&path.to_path_buf().clean()) { Ok(p) => p, Err(_) => return false, }; diff --git a/resolvers/node/package_json.rs b/resolvers/node/package_json.rs new file mode 100644 index 0000000000..ae016ebe3e --- /dev/null +++ b/resolvers/node/package_json.rs @@ -0,0 +1,129 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_package_json::PackageJson; +use deno_package_json::PackageJsonRc; +use deno_path_util::strip_unc_prefix; +use std::cell::RefCell; +use std::collections::HashMap; +use std::io::ErrorKind; +use std::path::Path; +use std::path::PathBuf; +use url::Url; + +use crate::env::NodeResolverEnv; +use crate::errors::CanonicalizingPkgJsonDirError; +use crate::errors::ClosestPkgJsonError; +use crate::errors::PackageJsonLoadError; + +// it would be nice if this was passed down as a ctor arg to the package.json resolver, +// but it's a little bit complicated to do that, so we just maintain a thread local cache +thread_local! { + static CACHE: RefCell> = RefCell::new(HashMap::new()); +} + +pub struct PackageJsonThreadLocalCache; + +impl PackageJsonThreadLocalCache { + pub fn clear() { + CACHE.with(|cache| cache.borrow_mut().clear()); + } +} + +impl deno_package_json::PackageJsonCache for PackageJsonThreadLocalCache { + fn get(&self, path: &Path) -> Option { + CACHE.with(|cache| cache.borrow().get(path).cloned()) + } + + fn set(&self, path: PathBuf, package_json: PackageJsonRc) { + CACHE.with(|cache| cache.borrow_mut().insert(path, package_json)); + } +} + +#[allow(clippy::disallowed_types)] +pub type PackageJsonResolverRc = + crate::sync::MaybeArc>; + +#[derive(Debug)] +pub struct PackageJsonResolver { + env: TEnv, +} + +impl PackageJsonResolver { + pub fn new(env: TEnv) -> Self { + Self { env } + } + + pub fn get_closest_package_json( + &self, + url: &Url, + ) -> Result, ClosestPkgJsonError> { + let Ok(file_path) = deno_path_util::url_to_file_path(url) else { + return Ok(None); + }; + self.get_closest_package_json_from_path(&file_path) + } + + pub fn get_closest_package_json_from_path( + &self, + file_path: &Path, + ) -> Result, ClosestPkgJsonError> { + // we use this for deno compile using byonm because the script paths + // won't be in virtual file system, but the package.json paths will be + fn canonicalize_first_ancestor_exists( + dir_path: &Path, + env: &TEnv, + ) -> Result, std::io::Error> { + for ancestor in dir_path.ancestors() { + match env.realpath_sync(ancestor) { + Ok(dir_path) => return Ok(Some(dir_path)), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // keep searching + } + Err(err) => return Err(err), + } + } + Ok(None) + } + + let parent_dir = file_path.parent().unwrap(); + let Some(start_dir) = canonicalize_first_ancestor_exists( + parent_dir, &self.env, + ) + .map_err(|source| CanonicalizingPkgJsonDirError { + dir_path: parent_dir.to_path_buf(), + source, + })? + else { + return Ok(None); + }; + let start_dir = strip_unc_prefix(start_dir); + for current_dir in start_dir.ancestors() { + let package_json_path = current_dir.join("package.json"); + if let Some(pkg_json) = self.load_package_json(&package_json_path)? { + return Ok(Some(pkg_json)); + } + } + + Ok(None) + } + + pub fn load_package_json( + &self, + path: &Path, + ) -> Result, PackageJsonLoadError> { + let result = PackageJson::load_from_path( + path, + self.env.pkg_json_fs(), + Some(&PackageJsonThreadLocalCache), + ); + match result { + Ok(pkg_json) => Ok(Some(pkg_json)), + Err(deno_package_json::PackageJsonLoadError::Io { source, .. }) + if source.kind() == ErrorKind::NotFound => + { + Ok(None) + } + Err(err) => Err(PackageJsonLoadError(err)), + } + } +} diff --git a/resolvers/node/path.rs b/resolvers/node/path.rs new file mode 100644 index 0000000000..8c2d35fadf --- /dev/null +++ b/resolvers/node/path.rs @@ -0,0 +1,81 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; + +/// Extension to path_clean::PathClean +pub trait PathClean { + fn clean(&self) -> T; +} + +impl PathClean for PathBuf { + fn clean(&self) -> PathBuf { + fn is_clean_path(path: &Path) -> bool { + let path = path.to_string_lossy(); + let mut current_index = 0; + while let Some(index) = path[current_index..].find("\\.") { + let trailing_index = index + current_index + 2; + let mut trailing_chars = path[trailing_index..].chars(); + match trailing_chars.next() { + Some('.') => match trailing_chars.next() { + Some('/') | Some('\\') | None => { + return false; + } + _ => {} + }, + Some('/') | Some('\\') => { + return false; + } + _ => {} + } + current_index = trailing_index; + } + true + } + + let path = path_clean::PathClean::clean(self); + if cfg!(windows) && !is_clean_path(&path) { + // temporary workaround because path_clean::PathClean::clean is + // not good enough on windows + let mut components = Vec::new(); + + for component in path.components() { + match component { + Component::CurDir => { + // skip + } + Component::ParentDir => { + let maybe_last_component = components.pop(); + if !matches!(maybe_last_component, Some(Component::Normal(_))) { + panic!("Error normalizing: {}", path.display()); + } + } + Component::Normal(_) | Component::RootDir | Component::Prefix(_) => { + components.push(component); + } + } + } + components.into_iter().collect::() + } else { + path + } + } +} + +#[cfg(test)] +mod test { + #[cfg(windows)] + #[test] + fn test_path_clean() { + use super::*; + + run_test("C:\\test\\./file.txt", "C:\\test\\file.txt"); + run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt"); + run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt"); + + fn run_test(input: &str, expected: &str) { + assert_eq!(PathBuf::from(input).clean(), PathBuf::from(expected)); + } + } +} diff --git a/ext/node_resolver/resolution.rs b/resolvers/node/resolution.rs similarity index 88% rename from ext/node_resolver/resolution.rs rename to resolvers/node/resolution.rs index ad9dbb7100..673a61abe6 100644 --- a/ext/node_resolver/resolution.rs +++ b/resolvers/node/resolution.rs @@ -6,16 +6,13 @@ use std::path::PathBuf; use anyhow::bail; use anyhow::Error as AnyError; -use deno_media_type::MediaType; -use deno_package_json::PackageJsonRc; +use deno_path_util::url_from_file_path; use serde_json::Map; use serde_json::Value; use url::Url; use crate::env::NodeResolverEnv; use crate::errors; -use crate::errors::CanonicalizingPkgJsonDirError; -use crate::errors::ClosestPkgJsonError; use crate::errors::DataUrlReferrerError; use crate::errors::FinalizeResolutionError; use crate::errors::InvalidModuleSpecifierError; @@ -30,7 +27,6 @@ use crate::errors::PackageExportsResolveError; use crate::errors::PackageImportNotDefinedError; use crate::errors::PackageImportsResolveError; use crate::errors::PackageImportsResolveErrorKind; -use crate::errors::PackageJsonLoadError; use crate::errors::PackagePathNotExportedError; use crate::errors::PackageResolveError; use crate::errors::PackageSubpathResolveError; @@ -40,16 +36,13 @@ use crate::errors::PackageTargetResolveError; use crate::errors::PackageTargetResolveErrorKind; use crate::errors::ResolveBinaryCommandsError; use crate::errors::ResolvePkgJsonBinExportError; -use crate::errors::ResolvePkgSubpathFromDenoModuleError; -use crate::errors::TypeScriptNotSupportedInNpmError; use crate::errors::TypesNotFoundError; use crate::errors::TypesNotFoundErrorData; use crate::errors::UnsupportedDirImportError; use crate::errors::UnsupportedEsmUrlSchemeError; -use crate::errors::UrlToNodeResolutionError; -use crate::path::strip_unc_prefix; -use crate::path::to_file_specifier; -use crate::NpmResolverRc; +use crate::npm::InNpmPackageCheckerRc; +use crate::NpmPackageFolderResolverRc; +use crate::PackageJsonResolverRc; use crate::PathClean; use deno_package_json::PackageJson; @@ -57,6 +50,15 @@ pub static DEFAULT_CONDITIONS: &[&str] = &["deno", "node", "import"]; pub static REQUIRE_CONDITIONS: &[&str] = &["require", "node"]; static TYPES_ONLY_CONDITIONS: &[&str] = &["types"]; +fn conditions_from_module_kind( + kind: NodeModuleKind, +) -> &'static [&'static str] { + match kind { + NodeModuleKind::Esm => DEFAULT_CONDITIONS, + NodeModuleKind::Cjs => REQUIRE_CONDITIONS, + } +} + pub type NodeModuleKind = deno_package_json::NodeModuleKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -73,16 +75,14 @@ impl NodeResolutionMode { #[derive(Debug)] pub enum NodeResolution { - Esm(Url), - CommonJs(Url), + Module(Url), BuiltIn(String), } impl NodeResolution { pub fn into_url(self) -> Url { match self { - Self::Esm(u) => u, - Self::CommonJs(u) => u, + Self::Module(u) => u, Self::BuiltIn(specifier) => { if specifier.starts_with("node:") { Url::parse(&specifier).unwrap() @@ -92,42 +92,6 @@ impl NodeResolution { } } } - - pub fn into_specifier_and_media_type( - resolution: Option, - ) -> (Url, MediaType) { - match resolution { - Some(NodeResolution::CommonJs(specifier)) => { - let media_type = MediaType::from_specifier(&specifier); - ( - specifier, - match media_type { - MediaType::JavaScript | MediaType::Jsx => MediaType::Cjs, - MediaType::TypeScript | MediaType::Tsx => MediaType::Cts, - MediaType::Dts => MediaType::Dcts, - _ => media_type, - }, - ) - } - Some(NodeResolution::Esm(specifier)) => { - let media_type = MediaType::from_specifier(&specifier); - ( - specifier, - match media_type { - MediaType::JavaScript | MediaType::Jsx => MediaType::Mjs, - MediaType::TypeScript | MediaType::Tsx => MediaType::Mts, - MediaType::Dts => MediaType::Dmts, - _ => media_type, - }, - ) - } - Some(resolution) => (resolution.into_url(), MediaType::Dts), - None => ( - Url::parse("internal:///missing_dependency.d.ts").unwrap(), - MediaType::Dts, - ), - } - } } #[allow(clippy::disallowed_types)] @@ -136,16 +100,28 @@ pub type NodeResolverRc = crate::sync::MaybeArc>; #[derive(Debug)] pub struct NodeResolver { env: TEnv, - npm_resolver: NpmResolverRc, + in_npm_pkg_checker: InNpmPackageCheckerRc, + npm_pkg_folder_resolver: NpmPackageFolderResolverRc, + pkg_json_resolver: PackageJsonResolverRc, } impl NodeResolver { - pub fn new(env: TEnv, npm_resolver: NpmResolverRc) -> Self { - Self { env, npm_resolver } + pub fn new( + env: TEnv, + in_npm_pkg_checker: InNpmPackageCheckerRc, + npm_pkg_folder_resolver: NpmPackageFolderResolverRc, + pkg_json_resolver: PackageJsonResolverRc, + ) -> Self { + Self { + env, + in_npm_pkg_checker, + npm_pkg_folder_resolver, + pkg_json_resolver, + } } pub fn in_npm_package(&self, specifier: &Url) -> bool { - self.npm_resolver.in_npm_package(specifier) + self.in_npm_pkg_checker.in_npm_package(specifier) } /// This function is an implementation of `defaultResolve` in @@ -166,7 +142,7 @@ impl NodeResolver { if let Ok(url) = Url::parse(specifier) { if url.scheme() == "data" { - return Ok(NodeResolution::Esm(url)); + return Ok(NodeResolution::Module(url)); } if let Some(module_name) = @@ -191,7 +167,7 @@ impl NodeResolver { let url = referrer .join(specifier) .map_err(|source| DataUrlReferrerError { source })?; - return Ok(NodeResolution::Esm(url)); + return Ok(NodeResolution::Module(url)); } } @@ -199,8 +175,7 @@ impl NodeResolver { specifier, referrer, referrer_kind, - // even though the referrer may be CJS, if we're here that means we're doing ESM resolution - DEFAULT_CONDITIONS, + conditions_from_module_kind(referrer_kind), mode, )?; @@ -212,7 +187,7 @@ impl NodeResolver { }; let url = self.finalize_resolution(url, Some(referrer))?; - let resolve_response = self.url_to_node_resolution(url)?; + let resolve_response = NodeResolution::Module(url); // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. Ok(resolve_response) @@ -236,6 +211,7 @@ impl NodeResolver { })?) } else if specifier.starts_with('#') { let pkg_config = self + .pkg_json_resolver .get_closest_package_json(referrer) .map_err(PackageImportsResolveErrorKind::ClosestPkgJson) .map_err(|err| PackageImportsResolveError(Box::new(err)))?; @@ -331,9 +307,9 @@ impl NodeResolver { package_dir: &Path, package_subpath: Option<&str>, maybe_referrer: Option<&Url>, + referrer_kind: NodeModuleKind, mode: NodeResolutionMode, - ) -> Result { - let node_module_kind = NodeModuleKind::Esm; + ) -> Result { let package_subpath = package_subpath .map(|s| format!("./{s}")) .unwrap_or_else(|| ".".to_string()); @@ -341,14 +317,13 @@ impl NodeResolver { package_dir, &package_subpath, maybe_referrer, - node_module_kind, - DEFAULT_CONDITIONS, + referrer_kind, + conditions_from_module_kind(referrer_kind), mode, )?; - let resolve_response = self.url_to_node_resolution(resolved_url)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. - Ok(resolve_response) + Ok(resolved_url) } pub fn resolve_binary_commands( @@ -356,7 +331,9 @@ impl NodeResolver { 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 { + let Some(package_json) = + self.pkg_json_resolver.load_package_json(&pkg_json_path)? + else { return Ok(Vec::new()); }; @@ -381,9 +358,11 @@ impl NodeResolver { &self, package_folder: &Path, sub_path: Option<&str>, - ) -> Result { + ) -> Result { let pkg_json_path = package_folder.join("package.json"); - let Some(package_json) = self.load_package_json(&pkg_json_path)? else { + let Some(package_json) = + self.pkg_json_resolver.load_package_json(&pkg_json_path)? + else { return Err(ResolvePkgJsonBinExportError::MissingPkgJson { pkg_json_path, }); @@ -394,39 +373,11 @@ impl NodeResolver { message: err.to_string(), } })?; - let url = to_file_specifier(&package_folder.join(bin_entry)); + let url = url_from_file_path(&package_folder.join(bin_entry)).unwrap(); - let resolve_response = self.url_to_node_resolution(url)?; // TODO(bartlomieju): skipped checking errors for commonJS resolution and // "preserveSymlinksMain"/"preserveSymlinks" options. - Ok(resolve_response) - } - - pub fn url_to_node_resolution( - &self, - url: Url, - ) -> 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)?; - match maybe_package_config { - Some(c) if c.typ == "module" => Ok(NodeResolution::Esm(url)), - Some(_) => Ok(NodeResolution::CommonJs(url)), - None => Ok(NodeResolution::Esm(url)), - } - } else if url_str.ends_with(".mjs") || url_str.ends_with(".d.mts") { - Ok(NodeResolution::Esm(url)) - } else if url_str.ends_with(".ts") || url_str.ends_with(".mts") { - if self.in_npm_package(&url) { - Err(TypeScriptNotSupportedInNpmError { specifier: url }.into()) - } else { - Ok(NodeResolution::Esm(url)) - } - } else { - Ok(NodeResolution::CommonJs(url)) - } + Ok(url) } /// Checks if the resolved file has a corresponding declaration file. @@ -485,12 +436,12 @@ impl NodeResolver { || lowercase_path.ends_with(".d.cts") || lowercase_path.ends_with(".d.mts") { - return Ok(to_file_specifier(path)); + return Ok(url_from_file_path(path).unwrap()); } if let Some(path) = probe_extensions(&self.env, path, &lowercase_path, referrer_kind) { - return Ok(to_file_specifier(&path)); + return Ok(url_from_file_path(&path).unwrap()); } if self.env.is_dir_sync(path) { let resolution_result = self.resolve_package_dir_subpath( @@ -498,10 +449,7 @@ impl NodeResolver { /* sub path */ ".", maybe_referrer, referrer_kind, - match referrer_kind { - NodeModuleKind::Esm => DEFAULT_CONDITIONS, - NodeModuleKind::Cjs => REQUIRE_CONDITIONS, - }, + conditions_from_module_kind(referrer_kind), NodeResolutionMode::Types, ); if let Ok(resolution) = resolution_result { @@ -514,15 +462,15 @@ impl NodeResolver { &index_path.to_string_lossy().to_lowercase(), referrer_kind, ) { - return Ok(to_file_specifier(&path)); + return Ok(url_from_file_path(&path).unwrap()); } } // allow resolving .css files for types resolution if lowercase_path.ends_with(".css") { - return Ok(to_file_specifier(path)); + return Ok(url_from_file_path(path).unwrap()); } Err(TypesNotFoundError(Box::new(TypesNotFoundErrorData { - code_specifier: to_file_specifier(path), + code_specifier: url_from_file_path(path).unwrap(), maybe_referrer: maybe_referrer.cloned(), }))) } @@ -673,7 +621,8 @@ impl NodeResolver { } else { format!("{target}{subpath}") }; - let package_json_url = to_file_specifier(package_json_path); + let package_json_url = + url_from_file_path(package_json_path).unwrap(); let result = match self.package_resolve( &export_target, &package_json_url, @@ -760,7 +709,7 @@ impl NodeResolver { ); } if subpath.is_empty() { - return Ok(to_file_specifier(&resolved_path)); + return Ok(url_from_file_path(&resolved_path).unwrap()); } if invalid_segment_re.is_match(subpath) { let request = if pattern { @@ -782,9 +731,11 @@ impl NodeResolver { let resolved_path_str = resolved_path.to_string_lossy(); let replaced = pattern_re .replace(&resolved_path_str, |_caps: ®ex::Captures| subpath); - return Ok(to_file_specifier(&PathBuf::from(replaced.to_string()))); + return Ok( + url_from_file_path(&PathBuf::from(replaced.to_string())).unwrap(), + ); } - Ok(to_file_specifier(&resolved_path.join(subpath).clean())) + Ok(url_from_file_path(&resolved_path.join(subpath).clean()).unwrap()) } #[allow(clippy::too_many_arguments)] @@ -871,7 +822,7 @@ impl NodeResolver { mode, )?; if mode.is_types() && url.scheme() == "file" { - let path = url.to_file_path().unwrap(); + let path = deno_path_util::url_to_file_path(&url).unwrap(); return Ok(Some(self.path_to_declaration_url( &path, maybe_referrer, @@ -1098,7 +1049,9 @@ impl NodeResolver { let (package_name, package_subpath, _is_scoped) = parse_npm_pkg_name(specifier, referrer)?; - if let Some(package_config) = self.get_closest_package_json(referrer)? { + if let Some(package_config) = + self.pkg_json_resolver.get_closest_package_json(referrer)? + { // ResolveSelf if package_config.name.as_ref() == Some(&package_name) { if let Some(exports) = &package_config.exports { @@ -1173,7 +1126,7 @@ impl NodeResolver { mode: NodeResolutionMode, ) -> Result { let package_dir_path = self - .npm_resolver + .npm_pkg_folder_resolver .resolve_package_folder_from_package(package_name, referrer)?; // todo: error with this instead when can't find package @@ -1213,7 +1166,10 @@ impl NodeResolver { mode: NodeResolutionMode, ) -> Result { let package_json_path = package_dir_path.join("package.json"); - match self.load_package_json(&package_json_path)? { + match self + .pkg_json_resolver + .load_package_json(&package_json_path)? + { Some(pkg_json) => self.resolve_package_subpath( &pkg_json, package_subpath, @@ -1307,7 +1263,7 @@ impl NodeResolver { if mode.is_types() { Ok(self.path_to_declaration_url(&file_path, referrer, referrer_kind)?) } else { - Ok(to_file_specifier(&file_path)) + Ok(url_from_file_path(&file_path).unwrap()) } } @@ -1334,70 +1290,6 @@ impl NodeResolver { } } - pub fn get_closest_package_json( - &self, - url: &Url, - ) -> Result, ClosestPkgJsonError> { - let Ok(file_path) = url.to_file_path() else { - return Ok(None); - }; - self.get_closest_package_json_from_path(&file_path) - } - - pub fn get_closest_package_json_from_path( - &self, - file_path: &Path, - ) -> Result, ClosestPkgJsonError> { - // we use this for deno compile using byonm because the script paths - // won't be in virtual file system, but the package.json paths will be - fn canonicalize_first_ancestor_exists( - dir_path: &Path, - env: &dyn NodeResolverEnv, - ) -> Result, std::io::Error> { - for ancestor in dir_path.ancestors() { - match env.realpath_sync(ancestor) { - Ok(dir_path) => return Ok(Some(dir_path)), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - // keep searching - } - Err(err) => return Err(err), - } - } - Ok(None) - } - - let parent_dir = file_path.parent().unwrap(); - let Some(start_dir) = canonicalize_first_ancestor_exists( - parent_dir, &self.env, - ) - .map_err(|source| CanonicalizingPkgJsonDirError { - dir_path: parent_dir.to_path_buf(), - source, - })? - else { - return Ok(None); - }; - let start_dir = strip_unc_prefix(start_dir); - for current_dir in start_dir.ancestors() { - let package_json_path = current_dir.join("package.json"); - if let Some(pkg_json) = self.load_package_json(&package_json_path)? { - return Ok(Some(pkg_json)); - } - } - - Ok(None) - } - - pub fn load_package_json( - &self, - package_json_path: &Path, - ) -> Result, PackageJsonLoadError> { - crate::package_json::load_pkg_json( - self.env.pkg_json_fs(), - package_json_path, - ) - } - pub(super) fn legacy_main_resolve( &self, package_json: &PackageJson, @@ -1433,7 +1325,7 @@ impl NodeResolver { if let Some(main) = maybe_main { let guess = package_json.path.parent().unwrap().join(main).clean(); if self.env.is_file_sync(&guess) { - return Ok(to_file_specifier(&guess)); + return Ok(url_from_file_path(&guess).unwrap()); } // todo(dsherret): investigate exactly how node and typescript handles this @@ -1463,7 +1355,7 @@ impl NodeResolver { .clean(); if self.env.is_file_sync(&guess) { // TODO(bartlomieju): emitLegacyIndexDeprecation() - return Ok(to_file_specifier(&guess)); + return Ok(url_from_file_path(&guess).unwrap()); } } } @@ -1496,14 +1388,15 @@ impl NodeResolver { let guess = directory.join(index_file_name).clean(); if self.env.is_file_sync(&guess) { // TODO(bartlomieju): emitLegacyIndexDeprecation() - return Ok(to_file_specifier(&guess)); + return Ok(url_from_file_path(&guess).unwrap()); } } if mode.is_types() { Err( TypesNotFoundError(Box::new(TypesNotFoundErrorData { - code_specifier: to_file_specifier(&directory.join("index.js")), + code_specifier: url_from_file_path(&directory.join("index.js")) + .unwrap(), maybe_referrer: maybe_referrer.cloned(), })) .into(), @@ -1511,7 +1404,7 @@ impl NodeResolver { } else { Err( ModuleNotFoundError { - specifier: to_file_specifier(&directory.join("index.js")), + specifier: url_from_file_path(&directory.join("index.js")).unwrap(), typ: "module", maybe_referrer: maybe_referrer.cloned(), } @@ -1519,6 +1412,25 @@ impl NodeResolver { ) } } + + /// Resolves a specifier that is pointing into a node_modules folder by canonicalizing it. + /// + /// Returns `None` when the specifier is not in a node_modules folder. + pub fn handle_if_in_node_modules(&self, specifier: &Url) -> Option { + // 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. + let specifier = resolve_specifier_into_node_modules(specifier, &|path| { + self.env.realpath_sync(path) + }); + return Some(specifier); + } + + None + } } fn resolve_bin_entry_value<'a>( @@ -1611,9 +1523,7 @@ fn resolve_bin_entry_value<'a>( } fn to_file_path(url: &Url) -> PathBuf { - url - .to_file_path() - .unwrap_or_else(|_| panic!("Provided URL was not file:// URL: {url}")) + deno_path_util::url_to_file_path(url).unwrap() } fn to_file_path_string(url: &Url) -> String { @@ -1692,7 +1602,7 @@ fn with_known_extension(path: &Path, ext: &str) -> PathBuf { } fn to_specifier_display_string(url: &Url) -> String { - if let Ok(path) = url.to_file_path() { + if let Ok(path) = deno_path_util::url_to_file_path(url) { path.display().to_string() } else { url.to_string() @@ -1769,6 +1679,28 @@ pub fn parse_npm_pkg_name( Ok((package_name, package_subpath, is_scoped)) } +/// Resolves a specifier that is pointing into a node_modules folder. +/// +/// Note: This should be called whenever getting the specifier from +/// a Module::External(module) reference because that module might +/// not be fully resolved at the time deno_graph is analyzing it +/// because the node_modules folder might not exist at that time. +pub fn resolve_specifier_into_node_modules( + specifier: &Url, + canonicalize: &impl Fn(&Path) -> std::io::Result, +) -> Url { + deno_path_util::url_to_file_path(specifier) + .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| { + deno_path_util::canonicalize_path_maybe_not_exists(&path, canonicalize) + .ok() + }) + .and_then(|path| deno_path_util::url_from_file_path(&path).ok()) + .unwrap_or_else(|| specifier.clone()) +} + fn pattern_key_compare(a: &str, b: &str) -> i32 { let a_pattern_index = a.find('*'); let b_pattern_index = b.find('*'); diff --git a/ext/node_resolver/sync.rs b/resolvers/node/sync.rs similarity index 100% rename from ext/node_resolver/sync.rs rename to resolvers/node/sync.rs diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index ea2978c49b..b59cd14fa9 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_runtime" -version = "0.177.0" +version = "0.186.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -88,6 +88,7 @@ deno_kv.workspace = true deno_napi.workspace = true deno_net.workspace = true deno_node.workspace = true +deno_path_util.workspace = true deno_permissions.workspace = true deno_terminal.workspace = true deno_tls.workspace = true @@ -99,6 +100,8 @@ deno_websocket.workspace = true deno_webstorage.workspace = true node_resolver = { workspace = true, features = ["sync"] } +async-trait.workspace = true +color-print.workspace = true dlopen2.workspace = true encoding_rs.workspace = true fastwebsockets.workspace = true @@ -112,13 +115,21 @@ log.workspace = true netif = "0.1.6" notify.workspace = true once_cell.workspace = true +opentelemetry.workspace = true +opentelemetry-http.workspace = true +opentelemetry-otlp.workspace = true +opentelemetry-semantic-conventions.workspace = true +opentelemetry_sdk.workspace = true percent-encoding.workspace = true +pin-project.workspace = true regex.workspace = true rustyline = { workspace = true, features = ["custom-bindings"] } +same-file = "1.0.6" serde.workspace = true signal-hook = "0.3.17" signal-hook-registry = "1.4.0" tempfile.workspace = true +thiserror.workspace = true tokio.workspace = true tokio-metrics.workspace = true twox-hash.workspace = true diff --git a/runtime/clippy.toml b/runtime/clippy.toml index 53676a90e6..79e6bbd083 100644 --- a/runtime/clippy.toml +++ b/runtime/clippy.toml @@ -42,4 +42,5 @@ disallowed-methods = [ { path = "std::fs::write", reason = "File system operations should be done using FileSystem trait" }, { path = "std::path::Path::canonicalize", reason = "File system operations should be done using FileSystem trait" }, { path = "std::path::Path::exists", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::process::exit", reason = "use deno_runtime::exit instead" }, ] diff --git a/runtime/errors.rs b/runtime/errors.rs index 694402773e..0c26e0e47a 100644 --- a/runtime/errors.rs +++ b/runtime/errors.rs @@ -9,15 +9,116 @@ //! Diagnostics are compile-time type errors, whereas JsErrors are runtime //! exceptions. +use crate::ops::fs_events::FsEventsError; +use crate::ops::http::HttpStartError; +use crate::ops::os::OsError; +use crate::ops::permissions::PermissionError; +use crate::ops::process::CheckRunPermissionError; +use crate::ops::process::ProcessError; +use crate::ops::signal::SignalError; +use crate::ops::tty::TtyError; +use crate::ops::web_worker::SyncFetchError; +use crate::ops::worker_host::CreateWorkerError; +use deno_broadcast_channel::BroadcastChannelError; +use deno_cache::CacheError; +use deno_canvas::CanvasError; use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::url; use deno_core::ModuleResolutionError; +use deno_cron::CronError; +use deno_crypto::DecryptError; +use deno_crypto::EncryptError; +use deno_crypto::ExportKeyError; +use deno_crypto::GenerateKeyError; +use deno_crypto::ImportKeyError; +use deno_fetch::FetchError; +use deno_fetch::HttpClientCreateError; +use deno_ffi::CallError; +use deno_ffi::CallbackError; +use deno_ffi::DlfcnError; +use deno_ffi::IRError; +use deno_ffi::ReprError; +use deno_ffi::StaticError; +use deno_fs::FsOpsError; +use deno_http::HttpError; +use deno_http::HttpNextError; +use deno_http::WebSocketUpgradeError; +use deno_io::fs::FsError; +use deno_kv::KvCheckError; +use deno_kv::KvError; +use deno_kv::KvMutationError; +use deno_napi::NApiError; +use deno_net::ops::NetError; +use deno_permissions::ChildPermissionError; +use deno_permissions::NetDescriptorFromUrlParseError; +use deno_permissions::PathResolveError; +use deno_permissions::PermissionCheckError; +use deno_permissions::RunDescriptorParseError; +use deno_permissions::SysDescriptorParseError; +use deno_tls::TlsError; +use deno_web::BlobError; +use deno_web::CompressionError; +use deno_web::MessagePortError; +use deno_web::StreamResourceError; +use deno_web::WebError; +use deno_websocket::HandshakeError; +use deno_websocket::WebsocketError; +use deno_webstorage::WebStorageError; +use rustyline::error::ReadlineError; use std::env; use std::error::Error; use std::io; use std::sync::Arc; +fn get_run_descriptor_parse_error(e: &RunDescriptorParseError) -> &'static str { + match e { + RunDescriptorParseError::Which(_) => "Error", + RunDescriptorParseError::PathResolve(e) => get_path_resolve_error(e), + RunDescriptorParseError::EmptyRunQuery => "Error", + } +} + +fn get_sys_descriptor_parse_error(e: &SysDescriptorParseError) -> &'static str { + match e { + SysDescriptorParseError::InvalidKind(_) => "TypeError", + SysDescriptorParseError::Empty => "Error", + } +} + +fn get_path_resolve_error(e: &PathResolveError) -> &'static str { + match e { + PathResolveError::CwdResolve(e) => get_io_error_class(e), + PathResolveError::EmptyPath => "Error", + } +} + +fn get_permission_error_class(e: &PermissionError) -> &'static str { + match e { + PermissionError::InvalidPermissionName(_) => "ReferenceError", + PermissionError::PathResolve(e) => get_path_resolve_error(e), + PermissionError::NetDescriptorParse(_) => "URIError", + PermissionError::SysDescriptorParse(e) => get_sys_descriptor_parse_error(e), + PermissionError::RunDescriptorParse(e) => get_run_descriptor_parse_error(e), + } +} + +fn get_permission_check_error_class(e: &PermissionCheckError) -> &'static str { + match e { + PermissionCheckError::PermissionDenied(_) => "NotCapable", + PermissionCheckError::InvalidFilePath(_) => "URIError", + PermissionCheckError::NetDescriptorForUrlParse(e) => match e { + NetDescriptorFromUrlParseError::MissingHost(_) => "TypeError", + NetDescriptorFromUrlParseError::Host(_) => "URIError", + }, + PermissionCheckError::SysDescriptorParse(e) => { + get_sys_descriptor_parse_error(e) + } + PermissionCheckError::PathResolve(e) => get_path_resolve_error(e), + PermissionCheckError::HostParse(_) => "URIError", + } +} + fn get_dlopen_error_class(error: &dlopen2::Error) -> &'static str { use dlopen2::Error::*; match error { @@ -153,12 +254,1624 @@ pub fn get_nix_error_class(error: &nix::Error) -> &'static str { } } +fn get_webgpu_error_class(e: &deno_webgpu::InitError) -> &'static str { + match e { + deno_webgpu::InitError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + deno_webgpu::InitError::InvalidAdapter(_) => "Error", + deno_webgpu::InitError::RequestDevice(_) => "DOMExceptionOperationError", + deno_webgpu::InitError::InvalidDevice(_) => "Error", + } +} + +fn get_webgpu_buffer_error_class( + e: &deno_webgpu::buffer::BufferError, +) -> &'static str { + match e { + deno_webgpu::buffer::BufferError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + deno_webgpu::buffer::BufferError::InvalidUsage => "TypeError", + deno_webgpu::buffer::BufferError::Access(_) => "DOMExceptionOperationError", + } +} + +fn get_webgpu_bundle_error_class( + e: &deno_webgpu::bundle::BundleError, +) -> &'static str { + match e { + deno_webgpu::bundle::BundleError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + deno_webgpu::bundle::BundleError::InvalidSize => "TypeError", + } +} + +fn get_webgpu_byow_error_class( + e: &deno_webgpu::byow::ByowError, +) -> &'static str { + match e { + deno_webgpu::byow::ByowError::WebGPUNotInitiated => "TypeError", + deno_webgpu::byow::ByowError::InvalidParameters => "TypeError", + deno_webgpu::byow::ByowError::CreateSurface(_) => "Error", + deno_webgpu::byow::ByowError::InvalidSystem => "TypeError", + #[cfg(any( + target_os = "windows", + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd" + ))] + deno_webgpu::byow::ByowError::NullWindow => "TypeError", + #[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd" + ))] + deno_webgpu::byow::ByowError::NullDisplay => "TypeError", + #[cfg(target_os = "macos")] + deno_webgpu::byow::ByowError::NSViewDisplay => "TypeError", + } +} + +fn get_webgpu_render_pass_error_class( + e: &deno_webgpu::render_pass::RenderPassError, +) -> &'static str { + match e { + deno_webgpu::render_pass::RenderPassError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + deno_webgpu::render_pass::RenderPassError::InvalidSize => "TypeError", + } +} + +fn get_webgpu_surface_error_class( + e: &deno_webgpu::surface::SurfaceError, +) -> &'static str { + match e { + deno_webgpu::surface::SurfaceError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + deno_webgpu::surface::SurfaceError::Surface(_) => "Error", + deno_webgpu::surface::SurfaceError::InvalidStatus => "Error", + } +} + +fn get_crypto_decrypt_error_class(e: &DecryptError) -> &'static str { + match e { + DecryptError::General(e) => get_crypto_shared_error_class(e), + DecryptError::Pkcs1(_) => "Error", + DecryptError::Failed => "DOMExceptionOperationError", + DecryptError::InvalidLength => "TypeError", + DecryptError::InvalidCounterLength => "TypeError", + DecryptError::InvalidTagLength => "TypeError", + DecryptError::InvalidKeyOrIv => "DOMExceptionOperationError", + DecryptError::TooMuchData => "DOMExceptionOperationError", + DecryptError::InvalidIvLength => "TypeError", + DecryptError::Rsa(_) => "DOMExceptionOperationError", + } +} + +fn get_crypto_encrypt_error_class(e: &EncryptError) -> &'static str { + match e { + EncryptError::General(e) => get_crypto_shared_error_class(e), + EncryptError::InvalidKeyOrIv => "DOMExceptionOperationError", + EncryptError::Failed => "DOMExceptionOperationError", + EncryptError::InvalidLength => "TypeError", + EncryptError::InvalidIvLength => "TypeError", + EncryptError::InvalidCounterLength => "TypeError", + EncryptError::TooMuchData => "DOMExceptionOperationError", + } +} + +fn get_crypto_shared_error_class(e: &deno_crypto::SharedError) -> &'static str { + match e { + deno_crypto::SharedError::ExpectedValidPrivateKey => "TypeError", + deno_crypto::SharedError::ExpectedValidPublicKey => "TypeError", + deno_crypto::SharedError::ExpectedValidPrivateECKey => "TypeError", + deno_crypto::SharedError::ExpectedValidPublicECKey => "TypeError", + deno_crypto::SharedError::ExpectedPrivateKey => "TypeError", + deno_crypto::SharedError::ExpectedPublicKey => "TypeError", + deno_crypto::SharedError::ExpectedSecretKey => "TypeError", + deno_crypto::SharedError::FailedDecodePrivateKey => { + "DOMExceptionOperationError" + } + deno_crypto::SharedError::FailedDecodePublicKey => { + "DOMExceptionOperationError" + } + deno_crypto::SharedError::UnsupportedFormat => { + "DOMExceptionNotSupportedError" + } + } +} + +fn get_crypto_ed25519_error_class( + e: &deno_crypto::Ed25519Error, +) -> &'static str { + match e { + deno_crypto::Ed25519Error::FailedExport => "DOMExceptionOperationError", + deno_crypto::Ed25519Error::Der(_) => "Error", + deno_crypto::Ed25519Error::KeyRejected(_) => "Error", + } +} + +fn get_crypto_export_key_error_class(e: &ExportKeyError) -> &'static str { + match e { + ExportKeyError::General(e) => get_crypto_shared_error_class(e), + ExportKeyError::Der(_) => "Error", + ExportKeyError::UnsupportedNamedCurve => "DOMExceptionNotSupportedError", + } +} + +fn get_crypto_generate_key_error_class(e: &GenerateKeyError) -> &'static str { + match e { + GenerateKeyError::General(e) => get_crypto_shared_error_class(e), + GenerateKeyError::BadPublicExponent => "DOMExceptionOperationError", + GenerateKeyError::InvalidHMACKeyLength => "DOMExceptionOperationError", + GenerateKeyError::FailedRSAKeySerialization => "DOMExceptionOperationError", + GenerateKeyError::InvalidAESKeyLength => "DOMExceptionOperationError", + GenerateKeyError::FailedRSAKeyGeneration => "DOMExceptionOperationError", + GenerateKeyError::FailedECKeyGeneration => "DOMExceptionOperationError", + GenerateKeyError::FailedKeyGeneration => "DOMExceptionOperationError", + } +} + +fn get_crypto_import_key_error_class(e: &ImportKeyError) -> &'static str { + match e { + ImportKeyError::General(e) => get_crypto_shared_error_class(e), + ImportKeyError::InvalidModulus => "DOMExceptionDataError", + ImportKeyError::InvalidPublicExponent => "DOMExceptionDataError", + ImportKeyError::InvalidPrivateExponent => "DOMExceptionDataError", + ImportKeyError::InvalidFirstPrimeFactor => "DOMExceptionDataError", + ImportKeyError::InvalidSecondPrimeFactor => "DOMExceptionDataError", + ImportKeyError::InvalidFirstCRTExponent => "DOMExceptionDataError", + ImportKeyError::InvalidSecondCRTExponent => "DOMExceptionDataError", + ImportKeyError::InvalidCRTCoefficient => "DOMExceptionDataError", + ImportKeyError::InvalidB64Coordinate => "DOMExceptionDataError", + ImportKeyError::InvalidRSAPublicKey => "DOMExceptionDataError", + ImportKeyError::InvalidRSAPrivateKey => "DOMExceptionDataError", + ImportKeyError::UnsupportedAlgorithm => "DOMExceptionDataError", + ImportKeyError::PublicKeyTooLong => "DOMExceptionDataError", + ImportKeyError::PrivateKeyTooLong => "DOMExceptionDataError", + ImportKeyError::InvalidP256ECPoint => "DOMExceptionDataError", + ImportKeyError::InvalidP384ECPoint => "DOMExceptionDataError", + ImportKeyError::InvalidP521ECPoint => "DOMExceptionDataError", + ImportKeyError::UnsupportedNamedCurve => "DOMExceptionDataError", + ImportKeyError::CurveMismatch => "DOMExceptionDataError", + ImportKeyError::InvalidKeyData => "DOMExceptionDataError", + ImportKeyError::InvalidJWKPrivateKey => "DOMExceptionDataError", + ImportKeyError::EllipticCurve(_) => "DOMExceptionDataError", + ImportKeyError::ExpectedValidPkcs8Data => "DOMExceptionDataError", + ImportKeyError::MalformedParameters => "DOMExceptionDataError", + ImportKeyError::Spki(_) => "DOMExceptionDataError", + ImportKeyError::InvalidP256ECSPKIData => "DOMExceptionDataError", + ImportKeyError::InvalidP384ECSPKIData => "DOMExceptionDataError", + ImportKeyError::InvalidP521ECSPKIData => "DOMExceptionDataError", + ImportKeyError::Der(_) => "DOMExceptionDataError", + } +} + +fn get_crypto_x448_error_class(e: &deno_crypto::X448Error) -> &'static str { + match e { + deno_crypto::X448Error::FailedExport => "DOMExceptionOperationError", + deno_crypto::X448Error::Der(_) => "Error", + } +} + +fn get_crypto_x25519_error_class(e: &deno_crypto::X25519Error) -> &'static str { + match e { + deno_crypto::X25519Error::FailedExport => "DOMExceptionOperationError", + deno_crypto::X25519Error::Der(_) => "Error", + } +} + +fn get_crypto_error_class(e: &deno_crypto::Error) -> &'static str { + match e { + deno_crypto::Error::Der(_) => "Error", + deno_crypto::Error::JoinError(_) => "Error", + deno_crypto::Error::MissingArgumentHash => "TypeError", + deno_crypto::Error::MissingArgumentSaltLength => "TypeError", + deno_crypto::Error::Other(e) => get_error_class_name(e).unwrap_or("Error"), + deno_crypto::Error::UnsupportedAlgorithm => "TypeError", + deno_crypto::Error::KeyRejected(_) => "Error", + deno_crypto::Error::RSA(_) => "Error", + deno_crypto::Error::Pkcs1(_) => "Error", + deno_crypto::Error::Unspecified(_) => "Error", + deno_crypto::Error::InvalidKeyFormat => "TypeError", + deno_crypto::Error::MissingArgumentPublicKey => "TypeError", + deno_crypto::Error::P256Ecdsa(_) => "Error", + deno_crypto::Error::DecodePrivateKey => "TypeError", + deno_crypto::Error::MissingArgumentNamedCurve => "TypeError", + deno_crypto::Error::MissingArgumentInfo => "TypeError", + deno_crypto::Error::HKDFLengthTooLarge => "DOMExceptionOperationError", + deno_crypto::Error::General(e) => get_crypto_shared_error_class(e), + deno_crypto::Error::Base64Decode(_) => "Error", + deno_crypto::Error::DataInvalidSize => "TypeError", + deno_crypto::Error::InvalidKeyLength => "TypeError", + deno_crypto::Error::EncryptionError => "DOMExceptionOperationError", + deno_crypto::Error::DecryptionError => "DOMExceptionOperationError", + deno_crypto::Error::ArrayBufferViewLengthExceeded(_) => { + "DOMExceptionQuotaExceededError" + } + } +} + +fn get_napi_error_class(e: &NApiError) -> &'static str { + match e { + NApiError::InvalidPath + | NApiError::LibLoading(_) + | NApiError::ModuleNotFound(_) => "TypeError", + NApiError::Permission(e) => get_permission_check_error_class(e), + } +} + +fn get_web_error_class(e: &WebError) -> &'static str { + match e { + WebError::Base64Decode => "DOMExceptionInvalidCharacterError", + WebError::InvalidEncodingLabel(_) => "RangeError", + WebError::BufferTooLong => "TypeError", + WebError::ValueTooLarge => "RangeError", + WebError::BufferTooSmall => "RangeError", + WebError::DataInvalid => "TypeError", + WebError::DataError(_) => "Error", + } +} + +fn get_web_compression_error_class(e: &CompressionError) -> &'static str { + match e { + CompressionError::UnsupportedFormat => "TypeError", + CompressionError::ResourceClosed => "TypeError", + CompressionError::IoTypeError(_) => "TypeError", + CompressionError::Io(e) => get_io_error_class(e), + } +} + +fn get_web_message_port_error_class(e: &MessagePortError) -> &'static str { + match e { + MessagePortError::InvalidTransfer => "TypeError", + MessagePortError::NotReady => "TypeError", + MessagePortError::TransferSelf => "TypeError", + MessagePortError::Canceled(e) => { + let io_err: io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + MessagePortError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + } +} + +fn get_web_stream_resource_error_class( + e: &StreamResourceError, +) -> &'static str { + match e { + StreamResourceError::Canceled(e) => { + let io_err: io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + StreamResourceError::Js(_) => "TypeError", + } +} + +fn get_web_blob_error_class(e: &BlobError) -> &'static str { + match e { + BlobError::BlobPartNotFound => "TypeError", + BlobError::SizeLargerThanBlobPart => "TypeError", + BlobError::BlobURLsNotSupported => "TypeError", + BlobError::Url(_) => "Error", + } +} + +fn get_ffi_repr_error_class(e: &ReprError) -> &'static str { + match e { + ReprError::InvalidOffset => "TypeError", + ReprError::InvalidArrayBuffer => "TypeError", + ReprError::DestinationLengthTooShort => "RangeError", + ReprError::InvalidCString => "TypeError", + ReprError::CStringTooLong => "TypeError", + ReprError::InvalidBool => "TypeError", + ReprError::InvalidU8 => "TypeError", + ReprError::InvalidI8 => "TypeError", + ReprError::InvalidU16 => "TypeError", + ReprError::InvalidI16 => "TypeError", + ReprError::InvalidU32 => "TypeError", + ReprError::InvalidI32 => "TypeError", + ReprError::InvalidU64 => "TypeError", + ReprError::InvalidI64 => "TypeError", + ReprError::InvalidF32 => "TypeError", + ReprError::InvalidF64 => "TypeError", + ReprError::InvalidPointer => "TypeError", + ReprError::Permission(e) => get_permission_check_error_class(e), + } +} + +fn get_ffi_dlfcn_error_class(e: &DlfcnError) -> &'static str { + match e { + DlfcnError::RegisterSymbol { .. } => "Error", + DlfcnError::Dlopen(_) => "Error", + DlfcnError::Permission(e) => get_permission_check_error_class(e), + DlfcnError::Other(e) => get_error_class_name(e).unwrap_or("Error"), + } +} + +fn get_ffi_static_error_class(e: &StaticError) -> &'static str { + match e { + StaticError::Dlfcn(e) => get_ffi_dlfcn_error_class(e), + StaticError::InvalidTypeVoid => "TypeError", + StaticError::InvalidTypeStruct => "TypeError", + StaticError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + } +} + +fn get_ffi_callback_error_class(e: &CallbackError) -> &'static str { + match e { + CallbackError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + CallbackError::Other(e) => get_error_class_name(e).unwrap_or("Error"), + CallbackError::Permission(e) => get_permission_check_error_class(e), + } +} + +fn get_ffi_call_error_class(e: &CallError) -> &'static str { + match e { + CallError::IR(_) => "TypeError", + CallError::NonblockingCallFailure(_) => "Error", + CallError::InvalidSymbol(_) => "TypeError", + CallError::Permission(e) => get_permission_check_error_class(e), + CallError::Callback(e) => get_ffi_callback_error_class(e), + CallError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + } +} + +fn get_webstorage_class_name(e: &WebStorageError) -> &'static str { + match e { + WebStorageError::ContextNotSupported => "DOMExceptionNotSupportedError", + WebStorageError::Sqlite(_) => "Error", + WebStorageError::Io(e) => get_io_error_class(e), + WebStorageError::StorageExceeded => "DOMExceptionQuotaExceededError", + } +} + +fn get_tls_error_class(e: &TlsError) -> &'static str { + match e { + TlsError::Rustls(_) => "Error", + TlsError::UnableAddPemFileToCert(e) => get_io_error_class(e), + TlsError::CertInvalid + | TlsError::CertsNotFound + | TlsError::KeysNotFound + | TlsError::KeyDecode => "InvalidData", + } +} + +pub fn get_cron_error_class(e: &CronError) -> &'static str { + match e { + CronError::Resource(e) => { + deno_core::error::get_custom_error_class(e).unwrap_or("Error") + } + CronError::NameExceeded(_) => "TypeError", + CronError::NameInvalid => "TypeError", + CronError::AlreadyExists => "TypeError", + CronError::TooManyCrons => "TypeError", + CronError::InvalidCron => "TypeError", + CronError::InvalidBackoff => "TypeError", + CronError::AcquireError(_) => "Error", + CronError::Other(e) => get_error_class_name(e).unwrap_or("Error"), + } +} + +fn get_canvas_error(e: &CanvasError) -> &'static str { + match e { + CanvasError::UnsupportedColorType(_) => "TypeError", + CanvasError::Image(_) => "Error", + } +} + +pub fn get_cache_error(error: &CacheError) -> &'static str { + match error { + CacheError::Sqlite(_) => "Error", + CacheError::JoinError(_) => "Error", + CacheError::Resource(err) => { + deno_core::error::get_custom_error_class(err).unwrap_or("Error") + } + CacheError::Other(e) => get_error_class_name(e).unwrap_or("Error"), + CacheError::Io(err) => get_io_error_class(err), + } +} + +fn get_broadcast_channel_error(error: &BroadcastChannelError) -> &'static str { + match error { + BroadcastChannelError::Resource(err) => { + deno_core::error::get_custom_error_class(err).unwrap() + } + BroadcastChannelError::MPSCSendError(_) => "Error", + BroadcastChannelError::BroadcastSendError(_) => "Error", + BroadcastChannelError::Other(err) => { + get_error_class_name(err).unwrap_or("Error") + } + } +} + +fn get_fetch_error(error: &FetchError) -> &'static str { + match error { + FetchError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + FetchError::Permission(e) => get_permission_check_error_class(e), + FetchError::NetworkError => "TypeError", + FetchError::FsNotGet(_) => "TypeError", + FetchError::InvalidUrl(_) => "TypeError", + FetchError::InvalidHeaderName(_) => "TypeError", + FetchError::InvalidHeaderValue(_) => "TypeError", + FetchError::DataUrl(_) => "TypeError", + FetchError::Base64(_) => "TypeError", + FetchError::BlobNotFound => "TypeError", + FetchError::SchemeNotSupported(_) => "TypeError", + FetchError::RequestCanceled => "TypeError", + FetchError::Http(_) => "Error", + FetchError::ClientCreate(e) => get_http_client_create_error(e), + FetchError::Url(e) => get_url_parse_error_class(e), + FetchError::Method(_) => "TypeError", + FetchError::ClientSend(_) => "TypeError", + FetchError::RequestBuilderHook(_) => "TypeError", + FetchError::Io(e) => get_io_error_class(e), + FetchError::Hyper(e) => get_hyper_error_class(e), + } +} + +fn get_http_client_create_error(error: &HttpClientCreateError) -> &'static str { + match error { + HttpClientCreateError::Tls(_) => "TypeError", + HttpClientCreateError::InvalidUserAgent(_) => "TypeError", + HttpClientCreateError::InvalidProxyUrl => "TypeError", + HttpClientCreateError::HttpVersionSelectionInvalid => "TypeError", + HttpClientCreateError::RootCertStore(_) => "TypeError", + } +} + +fn get_websocket_error(error: &WebsocketError) -> &'static str { + match error { + WebsocketError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + WebsocketError::Permission(e) => get_permission_check_error_class(e), + WebsocketError::Url(e) => get_url_parse_error_class(e), + WebsocketError::Io(e) => get_io_error_class(e), + WebsocketError::WebSocket(_) => "TypeError", + WebsocketError::ConnectionFailed(_) => "DOMExceptionNetworkError", + WebsocketError::Uri(_) => "Error", + WebsocketError::Canceled(e) => { + let io_err: io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + } +} + +fn get_websocket_handshake_error(error: &HandshakeError) -> &'static str { + match error { + HandshakeError::RootStoreError(e) => { + get_error_class_name(e).unwrap_or("Error") + } + HandshakeError::Tls(e) => get_tls_error_class(e), + HandshakeError::MissingPath => "TypeError", + HandshakeError::Http(_) => "Error", + HandshakeError::InvalidHostname(_) => "TypeError", + HandshakeError::Io(e) => get_io_error_class(e), + HandshakeError::Rustls(_) => "Error", + HandshakeError::H2(_) => "Error", + HandshakeError::NoH2Alpn => "Error", + HandshakeError::InvalidStatusCode(_) => "Error", + HandshakeError::WebSocket(_) => "TypeError", + HandshakeError::HeaderName(_) => "TypeError", + HandshakeError::HeaderValue(_) => "TypeError", + } +} + +fn get_fs_ops_error(error: &FsOpsError) -> &'static str { + match error { + FsOpsError::Io(e) => get_io_error_class(e), + FsOpsError::OperationError(e) => get_fs_error(&e.err), + FsOpsError::Permission(e) => get_permission_check_error_class(e), + FsOpsError::Resource(e) | FsOpsError::Other(e) => { + get_error_class_name(e).unwrap_or("Error") + } + FsOpsError::InvalidUtf8(_) => "InvalidData", + FsOpsError::StripPrefix(_) => "Error", + FsOpsError::Canceled(e) => { + let io_err: io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + FsOpsError::InvalidSeekMode(_) => "TypeError", + FsOpsError::InvalidControlCharacter(_) => "Error", + FsOpsError::InvalidCharacter(_) => "Error", + #[cfg(windows)] + FsOpsError::InvalidTrailingCharacter => "Error", + FsOpsError::NotCapableAccess { .. } => "NotCapable", + FsOpsError::NotCapable(_) => "NotCapable", + } +} + +fn get_kv_error(error: &KvError) -> &'static str { + match error { + KvError::DatabaseHandler(e) | KvError::Resource(e) | KvError::Kv(e) => { + get_error_class_name(e).unwrap_or("Error") + } + KvError::TooManyRanges(_) => "TypeError", + KvError::TooManyEntries(_) => "TypeError", + KvError::TooManyChecks(_) => "TypeError", + KvError::TooManyMutations(_) => "TypeError", + KvError::TooManyKeys(_) => "TypeError", + KvError::InvalidLimit => "TypeError", + KvError::InvalidBoundaryKey => "TypeError", + KvError::KeyTooLargeToRead(_) => "TypeError", + KvError::KeyTooLargeToWrite(_) => "TypeError", + KvError::TotalMutationTooLarge(_) => "TypeError", + KvError::TotalKeyTooLarge(_) => "TypeError", + KvError::Io(e) => get_io_error_class(e), + KvError::QueueMessageNotFound => "TypeError", + KvError::StartKeyNotInKeyspace => "TypeError", + KvError::EndKeyNotInKeyspace => "TypeError", + KvError::StartKeyGreaterThanEndKey => "TypeError", + KvError::InvalidCheck(e) => match e { + KvCheckError::InvalidVersionstamp => "TypeError", + KvCheckError::Io(e) => get_io_error_class(e), + }, + KvError::InvalidMutation(e) => match e { + KvMutationError::BigInt(_) => "Error", + KvMutationError::Io(e) => get_io_error_class(e), + KvMutationError::InvalidMutationWithValue(_) => "TypeError", + KvMutationError::InvalidMutationWithoutValue(_) => "TypeError", + }, + KvError::InvalidEnqueue(e) => get_io_error_class(e), + KvError::EmptyKey => "TypeError", + KvError::ValueTooLarge(_) => "TypeError", + KvError::EnqueuePayloadTooLarge(_) => "TypeError", + KvError::InvalidCursor => "TypeError", + KvError::CursorOutOfBounds => "TypeError", + KvError::InvalidRange => "TypeError", + } +} + +fn get_net_error(error: &NetError) -> &'static str { + match error { + NetError::ListenerClosed => "BadResource", + NetError::ListenerBusy => "Busy", + NetError::SocketClosed => "BadResource", + NetError::SocketClosedNotConnected => "NotConnected", + NetError::SocketBusy => "Busy", + NetError::Io(e) => get_io_error_class(e), + NetError::AcceptTaskOngoing => "Busy", + NetError::RootCertStore(e) | NetError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + NetError::Permission(e) => get_permission_check_error_class(e), + NetError::NoResolvedAddress => "Error", + NetError::AddrParse(_) => "Error", + NetError::Map(e) => get_net_map_error(e), + NetError::Canceled(e) => { + let io_err: io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + NetError::DnsNotFound(_) => "NotFound", + NetError::DnsNotConnected(_) => "NotConnected", + NetError::DnsTimedOut(_) => "TimedOut", + NetError::Dns(_) => "Error", + NetError::UnsupportedRecordType => "NotSupported", + NetError::InvalidUtf8(_) => "InvalidData", + NetError::UnexpectedKeyType => "Error", + NetError::InvalidHostname(_) => "TypeError", + NetError::TcpStreamBusy => "Busy", + NetError::Rustls(_) => "Error", + NetError::Tls(e) => get_tls_error_class(e), + NetError::ListenTlsRequiresKey => "InvalidData", + NetError::Reunite(_) => "Error", + } +} + +fn get_net_map_error(error: &deno_net::io::MapError) -> &'static str { + match error { + deno_net::io::MapError::Io(e) => get_io_error_class(e), + deno_net::io::MapError::NoResources => "Error", + } +} + +fn get_child_permission_error(e: &ChildPermissionError) -> &'static str { + match e { + ChildPermissionError::Escalation => "NotCapable", + ChildPermissionError::PathResolve(e) => get_path_resolve_error(e), + ChildPermissionError::NetDescriptorParse(_) => "URIError", + ChildPermissionError::EnvDescriptorParse(_) => "Error", + ChildPermissionError::SysDescriptorParse(e) => { + get_sys_descriptor_parse_error(e) + } + ChildPermissionError::RunDescriptorParse(e) => { + get_run_descriptor_parse_error(e) + } + } +} + +fn get_create_worker_error(error: &CreateWorkerError) -> &'static str { + match error { + CreateWorkerError::ClassicWorkers => "DOMExceptionNotSupportedError", + CreateWorkerError::Permission(e) => get_child_permission_error(e), + CreateWorkerError::ModuleResolution(e) => { + get_module_resolution_error_class(e) + } + CreateWorkerError::Io(e) => get_io_error_class(e), + CreateWorkerError::MessagePort(e) => get_web_message_port_error_class(e), + } +} + +fn get_tty_error(error: &TtyError) -> &'static str { + match error { + TtyError::Resource(e) | TtyError::Other(e) => { + get_error_class_name(e).unwrap_or("Error") + } + TtyError::Io(e) => get_io_error_class(e), + #[cfg(unix)] + TtyError::Nix(e) => get_nix_error_class(e), + } +} + +fn get_readline_error(error: &ReadlineError) -> &'static str { + match error { + ReadlineError::Io(e) => get_io_error_class(e), + ReadlineError::Eof => "Error", + ReadlineError::Interrupted => "Error", + #[cfg(unix)] + ReadlineError::Errno(e) => get_nix_error_class(e), + ReadlineError::WindowResized => "Error", + #[cfg(windows)] + ReadlineError::Decode(_) => "Error", + #[cfg(windows)] + ReadlineError::SystemError(_) => "Error", + _ => "Error", + } +} + +fn get_signal_error(error: &SignalError) -> &'static str { + match error { + SignalError::InvalidSignalStr(_) => "TypeError", + SignalError::InvalidSignalInt(_) => "TypeError", + SignalError::SignalNotAllowed(_) => "TypeError", + SignalError::Io(e) => get_io_error_class(e), + } +} + +fn get_fs_events_error(error: &FsEventsError) -> &'static str { + match error { + FsEventsError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + FsEventsError::Permission(e) => get_permission_check_error_class(e), + FsEventsError::Notify(e) => get_notify_error_class(e), + FsEventsError::Canceled(e) => { + let io_err: io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + } +} + +fn get_http_start_error(error: &HttpStartError) -> &'static str { + match error { + HttpStartError::TcpStreamInUse => "Busy", + HttpStartError::TlsStreamInUse => "Busy", + HttpStartError::UnixSocketInUse => "Busy", + HttpStartError::ReuniteTcp(_) => "Error", + #[cfg(unix)] + HttpStartError::ReuniteUnix(_) => "Error", + HttpStartError::Io(e) => get_io_error_class(e), + HttpStartError::Other(e) => get_error_class_name(e).unwrap_or("Error"), + } +} + +fn get_process_error(error: &ProcessError) -> &'static str { + match error { + ProcessError::SpawnFailed { error, .. } => get_process_error(error), + ProcessError::FailedResolvingCwd(e) | ProcessError::Io(e) => { + get_io_error_class(e) + } + ProcessError::Permission(e) => get_permission_check_error_class(e), + ProcessError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + ProcessError::BorrowMut(_) => "Error", + ProcessError::Which(_) => "Error", + ProcessError::ChildProcessAlreadyTerminated => "TypeError", + ProcessError::Signal(e) => get_signal_error(e), + ProcessError::MissingCmd => "Error", + ProcessError::InvalidPid => "TypeError", + #[cfg(unix)] + ProcessError::Nix(e) => get_nix_error_class(e), + ProcessError::RunPermission(e) => match e { + CheckRunPermissionError::Permission(e) => { + get_permission_check_error_class(e) + } + CheckRunPermissionError::Other(e) => { + get_error_class_name(e).unwrap_or("Error") + } + }, + } +} + +fn get_http_error(error: &HttpError) -> &'static str { + match error { + HttpError::Canceled(e) => { + let io_err: io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + HttpError::HyperV014(e) => get_hyper_v014_error_class(e), + HttpError::InvalidHeaderName(_) => "Error", + HttpError::InvalidHeaderValue(_) => "Error", + HttpError::Http(_) => "Error", + HttpError::ResponseHeadersAlreadySent => "Http", + HttpError::ConnectionClosedWhileSendingResponse => "Http", + HttpError::AlreadyInUse => "Http", + HttpError::Io(e) => get_io_error_class(e), + HttpError::NoResponseHeaders => "Http", + HttpError::ResponseAlreadyCompleted => "Http", + HttpError::UpgradeBodyUsed => "Http", + HttpError::Resource(e) | HttpError::Other(e) => { + get_error_class_name(e).unwrap_or("Error") + } + } +} + +fn get_http_next_error(error: &HttpNextError) -> &'static str { + match error { + HttpNextError::Io(e) => get_io_error_class(e), + HttpNextError::WebSocketUpgrade(e) => get_websocket_upgrade_error(e), + HttpNextError::Hyper(e) => get_hyper_error_class(e), + HttpNextError::JoinError(_) => "Error", + HttpNextError::Canceled(e) => { + let io_err: io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + HttpNextError::UpgradeUnavailable(_) => "Error", + HttpNextError::HttpPropertyExtractor(e) | HttpNextError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + } +} + +fn get_websocket_upgrade_error(error: &WebSocketUpgradeError) -> &'static str { + match error { + WebSocketUpgradeError::InvalidHeaders => "Http", + WebSocketUpgradeError::HttpParse(_) => "Error", + WebSocketUpgradeError::Http(_) => "Error", + WebSocketUpgradeError::Utf8(_) => "Error", + WebSocketUpgradeError::InvalidHeaderName(_) => "Error", + WebSocketUpgradeError::InvalidHeaderValue(_) => "Error", + WebSocketUpgradeError::InvalidHttpStatusLine => "Http", + WebSocketUpgradeError::UpgradeBufferAlreadyCompleted => "Http", + } +} + +fn get_fs_error(e: &FsError) -> &'static str { + match &e { + FsError::Io(e) => get_io_error_class(e), + FsError::FileBusy => "Busy", + FsError::NotSupported => "NotSupported", + FsError::NotCapable(_) => "NotCapable", + } +} + +mod node { + use super::get_error_class_name; + use super::get_io_error_class; + use super::get_permission_check_error_class; + use super::get_serde_json_error_class; + use super::get_url_parse_error_class; + pub use deno_node::ops::blocklist::BlocklistError; + pub use deno_node::ops::crypto::cipher::CipherContextError; + pub use deno_node::ops::crypto::cipher::CipherError; + pub use deno_node::ops::crypto::cipher::DecipherContextError; + pub use deno_node::ops::crypto::cipher::DecipherError; + pub use deno_node::ops::crypto::digest::HashError; + pub use deno_node::ops::crypto::keys::AsymmetricPrivateKeyDerError; + pub use deno_node::ops::crypto::keys::AsymmetricPrivateKeyError; + pub use deno_node::ops::crypto::keys::AsymmetricPublicKeyDerError; + pub use deno_node::ops::crypto::keys::AsymmetricPublicKeyError; + pub use deno_node::ops::crypto::keys::AsymmetricPublicKeyJwkError; + pub use deno_node::ops::crypto::keys::EcJwkError; + pub use deno_node::ops::crypto::keys::EdRawError; + pub use deno_node::ops::crypto::keys::ExportPrivateKeyPemError; + pub use deno_node::ops::crypto::keys::ExportPublicKeyPemError; + pub use deno_node::ops::crypto::keys::GenerateRsaPssError; + pub use deno_node::ops::crypto::keys::RsaJwkError; + pub use deno_node::ops::crypto::keys::RsaPssParamsParseError; + pub use deno_node::ops::crypto::keys::X509PublicKeyError; + pub use deno_node::ops::crypto::sign::KeyObjectHandlePrehashedSignAndVerifyError; + pub use deno_node::ops::crypto::x509::X509Error; + pub use deno_node::ops::crypto::DiffieHellmanError; + pub use deno_node::ops::crypto::EcdhEncodePubKey; + pub use deno_node::ops::crypto::HkdfError; + pub use deno_node::ops::crypto::Pbkdf2Error; + pub use deno_node::ops::crypto::PrivateEncryptDecryptError; + pub use deno_node::ops::crypto::ScryptAsyncError; + pub use deno_node::ops::crypto::SignEd25519Error; + pub use deno_node::ops::crypto::VerifyEd25519Error; + pub use deno_node::ops::fs::FsError; + pub use deno_node::ops::http2::Http2Error; + pub use deno_node::ops::idna::IdnaError; + pub use deno_node::ops::ipc::IpcError; + pub use deno_node::ops::ipc::IpcJsonStreamError; + use deno_node::ops::os::priority::PriorityError; + pub use deno_node::ops::os::OsError; + pub use deno_node::ops::require::RequireError; + pub use deno_node::ops::worker_threads::WorkerThreadsFilenameError; + pub use deno_node::ops::zlib::brotli::BrotliError; + pub use deno_node::ops::zlib::mode::ModeError; + pub use deno_node::ops::zlib::ZlibError; + + pub fn get_blocklist_error(error: &BlocklistError) -> &'static str { + match error { + BlocklistError::AddrParse(_) => "Error", + BlocklistError::IpNetwork(_) => "Error", + BlocklistError::InvalidAddress => "Error", + BlocklistError::IpVersionMismatch => "Error", + } + } + + pub fn get_fs_error(error: &FsError) -> &'static str { + match error { + FsError::Permission(e) => get_permission_check_error_class(e), + FsError::Io(e) => get_io_error_class(e), + #[cfg(windows)] + FsError::PathHasNoRoot => "Error", + #[cfg(not(any(unix, windows)))] + FsError::UnsupportedPlatform => "Error", + FsError::Fs(e) => super::get_fs_error(e), + } + } + + pub fn get_idna_error(error: &IdnaError) -> &'static str { + match error { + IdnaError::InvalidInput => "RangeError", + IdnaError::InputTooLong => "Error", + IdnaError::IllegalInput => "RangeError", + } + } + + pub fn get_ipc_json_stream_error(error: &IpcJsonStreamError) -> &'static str { + match error { + IpcJsonStreamError::Io(e) => get_io_error_class(e), + IpcJsonStreamError::SimdJson(_) => "Error", + } + } + + pub fn get_ipc_error(error: &IpcError) -> &'static str { + match error { + IpcError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + IpcError::IpcJsonStream(e) => get_ipc_json_stream_error(e), + IpcError::Canceled(e) => { + let io_err: std::io::Error = e.to_owned().into(); + get_io_error_class(&io_err) + } + IpcError::SerdeJson(e) => get_serde_json_error_class(e), + } + } + + pub fn get_worker_threads_filename_error( + error: &WorkerThreadsFilenameError, + ) -> &'static str { + match error { + WorkerThreadsFilenameError::Permission(e) => { + get_error_class_name(e).unwrap_or("Error") + } + WorkerThreadsFilenameError::UrlParse(e) => get_url_parse_error_class(e), + WorkerThreadsFilenameError::InvalidRelativeUrl => "Error", + WorkerThreadsFilenameError::UrlFromPathString => "Error", + WorkerThreadsFilenameError::UrlToPathString => "Error", + WorkerThreadsFilenameError::UrlToPath => "Error", + WorkerThreadsFilenameError::FileNotFound(_) => "Error", + WorkerThreadsFilenameError::Fs(e) => super::get_fs_error(e), + } + } + + pub fn get_require_error(error: &RequireError) -> &'static str { + match error { + RequireError::UrlParse(e) => get_url_parse_error_class(e), + RequireError::Permission(e) => get_error_class_name(e).unwrap_or("Error"), + RequireError::PackageExportsResolve(_) + | RequireError::PackageJsonLoad(_) + | RequireError::ClosestPkgJson(_) + | RequireError::FilePathConversion(_) + | RequireError::UrlConversion(_) + | RequireError::ReadModule(_) + | RequireError::PackageImportsResolve(_) => "Error", + RequireError::Fs(e) | RequireError::UnableToGetCwd(e) => { + super::get_fs_error(e) + } + } + } + + pub fn get_http2_error(error: &Http2Error) -> &'static str { + match error { + Http2Error::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + Http2Error::UrlParse(e) => get_url_parse_error_class(e), + Http2Error::H2(_) => "Error", + } + } + + pub fn get_os_error(error: &OsError) -> &'static str { + match error { + OsError::Priority(e) => match e { + PriorityError::Io(e) => get_io_error_class(e), + #[cfg(windows)] + PriorityError::InvalidPriority => "TypeError", + }, + OsError::Permission(e) => get_permission_check_error_class(e), + OsError::FailedToGetCpuInfo => "TypeError", + OsError::FailedToGetUserInfo(e) => get_io_error_class(e), + } + } + + pub fn get_brotli_error(error: &BrotliError) -> &'static str { + match error { + BrotliError::InvalidEncoderMode => "TypeError", + BrotliError::CompressFailed => "TypeError", + BrotliError::DecompressFailed => "TypeError", + BrotliError::Join(_) => "Error", + BrotliError::Resource(e) => get_error_class_name(e).unwrap_or("Error"), + BrotliError::Io(e) => get_io_error_class(e), + } + } + + pub fn get_mode_error(_: &ModeError) -> &'static str { + "Error" + } + + pub fn get_zlib_error(e: &ZlibError) -> &'static str { + match e { + ZlibError::NotInitialized => "TypeError", + ZlibError::Mode(e) => get_mode_error(e), + ZlibError::Other(e) => get_error_class_name(e).unwrap_or("Error"), + } + } + + pub fn get_crypto_cipher_context_error( + e: &CipherContextError, + ) -> &'static str { + match e { + CipherContextError::ContextInUse => "TypeError", + CipherContextError::Cipher(e) => get_crypto_cipher_error(e), + CipherContextError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + } + } + + pub fn get_crypto_cipher_error(e: &CipherError) -> &'static str { + match e { + CipherError::InvalidIvLength => "TypeError", + CipherError::InvalidKeyLength => "RangeError", + CipherError::InvalidInitializationVector => "TypeError", + CipherError::CannotPadInputData => "TypeError", + CipherError::UnknownCipher(_) => "TypeError", + } + } + + pub fn get_crypto_decipher_context_error( + e: &DecipherContextError, + ) -> &'static str { + match e { + DecipherContextError::ContextInUse => "TypeError", + DecipherContextError::Decipher(e) => get_crypto_decipher_error(e), + DecipherContextError::Resource(e) => { + get_error_class_name(e).unwrap_or("Error") + } + } + } + + pub fn get_crypto_decipher_error(e: &DecipherError) -> &'static str { + match e { + DecipherError::InvalidIvLength => "TypeError", + DecipherError::InvalidKeyLength => "RangeError", + DecipherError::InvalidInitializationVector => "TypeError", + DecipherError::CannotUnpadInputData => "TypeError", + DecipherError::DataAuthenticationFailed => "TypeError", + DecipherError::SetAutoPaddingFalseAes128GcmUnsupported => "TypeError", + DecipherError::SetAutoPaddingFalseAes256GcmUnsupported => "TypeError", + DecipherError::UnknownCipher(_) => "TypeError", + } + } + + pub fn get_x509_error(_: &X509Error) -> &'static str { + "Error" + } + + pub fn get_crypto_key_object_handle_prehashed_sign_and_verify_error( + e: &KeyObjectHandlePrehashedSignAndVerifyError, + ) -> &'static str { + match e { + KeyObjectHandlePrehashedSignAndVerifyError::InvalidDsaSignatureEncoding => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::KeyIsNotPrivate => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::DigestNotAllowedForRsaSignature(_) => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigestWithRsa => "Error", + KeyObjectHandlePrehashedSignAndVerifyError::DigestNotAllowedForRsaPssSignature(_) => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigestWithRsaPss => "Error", + KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigestWithDsa => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::RsaPssHashAlgorithmUnsupported => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::PrivateKeyDisallowsUsage { .. } => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigest => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::X25519KeyCannotBeUsedForSigning => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::Ed25519KeyCannotBeUsedForPrehashedSigning => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::DhKeyCannotBeUsedForSigning => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::KeyIsNotPublicOrPrivate => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::InvalidDsaSignature => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::X25519KeyCannotBeUsedForVerification => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::Ed25519KeyCannotBeUsedForPrehashedVerification => "TypeError", + KeyObjectHandlePrehashedSignAndVerifyError::DhKeyCannotBeUsedForVerification => "TypeError", + } + } + + pub fn get_crypto_hash_error(_: &HashError) -> &'static str { + "Error" + } + + pub fn get_asymmetric_public_key_jwk_error( + e: &AsymmetricPublicKeyJwkError, + ) -> &'static str { + match e { + AsymmetricPublicKeyJwkError::UnsupportedJwkEcCurveP224 => "TypeError", + AsymmetricPublicKeyJwkError::JwkExportNotImplementedForKeyType => { + "TypeError" + } + AsymmetricPublicKeyJwkError::KeyIsNotAsymmetricPublicKey => "TypeError", + } + } + + pub fn get_generate_rsa_pss_error(_: &GenerateRsaPssError) -> &'static str { + "TypeError" + } + + pub fn get_asymmetric_private_key_der_error( + e: &AsymmetricPrivateKeyDerError, + ) -> &'static str { + match e { + AsymmetricPrivateKeyDerError::KeyIsNotAsymmetricPrivateKey => "TypeError", + AsymmetricPrivateKeyDerError::InvalidRsaPrivateKey => "TypeError", + AsymmetricPrivateKeyDerError::ExportingNonRsaPrivateKeyAsPkcs1Unsupported => "TypeError", + AsymmetricPrivateKeyDerError::InvalidEcPrivateKey => "TypeError", + AsymmetricPrivateKeyDerError::ExportingNonEcPrivateKeyAsSec1Unsupported => "TypeError", + AsymmetricPrivateKeyDerError::ExportingNonRsaPssPrivateKeyAsPkcs8Unsupported => "Error", + AsymmetricPrivateKeyDerError::InvalidDsaPrivateKey => "TypeError", + AsymmetricPrivateKeyDerError::InvalidX25519PrivateKey => "TypeError", + AsymmetricPrivateKeyDerError::InvalidEd25519PrivateKey => "TypeError", + AsymmetricPrivateKeyDerError::InvalidDhPrivateKey => "TypeError", + AsymmetricPrivateKeyDerError::UnsupportedKeyType(_) => "TypeError", + } + } + + pub fn get_asymmetric_public_key_der_error( + _: &AsymmetricPublicKeyDerError, + ) -> &'static str { + "TypeError" + } + + pub fn get_export_public_key_pem_error( + e: &ExportPublicKeyPemError, + ) -> &'static str { + match e { + ExportPublicKeyPemError::AsymmetricPublicKeyDer(e) => { + get_asymmetric_public_key_der_error(e) + } + ExportPublicKeyPemError::VeryLargeData => "TypeError", + ExportPublicKeyPemError::Der(_) => "Error", + } + } + + pub fn get_export_private_key_pem_error( + e: &ExportPrivateKeyPemError, + ) -> &'static str { + match e { + ExportPrivateKeyPemError::AsymmetricPublicKeyDer(e) => { + get_asymmetric_private_key_der_error(e) + } + ExportPrivateKeyPemError::VeryLargeData => "TypeError", + ExportPrivateKeyPemError::Der(_) => "Error", + } + } + + pub fn get_x509_public_key_error(e: &X509PublicKeyError) -> &'static str { + match e { + X509PublicKeyError::X509(_) => "Error", + X509PublicKeyError::Rsa(_) => "Error", + X509PublicKeyError::Asn1(_) => "Error", + X509PublicKeyError::Ec(_) => "Error", + X509PublicKeyError::UnsupportedEcNamedCurve => "TypeError", + X509PublicKeyError::MissingEcParameters => "TypeError", + X509PublicKeyError::MalformedDssPublicKey => "TypeError", + X509PublicKeyError::UnsupportedX509KeyType => "TypeError", + } + } + + pub fn get_rsa_jwk_error(e: &RsaJwkError) -> &'static str { + match e { + RsaJwkError::Base64(_) => "Error", + RsaJwkError::Rsa(_) => "Error", + RsaJwkError::MissingRsaPrivateComponent => "TypeError", + } + } + + pub fn get_ec_jwk_error(e: &EcJwkError) -> &'static str { + match e { + EcJwkError::Ec(_) => "Error", + EcJwkError::UnsupportedCurve(_) => "TypeError", + } + } + + pub fn get_ed_raw_error(e: &EdRawError) -> &'static str { + match e { + EdRawError::Ed25519Signature(_) => "Error", + EdRawError::InvalidEd25519Key => "TypeError", + EdRawError::UnsupportedCurve => "TypeError", + } + } + + pub fn get_pbkdf2_error(e: &Pbkdf2Error) -> &'static str { + match e { + Pbkdf2Error::UnsupportedDigest(_) => "TypeError", + Pbkdf2Error::Join(_) => "Error", + } + } + + pub fn get_scrypt_async_error(e: &ScryptAsyncError) -> &'static str { + match e { + ScryptAsyncError::Join(_) => "Error", + ScryptAsyncError::Other(e) => get_error_class_name(e).unwrap_or("Error"), + } + } + + pub fn get_hkdf_error_error(e: &HkdfError) -> &'static str { + match e { + HkdfError::ExpectedSecretKey => "TypeError", + HkdfError::HkdfExpandFailed => "TypeError", + HkdfError::UnsupportedDigest(_) => "TypeError", + HkdfError::Join(_) => "Error", + } + } + + pub fn get_rsa_pss_params_parse_error( + _: &RsaPssParamsParseError, + ) -> &'static str { + "TypeError" + } + + pub fn get_asymmetric_private_key_error( + e: &AsymmetricPrivateKeyError, + ) -> &'static str { + match e { + AsymmetricPrivateKeyError::InvalidPemPrivateKeyInvalidUtf8(_) => "TypeError", + AsymmetricPrivateKeyError::InvalidEncryptedPemPrivateKey => "TypeError", + AsymmetricPrivateKeyError::InvalidPemPrivateKey => "TypeError", + AsymmetricPrivateKeyError::EncryptedPrivateKeyRequiresPassphraseToDecrypt => "TypeError", + AsymmetricPrivateKeyError::InvalidPkcs1PrivateKey => "TypeError", + AsymmetricPrivateKeyError::InvalidSec1PrivateKey => "TypeError", + AsymmetricPrivateKeyError::UnsupportedPemLabel(_) => "TypeError", + AsymmetricPrivateKeyError::RsaPssParamsParse(e) => get_rsa_pss_params_parse_error(e), + AsymmetricPrivateKeyError::InvalidEncryptedPkcs8PrivateKey => "TypeError", + AsymmetricPrivateKeyError::InvalidPkcs8PrivateKey => "TypeError", + AsymmetricPrivateKeyError::Pkcs1PrivateKeyDoesNotSupportEncryptionWithPassphrase => "TypeError", + AsymmetricPrivateKeyError::Sec1PrivateKeyDoesNotSupportEncryptionWithPassphrase => "TypeError", + AsymmetricPrivateKeyError::UnsupportedEcNamedCurve => "TypeError", + AsymmetricPrivateKeyError::InvalidPrivateKey => "TypeError", + AsymmetricPrivateKeyError::InvalidDsaPrivateKey => "TypeError", + AsymmetricPrivateKeyError::MalformedOrMissingNamedCurveInEcParameters => "TypeError", + AsymmetricPrivateKeyError::UnsupportedKeyType(_) => "TypeError", + AsymmetricPrivateKeyError::UnsupportedKeyFormat(_) => "TypeError", + AsymmetricPrivateKeyError::InvalidX25519PrivateKey => "TypeError", + AsymmetricPrivateKeyError::X25519PrivateKeyIsWrongLength => "TypeError", + AsymmetricPrivateKeyError::InvalidEd25519PrivateKey => "TypeError", + AsymmetricPrivateKeyError::MissingDhParameters => "TypeError", + AsymmetricPrivateKeyError::UnsupportedPrivateKeyOid => "TypeError", + } + } + + pub fn get_asymmetric_public_key_error( + e: &AsymmetricPublicKeyError, + ) -> &'static str { + match e { + AsymmetricPublicKeyError::InvalidPemPrivateKeyInvalidUtf8(_) => { + "TypeError" + } + AsymmetricPublicKeyError::InvalidPemPublicKey => "TypeError", + AsymmetricPublicKeyError::InvalidPkcs1PublicKey => "TypeError", + AsymmetricPublicKeyError::AsymmetricPrivateKey(e) => { + get_asymmetric_private_key_error(e) + } + AsymmetricPublicKeyError::InvalidX509Certificate => "TypeError", + AsymmetricPublicKeyError::X509(_) => "Error", + AsymmetricPublicKeyError::X509PublicKey(e) => { + get_x509_public_key_error(e) + } + AsymmetricPublicKeyError::UnsupportedPemLabel(_) => "TypeError", + AsymmetricPublicKeyError::InvalidSpkiPublicKey => "TypeError", + AsymmetricPublicKeyError::UnsupportedKeyType(_) => "TypeError", + AsymmetricPublicKeyError::UnsupportedKeyFormat(_) => "TypeError", + AsymmetricPublicKeyError::Spki(_) => "Error", + AsymmetricPublicKeyError::Pkcs1(_) => "Error", + AsymmetricPublicKeyError::RsaPssParamsParse(_) => "TypeError", + AsymmetricPublicKeyError::MalformedDssPublicKey => "TypeError", + AsymmetricPublicKeyError::MalformedOrMissingNamedCurveInEcParameters => { + "TypeError" + } + AsymmetricPublicKeyError::MalformedOrMissingPublicKeyInEcSpki => { + "TypeError" + } + AsymmetricPublicKeyError::Ec(_) => "Error", + AsymmetricPublicKeyError::UnsupportedEcNamedCurve => "TypeError", + AsymmetricPublicKeyError::MalformedOrMissingPublicKeyInX25519Spki => { + "TypeError" + } + AsymmetricPublicKeyError::X25519PublicKeyIsTooShort => "TypeError", + AsymmetricPublicKeyError::InvalidEd25519PublicKey => "TypeError", + AsymmetricPublicKeyError::MissingDhParameters => "TypeError", + AsymmetricPublicKeyError::MalformedDhParameters => "TypeError", + AsymmetricPublicKeyError::MalformedOrMissingPublicKeyInDhSpki => { + "TypeError" + } + AsymmetricPublicKeyError::UnsupportedPrivateKeyOid => "TypeError", + } + } + + pub fn get_private_encrypt_decrypt_error( + e: &PrivateEncryptDecryptError, + ) -> &'static str { + match e { + PrivateEncryptDecryptError::Pkcs8(_) => "Error", + PrivateEncryptDecryptError::Spki(_) => "Error", + PrivateEncryptDecryptError::Utf8(_) => "Error", + PrivateEncryptDecryptError::Rsa(_) => "Error", + PrivateEncryptDecryptError::UnknownPadding => "TypeError", + } + } + + pub fn get_ecdh_encode_pub_key_error(e: &EcdhEncodePubKey) -> &'static str { + match e { + EcdhEncodePubKey::InvalidPublicKey => "TypeError", + EcdhEncodePubKey::UnsupportedCurve => "TypeError", + EcdhEncodePubKey::Sec1(_) => "Error", + } + } + + pub fn get_diffie_hellman_error(_: &DiffieHellmanError) -> &'static str { + "TypeError" + } + + pub fn get_sign_ed25519_error(_: &SignEd25519Error) -> &'static str { + "TypeError" + } + + pub fn get_verify_ed25519_error(_: &VerifyEd25519Error) -> &'static str { + "TypeError" + } +} + +fn get_os_error(error: &OsError) -> &'static str { + match error { + OsError::Permission(e) => get_permission_check_error_class(e), + OsError::InvalidUtf8(_) => "InvalidData", + OsError::EnvEmptyKey => "TypeError", + OsError::EnvInvalidKey(_) => "TypeError", + OsError::EnvInvalidValue(_) => "TypeError", + OsError::Io(e) => get_io_error_class(e), + OsError::Var(e) => get_env_var_error_class(e), + } +} + +fn get_sync_fetch_error(error: &SyncFetchError) -> &'static str { + match error { + SyncFetchError::BlobUrlsNotSupportedInContext => "TypeError", + SyncFetchError::Io(e) => get_io_error_class(e), + SyncFetchError::InvalidScriptUrl => "TypeError", + SyncFetchError::InvalidStatusCode(_) => "TypeError", + SyncFetchError::ClassicScriptSchemeUnsupportedInWorkers(_) => "TypeError", + SyncFetchError::InvalidUri(_) => "Error", + SyncFetchError::InvalidMimeType(_) => "DOMExceptionNetworkError", + SyncFetchError::MissingMimeType => "DOMExceptionNetworkError", + SyncFetchError::Fetch(e) => get_fetch_error(e), + SyncFetchError::Join(_) => "Error", + SyncFetchError::Other(e) => get_error_class_name(e).unwrap_or("Error"), + } +} + pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { deno_core::error::get_custom_error_class(e) - .or_else(|| deno_webgpu::error::get_error_class_name(e)) - .or_else(|| deno_web::get_error_class_name(e)) - .or_else(|| deno_webstorage::get_not_supported_error_class_name(e)) - .or_else(|| deno_websocket::get_network_error_class_name(e)) + .or_else(|| { + e.downcast_ref::() + .map(get_child_permission_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_permission_check_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_permission_error_class) + }) + .or_else(|| e.downcast_ref::().map(get_fs_error)) + .or_else(|| { + e.downcast_ref::() + .map(node::get_blocklist_error) + }) + .or_else(|| e.downcast_ref::().map(node::get_fs_error)) + .or_else(|| { + e.downcast_ref::() + .map(node::get_idna_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_ipc_json_stream_error) + }) + .or_else(|| e.downcast_ref::().map(node::get_ipc_error)) + .or_else(|| { + e.downcast_ref::() + .map(node::get_worker_threads_filename_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_require_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_http2_error) + }) + .or_else(|| e.downcast_ref::().map(node::get_os_error)) + .or_else(|| { + e.downcast_ref::() + .map(node::get_brotli_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_mode_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_zlib_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_crypto_cipher_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_crypto_cipher_context_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_crypto_decipher_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_crypto_decipher_context_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_x509_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_crypto_key_object_handle_prehashed_sign_and_verify_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_crypto_hash_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_asymmetric_public_key_jwk_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_generate_rsa_pss_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_asymmetric_private_key_der_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_asymmetric_public_key_der_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_export_public_key_pem_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_export_private_key_pem_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_rsa_jwk_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_ec_jwk_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_ed_raw_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_pbkdf2_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_scrypt_async_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_hkdf_error_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_rsa_pss_params_parse_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_asymmetric_private_key_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_asymmetric_public_key_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_private_encrypt_decrypt_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_ecdh_encode_pub_key_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_diffie_hellman_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_sign_ed25519_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(node::get_verify_ed25519_error) + }) + .or_else(|| e.downcast_ref::().map(get_napi_error_class)) + .or_else(|| e.downcast_ref::().map(get_web_error_class)) + .or_else(|| { + e.downcast_ref::() + .map(get_create_worker_error) + }) + .or_else(|| e.downcast_ref::().map(get_tty_error)) + .or_else(|| e.downcast_ref::().map(get_readline_error)) + .or_else(|| e.downcast_ref::().map(get_signal_error)) + .or_else(|| e.downcast_ref::().map(get_fs_events_error)) + .or_else(|| e.downcast_ref::().map(get_http_start_error)) + .or_else(|| e.downcast_ref::().map(get_process_error)) + .or_else(|| e.downcast_ref::().map(get_os_error)) + .or_else(|| e.downcast_ref::().map(get_sync_fetch_error)) + .or_else(|| { + e.downcast_ref::() + .map(get_web_compression_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_web_message_port_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_web_stream_resource_error_class) + }) + .or_else(|| e.downcast_ref::().map(get_web_blob_error_class)) + .or_else(|| e.downcast_ref::().map(|_| "TypeError")) + .or_else(|| e.downcast_ref::().map(get_ffi_repr_error_class)) + .or_else(|| e.downcast_ref::().map(get_http_error)) + .or_else(|| e.downcast_ref::().map(get_http_next_error)) + .or_else(|| { + e.downcast_ref::() + .map(get_websocket_upgrade_error) + }) + .or_else(|| e.downcast_ref::().map(get_fs_ops_error)) + .or_else(|| { + e.downcast_ref::() + .map(get_ffi_dlfcn_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_ffi_static_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_ffi_callback_error_class) + }) + .or_else(|| e.downcast_ref::().map(get_ffi_call_error_class)) + .or_else(|| e.downcast_ref::().map(get_tls_error_class)) + .or_else(|| e.downcast_ref::().map(get_cron_error_class)) + .or_else(|| e.downcast_ref::().map(get_canvas_error)) + .or_else(|| e.downcast_ref::().map(get_cache_error)) + .or_else(|| e.downcast_ref::().map(get_websocket_error)) + .or_else(|| { + e.downcast_ref::() + .map(get_websocket_handshake_error) + }) + .or_else(|| e.downcast_ref::().map(get_kv_error)) + .or_else(|| e.downcast_ref::().map(get_fetch_error)) + .or_else(|| { + e.downcast_ref::() + .map(get_http_client_create_error) + }) + .or_else(|| e.downcast_ref::().map(get_net_error)) + .or_else(|| { + e.downcast_ref::() + .map(get_net_map_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_broadcast_channel_error) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_webgpu_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_webgpu_buffer_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_webgpu_bundle_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_webgpu_byow_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_webgpu_render_pass_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_webgpu_surface_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_decrypt_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_encrypt_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_shared_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_ed25519_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_export_key_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_generate_key_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_import_key_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_x448_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_x25519_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_crypto_error_class) + }) + .or_else(|| { + e.downcast_ref::() + .map(get_webstorage_class_name) + }) + .or_else(|| { + e.downcast_ref::() + .map(|_| "TypeError") + }) .or_else(|| { e.downcast_ref::() .map(get_dlopen_error_class) diff --git a/runtime/examples/extension/main.rs b/runtime/examples/extension/main.rs index 6f4f02508b..9889b28dcf 100644 --- a/runtime/examples/extension/main.rs +++ b/runtime/examples/extension/main.rs @@ -16,6 +16,7 @@ use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; +use deno_runtime::worker::WorkerServiceOptions; #[op2(fast)] fn op_hello(#[string] text: &str) { @@ -35,13 +36,26 @@ async fn main() -> Result<(), AnyError> { Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/extension/main.js"); let main_module = ModuleSpecifier::from_file_path(js_path).unwrap(); eprintln!("Running {main_module}..."); + let fs = Arc::new(RealFs); + let permission_desc_parser = + Arc::new(RuntimePermissionDescriptorParser::new(fs.clone())); let mut worker = MainWorker::bootstrap_from_options( main_module.clone(), - PermissionsContainer::allow_all(Arc::new( - RuntimePermissionDescriptorParser::new(Arc::new(RealFs)), - )), - WorkerOptions { + WorkerServiceOptions { module_loader: Rc::new(FsModuleLoader), + permissions: PermissionsContainer::allow_all(permission_desc_parser), + blob_store: Default::default(), + broadcast_channel: Default::default(), + feature_checker: Default::default(), + node_services: Default::default(), + npm_process_state_provider: Default::default(), + root_cert_store_provider: Default::default(), + shared_array_buffer_store: Default::default(), + compiled_wasm_module_store: Default::default(), + v8_code_cache: Default::default(), + fs, + }, + WorkerOptions { extensions: vec![hello_runtime::init_ops_and_esm()], ..Default::default() }, diff --git a/runtime/fmt_errors.rs b/runtime/fmt_errors.rs index b2cec2a5a6..6c05fbc633 100644 --- a/runtime/fmt_errors.rs +++ b/runtime/fmt_errors.rs @@ -1,11 +1,10 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. //! This mod provides DenoError to unify errors across Deno. +use color_print::cformat; +use color_print::cstr; use deno_core::error::format_frame; use deno_core::error::JsError; -use deno_terminal::colors::cyan; -use deno_terminal::colors::italic_bold; -use deno_terminal::colors::red; -use deno_terminal::colors::yellow; +use deno_terminal::colors; use std::fmt::Write as _; #[derive(Debug, Clone)] @@ -24,26 +23,54 @@ struct IndexedErrorReference<'a> { enum FixSuggestionKind { Info, Hint, + Docs, +} + +#[derive(Debug)] +enum FixSuggestionMessage<'a> { + Single(&'a str), + Multiline(&'a [&'a str]), } #[derive(Debug)] pub struct FixSuggestion<'a> { kind: FixSuggestionKind, - message: &'a str, + message: FixSuggestionMessage<'a>, } impl<'a> FixSuggestion<'a> { pub fn info(message: &'a str) -> Self { Self { kind: FixSuggestionKind::Info, - message, + message: FixSuggestionMessage::Single(message), + } + } + + pub fn info_multiline(messages: &'a [&'a str]) -> Self { + Self { + kind: FixSuggestionKind::Info, + message: FixSuggestionMessage::Multiline(messages), } } pub fn hint(message: &'a str) -> Self { Self { kind: FixSuggestionKind::Hint, - message, + message: FixSuggestionMessage::Single(message), + } + } + + pub fn hint_multiline(messages: &'a [&'a str]) -> Self { + Self { + kind: FixSuggestionKind::Hint, + message: FixSuggestionMessage::Multiline(messages), + } + } + + pub fn docs(url: &'a str) -> Self { + Self { + kind: FixSuggestionKind::Docs, + message: FixSuggestionMessage::Single(url), } } } @@ -58,10 +85,10 @@ impl deno_core::error::ErrorFormat for AnsiColors { use deno_core::error::ErrorElement::*; match element { Anonymous | NativeFrame | FileName | EvalOrigin => { - cyan(s).to_string().into() + colors::cyan(s).to_string().into() } - LineNumber | ColumnNumber => yellow(s).to_string().into(), - FunctionName | PromiseAll => italic_bold(s).to_string().into(), + LineNumber | ColumnNumber => colors::yellow(s).to_string().into(), + FunctionName | PromiseAll => colors::italic_bold(s).to_string().into(), } } } @@ -94,7 +121,7 @@ fn format_maybe_source_line( if column_number as usize > source_line.len() { return format!( "\n{} Couldn't format source line: Column {} is out of bounds (source may have changed at runtime)", - yellow("Warning"), column_number, + colors::yellow("Warning"), column_number, ); } @@ -107,9 +134,9 @@ fn format_maybe_source_line( } s.push('^'); let color_underline = if is_error { - red(&s).to_string() + colors::red(&s).to_string() } else { - cyan(&s).to_string() + colors::cyan(&s).to_string() }; let indent = format!("{:indent$}", "", indent = level); @@ -180,7 +207,8 @@ fn format_js_error_inner( if let Some(circular) = &circular { if js_error.is_same_error(circular.reference.to) { - write!(s, " {}", cyan(format!("", circular.index))).unwrap(); + write!(s, " {}", colors::cyan(format!("", circular.index))) + .unwrap(); } } @@ -218,7 +246,8 @@ fn format_js_error_inner( .unwrap_or(false); let error_string = if is_caused_by_circular { - cyan(format!("[Circular *{}]", circular.unwrap().index)).to_string() + colors::cyan(format!("[Circular *{}]", circular.unwrap().index)) + .to_string() } else { format_js_error_inner(cause, circular, false, vec![]) }; @@ -235,10 +264,35 @@ fn format_js_error_inner( for (index, suggestion) in suggestions.iter().enumerate() { write!(s, " ").unwrap(); match suggestion.kind { - FixSuggestionKind::Hint => write!(s, "{} ", cyan("hint:")).unwrap(), - FixSuggestionKind::Info => write!(s, "{} ", yellow("info:")).unwrap(), + FixSuggestionKind::Hint => { + write!(s, "{} ", colors::cyan("hint:")).unwrap() + } + FixSuggestionKind::Info => { + write!(s, "{} ", colors::yellow("info:")).unwrap() + } + FixSuggestionKind::Docs => { + write!(s, "{} ", colors::green("docs:")).unwrap() + } }; - write!(s, "{}", suggestion.message).unwrap(); + match suggestion.message { + FixSuggestionMessage::Single(msg) => { + if matches!(suggestion.kind, FixSuggestionKind::Docs) { + write!(s, "{}", cformat!("{}", msg)).unwrap(); + } else { + write!(s, "{}", msg).unwrap(); + } + } + FixSuggestionMessage::Multiline(messages) => { + for (idx, message) in messages.iter().enumerate() { + if idx != 0 { + writeln!(s).unwrap(); + write!(s, " ").unwrap(); + } + write!(s, "{}", message).unwrap(); + } + } + } + if index != (suggestions.len() - 1) { writeln!(s).unwrap(); } @@ -248,6 +302,166 @@ fn format_js_error_inner( s } +fn get_suggestions_for_terminal_errors(e: &JsError) -> Vec { + if let Some(msg) = &e.message { + if msg.contains("module is not defined") + || msg.contains("exports is not defined") + || msg.contains("require is not defined") + { + return vec![ + FixSuggestion::info_multiline(&[ + cstr!("Deno supports CommonJS modules in .cjs files, or when the closest"), + cstr!("package.json has a \"type\": \"commonjs\" option.") + ]), + FixSuggestion::hint_multiline(&[ + "Rewrite this module to ESM,", + cstr!("or change the file extension to .cjs,"), + cstr!("or add package.json next to the file with \"type\": \"commonjs\" option."), + ]), + FixSuggestion::docs("https://docs.deno.com/go/commonjs"), + ]; + } else if msg.contains("__filename is not defined") { + return vec![ + FixSuggestion::info(cstr!( + "__filename global is not available in ES modules." + )), + FixSuggestion::hint(cstr!("Use import.meta.filename instead.")), + ]; + } else if msg.contains("__dirname is not defined") { + return vec![ + FixSuggestion::info(cstr!( + "__dirname global is not available in ES modules." + )), + FixSuggestion::hint(cstr!("Use import.meta.dirname instead.")), + ]; + } else if msg.contains("Buffer is not defined") { + return vec![ + FixSuggestion::info(cstr!( + "Buffer is not available in the global scope in Deno." + )), + FixSuggestion::hint_multiline(&[ + cstr!("Import it explicitly with import { Buffer } from \"node:buffer\";,"), + cstr!("or run again with --unstable-node-globals flag to add this global."), + ]), + ]; + } else if msg.contains("clearImmediate is not defined") { + return vec![ + FixSuggestion::info(cstr!( + "clearImmediate is not available in the global scope in Deno." + )), + FixSuggestion::hint_multiline(&[ + cstr!("Import it explicitly with import { clearImmediate } from \"node:timers\";,"), + cstr!("or run again with --unstable-node-globals flag to add this global."), + ]), + ]; + } else if msg.contains("setImmediate is not defined") { + return vec![ + FixSuggestion::info(cstr!( + "setImmediate is not available in the global scope in Deno." + )), + FixSuggestion::hint_multiline( + &[cstr!("Import it explicitly with import { setImmediate } from \"node:timers\";,"), + cstr!("or run again with --unstable-node-globals flag to add this global."), + ]), + ]; + } else if msg.contains("global is not defined") { + return vec![ + FixSuggestion::info(cstr!( + "global is not available in the global scope in Deno." + )), + FixSuggestion::hint_multiline(&[ + cstr!("Use globalThis instead, or assign globalThis.global = globalThis,"), + cstr!("or run again with --unstable-node-globals flag to add this global."), + ]), + ]; + } else if msg.contains("openKv is not a function") { + return vec![ + FixSuggestion::info("Deno.openKv() is an unstable API."), + FixSuggestion::hint( + "Run again with `--unstable-kv` flag to enable this API.", + ), + ]; + } else if msg.contains("cron is not a function") { + return vec![ + FixSuggestion::info("Deno.cron() is an unstable API."), + FixSuggestion::hint( + "Run again with `--unstable-cron` flag to enable this API.", + ), + ]; + } else if msg.contains("WebSocketStream is not defined") { + return vec![ + FixSuggestion::info("new WebSocketStream() is an unstable API."), + FixSuggestion::hint( + "Run again with `--unstable-net` flag to enable this API.", + ), + ]; + } else if msg.contains("Temporal is not defined") { + return vec![ + FixSuggestion::info("Temporal is an unstable API."), + FixSuggestion::hint( + "Run again with `--unstable-temporal` flag to enable this API.", + ), + ]; + } else if msg.contains("BroadcastChannel is not defined") { + return vec![ + FixSuggestion::info("BroadcastChannel is an unstable API."), + FixSuggestion::hint( + "Run again with `--unstable-broadcast-channel` flag to enable this API.", + ), + ]; + } else if msg.contains("window is not defined") { + return vec![ + FixSuggestion::info("window global is not available in Deno 2."), + FixSuggestion::hint("Replace `window` with `globalThis`."), + ]; + } else if msg.contains("UnsafeWindowSurface is not a constructor") { + return vec![ + FixSuggestion::info("Deno.UnsafeWindowSurface is an unstable API."), + FixSuggestion::hint( + "Run again with `--unstable-webgpu` flag to enable this API.", + ), + ]; + // Try to capture errors like: + // ``` + // Uncaught Error: Cannot find module '../build/Release/canvas.node' + // Require stack: + // - /.../deno/npm/registry.npmjs.org/canvas/2.11.2/lib/bindings.js + // - /.../.cache/deno/npm/registry.npmjs.org/canvas/2.11.2/lib/canvas.js + // ``` + } else if msg.contains("Cannot find module") + && msg.contains("Require stack") + && msg.contains(".node'") + { + return vec![ + FixSuggestion::info_multiline( + &[ + "Trying to execute an npm package using Node-API addons,", + "these packages require local `node_modules` directory to be present." + ] + ), + FixSuggestion::hint_multiline( + &[ + "Add `\"nodeModulesDir\": \"auto\" option to `deno.json`, and then run", + "`deno install --allow-scripts=npm: --entrypoint