diff --git a/.github/workflows/ci.generate.ts b/.github/workflows/ci.generate.ts index 3214fad509..f24034d494 100755 --- a/.github/workflows/ci.generate.ts +++ b/.github/workflows/ci.generate.ts @@ -5,7 +5,7 @@ import { stringify } from "jsr:@std/yaml@^0.221/stringify"; // Bump this number when you want to purge the cache. // Note: the tools/release/01_bump_crate_versions.ts script will update this version // automatically via regex, so ensure that this line maintains this format. -const cacheVersion = 33; +const cacheVersion = 37; const ubuntuX86Runner = "ubuntu-24.04"; const ubuntuX86XlRunner = "ubuntu-24.04-xl"; @@ -14,7 +14,7 @@ const windowsX86Runner = "windows-2022"; const windowsX86XlRunner = "windows-2022-xl"; const macosX86Runner = "macos-13"; const macosArmRunner = "macos-14"; -const selfHostedMacosArmRunner = "self-hosted"; +const selfHostedMacosArmRunner = "ghcr.io/cirruslabs/macos-runner:sonoma"; const Runners = { linuxX86: { @@ -41,8 +41,14 @@ const Runners = { macosArm: { os: "macos", arch: "aarch64", + runner: macosArmRunner, + }, + macosArmSelfHosted: { + os: "macos", + arch: "aarch64", + // Actually use self-hosted runner only in denoland/deno on `main` branch and for tags (release) builds. runner: - `\${{ github.repository == 'denoland/deno' && startsWith(github.ref, 'refs/tags/') && '${selfHostedMacosArmRunner}' || '${macosArmRunner}' }}`, + `\${{ github.repository == 'denoland/deno' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) && '${selfHostedMacosArmRunner}' || '${macosArmRunner}' }}`, }, windowsX86: { os: "windows", @@ -124,9 +130,7 @@ cat /sysroot/.env # to build because the object formats are not compatible. echo " CARGO_PROFILE_BENCH_INCREMENTAL=false -CARGO_PROFILE_BENCH_LTO=false CARGO_PROFILE_RELEASE_INCREMENTAL=false -CARGO_PROFILE_RELEASE_LTO=false RUSTFLAGS<<__1 -C linker-plugin-lto=true -C linker=clang-${llvmVersion} @@ -150,7 +154,7 @@ RUSTDOCFLAGS<<__1 $RUSTFLAGS __1 CC=/usr/bin/clang-${llvmVersion} -CFLAGS=-flto=thin $CFLAGS +CFLAGS=$CFLAGS " > $GITHUB_ENV`, }; @@ -384,7 +388,7 @@ const ci = { job: "test", profile: "debug", }, { - ...Runners.macosArm, + ...Runners.macosArmSelfHosted, job: "test", profile: "release", skip_pr: true, @@ -486,7 +490,7 @@ const ci = { }, { name: "Cache Cargo home", - uses: "actions/cache@v4", + uses: "cirruslabs/cache@v4", with: { // See https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci // Note that with the new sparse registry format, we no longer have to cache a `.git` dir diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e560840d2e..efbf6f6166 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,12 +68,12 @@ jobs: skip: '${{ !contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'') }}' - os: macos arch: aarch64 - runner: '${{ github.repository == ''denoland/deno'' && startsWith(github.ref, ''refs/tags/'') && ''self-hosted'' || ''macos-14'' }}' + runner: 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-24.04'' || github.repository == ''denoland/deno'' && startsWith(github.ref, ''refs/tags/'') && ''self-hosted'' || ''macos-14'' }}' + runner: '${{ (!contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'')) && ''ubuntu-24.04'' || github.repository == ''denoland/deno'' && (github.ref == ''refs/heads/main'' || startsWith(github.ref, ''refs/tags/'')) && ''ghcr.io/cirruslabs/macos-runner:sonoma'' || ''macos-14'' }}' job: test profile: release skip: '${{ !contains(github.event.pull_request.labels.*.name, ''ci-full'') && (github.event_name == ''pull_request'') }}' @@ -175,7 +175,7 @@ jobs: tar --exclude=".git*" --exclude=target --exclude=third_party/prebuilt \ -czvf target/release/deno_src.tar.gz -C .. deno - name: Cache Cargo home - uses: actions/cache@v4 + uses: cirruslabs/cache@v4 with: path: |- ~/.cargo/.crates.toml @@ -184,8 +184,8 @@ jobs: ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db - key: '33-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles(''Cargo.lock'') }}' - restore-keys: '33-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-' + key: '37-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles(''Cargo.lock'') }}' + restore-keys: '37-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-' if: '!(matrix.skip)' - uses: dsherret/rust-toolchain-file@v1 if: '!(matrix.skip)' @@ -307,9 +307,7 @@ jobs: # to build because the object formats are not compatible. echo " CARGO_PROFILE_BENCH_INCREMENTAL=false - CARGO_PROFILE_BENCH_LTO=false CARGO_PROFILE_RELEASE_INCREMENTAL=false - CARGO_PROFILE_RELEASE_LTO=false RUSTFLAGS<<__1 -C linker-plugin-lto=true -C linker=clang-19 @@ -333,7 +331,7 @@ jobs: $RUSTFLAGS __1 CC=/usr/bin/clang-19 - CFLAGS=-flto=thin $CFLAGS + CFLAGS=$CFLAGS " > $GITHUB_ENV - name: Remove macOS cURL --ipv4 flag run: |- @@ -379,7 +377,7 @@ jobs: !./target/*/*.zip !./target/*/*.tar.gz key: never_saved - restore-keys: '33-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-' + restore-keys: '37-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 @@ -689,7 +687,7 @@ jobs: !./target/*/gn_root !./target/*/*.zip !./target/*/*.tar.gz - key: '33-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-${{ github.sha }}' + key: '37-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-${{ github.sha }}' publish-canary: name: publish canary runs-on: ubuntu-24.04 diff --git a/Cargo.lock b/Cargo.lock index 1f13898fe0..311a7ac885 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -646,9 +646,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -838,18 +838,16 @@ checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" name = "cli_tests" version = "0.0.0" dependencies = [ + "anyhow", "bytes", "chrono", - "deno_ast", "deno_bench_util", "deno_cache_dir", - "deno_core", - "deno_fetch", "deno_lockfile", "deno_semver", "deno_terminal 0.2.0", - "deno_tls", "deno_tower_lsp", + "deno_unsync", "fastwebsockets", "file_test_runner", "flaky_test", @@ -866,7 +864,11 @@ dependencies = [ "pretty_assertions", "regex", "reqwest", + "rustls", + "rustls-pemfile", + "rustls-tokio-stream", "serde", + "serde_json", "sys_traits", "test_server", "tokio", @@ -1244,7 +1246,7 @@ dependencies = [ [[package]] name = "deno" -version = "2.1.5" +version = "2.1.7" dependencies = [ "anstream", "async-trait", @@ -1270,8 +1272,10 @@ dependencies = [ "deno_doc", "deno_error", "deno_graph", + "deno_lib", "deno_lint", "deno_lockfile", + "deno_media_type", "deno_npm", "deno_npm_cache", "deno_package_json", @@ -1279,6 +1283,7 @@ dependencies = [ "deno_resolver", "deno_runtime", "deno_semver", + "deno_snapshots", "deno_task_shell", "deno_telemetry", "deno_terminal 0.2.0", @@ -1290,7 +1295,6 @@ dependencies = [ "dprint-plugin-jupyter", "dprint-plugin-markdown", "dprint-plugin-typescript", - "env_logger", "fancy-regex", "faster-hex", "flate2", @@ -1421,7 +1425,7 @@ dependencies = [ [[package]] name = "deno_bench_util" -version = "0.179.0" +version = "0.181.0" dependencies = [ "bencher", "deno_core", @@ -1430,7 +1434,7 @@ dependencies = [ [[package]] name = "deno_broadcast_channel" -version = "0.179.0" +version = "0.181.0" dependencies = [ "async-trait", "deno_core", @@ -1442,7 +1446,7 @@ dependencies = [ [[package]] name = "deno_cache" -version = "0.117.0" +version = "0.119.0" dependencies = [ "async-trait", "deno_core", @@ -1485,7 +1489,7 @@ dependencies = [ [[package]] name = "deno_canvas" -version = "0.54.0" +version = "0.56.0" dependencies = [ "deno_core", "deno_error", @@ -1497,11 +1501,12 @@ dependencies = [ [[package]] name = "deno_config" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c11bd51ef6738cabfc3c53f16c209a0b8615cb1e4e5bf3b14e3b5deebfe21" +checksum = "47a47412627aa0d08414eca0e8329128013ab70bdb2cdfdc5456c2214cf24c8f" dependencies = [ "boxed_error", + "capacity_builder 0.5.0", "deno_error", "deno_package_json", "deno_path_util", @@ -1523,16 +1528,16 @@ dependencies = [ [[package]] name = "deno_console" -version = "0.185.0" +version = "0.187.0" dependencies = [ "deno_core", ] [[package]] name = "deno_core" -version = "0.330.0" +version = "0.331.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd38bbbd68ed873165ccb630322704b44140d3a8c8d50f898beac4d1a8a3358c" +checksum = "ce2d1779358cad2bc56d71176298767be628d707bb75585f6f8a4be2da8ccda1" dependencies = [ "anyhow", "az", @@ -1574,7 +1579,7 @@ checksum = "fe4dccb6147bb3f3ba0c7a48e993bfeb999d2c2e47a81badee80e2b370c8d695" [[package]] name = "deno_cron" -version = "0.65.0" +version = "0.67.0" dependencies = [ "anyhow", "async-trait", @@ -1588,7 +1593,7 @@ dependencies = [ [[package]] name = "deno_crypto" -version = "0.199.0" +version = "0.201.0" dependencies = [ "aes", "aes-gcm", @@ -1656,9 +1661,9 @@ dependencies = [ [[package]] name = "deno_error" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da6a58de6932a96f84e133c072fd3b525966ee122a71f3efd48bbff2eed5ac" +checksum = "9c23dbc46d5804814b08b4675838f9884e3a52916987ec5105af36d42f9911b5" dependencies = [ "deno_error_macro", "libc", @@ -1670,9 +1675,9 @@ dependencies = [ [[package]] name = "deno_error_macro" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46351dff93aed2039407c91e2ded2a5591e42d2795ab3d111288625bb710d3d2" +checksum = "babccedee31ce7e57c3e6dff2cb3ab8d68c49d0df8222fe0d11d628e65192790" dependencies = [ "proc-macro2", "quote", @@ -1681,7 +1686,7 @@ dependencies = [ [[package]] name = "deno_fetch" -version = "0.209.0" +version = "0.211.0" dependencies = [ "base64 0.21.7", "bytes", @@ -1718,7 +1723,7 @@ dependencies = [ [[package]] name = "deno_ffi" -version = "0.172.0" +version = "0.174.0" dependencies = [ "deno_core", "deno_error", @@ -1739,7 +1744,7 @@ dependencies = [ [[package]] name = "deno_fs" -version = "0.95.0" +version = "0.97.0" dependencies = [ "async-trait", "base32", @@ -1797,7 +1802,7 @@ dependencies = [ [[package]] name = "deno_http" -version = "0.183.0" +version = "0.185.0" dependencies = [ "async-compression", "async-trait", @@ -1837,7 +1842,7 @@ dependencies = [ [[package]] name = "deno_io" -version = "0.95.0" +version = "0.97.0" dependencies = [ "async-trait", "deno_core", @@ -1859,7 +1864,7 @@ dependencies = [ [[package]] name = "deno_kv" -version = "0.93.0" +version = "0.95.0" dependencies = [ "anyhow", "async-trait", @@ -1890,6 +1895,40 @@ dependencies = [ "url", ] +[[package]] +name = "deno_lib" +version = "0.3.0" +dependencies = [ + "capacity_builder 0.5.0", + "deno_config", + "deno_error", + "deno_fs", + "deno_media_type", + "deno_node", + "deno_npm", + "deno_path_util", + "deno_resolver", + "deno_runtime", + "deno_semver", + "deno_terminal 0.2.0", + "env_logger", + "faster-hex", + "indexmap 2.3.0", + "libsui", + "log", + "node_resolver", + "parking_lot", + "ring", + "serde", + "serde_json", + "sys_traits", + "test_server", + "thiserror 2.0.3", + "tokio", + "twox-hash", + "url", +] + [[package]] name = "deno_lint" version = "0.68.2" @@ -1922,18 +1961,19 @@ dependencies = [ [[package]] name = "deno_media_type" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a417f8bd3f1074185c4c8ccb6ea6261ae173781596cc358e68ad07aaac11009d" +checksum = "600222d059ab31ff31182b3e12615df2134a9e01605836b78ad8df91ba39eab3" dependencies = [ "data-url", + "encoding_rs", "serde", "url", ] [[package]] name = "deno_napi" -version = "0.116.0" +version = "0.118.0" dependencies = [ "deno_core", "deno_error", @@ -1962,7 +2002,7 @@ dependencies = [ [[package]] name = "deno_net" -version = "0.177.0" +version = "0.179.0" dependencies = [ "deno_core", "deno_error", @@ -1981,7 +2021,7 @@ dependencies = [ [[package]] name = "deno_node" -version = "0.123.0" +version = "0.125.0" dependencies = [ "aead-gcm-stream", "aes", @@ -1999,11 +2039,11 @@ dependencies = [ "deno_fetch", "deno_fs", "deno_io", - "deno_media_type", "deno_net", "deno_package_json", "deno_path_util", "deno_permissions", + "deno_process", "deno_whoami", "der", "digest", @@ -2012,7 +2052,7 @@ dependencies = [ "ecdsa", "ed25519-dalek", "elliptic-curve", - "errno 0.2.8", + "errno", "faster-hex", "h2 0.4.4", "hkdf", @@ -2041,7 +2081,6 @@ dependencies = [ "p384", "path-clean", "pbkdf2", - "pin-project-lite", "pkcs8", "rand", "regex", @@ -2074,9 +2113,9 @@ dependencies = [ [[package]] name = "deno_npm" -version = "0.27.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f818ad5dc4c206b50b5cfa6f10b4b94b127e15c8342c152768eba40c225ca23" +checksum = "4adceb4c34f10e837d0e3ae76e88dddefb13e83c05c1ef1699fa5519241c9d27" dependencies = [ "async-trait", "capacity_builder 0.5.0", @@ -2094,7 +2133,7 @@ dependencies = [ [[package]] name = "deno_npm_cache" -version = "0.4.0" +version = "0.6.0" dependencies = [ "async-trait", "base64 0.21.7", @@ -2124,9 +2163,9 @@ dependencies = [ [[package]] name = "deno_ops" -version = "0.206.0" +version = "0.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c25ffa9d088ea00748dbef870bba110ac22ebf8cf7b2e9eb288409c5d852af3" +checksum = "96f000a21f6969b4c945bc8e9e785aa439f11ca4fd3fbddcd5bebc102167eb37" dependencies = [ "indexmap 2.3.0", "proc-macro-rules", @@ -2139,6 +2178,27 @@ dependencies = [ "thiserror 2.0.3", ] +[[package]] +name = "deno_os" +version = "0.4.0" +dependencies = [ + "deno_core", + "deno_error", + "deno_path_util", + "deno_permissions", + "deno_telemetry", + "libc", + "netif", + "ntapi", + "once_cell", + "serde", + "signal-hook", + "signal-hook-registry", + "thiserror 2.0.3", + "tokio", + "winapi", +] + [[package]] name = "deno_package_json" version = "0.4.0" @@ -2172,7 +2232,7 @@ dependencies = [ [[package]] name = "deno_permissions" -version = "0.44.0" +version = "0.46.0" dependencies = [ "capacity_builder 0.5.0", "deno_core", @@ -2190,9 +2250,36 @@ dependencies = [ "winapi", ] +[[package]] +name = "deno_process" +version = "0.2.0" +dependencies = [ + "deno_core", + "deno_error", + "deno_fs", + "deno_io", + "deno_os", + "deno_path_util", + "deno_permissions", + "libc", + "log", + "memchr", + "nix", + "pin-project-lite", + "rand", + "serde", + "simd-json", + "tempfile", + "thiserror 2.0.3", + "tokio", + "which", + "winapi", + "windows-sys 0.59.0", +] + [[package]] name = "deno_resolver" -version = "0.16.0" +version = "0.18.0" dependencies = [ "anyhow", "async-trait", @@ -2218,7 +2305,7 @@ dependencies = [ [[package]] name = "deno_runtime" -version = "0.193.0" +version = "0.195.0" dependencies = [ "color-print", "deno_ast", @@ -2239,8 +2326,11 @@ dependencies = [ "deno_napi", "deno_net", "deno_node", + "deno_os", "deno_path_util", "deno_permissions", + "deno_process", + "deno_resolver", "deno_telemetry", "deno_terminal 0.2.0", "deno_tls", @@ -2253,7 +2343,6 @@ dependencies = [ "dlopen2", "encoding_rs", "fastwebsockets", - "flate2", "http 1.1.0", "http-body-util", "hyper 0.14.28", @@ -2261,7 +2350,6 @@ dependencies = [ "hyper-util", "libc", "log", - "netif", "nix", "node_resolver", "notify", @@ -2272,8 +2360,6 @@ dependencies = [ "rustyline", "same-file", "serde", - "signal-hook", - "signal-hook-registry", "sys_traits", "tempfile", "test_server", @@ -2304,6 +2390,13 @@ dependencies = [ "url", ] +[[package]] +name = "deno_snapshots" +version = "0.2.0" +dependencies = [ + "deno_runtime", +] + [[package]] name = "deno_task_shell" version = "0.20.2" @@ -2324,13 +2417,15 @@ dependencies = [ [[package]] name = "deno_telemetry" -version = "0.7.0" +version = "0.9.0" dependencies = [ "async-trait", "deno_core", "deno_error", + "deno_tls", "http-body-util", "hyper 1.4.1", + "hyper-rustls", "hyper-util", "log", "once_cell", @@ -2367,7 +2462,7 @@ dependencies = [ [[package]] name = "deno_tls" -version = "0.172.0" +version = "0.174.0" dependencies = [ "deno_core", "deno_error", @@ -2418,7 +2513,7 @@ dependencies = [ [[package]] name = "deno_url" -version = "0.185.0" +version = "0.187.0" dependencies = [ "deno_bench_util", "deno_console", @@ -2431,7 +2526,7 @@ dependencies = [ [[package]] name = "deno_web" -version = "0.216.0" +version = "0.218.0" dependencies = [ "async-trait", "base64-simd 0.8.0", @@ -2454,7 +2549,7 @@ dependencies = [ [[package]] name = "deno_webgpu" -version = "0.152.0" +version = "0.154.0" dependencies = [ "deno_core", "deno_error", @@ -2468,7 +2563,7 @@ dependencies = [ [[package]] name = "deno_webidl" -version = "0.185.0" +version = "0.187.0" dependencies = [ "deno_bench_util", "deno_core", @@ -2476,7 +2571,7 @@ dependencies = [ [[package]] name = "deno_websocket" -version = "0.190.0" +version = "0.192.0" dependencies = [ "bytes", "deno_core", @@ -2499,7 +2594,7 @@ dependencies = [ [[package]] name = "deno_webstorage" -version = "0.180.0" +version = "0.182.0" dependencies = [ "deno_core", "deno_error", @@ -2585,6 +2680,43 @@ dependencies = [ "v8_valueserializer", ] +[[package]] +name = "denort" +version = "2.1.7" +dependencies = [ + "async-trait", + "bincode", + "deno_cache_dir", + "deno_config", + "deno_core", + "deno_error", + "deno_lib", + "deno_media_type", + "deno_npm", + "deno_package_json", + "deno_path_util", + "deno_resolver", + "deno_runtime", + "deno_semver", + "deno_snapshots", + "deno_terminal 0.2.0", + "import_map", + "indexmap 2.3.0", + "libsui", + "log", + "node_resolver", + "pretty_assertions", + "serde", + "serde_json", + "sys_traits", + "test_server", + "thiserror 2.0.3", + "tokio", + "tokio-util", + "twox-hash", + "url", +] + [[package]] name = "der" version = "0.7.9" @@ -3114,17 +3246,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - [[package]] name = "errno" version = "0.3.8" @@ -3135,16 +3256,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "error-code" version = "3.2.0" @@ -3464,9 +3575,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -3474,9 +3585,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -3491,15 +3602,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -3508,21 +3619,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -5071,7 +5182,7 @@ dependencies = [ [[package]] name = "napi_sym" -version = "0.115.0" +version = "0.117.0" dependencies = [ "quote", "serde", @@ -5126,13 +5237,12 @@ dependencies = [ [[package]] name = "node_resolver" -version = "0.23.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", "boxed_error", "deno_error", - "deno_media_type", "deno_package_json", "deno_path_util", "futures", @@ -5140,6 +5250,7 @@ dependencies = [ "once_cell", "path-clean", "regex", + "serde", "serde_json", "sys_traits", "thiserror 2.0.3", @@ -5606,20 +5717,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.9" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 1.0.64", + "thiserror 2.0.3", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.9" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" dependencies = [ "pest", "pest_generator", @@ -5627,9 +5738,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.9" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" dependencies = [ "pest", "pest_meta", @@ -5640,9 +5751,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.9" +version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" dependencies = [ "once_cell", "pest", @@ -6537,7 +6648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags 2.6.0", - "errno 0.3.8", + "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", @@ -6872,9 +6983,9 @@ dependencies = [ [[package]] name = "serde_v8" -version = "0.239.0" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa6d882827148e5d9052d9d8d6d1c9d6ad426ed00cab46cafb8c07a0e7126a" +checksum = "cd0494d74c40ab94f53a19485de359ea6a55f05341b817b93440b673c1ce8ec6" dependencies = [ "deno_error", "num-bigint", @@ -8468,9 +8579,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", diff --git a/Cargo.toml b/Cargo.toml index 48abe48305..c009bc3c5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ resolver = "2" members = [ "bench_util", "cli", + "cli/lib", + "cli/rt", + "cli/snapshot", "ext/broadcast_channel", "ext/cache", "ext/canvas", @@ -48,19 +51,19 @@ repository = "https://github.com/denoland/deno" [workspace.dependencies] deno_ast = { version = "=0.44.0", features = ["transpiling"] } -deno_core = { version = "0.330.0" } +deno_core = { version = "0.331.0" } -deno_bench_util = { version = "0.179.0", path = "./bench_util" } -deno_config = { version = "=0.43.0", features = ["workspace", "sync"] } +deno_bench_util = { version = "0.181.0", path = "./bench_util" } +deno_config = { version = "=0.45.0", features = ["workspace", "sync"] } deno_lockfile = "=0.24.0" -deno_media_type = { version = "0.2.3", features = ["module_specifier"] } -deno_npm = "=0.27.0" +deno_media_type = { version = "=0.2.5", features = ["module_specifier"] } +deno_npm = "=0.27.2" deno_path_util = "=0.3.0" -deno_permissions = { version = "0.44.0", path = "./runtime/permissions" } -deno_runtime = { version = "0.193.0", path = "./runtime" } +deno_permissions = { version = "0.46.0", path = "./runtime/permissions" } +deno_runtime = { version = "0.195.0", path = "./runtime" } deno_semver = "=0.7.1" deno_terminal = "0.2.0" -napi_sym = { version = "0.115.0", path = "./ext/napi/sym" } +napi_sym = { version = "0.117.0", path = "./ext/napi/sym" } test_util = { package = "test_server", path = "./tests/util/server" } denokv_proto = "0.9.0" @@ -69,34 +72,38 @@ denokv_remote = "0.9.0" denokv_sqlite = { default-features = false, version = "0.9.0" } # exts -deno_broadcast_channel = { version = "0.179.0", path = "./ext/broadcast_channel" } -deno_cache = { version = "0.117.0", path = "./ext/cache" } -deno_canvas = { version = "0.54.0", path = "./ext/canvas" } -deno_console = { version = "0.185.0", path = "./ext/console" } -deno_cron = { version = "0.65.0", path = "./ext/cron" } -deno_crypto = { version = "0.199.0", path = "./ext/crypto" } -deno_fetch = { version = "0.209.0", path = "./ext/fetch" } -deno_ffi = { version = "0.172.0", path = "./ext/ffi" } -deno_fs = { version = "0.95.0", path = "./ext/fs" } -deno_http = { version = "0.183.0", path = "./ext/http" } -deno_io = { version = "0.95.0", path = "./ext/io" } -deno_kv = { version = "0.93.0", path = "./ext/kv" } -deno_napi = { version = "0.116.0", path = "./ext/napi" } -deno_net = { version = "0.177.0", path = "./ext/net" } -deno_node = { version = "0.123.0", path = "./ext/node" } -deno_telemetry = { version = "0.7.0", path = "./ext/telemetry" } -deno_tls = { version = "0.172.0", path = "./ext/tls" } -deno_url = { version = "0.185.0", path = "./ext/url" } -deno_web = { version = "0.216.0", path = "./ext/web" } -deno_webgpu = { version = "0.152.0", path = "./ext/webgpu" } -deno_webidl = { version = "0.185.0", path = "./ext/webidl" } -deno_websocket = { version = "0.190.0", path = "./ext/websocket" } -deno_webstorage = { version = "0.180.0", path = "./ext/webstorage" } +deno_broadcast_channel = { version = "0.181.0", path = "./ext/broadcast_channel" } +deno_cache = { version = "0.119.0", path = "./ext/cache" } +deno_canvas = { version = "0.56.0", path = "./ext/canvas" } +deno_console = { version = "0.187.0", path = "./ext/console" } +deno_cron = { version = "0.67.0", path = "./ext/cron" } +deno_crypto = { version = "0.201.0", path = "./ext/crypto" } +deno_fetch = { version = "0.211.0", path = "./ext/fetch" } +deno_ffi = { version = "0.174.0", path = "./ext/ffi" } +deno_fs = { version = "0.97.0", path = "./ext/fs" } +deno_http = { version = "0.185.0", path = "./ext/http" } +deno_io = { version = "0.97.0", path = "./ext/io" } +deno_kv = { version = "0.95.0", path = "./ext/kv" } +deno_napi = { version = "0.118.0", path = "./ext/napi" } +deno_net = { version = "0.179.0", path = "./ext/net" } +deno_node = { version = "0.125.0", path = "./ext/node" } +deno_os = { version = "0.4.0", path = "./ext/os" } +deno_process = { version = "0.2.0", path = "./ext/process" } +deno_telemetry = { version = "0.9.0", path = "./ext/telemetry" } +deno_tls = { version = "0.174.0", path = "./ext/tls" } +deno_url = { version = "0.187.0", path = "./ext/url" } +deno_web = { version = "0.218.0", path = "./ext/web" } +deno_webgpu = { version = "0.154.0", path = "./ext/webgpu" } +deno_webidl = { version = "0.187.0", path = "./ext/webidl" } +deno_websocket = { version = "0.192.0", path = "./ext/websocket" } +deno_webstorage = { version = "0.182.0", path = "./ext/webstorage" } -# resolvers -deno_npm_cache = { version = "0.4.0", path = "./resolvers/npm_cache" } -deno_resolver = { version = "0.16.0", path = "./resolvers/deno" } -node_resolver = { version = "0.23.0", path = "./resolvers/node" } +# workspace libraries +deno_lib = { version = "0.3.0", path = "./cli/lib" } +deno_npm_cache = { version = "0.6.0", path = "./resolvers/npm_cache" } +deno_resolver = { version = "0.18.0", path = "./resolvers/deno" } +deno_snapshots = { version = "0.2.0", path = "./cli/snapshot" } +node_resolver = { version = "0.25.0", path = "./resolvers/node" } aes = "=0.8.3" anyhow = "1.0.57" @@ -119,7 +126,7 @@ dashmap = "5.5.3" data-encoding = "2.3.3" data-url = "=0.3.1" deno_cache_dir = "=0.16.0" -deno_error = "=0.5.3" +deno_error = "=0.5.5" deno_package_json = { version = "0.4.0", default-features = false } deno_unsync = "0.4.2" dlopen2 = "0.6.1" @@ -150,6 +157,7 @@ ipnet = "2.3" jsonc-parser = { version = "=0.26.2", features = ["serde"] } lazy-regex = "3" libc = "0.2.168" +libsui = "0.5.0" 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 diff --git a/README.md b/README.md index 19d4fa8a12..ca71529e28 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ the deno mascot dinosaur standing in the rain -[Deno](https://www.deno.com) -([/ˈdiːnoʊ/](http://ipa-reader.xyz/?text=%CB%88di%CB%90no%CA%8A), pronounced +[Deno](https://deno.com) +([/ˈdiːnoʊ/](https://ipa-reader.com/?text=%CB%88di%CB%90no%CA%8A), pronounced `dee-no`) is a JavaScript, TypeScript, and WebAssembly runtime with secure defaults and a great developer experience. It's built on [V8](https://v8.dev/), [Rust](https://www.rust-lang.org/), and [Tokio](https://tokio.rs/). diff --git a/Releases.md b/Releases.md index 71cf524ca2..d21aef3b28 100644 --- a/Releases.md +++ b/Releases.md @@ -6,6 +6,45 @@ https://github.com/denoland/deno/releases We also have one-line install commands at: https://github.com/denoland/deno_install +### 2.1.7 / 2025.01.21 + +- fix(deps): update yanked crates (#27512) +- fix(ext/node): GCM auth tag check on DechiperIv#final (#27733) +- fix(ext/node): add FileHandle#sync (#27677) +- fix(ext/node): propagate socket error to client request object (#27678) +- fix(ext/node): tls.connect regression (#27707) +- fix(ext/os): pass SignalState to web worker (#27741) +- fix(install/global): remove importMap field from specified config file + (#27744) +- fix: use 'getrandom' feature for 'sys_traits' crate +- perf(compile): remove swc from denort (#27721) + +### 2.1.6 / 2025.01.16 + +- fix(check/lsp): correctly resolve compilerOptions.types (#27686) +- fix(check/lsp): fix bugs with tsc type resolution, allow npm packages to + augment `ImportMeta` (#27690) +- fix(compile): store embedded fs case sensitivity (#27653) +- fix(compile/windows): better handling of deno_dir on different drive letter + than code (#27654) +- fix(ext/console): change Temporal color (#27684) +- fix(ext/node): add `writev` method to `FileHandle` (#27563) +- fix(ext/node): add chown method to FileHandle class (#27638) +- fix(ext/node): apply `@npmcli/agent` workaround to `npm-check-updates` + (#27639) +- fix(ext/node): fix playwright http client (#27662) +- fix(ext/node): show bare-node-builtin hint when using an import map (#27632) +- fix(ext/node): use primordials in `ext/node/polyfills/_fs_common.ts` (#27589) +- fix(lsp): handle pathless untitled URIs (#27637) +- fix(lsp/check): don't resolve unknown media types to a `.js` extension + (#27631) +- fix(node): Prevent node:child_process from always inheriting the parent + environment (#27343) (#27340) +- fix(node/fs): add utimes method to the FileHandle class (#27582) +- fix(outdated): Use `latest` tag even when it's the same as the current version + (#27699) +- fix(outdated): retain strict semver specifier when updating (#27701) + ### 2.1.5 / 2025.01.09 - feat(unstable): implement QUIC (#21942) diff --git a/bench_util/Cargo.toml b/bench_util/Cargo.toml index 30a88e931e..3d54b07d99 100644 --- a/bench_util/Cargo.toml +++ b/bench_util/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno_bench_util" -version = "0.179.0" +version = "0.181.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8248d40701..a9b0b64577 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "deno" -version = "2.1.5" +version = "2.1.7" authors.workspace = true default-run = "deno" edition.workspace = true @@ -16,11 +16,6 @@ name = "deno" path = "main.rs" doc = false -[[bin]] -name = "denort" -path = "mainrt.rs" -doc = false - [[test]] name = "integration" path = "integration_tests_runner.rs" @@ -49,7 +44,7 @@ dhat-heap = ["dhat"] upgrade = [] # A dev feature to disable creations and loading of snapshots in favor of # loading JS sources at runtime. -hmr = ["deno_runtime/hmr"] +hmr = ["deno_runtime/hmr", "deno_snapshots/disable"] # Vendor zlib as zlib-ng __vendored_zlib_ng = ["flate2/zlib-ng-compat", "libz-sys/zlib-ng"] @@ -60,10 +55,12 @@ lazy-regex.workspace = true serde.workspace = true serde_json.workspace = true zstd.workspace = true -glibc_version = "0.1.2" flate2 = { workspace = true, features = ["default"] } deno_error.workspace = true +[target.'cfg(unix)'.build-dependencies] +glibc_version = "0.1.2" + [target.'cfg(windows)'.build-dependencies] winapi.workspace = true winres.workspace = true @@ -76,8 +73,10 @@ deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] deno_doc = { version = "=0.164.0", features = ["rust", "comrak"] } deno_error.workspace = true deno_graph = { version = "=0.87.0" } -deno_lint = { version = "=0.68.2", features = ["docs"] } +deno_lib.workspace = true +deno_lint = { version = "=0.68.2" } deno_lockfile.workspace = true +deno_media_type = { workspace = true, features = ["data_url", "decoding", "module_specifier"] } deno_npm.workspace = true deno_npm_cache.workspace = true deno_package_json.workspace = true @@ -85,10 +84,11 @@ deno_path_util.workspace = true deno_resolver = { workspace = true, features = ["sync"] } deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] } deno_semver.workspace = true +deno_snapshots = { workspace = true } deno_task_shell = "=0.20.2" deno_telemetry.workspace = true deno_terminal.workspace = true -libsui = "0.5.0" +libsui.workspace = true node_resolver.workspace = true anstream = "0.6.14" @@ -114,7 +114,6 @@ dprint-plugin-json = "=0.19.4" dprint-plugin-jupyter = "=0.1.5" dprint-plugin-markdown = "=0.17.8" dprint-plugin-typescript = "=0.93.3" -env_logger = "=0.10.0" fancy-regex = "=0.10.0" faster-hex.workspace = true # If you disable the default __vendored_zlib_ng feature above, you _must_ be able to link against `-lz`. @@ -155,7 +154,6 @@ rustyline-derive = "=0.7.0" serde.workspace = true serde_repr.workspace = true sha2.workspace = true -shell-escape = "=0.1.5" spki = { version = "0.7", features = ["pem"] } sqlformat = "=0.3.2" strsim = "0.11.1" @@ -184,6 +182,7 @@ winapi = { workspace = true, features = ["knownfolders", "mswsock", "objbase", " [target.'cfg(unix)'.dependencies] nix.workspace = true +shell-escape = "=0.1.5" [dev-dependencies] deno_bench_util.workspace = true diff --git a/cli/args/flags.rs b/cli/args/flags.rs index fb64b4eeaa..0ddd04da2d 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -31,6 +31,9 @@ use deno_core::error::AnyError; use deno_core::resolve_url_or_path; use deno_core::url::Url; use deno_graph::GraphKind; +use deno_lib::args::CaData; +use deno_lib::args::UnstableConfig; +use deno_lib::version::DENO_VERSION_INFO; use deno_path_util::normalize_path; use deno_path_util::url_to_file_path; use deno_runtime::deno_permissions::SysDescriptor; @@ -546,15 +549,6 @@ impl Default for TypeCheckMode { } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum CaData { - /// The string is a file path - File(String), - /// This variant is not exposed as an option in the CLI, it is used internally - /// for standalone binaries. - Bytes(Vec), -} - // Info needed to run NPM lifecycle scripts #[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct LifecycleScriptsConfig { @@ -582,19 +576,6 @@ fn parse_packages_allowed_scripts(s: &str) -> Result { } } -#[derive( - Clone, Default, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, -)] -pub struct UnstableConfig { - // TODO(bartlomieju): remove in Deno 2.5 - pub legacy_flag_enabled: bool, // --unstable - pub bare_node_builtins: bool, - pub detect_cjs: bool, - pub sloppy_imports: bool, - pub npm_lazy_caching: 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 @@ -1484,14 +1465,15 @@ fn handle_repl_flags(flags: &mut Flags, repl_flags: ReplFlags) { } pub fn clap_root() -> Command { + debug_assert_eq!(DENO_VERSION_INFO.typescript, deno_snapshots::TS_VERSION); let long_version = format!( "{} ({}, {}, {})\nv8 {}\ntypescript {}", - crate::version::DENO_VERSION_INFO.deno, - crate::version::DENO_VERSION_INFO.release_channel.name(), + DENO_VERSION_INFO.deno, + DENO_VERSION_INFO.release_channel.name(), env!("PROFILE"), env!("TARGET"), deno_core::v8::VERSION_STRING, - crate::version::DENO_VERSION_INFO.typescript + DENO_VERSION_INFO.typescript ); run_args(Command::new("deno"), true) @@ -1507,7 +1489,7 @@ pub fn clap_root() -> Command { ) .color(ColorChoice::Auto) .term_width(800) - .version(crate::version::DENO_VERSION_INFO.deno) + .version(DENO_VERSION_INFO.deno) .long_version(long_version) .disable_version_flag(true) .disable_help_flag(true) @@ -4274,7 +4256,7 @@ impl CommandExt for Command { 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 + .help(cstr!("The `--unstable` flag has been deprecated. Use granular `--unstable-*` flags instead To view the list of individual unstable feature flags, run this command again with --help=unstable")) .action(ArgAction::SetTrue) .hide(matches!(cfg, UnstableArgsConfig::None)) diff --git a/cli/args/import_map.rs b/cli/args/import_map.rs index ff7e42ef20..75a1611120 100644 --- a/cli/args/import_map.rs +++ b/cli/args/import_map.rs @@ -13,7 +13,7 @@ pub async fn resolve_import_map_value_from_specifier( ) -> Result { if specifier.scheme() == "data" { let data_url_text = - deno_graph::source::RawDataUrl::parse(specifier)?.decode()?; + deno_media_type::data_url::RawDataUrl::parse(specifier)?.decode()?; Ok(serde_json::from_str(&data_url_text)?) } else { let file = TextDecodedFile::decode( diff --git a/cli/args/lockfile.rs b/cli/args/lockfile.rs index 976992aac8..5fa6a49c43 100644 --- a/cli/args/lockfile.rs +++ b/cli/args/lockfile.rs @@ -61,11 +61,13 @@ impl<'a, T> std::ops::DerefMut for Guard<'a, T> { } #[derive(Debug, thiserror::Error, deno_error::JsError)] -#[error("Failed writing lockfile")] -#[class(inherit)] -struct AtomicWriteFileWithRetriesError { - #[source] - source: std::io::Error, +pub enum AtomicWriteFileWithRetriesError { + #[class(inherit)] + #[error(transparent)] + Changed(JsErrorBox), + #[class(inherit)] + #[error("Failed writing lockfile")] + Io(#[source] std::io::Error), } impl CliLockfile { @@ -87,12 +89,16 @@ impl CliLockfile { self.lockfile.lock().overwrite } - pub fn write_if_changed(&self) -> Result<(), JsErrorBox> { + pub fn write_if_changed( + &self, + ) -> Result<(), AtomicWriteFileWithRetriesError> { if self.skip_write { return Ok(()); } - self.error_if_changed()?; + self + .error_if_changed() + .map_err(AtomicWriteFileWithRetriesError::Changed)?; let mut lockfile = self.lockfile.lock(); let Some(bytes) = lockfile.resolve_write_bytes() else { return Ok(()); // nothing to do @@ -105,9 +111,7 @@ impl CliLockfile { &bytes, cache::CACHE_PERM, ) - .map_err(|source| { - JsErrorBox::from_err(AtomicWriteFileWithRetriesError { source }) - })?; + .map_err(AtomicWriteFileWithRetriesError::Io)?; lockfile.has_content_changed = false; Ok(()) } diff --git a/cli/args/mod.rs b/cli/args/mod.rs index ebd321a20a..e4f332a8bc 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -10,10 +10,6 @@ mod package_json; use std::borrow::Cow; use std::collections::HashMap; use std::env; -use std::io::BufReader; -use std::io::Cursor; -use std::io::Read; -use std::io::Seek; use std::net::SocketAddr; use std::num::NonZeroUsize; use std::path::Path; @@ -56,9 +52,15 @@ use deno_core::error::AnyError; use deno_core::resolve_url_or_path; use deno_core::serde_json; use deno_core::url::Url; -use deno_error::JsErrorBox; use deno_graph::GraphKind; pub use deno_json::check_warn_tsconfig; +use deno_lib::args::has_flag_env_var; +use deno_lib::args::npm_pkg_req_ref_to_binary_command; +use deno_lib::args::CaData; +use deno_lib::args::NpmProcessStateKind; +use deno_lib::args::NPM_PROCESS_STATE; +use deno_lib::version::DENO_VERSION_INFO; +use deno_lib::worker::StorageKeyResolver; use deno_lint::linter::LintConfig as DenoLintConfig; use deno_npm::npm_rc::NpmRc; use deno_npm::npm_rc::ResolvedNpmRc; @@ -66,27 +68,20 @@ use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use deno_npm::NpmSystemInfo; use deno_path_util::normalize_path; use deno_runtime::deno_permissions::PermissionsOptions; -use deno_runtime::deno_tls::deno_native_certs::load_native_certs; -use deno_runtime::deno_tls::rustls; -use deno_runtime::deno_tls::rustls::RootCertStore; -use deno_runtime::deno_tls::rustls_pemfile; -use deno_runtime::deno_tls::webpki_roots; use deno_runtime::inspector_server::InspectorServer; use deno_semver::npm::NpmPackageReqReference; use deno_semver::StackString; use deno_telemetry::OtelConfig; -use deno_telemetry::OtelRuntimeConfig; use deno_terminal::colors; use dotenvy::from_filename; pub use flags::*; use import_map::resolve_import_map_value_from_specifier; +pub use lockfile::AtomicWriteFileWithRetriesError; pub use lockfile::CliLockfile; pub use lockfile::CliLockfileReadFromPathOptions; use once_cell::sync::Lazy; pub use package_json::NpmInstallDepsProvider; pub use package_json::PackageJsonDepValueParseWithLocationError; -use serde::Deserialize; -use serde::Serialize; use sys_traits::EnvHomeDir; use thiserror::Error; @@ -94,7 +89,6 @@ use crate::cache::DenoDirProvider; use crate::file_fetcher::CliFileFetcher; use crate::sys::CliSys; use crate::util::fs::canonicalize_path_maybe_not_exists; -use crate::version; pub fn npm_registry_url() -> &'static Url { static NPM_REGISTRY_DEFAULT_URL: Lazy = Lazy::new(|| { @@ -606,147 +600,6 @@ pub fn create_default_npmrc() -> Arc { }) } -#[derive(Error, Debug, Clone, deno_error::JsError)] -#[class(generic)] -pub enum RootCertStoreLoadError { - #[error( - "Unknown certificate store \"{0}\" specified (allowed: \"system,mozilla\")" - )] - UnknownStore(String), - #[error("Unable to add pem file to certificate store: {0}")] - FailedAddPemFile(String), - #[error("Failed opening CA file: {0}")] - CaFileOpenError(String), -} - -/// Create and populate a root cert store based on the passed options and -/// environment. -pub fn get_root_cert_store( - maybe_root_path: Option, - maybe_ca_stores: Option>, - maybe_ca_data: Option, -) -> Result { - let mut root_cert_store = RootCertStore::empty(); - let ca_stores: Vec = maybe_ca_stores - .or_else(|| { - let env_ca_store = env::var("DENO_TLS_CA_STORE").ok()?; - Some( - env_ca_store - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(), - ) - }) - .unwrap_or_else(|| vec!["mozilla".to_string()]); - - for store in ca_stores.iter() { - match store.as_str() { - "mozilla" => { - root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.to_vec()); - } - "system" => { - let roots = load_native_certs().expect("could not load platform certs"); - for root in roots { - if let Err(err) = root_cert_store - .add(rustls::pki_types::CertificateDer::from(root.0.clone())) - { - log::error!( - "{}", - colors::yellow(&format!( - "Unable to add system certificate to certificate store: {:?}", - err - )) - ); - let hex_encoded_root = faster_hex::hex_string(&root.0); - log::error!("{}", colors::gray(&hex_encoded_root)); - } - } - } - _ => { - return Err(RootCertStoreLoadError::UnknownStore(store.clone())); - } - } - } - - let ca_data = - maybe_ca_data.or_else(|| env::var("DENO_CERT").ok().map(CaData::File)); - if let Some(ca_data) = ca_data { - let result = match ca_data { - CaData::File(ca_file) => { - let ca_file = if let Some(root) = &maybe_root_path { - root.join(&ca_file) - } else { - PathBuf::from(ca_file) - }; - let certfile = std::fs::File::open(ca_file).map_err(|err| { - RootCertStoreLoadError::CaFileOpenError(err.to_string()) - })?; - let mut reader = BufReader::new(certfile); - rustls_pemfile::certs(&mut reader).collect::, _>>() - } - CaData::Bytes(data) => { - let mut reader = BufReader::new(Cursor::new(data)); - rustls_pemfile::certs(&mut reader).collect::, _>>() - } - }; - - match result { - Ok(certs) => { - root_cert_store.add_parsable_certificates(certs); - } - Err(e) => { - return Err(RootCertStoreLoadError::FailedAddPemFile(e.to_string())); - } - } - } - - Ok(root_cert_store) -} - -/// State provided to the process via an environment variable. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct NpmProcessState { - pub kind: NpmProcessStateKind, - pub local_node_modules_path: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum NpmProcessStateKind { - Snapshot(deno_npm::resolution::SerializedNpmResolutionSnapshot), - Byonm, -} - -static NPM_PROCESS_STATE: Lazy> = Lazy::new(|| { - use deno_runtime::ops::process::NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME; - let fd = std::env::var(NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME).ok()?; - std::env::remove_var(NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME); - let fd = fd.parse::().ok()?; - let mut file = { - use deno_runtime::deno_io::FromRawIoHandle; - unsafe { std::fs::File::from_raw_io_handle(fd as _) } - }; - let mut buf = Vec::new(); - // seek to beginning. after the file is written the position will be inherited by this subprocess, - // and also this file might have been read before - file.seek(std::io::SeekFrom::Start(0)).unwrap(); - file - .read_to_end(&mut buf) - .inspect_err(|e| { - log::error!("failed to read npm process state from fd {fd}: {e}"); - }) - .ok()?; - let state: NpmProcessState = serde_json::from_slice(&buf) - .inspect_err(|e| { - log::error!( - "failed to deserialize npm process state: {e} {}", - String::from_utf8_lossy(&buf) - ) - }) - .ok()?; - Some(state) -}); - /// Overrides for the options below that when set will /// use these values over the values derived from the /// CLI flags or config file. @@ -842,8 +695,6 @@ impl CliOptions { } else { &[] }; - let config_parse_options = - deno_config::deno_json::ConfigParseOptions::default(); let discover_pkg_json = flags.config_flag != ConfigFlag::Disabled && !flags.no_npm && !has_flag_env_var("DENO_NO_PACKAGE_JSON"); @@ -854,7 +705,6 @@ impl CliOptions { deno_json_cache: None, pkg_json_cache: Some(&node_resolver::PackageJsonThreadLocalCache), workspace_cache: None, - config_parse_options, additional_config_file_names, discover_pkg_json, maybe_vendor_override, @@ -1103,11 +953,11 @@ impl CliOptions { } }; Ok(self.workspace().create_resolver( + &CliSys::default(), CreateResolverOptions { pkg_json_dep_resolution, specified_import_map: cli_arg_specified_import_map, }, - |path| std::fs::read_to_string(path).map_err(JsErrorBox::from_err), )?) } @@ -1231,6 +1081,16 @@ impl CliOptions { } } + pub fn resolve_storage_key_resolver(&self) -> StorageKeyResolver { + if let Some(location) = &self.flags.location { + StorageKeyResolver::from_flag(location) + } else if let Some(deno_json) = self.start_dir.maybe_deno_json() { + StorageKeyResolver::from_config_file_url(&deno_json.specifier) + } else { + StorageKeyResolver::new_use_main_module() + } + } + // If the main module should be treated as being in an npm package. // This is triggered via a secret environment variable which is used // for functionality like child_process.fork. Users should NOT depend @@ -1285,7 +1145,7 @@ impl CliOptions { Ok(Some(InspectorServer::new( host, - version::DENO_VERSION_INFO.user_agent, + DENO_VERSION_INFO.user_agent, )?)) } @@ -1979,72 +1839,11 @@ fn resolve_import_map_specifier( } } -pub struct StorageKeyResolver(Option>); - -impl StorageKeyResolver { - pub fn from_options(options: &CliOptions) -> Self { - Self(if let Some(location) = &options.flags.location { - // if a location is set, then the ascii serialization of the location is - // used, unless the origin is opaque, and then no storage origin is set, as - // we can't expect the origin to be reproducible - let storage_origin = location.origin(); - if storage_origin.is_tuple() { - Some(Some(storage_origin.ascii_serialization())) - } else { - Some(None) - } - } else { - // otherwise we will use the path to the config file or None to - // fall back to using the main module's path - options - .start_dir - .maybe_deno_json() - .map(|config_file| Some(config_file.specifier.to_string())) - }) - } - - /// Creates a storage key resolver that will always resolve to being empty. - pub fn empty() -> Self { - Self(Some(None)) - } - - /// Resolves the storage key to use based on the current flags, config, or main module. - pub fn resolve_storage_key( - &self, - main_module: &ModuleSpecifier, - ) -> Option { - // use the stored value or fall back to using the path of the main module. - if let Some(maybe_value) = &self.0 { - maybe_value.clone() - } else { - Some(main_module.to_string()) - } - } -} - /// Resolves the no_prompt value based on the cli flags and environment. pub fn resolve_no_prompt(flags: &PermissionFlags) -> bool { flags.no_prompt || has_flag_env_var("DENO_NO_PROMPT") } -pub fn has_trace_permissions_enabled() -> bool { - has_flag_env_var("DENO_TRACE_PERMISSIONS") -} - -pub fn has_flag_env_var(name: &str) -> bool { - let value = env::var(name); - matches!(value.as_ref().map(|s| s.as_str()), Ok("1")) -} - -pub fn npm_pkg_req_ref_to_binary_command( - req_ref: &NpmPackageReqReference, -) -> String { - req_ref - .sub_path() - .map(|s| s.to_string()) - .unwrap_or_else(|| req_ref.req().name.to_string()) -} - pub fn config_to_deno_graph_workspace_member( config: &ConfigFile, ) -> Result { @@ -2105,13 +1904,6 @@ pub enum NpmCachingStrategy { Manual, } -pub fn otel_runtime_config() -> OtelRuntimeConfig { - OtelRuntimeConfig { - runtime_name: Cow::Borrowed("deno"), - runtime_version: Cow::Borrowed(crate::version::DENO_VERSION_INFO.deno), - } -} - #[cfg(test)] mod test { use pretty_assertions::assert_eq; @@ -2126,12 +1918,7 @@ mod test { let cwd = &std::env::current_dir().unwrap(); let config_specifier = ModuleSpecifier::parse("file:///deno/deno.jsonc").unwrap(); - let config_file = ConfigFile::new( - config_text, - config_specifier, - &deno_config::deno_json::ConfigParseOptions::default(), - ) - .unwrap(); + let config_file = ConfigFile::new(config_text, config_specifier).unwrap(); let actual = resolve_import_map_specifier( Some("import-map.json"), Some(&config_file), @@ -2150,12 +1937,7 @@ mod test { let config_text = r#"{}"#; let config_specifier = ModuleSpecifier::parse("file:///deno/deno.jsonc").unwrap(); - let config_file = ConfigFile::new( - config_text, - config_specifier, - &deno_config::deno_json::ConfigParseOptions::default(), - ) - .unwrap(); + let config_file = ConfigFile::new(config_text, config_specifier).unwrap(); let actual = resolve_import_map_specifier( None, Some(&config_file), @@ -2174,27 +1956,6 @@ mod test { assert_eq!(actual, None); } - #[test] - fn storage_key_resolver_test() { - let resolver = StorageKeyResolver(None); - let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap(); - assert_eq!( - resolver.resolve_storage_key(&specifier), - Some(specifier.to_string()) - ); - let resolver = StorageKeyResolver(Some(None)); - assert_eq!(resolver.resolve_storage_key(&specifier), None); - let resolver = StorageKeyResolver(Some(Some("value".to_string()))); - assert_eq!( - resolver.resolve_storage_key(&specifier), - Some("value".to_string()) - ); - - // test empty - let resolver = StorageKeyResolver::empty(); - assert_eq!(resolver.resolve_storage_key(&specifier), None); - } - #[test] fn jsr_urls() { let reg_url = jsr_url(); diff --git a/cli/build.rs b/cli/build.rs index 590fee795d..742f227ec9 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -5,7 +5,6 @@ use std::path::PathBuf; use deno_core::snapshot::*; use deno_runtime::*; -mod shared; mod ts { use std::collections::HashMap; @@ -123,10 +122,16 @@ mod ts { deno_core::extension!(deno_tsc, ops = [op_build_info, op_is_node_file, op_load, op_script_version], + esm_entry_point = "ext:deno_tsc/99_main_compiler.js", + esm = [ + dir "tsc", + "97_ts_host.js", + "98_lsp.js", + "99_main_compiler.js", + ], js = [ dir "tsc", "00_typescript.js", - "99_main_compiler.js", ], options = { op_crate_libs: HashMap<&'static str, PathBuf>, @@ -310,57 +315,6 @@ mod ts { println!("cargo:rerun-if-changed={}", path.display()); } } - - pub(crate) fn version() -> String { - let file_text = std::fs::read_to_string("tsc/00_typescript.js").unwrap(); - let version_text = " version = \""; - for line in file_text.lines() { - if let Some(index) = line.find(version_text) { - let remaining_line = &line[index + version_text.len()..]; - return remaining_line[..remaining_line.find('"').unwrap()].to_string(); - } - } - panic!("Could not find ts version.") - } -} - -#[cfg(not(feature = "hmr"))] -fn create_cli_snapshot(snapshot_path: PathBuf) { - use deno_runtime::ops::bootstrap::SnapshotOptions; - - let snapshot_options = SnapshotOptions { - ts_version: ts::version(), - v8_version: deno_core::v8::VERSION_STRING, - target: std::env::var("TARGET").unwrap(), - }; - - deno_runtime::snapshot::create_runtime_snapshot( - snapshot_path, - snapshot_options, - vec![], - ); -} - -fn git_commit_hash() -> String { - if let Ok(output) = std::process::Command::new("git") - .arg("rev-list") - .arg("-1") - .arg("HEAD") - .output() - { - if output.status.success() { - std::str::from_utf8(&output.stdout[..40]) - .unwrap() - .to_string() - } else { - // When not in git repository - // (e.g. when the user install by `cargo install deno`) - "UNKNOWN".to_string() - } - } else { - // When there is no git command for some reason - "UNKNOWN".to_string() - } } fn main() { @@ -370,7 +324,7 @@ fn main() { } deno_napi::print_linker_flags("deno"); - deno_napi::print_linker_flags("denort"); + deno_webgpu::print_linker_flags("deno"); // Host snapshots won't work when cross compiling. let target = env::var("TARGET").unwrap(); @@ -389,51 +343,15 @@ fn main() { } println!("cargo:rerun-if-env-changed=DENO_CANARY"); - println!("cargo:rustc-env=GIT_COMMIT_HASH={}", git_commit_hash()); - println!("cargo:rerun-if-env-changed=GIT_COMMIT_HASH"); - println!( - "cargo:rustc-env=GIT_COMMIT_HASH_SHORT={}", - &git_commit_hash()[..7] - ); - - let ts_version = ts::version(); - debug_assert_eq!(ts_version, "5.6.2"); // bump this assertion when it changes - println!("cargo:rustc-env=TS_VERSION={}", ts_version); - println!("cargo:rerun-if-env-changed=TS_VERSION"); - println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap()); println!("cargo:rustc-env=PROFILE={}", env::var("PROFILE").unwrap()); - if cfg!(windows) { - // these dls load slowly, so delay loading them - let dlls = [ - // webgpu - "d3dcompiler_47", - "OPENGL32", - // network related functions - "iphlpapi", - ]; - for dll in dlls { - println!("cargo:rustc-link-arg-bin=deno=/delayload:{dll}.dll"); - println!("cargo:rustc-link-arg-bin=denort=/delayload:{dll}.dll"); - } - // enable delay loading - println!("cargo:rustc-link-arg-bin=deno=delayimp.lib"); - println!("cargo:rustc-link-arg-bin=denort=delayimp.lib"); - } - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let o = PathBuf::from(env::var_os("OUT_DIR").unwrap()); let compiler_snapshot_path = o.join("COMPILER_SNAPSHOT.bin"); ts::create_compiler_snapshot(compiler_snapshot_path, &c); - #[cfg(not(feature = "hmr"))] - { - let cli_snapshot_path = o.join("CLI_SNAPSHOT.bin"); - create_cli_snapshot(cli_snapshot_path); - } - #[cfg(target_os = "windows")] { let mut res = winres::WindowsResource::new(); diff --git a/cli/cache/cache_db.rs b/cli/cache/cache_db.rs index 7fd66e9333..6d6d967304 100644 --- a/cli/cache/cache_db.rs +++ b/cli/cache/cache_db.rs @@ -9,14 +9,13 @@ use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::parking_lot::MutexGuard; use deno_core::unsync::spawn_blocking; +use deno_lib::util::hash::FastInsecureHasher; use deno_runtime::deno_webstorage::rusqlite; use deno_runtime::deno_webstorage::rusqlite::Connection; use deno_runtime::deno_webstorage::rusqlite::OptionalExtension; use deno_runtime::deno_webstorage::rusqlite::Params; use once_cell::sync::OnceCell; -use super::FastInsecureHasher; - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct CacheDBHash(u64); diff --git a/cli/cache/caches.rs b/cli/cache/caches.rs index b83364c61b..fad61f1dc3 100644 --- a/cli/cache/caches.rs +++ b/cli/cache/caches.rs @@ -3,17 +3,18 @@ use std::path::PathBuf; use std::sync::Arc; +use deno_lib::version::DENO_VERSION_INFO; use once_cell::sync::OnceCell; use super::cache_db::CacheDB; use super::cache_db::CacheDBConfiguration; use super::check::TYPE_CHECK_CACHE_DB; use super::code_cache::CODE_CACHE_DB; -use super::deno_dir::DenoDirProvider; use super::fast_check::FAST_CHECK_CACHE_DB; use super::incremental::INCREMENTAL_CACHE_DB; use super::module_info::MODULE_INFO_CACHE_DB; use super::node::NODE_ANALYSIS_CACHE_DB; +use crate::cache::DenoDirProvider; pub struct Caches { dir_provider: Arc, @@ -48,13 +49,9 @@ impl Caches { cell .get_or_init(|| { if let Some(path) = path { - CacheDB::from_path( - config, - path, - crate::version::DENO_VERSION_INFO.deno, - ) + CacheDB::from_path(config, path, DENO_VERSION_INFO.deno) } else { - CacheDB::in_memory(config, crate::version::DENO_VERSION_INFO.deno) + CacheDB::in_memory(config, DENO_VERSION_INFO.deno) } }) .clone() diff --git a/cli/cache/code_cache.rs b/cli/cache/code_cache.rs index 27ec544b5f..d938732635 100644 --- a/cli/cache/code_cache.rs +++ b/cli/cache/code_cache.rs @@ -1,7 +1,5 @@ // Copyright 2018-2025 the Deno authors. MIT license. -use std::sync::Arc; - use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_runtime::code_cache; @@ -11,7 +9,6 @@ use super::cache_db::CacheDB; use super::cache_db::CacheDBConfiguration; use super::cache_db::CacheDBHash; use super::cache_db::CacheFailure; -use crate::worker::CliCodeCache; pub static CODE_CACHE_DB: CacheDBConfiguration = CacheDBConfiguration { table_initializer: concat!( @@ -85,12 +82,6 @@ impl CodeCache { } } -impl CliCodeCache for CodeCache { - fn as_code_cache(self: Arc) -> Arc { - self - } -} - impl code_cache::CodeCache for CodeCache { fn get_sync( &self, diff --git a/cli/cache/deno_dir.rs b/cli/cache/deno_dir.rs index 1b35f53071..5be08446cf 100644 --- a/cli/cache/deno_dir.rs +++ b/cli/cache/deno_dir.rs @@ -4,7 +4,6 @@ use std::env; use std::path::PathBuf; use deno_cache_dir::DenoDirResolutionError; -use once_cell::sync::OnceCell; use super::DiskCache; use crate::sys::CliSys; @@ -14,7 +13,7 @@ use crate::sys::CliSys; pub struct DenoDirProvider { sys: CliSys, maybe_custom_root: Option, - deno_dir: OnceCell>, + deno_dir: std::sync::OnceLock>, } impl DenoDirProvider { diff --git a/cli/cache/disk_cache.rs b/cli/cache/disk_cache.rs index f03b60854f..cb28c02922 100644 --- a/cli/cache/disk_cache.rs +++ b/cli/cache/disk_cache.rs @@ -9,11 +9,11 @@ use std::path::Prefix; use std::str; use deno_cache_dir::url_to_filename; +use deno_cache_dir::CACHE_PERM; use deno_core::url::Host; use deno_core::url::Url; use deno_path_util::fs::atomic_write_file_with_retries; -use super::CACHE_PERM; use crate::sys::CliSys; #[derive(Debug, Clone)] @@ -130,6 +130,9 @@ impl DiskCache { #[cfg(test)] mod tests { + // ok, testing + #[allow(clippy::disallowed_types)] + use sys_traits::impls::RealSys; use test_util::TempDir; use super::*; @@ -138,7 +141,7 @@ mod tests { fn test_set_get_cache_file() { let temp_dir = TempDir::new(); let sub_dir = temp_dir.path().join("sub_dir"); - let cache = DiskCache::new(CliSys::default(), &sub_dir.to_path_buf()); + let cache = DiskCache::new(RealSys, &sub_dir.to_path_buf()); let path = PathBuf::from("foo/bar.txt"); cache.set(&path, b"hello").unwrap(); assert_eq!(cache.get(&path).unwrap(), b"hello"); @@ -152,7 +155,7 @@ mod tests { PathBuf::from("/deno_dir/") }; - let cache = DiskCache::new(CliSys::default(), &cache_location); + let cache = DiskCache::new(RealSys, &cache_location); let mut test_cases = vec![ ( @@ -208,7 +211,7 @@ mod tests { } else { "/foo" }; - let cache = DiskCache::new(CliSys::default(), &PathBuf::from(p)); + let cache = DiskCache::new(RealSys, &PathBuf::from(p)); let mut test_cases = vec![ ( @@ -256,7 +259,7 @@ mod tests { PathBuf::from("/deno_dir/") }; - let cache = DiskCache::new(CliSys::default(), &cache_location); + let cache = DiskCache::new(RealSys, &cache_location); let mut test_cases = vec!["unknown://localhost/test.ts"]; diff --git a/cli/cache/emit.rs b/cli/cache/emit.rs index 2ba43d58b9..116454dd9a 100644 --- a/cli/cache/emit.rs +++ b/cli/cache/emit.rs @@ -6,6 +6,7 @@ use deno_ast::ModuleSpecifier; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::unsync::sync::AtomicFlag; +use deno_lib::version::DENO_VERSION_INFO; use super::DiskCache; @@ -23,7 +24,7 @@ impl EmitCache { disk_cache, emit_failed_flag: Default::default(), file_serializer: EmitFileSerializer { - cli_version: crate::version::DENO_VERSION_INFO.deno, + cli_version: DENO_VERSION_INFO.deno, }, } } @@ -147,7 +148,7 @@ impl EmitFileSerializer { // it's ok to use an insecure hash here because // if someone can change the emit source then they // can also change the version hash - crate::cache::FastInsecureHasher::new_without_deno_version() // use cli_version property instead + deno_lib::util::hash::FastInsecureHasher::new_without_deno_version() // use cli_version property instead .write(bytes) // emit should not be re-used between cli versions .write_str(self.cli_version) diff --git a/cli/cache/mod.rs b/cli/cache/mod.rs index ff9f07fc4e..695d21f3d1 100644 --- a/cli/cache/mod.rs +++ b/cli/cache/mod.rs @@ -15,6 +15,7 @@ use deno_graph::source::CacheInfo; use deno_graph::source::LoadFuture; use deno_graph::source::LoadResponse; use deno_graph::source::Loader; +use deno_resolver::npm::DenoInNpmPackageChecker; use deno_runtime::deno_permissions::PermissionsContainer; use node_resolver::InNpmPackageChecker; @@ -29,7 +30,6 @@ mod cache_db; mod caches; mod check; mod code_cache; -mod common; mod deno_dir; mod disk_cache; mod emit; @@ -43,7 +43,6 @@ pub use cache_db::CacheDBHash; pub use caches::Caches; pub use check::TypeCheckCache; pub use code_cache::CodeCache; -pub use common::FastInsecureHasher; /// Permissions used to save a file in the disk caches. pub use deno_cache_dir::CACHE_PERM; pub use deno_dir::DenoDir; @@ -76,7 +75,7 @@ pub struct FetchCacher { pub file_header_overrides: HashMap>, file_fetcher: Arc, global_http_cache: Arc, - in_npm_pkg_checker: Arc, + in_npm_pkg_checker: DenoInNpmPackageChecker, module_info_cache: Arc, permissions: PermissionsContainer, sys: CliSys, @@ -88,7 +87,7 @@ impl FetchCacher { pub fn new( file_fetcher: Arc, global_http_cache: Arc, - in_npm_pkg_checker: Arc, + in_npm_pkg_checker: DenoInNpmPackageChecker, module_info_cache: Arc, sys: CliSys, options: FetchCacherOptions, diff --git a/cli/emit.rs b/cli/emit.rs index e9b5a4e250..5e39df2874 100644 --- a/cli/emit.rs +++ b/cli/emit.rs @@ -20,15 +20,15 @@ use deno_error::JsErrorBox; use deno_graph::MediaType; use deno_graph::Module; use deno_graph::ModuleGraph; +use deno_lib::util::hash::FastInsecureHasher; use crate::cache::EmitCache; -use crate::cache::FastInsecureHasher; use crate::cache::ParsedSourceCache; -use crate::resolver::CjsTracker; +use crate::resolver::CliCjsTracker; #[derive(Debug)] pub struct Emitter { - cjs_tracker: Arc, + cjs_tracker: Arc, emit_cache: Arc, parsed_source_cache: Arc, transpile_and_emit_options: @@ -39,7 +39,7 @@ pub struct Emitter { impl Emitter { pub fn new( - cjs_tracker: Arc, + cjs_tracker: Arc, emit_cache: Arc, parsed_source_cache: Arc, transpile_options: deno_ast::TranspileOptions, @@ -112,9 +112,9 @@ impl Emitter { &self, specifier: &ModuleSpecifier, media_type: MediaType, - module_kind: deno_ast::ModuleKind, + module_kind: 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, module_kind, source) { diff --git a/cli/factory.rs b/cli/factory.rs index 5cc99830bc..91c5d07b75 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -11,9 +11,20 @@ use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::FeatureChecker; use deno_error::JsErrorBox; +use deno_lib::args::get_root_cert_store; +use deno_lib::args::CaData; +use deno_lib::loader::NpmModuleLoader; +use deno_lib::npm::create_npm_process_state_provider; +use deno_lib::npm::NpmRegistryReadPermissionChecker; +use deno_lib::npm::NpmRegistryReadPermissionCheckerMode; +use deno_lib::worker::LibMainWorkerFactory; +use deno_lib::worker::LibMainWorkerOptions; +use deno_npm_cache::NpmCacheSetting; use deno_resolver::cjs::IsCjsResolutionMode; use deno_resolver::npm::managed::ManagedInNpmPkgCheckerCreateOptions; +use deno_resolver::npm::managed::NpmResolutionCell; use deno_resolver::npm::CreateInNpmPkgCheckerOptions; +use deno_resolver::npm::DenoInNpmPackageChecker; use deno_resolver::npm::NpmReqResolverOptions; use deno_resolver::sloppy_imports::SloppyImportsCachedFs; use deno_resolver::DenoResolverOptions; @@ -30,17 +41,13 @@ 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 crate::args::check_warn_tsconfig; -use crate::args::get_root_cert_store; -use crate::args::CaData; use crate::args::CliOptions; use crate::args::DenoSubcommand; use crate::args::Flags; use crate::args::NpmInstallDepsProvider; -use crate::args::StorageKeyResolver; use crate::args::TsConfigType; use crate::cache::Caches; use crate::cache::CodeCache; @@ -66,21 +73,25 @@ use crate::node::CliCjsCodeAnalyzer; use crate::node::CliNodeCodeTranslator; use crate::node::CliNodeResolver; use crate::node::CliPackageJsonResolver; -use crate::npm::create_cli_npm_resolver; +use crate::npm::installer::NpmInstaller; +use crate::npm::installer::NpmResolutionInstaller; use crate::npm::CliByonmNpmResolverCreateOptions; use crate::npm::CliManagedNpmResolverCreateOptions; +use crate::npm::CliNpmCache; +use crate::npm::CliNpmCacheHttpClient; +use crate::npm::CliNpmRegistryInfoProvider; use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolverCreateOptions; use crate::npm::CliNpmResolverManagedSnapshotOption; -use crate::npm::NpmRegistryReadPermissionChecker; -use crate::npm::NpmRegistryReadPermissionCheckerMode; -use crate::resolver::CjsTracker; +use crate::npm::CliNpmTarballCache; +use crate::npm::NpmResolutionInitializer; +use crate::resolver::CliCjsTracker; use crate::resolver::CliDenoResolver; +use crate::resolver::CliNpmGraphResolver; use crate::resolver::CliNpmReqResolver; use crate::resolver::CliResolver; -use crate::resolver::CliResolverOptions; use crate::resolver::CliSloppyImportsResolver; -use crate::resolver::NpmModuleLoader; +use crate::resolver::FoundPackageJsonDepFlag; use crate::standalone::binary::DenoCompileBinaryWriter; use crate::sys::CliSys; use crate::tools::check::TypeChecker; @@ -180,7 +191,7 @@ impl Deferred { struct CliFactoryServices { blob_store: Deferred>, caches: Deferred>, - cjs_tracker: Deferred>, + cjs_tracker: Deferred>, cli_options: Deferred>, code_cache: Deferred>, deno_resolver: Deferred>, @@ -188,11 +199,12 @@ struct CliFactoryServices { emitter: Deferred>, feature_checker: Deferred>, file_fetcher: Deferred>, + found_pkg_json_dep_flag: Arc, fs: Deferred>, global_http_cache: Deferred>, http_cache: Deferred>, http_client_provider: Deferred>, - in_npm_pkg_checker: Deferred>, + in_npm_pkg_checker: Deferred, main_graph_container: Deferred>, maybe_file_watcher_reporter: Deferred>, maybe_inspector_server: Deferred>>, @@ -202,9 +214,18 @@ struct CliFactoryServices { module_load_preparer: Deferred>, node_code_translator: Deferred>, node_resolver: Deferred>, + npm_cache: Deferred>, npm_cache_dir: Deferred>, + npm_cache_http_client: Deferred>, + npm_graph_resolver: Deferred>, + npm_installer: Deferred>, + npm_registry_info_provider: Deferred>, npm_req_resolver: Deferred>, - npm_resolver: Deferred>, + npm_resolution: Arc, + npm_resolution_initializer: Deferred>, + npm_resolution_installer: Deferred>, + npm_resolver: Deferred, + npm_tarball_cache: Deferred>, parsed_source_cache: Deferred>, permission_desc_parser: Deferred>>, @@ -379,7 +400,7 @@ impl CliFactory { pub fn in_npm_pkg_checker( &self, - ) -> Result<&Arc, AnyError> { + ) -> Result<&DenoInNpmPackageChecker, AnyError> { self.services.in_npm_pkg_checker.get_or_try_init(|| { let cli_options = self.cli_options()?; let options = if cli_options.use_byonm() { @@ -394,7 +415,19 @@ impl CliFactory { }, ) }; - Ok(deno_resolver::npm::create_in_npm_pkg_checker(options)) + Ok(DenoInNpmPackageChecker::new(options)) + }) + } + + pub fn npm_cache(&self) -> Result<&Arc, AnyError> { + self.services.npm_cache.get_or_try_init(|| { + let cli_options = self.cli_options()?; + Ok(Arc::new(CliNpmCache::new( + self.npm_cache_dir()?.clone(), + self.sys(), + NpmCacheSetting::from_cache_setting(&cli_options.cache_setting()), + cli_options.npmrc().clone(), + ))) }) } @@ -410,16 +443,131 @@ impl CliFactory { }) } - pub async fn npm_resolver( + pub fn npm_cache_http_client(&self) -> &Arc { + self.services.npm_cache_http_client.get_or_init(|| { + Arc::new(CliNpmCacheHttpClient::new( + self.http_client_provider().clone(), + self.text_only_progress_bar().clone(), + )) + }) + } + + pub fn npm_graph_resolver( &self, - ) -> Result<&Arc, AnyError> { + ) -> Result<&Arc, AnyError> { + self.services.npm_graph_resolver.get_or_try_init(|| { + let cli_options = self.cli_options()?; + Ok(Arc::new(CliNpmGraphResolver::new( + self.npm_installer_if_managed()?.cloned(), + self.services.found_pkg_json_dep_flag.clone(), + cli_options.unstable_bare_node_builtins(), + cli_options.default_npm_caching_strategy(), + ))) + }) + } + + pub fn npm_installer_if_managed( + &self, + ) -> Result>, AnyError> { + let options = self.cli_options()?; + if options.use_byonm() || options.no_npm() { + Ok(None) + } else { + Ok(Some(self.npm_installer()?)) + } + } + + pub fn npm_installer(&self) -> Result<&Arc, AnyError> { + self.services.npm_installer.get_or_try_init(|| { + let cli_options = self.cli_options()?; + Ok(Arc::new(NpmInstaller::new( + self.npm_cache()?.clone(), + Arc::new(NpmInstallDepsProvider::from_workspace( + cli_options.workspace(), + )), + self.npm_resolution().clone(), + self.npm_resolution_initializer()?.clone(), + self.npm_resolution_installer()?.clone(), + self.text_only_progress_bar(), + self.sys(), + self.npm_tarball_cache()?.clone(), + cli_options.maybe_lockfile().cloned(), + cli_options.node_modules_dir_path().cloned(), + cli_options.lifecycle_scripts_config(), + cli_options.npm_system_info(), + ))) + }) + } + + pub fn npm_registry_info_provider( + &self, + ) -> Result<&Arc, AnyError> { + self + .services + .npm_registry_info_provider + .get_or_try_init(|| { + let cli_options = self.cli_options()?; + Ok(Arc::new(CliNpmRegistryInfoProvider::new( + self.npm_cache()?.clone(), + self.npm_cache_http_client().clone(), + cli_options.npmrc().clone(), + ))) + }) + } + + pub fn npm_resolution(&self) -> &Arc { + &self.services.npm_resolution + } + + pub fn npm_resolution_initializer( + &self, + ) -> Result<&Arc, AnyError> { + self + .services + .npm_resolution_initializer + .get_or_try_init(|| { + let cli_options = self.cli_options()?; + Ok(Arc::new(NpmResolutionInitializer::new( + self.npm_registry_info_provider()?.clone(), + self.npm_resolution().clone(), + 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), + }, + }, + ))) + }) + } + + pub fn npm_resolution_installer( + &self, + ) -> Result<&Arc, AnyError> { + self.services.npm_resolution_installer.get_or_try_init(|| { + let cli_options = self.cli_options()?; + Ok(Arc::new(NpmResolutionInstaller::new( + self.npm_registry_info_provider()?.clone(), + self.npm_resolution().clone(), + cli_options.maybe_lockfile().cloned(), + ))) + }) + } + + pub async fn npm_resolver(&self) -> Result<&CliNpmResolver, AnyError> { self .services .npm_resolver .get_or_try_init_async( async { let cli_options = self.cli_options()?; - create_cli_npm_resolver(if cli_options.use_byonm() { + Ok(CliNpmResolver::new(if cli_options.use_byonm() { CliNpmResolverCreateOptions::Byonm( CliByonmNpmResolverCreateOptions { sys: self.sys(), @@ -438,52 +586,43 @@ impl CliFactory { }, ) } else { + self + .npm_resolution_initializer()? + .ensure_initialized() + .await?; CliNpmResolverCreateOptions::Managed( CliManagedNpmResolverCreateOptions { - http_client_provider: self.http_client_provider().clone(), - npm_install_deps_provider: Arc::new( - NpmInstallDepsProvider::from_workspace( - cli_options.workspace(), - ), - ), sys: self.sys(), - 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(), + npm_resolution: self.npm_resolution().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_system_info: cli_options.npm_system_info(), npmrc: cli_options.npmrc().clone(), - lifecycle_scripts: cli_options.lifecycle_scripts_config(), }, ) - }) - .await + })) } .boxed_local(), ) .await } + pub fn npm_tarball_cache( + &self, + ) -> Result<&Arc, AnyError> { + self.services.npm_tarball_cache.get_or_try_init(|| { + let cli_options = self.cli_options()?; + Ok(Arc::new(CliNpmTarballCache::new( + self.npm_cache()?.clone(), + self.npm_cache_http_client().clone(), + self.sys(), + cli_options.npmrc().clone(), + ))) + }) + } + pub fn sloppy_imports_resolver( &self, ) -> Result>, AnyError> { @@ -571,17 +710,10 @@ impl CliFactory { .resolver .get_or_try_init_async( async { - let cli_options = self.cli_options()?; - Ok(Arc::new(CliResolver::new(CliResolverOptions { - npm_resolver: if cli_options.no_npm() { - None - } else { - Some(self.npm_resolver().await?.clone()) - }, - bare_node_builtins_enabled: cli_options - .unstable_bare_node_builtins(), - deno_resolver: self.deno_resolver().await?.clone(), - }))) + Ok(Arc::new(CliResolver::new( + self.deno_resolver().await?.clone(), + self.services.found_pkg_json_dep_flag.clone(), + ))) } .boxed_local(), ) @@ -663,13 +795,10 @@ impl CliFactory { Ok(Arc::new(CliNodeResolver::new( self.in_npm_pkg_checker()?.clone(), RealIsBuiltInNodeModuleChecker, - self - .npm_resolver() - .await? - .clone() - .into_npm_pkg_folder_resolver(), + self.npm_resolver().await?.clone(), self.pkg_json_resolver().clone(), self.sys(), + node_resolver::ConditionsFromResolutionMode::default(), ))) } .boxed_local(), @@ -684,26 +813,14 @@ impl CliFactory { .services .node_code_translator .get_or_try_init_async(async { - let caches = self.caches()?; - let node_analysis_cache = - NodeAnalysisCache::new(caches.node_analysis_db()); let node_resolver = self.node_resolver().await?.clone(); - let cjs_esm_analyzer = CliCjsCodeAnalyzer::new( - node_analysis_cache, - self.cjs_tracker()?.clone(), - self.fs().clone(), - Some(self.parsed_source_cache().clone()), - ); + let cjs_code_analyzer = self.create_cjs_code_analyzer()?; Ok(Arc::new(NodeCodeTranslator::new( - cjs_esm_analyzer, + cjs_code_analyzer, self.in_npm_pkg_checker()?.clone(), node_resolver, - self - .npm_resolver() - .await? - .clone() - .into_npm_pkg_folder_resolver(), + self.npm_resolver().await?.clone(), self.pkg_json_resolver().clone(), self.sys(), ))) @@ -711,6 +828,17 @@ impl CliFactory { .await } + fn create_cjs_code_analyzer(&self) -> Result { + let caches = self.caches()?; + let node_analysis_cache = NodeAnalysisCache::new(caches.node_analysis_db()); + Ok(CliCjsCodeAnalyzer::new( + node_analysis_cache, + self.cjs_tracker()?.clone(), + self.fs().clone(), + Some(self.parsed_source_cache().clone()), + )) + } + pub async fn npm_req_resolver( &self, ) -> Result<&Arc, AnyError> { @@ -723,7 +851,7 @@ impl CliFactory { sys: self.sys(), in_npm_pkg_checker: self.in_npm_pkg_checker()?.clone(), node_resolver: self.node_resolver().await?.clone(), - npm_resolver: npm_resolver.clone().into_byonm_or_managed(), + npm_resolver: npm_resolver.clone(), }))) }) .await @@ -751,6 +879,7 @@ impl CliFactory { cli_options.clone(), self.module_graph_builder().await?.clone(), self.node_resolver().await?.clone(), + self.npm_installer_if_managed()?.cloned(), self.npm_resolver().await?.clone(), self.sys(), ))) @@ -776,6 +905,8 @@ impl CliFactory { cli_options.maybe_lockfile().cloned(), self.maybe_file_watcher_reporter().clone(), self.module_info_cache()?.clone(), + self.npm_graph_resolver()?.clone(), + self.npm_installer_if_managed()?.cloned(), self.npm_resolver().await?.clone(), self.parsed_source_cache().clone(), self.resolver().await?.clone(), @@ -796,7 +927,7 @@ impl CliFactory { let cli_options = self.cli_options()?; Ok(Arc::new(ModuleGraphCreator::new( cli_options.clone(), - self.npm_resolver().await?.clone(), + self.npm_installer_if_managed()?.cloned(), self.module_graph_builder().await?.clone(), self.type_checker().await?.clone(), ))) @@ -851,10 +982,10 @@ impl CliFactory { .await } - pub fn cjs_tracker(&self) -> Result<&Arc, AnyError> { + 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( + Ok(Arc::new(CliCjsTracker::new( self.in_npm_pkg_checker()?.clone(), self.pkg_json_resolver().clone(), if options.is_node_main() || options.unstable_detect_cjs() { @@ -897,13 +1028,13 @@ impl CliFactory { ) -> Result { let cli_options = self.cli_options()?; Ok(DenoCompileBinaryWriter::new( + self.create_cjs_code_analyzer()?, self.cjs_tracker()?, self.cli_options()?, self.deno_dir()?, self.emitter()?, - self.file_fetcher()?, self.http_client_provider(), - self.npm_resolver().await?.as_ref(), + self.npm_resolver().await?, self.workspace_resolver().await?.as_ref(), cli_options.npm_system_info(), )) @@ -957,7 +1088,34 @@ impl CliFactory { Arc::new(NpmRegistryReadPermissionChecker::new(self.sys(), mode)) }; - Ok(CliMainWorkerFactory::new( + let module_loader_factory = CliModuleLoaderFactory::new( + cli_options, + cjs_tracker, + if cli_options.code_cache_enabled() { + Some(self.code_cache()?.clone()) + } else { + None + }, + self.emitter()?.clone(), + in_npm_pkg_checker.clone(), + self.main_module_graph_container().await?.clone(), + self.module_load_preparer().await?.clone(), + node_code_translator.clone(), + node_resolver.clone(), + NpmModuleLoader::new( + self.cjs_tracker()?.clone(), + node_code_translator.clone(), + self.sys(), + ), + npm_registry_permission_checker, + npm_req_resolver.clone(), + cli_npm_resolver.clone(), + self.parsed_source_cache().clone(), + self.resolver().await?.clone(), + self.sys(), + ); + + let lib_main_worker_factory = LibMainWorkerFactory::new( self.blob_store().clone(), if cli_options.code_cache_enabled() { Some(self.code_cache()?.clone()) @@ -966,49 +1124,67 @@ impl CliFactory { }, self.feature_checker()?.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(), - in_npm_pkg_checker.clone(), - self.main_module_graph_container().await?.clone(), - self.module_load_preparer().await?.clone(), - node_code_translator.clone(), - node_resolver.clone(), - NpmModuleLoader::new( - self.cjs_tracker()?.clone(), - fs.clone(), - node_code_translator.clone(), - ), - npm_registry_permission_checker, - npm_req_resolver.clone(), - cli_npm_resolver.clone(), - self.parsed_source_cache().clone(), - self.resolver().await?.clone(), - self.sys(), - )), + Box::new(module_loader_factory), node_resolver.clone(), - npm_resolver.clone(), + create_npm_process_state_provider(npm_resolver), pkg_json_resolver, self.root_cert_store_provider().clone(), - self.root_permissions_container()?.clone(), - StorageKeyResolver::from_options(cli_options), + cli_options.resolve_storage_key_resolver(), + self.sys(), + self.create_lib_main_worker_options()?, + ); + + Ok(CliMainWorkerFactory::new( + lib_main_worker_factory, + maybe_file_watcher_communicator, + cli_options.maybe_lockfile().cloned(), + self.npm_installer_if_managed()?.cloned(), + npm_resolver.clone(), self.sys(), - cli_options.sub_command().clone(), self.create_cli_main_worker_options()?, - self.cli_options()?.otel_config(), - self.cli_options()?.default_npm_caching_strategy(), + self.root_permissions_container()?.clone(), )) } + fn create_lib_main_worker_options( + &self, + ) -> Result { + let cli_options = self.cli_options()?; + Ok(LibMainWorkerOptions { + argv: cli_options.argv().clone(), + // This optimization is only available for "run" subcommand + // because we need to register new ops for testing and jupyter + // integration. + skip_op_registration: cli_options.sub_command().is_run(), + log_level: cli_options.log_level().unwrap_or(log::Level::Info).into(), + enable_op_summary_metrics: cli_options.enable_op_summary_metrics(), + enable_testing_features: cli_options.enable_testing_features(), + has_node_modules_dir: cli_options.has_node_modules_dir(), + inspect_brk: cli_options.inspect_brk().is_some(), + inspect_wait: cli_options.inspect_wait().is_some(), + strace_ops: cli_options.strace_ops().clone(), + is_inspecting: cli_options.is_inspecting(), + 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 + argv0: cli_options + .take_binary_npm_command_name() + .or(std::env::args().next()), + node_debug: std::env::var("NODE_DEBUG").ok(), + origin_data_folder_path: Some(self.deno_dir()?.origin_data_folder_path()), + seed: cli_options.seed(), + unsafely_ignore_certificate_errors: cli_options + .unsafely_ignore_certificate_errors() + .clone(), + node_ipc: cli_options.node_ipc_fd(), + serve_port: cli_options.serve_port(), + serve_host: cli_options.serve_host(), + otel_config: self.cli_options()?.otel_config(), + startup_snapshot: crate::js::deno_isolate_init(), + }) + } + fn create_cli_main_worker_options( &self, ) -> Result { @@ -1040,37 +1216,10 @@ impl CliFactory { }; Ok(CliMainWorkerOptions { - argv: cli_options.argv().clone(), - // This optimization is only available for "run" subcommand - // because we need to register new ops for testing and jupyter - // integration. - skip_op_registration: cli_options.sub_command().is_run(), - log_level: cli_options.log_level().unwrap_or(log::Level::Info).into(), - enable_op_summary_metrics: cli_options.enable_op_summary_metrics(), - enable_testing_features: cli_options.enable_testing_features(), - has_node_modules_dir: cli_options.has_node_modules_dir(), - hmr: cli_options.has_hmr(), - inspect_brk: cli_options.inspect_brk().is_some(), - inspect_wait: cli_options.inspect_wait().is_some(), - strace_ops: cli_options.strace_ops().clone(), - is_inspecting: cli_options.is_inspecting(), - 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 - argv0: cli_options - .take_binary_npm_command_name() - .or(std::env::args().next()), - node_debug: std::env::var("NODE_DEBUG").ok(), - origin_data_folder_path: Some(self.deno_dir()?.origin_data_folder_path()), - seed: cli_options.seed(), - unsafely_ignore_certificate_errors: cli_options - .unsafely_ignore_certificate_errors() - .clone(), + needs_test_modules: cli_options.sub_command().needs_test(), create_hmr_runner, create_coverage_collector, - node_ipc: cli_options.node_ipc_fd(), - serve_port: cli_options.serve_port(), - serve_host: cli_options.serve_host(), + default_npm_caching_strategy: cli_options.default_npm_caching_strategy(), }) } } diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index cfc26d7e69..723571649a 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -60,11 +60,10 @@ impl TextDecodedFile { file.maybe_headers.as_ref(), ); let specifier = file.url; - match deno_graph::source::decode_source( - &specifier, - file.source, - maybe_charset, - ) { + let charset = maybe_charset.unwrap_or_else(|| { + deno_media_type::encoding::detect_charset(&specifier, &file.source) + }); + match deno_media_type::encoding::decode_arc_source(charset, file.source) { Ok(source) => Ok(TextDecodedFile { media_type, specifier, diff --git a/cli/graph_util.rs b/cli/graph_util.rs index 8da44c76ab..e57fcf8a94 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use deno_config::deno_json; use deno_config::deno_json::JsxImportSourceConfig; +use deno_config::deno_json::NodeModulesDirMode; use deno_config::workspace::JsrPackageConfig; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; @@ -29,6 +30,7 @@ use deno_graph::ResolutionError; use deno_graph::SpecifierError; use deno_graph::WorkspaceFastCheckOption; use deno_path_util::url_to_file_path; +use deno_resolver::npm::DenoInNpmPackageChecker; use deno_resolver::sloppy_imports::SloppyImportsCachedFs; use deno_resolver::sloppy_imports::SloppyImportsResolutionKind; use deno_runtime::deno_node; @@ -36,7 +38,6 @@ use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::jsr::JsrDepPackageReq; use deno_semver::package::PackageNv; use deno_semver::SmallStackString; -use node_resolver::InNpmPackageChecker; use crate::args::config_to_deno_graph_workspace_member; use crate::args::jsr_url; @@ -51,8 +52,11 @@ use crate::cache::ModuleInfoCache; use crate::cache::ParsedSourceCache; use crate::colors; use crate::file_fetcher::CliFileFetcher; +use crate::npm::installer::NpmInstaller; +use crate::npm::installer::PackageCaching; use crate::npm::CliNpmResolver; -use crate::resolver::CjsTracker; +use crate::resolver::CliCjsTracker; +use crate::resolver::CliNpmGraphResolver; use crate::resolver::CliResolver; use crate::resolver::CliSloppyImportsResolver; use crate::sys::CliSys; @@ -263,7 +267,7 @@ pub struct CreateGraphOptions<'a> { pub struct ModuleGraphCreator { options: Arc, - npm_resolver: Arc, + npm_installer: Option>, module_graph_builder: Arc, type_checker: Arc, } @@ -271,13 +275,13 @@ pub struct ModuleGraphCreator { impl ModuleGraphCreator { pub fn new( options: Arc, - npm_resolver: Arc, + npm_installer: Option>, module_graph_builder: Arc, type_checker: Arc, ) -> Self { Self { options, - npm_resolver, + npm_installer, module_graph_builder, type_checker, } @@ -400,9 +404,9 @@ impl ModuleGraphCreator { .build_graph_with_npm_resolution(&mut graph, options) .await?; - if let Some(npm_resolver) = self.npm_resolver.as_managed() { + if let Some(npm_installer) = &self.npm_installer { if graph.has_node_specifier && self.options.type_check_mode().is_true() { - npm_resolver.inject_synthetic_types_node_package().await?; + npm_installer.inject_synthetic_types_node_package().await?; } } @@ -489,15 +493,17 @@ pub enum BuildGraphWithNpmResolutionError { pub struct ModuleGraphBuilder { caches: Arc, - cjs_tracker: Arc, + cjs_tracker: Arc, cli_options: Arc, file_fetcher: Arc, global_http_cache: Arc, - in_npm_pkg_checker: Arc, + in_npm_pkg_checker: DenoInNpmPackageChecker, lockfile: Option>, maybe_file_watcher_reporter: Option, module_info_cache: Arc, - npm_resolver: Arc, + npm_graph_resolver: Arc, + npm_installer: Option>, + npm_resolver: CliNpmResolver, parsed_source_cache: Arc, resolver: Arc, root_permissions_container: PermissionsContainer, @@ -508,15 +514,17 @@ impl ModuleGraphBuilder { #[allow(clippy::too_many_arguments)] pub fn new( caches: Arc, - cjs_tracker: Arc, + cjs_tracker: Arc, cli_options: Arc, file_fetcher: Arc, global_http_cache: Arc, - in_npm_pkg_checker: Arc, + in_npm_pkg_checker: DenoInNpmPackageChecker, lockfile: Option>, maybe_file_watcher_reporter: Option, module_info_cache: Arc, - npm_resolver: Arc, + npm_graph_resolver: Arc, + npm_installer: Option>, + npm_resolver: CliNpmResolver, parsed_source_cache: Arc, resolver: Arc, root_permissions_container: PermissionsContainer, @@ -532,6 +540,8 @@ impl ModuleGraphBuilder { lockfile, maybe_file_watcher_reporter, module_info_cache, + npm_graph_resolver, + npm_installer, npm_resolver, parsed_source_cache, resolver, @@ -630,10 +640,7 @@ impl ModuleGraphBuilder { Some(loader) => MutLoaderRef::Borrowed(loader), None => MutLoaderRef::Owned(self.create_graph_loader()), }; - let cli_resolver = &self.resolver; let graph_resolver = self.create_graph_resolver()?; - let graph_npm_resolver = - cli_resolver.create_graph_npm_resolver(options.npm_caching); let maybe_file_watcher_reporter = self .maybe_file_watcher_reporter .as_ref() @@ -654,7 +661,7 @@ impl ModuleGraphBuilder { executor: Default::default(), file_system: &self.sys, jsr_url_provider: &CliJsrUrlProvider, - npm_resolver: Some(&graph_npm_resolver), + npm_resolver: Some(self.npm_graph_resolver.as_ref()), module_analyzer: &analyzer, reporter: maybe_file_watcher_reporter, resolver: Some(&graph_resolver), @@ -678,16 +685,15 @@ impl ModuleGraphBuilder { if self .cli_options .node_modules_dir()? - .map(|m| m.uses_node_modules_dir()) + .map(|m| m == NodeModulesDirMode::Auto) .unwrap_or(false) { - if let Some(npm_resolver) = self.npm_resolver.as_managed() { - let already_done = - npm_resolver.ensure_top_level_package_json_install().await?; + if let Some(npm_installer) = &self.npm_installer { + let already_done = npm_installer + .ensure_top_level_package_json_install() + .await?; if !already_done && matches!(npm_caching, NpmCachingStrategy::Eager) { - npm_resolver - .cache_packages(crate::npm::PackageCaching::All) - .await?; + npm_installer.cache_packages(PackageCaching::All).await?; } } } @@ -706,8 +712,7 @@ 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() + if roots.iter().any(|r| r.scheme() == "npm") && self.npm_resolver.is_byonm() { return Err(BuildGraphWithNpmResolutionError::UnsupportedNpmSpecifierEntrypointResolutionWay); } @@ -775,11 +780,7 @@ impl ModuleGraphBuilder { None }; let parser = self.parsed_source_cache.as_capturing_parser(); - let cli_resolver = &self.resolver; let graph_resolver = self.create_graph_resolver()?; - let graph_npm_resolver = cli_resolver.create_graph_npm_resolver( - self.cli_options.default_npm_caching_strategy(), - ); graph.build_fast_check_type_graph( deno_graph::BuildFastCheckTypeGraphOptions { @@ -788,7 +789,7 @@ impl ModuleGraphBuilder { fast_check_dts: false, jsr_url_provider: &CliJsrUrlProvider, resolver: Some(&graph_resolver), - npm_resolver: Some(&graph_npm_resolver), + npm_resolver: Some(self.npm_graph_resolver.as_ref()), workspace_fast_check: options.workspace_fast_check, }, ); @@ -1224,7 +1225,7 @@ fn format_deno_graph_error(err: &dyn Error) -> String { #[derive(Debug)] struct CliGraphResolver<'a> { - cjs_tracker: &'a CjsTracker, + cjs_tracker: &'a CliCjsTracker, resolver: &'a CliResolver, jsx_import_source_config: Option, } diff --git a/cli/http_util.rs b/cli/http_util.rs index 5e63ab0a4a..a12fde937e 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -14,6 +14,7 @@ use deno_core::serde_json; use deno_core::url::Url; use deno_error::JsError; use deno_error::JsErrorBox; +use deno_lib::version::DENO_VERSION_INFO; use deno_runtime::deno_fetch; use deno_runtime::deno_fetch::create_http_client; use deno_runtime::deno_fetch::CreateHttpClientOptions; @@ -28,7 +29,6 @@ use http_body_util::BodyExt; use thiserror::Error; use crate::util::progress_bar::UpdateGuard; -use crate::version; #[derive(Debug, Error)] pub enum SendError { @@ -79,7 +79,7 @@ impl HttpClientProvider { Entry::Occupied(entry) => Ok(HttpClient::new(entry.get().clone())), Entry::Vacant(entry) => { let client = create_http_client( - version::DENO_VERSION_INFO.user_agent, + DENO_VERSION_INFO.user_agent, CreateHttpClientOptions { root_cert_store: match &self.root_cert_store_provider { Some(provider) => Some(provider.get_or_try_init()?.clone()), @@ -481,7 +481,7 @@ mod test { let client = HttpClient::new( create_http_client( - version::DENO_VERSION_INFO.user_agent, + DENO_VERSION_INFO.user_agent, CreateHttpClientOptions { ca_certs: vec![std::fs::read( test_util::testdata_path().join("tls/RootCA.pem"), @@ -525,7 +525,7 @@ mod test { let client = HttpClient::new( create_http_client( - version::DENO_VERSION_INFO.user_agent, + DENO_VERSION_INFO.user_agent, CreateHttpClientOptions::default(), ) .unwrap(), @@ -566,7 +566,7 @@ mod test { let client = HttpClient::new( create_http_client( - version::DENO_VERSION_INFO.user_agent, + DENO_VERSION_INFO.user_agent, CreateHttpClientOptions { root_cert_store: Some(root_cert_store), ..Default::default() @@ -587,7 +587,7 @@ mod test { .unwrap(); let client = HttpClient::new( create_http_client( - version::DENO_VERSION_INFO.user_agent, + DENO_VERSION_INFO.user_agent, CreateHttpClientOptions { ca_certs: vec![std::fs::read( test_util::testdata_path() @@ -620,7 +620,7 @@ mod test { let url = Url::parse("https://localhost:5545/etag_script.ts").unwrap(); let client = HttpClient::new( create_http_client( - version::DENO_VERSION_INFO.user_agent, + DENO_VERSION_INFO.user_agent, CreateHttpClientOptions { ca_certs: vec![std::fs::read( test_util::testdata_path() @@ -661,7 +661,7 @@ mod test { .unwrap(); let client = HttpClient::new( create_http_client( - version::DENO_VERSION_INFO.user_agent, + DENO_VERSION_INFO.user_agent, CreateHttpClientOptions { ca_certs: vec![std::fs::read( test_util::testdata_path() diff --git a/cli/integration_tests_runner.rs b/cli/integration_tests_runner.rs index 7342e62fa0..63f2abe460 100644 --- a/cli/integration_tests_runner.rs +++ b/cli/integration_tests_runner.rs @@ -1,18 +1,5 @@ // Copyright 2018-2025 the Deno authors. MIT license. + pub fn main() { - let mut args = vec!["cargo", "test", "-p", "cli_tests", "--features", "run"]; - - if !cfg!(debug_assertions) { - args.push("--release"); - } - - args.push("--"); - - // If any args were passed to this process, pass them through to the child - let orig_args = std::env::args().skip(1).collect::>(); - let orig_args: Vec<&str> = - orig_args.iter().map(|x| x.as_ref()).collect::>(); - args.extend(orig_args); - - test_util::spawn::exec_replace("cargo", &args).unwrap(); + // this file exists to cause the executable to be built when running cargo test } diff --git a/cli/js.rs b/cli/js.rs index 5337c53f76..37004ad444 100644 --- a/cli/js.rs +++ b/cli/js.rs @@ -2,18 +2,7 @@ use log::debug; -#[cfg(not(feature = "hmr"))] -static CLI_SNAPSHOT: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/CLI_SNAPSHOT.bin")); - pub fn deno_isolate_init() -> Option<&'static [u8]> { debug!("Deno isolate init with snapshots."); - #[cfg(not(feature = "hmr"))] - { - Some(CLI_SNAPSHOT) - } - #[cfg(feature = "hmr")] - { - None - } + deno_snapshots::CLI_SNAPSHOT } diff --git a/cli/js/40_bench.js b/cli/js/40_bench.js index 0ad05c5197..fb0e86463d 100644 --- a/cli/js/40_bench.js +++ b/cli/js/40_bench.js @@ -8,7 +8,7 @@ import { restorePermissions, } from "ext:cli/40_test_common.js"; import { Console } from "ext:deno_console/01_console.js"; -import { setExitHandler } from "ext:runtime/30_os.js"; +import { setExitHandler } from "ext:deno_os/30_os.js"; const { op_register_bench, op_bench_get_origin, diff --git a/cli/js/40_lint.js b/cli/js/40_lint.js index 4adbc1baa1..9f85f0871d 100644 --- a/cli/js/40_lint.js +++ b/cli/js/40_lint.js @@ -8,10 +8,27 @@ import { splitSelectors, } from "ext:cli/40_lint_selector.js"; import { core, internals } from "ext:core/mod.js"; + const { op_lint_create_serialized_ast, } = core.ops; +// Keep these in sync with Rust +const AST_IDX_INVALID = 0; +const AST_GROUP_TYPE = 1; +/// +/// +/// +/// +/// +const NODE_SIZE = 1 + 4 + 4 + 4 + 4; +const PROP_OFFSET = 1; +const CHILD_OFFSET = 1 + 4; +const NEXT_OFFSET = 1 + 4 + 4; +const PARENT_OFFSET = 1 + 4 + 4 + 4; +// Span size in buffer: u32 + u32 +const SPAN_SIZE = 4 + 4; + // Keep in sync with Rust // These types are expected to be present on every node. Note that this // isn't set in stone. We could revise this at a future point. @@ -34,12 +51,21 @@ const PropFlags = { * the string table that was included in the message. */ String: 2, + /** + * A numnber field. Numbers are represented as strings internally. + */ + Number: 3, /** This value is either 0 = false, or 1 = true */ - Bool: 3, + Bool: 4, /** No value, it's null */ - Null: 4, + Null: 5, /** No value, it's undefined */ - Undefined: 5, + Undefined: 6, + /** An object */ + Obj: 7, + Regex: 8, + BigInt: 9, + Array: 10, }; /** @typedef {import("./40_lint_types.d.ts").AstContext} AstContext */ @@ -51,6 +77,7 @@ const PropFlags = { /** @typedef {import("./40_lint_types.d.ts").LintPlugin} LintPlugin */ /** @typedef {import("./40_lint_types.d.ts").TransformFn} TransformFn */ /** @typedef {import("./40_lint_types.d.ts").MatchContext} MatchContext */ +/** @typedef {import("./40_lint_types.d.ts").Node} Node */ /** @type {LintState} */ const state = { @@ -100,17 +127,17 @@ export function installPlugin(plugin) { /** * @param {AstContext} ctx - * @param {number} offset - * @returns + * @param {number} idx + * @returns {FacadeNode | null} */ -function getNode(ctx, offset) { - if (offset === 0) return null; - const cached = ctx.nodes.get(offset); - if (cached !== undefined) return cached; +function getNode(ctx, idx) { + if (idx === AST_IDX_INVALID) return null; + const cached = ctx.nodes.get(idx); + if (cached !== undefined) return /** @type {*} */ (cached); - const node = new Node(ctx, offset); - ctx.nodes.set(offset, /** @type {*} */ (cached)); - return node; + const node = new FacadeNode(ctx, idx); + ctx.nodes.set(idx, /** @type {*} */ (node)); + return /** @type {*} */ (node); } /** @@ -122,31 +149,22 @@ function getNode(ctx, offset) { * @returns {number} */ function findPropOffset(buf, offset, search) { - // type + parentId + SpanLo + SpanHi - offset += 1 + 4 + 4 + 4; - - const propCount = buf[offset]; + const count = buf[offset]; offset += 1; - for (let i = 0; i < propCount; i++) { + for (let i = 0; i < count; i++) { const maybe = offset; const prop = buf[offset++]; const kind = buf[offset++]; if (prop === search) return maybe; - if (kind === PropFlags.Ref) { - offset += 4; - } else if (kind === PropFlags.RefArr) { + if (kind === PropFlags.Obj) { const len = readU32(buf, offset); - offset += 4 + (len * 4); - } else if (kind === PropFlags.String) { offset += 4; - } else if (kind === PropFlags.Bool) { - offset++; - } else if (kind === PropFlags.Null || kind === PropFlags.Undefined) { - // No value + // prop + kind + value + offset += len * (1 + 1 + 4); } else { - offset++; + offset += 4; } } @@ -154,23 +172,23 @@ function findPropOffset(buf, offset, search) { } const INTERNAL_CTX = Symbol("ctx"); -const INTERNAL_OFFSET = Symbol("offset"); +const INTERNAL_IDX = Symbol("offset"); // This class is a facade for all materialized nodes. Instead of creating a // unique class per AST node, we have one class with getters for every // possible node property. This allows us to lazily materialize child node // only when they are needed. -class Node { +class FacadeNode { [INTERNAL_CTX]; - [INTERNAL_OFFSET]; + [INTERNAL_IDX]; /** * @param {AstContext} ctx - * @param {number} offset + * @param {number} idx */ - constructor(ctx, offset) { + constructor(ctx, idx) { this[INTERNAL_CTX] = ctx; - this[INTERNAL_OFFSET] = offset; + this[INTERNAL_IDX] = idx; } /** @@ -186,12 +204,12 @@ class Node { * @returns {string} */ [Symbol.for("Deno.customInspect")](_, options) { - const json = toJsValue(this[INTERNAL_CTX], this[INTERNAL_OFFSET]); + const json = nodeToJson(this[INTERNAL_CTX], this[INTERNAL_IDX]); return Deno.inspect(json, options); } [Symbol.for("Deno.lint.toJsValue")]() { - return toJsValue(this[INTERNAL_CTX], this[INTERNAL_OFFSET]); + return nodeToJson(this[INTERNAL_CTX], this[INTERNAL_IDX]); } } @@ -212,125 +230,243 @@ function setNodeGetters(ctx) { const name = getString(ctx.strTable, id); - Object.defineProperty(Node.prototype, name, { + Object.defineProperty(FacadeNode.prototype, name, { get() { - return readValue(this[INTERNAL_CTX], this[INTERNAL_OFFSET], i); + return readValue( + this[INTERNAL_CTX], + this[INTERNAL_IDX], + i, + getNode, + ); }, }); } } /** - * Serialize a node recursively to plain JSON * @param {AstContext} ctx - * @param {number} offset - * @returns {*} + * @param {number} idx */ -function toJsValue(ctx, offset) { - const { buf } = ctx; - +function nodeToJson(ctx, idx) { /** @type {Record} */ const node = { - type: readValue(ctx, offset, AST_PROP_TYPE), - range: readValue(ctx, offset, AST_PROP_RANGE), + type: readValue(ctx, idx, AST_PROP_TYPE, nodeToJson), + range: readValue(ctx, idx, AST_PROP_RANGE, nodeToJson), }; - // type + parentId + SpanLo + SpanHi - offset += 1 + 4 + 4 + 4; + const { buf } = ctx; + let offset = readPropOffset(ctx, idx); const count = buf[offset++]; - for (let i = 0; i < count; i++) { - const prop = buf[offset++]; - const kind = buf[offset++]; - const name = getString(ctx.strTable, ctx.strByProp[prop]); - if (kind === PropFlags.Ref) { - const v = readU32(buf, offset); - offset += 4; - node[name] = v === 0 ? null : toJsValue(ctx, v); - } else if (kind === PropFlags.RefArr) { - const len = readU32(buf, offset); - offset += 4; - const nodes = new Array(len); - for (let i = 0; i < len; i++) { - const v = readU32(buf, offset); - if (v === 0) continue; - nodes[i] = toJsValue(ctx, v); - offset += 4; - } - node[name] = nodes; - } else if (kind === PropFlags.Bool) { - const v = buf[offset++]; - node[name] = v === 1; - } else if (kind === PropFlags.String) { - const v = readU32(buf, offset); - offset += 4; - node[name] = getString(ctx.strTable, v); - } else if (kind === PropFlags.Null) { - node[name] = null; - } else if (kind === PropFlags.Undefined) { - node[name] = undefined; - } + for (let i = 0; i < count; i++) { + const prop = buf[offset]; + const _kind = buf[offset + 1]; + + const name = getString(ctx.strTable, ctx.strByProp[prop]); + node[name] = readProperty(ctx, offset, nodeToJson); + + // prop + type + value + offset += 1 + 1 + 4; } return node; } /** - * Read a specific property from a node + * @param {AstContext["buf"]} buf + * @param {number} idx + * @returns {number} + */ +function readType(buf, idx) { + return buf[idx * NODE_SIZE]; +} + +/** + * @param {AstContext} ctx + * @param {number} idx + * @returns {Node["range"]} + */ +function readSpan(ctx, idx) { + let offset = ctx.spansOffset + (idx * SPAN_SIZE); + const start = readU32(ctx.buf, offset); + offset += 4; + const end = readU32(ctx.buf, offset); + + return [start, end]; +} + +/** + * @param {AstContext["buf"]} buf + * @param {number} idx + * @returns {number} + */ +function readRawPropOffset(buf, idx) { + const offset = (idx * NODE_SIZE) + PROP_OFFSET; + return readU32(buf, offset); +} + +/** + * @param {AstContext} ctx + * @param {number} idx + * @returns {number} + */ +function readPropOffset(ctx, idx) { + return readRawPropOffset(ctx.buf, idx) + ctx.propsOffset; +} + +/** + * @param {AstContext["buf"]} buf + * @param {number} idx + * @returns {number} + */ +function readChild(buf, idx) { + const offset = (idx * NODE_SIZE) + CHILD_OFFSET; + return readU32(buf, offset); +} +/** + * @param {AstContext["buf"]} buf + * @param {number} idx + * @returns {number} + */ +function readNext(buf, idx) { + const offset = (idx * NODE_SIZE) + NEXT_OFFSET; + return readU32(buf, offset); +} + +/** + * @param {AstContext["buf"]} buf + * @param {number} idx + * @returns {number} + */ +function readParent(buf, idx) { + const offset = (idx * NODE_SIZE) + PARENT_OFFSET; + return readU32(buf, offset); +} + +/** + * @param {AstContext["strTable"]} strTable + * @param {number} strId + * @returns {RegExp} + */ +function readRegex(strTable, strId) { + const raw = getString(strTable, strId); + const idx = raw.lastIndexOf("/"); + const pattern = raw.slice(1, idx); + const flags = idx < raw.length - 1 ? raw.slice(idx + 1) : undefined; + + return new RegExp(pattern, flags); +} + +/** * @param {AstContext} ctx * @param {number} offset - * @param {number} search - * @returns {*} + * @param {(ctx: AstContext, idx: number) => any} parseNode + * @returns {Record} */ -function readValue(ctx, offset, search) { - const { buf } = ctx; - const type = buf[offset]; +function readObject(ctx, offset, parseNode) { + const { buf, strTable, strByProp } = ctx; - if (search === AST_PROP_TYPE) { - return getString(ctx.strTable, ctx.strByType[type]); - } else if (search === AST_PROP_RANGE) { - const start = readU32(buf, offset + 1 + 4); - const end = readU32(buf, offset + 1 + 4 + 4); - return [start, end]; - } else if (search === AST_PROP_PARENT) { - const pos = readU32(buf, offset + 1); - return getNode(ctx, pos); + /** @type {Record} */ + const obj = {}; + + const count = readU32(buf, offset); + offset += 4; + + for (let i = 0; i < count; i++) { + const prop = buf[offset]; + const name = getString(strTable, strByProp[prop]); + obj[name] = readProperty(ctx, offset, parseNode); + // name + kind + value + offset += 1 + 1 + 4; } - offset = findPropOffset(ctx.buf, offset, search); - if (offset === -1) return undefined; + return obj; +} - const kind = buf[offset + 1]; - offset += 2; +/** + * @param {AstContext} ctx + * @param {number} offset + * @param {(ctx: AstContext, idx: number) => any} parseNode + * @returns {any} + */ +function readProperty(ctx, offset, parseNode) { + const { buf } = ctx; + + // skip over name + const _name = buf[offset++]; + const kind = buf[offset++]; if (kind === PropFlags.Ref) { const value = readU32(buf, offset); - return getNode(ctx, value); + return parseNode(ctx, value); } else if (kind === PropFlags.RefArr) { - const len = readU32(buf, offset); - offset += 4; + const groupId = readU32(buf, offset); - const nodes = new Array(len); - for (let i = 0; i < len; i++) { - nodes[i] = getNode(ctx, readU32(buf, offset)); - offset += 4; + const nodes = []; + let next = readChild(buf, groupId); + while (next > AST_IDX_INVALID) { + nodes.push(parseNode(ctx, next)); + next = readNext(buf, next); } + return nodes; } else if (kind === PropFlags.Bool) { - return buf[offset] === 1; + const v = readU32(buf, offset); + return v === 1; } else if (kind === PropFlags.String) { const v = readU32(buf, offset); return getString(ctx.strTable, v); + } else if (kind === PropFlags.Number) { + const v = readU32(buf, offset); + return Number(getString(ctx.strTable, v)); + } else if (kind === PropFlags.BigInt) { + const v = readU32(buf, offset); + return BigInt(getString(ctx.strTable, v)); + } else if (kind === PropFlags.Regex) { + const v = readU32(buf, offset); + return readRegex(ctx.strTable, v); } else if (kind === PropFlags.Null) { return null; } else if (kind === PropFlags.Undefined) { return undefined; + } else if (kind === PropFlags.Obj) { + const objOffset = readU32(buf, offset) + ctx.propsOffset; + return readObject(ctx, objOffset, parseNode); } throw new Error(`Unknown prop kind: ${kind}`); } +/** + * Read a specific property from a node + * @param {AstContext} ctx + * @param {number} idx + * @param {number} search + * @param {(ctx: AstContext, idx: number) => any} parseNode + * @returns {*} + */ +function readValue(ctx, idx, search, parseNode) { + const { buf } = ctx; + + if (search === AST_PROP_TYPE) { + const type = readType(buf, idx); + return getString(ctx.strTable, ctx.strByType[type]); + } else if (search === AST_PROP_RANGE) { + return readSpan(ctx, idx); + } else if (search === AST_PROP_PARENT) { + const parent = readParent(buf, idx); + return getNode(ctx, parent); + } + + const propOffset = readPropOffset(ctx, idx); + + const offset = findPropOffset(ctx.buf, propOffset, search); + if (offset === -1) return undefined; + + return readProperty(ctx, offset, parseNode); +} + const DECODER = new TextDecoder(); /** @@ -359,322 +495,149 @@ function getString(strTable, id) { return name; } -/** - * @param {AstContext["buf"]} buf - * @param {number} child - * @returns {null | [number, number]} - */ -function findChildOffset(buf, child) { - let offset = readU32(buf, child + 1); - - // type + parentId + SpanLo + SpanHi - offset += 1 + 4 + 4 + 4; - - const propCount = buf[offset++]; - for (let i = 0; i < propCount; i++) { - const _prop = buf[offset++]; - const kind = buf[offset++]; - - switch (kind) { - case PropFlags.Ref: { - const start = offset; - const value = readU32(buf, offset); - offset += 4; - if (value === child) { - return [start, -1]; - } - break; - } - case PropFlags.RefArr: { - const start = offset; - - const len = readU32(buf, offset); - offset += 4; - - for (let j = 0; j < len; j++) { - const value = readU32(buf, offset); - offset += 4; - if (value === child) { - return [start, j]; - } - } - - break; - } - case PropFlags.String: - offset += 4; - break; - case PropFlags.Bool: - offset++; - break; - case PropFlags.Null: - case PropFlags.Undefined: - break; - } - } - - return null; -} - /** @implements {MatchContext} */ class MatchCtx { /** - * @param {AstContext["buf"]} buf - * @param {AstContext["strTable"]} strTable - * @param {AstContext["strByType"]} strByType + * @param {AstContext} ctx */ - constructor(buf, strTable, strByType) { - this.buf = buf; - this.strTable = strTable; - this.strByType = strByType; + constructor(ctx) { + this.ctx = ctx; } /** - * @param {number} offset - * @returns {number} - */ - getParent(offset) { - return readU32(this.buf, offset + 1); - } - - /** - * @param {number} offset - * @returns {number} - */ - getType(offset) { - return this.buf[offset]; - } - - /** - * @param {number} offset - * @param {number[]} propIds * @param {number} idx + * @returns {number} + */ + getParent(idx) { + return readParent(this.ctx.buf, idx); + } + + /** + * @param {number} idx + * @returns {number} + */ + getType(idx) { + return readType(this.ctx.buf, idx); + } + + /** + * @param {number} idx - Node idx + * @param {number[]} propIds + * @param {number} propIdx * @returns {unknown} */ - getAttrPathValue(offset, propIds, idx) { - const { buf } = this; + getAttrPathValue(idx, propIds, propIdx) { + if (idx === 0) throw -1; - const propId = propIds[idx]; + const { buf, strTable, strByType } = this.ctx; + + const propId = propIds[propIdx]; switch (propId) { case AST_PROP_TYPE: { - const type = this.getType(offset); - return getString(this.strTable, this.strByType[type]); + const type = readType(buf, idx); + return getString(strTable, strByType[type]); } case AST_PROP_PARENT: case AST_PROP_RANGE: - throw new Error(`Not supported`); + throw -1; } + let offset = readPropOffset(this.ctx, idx); + offset = findPropOffset(buf, offset, propId); - if (offset === -1) return undefined; + if (offset === -1) throw -1; const _prop = buf[offset++]; const kind = buf[offset++]; if (kind === PropFlags.Ref) { const value = readU32(buf, offset); // Checks need to end with a value, not a node - if (idx === propIds.length - 1) return undefined; - return this.getAttrPathValue(value, propIds, idx + 1); + if (propIdx === propIds.length - 1) throw -1; + return this.getAttrPathValue(value, propIds, propIdx + 1); } else if (kind === PropFlags.RefArr) { - const count = readU32(buf, offset); + const arrIdx = readU32(buf, offset); offset += 4; - if (idx < propIds.length - 1 && propIds[idx + 1] === AST_PROP_LENGTH) { + let count = 0; + let child = readChild(buf, arrIdx); + while (child > AST_IDX_INVALID) { + count++; + child = readNext(buf, child); + } + + if ( + propIdx < propIds.length - 1 && propIds[propIdx + 1] === AST_PROP_LENGTH + ) { return count; } // TODO(@marvinhagemeister): Allow traversing into array children? + throw -1; + } else if (kind === PropFlags.Obj) { + // TODO(@marvinhagemeister) } // Cannot traverse into primitives further - if (idx < propIds.length - 1) return undefined; + if (propIdx < propIds.length - 1) throw -1; if (kind === PropFlags.String) { const s = readU32(buf, offset); - return getString(this.strTable, s); + return getString(strTable, s); + } else if (kind === PropFlags.Number) { + const s = readU32(buf, offset); + return Number(getString(strTable, s)); + } else if (kind === PropFlags.Regex) { + const v = readU32(buf, offset); + return readRegex(strTable, v); } else if (kind === PropFlags.Bool) { - return buf[offset] === 1; + return readU32(buf, offset) === 1; } else if (kind === PropFlags.Null) { return null; } else if (kind === PropFlags.Undefined) { return undefined; } - return undefined; + throw -1; } /** - * @param {number} offset - * @param {number[]} propIds * @param {number} idx - * @returns {boolean} - */ - hasAttrPath(offset, propIds, idx) { - const { buf } = this; - - const propId = propIds[idx]; - // If propId is 0 then the property doesn't exist in the AST - if (propId === 0) return false; - - switch (propId) { - case AST_PROP_TYPE: - case AST_PROP_PARENT: - case AST_PROP_RANGE: - return true; - } - - offset = findPropOffset(buf, offset, propId); - if (offset === -1) return false; - if (idx === propIds.length - 1) return true; - - const _prop = buf[offset++]; - const kind = buf[offset++]; - if (kind === PropFlags.Ref) { - const value = readU32(buf, offset); - return this.hasAttrPath(value, propIds, idx + 1); - } else if (kind === PropFlags.RefArr) { - const _count = readU32(buf, offset); - offset += 4; - - if (idx < propIds.length - 1 && propIds[idx + 1] === AST_PROP_LENGTH) { - return true; - } - - // TODO(@marvinhagemeister): Allow traversing into array children? - } - - // Primitives cannot be traversed further. This means we - // didn't found the attribute. - if (idx < propIds.length - 1) return false; - - return true; - } - - /** - * @param {number} offset * @returns {number} */ - getFirstChild(offset) { - const { buf } = this; - - // type + parentId + SpanLo + SpanHi - offset += 1 + 4 + 4 + 4; - - const count = buf[offset++]; - for (let i = 0; i < count; i++) { - const _prop = buf[offset++]; - const kind = buf[offset++]; - - switch (kind) { - case PropFlags.Ref: { - const v = readU32(buf, offset); - offset += 4; - return v; - } - case PropFlags.RefArr: { - const len = readU32(buf, offset); - offset += 4; - for (let j = 0; j < len; j++) { - const v = readU32(buf, offset); - offset += 4; - return v; - } - - return len; - } - - case PropFlags.String: - offset += 4; - break; - case PropFlags.Bool: - offset++; - break; - case PropFlags.Null: - case PropFlags.Undefined: - break; - } - } - - return -1; + getFirstChild(idx) { + const siblings = this.getSiblings(idx); + return siblings[0] ?? -1; } /** - * @param {number} offset + * @param {number} idx * @returns {number} */ - getLastChild(offset) { - const { buf } = this; - - // type + parentId + SpanLo + SpanHi - offset += 1 + 4 + 4 + 4; - - let last = -1; - - const count = buf[offset++]; - for (let i = 0; i < count; i++) { - const _prop = buf[offset++]; - const kind = buf[offset++]; - - switch (kind) { - case PropFlags.Ref: { - const v = readU32(buf, offset); - offset += 4; - last = v; - break; - } - case PropFlags.RefArr: { - const len = readU32(buf, offset); - offset += 4; - for (let j = 0; j < len; j++) { - const v = readU32(buf, offset); - last = v; - offset += 4; - } - - break; - } - - case PropFlags.String: - offset += 4; - break; - case PropFlags.Bool: - offset++; - break; - case PropFlags.Null: - case PropFlags.Undefined: - break; - } - } - - return last; + getLastChild(idx) { + const siblings = this.getSiblings(idx); + return siblings.at(-1) ?? -1; } /** - * @param {number} id + * @param {number} idx * @returns {number[]} */ - getSiblings(id) { - const { buf } = this; + getSiblings(idx) { + const { buf } = this.ctx; + const parent = readParent(buf, idx); - const result = findChildOffset(buf, id); - // Happens for program nodes - if (result === null) return []; - - if (result[1] === -1) { - return [id]; + // Only RefArrays have siblings + const parentType = readType(buf, parent); + if (parentType !== AST_GROUP_TYPE) { + return []; } - let offset = result[0]; - const count = readU32(buf, offset); - offset += 4; - - /** @type {number[]} */ const out = []; - for (let i = 0; i < count; i++) { - const v = readU32(buf, offset); - offset += 4; - out.push(v); + let child = readChild(buf, parent); + while (child > AST_IDX_INVALID) { + out.push(child); + child = readNext(buf, child); } return out; @@ -683,7 +646,7 @@ class MatchCtx { /** * @param {Uint8Array} buf - * @param {AstContext} buf + * @returns {AstContext} */ function createAstContext(buf) { /** @type {Map} */ @@ -691,6 +654,8 @@ function createAstContext(buf) { // The buffer has a few offsets at the end which allows us to easily // jump to the relevant sections of the message. + const propsOffset = readU32(buf, buf.length - 24); + const spansOffset = readU32(buf, buf.length - 20); const typeMapOffset = readU32(buf, buf.length - 16); const propMapOffset = readU32(buf, buf.length - 12); const strTableOffset = readU32(buf, buf.length - 8); @@ -702,9 +667,7 @@ function createAstContext(buf) { const stringCount = readU32(buf, offset); offset += 4; - // TODO(@marvinhagemeister): We could lazily decode the strings on an as needed basis. - // Not sure if this matters much in practice though. - let id = 0; + let strId = 0; for (let i = 0; i < stringCount; i++) { const len = readU32(buf, offset); offset += 4; @@ -712,8 +675,8 @@ function createAstContext(buf) { const strBytes = buf.slice(offset, offset + len); offset += len; const s = DECODER.decode(strBytes); - strTable.set(id, s); - id++; + strTable.set(strId, s); + strId++; } if (strTable.size !== stringCount) { @@ -755,14 +718,17 @@ function createAstContext(buf) { buf, strTable, rootOffset, + spansOffset, + propsOffset, nodes: new Map(), strTableOffset, strByProp, strByType, typeByStr, propByStr, - matcher: new MatchCtx(buf, strTable, strByType), + matcher: /** @type {*} */ (null), }; + ctx.matcher = new MatchCtx(ctx); setNodeGetters(ctx); @@ -903,76 +869,53 @@ export function runPluginsForFile(fileName, serializedAst) { /** * @param {AstContext} ctx * @param {CompiledVisitor[]} visitors - * @param {number} offset + * @param {number} idx */ -function traverse(ctx, visitors, offset) { - // The 0 offset is used to denote an empty/placeholder node - if (offset === 0) return; - - const originalOffset = offset; +function traverse(ctx, visitors, idx) { + if (idx === AST_IDX_INVALID) return; const { buf } = ctx; + const nodeType = readType(ctx.buf, idx); /** @type {VisitorFn[] | null} */ let exits = null; - for (let i = 0; i < visitors.length; i++) { - const v = visitors[i]; - - if (v.matcher(ctx.matcher, offset)) { - if (v.info.exit !== NOOP) { - if (exits === null) { - exits = [v.info.exit]; - } else { - exits.push(v.info.exit); + // Only visit if it's an actual node + if (nodeType !== AST_GROUP_TYPE) { + // Loop over visitors and check if any selector matches + for (let i = 0; i < visitors.length; i++) { + const v = visitors[i]; + if (v.matcher(ctx.matcher, idx)) { + if (v.info.exit !== NOOP) { + if (exits === null) { + exits = [v.info.exit]; + } else { + exits.push(v.info.exit); + } } - } - if (v.info.enter !== NOOP) { - const node = /** @type {*} */ (getNode(ctx, offset)); - v.info.enter(node); + if (v.info.enter !== NOOP) { + const node = /** @type {*} */ (getNode(ctx, idx)); + v.info.enter(node); + } } } } - // Search for node references in the properties of the current node. All - // other properties can be ignored. try { - // type + parentId + SpanLo + SpanHi - offset += 1 + 4 + 4 + 4; + const childIdx = readChild(buf, idx); + if (childIdx > AST_IDX_INVALID) { + traverse(ctx, visitors, childIdx); + } - const propCount = buf[offset]; - offset += 1; - - for (let i = 0; i < propCount; i++) { - const kind = buf[offset + 1]; - offset += 2; // propId + propFlags - - if (kind === PropFlags.Ref) { - const next = readU32(buf, offset); - offset += 4; - traverse(ctx, visitors, next); - } else if (kind === PropFlags.RefArr) { - const len = readU32(buf, offset); - offset += 4; - - for (let j = 0; j < len; j++) { - const child = readU32(buf, offset); - offset += 4; - traverse(ctx, visitors, child); - } - } else if (kind === PropFlags.String) { - offset += 4; - } else if (kind === PropFlags.Bool) { - offset += 1; - } else if (kind === PropFlags.Null || kind === PropFlags.Undefined) { - // No value - } + const nextIdx = readNext(buf, idx); + if (nextIdx > AST_IDX_INVALID) { + traverse(ctx, visitors, nextIdx); } } finally { if (exits !== null) { for (let i = 0; i < exits.length; i++) { - const node = /** @type {*} */ (getNode(ctx, originalOffset)); + const node = /** @type {*} */ (getNode(ctx, idx)); exits[i](node); } } @@ -1009,38 +952,46 @@ function _dump(ctx) { // deno-lint-ignore no-console console.log(); - let offset = 0; + // @ts-ignore dump fn + // deno-lint-ignore no-console + console.log(); - while (offset < strTableOffset) { - const type = buf[offset]; - const name = getString(ctx.strTable, ctx.strByType[type]); + let idx = 0; + while (idx < (strTableOffset / NODE_SIZE)) { + const type = readType(buf, idx); + const child = readChild(buf, idx); + const next = readNext(buf, idx); + const parent = readParent(buf, idx); + const range = readSpan(ctx, idx); + + const name = type === AST_IDX_INVALID + ? "" + : type === AST_GROUP_TYPE + ? "" + : getString(ctx.strTable, ctx.strByType[type]); // @ts-ignore dump fn // deno-lint-ignore no-console - console.log(`${name}, offset: ${offset}, type: ${type}`); - offset += 1; + console.log(`${name}, idx: ${idx}, type: ${type}`); - const parent = readU32(buf, offset); - offset += 4; // @ts-ignore dump fn // deno-lint-ignore no-console - console.log(` parent: ${parent}`); - - const start = readU32(buf, offset); - offset += 4; - const end = readU32(buf, offset); - offset += 4; + console.log(` child: ${child}, next: ${next}, parent: ${parent}`); // @ts-ignore dump fn // deno-lint-ignore no-console - console.log(` range: ${start} -> ${end}`); + console.log(` range: ${range[0]}, ${range[1]}`); - const count = buf[offset++]; + const rawOffset = readRawPropOffset(ctx.buf, idx); + let propOffset = readPropOffset(ctx, idx); + const count = buf[propOffset++]; // @ts-ignore dump fn // deno-lint-ignore no-console - console.log(` prop count: ${count}`); + console.log( + ` prop count: ${count}, prop offset: ${propOffset} raw offset: ${rawOffset}`, + ); for (let i = 0; i < count; i++) { - const prop = buf[offset++]; - const kind = buf[offset++]; + const prop = buf[propOffset++]; + const kind = buf[propOffset++]; const name = getString(ctx.strTable, ctx.strByProp[prop]); let kindName = "unknown"; @@ -1051,40 +1002,36 @@ function _dump(ctx) { } } + const v = readU32(buf, propOffset); + propOffset += 4; + if (kind === PropFlags.Ref) { - const v = readU32(buf, offset); - offset += 4; // @ts-ignore dump fn // deno-lint-ignore no-console console.log(` ${name}: ${v} (${kindName}, ${prop})`); } else if (kind === PropFlags.RefArr) { - const len = readU32(buf, offset); - offset += 4; // @ts-ignore dump fn // deno-lint-ignore no-console - console.log(` ${name}: Array(${len}) (${kindName}, ${prop})`); - - for (let j = 0; j < len; j++) { - const v = readU32(buf, offset); - offset += 4; - // @ts-ignore dump fn - // deno-lint-ignore no-console - console.log(` - ${v} (${prop})`); - } + console.log(` ${name}: RefArray: ${v}, (${kindName}, ${prop})`); } else if (kind === PropFlags.Bool) { - const v = buf[offset]; - offset += 1; // @ts-ignore dump fn // deno-lint-ignore no-console console.log(` ${name}: ${v} (${kindName}, ${prop})`); } else if (kind === PropFlags.String) { - const v = readU32(buf, offset); - offset += 4; + const raw = getString(ctx.strTable, v); // @ts-ignore dump fn // deno-lint-ignore no-console - console.log( - ` ${name}: ${getString(ctx.strTable, v)} (${kindName}, ${prop})`, - ); + console.log(` ${name}: ${raw} (${kindName}, ${prop})`); + } else if (kind === PropFlags.Number) { + const raw = getString(ctx.strTable, v); + // @ts-ignore dump fn + // deno-lint-ignore no-console + console.log(` ${name}: ${raw} (${kindName}, ${prop})`); + } else if (kind === PropFlags.Regex) { + const raw = getString(ctx.strTable, v); + // @ts-ignore dump fn + // deno-lint-ignore no-console + console.log(` ${name}: ${raw} (${kindName}, ${prop})`); } else if (kind === PropFlags.Null) { // @ts-ignore dump fn // deno-lint-ignore no-console @@ -1093,8 +1040,27 @@ function _dump(ctx) { // @ts-ignore dump fn // deno-lint-ignore no-console console.log(` ${name}: undefined (${kindName}, ${prop})`); + } else if (kind === PropFlags.BigInt) { + const raw = getString(ctx.strTable, v); + // @ts-ignore dump fn + // deno-lint-ignore no-console + console.log(` ${name}: ${raw} (${kindName}, ${prop})`); + } else if (kind === PropFlags.Obj) { + let offset = v + ctx.propsOffset; + const count = readU32(ctx.buf, offset); + offset += 4; + + // @ts-ignore dump fn + // deno-lint-ignore no-console + console.log( + ` ${name}: Object (${count}) (${kindName}, ${prop}), raw offset ${v}`, + ); + + // TODO(@marvinhagemeister): Show object } } + + idx++; } } @@ -1107,9 +1073,10 @@ function _dump(ctx) { */ function runLintPlugin(plugin, fileName, sourceText) { installPlugin(plugin); - const serializedAst = op_lint_create_serialized_ast(fileName, sourceText); try { + const serializedAst = op_lint_create_serialized_ast(fileName, sourceText); + runPluginsForFile(fileName, serializedAst); } finally { // During testing we don't want to keep plugins around diff --git a/cli/js/40_lint_selector.js b/cli/js/40_lint_selector.js index c4cd523f9f..362130076a 100644 --- a/cli/js/40_lint_selector.js +++ b/cli/js/40_lint_selector.js @@ -744,8 +744,7 @@ export function compileSelector(selector) { fn = matchNthChild(node, fn); break; case PSEUDO_HAS: - // FIXME - // fn = matchIs(part, fn); + // TODO(@marvinhagemeister) throw new Error("TODO: :has"); case PSEUDO_NOT: fn = matchNot(node.selectors, fn); @@ -767,8 +766,7 @@ export function compileSelector(selector) { */ function matchFirstChild(next) { return (ctx, id) => { - const parent = ctx.getParent(id); - const first = ctx.getFirstChild(parent); + const first = ctx.getFirstChild(id); return first === id && next(ctx, first); }; } @@ -779,8 +777,7 @@ function matchFirstChild(next) { */ function matchLastChild(next) { return (ctx, id) => { - const parent = ctx.getParent(id); - const last = ctx.getLastChild(parent); + const last = ctx.getLastChild(id); return last === id && next(ctx, id); }; } @@ -955,7 +952,9 @@ function matchElem(part, next) { else if (part.elem === 0) return false; const type = ctx.getType(id); - if (type > 0 && type === part.elem) return next(ctx, id); + if (type > 0 && type === part.elem) { + return next(ctx, id); + } return false; }; @@ -968,7 +967,16 @@ function matchElem(part, next) { */ function matchAttrExists(attr, next) { return (ctx, id) => { - return ctx.hasAttrPath(id, attr.prop, 0) ? next(ctx, id) : false; + try { + ctx.getAttrPathValue(id, attr.prop, 0); + return next(ctx, id); + } catch (err) { + if (err === -1) { + return false; + } + + throw err; + } }; } @@ -979,9 +987,15 @@ function matchAttrExists(attr, next) { */ function matchAttrBin(attr, next) { return (ctx, id) => { - if (!ctx.hasAttrPath(id, attr.prop, 0)) return false; - const value = ctx.getAttrPathValue(id, attr.prop, 0); - if (!matchAttrValue(attr, value)) return false; + try { + const value = ctx.getAttrPathValue(id, attr.prop, 0); + if (!matchAttrValue(attr, value)) return false; + } catch (err) { + if (err === -1) { + return false; + } + throw err; + } return next(ctx, id); }; } diff --git a/cli/js/40_lint_types.d.ts b/cli/js/40_lint_types.d.ts index db2202981a..f07d16581e 100644 --- a/cli/js/40_lint_types.d.ts +++ b/cli/js/40_lint_types.d.ts @@ -12,6 +12,8 @@ export interface AstContext { strTableOffset: number; rootOffset: number; nodes: Map; + spansOffset: number; + propsOffset: number; strByType: number[]; strByProp: number[]; typeByStr: Map; @@ -19,6 +21,12 @@ export interface AstContext { matcher: MatchContext; } +export interface Node { + range: Range; +} + +export type Range = [number, number]; + // TODO(@marvinhagemeister) Remove once we land "official" types export interface RuleContext { id: string; @@ -121,7 +129,6 @@ export interface MatchContext { getSiblings(id: number): number[]; getParent(id: number): number; getType(id: number): number; - hasAttrPath(id: number, propIds: number[], idx: number): boolean; getAttrPathValue(id: number, propIds: number[], idx: number): unknown; } diff --git a/cli/js/40_test.js b/cli/js/40_test.js index 34d9ec6550..3dbb7ec340 100644 --- a/cli/js/40_test.js +++ b/cli/js/40_test.js @@ -26,7 +26,7 @@ const { TypeError, } = primordials; -import { setExitHandler } from "ext:runtime/30_os.js"; +import { setExitHandler } from "ext:deno_os/30_os.js"; // Capture `Deno` global so that users deleting or mangling it, won't // have impact on our sanitizers. diff --git a/cli/lib/Cargo.toml b/cli/lib/Cargo.toml new file mode 100644 index 0000000000..945e189f45 --- /dev/null +++ b/cli/lib/Cargo.toml @@ -0,0 +1,46 @@ +# Copyright 2018-2025 the Deno authors. MIT license. + +[package] +name = "deno_lib" +version = "0.3.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true +description = "Shared code between the Deno CLI and denort" + +[lib] +path = "lib.rs" + +[dependencies] +capacity_builder.workspace = true +deno_config.workspace = true +deno_error.workspace = true +deno_fs = { workspace = true, features = ["sync_fs"] } +deno_media_type.workspace = true +deno_node = { workspace = true, features = ["sync_fs"] } +deno_npm.workspace = true +deno_path_util.workspace = true +deno_resolver = { workspace = true, features = ["sync"] } +deno_runtime.workspace = true +deno_semver.workspace = true +deno_terminal.workspace = true +env_logger = "=0.10.0" +faster-hex.workspace = true +indexmap.workspace = true +libsui.workspace = true +log = { workspace = true, features = ["serde"] } +node_resolver = { workspace = true, features = ["sync"] } +parking_lot.workspace = true +ring.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sys_traits = { workspace = true, features = ["getrandom"] } +thiserror.workspace = true +tokio.workspace = true +twox-hash.workspace = true +url.workspace = true + +[dev-dependencies] +test_util.workspace = true diff --git a/cli/lib/README.md b/cli/lib/README.md new file mode 100644 index 0000000000..bc6d7b57d0 --- /dev/null +++ b/cli/lib/README.md @@ -0,0 +1,4 @@ +# deno_lib + +This crate contains the shared code between the Deno CLI and denort. It is +highly unstable. diff --git a/cli/lib/args.rs b/cli/lib/args.rs new file mode 100644 index 0000000000..3e64f5ab4c --- /dev/null +++ b/cli/lib/args.rs @@ -0,0 +1,199 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::io::BufReader; +use std::io::Cursor; +use std::io::Read; +use std::io::Seek; +use std::path::PathBuf; +use std::sync::LazyLock; + +use deno_runtime::colors; +use deno_runtime::deno_tls::deno_native_certs::load_native_certs; +use deno_runtime::deno_tls::rustls; +use deno_runtime::deno_tls::rustls::RootCertStore; +use deno_runtime::deno_tls::rustls_pemfile; +use deno_runtime::deno_tls::webpki_roots; +use deno_semver::npm::NpmPackageReqReference; +use serde::Deserialize; +use serde::Serialize; +use thiserror::Error; + +pub fn npm_pkg_req_ref_to_binary_command( + req_ref: &NpmPackageReqReference, +) -> String { + req_ref + .sub_path() + .map(|s| s.to_string()) + .unwrap_or_else(|| req_ref.req().name.to_string()) +} + +pub fn has_trace_permissions_enabled() -> bool { + has_flag_env_var("DENO_TRACE_PERMISSIONS") +} + +pub fn has_flag_env_var(name: &str) -> bool { + let value = std::env::var(name); + matches!(value.as_ref().map(|s| s.as_str()), Ok("1")) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum CaData { + /// The string is a file path + File(String), + /// This variant is not exposed as an option in the CLI, it is used internally + /// for standalone binaries. + Bytes(Vec), +} + +#[derive(Error, Debug, Clone, deno_error::JsError)] +#[class(generic)] +pub enum RootCertStoreLoadError { + #[error( + "Unknown certificate store \"{0}\" specified (allowed: \"system,mozilla\")" + )] + UnknownStore(String), + #[error("Unable to add pem file to certificate store: {0}")] + FailedAddPemFile(String), + #[error("Failed opening CA file: {0}")] + CaFileOpenError(String), +} + +/// Create and populate a root cert store based on the passed options and +/// environment. +pub fn get_root_cert_store( + maybe_root_path: Option, + maybe_ca_stores: Option>, + maybe_ca_data: Option, +) -> Result { + let mut root_cert_store = RootCertStore::empty(); + let ca_stores: Vec = maybe_ca_stores + .or_else(|| { + let env_ca_store = std::env::var("DENO_TLS_CA_STORE").ok()?; + Some( + env_ca_store + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(), + ) + }) + .unwrap_or_else(|| vec!["mozilla".to_string()]); + + for store in ca_stores.iter() { + match store.as_str() { + "mozilla" => { + root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.to_vec()); + } + "system" => { + let roots = load_native_certs().expect("could not load platform certs"); + for root in roots { + if let Err(err) = root_cert_store + .add(rustls::pki_types::CertificateDer::from(root.0.clone())) + { + log::error!( + "{}", + colors::yellow(&format!( + "Unable to add system certificate to certificate store: {:?}", + err + )) + ); + let hex_encoded_root = faster_hex::hex_string(&root.0); + log::error!("{}", colors::gray(&hex_encoded_root)); + } + } + } + _ => { + return Err(RootCertStoreLoadError::UnknownStore(store.clone())); + } + } + } + + let ca_data = + maybe_ca_data.or_else(|| std::env::var("DENO_CERT").ok().map(CaData::File)); + if let Some(ca_data) = ca_data { + let result = match ca_data { + CaData::File(ca_file) => { + let ca_file = if let Some(root) = &maybe_root_path { + root.join(&ca_file) + } else { + PathBuf::from(ca_file) + }; + let certfile = std::fs::File::open(ca_file).map_err(|err| { + RootCertStoreLoadError::CaFileOpenError(err.to_string()) + })?; + let mut reader = BufReader::new(certfile); + rustls_pemfile::certs(&mut reader).collect::, _>>() + } + CaData::Bytes(data) => { + let mut reader = BufReader::new(Cursor::new(data)); + rustls_pemfile::certs(&mut reader).collect::, _>>() + } + }; + + match result { + Ok(certs) => { + root_cert_store.add_parsable_certificates(certs); + } + Err(e) => { + return Err(RootCertStoreLoadError::FailedAddPemFile(e.to_string())); + } + } + } + + Ok(root_cert_store) +} + +/// State provided to the process via an environment variable. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NpmProcessState { + pub kind: NpmProcessStateKind, + pub local_node_modules_path: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum NpmProcessStateKind { + Snapshot(deno_npm::resolution::SerializedNpmResolutionSnapshot), + Byonm, +} + +pub static NPM_PROCESS_STATE: LazyLock> = + LazyLock::new(|| { + use deno_runtime::deno_process::NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME; + let fd = std::env::var(NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME).ok()?; + std::env::remove_var(NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME); + let fd = fd.parse::().ok()?; + let mut file = { + use deno_runtime::deno_io::FromRawIoHandle; + unsafe { std::fs::File::from_raw_io_handle(fd as _) } + }; + let mut buf = Vec::new(); + // seek to beginning. after the file is written the position will be inherited by this subprocess, + // and also this file might have been read before + file.seek(std::io::SeekFrom::Start(0)).unwrap(); + file + .read_to_end(&mut buf) + .inspect_err(|e| { + log::error!("failed to read npm process state from fd {fd}: {e}"); + }) + .ok()?; + let state: NpmProcessState = serde_json::from_slice(&buf) + .inspect_err(|e| { + log::error!( + "failed to deserialize npm process state: {e} {}", + String::from_utf8_lossy(&buf) + ) + }) + .ok()?; + Some(state) + }); + +#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct UnstableConfig { + // TODO(bartlomieju): remove in Deno 2.5 + pub legacy_flag_enabled: bool, // --unstable + pub bare_node_builtins: bool, + pub detect_cjs: bool, + pub sloppy_imports: bool, + pub npm_lazy_caching: bool, + pub features: Vec, // --unstabe-kv --unstable-cron +} diff --git a/cli/lib/build.rs b/cli/lib/build.rs new file mode 100644 index 0000000000..1f52e0c02a --- /dev/null +++ b/cli/lib/build.rs @@ -0,0 +1,42 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +fn main() { + // todo(dsherret): remove this after Deno 2.2.0 is published and then + // align the version of this crate with Deno then. We need to wait because + // there was previously a deno_lib 2.2.0 published (https://crates.io/crates/deno_lib/versions) + let version_path = std::path::Path::new(".").join("version.txt"); + println!("cargo:rerun-if-changed={}", version_path.display()); + #[allow(clippy::disallowed_methods)] + let text = std::fs::read_to_string(version_path).unwrap(); + println!("cargo:rustc-env=DENO_VERSION={}", text); + + let commit_hash = git_commit_hash(); + println!("cargo:rustc-env=GIT_COMMIT_HASH={}", commit_hash); + println!("cargo:rerun-if-env-changed=GIT_COMMIT_HASH"); + println!( + "cargo:rustc-env=GIT_COMMIT_HASH_SHORT={}", + &commit_hash[..7] + ); +} + +fn git_commit_hash() -> String { + if let Ok(output) = std::process::Command::new("git") + .arg("rev-list") + .arg("-1") + .arg("HEAD") + .output() + { + if output.status.success() { + std::str::from_utf8(&output.stdout[..40]) + .unwrap() + .to_string() + } else { + // When not in git repository + // (e.g. when the user install by `cargo install deno`) + "UNKNOWN".to_string() + } + } else { + // When there is no git command for some reason + "UNKNOWN".to_string() + } +} diff --git a/cli/lib/clippy.toml b/cli/lib/clippy.toml new file mode 100644 index 0000000000..0060289cf2 --- /dev/null +++ b/cli/lib/clippy.toml @@ -0,0 +1,48 @@ +disallowed-methods = [ + { path = "std::env::current_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::canonicalize", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::is_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::is_file", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::is_symlink", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::metadata", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::read_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::read_link", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::symlink_metadata", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::try_exists", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::exists", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::canonicalize", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::is_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::is_file", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::is_symlink", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::metadata", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::read_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::read_link", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::symlink_metadata", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::PathBuf::try_exists", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::env::set_current_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::env::temp_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::canonicalize", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::copy", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::create_dir_all", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::create_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::DirBuilder::new", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::hard_link", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::metadata", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::OpenOptions::new", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::read_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::read_link", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::read_to_string", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::read", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::remove_dir_all", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::remove_dir", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::remove_file", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::rename", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::set_permissions", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::symlink_metadata", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::fs::write", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::canonicalize", reason = "File system operations should be done using DenoLibSys" }, + { path = "std::path::Path::exists", reason = "File system operations should be done using DenoLibSys" }, + { path = "url::Url::to_file_path", reason = "Use deno_path_util instead" }, + { path = "url::Url::from_file_path", reason = "Use deno_path_util instead" }, + { path = "url::Url::from_directory_path", reason = "Use deno_path_util instead" }, +] diff --git a/cli/lib/lib.rs b/cli/lib/lib.rs new file mode 100644 index 0000000000..6b9267805d --- /dev/null +++ b/cli/lib/lib.rs @@ -0,0 +1,11 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +pub mod args; +pub mod loader; +pub mod npm; +pub mod shared; +pub mod standalone; +pub mod sys; +pub mod util; +pub mod version; +pub mod worker; diff --git a/cli/lib/loader.rs b/cli/lib/loader.rs new file mode 100644 index 0000000000..32f97dc718 --- /dev/null +++ b/cli/lib/loader.rs @@ -0,0 +1,213 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; +use std::path::PathBuf; +use std::sync::Arc; + +use deno_media_type::MediaType; +use deno_resolver::cjs::CjsTracker; +use deno_resolver::npm::DenoInNpmPackageChecker; +use deno_runtime::deno_core::ModuleSourceCode; +use node_resolver::analyze::CjsCodeAnalyzer; +use node_resolver::analyze::NodeCodeTranslator; +use node_resolver::InNpmPackageChecker; +use node_resolver::IsBuiltInNodeModuleChecker; +use node_resolver::NpmPackageFolderResolver; +use thiserror::Error; +use url::Url; + +use crate::sys::DenoLibSys; +use crate::util::text_encoding::from_utf8_lossy_cow; + +pub struct ModuleCodeStringSource { + pub code: ModuleSourceCode, + pub found_url: Url, + pub media_type: MediaType, +} + +#[derive(Debug, Error, deno_error::JsError)] +#[class(type)] +#[error("{media_type} files are not supported in npm packages: {specifier}")] +pub struct NotSupportedKindInNpmError { + pub media_type: MediaType, + pub specifier: Url, +} + +#[derive(Debug, Error, deno_error::JsError)] +pub enum NpmModuleLoadError { + #[class(inherit)] + #[error(transparent)] + UrlToFilePath(#[from] deno_path_util::UrlToFilePathError), + #[class(inherit)] + #[error(transparent)] + NotSupportedKindInNpm(#[from] NotSupportedKindInNpmError), + #[class(inherit)] + #[error(transparent)] + ClosestPkgJson(#[from] node_resolver::errors::ClosestPkgJsonError), + #[class(inherit)] + #[error(transparent)] + TranslateCjsToEsm(#[from] node_resolver::analyze::TranslateCjsToEsmError), + #[class(inherit)] + #[error("Unable to load {}{}", file_path.display(), maybe_referrer.as_ref().map(|r| format!(" imported from {}", r)).unwrap_or_default())] + UnableToLoad { + file_path: PathBuf, + maybe_referrer: Option, + #[source] + #[inherit] + source: std::io::Error, + }, + #[class(inherit)] + #[error( + "{}", + format_dir_import_message(file_path, maybe_referrer, suggestion) + )] + DirImport { + file_path: PathBuf, + maybe_referrer: Option, + suggestion: Option<&'static str>, + #[source] + #[inherit] + source: std::io::Error, + }, +} + +fn format_dir_import_message( + file_path: &std::path::Path, + maybe_referrer: &Option, + suggestion: &Option<&'static str>, +) -> String { + // directory imports are not allowed when importing from an + // ES module, so provide the user with a helpful error message + let dir_path = file_path; + let mut msg = "Directory import ".to_string(); + msg.push_str(&dir_path.to_string_lossy()); + if let Some(referrer) = maybe_referrer { + msg.push_str(" is not supported resolving import from "); + msg.push_str(referrer.as_str()); + if let Some(entrypoint_name) = suggestion { + msg.push_str("\nDid you mean to import "); + msg.push_str(entrypoint_name); + msg.push_str(" within the directory?"); + } + } + msg +} + +#[derive(Clone)] +pub struct NpmModuleLoader< + TCjsCodeAnalyzer: CjsCodeAnalyzer, + TInNpmPackageChecker: InNpmPackageChecker, + TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver: NpmPackageFolderResolver, + TSys: DenoLibSys, +> { + cjs_tracker: Arc>, + sys: TSys, + node_code_translator: Arc< + NodeCodeTranslator< + TCjsCodeAnalyzer, + TInNpmPackageChecker, + TIsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver, + TSys, + >, + >, +} + +impl< + TCjsCodeAnalyzer: CjsCodeAnalyzer, + TInNpmPackageChecker: InNpmPackageChecker, + TIsBuiltInNodeModuleChecker: IsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver: NpmPackageFolderResolver, + TSys: DenoLibSys, + > + NpmModuleLoader< + TCjsCodeAnalyzer, + TInNpmPackageChecker, + TIsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver, + TSys, + > +{ + pub fn new( + cjs_tracker: Arc>, + node_code_translator: Arc< + NodeCodeTranslator< + TCjsCodeAnalyzer, + TInNpmPackageChecker, + TIsBuiltInNodeModuleChecker, + TNpmPackageFolderResolver, + TSys, + >, + >, + sys: TSys, + ) -> Self { + Self { + cjs_tracker, + node_code_translator, + sys, + } + } + + pub async fn load( + &self, + specifier: &Url, + maybe_referrer: Option<&Url>, + ) -> Result { + let file_path = deno_path_util::url_to_file_path(specifier)?; + let code = self.sys.fs_read(&file_path).map_err(|source| { + if self.sys.fs_is_dir_no_err(&file_path) { + let suggestion = ["index.mjs", "index.js", "index.cjs"] + .into_iter() + .find(|e| self.sys.fs_is_file_no_err(file_path.join(e))); + NpmModuleLoadError::DirImport { + file_path, + maybe_referrer: maybe_referrer.cloned(), + suggestion, + source, + } + } else { + NpmModuleLoadError::UnableToLoad { + file_path, + maybe_referrer: maybe_referrer.cloned(), + source, + } + } + })?; + + let media_type = MediaType::from_specifier(specifier); + if media_type.is_emittable() { + return Err(NpmModuleLoadError::NotSupportedKindInNpm( + 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 = from_utf8_lossy_cow(code); + ModuleSourceCode::String( + self + .node_code_translator + .translate_cjs_to_esm(specifier, Some(code)) + .await? + .into_owned() + .into(), + ) + } else { + // esm and json code is untouched + ModuleSourceCode::Bytes(match code { + Cow::Owned(bytes) => bytes.into_boxed_slice().into(), + Cow::Borrowed(bytes) => bytes.into(), + }) + }; + + Ok(ModuleCodeStringSource { + code, + found_url: specifier.clone(), + media_type: MediaType::from_specifier(specifier), + }) + } +} diff --git a/cli/lib/npm/mod.rs b/cli/lib/npm/mod.rs new file mode 100644 index 0000000000..b6ad5d1be5 --- /dev/null +++ b/cli/lib/npm/mod.rs @@ -0,0 +1,80 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +mod permission_checker; + +use std::path::Path; +use std::sync::Arc; + +use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; +use deno_resolver::npm::ByonmNpmResolver; +use deno_resolver::npm::ManagedNpmResolverRc; +use deno_resolver::npm::NpmResolver; +use deno_runtime::deno_process::NpmProcessStateProvider; +use deno_runtime::deno_process::NpmProcessStateProviderRc; +pub use permission_checker::NpmRegistryReadPermissionChecker; +pub use permission_checker::NpmRegistryReadPermissionCheckerMode; + +use crate::args::NpmProcessState; +use crate::args::NpmProcessStateKind; +use crate::sys::DenoLibSys; + +pub fn create_npm_process_state_provider( + npm_resolver: &NpmResolver, +) -> NpmProcessStateProviderRc { + match npm_resolver { + NpmResolver::Byonm(byonm_npm_resolver) => { + Arc::new(ByonmNpmProcessStateProvider(byonm_npm_resolver.clone())) + } + NpmResolver::Managed(managed_npm_resolver) => { + Arc::new(ManagedNpmProcessStateProvider(managed_npm_resolver.clone())) + } + } +} + +pub fn npm_process_state( + snapshot: ValidSerializedNpmResolutionSnapshot, + node_modules_path: Option<&Path>, +) -> String { + serde_json::to_string(&NpmProcessState { + kind: NpmProcessStateKind::Snapshot(snapshot.into_serialized()), + local_node_modules_path: node_modules_path + .map(|p| p.to_string_lossy().to_string()), + }) + .unwrap() +} + +#[derive(Debug)] +pub struct ManagedNpmProcessStateProvider( + pub ManagedNpmResolverRc, +); + +impl NpmProcessStateProvider + for ManagedNpmProcessStateProvider +{ + fn get_npm_process_state(&self) -> String { + npm_process_state( + self.0.resolution().serialized_valid_snapshot(), + self.0.root_node_modules_path(), + ) + } +} + +#[derive(Debug)] +pub struct ByonmNpmProcessStateProvider( + pub Arc>, +); + +impl NpmProcessStateProvider + for ByonmNpmProcessStateProvider +{ + fn get_npm_process_state(&self) -> String { + serde_json::to_string(&NpmProcessState { + kind: NpmProcessStateKind::Byonm, + local_node_modules_path: self + .0 + .root_node_modules_path() + .map(|p| p.to_string_lossy().to_string()), + }) + .unwrap() + } +} diff --git a/cli/npm/permission_checker.rs b/cli/lib/npm/permission_checker.rs similarity index 92% rename from cli/npm/permission_checker.rs rename to cli/lib/npm/permission_checker.rs index 53031b5bd4..ebed1270f3 100644 --- a/cli/npm/permission_checker.rs +++ b/cli/lib/npm/permission_checker.rs @@ -6,12 +6,11 @@ use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; -use deno_core::parking_lot::Mutex; use deno_error::JsErrorBox; use deno_runtime::deno_node::NodePermissions; -use sys_traits::FsCanonicalize; +use parking_lot::Mutex; -use crate::sys::CliSys; +use crate::sys::DenoLibSys; #[derive(Debug)] pub enum NpmRegistryReadPermissionCheckerMode { @@ -21,8 +20,8 @@ pub enum NpmRegistryReadPermissionCheckerMode { } #[derive(Debug)] -pub struct NpmRegistryReadPermissionChecker { - sys: CliSys, +pub struct NpmRegistryReadPermissionChecker { + sys: TSys, cache: Mutex>, mode: NpmRegistryReadPermissionCheckerMode, } @@ -37,8 +36,8 @@ struct EnsureRegistryReadPermissionError { source: std::io::Error, } -impl NpmRegistryReadPermissionChecker { - pub fn new(sys: CliSys, mode: NpmRegistryReadPermissionCheckerMode) -> Self { +impl NpmRegistryReadPermissionChecker { + pub fn new(sys: TSys, mode: NpmRegistryReadPermissionCheckerMode) -> Self { Self { sys, cache: Default::default(), diff --git a/cli/shared.rs b/cli/lib/shared.rs similarity index 79% rename from cli/shared.rs rename to cli/lib/shared.rs index 6a28473edd..15ec3c2440 100644 --- a/cli/shared.rs +++ b/cli/lib/shared.rs @@ -1,8 +1,11 @@ // Copyright 2018-2025 the Deno authors. MIT license. /// This module is shared between build script and the binaries. Use it sparsely. -use deno_core::anyhow::bail; -use deno_core::error::AnyError; +use thiserror::Error; + +#[derive(Debug, Error)] +#[error("Unrecognized release channel: {0}")] +pub struct UnrecognizedReleaseChannelError(pub String); #[derive(Debug, Clone, Copy, PartialEq)] pub enum ReleaseChannel { @@ -50,13 +53,17 @@ impl ReleaseChannel { // NOTE(bartlomieju): do not ever change these values, tools like `patchver` // rely on them. #[allow(unused)] - pub fn deserialize(str_: &str) -> Result { + pub fn deserialize( + str_: &str, + ) -> Result { Ok(match str_ { "stable" => Self::Stable, "canary" => Self::Canary, "rc" => Self::Rc, "lts" => Self::Lts, - unknown => bail!("Unrecognized release channel: {}", unknown), + unknown => { + return Err(UnrecognizedReleaseChannelError(unknown.to_string())) + } }) } } diff --git a/cli/lib/standalone/binary.rs b/cli/lib/standalone/binary.rs new file mode 100644 index 0000000000..ae02197bf4 --- /dev/null +++ b/cli/lib/standalone/binary.rs @@ -0,0 +1,389 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; +use std::collections::BTreeMap; + +use deno_config::workspace::PackageJsonDepResolution; +use deno_media_type::MediaType; +use deno_runtime::deno_permissions::PermissionsOptions; +use deno_runtime::deno_telemetry::OtelConfig; +use deno_semver::Version; +use indexmap::IndexMap; +use node_resolver::analyze::CjsAnalysisExports; +use serde::Deserialize; +use serde::Serialize; +use url::Url; + +use super::virtual_fs::FileSystemCaseSensitivity; +use crate::args::UnstableConfig; + +pub const MAGIC_BYTES: &[u8; 8] = b"d3n0l4nd"; + +pub trait DenoRtDeserializable<'a>: Sized { + fn deserialize(input: &'a [u8]) -> std::io::Result<(&'a [u8], Self)>; +} + +impl<'a> DenoRtDeserializable<'a> for Cow<'a, [u8]> { + fn deserialize(input: &'a [u8]) -> std::io::Result<(&'a [u8], Self)> { + let (input, data) = read_bytes_with_u32_len(input)?; + Ok((input, Cow::Borrowed(data))) + } +} + +pub trait DenoRtSerializable<'a> { + fn serialize( + &'a self, + builder: &mut capacity_builder::BytesBuilder<'a, Vec>, + ); +} + +#[derive(Deserialize, Serialize)] +pub enum NodeModules { + Managed { + /// Relative path for the node_modules directory in the vfs. + node_modules_dir: Option, + }, + Byonm { + root_node_modules_dir: Option, + }, +} + +#[derive(Deserialize, Serialize)] +pub struct SerializedWorkspaceResolverImportMap { + pub specifier: String, + pub json: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializedResolverWorkspaceJsrPackage { + pub relative_base: String, + pub name: String, + pub version: Option, + pub exports: IndexMap, +} + +#[derive(Deserialize, Serialize)] +pub struct SerializedWorkspaceResolver { + pub import_map: Option, + pub jsr_pkgs: Vec, + pub package_jsons: BTreeMap, + pub pkg_json_resolution: PackageJsonDepResolution, +} + +// Note: Don't use hashmaps/hashsets. Ensure the serialization +// is deterministic. +#[derive(Deserialize, Serialize)] +pub struct Metadata { + pub argv: Vec, + pub seed: Option, + pub code_cache_key: Option, + pub permissions: PermissionsOptions, + pub location: Option, + pub v8_flags: Vec, + pub log_level: Option, + pub ca_stores: Option>, + pub ca_data: Option>, + pub unsafely_ignore_certificate_errors: Option>, + pub env_vars_from_env_file: IndexMap, + pub workspace_resolver: SerializedWorkspaceResolver, + pub entrypoint_key: String, + pub node_modules: Option, + pub unstable_config: UnstableConfig, + pub otel_config: OtelConfig, + pub vfs_case_sensitivity: FileSystemCaseSensitivity, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct SpecifierId(u32); + +impl SpecifierId { + pub fn new(id: u32) -> Self { + Self(id) + } +} + +impl<'a> capacity_builder::BytesAppendable<'a> for SpecifierId { + fn append_to_builder( + self, + builder: &mut capacity_builder::BytesBuilder<'a, TBytes>, + ) { + builder.append_le(self.0); + } +} + +impl<'a> DenoRtSerializable<'a> for SpecifierId { + fn serialize( + &'a self, + builder: &mut capacity_builder::BytesBuilder<'a, Vec>, + ) { + builder.append_le(self.0); + } +} + +impl<'a> DenoRtDeserializable<'a> for SpecifierId { + fn deserialize(input: &'a [u8]) -> std::io::Result<(&'a [u8], Self)> { + let (input, id) = read_u32(input)?; + Ok((input, Self(id))) + } +} + +#[derive(Deserialize, Serialize)] +pub enum CjsExportAnalysisEntry { + Esm, + Cjs(CjsAnalysisExports), +} + +const HAS_TRANSPILED_FLAG: u8 = 1 << 0; +const HAS_SOURCE_MAP_FLAG: u8 = 1 << 1; +const HAS_CJS_EXPORT_ANALYSIS_FLAG: u8 = 1 << 2; + +pub struct RemoteModuleEntry<'a> { + pub media_type: MediaType, + pub data: Cow<'a, [u8]>, + pub maybe_transpiled: Option>, + pub maybe_source_map: Option>, + pub maybe_cjs_export_analysis: Option>, +} + +impl<'a> DenoRtSerializable<'a> for RemoteModuleEntry<'a> { + fn serialize( + &'a self, + builder: &mut capacity_builder::BytesBuilder<'a, Vec>, + ) { + fn append_maybe_data<'a>( + builder: &mut capacity_builder::BytesBuilder<'a, Vec>, + maybe_data: Option<&'a [u8]>, + ) { + if let Some(data) = maybe_data { + builder.append_le(data.len() as u32); + builder.append(data); + } + } + + let mut has_data_flags = 0; + if self.maybe_transpiled.is_some() { + has_data_flags |= HAS_TRANSPILED_FLAG; + } + if self.maybe_source_map.is_some() { + has_data_flags |= HAS_SOURCE_MAP_FLAG; + } + if self.maybe_cjs_export_analysis.is_some() { + has_data_flags |= HAS_CJS_EXPORT_ANALYSIS_FLAG; + } + builder.append(serialize_media_type(self.media_type)); + builder.append_le(self.data.len() as u32); + builder.append(self.data.as_ref()); + builder.append(has_data_flags); + append_maybe_data(builder, self.maybe_transpiled.as_deref()); + append_maybe_data(builder, self.maybe_source_map.as_deref()); + append_maybe_data(builder, self.maybe_cjs_export_analysis.as_deref()); + } +} + +impl<'a> DenoRtDeserializable<'a> for RemoteModuleEntry<'a> { + fn deserialize(input: &'a [u8]) -> std::io::Result<(&'a [u8], Self)> { + #[allow(clippy::type_complexity)] + fn deserialize_data_if_has_flag( + input: &[u8], + has_data_flags: u8, + flag: u8, + ) -> std::io::Result<(&[u8], Option>)> { + if has_data_flags & flag != 0 { + let (input, bytes) = read_bytes_with_u32_len(input)?; + Ok((input, Some(Cow::Borrowed(bytes)))) + } else { + Ok((input, None)) + } + } + + let (input, media_type) = MediaType::deserialize(input)?; + let (input, data) = read_bytes_with_u32_len(input)?; + let (input, has_data_flags) = read_u8(input)?; + let (input, maybe_transpiled) = + deserialize_data_if_has_flag(input, has_data_flags, HAS_TRANSPILED_FLAG)?; + let (input, maybe_source_map) = + deserialize_data_if_has_flag(input, has_data_flags, HAS_SOURCE_MAP_FLAG)?; + let (input, maybe_cjs_export_analysis) = deserialize_data_if_has_flag( + input, + has_data_flags, + HAS_CJS_EXPORT_ANALYSIS_FLAG, + )?; + Ok(( + input, + Self { + media_type, + data: Cow::Borrowed(data), + maybe_transpiled, + maybe_source_map, + maybe_cjs_export_analysis, + }, + )) + } +} + +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, + } +} + +impl<'a> DenoRtDeserializable<'a> for MediaType { + fn deserialize(input: &'a [u8]) -> std::io::Result<(&'a [u8], Self)> { + let (input, value) = read_u8(input)?; + let value = match value { + 0 => MediaType::JavaScript, + 1 => MediaType::Jsx, + 2 => MediaType::Mjs, + 3 => MediaType::Cjs, + 4 => MediaType::TypeScript, + 5 => MediaType::Mts, + 6 => MediaType::Cts, + 7 => MediaType::Dts, + 8 => MediaType::Dmts, + 9 => MediaType::Dcts, + 10 => MediaType::Tsx, + 11 => MediaType::Json, + 12 => MediaType::Wasm, + 13 => MediaType::Css, + 14 => MediaType::SourceMap, + 15 => MediaType::Unknown, + value => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Unknown media type value: {value}"), + )) + } + }; + Ok((input, value)) + } +} + +/// Data stored keyed by specifier. +pub struct SpecifierDataStore { + data: IndexMap, +} + +impl Default for SpecifierDataStore { + fn default() -> Self { + Self { + data: IndexMap::new(), + } + } +} + +impl SpecifierDataStore { + pub fn with_capacity(capacity: usize) -> Self { + Self { + data: IndexMap::with_capacity(capacity), + } + } + + pub fn iter(&self) -> impl Iterator { + self.data.iter().map(|(k, v)| (*k, v)) + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn contains(&self, specifier: SpecifierId) -> bool { + self.data.contains_key(&specifier) + } + + pub fn add(&mut self, specifier: SpecifierId, value: TData) { + self.data.insert(specifier, value); + } + + pub fn get(&self, specifier: SpecifierId) -> Option<&TData> { + self.data.get(&specifier) + } +} + +impl<'a, TData> SpecifierDataStore +where + TData: DenoRtSerializable<'a> + 'a, +{ + pub fn serialize( + &'a self, + builder: &mut capacity_builder::BytesBuilder<'a, Vec>, + ) { + builder.append_le(self.len() as u32); + for (specifier, value) in self.iter() { + builder.append(specifier); + value.serialize(builder); + } + } +} + +impl<'a, TData> DenoRtDeserializable<'a> for SpecifierDataStore +where + TData: DenoRtDeserializable<'a>, +{ + fn deserialize(input: &'a [u8]) -> std::io::Result<(&'a [u8], Self)> { + let (input, len) = read_u32_as_usize(input)?; + let mut data = IndexMap::with_capacity(len); + let mut input = input; + for _ in 0..len { + let (new_input, specifier) = SpecifierId::deserialize(input)?; + let (new_input, value) = TData::deserialize(new_input)?; + data.insert(specifier, value); + input = new_input; + } + Ok((input, Self { data })) + } +} + +fn read_bytes_with_u32_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> { + let (input, len) = read_u32_as_usize(input)?; + let (input, data) = read_bytes(input, len)?; + Ok((input, data)) +} + +fn read_u32_as_usize(input: &[u8]) -> std::io::Result<(&[u8], usize)> { + read_u32(input).map(|(input, len)| (input, len as usize)) +} + +fn read_u32(input: &[u8]) -> std::io::Result<(&[u8], u32)> { + let (input, len_bytes) = read_bytes(input, 4)?; + let len = u32::from_le_bytes(len_bytes.try_into().unwrap()); + Ok((input, len)) +} + +fn read_u8(input: &[u8]) -> std::io::Result<(&[u8], u8)> { + check_has_len(input, 1)?; + Ok((&input[1..], input[0])) +} + +fn read_bytes(input: &[u8], len: usize) -> std::io::Result<(&[u8], &[u8])> { + check_has_len(input, len)?; + let (len_bytes, input) = input.split_at(len); + Ok((input, len_bytes)) +} + +#[inline(always)] +fn check_has_len(input: &[u8], len: usize) -> std::io::Result<()> { + if input.len() < len { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Unexpected end of data", + )) + } else { + Ok(()) + } +} diff --git a/cli/lib/standalone/mod.rs b/cli/lib/standalone/mod.rs new file mode 100644 index 0000000000..42a5f20122 --- /dev/null +++ b/cli/lib/standalone/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +pub mod binary; +pub mod virtual_fs; diff --git a/cli/lib/standalone/virtual_fs.rs b/cli/lib/standalone/virtual_fs.rs new file mode 100644 index 0000000000..124c2a0002 --- /dev/null +++ b/cli/lib/standalone/virtual_fs.rs @@ -0,0 +1,999 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::cmp::Ordering; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::collections::VecDeque; +use std::fmt; +use std::path::Path; +use std::path::PathBuf; + +use deno_path_util::normalize_path; +use deno_path_util::strip_unc_prefix; +use deno_runtime::colors; +use deno_runtime::deno_core::anyhow::bail; +use deno_runtime::deno_core::anyhow::Context; +use deno_runtime::deno_core::error::AnyError; +use indexmap::IndexSet; +use serde::de; +use serde::de::SeqAccess; +use serde::de::Visitor; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; +use serde::Serializer; + +#[derive(Debug, PartialEq, Eq)] +pub enum WindowsSystemRootablePath { + /// The root of the system above any drive letters. + WindowSystemRoot, + Path(PathBuf), +} + +impl WindowsSystemRootablePath { + pub fn root_for_current_os() -> Self { + if cfg!(windows) { + WindowsSystemRootablePath::WindowSystemRoot + } else { + WindowsSystemRootablePath::Path(PathBuf::from("/")) + } + } + + pub fn join(&self, name_component: &str) -> PathBuf { + // this method doesn't handle multiple components + debug_assert!( + !name_component.contains('\\'), + "Invalid component: {}", + name_component + ); + debug_assert!( + !name_component.contains('/'), + "Invalid component: {}", + name_component + ); + + match self { + WindowsSystemRootablePath::WindowSystemRoot => { + // windows drive letter + PathBuf::from(&format!("{}\\", name_component)) + } + WindowsSystemRootablePath::Path(path) => path.join(name_component), + } + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum FileSystemCaseSensitivity { + #[serde(rename = "s")] + Sensitive, + #[serde(rename = "i")] + Insensitive, +} +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct VirtualDirectoryEntries(Vec); + +impl VirtualDirectoryEntries { + pub fn new(mut entries: Vec) -> Self { + // needs to be sorted by name + entries.sort_by(|a, b| a.name().cmp(b.name())); + Self(entries) + } + + pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, VfsEntry> { + self.0.iter_mut() + } + + pub fn iter(&self) -> std::slice::Iter<'_, VfsEntry> { + self.0.iter() + } + + pub fn take_inner(&mut self) -> Vec { + std::mem::take(&mut self.0) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn get_by_name( + &self, + name: &str, + case_sensitivity: FileSystemCaseSensitivity, + ) -> Option<&VfsEntry> { + self + .binary_search(name, case_sensitivity) + .ok() + .map(|index| &self.0[index]) + } + + pub fn get_mut_by_name( + &mut self, + name: &str, + case_sensitivity: FileSystemCaseSensitivity, + ) -> Option<&mut VfsEntry> { + self + .binary_search(name, case_sensitivity) + .ok() + .map(|index| &mut self.0[index]) + } + + pub fn get_mut_by_index(&mut self, index: usize) -> Option<&mut VfsEntry> { + self.0.get_mut(index) + } + + pub fn get_by_index(&self, index: usize) -> Option<&VfsEntry> { + self.0.get(index) + } + + pub fn binary_search( + &self, + name: &str, + case_sensitivity: FileSystemCaseSensitivity, + ) -> Result { + match case_sensitivity { + FileSystemCaseSensitivity::Sensitive => { + self.0.binary_search_by(|e| e.name().cmp(name)) + } + FileSystemCaseSensitivity::Insensitive => self.0.binary_search_by(|e| { + e.name() + .chars() + .zip(name.chars()) + .map(|(a, b)| a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase())) + .find(|&ord| ord != Ordering::Equal) + .unwrap_or_else(|| e.name().len().cmp(&name.len())) + }), + } + } + + pub fn insert( + &mut self, + entry: VfsEntry, + case_sensitivity: FileSystemCaseSensitivity, + ) -> usize { + match self.binary_search(entry.name(), case_sensitivity) { + Ok(index) => { + self.0[index] = entry; + index + } + Err(insert_index) => { + self.0.insert(insert_index, entry); + insert_index + } + } + } + + pub fn insert_or_modify( + &mut self, + name: &str, + case_sensitivity: FileSystemCaseSensitivity, + on_insert: impl FnOnce() -> VfsEntry, + on_modify: impl FnOnce(&mut VfsEntry), + ) -> usize { + match self.binary_search(name, case_sensitivity) { + Ok(index) => { + on_modify(&mut self.0[index]); + index + } + Err(insert_index) => { + self.0.insert(insert_index, on_insert()); + insert_index + } + } + } + + pub fn remove(&mut self, index: usize) -> VfsEntry { + self.0.remove(index) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VirtualDirectory { + #[serde(rename = "n")] + pub name: String, + // should be sorted by name + #[serde(rename = "e")] + pub entries: VirtualDirectoryEntries, +} + +#[derive(Debug, Clone, Copy)] +pub struct OffsetWithLength { + pub offset: u64, + pub len: u64, +} + +// serialize as an array in order to save space +impl Serialize for OffsetWithLength { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let array = [self.offset, self.len]; + array.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for OffsetWithLength { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct OffsetWithLengthVisitor; + + impl<'de> Visitor<'de> for OffsetWithLengthVisitor { + type Value = OffsetWithLength; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array with two elements: [offset, len]") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let offset = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let len = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &self))?; + Ok(OffsetWithLength { offset, len }) + } + } + + deserializer.deserialize_seq(OffsetWithLengthVisitor) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VirtualFile { + #[serde(rename = "n")] + pub name: String, + #[serde(rename = "o")] + pub offset: OffsetWithLength, + #[serde(rename = "m", skip_serializing_if = "Option::is_none")] + pub transpiled_offset: Option, + #[serde(rename = "c", skip_serializing_if = "Option::is_none")] + pub cjs_export_analysis_offset: Option, + #[serde(rename = "s", skip_serializing_if = "Option::is_none")] + pub source_map_offset: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VirtualSymlinkParts(Vec); + +impl VirtualSymlinkParts { + pub fn from_path(path: &Path) -> Self { + Self( + path + .components() + .filter(|c| !matches!(c, std::path::Component::RootDir)) + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect(), + ) + } + + pub fn take_parts(&mut self) -> Vec { + std::mem::take(&mut self.0) + } + + pub fn parts(&self) -> &[String] { + &self.0 + } + + pub fn set_parts(&mut self, parts: Vec) { + self.0 = parts; + } + + pub fn display(&self) -> String { + self.0.join("/") + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VirtualSymlink { + #[serde(rename = "n")] + pub name: String, + #[serde(rename = "p")] + pub dest_parts: VirtualSymlinkParts, +} + +impl VirtualSymlink { + pub fn resolve_dest_from_root(&self, root: &Path) -> PathBuf { + let mut dest = root.to_path_buf(); + for part in &self.dest_parts.0 { + dest.push(part); + } + dest + } +} + +#[derive(Debug, Copy, Clone)] +pub enum VfsEntryRef<'a> { + Dir(&'a VirtualDirectory), + File(&'a VirtualFile), + Symlink(&'a VirtualSymlink), +} + +impl VfsEntryRef<'_> { + pub fn name(&self) -> &str { + match self { + Self::Dir(dir) => &dir.name, + Self::File(file) => &file.name, + Self::Symlink(symlink) => &symlink.name, + } + } +} + +// todo(dsherret): we should store this more efficiently in the binary +#[derive(Debug, Serialize, Deserialize)] +pub enum VfsEntry { + Dir(VirtualDirectory), + File(VirtualFile), + Symlink(VirtualSymlink), +} + +impl VfsEntry { + pub fn name(&self) -> &str { + match self { + Self::Dir(dir) => &dir.name, + Self::File(file) => &file.name, + Self::Symlink(symlink) => &symlink.name, + } + } + + pub fn as_ref(&self) -> VfsEntryRef { + match self { + VfsEntry::Dir(dir) => VfsEntryRef::Dir(dir), + VfsEntry::File(file) => VfsEntryRef::File(file), + VfsEntry::Symlink(symlink) => VfsEntryRef::Symlink(symlink), + } + } +} + +pub static DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME: &str = + ".deno_compile_node_modules"; + +#[derive(Debug)] +pub struct BuiltVfs { + pub root_path: WindowsSystemRootablePath, + pub case_sensitivity: FileSystemCaseSensitivity, + pub entries: VirtualDirectoryEntries, + pub files: Vec>, +} + +#[derive(Debug, Default)] +struct FilesData { + files: Vec>, + current_offset: u64, + file_offsets: HashMap<(String, usize), OffsetWithLength>, +} + +impl FilesData { + pub fn file_bytes(&self, offset: OffsetWithLength) -> Option<&[u8]> { + if offset.len == 0 { + return Some(&[]); + } + + // the debug assertions in this method should never happen + // because it would indicate providing an offset not in the vfs + let mut count: u64 = 0; + for file in &self.files { + // clippy wanted a match + match count.cmp(&offset.offset) { + Ordering::Equal => { + debug_assert_eq!(offset.len, file.len() as u64); + if offset.len == file.len() as u64 { + return Some(file); + } else { + return None; + } + } + Ordering::Less => { + count += file.len() as u64; + } + Ordering::Greater => { + debug_assert!(false); + return None; + } + } + } + debug_assert!(false); + None + } + + pub fn add_data(&mut self, data: Vec) -> OffsetWithLength { + if data.is_empty() { + return OffsetWithLength { offset: 0, len: 0 }; + } + let checksum = crate::util::checksum::gen(&[&data]); + match self.file_offsets.entry((checksum, data.len())) { + Entry::Occupied(occupied_entry) => { + let offset_and_len = *occupied_entry.get(); + debug_assert_eq!(data.len() as u64, offset_and_len.len); + offset_and_len + } + Entry::Vacant(vacant_entry) => { + let offset_and_len = OffsetWithLength { + offset: self.current_offset, + len: data.len() as u64, + }; + vacant_entry.insert(offset_and_len); + self.current_offset += offset_and_len.len; + self.files.push(data); + offset_and_len + } + } + } +} + +pub struct AddFileDataOptions { + pub data: Vec, + pub maybe_transpiled: Option>, + pub maybe_source_map: Option>, + pub maybe_cjs_export_analysis: Option>, +} + +#[derive(Debug)] +pub struct VfsBuilder { + executable_root: VirtualDirectory, + files: FilesData, + /// The minimum root directory that should be included in the VFS. + min_root_dir: Option, + case_sensitivity: FileSystemCaseSensitivity, +} + +impl Default for VfsBuilder { + fn default() -> Self { + Self::new() + } +} + +impl VfsBuilder { + pub fn new() -> Self { + Self { + executable_root: VirtualDirectory { + name: "/".to_string(), + entries: Default::default(), + }, + files: Default::default(), + min_root_dir: Default::default(), + // This is not exactly correct because file systems on these OSes + // may be case-sensitive or not based on the directory, but this + // is a good enough approximation and limitation. In the future, + // we may want to store this information per directory instead + // depending on the feedback we get. + case_sensitivity: if cfg!(windows) || cfg!(target_os = "macos") { + FileSystemCaseSensitivity::Insensitive + } else { + FileSystemCaseSensitivity::Sensitive + }, + } + } + + pub fn case_sensitivity(&self) -> FileSystemCaseSensitivity { + self.case_sensitivity + } + + pub fn files_len(&self) -> usize { + self.files.files.len() + } + + pub fn file_bytes(&self, offset: OffsetWithLength) -> Option<&[u8]> { + self.files.file_bytes(offset) + } + + /// Add a directory that might be the minimum root directory + /// of the VFS. + /// + /// For example, say the user has a deno.json and specifies an + /// import map in a parent directory. The import map won't be + /// included in the VFS, but its base will meaning we need to + /// tell the VFS builder to include the base of the import map + /// by calling this method. + pub fn add_possible_min_root_dir(&mut self, path: &Path) { + self.add_dir_raw(path); + + match &self.min_root_dir { + Some(WindowsSystemRootablePath::WindowSystemRoot) => { + // already the root dir + } + Some(WindowsSystemRootablePath::Path(current_path)) => { + let mut common_components = Vec::new(); + for (a, b) in current_path.components().zip(path.components()) { + if a != b { + break; + } + common_components.push(a); + } + if common_components.is_empty() { + self.min_root_dir = + Some(WindowsSystemRootablePath::root_for_current_os()); + } else { + self.min_root_dir = Some(WindowsSystemRootablePath::Path( + common_components.iter().collect(), + )); + } + } + None => { + self.min_root_dir = + Some(WindowsSystemRootablePath::Path(path.to_path_buf())); + } + } + } + + pub fn add_dir_recursive(&mut self, path: &Path) -> Result<(), AnyError> { + let target_path = self.resolve_target_path(path)?; + self.add_dir_recursive_not_symlink(&target_path) + } + + fn add_dir_recursive_not_symlink( + &mut self, + path: &Path, + ) -> Result<(), AnyError> { + self.add_dir_raw(path); + // ok, building fs implementation + #[allow(clippy::disallowed_methods)] + let read_dir = std::fs::read_dir(path) + .with_context(|| format!("Reading {}", path.display()))?; + + let mut dir_entries = + read_dir.into_iter().collect::, _>>()?; + dir_entries.sort_by_cached_key(|entry| entry.file_name()); // determinism + + for entry in dir_entries { + let file_type = entry.file_type()?; + let path = entry.path(); + + if file_type.is_dir() { + self.add_dir_recursive_not_symlink(&path)?; + } else if file_type.is_file() { + self.add_file_at_path_not_symlink(&path)?; + } else if file_type.is_symlink() { + match self.add_symlink(&path) { + Ok(target) => match target { + SymlinkTarget::File(target) => { + self.add_file_at_path_not_symlink(&target)? + } + SymlinkTarget::Dir(target) => { + self.add_dir_recursive_not_symlink(&target)?; + } + }, + Err(err) => { + log::warn!( + "{} Failed resolving symlink. Ignoring.\n Path: {}\n Message: {:#}", + colors::yellow("Warning"), + path.display(), + err + ); + } + } + } + } + + Ok(()) + } + + fn add_dir_raw(&mut self, path: &Path) -> &mut VirtualDirectory { + log::debug!("Ensuring directory '{}'", path.display()); + debug_assert!(path.is_absolute()); + let mut current_dir = &mut self.executable_root; + + for component in path.components() { + if matches!(component, std::path::Component::RootDir) { + continue; + } + let name = component.as_os_str().to_string_lossy(); + let index = current_dir.entries.insert_or_modify( + &name, + self.case_sensitivity, + || { + VfsEntry::Dir(VirtualDirectory { + name: name.to_string(), + entries: Default::default(), + }) + }, + |_| { + // ignore + }, + ); + match current_dir.entries.get_mut_by_index(index) { + Some(VfsEntry::Dir(dir)) => { + current_dir = dir; + } + _ => unreachable!(), + }; + } + + current_dir + } + + pub fn get_system_root_dir_mut(&mut self) -> &mut VirtualDirectory { + &mut self.executable_root + } + + pub fn get_dir_mut(&mut self, path: &Path) -> Option<&mut VirtualDirectory> { + debug_assert!(path.is_absolute()); + let mut current_dir = &mut self.executable_root; + + for component in path.components() { + if matches!(component, std::path::Component::RootDir) { + continue; + } + let name = component.as_os_str().to_string_lossy(); + let entry = current_dir + .entries + .get_mut_by_name(&name, self.case_sensitivity)?; + match entry { + VfsEntry::Dir(dir) => { + current_dir = dir; + } + _ => unreachable!("{}", path.display()), + }; + } + + Some(current_dir) + } + + pub fn add_file_at_path(&mut self, path: &Path) -> Result<(), AnyError> { + // ok, building fs implementation + #[allow(clippy::disallowed_methods)] + let file_bytes = std::fs::read(path) + .with_context(|| format!("Reading {}", path.display()))?; + self.add_file_with_data( + path, + AddFileDataOptions { + data: file_bytes, + maybe_cjs_export_analysis: None, + maybe_transpiled: None, + maybe_source_map: None, + }, + ) + } + + fn add_file_at_path_not_symlink( + &mut self, + path: &Path, + ) -> Result<(), AnyError> { + // ok, building fs implementation + #[allow(clippy::disallowed_methods)] + let file_bytes = std::fs::read(path) + .with_context(|| format!("Reading {}", path.display()))?; + self.add_file_with_data_raw(path, file_bytes) + } + + pub fn add_file_with_data( + &mut self, + path: &Path, + options: AddFileDataOptions, + ) -> Result<(), AnyError> { + // ok, fs implementation + #[allow(clippy::disallowed_methods)] + let metadata = std::fs::symlink_metadata(path).with_context(|| { + format!("Resolving target path for '{}'", path.display()) + })?; + if metadata.is_symlink() { + let target = self.add_symlink(path)?.into_path_buf(); + self.add_file_with_data_raw_options(&target, options) + } else { + self.add_file_with_data_raw_options(path, options) + } + } + + pub fn add_file_with_data_raw( + &mut self, + path: &Path, + data: Vec, + ) -> Result<(), AnyError> { + self.add_file_with_data_raw_options( + path, + AddFileDataOptions { + data, + maybe_transpiled: None, + maybe_cjs_export_analysis: None, + maybe_source_map: None, + }, + ) + } + + fn add_file_with_data_raw_options( + &mut self, + path: &Path, + options: AddFileDataOptions, + ) -> Result<(), AnyError> { + log::debug!("Adding file '{}'", path.display()); + let case_sensitivity = self.case_sensitivity; + + let offset_and_len = self.files.add_data(options.data); + let transpiled_offset = options + .maybe_transpiled + .map(|data| self.files.add_data(data)); + let source_map_offset = options + .maybe_source_map + .map(|data| self.files.add_data(data)); + let cjs_export_analysis_offset = options + .maybe_cjs_export_analysis + .map(|data| self.files.add_data(data)); + let dir = self.add_dir_raw(path.parent().unwrap()); + let name = path.file_name().unwrap().to_string_lossy(); + + dir.entries.insert_or_modify( + &name, + case_sensitivity, + || { + VfsEntry::File(VirtualFile { + name: name.to_string(), + offset: offset_and_len, + transpiled_offset, + cjs_export_analysis_offset, + source_map_offset, + }) + }, + |entry| match entry { + VfsEntry::File(virtual_file) => { + virtual_file.offset = offset_and_len; + // doesn't overwrite to None + if transpiled_offset.is_some() { + virtual_file.transpiled_offset = transpiled_offset; + } + if source_map_offset.is_some() { + virtual_file.source_map_offset = source_map_offset; + } + if cjs_export_analysis_offset.is_some() { + virtual_file.cjs_export_analysis_offset = + cjs_export_analysis_offset; + } + } + VfsEntry::Dir(_) | VfsEntry::Symlink(_) => unreachable!(), + }, + ); + + Ok(()) + } + + fn resolve_target_path(&mut self, path: &Path) -> Result { + // ok, fs implementation + #[allow(clippy::disallowed_methods)] + let metadata = std::fs::symlink_metadata(path).with_context(|| { + format!("Resolving target path for '{}'", path.display()) + })?; + if metadata.is_symlink() { + Ok(self.add_symlink(path)?.into_path_buf()) + } else { + Ok(path.to_path_buf()) + } + } + + pub fn add_symlink( + &mut self, + path: &Path, + ) -> Result { + self.add_symlink_inner(path, &mut IndexSet::new()) + } + + fn add_symlink_inner( + &mut self, + path: &Path, + visited: &mut IndexSet, + ) -> Result { + log::debug!("Adding symlink '{}'", path.display()); + let target = strip_unc_prefix( + // ok, fs implementation + #[allow(clippy::disallowed_methods)] + std::fs::read_link(path) + .with_context(|| format!("Reading symlink '{}'", path.display()))?, + ); + let case_sensitivity = self.case_sensitivity; + let target = normalize_path(path.parent().unwrap().join(&target)); + let dir = self.add_dir_raw(path.parent().unwrap()); + let name = path.file_name().unwrap().to_string_lossy(); + dir.entries.insert_or_modify( + &name, + case_sensitivity, + || { + VfsEntry::Symlink(VirtualSymlink { + name: name.to_string(), + dest_parts: VirtualSymlinkParts::from_path(&target), + }) + }, + |_| { + // ignore previously inserted + }, + ); + // ok, fs implementation + #[allow(clippy::disallowed_methods)] + let target_metadata = + std::fs::symlink_metadata(&target).with_context(|| { + format!("Reading symlink target '{}'", target.display()) + })?; + if target_metadata.is_symlink() { + if !visited.insert(target.clone()) { + // todo: probably don't error in this scenario + bail!( + "Circular symlink detected: {} -> {}", + visited + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(" -> "), + target.display() + ); + } + self.add_symlink_inner(&target, visited) + } else if target_metadata.is_dir() { + Ok(SymlinkTarget::Dir(target)) + } else { + Ok(SymlinkTarget::File(target)) + } + } + + /// Adds the CJS export analysis to the provided file. + /// + /// Warning: This will panic if the file wasn't properly + /// setup before calling this. + pub fn add_cjs_export_analysis(&mut self, path: &Path, data: Vec) { + self.add_data_for_file_or_panic(path, data, |file, offset_with_length| { + file.cjs_export_analysis_offset = Some(offset_with_length); + }) + } + + fn add_data_for_file_or_panic( + &mut self, + path: &Path, + data: Vec, + update_file: impl FnOnce(&mut VirtualFile, OffsetWithLength), + ) { + let offset_with_length = self.files.add_data(data); + let case_sensitivity = self.case_sensitivity; + let dir = self.get_dir_mut(path.parent().unwrap()).unwrap(); + let name = path.file_name().unwrap().to_string_lossy(); + let file = dir + .entries + .get_mut_by_name(&name, case_sensitivity) + .unwrap(); + match file { + VfsEntry::File(virtual_file) => { + update_file(virtual_file, offset_with_length); + } + VfsEntry::Dir(_) | VfsEntry::Symlink(_) => { + unreachable!() + } + } + } + + /// Iterates through all the files in the virtual file system. + pub fn iter_files( + &self, + ) -> impl Iterator + '_ { + FileIterator { + pending_dirs: VecDeque::from([( + WindowsSystemRootablePath::root_for_current_os(), + &self.executable_root, + )]), + current_dir_index: 0, + } + } + + pub fn build(self) -> BuiltVfs { + fn strip_prefix_from_symlinks( + dir: &mut VirtualDirectory, + parts: &[String], + ) { + for entry in dir.entries.iter_mut() { + match entry { + VfsEntry::Dir(dir) => { + strip_prefix_from_symlinks(dir, parts); + } + VfsEntry::File(_) => {} + VfsEntry::Symlink(symlink) => { + let parts = symlink + .dest_parts + .take_parts() + .into_iter() + .skip(parts.len()) + .collect(); + symlink.dest_parts.set_parts(parts); + } + } + } + } + + let mut current_dir = self.executable_root; + let mut current_path = WindowsSystemRootablePath::root_for_current_os(); + loop { + if current_dir.entries.len() != 1 { + break; + } + if self.min_root_dir.as_ref() == Some(¤t_path) { + break; + } + match current_dir.entries.iter().next().unwrap() { + VfsEntry::Dir(dir) => { + if dir.name == DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME { + // special directory we want to maintain + break; + } + match current_dir.entries.remove(0) { + VfsEntry::Dir(dir) => { + current_path = + WindowsSystemRootablePath::Path(current_path.join(&dir.name)); + current_dir = dir; + } + _ => unreachable!(), + }; + } + VfsEntry::File(_) | VfsEntry::Symlink(_) => break, + } + } + if let WindowsSystemRootablePath::Path(path) = ¤t_path { + strip_prefix_from_symlinks( + &mut current_dir, + VirtualSymlinkParts::from_path(path).parts(), + ); + } + BuiltVfs { + root_path: current_path, + case_sensitivity: self.case_sensitivity, + entries: current_dir.entries, + files: self.files.files, + } + } +} + +struct FileIterator<'a> { + pending_dirs: VecDeque<(WindowsSystemRootablePath, &'a VirtualDirectory)>, + current_dir_index: usize, +} + +impl<'a> Iterator for FileIterator<'a> { + type Item = (PathBuf, &'a VirtualFile); + + fn next(&mut self) -> Option { + while !self.pending_dirs.is_empty() { + let (dir_path, current_dir) = self.pending_dirs.front()?; + if let Some(entry) = + current_dir.entries.get_by_index(self.current_dir_index) + { + self.current_dir_index += 1; + match entry { + VfsEntry::Dir(virtual_directory) => { + self.pending_dirs.push_back(( + WindowsSystemRootablePath::Path( + dir_path.join(&virtual_directory.name), + ), + virtual_directory, + )); + } + VfsEntry::File(virtual_file) => { + return Some((dir_path.join(&virtual_file.name), virtual_file)); + } + VfsEntry::Symlink(_) => { + // ignore + } + } + } else { + self.pending_dirs.pop_front(); + self.current_dir_index = 0; + } + } + None + } +} + +#[derive(Debug)] +pub enum SymlinkTarget { + File(PathBuf), + Dir(PathBuf), +} + +impl SymlinkTarget { + pub fn into_path_buf(self) -> PathBuf { + match self { + Self::File(path) => path, + Self::Dir(path) => path, + } + } +} diff --git a/cli/lib/sys.rs b/cli/lib/sys.rs new file mode 100644 index 0000000000..f5ca48b41c --- /dev/null +++ b/cli/lib/sys.rs @@ -0,0 +1,37 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use deno_node::ExtNodeSys; +use sys_traits::FsCanonicalize; +use sys_traits::FsCreateDirAll; +use sys_traits::FsMetadata; +use sys_traits::FsOpen; +use sys_traits::FsRead; +use sys_traits::FsReadDir; +use sys_traits::FsRemoveFile; +use sys_traits::FsRename; +use sys_traits::SystemRandom; +use sys_traits::ThreadSleep; + +pub trait DenoLibSys: + FsCanonicalize + + FsCreateDirAll + + FsReadDir + + FsMetadata + + FsOpen + + FsRemoveFile + + FsRename + + FsRead + + ThreadSleep + + SystemRandom + + ExtNodeSys + + Clone + + Send + + Sync + + std::fmt::Debug + + 'static +{ +} + +// ok, implementation +#[allow(clippy::disallowed_types)] +impl DenoLibSys for sys_traits::impls::RealSys {} diff --git a/cli/util/checksum.rs b/cli/lib/util/checksum.rs similarity index 100% rename from cli/util/checksum.rs rename to cli/lib/util/checksum.rs diff --git a/cli/cache/common.rs b/cli/lib/util/hash.rs similarity index 100% rename from cli/cache/common.rs rename to cli/lib/util/hash.rs diff --git a/cli/util/logger.rs b/cli/lib/util/logger.rs similarity index 74% rename from cli/util/logger.rs rename to cli/lib/util/logger.rs index 2bd4760ebd..b280dc22ed 100644 --- a/cli/util/logger.rs +++ b/cli/lib/util/logger.rs @@ -2,44 +2,33 @@ use std::io::Write; -use deno_telemetry::OtelConfig; -use deno_telemetry::OtelConsoleConfig; +use deno_runtime::deno_telemetry; +use deno_runtime::deno_telemetry::OtelConfig; +use deno_runtime::deno_telemetry::OtelConsoleConfig; -use super::draw_thread::DrawThread; - -struct CliLogger { +struct CliLogger { otel_console_config: OtelConsoleConfig, logger: env_logger::Logger, + on_log_start: FnOnLogStart, + on_log_end: FnOnLogEnd, } -impl CliLogger { - pub fn new( - logger: env_logger::Logger, - otel_console_config: OtelConsoleConfig, - ) -> Self { - Self { - logger, - otel_console_config, - } - } - +impl CliLogger { pub fn filter(&self) -> log::LevelFilter { self.logger.filter() } } -impl log::Log for CliLogger { +impl log::Log + for CliLogger +{ fn enabled(&self, metadata: &log::Metadata) -> bool { self.logger.enabled(metadata) } fn log(&self, record: &log::Record) { if self.enabled(record.metadata()) { - // it was considered to hold the draw thread's internal lock - // across logging, but if outputting to stderr blocks then that - // could potentially block other threads that access the draw - // thread's state - DrawThread::hide(); + (self.on_log_start)(); match self.otel_console_config { OtelConsoleConfig::Ignore => { @@ -54,7 +43,7 @@ impl log::Log for CliLogger { } } - DrawThread::show(); + (self.on_log_end)(); } } @@ -63,8 +52,20 @@ impl log::Log for CliLogger { } } -pub fn init(maybe_level: Option, otel_config: Option) { - let log_level = maybe_level.unwrap_or(log::Level::Info); +pub struct InitLoggingOptions { + pub on_log_start: FnOnLogStart, + pub on_log_end: FnOnLogEnd, + pub maybe_level: Option, + pub otel_config: Option, +} + +pub fn init< + FOnLogStart: Fn() + Send + Sync + 'static, + FnOnLogEnd: Fn() + Send + Sync + 'static, +>( + options: InitLoggingOptions, +) { + let log_level = options.maybe_level.unwrap_or(log::Level::Info); let logger = env_logger::Builder::from_env( env_logger::Env::new() // Use `DENO_LOG` and `DENO_LOG_STYLE` instead of `RUST_` prefix @@ -117,12 +118,15 @@ pub fn init(maybe_level: Option, otel_config: Option) { }) .build(); - let cli_logger = CliLogger::new( + let cli_logger = CliLogger { + on_log_start: options.on_log_start, + on_log_end: options.on_log_end, logger, - otel_config + otel_console_config: options + .otel_config .map(|c| c.console) .unwrap_or(OtelConsoleConfig::Ignore), - ); + }; let max_level = cli_logger.filter(); let r = log::set_boxed_logger(Box::new(cli_logger)); if r.is_ok() { diff --git a/cli/lib/util/mod.rs b/cli/lib/util/mod.rs new file mode 100644 index 0000000000..27643a2009 --- /dev/null +++ b/cli/lib/util/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +pub mod checksum; +pub mod hash; +pub mod logger; +pub mod result; +pub mod text_encoding; +pub mod v8; diff --git a/cli/util/result.rs b/cli/lib/util/result.rs similarity index 84% rename from cli/util/result.rs rename to cli/lib/util/result.rs index 0c1a75b1ce..3e302e5dfc 100644 --- a/cli/util/result.rs +++ b/cli/lib/util/result.rs @@ -4,10 +4,10 @@ use std::convert::Infallible; use std::fmt::Debug; use std::fmt::Display; -use deno_core::error::AnyError; -use deno_core::error::CoreError; use deno_error::JsErrorBox; use deno_error::JsErrorClass; +use deno_runtime::deno_core::error::AnyError; +use deno_runtime::deno_core::error::CoreError; pub trait InfallibleResultExt { fn unwrap_infallible(self) -> T; @@ -36,7 +36,7 @@ pub fn any_and_jserrorbox_downcast_ref< }) .or_else(|| { err.downcast_ref::().and_then(|e| match e { - CoreError::JsNative(e) => e.as_any().downcast_ref::(), + CoreError::JsBox(e) => e.as_any().downcast_ref::(), _ => None, }) }) diff --git a/cli/lib/util/text_encoding.rs b/cli/lib/util/text_encoding.rs new file mode 100644 index 0000000000..5b6e5f43b6 --- /dev/null +++ b/cli/lib/util/text_encoding.rs @@ -0,0 +1,45 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; +use std::sync::Arc; + +#[inline(always)] +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) }, + } +} + +#[inline(always)] +pub fn from_utf8_lossy_cow(bytes: Cow<[u8]>) -> Cow { + match bytes { + Cow::Borrowed(bytes) => String::from_utf8_lossy(bytes), + Cow::Owned(bytes) => Cow::Owned(from_utf8_lossy_owned(bytes)), + } +} + +/// 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) + }) +} diff --git a/cli/lib/util/v8.rs b/cli/lib/util/v8.rs new file mode 100644 index 0000000000..976fbf531b --- /dev/null +++ b/cli/lib/util/v8.rs @@ -0,0 +1,14 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +#[inline(always)] +pub fn construct_v8_flags( + default_v8_flags: &[String], + v8_flags: &[String], + env_v8_flags: Vec, +) -> Vec { + std::iter::once("UNUSED_BUT_NECESSARY_ARG0".to_owned()) + .chain(default_v8_flags.iter().cloned()) + .chain(env_v8_flags) + .chain(v8_flags.iter().cloned()) + .collect::>() +} diff --git a/cli/lib/version.rs b/cli/lib/version.rs new file mode 100644 index 0000000000..88a25dffeb --- /dev/null +++ b/cli/lib/version.rs @@ -0,0 +1,94 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; + +use deno_runtime::deno_telemetry::OtelRuntimeConfig; + +use crate::shared::ReleaseChannel; + +pub fn otel_runtime_config() -> OtelRuntimeConfig { + OtelRuntimeConfig { + runtime_name: Cow::Borrowed("deno"), + runtime_version: Cow::Borrowed(crate::version::DENO_VERSION_INFO.deno), + } +} + +const GIT_COMMIT_HASH: &str = env!("GIT_COMMIT_HASH"); +const TYPESCRIPT: &str = "5.6.2"; +const DENO_VERSION: &str = env!("DENO_VERSION"); +// TODO(bartlomieju): ideally we could remove this const. +const IS_CANARY: bool = option_env!("DENO_CANARY").is_some(); +// TODO(bartlomieju): this is temporary, to allow Homebrew to cut RC releases as well +const IS_RC: bool = option_env!("DENO_RC").is_some(); + +pub static DENO_VERSION_INFO: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + let release_channel = libsui::find_section("denover") + .and_then(|buf| std::str::from_utf8(buf).ok()) + .and_then(|str_| ReleaseChannel::deserialize(str_).ok()) + .unwrap_or({ + if IS_CANARY { + ReleaseChannel::Canary + } else if IS_RC { + ReleaseChannel::Rc + } else { + ReleaseChannel::Stable + } + }); + + DenoVersionInfo { + deno: if release_channel == ReleaseChannel::Canary { + concat!(env!("DENO_VERSION"), "+", env!("GIT_COMMIT_HASH_SHORT")) + } else { + env!("DENO_VERSION") + }, + + release_channel, + + git_hash: GIT_COMMIT_HASH, + + // Keep in sync with `deno` field. + user_agent: if release_channel == ReleaseChannel::Canary { + concat!( + "Deno/", + env!("DENO_VERSION"), + "+", + env!("GIT_COMMIT_HASH_SHORT") + ) + } else { + concat!("Deno/", env!("DENO_VERSION")) + }, + + typescript: TYPESCRIPT, + } + }); + +pub struct DenoVersionInfo { + /// Human-readable version of the current Deno binary. + /// + /// For stable release, a semver, eg. `v1.46.2`. + /// For canary release, a semver + 7-char git hash, eg. `v1.46.3+asdfqwq`. + pub deno: &'static str, + + pub release_channel: ReleaseChannel, + + /// A full git hash. + pub git_hash: &'static str, + + /// A user-agent header that will be used in HTTP client. + pub user_agent: &'static str, + + pub typescript: &'static str, +} + +impl DenoVersionInfo { + /// For stable release, a semver like, eg. `v1.46.2`. + /// For canary release a full git hash, eg. `9bdab6fb6b93eb43b1930f40987fa4997287f9c8`. + pub fn version_or_git_hash(&self) -> &'static str { + if self.release_channel == ReleaseChannel::Canary { + self.git_hash + } else { + DENO_VERSION + } + } +} diff --git a/cli/lib/version.txt b/cli/lib/version.txt new file mode 100644 index 0000000000..9671f9a9bd --- /dev/null +++ b/cli/lib/version.txt @@ -0,0 +1 @@ +2.1.7 \ No newline at end of file diff --git a/cli/lib/worker.rs b/cli/lib/worker.rs new file mode 100644 index 0000000000..01862bfd82 --- /dev/null +++ b/cli/lib/worker.rs @@ -0,0 +1,716 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; + +use deno_core::error::JsError; +use deno_node::NodeRequireLoaderRc; +use deno_resolver::npm::DenoInNpmPackageChecker; +use deno_resolver::npm::NpmResolver; +use deno_runtime::colors; +use deno_runtime::deno_broadcast_channel::InMemoryBroadcastChannel; +use deno_runtime::deno_core; +use deno_runtime::deno_core::error::CoreError; +use deno_runtime::deno_core::v8; +use deno_runtime::deno_core::CompiledWasmModuleStore; +use deno_runtime::deno_core::Extension; +use deno_runtime::deno_core::FeatureChecker; +use deno_runtime::deno_core::JsRuntime; +use deno_runtime::deno_core::LocalInspectorSession; +use deno_runtime::deno_core::ModuleLoader; +use deno_runtime::deno_core::SharedArrayBufferStore; +use deno_runtime::deno_fs; +use deno_runtime::deno_node::NodeExtInitServices; +use deno_runtime::deno_node::NodeRequireLoader; +use deno_runtime::deno_node::NodeResolver; +use deno_runtime::deno_permissions::PermissionsContainer; +use deno_runtime::deno_process::NpmProcessStateProviderRc; +use deno_runtime::deno_telemetry::OtelConfig; +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::worker_host::CreateWebWorkerCb; +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_runtime::UNSTABLE_GRANULAR_FLAGS; +use node_resolver::errors::ResolvePkgJsonBinExportError; +use url::Url; + +use crate::args::has_trace_permissions_enabled; +use crate::sys::DenoLibSys; +use crate::util::checksum; + +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, + ) -> CreateModuleLoaderResult; + + fn create_for_worker( + &self, + parent_permissions: PermissionsContainer, + permissions: PermissionsContainer, + ) -> CreateModuleLoaderResult; +} + +enum StorageKeyResolverStrategy { + Specified(Option), + UseMainModule, +} + +pub struct StorageKeyResolver(StorageKeyResolverStrategy); + +impl StorageKeyResolver { + pub fn from_flag(location: &Url) -> Self { + // if a location is set, then the ascii serialization of the location is + // used, unless the origin is opaque, and then no storage origin is set, as + // we can't expect the origin to be reproducible + let storage_origin = location.origin(); + Self(StorageKeyResolverStrategy::Specified( + if storage_origin.is_tuple() { + Some(storage_origin.ascii_serialization()) + } else { + None + }, + )) + } + + pub fn from_config_file_url(url: &Url) -> Self { + Self(StorageKeyResolverStrategy::Specified(Some(url.to_string()))) + } + + pub fn new_use_main_module() -> Self { + Self(StorageKeyResolverStrategy::UseMainModule) + } + + /// Creates a storage key resolver that will always resolve to being empty. + pub fn empty() -> Self { + Self(StorageKeyResolverStrategy::Specified(None)) + } + + /// Resolves the storage key to use based on the current flags, config, or main module. + pub fn resolve_storage_key(&self, main_module: &Url) -> Option { + // use the stored value or fall back to using the path of the main module. + match &self.0 { + StorageKeyResolverStrategy::Specified(value) => value.clone(), + StorageKeyResolverStrategy::UseMainModule => { + Some(main_module.to_string()) + } + } + } +} + +pub fn get_cache_storage_dir() -> PathBuf { + // ok because this won't ever be used by the js runtime + #[allow(clippy::disallowed_methods)] + // Note: we currently use temp_dir() to avoid managing storage size. + std::env::temp_dir().join("deno_cache") +} + +/// 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::deno_os::sys_info::mem_info(); + maybe_mem_info.map(|mem_info| { + v8::CreateParams::default() + .heap_limits_from_system_memory(mem_info.total, 0) + }) +} + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum ResolveNpmBinaryEntrypointError { + #[class(inherit)] + #[error(transparent)] + ResolvePkgJsonBinExport(ResolvePkgJsonBinExportError), + #[class(generic)] + #[error("{original:#}\n\nFallback failed: {fallback:#}")] + Fallback { + fallback: ResolveNpmBinaryEntrypointFallbackError, + original: ResolvePkgJsonBinExportError, + }, +} + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum ResolveNpmBinaryEntrypointFallbackError { + #[class(inherit)] + #[error(transparent)] + PackageSubpathResolve(node_resolver::errors::PackageSubpathResolveError), + #[class(generic)] + #[error("Cannot find module '{0}'")] + ModuleNotFound(Url), +} + +pub struct LibMainWorkerOptions { + pub argv: Vec, + pub log_level: WorkerLogLevel, + pub enable_op_summary_metrics: bool, + pub enable_testing_features: bool, + pub has_node_modules_dir: bool, + pub inspect_brk: bool, + pub inspect_wait: bool, + pub strace_ops: Option>, + pub is_inspecting: bool, + pub location: Option, + pub argv0: Option, + pub node_debug: Option, + pub otel_config: OtelConfig, + pub origin_data_folder_path: Option, + pub seed: Option, + pub unsafely_ignore_certificate_errors: Option>, + pub skip_op_registration: bool, + pub node_ipc: Option, + pub startup_snapshot: Option<&'static [u8]>, + pub serve_port: Option, + pub serve_host: Option, +} + +struct LibWorkerFactorySharedState { + blob_store: Arc, + broadcast_channel: InMemoryBroadcastChannel, + code_cache: Option>, + compiled_wasm_module_store: CompiledWasmModuleStore, + feature_checker: Arc, + fs: Arc, + maybe_inspector_server: Option>, + module_loader_factory: Box, + node_resolver: + Arc, TSys>>, + npm_process_state_provider: NpmProcessStateProviderRc, + pkg_json_resolver: Arc>, + root_cert_store_provider: Arc, + shared_array_buffer_store: SharedArrayBufferStore, + storage_key_resolver: StorageKeyResolver, + sys: TSys, + options: LibMainWorkerOptions, +} + +impl LibWorkerFactorySharedState { + fn resolve_unstable_features( + &self, + feature_checker: &FeatureChecker, + ) -> Vec { + let mut unstable_features = + Vec::with_capacity(UNSTABLE_GRANULAR_FLAGS.len()); + for granular_flag in UNSTABLE_GRANULAR_FLAGS { + if feature_checker.check(granular_flag.name) { + unstable_features.push(granular_flag.id); + } + } + unstable_features + } + + fn create_node_init_services( + &self, + node_require_loader: NodeRequireLoaderRc, + ) -> NodeExtInitServices, TSys> { + NodeExtInitServices { + node_require_loader, + node_resolver: self.node_resolver.clone(), + pkg_json_resolver: self.pkg_json_resolver.clone(), + sys: self.sys.clone(), + } + } + + fn create_web_worker_callback( + self: &Arc, + stdio: deno_runtime::deno_io::Stdio, + ) -> Arc { + let shared = self.clone(); + Arc::new(move |args| { + let maybe_inspector_server = shared.maybe_inspector_server.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 = + shared.create_web_worker_callback(stdio.clone()); + + let maybe_storage_key = shared + .storage_key_resolver + .resolve_storage_key(&args.main_module); + let cache_storage_dir = maybe_storage_key.map(|key| { + // TODO(@satyarohith): storage quota management + get_cache_storage_dir().join(checksum::gen(&[key.as_bytes()])) + }); + + // TODO(bartlomieju): this is cruft, update FeatureChecker to spit out + // list of enabled features. + let feature_checker = shared.feature_checker.clone(); + let unstable_features = + shared.resolve_unstable_features(feature_checker.as_ref()); + + 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.clone(), + ), + 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(), + cpu_count: std::thread::available_parallelism() + .map(|p| p.get()) + .unwrap_or(1), + log_level: shared.options.log_level, + 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), + no_color: !colors::use_color(), + color_level: colors::get_color_level(), + is_stdout_tty: deno_terminal::is_stdout_tty(), + is_stderr_tty: deno_terminal::is_stderr_tty(), + unstable_features, + user_agent: crate::version::DENO_VERSION_INFO.user_agent.to_string(), + inspect: shared.options.is_inspecting, + has_node_modules_dir: shared.options.has_node_modules_dir, + argv0: shared.options.argv0.clone(), + node_debug: shared.options.node_debug.clone(), + node_ipc_fd: None, + mode: WorkerExecutionMode::Worker, + serve_port: shared.options.serve_port, + serve_host: shared.options.serve_host.clone(), + otel_config: shared.options.otel_config.clone(), + close_on_idle: args.close_on_idle, + }, + extensions: vec![], + startup_snapshot: shared.options.startup_snapshot, + create_params: create_isolate_create_params(), + unsafely_ignore_certificate_errors: shared + .options + .unsafely_ignore_certificate_errors + .clone(), + seed: shared.options.seed, + create_web_worker_cb, + format_js_error_fn: Some(Arc::new(format_js_error)), + worker_type: args.worker_type, + stdio: stdio.clone(), + cache_storage_dir, + strace_ops: shared.options.strace_ops.clone(), + close_on_idle: args.close_on_idle, + maybe_worker_metadata: args.maybe_worker_metadata, + enable_stack_trace_arg_in_ops: has_trace_permissions_enabled(), + }; + + WebWorker::bootstrap_from_options(services, options) + }) + } +} + +pub struct LibMainWorkerFactory { + shared: Arc>, +} + +impl LibMainWorkerFactory { + #[allow(clippy::too_many_arguments)] + pub fn new( + blob_store: Arc, + code_cache: Option>, + feature_checker: Arc, + fs: Arc, + maybe_inspector_server: Option>, + module_loader_factory: Box, + node_resolver: Arc< + NodeResolver, TSys>, + >, + npm_process_state_provider: NpmProcessStateProviderRc, + pkg_json_resolver: Arc>, + root_cert_store_provider: Arc, + storage_key_resolver: StorageKeyResolver, + sys: TSys, + options: LibMainWorkerOptions, + ) -> Self { + Self { + shared: Arc::new(LibWorkerFactorySharedState { + blob_store, + broadcast_channel: Default::default(), + code_cache, + compiled_wasm_module_store: Default::default(), + feature_checker, + fs, + maybe_inspector_server, + module_loader_factory, + node_resolver, + npm_process_state_provider, + pkg_json_resolver, + root_cert_store_provider, + shared_array_buffer_store: Default::default(), + storage_key_resolver, + sys, + options, + }), + } + } + + pub fn create_main_worker( + &self, + mode: WorkerExecutionMode, + permissions: PermissionsContainer, + main_module: Url, + ) -> Result { + self.create_custom_worker( + mode, + main_module, + permissions, + vec![], + Default::default(), + ) + } + + pub fn create_custom_worker( + &self, + mode: WorkerExecutionMode, + main_module: Url, + permissions: PermissionsContainer, + custom_extensions: Vec, + stdio: deno_runtime::deno_io::Stdio, + ) -> Result { + let shared = &self.shared; + let CreateModuleLoaderResult { + module_loader, + node_require_loader, + } = shared + .module_loader_factory + .create_for_main(permissions.clone()); + + // TODO(bartlomieju): this is cruft, update FeatureChecker to spit out + // list of enabled features. + let feature_checker = shared.feature_checker.clone(); + let unstable_features = + shared.resolve_unstable_features(feature_checker.as_ref()); + let maybe_storage_key = shared + .storage_key_resolver + .resolve_storage_key(&main_module); + let origin_storage_dir = maybe_storage_key.as_ref().map(|key| { + shared + .options + .origin_data_folder_path + .as_ref() + .unwrap() // must be set if storage key resolver returns a value + .join(checksum::gen(&[key.as_bytes()])) + }); + let cache_storage_dir = maybe_storage_key.map(|key| { + // TODO(@satyarohith): storage quota management + get_cache_storage_dir().join(checksum::gen(&[key.as_bytes()])) + }); + + 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.clone(), + ), + blob_store: shared.blob_store.clone(), + broadcast_channel: shared.broadcast_channel.clone(), + fetch_dns_resolver: Default::default(), + 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(), + args: shared.options.argv.clone(), + cpu_count: std::thread::available_parallelism() + .map(|p| p.get()) + .unwrap_or(1), + log_level: shared.options.log_level, + 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: shared.options.location.clone(), + no_color: !colors::use_color(), + is_stdout_tty: deno_terminal::is_stdout_tty(), + is_stderr_tty: deno_terminal::is_stderr_tty(), + color_level: colors::get_color_level(), + unstable_features, + user_agent: crate::version::DENO_VERSION_INFO.user_agent.to_string(), + inspect: shared.options.is_inspecting, + has_node_modules_dir: shared.options.has_node_modules_dir, + argv0: shared.options.argv0.clone(), + node_debug: shared.options.node_debug.clone(), + node_ipc_fd: shared.options.node_ipc, + mode, + serve_port: shared.options.serve_port, + serve_host: shared.options.serve_host.clone(), + otel_config: shared.options.otel_config.clone(), + close_on_idle: true, + }, + extensions: custom_extensions, + startup_snapshot: shared.options.startup_snapshot, + create_params: create_isolate_create_params(), + unsafely_ignore_certificate_errors: shared + .options + .unsafely_ignore_certificate_errors + .clone(), + seed: shared.options.seed, + format_js_error_fn: Some(Arc::new(format_js_error)), + create_web_worker_cb: shared.create_web_worker_callback(stdio.clone()), + maybe_inspector_server: shared.maybe_inspector_server.clone(), + 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(), + cache_storage_dir, + origin_storage_dir, + stdio, + skip_op_registration: shared.options.skip_op_registration, + enable_stack_trace_arg_in_ops: has_trace_permissions_enabled(), + }; + + let worker = + MainWorker::bootstrap_from_options(&main_module, services, options); + + Ok(LibMainWorker { + main_module, + worker, + }) + } + + pub fn resolve_npm_binary_entrypoint( + &self, + package_folder: &Path, + sub_path: Option<&str>, + ) -> Result { + match self + .shared + .node_resolver + .resolve_binary_export(package_folder, sub_path) + { + 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(specifier)) => Ok(specifier), + Ok(None) => { + Err(ResolveNpmBinaryEntrypointError::ResolvePkgJsonBinExport( + original_err, + )) + } + Err(fallback_err) => Err(ResolveNpmBinaryEntrypointError::Fallback { + original: original_err, + fallback: fallback_err, + }), + } + } + } + } + + /// resolve the binary entrypoint using regular node resolution + fn resolve_binary_entrypoint_fallback( + &self, + package_folder: &Path, + sub_path: Option<&str>, + ) -> Result, ResolveNpmBinaryEntrypointFallbackError> { + // 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 + // entrypoint and we just execute the main script which will likely + // have blank output, so do not resolve the entrypoint in this case + return Ok(None); + } + + let specifier = self + .shared + .node_resolver + .resolve_package_subpath_from_deno_module( + package_folder, + sub_path, + /* referrer */ None, + node_resolver::ResolutionMode::Import, + node_resolver::NodeResolutionKind::Execution, + ) + .map_err( + ResolveNpmBinaryEntrypointFallbackError::PackageSubpathResolve, + )?; + if deno_path_util::url_to_file_path(&specifier) + .map(|p| self.shared.sys.fs_exists_no_err(p)) + .unwrap_or(false) + { + Ok(Some(specifier)) + } else { + Err(ResolveNpmBinaryEntrypointFallbackError::ModuleNotFound( + specifier, + )) + } + } +} + +pub struct LibMainWorker { + main_module: Url, + worker: MainWorker, +} + +impl LibMainWorker { + pub fn into_main_worker(self) -> MainWorker { + self.worker + } + + pub fn main_module(&self) -> &Url { + &self.main_module + } + + pub fn js_runtime(&mut self) -> &mut JsRuntime { + &mut self.worker.js_runtime + } + + #[inline] + pub fn create_inspector_session(&mut self) -> LocalInspectorSession { + self.worker.create_inspector_session() + } + + #[inline] + pub fn dispatch_load_event(&mut self) -> Result<(), JsError> { + self.worker.dispatch_load_event() + } + + #[inline] + pub fn dispatch_beforeunload_event(&mut self) -> Result { + self.worker.dispatch_beforeunload_event() + } + + #[inline] + pub fn dispatch_process_beforeexit_event(&mut self) -> Result { + self.worker.dispatch_process_beforeexit_event() + } + + #[inline] + pub fn dispatch_unload_event(&mut self) -> Result<(), JsError> { + self.worker.dispatch_unload_event() + } + + #[inline] + pub fn dispatch_process_exit_event(&mut self) -> Result<(), JsError> { + self.worker.dispatch_process_exit_event() + } + + pub async fn execute_main_module(&mut self) -> Result<(), CoreError> { + let id = self.worker.preload_main_module(&self.main_module).await?; + self.worker.evaluate_module(id).await + } + + pub async fn execute_side_module(&mut self) -> Result<(), CoreError> { + let id = self.worker.preload_side_module(&self.main_module).await?; + self.worker.evaluate_module(id).await + } + + pub async fn run(&mut self) -> Result { + log::debug!("main_module {}", self.main_module); + + self.execute_main_module().await?; + self.worker.dispatch_load_event()?; + + loop { + self + .worker + .run_event_loop(/* wait for inspector */ false) + .await?; + + let web_continue = self.worker.dispatch_beforeunload_event()?; + if !web_continue { + let node_continue = self.worker.dispatch_process_beforeexit_event()?; + if !node_continue { + break; + } + } + } + + self.worker.dispatch_unload_event()?; + self.worker.dispatch_process_exit_event()?; + + Ok(self.worker.exit_code()) + } + + #[inline] + pub async fn run_event_loop( + &mut self, + wait_for_inspector: bool, + ) -> Result<(), CoreError> { + self.worker.run_event_loop(wait_for_inspector).await + } + + #[inline] + pub fn exit_code(&self) -> i32 { + self.worker.exit_code() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn storage_key_resolver_test() { + let resolver = + StorageKeyResolver(StorageKeyResolverStrategy::UseMainModule); + let specifier = Url::parse("file:///a.ts").unwrap(); + assert_eq!( + resolver.resolve_storage_key(&specifier), + Some(specifier.to_string()) + ); + let resolver = + StorageKeyResolver(StorageKeyResolverStrategy::Specified(None)); + assert_eq!(resolver.resolve_storage_key(&specifier), None); + let resolver = StorageKeyResolver(StorageKeyResolverStrategy::Specified( + Some("value".to_string()), + )); + assert_eq!( + resolver.resolve_storage_key(&specifier), + Some("value".to_string()) + ); + + // test empty + let resolver = StorageKeyResolver::empty(); + assert_eq!(resolver.resolve_storage_key(&specifier), None); + } +} diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index f7b487f055..f8f382f594 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -19,6 +19,7 @@ use deno_core::ModuleSpecifier; use deno_error::JsErrorBox; use deno_lint::diagnostic::LintDiagnosticRange; use deno_path_util::url_to_file_path; +use deno_resolver::npm::managed::NpmResolutionCell; use deno_runtime::deno_node::PathClean; use deno_semver::jsr::JsrPackageNvReference; use deno_semver::jsr::JsrPackageReqReference; @@ -31,6 +32,7 @@ use deno_semver::SmallStackString; use deno_semver::StackString; use deno_semver::Version; use import_map::ImportMap; +use node_resolver::InNpmPackageChecker; use node_resolver::NodeResolutionKind; use node_resolver::ResolutionMode; use once_cell::sync::Lazy; @@ -365,7 +367,9 @@ impl<'a> TsResponseImportMapper<'a> { if let Ok(Some(pkg_id)) = npm_resolver.resolve_pkg_id_from_specifier(specifier) { - let pkg_reqs = npm_resolver.resolve_pkg_reqs_from_pkg_id(&pkg_id); + let pkg_reqs = npm_resolver + .resolution() + .resolve_pkg_reqs_from_pkg_id(&pkg_id); // check if any pkg reqs match what is found in an import map if !pkg_reqs.is_empty() { let sub_path = npm_resolver @@ -1295,6 +1299,19 @@ impl CodeActionCollection { range: &lsp::Range, language_server: &language_server::Inner, ) -> Option { + fn top_package_req_for_name( + resolution: &NpmResolutionCell, + name: &str, + ) -> Option { + let package_reqs = resolution.package_reqs(); + let mut entries = package_reqs + .into_iter() + .filter(|(_, nv)| nv.name == name) + .collect::>(); + entries.sort_by(|a, b| a.1.version.cmp(&b.1.version)); + Some(entries.pop()?.0) + } + let (dep_key, dependency, _) = document.get_maybe_dependency(&range.end)?; if dependency.maybe_deno_types_specifier.is_some() { @@ -1382,9 +1399,10 @@ impl CodeActionCollection { .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) - { + let mut specifier_text = if let Some(req) = top_package_req_for_name( + npm_resolver.resolution(), + &types_package_name, + ) { format!("npm:{req}") } else { format!("npm:{}@^{}", &types_package_name, types_package_version) diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 7841ee0783..a0456c14ed 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -41,7 +41,8 @@ use deno_core::serde_json::json; use deno_core::serde_json::Value; use deno_core::url::Url; use deno_core::ModuleSpecifier; -use deno_error::JsErrorBox; +use deno_lib::args::has_flag_env_var; +use deno_lib::util::hash::FastInsecureHasher; use deno_lint::linter::LintConfig as DenoLintConfig; use deno_npm::npm_rc::ResolvedNpmRc; use deno_package_json::PackageJsonCache; @@ -56,13 +57,11 @@ 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::CliFileFetcher; use crate::lsp::logging::lsp_warn; use crate::resolver::CliSloppyImportsResolver; @@ -1247,7 +1246,6 @@ impl ConfigData { pkg_json_cache: Some(pkg_json_cache), workspace_cache: Some(workspace_cache), discover_pkg_json: !has_flag_env_var("DENO_NO_PACKAGE_JSON"), - config_parse_options: Default::default(), maybe_vendor_override: None, }, ) @@ -1572,11 +1570,11 @@ impl ConfigData { let resolver = member_dir .workspace .create_resolver( + &CliSys::default(), CreateResolverOptions { pkg_json_dep_resolution, specified_import_map, }, - |path| std::fs::read_to_string(path).map_err(JsErrorBox::from_err), ) .inspect_err(|err| { lsp_warn!( @@ -2078,7 +2076,6 @@ impl deno_config::workspace::WorkspaceCache for WorkspaceMemCache { #[cfg(test)] mod tests { - use deno_config::deno_json::ConfigParseOptions; use deno_core::resolve_url; use deno_core::serde_json; use deno_core::serde_json::json; @@ -2352,12 +2349,7 @@ mod tests { config .tree .inject_config_file( - ConfigFile::new( - "{}", - root_uri.join("deno.json").unwrap(), - &ConfigParseOptions::default(), - ) - .unwrap(), + ConfigFile::new("{}", root_uri.join("deno.json").unwrap()).unwrap(), ) .await; assert!(config.specifier_enabled(&root_uri)); @@ -2413,7 +2405,6 @@ mod tests { }) .to_string(), root_uri.join("deno.json").unwrap(), - &ConfigParseOptions::default(), ) .unwrap(), ) @@ -2439,7 +2430,6 @@ mod tests { }) .to_string(), root_uri.join("deno.json").unwrap(), - &ConfigParseOptions::default(), ) .unwrap(), ) @@ -2457,7 +2447,6 @@ mod tests { }) .to_string(), root_uri.join("deno.json").unwrap(), - &ConfigParseOptions::default(), ) .unwrap(), ) diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 42a1a0c52a..126e8ef01d 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -265,7 +265,7 @@ impl TsDiagnosticsStore { } pub fn should_send_diagnostic_batch_index_notifications() -> bool { - crate::args::has_flag_env_var( + deno_lib::args::has_flag_env_var( "DENO_DONT_USE_INTERNAL_LSP_DIAGNOSTIC_SYNC_FLAG", ) } @@ -1695,12 +1695,7 @@ mod tests { let mut config = Config::new_with_roots([root_uri.clone()]); if let Some((relative_path, json_string)) = maybe_import_map { let base_url = root_uri.join(relative_path).unwrap(); - let config_file = ConfigFile::new( - json_string, - base_url, - &deno_config::deno_json::ConfigParseOptions::default(), - ) - .unwrap(); + let config_file = ConfigFile::new(json_string, base_url).unwrap(); config.tree.inject_config_file(config_file).await; } let resolver = diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index f31353d436..b2bf37edf2 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -480,7 +480,7 @@ impl Document { let is_cjs_resolver = resolver.as_is_cjs_resolver(self.file_referrer.as_ref()); let npm_resolver = - resolver.create_graph_npm_resolver(self.file_referrer.as_ref()); + resolver.as_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()); @@ -503,7 +503,7 @@ impl Document { s, &CliJsrUrlProvider, Some(&resolver), - Some(&npm_resolver), + Some(npm_resolver.as_ref()), ), ) }) @@ -513,7 +513,7 @@ impl Document { Arc::new(d.with_new_resolver( &CliJsrUrlProvider, Some(&resolver), - Some(&npm_resolver), + Some(npm_resolver.as_ref()), )) }); is_script = self.is_script; @@ -936,7 +936,7 @@ impl FileSystemDocuments { file_referrer.cloned(), ) } else if specifier.scheme() == "data" { - let source = deno_graph::source::RawDataUrl::parse(specifier) + let source = deno_media_type::data_url::RawDataUrl::parse(specifier) .ok()? .decode() .ok()?; @@ -1702,7 +1702,7 @@ fn analyze_module( ) -> (ModuleResult, ResolutionMode) { match parsed_source_result { Ok(parsed_source) => { - let npm_resolver = resolver.create_graph_npm_resolver(file_referrer); + let npm_resolver = resolver.as_graph_npm_resolver(file_referrer); let cli_resolver = resolver.as_cli_resolver(file_referrer); let is_cjs_resolver = resolver.as_is_cjs_resolver(file_referrer); let config_data = resolver.as_config_data(file_referrer); @@ -1731,7 +1731,7 @@ fn analyze_module( file_system: &deno_graph::source::NullFileSystem, jsr_url_provider: &CliJsrUrlProvider, maybe_resolver: Some(&resolver), - maybe_npm_resolver: Some(&npm_resolver), + maybe_npm_resolver: Some(npm_resolver.as_ref()), }, )), module_resolution_mode, @@ -1756,10 +1756,11 @@ fn bytes_to_content( // we use the dts representation for Wasm modules Ok(deno_graph::source::wasm::wasm_module_to_dts(&bytes)?) } else { - Ok(deno_graph::source::decode_owned_source( - specifier, - bytes, - maybe_charset, + let charset = maybe_charset.unwrap_or_else(|| { + deno_media_type::encoding::detect_charset(specifier, &bytes) + }); + Ok(deno_media_type::encoding::decode_owned_source( + charset, bytes, )?) } } @@ -1767,7 +1768,6 @@ fn bytes_to_content( #[cfg(test)] mod tests { use deno_config::deno_json::ConfigFile; - use deno_config::deno_json::ConfigParseOptions; use deno_core::serde_json; use deno_core::serde_json::json; use pretty_assertions::assert_eq; @@ -1924,7 +1924,6 @@ console.log(b, "hello deno"); }) .to_string(), config.root_uri().unwrap().join("deno.json").unwrap(), - &ConfigParseOptions::default(), ) .unwrap(), ) @@ -1968,7 +1967,6 @@ console.log(b, "hello deno"); }) .to_string(), config.root_uri().unwrap().join("deno.json").unwrap(), - &ConfigParseOptions::default(), ) .unwrap(), ) diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 35f5374efe..8bb77681a2 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -27,6 +27,10 @@ use deno_core::url::Url; use deno_core::ModuleSpecifier; use deno_graph::GraphKind; use deno_graph::Resolution; +use deno_lib::args::get_root_cert_store; +use deno_lib::args::has_flag_env_var; +use deno_lib::args::CaData; +use deno_lib::version::DENO_VERSION_INFO; use deno_path_util::url_to_file_path; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::RootCertStoreProvider; @@ -94,9 +98,6 @@ use super::urls; use super::urls::uri_to_url; use super::urls::url_to_uri; use crate::args::create_default_npmrc; -use crate::args::get_root_cert_store; -use crate::args::has_flag_env_var; -use crate::args::CaData; use crate::args::CliOptions; use crate::args::Flags; use crate::args::InternalFlags; @@ -703,7 +704,7 @@ impl Inner { let version = format!( "{} ({}, {})", - crate::version::DENO_VERSION_INFO.deno, + DENO_VERSION_INFO.deno, env!("PROFILE"), env!("TARGET") ); @@ -1419,18 +1420,16 @@ impl Inner { // the file path is only used to determine what formatter should // be used to format the file, so give the filepath an extension // that matches what the user selected as the language - let file_path = document + let ext = document .maybe_language_id() - .and_then(|id| id.as_extension()) - .map(|ext| file_path.with_extension(ext)) - .unwrap_or(file_path); + .and_then(|id| id.as_extension().map(|s| s.to_string())); // it's not a js/ts file, so attempt to format its contents format_file( &file_path, document.content(), &fmt_options, &unstable_options, - None, + ext, ) } }; @@ -1885,7 +1884,7 @@ impl Inner { })?; let asset_or_doc = self.get_asset_or_document(&action_data.specifier)?; let line_index = asset_or_doc.line_index(); - let mut refactor_edit_info = self + let refactor_edit_info = self .ts_server .get_edits_for_refactor( self.snapshot(), @@ -1906,19 +1905,34 @@ impl Inner { )), asset_or_doc.scope().cloned(), ) - .await?; - if kind_suffix == ".rewrite.function.returnType" - || kind_suffix == ".move.newFile" - { - refactor_edit_info.edits = - fix_ts_import_changes(&refactor_edit_info.edits, self).map_err( - |err| { - error!("Unable to remap changes: {:#}", err); - LspError::internal_error() - }, - )? + .await; + + match refactor_edit_info { + Ok(mut refactor_edit_info) => { + if kind_suffix == ".rewrite.function.returnType" + || kind_suffix == ".move.newFile" + { + refactor_edit_info.edits = + fix_ts_import_changes(&refactor_edit_info.edits, self).map_err( + |err| { + error!("Unable to remap changes: {:#}", err); + LspError::internal_error() + }, + )? + } + code_action.edit = refactor_edit_info.to_workspace_edit(self)?; + } + Err(err) => { + // TODO(nayeemrmn): Investigate cause for + // https://github.com/denoland/deno/issues/27778. Prevent popups for + // this error for now. + if kind_suffix == ".move.newFile" { + lsp_warn!("{:#}", err); + } else { + return Err(err); + } + } } - code_action.edit = refactor_edit_info.to_workspace_edit(self)?; code_action } else { // The code action doesn't need to be resolved @@ -3623,8 +3637,6 @@ impl Inner { deno_json_cache: None, pkg_json_cache: None, workspace_cache: None, - config_parse_options: - deno_config::deno_json::ConfigParseOptions::default(), additional_config_file_names: &[], discover_pkg_json: !has_flag_env_var("DENO_NO_PACKAGE_JSON"), maybe_vendor_override: if force_global_cache { diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs index 57ef2e6a3c..1b393ad22b 100644 --- a/cli/lsp/resolver.rs +++ b/cli/lsp/resolver.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use dashmap::DashMap; use deno_ast::MediaType; -use deno_cache_dir::file_fetcher::CacheSetting; use deno_cache_dir::npm::NpmCacheDir; use deno_cache_dir::HttpCache; use deno_config::deno_json::JsxImportSourceConfig; @@ -21,10 +20,13 @@ use deno_graph::GraphImport; use deno_graph::ModuleSpecifier; use deno_graph::Range; use deno_npm::NpmSystemInfo; +use deno_npm_cache::TarballCache; use deno_path_util::url_to_file_path; use deno_resolver::cjs::IsCjsResolutionMode; use deno_resolver::npm::managed::ManagedInNpmPkgCheckerCreateOptions; +use deno_resolver::npm::managed::NpmResolutionCell; use deno_resolver::npm::CreateInNpmPkgCheckerOptions; +use deno_resolver::npm::DenoInNpmPackageChecker; use deno_resolver::npm::NpmReqResolverOptions; use deno_resolver::DenoResolverOptions; use deno_resolver::NodeAndNpmReqResolver; @@ -34,7 +36,6 @@ use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use indexmap::IndexMap; -use node_resolver::InNpmPackageChecker; use node_resolver::NodeResolutionKind; use node_resolver::ResolutionMode; @@ -42,6 +43,8 @@ use super::cache::LspCache; use super::jsr::JsrCacheResolver; use crate::args::create_default_npmrc; use crate::args::CliLockfile; +use crate::args::LifecycleScriptsConfig; +use crate::args::NpmCachingStrategy; use crate::args::NpmInstallDepsProvider; use crate::factory::Deferred; use crate::graph_util::to_node_resolution_kind; @@ -53,19 +56,24 @@ use crate::lsp::config::ConfigData; use crate::lsp::logging::lsp_warn; use crate::node::CliNodeResolver; use crate::node::CliPackageJsonResolver; -use crate::npm::create_cli_npm_resolver_for_lsp; +use crate::npm::installer::NpmInstaller; +use crate::npm::installer::NpmResolutionInstaller; use crate::npm::CliByonmNpmResolverCreateOptions; +use crate::npm::CliManagedNpmResolver; use crate::npm::CliManagedNpmResolverCreateOptions; +use crate::npm::CliNpmCache; +use crate::npm::CliNpmCacheHttpClient; +use crate::npm::CliNpmRegistryInfoProvider; use crate::npm::CliNpmResolver; use crate::npm::CliNpmResolverCreateOptions; use crate::npm::CliNpmResolverManagedSnapshotOption; -use crate::npm::ManagedCliNpmResolver; +use crate::npm::NpmResolutionInitializer; use crate::resolver::CliDenoResolver; +use crate::resolver::CliIsCjsResolver; +use crate::resolver::CliNpmGraphResolver; use crate::resolver::CliNpmReqResolver; use crate::resolver::CliResolver; -use crate::resolver::CliResolverOptions; -use crate::resolver::IsCjsResolver; -use crate::resolver::WorkerCliNpmGraphResolver; +use crate::resolver::FoundPackageJsonDepFlag; use crate::sys::CliSys; use crate::tsc::into_specifier_and_media_type; use crate::util::progress_bar::ProgressBar; @@ -74,10 +82,13 @@ use crate::util::progress_bar::ProgressBarStyle; #[derive(Debug, Clone)] struct LspScopeResolver { resolver: Arc, - in_npm_pkg_checker: Arc, - is_cjs_resolver: Arc, + in_npm_pkg_checker: DenoInNpmPackageChecker, + is_cjs_resolver: Arc, jsr_resolver: Option>, - npm_resolver: Option>, + npm_graph_resolver: Arc, + npm_installer: Option>, + npm_resolution: Arc, + npm_resolver: Option, node_resolver: Option>, npm_pkg_req_resolver: Option>, pkg_json_resolver: Arc, @@ -96,8 +107,11 @@ impl Default for LspScopeResolver { in_npm_pkg_checker: factory.in_npm_pkg_checker().clone(), is_cjs_resolver: factory.is_cjs_resolver().clone(), jsr_resolver: None, + npm_graph_resolver: factory.npm_graph_resolver().clone(), + npm_installer: None, npm_resolver: None, node_resolver: None, + npm_resolution: factory.services.npm_resolution.clone(), npm_pkg_req_resolver: None, pkg_json_resolver: factory.pkg_json_resolver().clone(), redirect_resolver: None, @@ -121,6 +135,7 @@ impl LspScopeResolver { } let in_npm_pkg_checker = factory.in_npm_pkg_checker().clone(); let npm_resolver = factory.npm_resolver().cloned(); + let npm_installer = factory.npm_installer().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(); @@ -133,8 +148,7 @@ 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 = cli_resolver - .create_graph_npm_resolver(crate::graph_util::NpmCachingStrategy::Eager); + let npm_graph_resolver = factory.npm_graph_resolver(); let maybe_jsx_import_source_config = config_data.and_then(|d| d.maybe_jsx_import_source_config()); let graph_imports = config_data @@ -156,7 +170,7 @@ impl LspScopeResolver { imports, &CliJsrUrlProvider, Some(&resolver), - Some(&npm_graph_resolver), + Some(npm_graph_resolver.as_ref()), ); (referrer, graph_import) }) @@ -207,8 +221,11 @@ impl LspScopeResolver { in_npm_pkg_checker, is_cjs_resolver: factory.is_cjs_resolver().clone(), jsr_resolver, + npm_graph_resolver: factory.npm_graph_resolver().clone(), npm_pkg_req_resolver, npm_resolver, + npm_installer, + npm_resolution: factory.services.npm_resolution.clone(), node_resolver, pkg_json_resolver, redirect_resolver, @@ -220,18 +237,68 @@ impl LspScopeResolver { } fn snapshot(&self) -> Arc { + // create a copy of the resolution and then re-initialize the npm resolver from that + // todo(dsherret): this is pretty terrible... we should improve this. It should + // be possible to just change the npm_resolution on the new factory then access + // another method to create a new npm resolver let mut factory = ResolverFactory::new(self.config_data.as_ref()); - let npm_resolver = - self.npm_resolver.as_ref().map(|r| r.clone_snapshotted()); + factory + .services + .npm_resolution + .set_snapshot(self.npm_resolution.snapshot()); + let npm_resolver = self.npm_resolver.as_ref(); if let Some(npm_resolver) = &npm_resolver { - factory.set_npm_resolver(npm_resolver.clone()); + factory.set_npm_resolver(CliNpmResolver::new::( + match npm_resolver { + CliNpmResolver::Byonm(byonm_npm_resolver) => { + CliNpmResolverCreateOptions::Byonm( + CliByonmNpmResolverCreateOptions { + root_node_modules_dir: byonm_npm_resolver + .root_node_modules_path() + .map(|p| p.to_path_buf()), + sys: CliSys::default(), + pkg_json_resolver: self.pkg_json_resolver.clone(), + }, + ) + } + CliNpmResolver::Managed(managed_npm_resolver) => { + CliNpmResolverCreateOptions::Managed({ + let npmrc = self + .config_data + .as_ref() + .and_then(|d| d.npmrc.clone()) + .unwrap_or_else(create_default_npmrc); + let npm_cache_dir = Arc::new(NpmCacheDir::new( + &CliSys::default(), + managed_npm_resolver.global_cache_root_path().to_path_buf(), + npmrc.get_all_known_registries_urls(), + )); + CliManagedNpmResolverCreateOptions { + sys: CliSys::default(), + npm_cache_dir, + maybe_node_modules_path: managed_npm_resolver + .root_node_modules_path() + .map(|p| p.to_path_buf()), + npmrc, + npm_resolution: factory.services.npm_resolution.clone(), + npm_system_info: NpmSystemInfo::default(), + } + }) + } + }, + )); } + Arc::new(Self { resolver: factory.cli_resolver().clone(), in_npm_pkg_checker: factory.in_npm_pkg_checker().clone(), is_cjs_resolver: factory.is_cjs_resolver().clone(), jsr_resolver: self.jsr_resolver.clone(), + npm_graph_resolver: factory.npm_graph_resolver().clone(), + // npm installer isn't necessary for a snapshot + npm_installer: None, npm_pkg_req_resolver: factory.npm_pkg_req_resolver().cloned(), + npm_resolution: factory.services.npm_resolution.clone(), npm_resolver: factory.npm_resolver().cloned(), node_resolver: factory.node_resolver().cloned(), redirect_resolver: self.redirect_resolver.clone(), @@ -318,14 +385,12 @@ impl LspResolver { if let Some(dep_info) = dep_info { *resolver.dep_info.lock() = dep_info.clone(); } - if let Some(npm_resolver) = resolver.npm_resolver.as_ref() { - if let Some(npm_resolver) = npm_resolver.as_managed() { - let reqs = dep_info - .map(|i| i.npm_reqs.iter().cloned().collect::>()) - .unwrap_or_default(); - if let Err(err) = npm_resolver.set_package_reqs(&reqs).await { - lsp_warn!("Could not set npm package requirements: {:#}", err); - } + if let Some(npm_installer) = resolver.npm_installer.as_ref() { + let reqs = dep_info + .map(|i| i.npm_reqs.iter().cloned().collect::>()) + .unwrap_or_default(); + if let Err(err) = npm_installer.set_package_reqs(&reqs).await { + lsp_warn!("Could not set npm package requirements: {:#}", err); } } } @@ -339,20 +404,18 @@ impl LspResolver { resolver.resolver.as_ref() } - pub fn create_graph_npm_resolver( + pub fn as_graph_npm_resolver( &self, file_referrer: Option<&ModuleSpecifier>, - ) -> WorkerCliNpmGraphResolver { + ) -> &Arc { let resolver = self.get_scope_resolver(file_referrer); - resolver - .resolver - .create_graph_npm_resolver(crate::graph_util::NpmCachingStrategy::Eager) + &resolver.npm_graph_resolver } pub fn as_is_cjs_resolver( &self, file_referrer: Option<&ModuleSpecifier>, - ) -> &IsCjsResolver { + ) -> &CliIsCjsResolver { let resolver = self.get_scope_resolver(file_referrer); resolver.is_cjs_resolver.as_ref() } @@ -368,7 +431,7 @@ impl LspResolver { pub fn in_npm_pkg_checker( &self, file_referrer: Option<&ModuleSpecifier>, - ) -> &Arc { + ) -> &DenoInNpmPackageChecker { let resolver = self.get_scope_resolver(file_referrer); &resolver.in_npm_pkg_checker } @@ -376,7 +439,7 @@ impl LspResolver { pub fn maybe_managed_npm_resolver( &self, file_referrer: Option<&ModuleSpecifier>, - ) -> Option<&ManagedCliNpmResolver> { + ) -> Option<&CliManagedNpmResolver> { let resolver = self.get_scope_resolver(file_referrer); resolver.npm_resolver.as_ref().and_then(|r| r.as_managed()) } @@ -590,11 +653,15 @@ pub struct ScopeDepInfo { #[derive(Default)] struct ResolverFactoryServices { cli_resolver: Deferred>, - in_npm_pkg_checker: Deferred>, - is_cjs_resolver: Deferred>, + found_pkg_json_dep_flag: Arc, + in_npm_pkg_checker: Deferred, + is_cjs_resolver: Deferred>, node_resolver: Deferred>>, + npm_graph_resolver: Deferred>, + npm_installer: Option>, npm_pkg_req_resolver: Deferred>>, - npm_resolver: Option>, + npm_resolver: Option, + npm_resolution: Arc, } struct ResolverFactory<'a> { @@ -616,6 +683,10 @@ impl<'a> ResolverFactory<'a> { } } + // todo(dsherret): probably this method could be removed in the future + // and instead just `npm_resolution_initializer.ensure_initialized()` could + // be called. The reason this exists is because creating the npm resolvers + // used to be async. async fn init_npm_resolver( &mut self, http_client_provider: &Arc, @@ -645,11 +716,30 @@ impl<'a> ResolverFactory<'a> { cache.deno_dir().npm_folder_path(), npmrc.get_all_known_registries_urls(), )); - CliNpmResolverCreateOptions::Managed(CliManagedNpmResolverCreateOptions { - http_client_provider: http_client_provider.clone(), - // only used for top level install, so we can ignore this - npm_install_deps_provider: Arc::new(NpmInstallDepsProvider::empty()), - snapshot: match self.config_data.and_then(|d| d.lockfile.as_ref()) { + let npm_cache = Arc::new(CliNpmCache::new( + npm_cache_dir.clone(), + sys.clone(), + // 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. + deno_npm_cache::NpmCacheSetting::Only, + npmrc.clone(), + )); + let pb = ProgressBar::new(ProgressBarStyle::TextOnly); + let npm_client = Arc::new(CliNpmCacheHttpClient::new( + http_client_provider.clone(), + pb.clone(), + )); + let registry_info_provider = Arc::new(CliNpmRegistryInfoProvider::new( + npm_cache.clone(), + npm_client.clone(), + npmrc.clone(), + )); + let npm_resolution_initializer = Arc::new(NpmResolutionInitializer::new( + registry_info_provider.clone(), + self.services.npm_resolution.clone(), + match self.config_data.and_then(|d| d.lockfile.as_ref()) { Some(lockfile) => { CliNpmResolverManagedSnapshotOption::ResolveFromLockfile( lockfile.clone(), @@ -657,33 +747,69 @@ impl<'a> ResolverFactory<'a> { } 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. + let maybe_lockfile: Option> = None; + let maybe_node_modules_path = + self.config_data.and_then(|d| d.node_modules_dir.clone()); + let tarball_cache = Arc::new(TarballCache::new( + npm_cache.clone(), + npm_client.clone(), + sys.clone(), + npmrc.clone(), + )); + let npm_resolution_installer = Arc::new(NpmResolutionInstaller::new( + registry_info_provider, + self.services.npm_resolution.clone(), + maybe_lockfile.clone(), + )); + let npm_installer = Arc::new(NpmInstaller::new( + npm_cache.clone(), + Arc::new(NpmInstallDepsProvider::empty()), + self.services.npm_resolution.clone(), + npm_resolution_initializer.clone(), + npm_resolution_installer, + &pb, + sys.clone(), + tarball_cache.clone(), + maybe_lockfile, + maybe_node_modules_path.clone(), + LifecycleScriptsConfig::default(), + NpmSystemInfo::default(), + )); + self.set_npm_installer(npm_installer); + // spawn due to the lsp's `Send` requirement + deno_core::unsync::spawn(async move { + if let Err(err) = npm_resolution_initializer.ensure_initialized().await + { + log::warn!("failed to initialize npm resolution: {}", err); + } + }) + .await + .unwrap(); + + CliNpmResolverCreateOptions::Managed(CliManagedNpmResolverCreateOptions { sys: CliSys::default(), 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), - // Don't provide the lockfile. We don't want these resolvers - // updating it. Only the cache request should update the lockfile. - maybe_lockfile: None, - maybe_node_modules_path: self - .config_data - .and_then(|d| d.node_modules_dir.clone()), + maybe_node_modules_path, npmrc, + npm_resolution: self.services.npm_resolution.clone(), npm_system_info: NpmSystemInfo::default(), - lifecycle_scripts: Default::default(), }) }; - self.set_npm_resolver(create_cli_npm_resolver_for_lsp(options).await); + self.set_npm_resolver(CliNpmResolver::new(options)); } - pub fn set_npm_resolver(&mut self, npm_resolver: Arc) { + pub fn set_npm_installer(&mut self, npm_installer: Arc) { + self.services.npm_installer = Some(npm_installer); + } + + pub fn set_npm_resolver(&mut self, npm_resolver: CliNpmResolver) { self.services.npm_resolver = Some(npm_resolver); } - pub fn npm_resolver(&self) -> Option<&Arc> { + pub fn npm_resolver(&self) -> Option<&CliNpmResolver> { self.services.npm_resolver.as_ref() } @@ -720,13 +846,27 @@ impl<'a> ResolverFactory<'a> { 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 { + Arc::new(CliResolver::new( deno_resolver, - npm_resolver: self.npm_resolver().cloned(), - bare_node_builtins_enabled: self + self.services.found_pkg_json_dep_flag.clone(), + )) + }) + } + + pub fn npm_installer(&self) -> Option<&Arc> { + self.services.npm_installer.as_ref() + } + + pub fn npm_graph_resolver(&self) -> &Arc { + self.services.npm_graph_resolver.get_or_init(|| { + Arc::new(CliNpmGraphResolver::new( + None, + self.services.found_pkg_json_dep_flag.clone(), + self .config_data .is_some_and(|d| d.unstable.contains("bare-node-builtins")), - })) + NpmCachingStrategy::Eager, + )) }) } @@ -734,29 +874,27 @@ impl<'a> ResolverFactory<'a> { &self.pkg_json_resolver } - pub fn in_npm_pkg_checker(&self) -> &Arc { + pub fn in_npm_pkg_checker(&self) -> &DenoInNpmPackageChecker { self.services.in_npm_pkg_checker.get_or_init(|| { - deno_resolver::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( - ManagedInNpmPkgCheckerCreateOptions { - root_cache_dir_url: m.global_cache_root_url(), - maybe_node_modules_path: m.maybe_node_modules_path(), - }, - ) - } - }, - ) + DenoInNpmPackageChecker::new(match &self.services.npm_resolver { + Some(CliNpmResolver::Byonm(_)) | None => { + CreateInNpmPkgCheckerOptions::Byonm + } + Some(CliNpmResolver::Managed(m)) => { + CreateInNpmPkgCheckerOptions::Managed( + ManagedInNpmPkgCheckerCreateOptions { + root_cache_dir_url: m.global_cache_root_url(), + maybe_node_modules_path: m.root_node_modules_path(), + }, + ) + } + }) }) } - pub fn is_cjs_resolver(&self) -> &Arc { + pub fn is_cjs_resolver(&self) -> &Arc { self.services.is_cjs_resolver.get_or_init(|| { - Arc::new(IsCjsResolver::new( + Arc::new(CliIsCjsResolver::new( self.in_npm_pkg_checker().clone(), self.pkg_json_resolver().clone(), if self @@ -780,9 +918,10 @@ impl<'a> ResolverFactory<'a> { Some(Arc::new(CliNodeResolver::new( self.in_npm_pkg_checker().clone(), RealIsBuiltInNodeModuleChecker, - npm_resolver.clone().into_npm_pkg_folder_resolver(), + npm_resolver.clone(), self.pkg_json_resolver.clone(), self.sys.clone(), + node_resolver::ConditionsFromResolutionMode::default(), ))) }) .as_ref() @@ -798,7 +937,7 @@ impl<'a> ResolverFactory<'a> { Some(Arc::new(CliNpmReqResolver::new(NpmReqResolverOptions { in_npm_pkg_checker: self.in_npm_pkg_checker().clone(), node_resolver: node_resolver.clone(), - npm_resolver: npm_resolver.clone().into_byonm_or_managed(), + npm_resolver: npm_resolver.clone(), sys: self.sys.clone(), }))) }) diff --git a/cli/lsp/testing/definitions.rs b/cli/lsp/testing/definitions.rs index 8277dcbf00..d6630c1844 100644 --- a/cli/lsp/testing/definitions.rs +++ b/cli/lsp/testing/definitions.rs @@ -5,6 +5,7 @@ use std::collections::HashSet; use deno_core::error::AnyError; use deno_core::ModuleSpecifier; +use deno_lib::util::checksum; use lsp::Range; use tower_lsp::lsp_types as lsp; @@ -15,7 +16,6 @@ use crate::lsp::logging::lsp_warn; use crate::lsp::urls::url_to_uri; use crate::tools::test::TestDescription; use crate::tools::test::TestStepDescription; -use crate::util::checksum; #[derive(Debug, Clone, PartialEq)] pub struct TestDefinition { diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 8d9a5a46a1..e1b3691c0a 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -39,6 +39,8 @@ use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_core::PollEventLoopOptions; use deno_core::RuntimeOptions; +use deno_lib::util::result::InfallibleResultExt; +use deno_lib::worker::create_isolate_create_params; use deno_path_util::url_to_file_path; use deno_runtime::deno_node::SUPPORTED_BUILTIN_NODE_MODULES; use deno_runtime::inspector_server::InspectorServer; @@ -72,6 +74,7 @@ use super::documents::Document; use super::documents::DocumentsFilter; use super::language_server; use super::language_server::StateSnapshot; +use super::logging::lsp_log; use super::performance::Performance; use super::performance::PerformanceMark; use super::refactor::RefactorCodeActionData; @@ -94,9 +97,7 @@ use crate::tsc::ResolveArgs; use crate::tsc::MISSING_DEPENDENCY_SPECIFIER; 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; static BRACKET_ACCESSOR_RE: Lazy = lazy_regex!(r#"^\[['"](.+)[\['"]\]$"#); @@ -4340,7 +4341,9 @@ impl TscSpecifierMap { if let Some(specifier) = self.normalized_specifiers.get(original) { return Ok(specifier.clone()); } - let specifier_str = original.replace(".d.ts.d.ts", ".d.ts"); + let specifier_str = original + .replace(".d.ts.d.ts", ".d.ts") + .replace("$node_modules", "node_modules"); let specifier = match ModuleSpecifier::parse(&specifier_str) { Ok(s) => s, Err(err) => return Err(err), @@ -4695,7 +4698,24 @@ fn op_script_names(state: &mut OpState) -> ScriptNames { .graph_imports_by_referrer(scope) { for specifier in specifiers { - script_names.insert(specifier.to_string()); + if let Ok(req_ref) = + deno_semver::npm::NpmPackageReqReference::from_specifier(specifier) + { + let Some((resolved, _)) = + state.state_snapshot.resolver.npm_to_file_url( + &req_ref, + scope, + ResolutionMode::Import, + Some(scope), + ) + else { + lsp_log!("failed to resolve {req_ref} to file URL"); + continue; + }; + script_names.insert(resolved.to_string()); + } else { + script_names.insert(specifier.to_string()); + } } } } @@ -5589,7 +5609,6 @@ mod tests { }) .to_string(), temp_dir.url().join("deno.json").unwrap(), - &Default::default(), ) .unwrap(), ) @@ -6246,7 +6265,40 @@ mod tests { "kind": "keyword" } ], - "documentation": [] + "documentation": [ + { + "text": "Outputs a message to the console", + "kind": "text", + }, + ], + "tags": [ + { + "name": "param", + "text": [ + { + "text": "data", + "kind": "parameterName", + }, + { + "text": " ", + "kind": "space", + }, + { + "text": "Values to be printed to the console", + "kind": "text", + }, + ], + }, + { + "name": "example", + "text": [ + { + "text": "```ts\nconsole.log('Hello', 'World', 123);\n```", + "kind": "text", + }, + ], + }, + ] }) ); } diff --git a/cli/lsp/urls.rs b/cli/lsp/urls.rs index 91c04f11c0..9c9f50dc34 100644 --- a/cli/lsp/urls.rs +++ b/cli/lsp/urls.rs @@ -81,7 +81,7 @@ fn hash_data_specifier(specifier: &ModuleSpecifier) -> String { file_name_str.push('?'); file_name_str.push_str(query); } - crate::util::checksum::gen(&[file_name_str.as_bytes()]) + deno_lib::util::checksum::gen(&[file_name_str.as_bytes()]) } fn to_deno_uri(specifier: &Url) -> String { @@ -219,7 +219,8 @@ impl LspUrlMap { let uri_str = if specifier.scheme() == "asset" { format!("deno:/asset{}", specifier.path()) } else if specifier.scheme() == "data" { - let data_url = deno_graph::source::RawDataUrl::parse(specifier)?; + let data_url = + deno_media_type::data_url::RawDataUrl::parse(specifier)?; let media_type = data_url.media_type(); let extension = if media_type == MediaType::Unknown { "" @@ -282,24 +283,26 @@ impl LspUrlMap { } } -/// Convert a e.g. `deno-notebook-cell:` specifier to a `file:` specifier. +/// Convert a e.g. `vscode-notebook-cell:` specifier to a `file:` specifier. /// ```rust /// assert_eq!( /// file_like_to_file_specifier( -/// &Url::parse("deno-notebook-cell:/path/to/file.ipynb#abc").unwrap(), +/// &Url::parse("vscode-notebook-cell:/path/to/file.ipynb#abc").unwrap(), /// ), -/// Some(Url::parse("file:///path/to/file.ipynb.ts?scheme=deno-notebook-cell#abc").unwrap()), +/// Some(Url::parse("file:///path/to/file.ipynb?scheme=untitled#abc").unwrap()), /// ); fn file_like_to_file_specifier(specifier: &Url) -> Option { - if matches!(specifier.scheme(), "untitled" | "deno-notebook-cell") { + if matches!( + specifier.scheme(), + "untitled" | "vscode-notebook-cell" | "deno-notebook-cell" + ) { if let Ok(mut s) = ModuleSpecifier::parse(&format!( - "file://{}", + "file:///{}", &specifier.as_str()[deno_core::url::quirks::internal_components(specifier) - .host_end as usize..], + .host_end as usize..].trim_start_matches('/'), )) { s.query_pairs_mut() .append_pair("scheme", specifier.scheme()); - s.set_path(&format!("{}.ts", s.path())); return Some(s); } } @@ -432,11 +435,11 @@ mod tests { fn test_file_like_to_file_specifier() { assert_eq!( file_like_to_file_specifier( - &Url::parse("deno-notebook-cell:/path/to/file.ipynb#abc").unwrap(), + &Url::parse("vscode-notebook-cell:/path/to/file.ipynb#abc").unwrap(), ), Some( Url::parse( - "file:///path/to/file.ipynb.ts?scheme=deno-notebook-cell#abc" + "file:///path/to/file.ipynb?scheme=vscode-notebook-cell#abc" ) .unwrap() ), @@ -446,8 +449,7 @@ mod tests { &Url::parse("untitled:/path/to/file.ipynb#123").unwrap(), ), Some( - Url::parse("file:///path/to/file.ipynb.ts?scheme=untitled#123") - .unwrap() + Url::parse("file:///path/to/file.ipynb?scheme=untitled#123").unwrap() ), ); } diff --git a/cli/main.rs b/cli/main.rs index 6bbefcf956..8501a3487f 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -17,16 +17,18 @@ mod node; mod npm; mod ops; mod resolver; -mod shared; mod standalone; -mod sys; mod task_runner; mod tools; mod tsc; mod util; -mod version; mod worker; +pub mod sys { + #[allow(clippy::disallowed_types)] // ok, definition + pub type CliSys = sys_traits::impls::RealSys; +} + use std::env; use std::future::Future; use std::io::IsTerminal; @@ -40,18 +42,22 @@ use deno_core::error::AnyError; use deno_core::error::CoreError; use deno_core::futures::FutureExt; use deno_core::unsync::JoinHandle; -use deno_npm::resolution::SnapshotFromLockfileError; +use deno_lib::util::result::any_and_jserrorbox_downcast_ref; use deno_resolver::npm::ByonmResolvePkgFolderFromDenoReqError; use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; use deno_runtime::fmt_errors::format_js_error; use deno_runtime::tokio_util::create_and_run_current_thread_with_maybe_metrics; use deno_runtime::WorkerExecutionMode; pub use deno_runtime::UNSTABLE_GRANULAR_FLAGS; +use deno_telemetry::OtelConfig; use deno_terminal::colors; use factory::CliFactory; -use standalone::MODULE_NOT_FOUND; -use standalone::UNSUPPORTED_SCHEME; +const MODULE_NOT_FOUND: &str = "Module not found"; +const UNSUPPORTED_SCHEME: &str = "Unsupported scheme"; + +use self::npm::ResolveSnapshotError; +use self::util::draw_thread::DrawThread; use crate::args::flags_from_vec; use crate::args::DenoSubcommand; use crate::args::Flags; @@ -201,7 +207,7 @@ async fn run_subcommand(flags: Arc) -> Result { match result { Ok(v) => Ok(v), Err(script_err) => { - if let Some(ResolvePkgFolderFromDenoReqError::Byonm(ByonmResolvePkgFolderFromDenoReqError::UnmatchedReq(_))) = util::result::any_and_jserrorbox_downcast_ref::(&script_err) { + if let Some(worker::CreateCustomWorkerError::ResolvePkgFolderFromDenoReq(ResolvePkgFolderFromDenoReqError::Byonm(ByonmResolvePkgFolderFromDenoReqError::UnmatchedReq(_)))) = any_and_jserrorbox_downcast_ref::(&script_err) { if flags.node_modules_dir.is_none() { let mut flags = flags.deref().clone(); let watch = match &flags.subcommand { @@ -351,7 +357,7 @@ fn setup_panic_hook() { eprintln!("var set and include the backtrace in your report."); eprintln!(); eprintln!("Platform: {} {}", env::consts::OS, env::consts::ARCH); - eprintln!("Version: {}", version::DENO_VERSION_INFO.deno); + eprintln!("Version: {}", deno_lib::version::DENO_VERSION_INFO.deno); eprintln!("Args: {:?}", env::args().collect::>()); eprintln!(); orig_hook(panic_info); @@ -373,16 +379,16 @@ fn exit_for_error(error: AnyError) -> ! { let mut error_code = 1; if let Some(CoreError::Js(e)) = - util::result::any_and_jserrorbox_downcast_ref::(&error) + any_and_jserrorbox_downcast_ref::(&error) { error_string = format_js_error(e); - } else if let Some(SnapshotFromLockfileError::IntegrityCheckFailed(e)) = - util::result::any_and_jserrorbox_downcast_ref::( - &error, - ) + } else if let Some(e @ ResolveSnapshotError { .. }) = + any_and_jserrorbox_downcast_ref::(&error) { - error_string = e.to_string(); - error_code = 10; + if let Some(e) = e.maybe_integrity_check_error() { + error_string = e.to_string(); + error_code = 10; + } } exit_with_message(&error_string, error_code); @@ -440,19 +446,19 @@ fn resolve_flags_and_init( if err.kind() == clap::error::ErrorKind::DisplayVersion => { // Ignore results to avoid BrokenPipe errors. - util::logger::init(None, None); + init_logging(None, None); let _ = err.print(); deno_runtime::exit(0); } Err(err) => { - util::logger::init(None, None); + init_logging(None, None); exit_for_error(AnyError::from(err)) } }; let otel_config = flags.otel_config(); - deno_telemetry::init(crate::args::otel_runtime_config(), &otel_config)?; - util::logger::init(flags.log_level, Some(otel_config)); + deno_telemetry::init(deno_lib::version::otel_runtime_config(), &otel_config)?; + init_logging(flags.log_level, Some(otel_config)); // TODO(bartlomieju): remove in Deno v2.5 and hard error then. if flags.unstable_config.legacy_flag_enabled { @@ -485,3 +491,19 @@ fn resolve_flags_and_init( Ok(flags) } + +fn init_logging( + maybe_level: Option, + otel_config: Option, +) { + deno_lib::util::logger::init(deno_lib::util::logger::InitLoggingOptions { + maybe_level, + otel_config, + // it was considered to hold the draw thread's internal lock + // across logging, but if outputting to stderr blocks then that + // could potentially block other threads that access the draw + // thread's state + on_log_start: DrawThread::hide, + on_log_end: DrawThread::show, + }) +} diff --git a/cli/module_loader.rs b/cli/module_loader.rs index ba53077a3c..279fc6422e 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -13,8 +13,6 @@ use std::sync::Arc; use deno_ast::MediaType; use deno_ast::ModuleKind; -use deno_core::anyhow::anyhow; -use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::error::ModuleLoaderError; use deno_core::futures::future::FutureExt; @@ -39,9 +37,19 @@ use deno_graph::ModuleGraph; use deno_graph::ModuleGraphError; use deno_graph::Resolution; use deno_graph::WasmModule; +use deno_lib::loader::ModuleCodeStringSource; +use deno_lib::loader::NotSupportedKindInNpmError; +use deno_lib::loader::NpmModuleLoadError; +use deno_lib::npm::NpmRegistryReadPermissionChecker; +use deno_lib::util::hash::FastInsecureHasher; +use deno_lib::worker::CreateModuleLoaderResult; +use deno_lib::worker::ModuleLoaderFactory; +use deno_resolver::npm::DenoInNpmPackageChecker; use deno_runtime::code_cache; use deno_runtime::deno_node::create_host_defined_options; +use deno_runtime::deno_node::ops::require::UnableToGetCwdError; use deno_runtime::deno_node::NodeRequireLoader; +use deno_runtime::deno_node::RealIsBuiltInNodeModuleChecker; use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::npm::NpmPackageReqReference; use node_resolver::errors::ClosestPkgJsonError; @@ -56,7 +64,6 @@ use crate::args::CliOptions; use crate::args::DenoSubcommand; use crate::args::TsTypeLib; use crate::cache::CodeCache; -use crate::cache::FastInsecureHasher; use crate::cache::ParsedSourceCache; use crate::emit::Emitter; use crate::graph_container::MainModuleGraphContainer; @@ -66,16 +73,13 @@ use crate::graph_util::enhance_graph_error; use crate::graph_util::CreateGraphOptions; use crate::graph_util::EnhanceGraphErrorMode; use crate::graph_util::ModuleGraphBuilder; +use crate::node::CliCjsCodeAnalyzer; use crate::node::CliNodeCodeTranslator; use crate::node::CliNodeResolver; use crate::npm::CliNpmResolver; -use crate::npm::NpmRegistryReadPermissionChecker; -use crate::resolver::CjsTracker; +use crate::resolver::CliCjsTracker; use crate::resolver::CliNpmReqResolver; use crate::resolver::CliResolver; -use crate::resolver::ModuleCodeStringSource; -use crate::resolver::NotSupportedKindInNpmError; -use crate::resolver::NpmModuleLoader; use crate::sys::CliSys; use crate::tools::check; use crate::tools::check::CheckError; @@ -83,8 +87,14 @@ 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::CreateModuleLoaderResult; -use crate::worker::ModuleLoaderFactory; + +pub type CliNpmModuleLoader = deno_lib::loader::NpmModuleLoader< + CliCjsCodeAnalyzer, + DenoInNpmPackageChecker, + RealIsBuiltInNodeModuleChecker, + CliNpmResolver, + CliSys, +>; #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum PrepareModuleLoadError { @@ -98,6 +108,11 @@ pub enum PrepareModuleLoadError { Check(#[from] CheckError), #[class(inherit)] #[error(transparent)] + AtomicWriteFileWithRetries( + #[from] crate::args::AtomicWriteFileWithRetriesError, + ), + #[class(inherit)] + #[error(transparent)] Other(#[from] JsErrorBox), } @@ -233,18 +248,19 @@ struct SharedCliModuleLoaderState { initial_cwd: PathBuf, is_inspecting: bool, is_repl: bool, - cjs_tracker: Arc, + cjs_tracker: Arc, code_cache: Option>, emitter: Arc, - in_npm_pkg_checker: Arc, + in_npm_pkg_checker: DenoInNpmPackageChecker, main_module_graph_container: Arc, module_load_preparer: Arc, node_code_translator: Arc, node_resolver: Arc, - npm_module_loader: NpmModuleLoader, - npm_registry_permission_checker: Arc, + npm_module_loader: CliNpmModuleLoader, + npm_registry_permission_checker: + Arc>, npm_req_resolver: Arc, - npm_resolver: Arc, + npm_resolver: CliNpmResolver, parsed_source_cache: Arc, resolver: Arc, sys: CliSys, @@ -294,18 +310,20 @@ impl CliModuleLoaderFactory { #[allow(clippy::too_many_arguments)] pub fn new( options: &CliOptions, - cjs_tracker: Arc, + cjs_tracker: Arc, code_cache: Option>, emitter: Arc, - in_npm_pkg_checker: Arc, + in_npm_pkg_checker: DenoInNpmPackageChecker, main_module_graph_container: Arc, module_load_preparer: Arc, node_code_translator: Arc, node_resolver: Arc, - npm_module_loader: NpmModuleLoader, - npm_registry_permission_checker: Arc, + npm_module_loader: CliNpmModuleLoader, + npm_registry_permission_checker: Arc< + NpmRegistryReadPermissionChecker, + >, npm_req_resolver: Arc, - npm_resolver: Arc, + npm_resolver: CliNpmResolver, parsed_source_cache: Arc, resolver: Arc, sys: CliSys, @@ -415,6 +433,55 @@ impl ModuleLoaderFactory for CliModuleLoaderFactory { } } +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum LoadCodeSourceError { + #[class(inherit)] + #[error(transparent)] + NpmModuleLoad(NpmModuleLoadError), + #[class(inherit)] + #[error(transparent)] + LoadPreparedModule(#[from] LoadPreparedModuleError), + #[class(generic)] + #[error("Loading unprepared module: {}{}", .specifier, .maybe_referrer.as_ref().map(|r| format!(", imported from: {}", r)).unwrap_or_default())] + LoadUnpreparedModule { + specifier: ModuleSpecifier, + maybe_referrer: Option, + }, +} + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum LoadPreparedModuleError { + #[class(inherit)] + #[error(transparent)] + NpmModuleLoad(#[from] crate::emit::EmitParsedSourceHelperError), + #[class(inherit)] + #[error(transparent)] + LoadMaybeCjs(#[from] LoadMaybeCjsError), + #[class(inherit)] + #[error(transparent)] + Other(#[from] JsErrorBox), +} + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum LoadMaybeCjsError { + #[class(inherit)] + #[error(transparent)] + NpmModuleLoad(#[from] crate::emit::EmitParsedSourceHelperError), + #[class(inherit)] + #[error(transparent)] + TranslateCjsToEsm(#[from] node_resolver::analyze::TranslateCjsToEsmError), +} + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +#[class(inherit)] +#[error("Could not resolve '{reference}'")] +pub struct CouldNotResolveError { + reference: deno_semver::npm::NpmPackageNvReference, + #[source] + #[inherit] + source: node_resolver::errors::PackageSubpathResolveError, +} + struct CliModuleLoaderInner { lib: TsTypeLib, is_worker: bool, @@ -439,7 +506,10 @@ impl maybe_referrer: Option<&ModuleSpecifier>, requested_module_type: RequestedModuleType, ) -> Result { - let code_source = self.load_code_source(specifier, maybe_referrer).await?; + let code_source = self + .load_code_source(specifier, maybe_referrer) + .await + .map_err(JsErrorBox::from_err)?; let code = if self.shared.is_inspecting || code_source.media_type == MediaType::Wasm { @@ -500,7 +570,7 @@ impl &self, specifier: &ModuleSpecifier, maybe_referrer: Option<&ModuleSpecifier>, - ) -> Result { + ) -> Result { if let Some(code_source) = self.load_prepared_module(specifier).await? { return Ok(code_source); } @@ -509,14 +579,14 @@ impl .shared .npm_module_loader .load(specifier, maybe_referrer) - .await; + .await + .map_err(LoadCodeSourceError::NpmModuleLoad); } - 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)) + Err(LoadCodeSourceError::LoadUnpreparedModule { + specifier: specifier.clone(), + maybe_referrer: maybe_referrer.cloned(), + }) } fn resolve_referrer( @@ -539,7 +609,8 @@ impl .map_err(|e| e.into()) } else { // this cwd check is slow, so try to avoid it - let cwd = std::env::current_dir().context("Unable to get CWD")?; + let cwd = std::env::current_dir() + .map_err(|e| JsErrorBox::from_err(UnableToGetCwdError(e)))?; deno_core::resolve_path(referrer, &cwd).map_err(|e| e.into()) } } @@ -618,8 +689,11 @@ impl ResolutionMode::Import, NodeResolutionKind::Execution, ) - .with_context(|| { - format!("Could not resolve '{}'.", module.nv_reference) + .map_err(|source| { + JsErrorBox::from_err(CouldNotResolveError { + reference: module.nv_reference.clone(), + source, + }) })? } Some(Module::Node(module)) => module.specifier.clone(), @@ -640,7 +714,7 @@ impl async fn load_prepared_module( &self, specifier: &ModuleSpecifier, - ) -> Result, AnyError> { + ) -> Result, LoadPreparedModuleError> { // 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)? { @@ -672,7 +746,8 @@ impl }) => self .load_maybe_cjs(specifier, media_type, source) .await - .map(Some), + .map(Some) + .map_err(LoadPreparedModuleError::LoadMaybeCjs), None => Ok(None), } } @@ -833,7 +908,7 @@ impl specifier: &ModuleSpecifier, media_type: MediaType, original_source: &Arc, - ) -> Result { + ) -> Result { let js_source = if media_type.is_emittable() { Cow::Owned( self @@ -1139,12 +1214,13 @@ impl ModuleGraphUpdatePermit for WorkerModuleGraphUpdatePermit { #[derive(Debug)] struct CliNodeRequireLoader { - cjs_tracker: Arc, + cjs_tracker: Arc, emitter: Arc, sys: CliSys, graph_container: TGraphContainer, - in_npm_pkg_checker: Arc, - npm_registry_permission_checker: Arc, + in_npm_pkg_checker: DenoInNpmPackageChecker, + npm_registry_permission_checker: + Arc>, } impl NodeRequireLoader diff --git a/cli/node.rs b/cli/node.rs index aa44dcab18..90fd5645c7 100644 --- a/cli/node.rs +++ b/cli/node.rs @@ -5,8 +5,9 @@ use std::sync::Arc; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; -use deno_core::error::AnyError; +use deno_error::JsErrorBox; use deno_graph::ParsedSourceStore; +use deno_resolver::npm::DenoInNpmPackageChecker; use deno_runtime::deno_fs; use deno_runtime::deno_node::RealIsBuiltInNodeModuleChecker; use node_resolver::analyze::CjsAnalysis as ExtNodeCjsAnalysis; @@ -19,15 +20,22 @@ use serde::Serialize; use crate::cache::CacheDBHash; use crate::cache::NodeAnalysisCache; use crate::cache::ParsedSourceCache; -use crate::resolver::CjsTracker; +use crate::npm::CliNpmResolver; +use crate::resolver::CliCjsTracker; use crate::sys::CliSys; pub type CliNodeCodeTranslator = NodeCodeTranslator< CliCjsCodeAnalyzer, + DenoInNpmPackageChecker, RealIsBuiltInNodeModuleChecker, + CliNpmResolver, + CliSys, +>; +pub type CliNodeResolver = deno_runtime::deno_node::NodeResolver< + DenoInNpmPackageChecker, + CliNpmResolver, CliSys, >; -pub type CliNodeResolver = deno_runtime::deno_node::NodeResolver; pub type CliPackageJsonResolver = node_resolver::PackageJsonResolver; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -43,7 +51,7 @@ pub enum CliCjsAnalysis { pub struct CliCjsCodeAnalyzer { cache: NodeAnalysisCache, - cjs_tracker: Arc, + cjs_tracker: Arc, fs: deno_fs::FileSystemRc, parsed_source_cache: Option>, } @@ -51,7 +59,7 @@ pub struct CliCjsCodeAnalyzer { impl CliCjsCodeAnalyzer { pub fn new( cache: NodeAnalysisCache, - cjs_tracker: Arc, + cjs_tracker: Arc, fs: deno_fs::FileSystemRc, parsed_source_cache: Option>, ) -> Self { @@ -67,7 +75,7 @@ impl CliCjsCodeAnalyzer { &self, specifier: &ModuleSpecifier, source: &str, - ) -> Result { + ) -> Result { let source_hash = CacheDBHash::from_hashable(source); if let Some(analysis) = self.cache.get_cjs_analysis(specifier.as_str(), source_hash) @@ -84,7 +92,9 @@ impl CliCjsCodeAnalyzer { } let cjs_tracker = self.cjs_tracker.clone(); - let is_maybe_cjs = cjs_tracker.is_maybe_cjs(specifier, media_type)?; + let is_maybe_cjs = cjs_tracker + .is_maybe_cjs(specifier, media_type) + .map_err(JsErrorBox::from_err)?; let analysis = if is_maybe_cjs { let maybe_parsed_source = self .parsed_source_cache @@ -94,9 +104,10 @@ impl CliCjsCodeAnalyzer { 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(|| { + move || -> Result<_, JsErrorBox> { + let parsed_source = maybe_parsed_source + .map(Ok) + .unwrap_or_else(|| { deno_ast::parse_program(deno_ast::ParseParams { specifier, text: source, @@ -105,13 +116,16 @@ impl CliCjsCodeAnalyzer { scope_analysis: false, maybe_syntax: None, }) - })?; + }) + .map_err(JsErrorBox::from_err)?; 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, - )?; + let is_cjs = cjs_tracker + .is_cjs_with_known_is_script( + parsed_source.specifier(), + media_type, + is_script, + ) + .map_err(JsErrorBox::from_err)?; if is_cjs { let analysis = parsed_source.analyze_cjs(); Ok(CliCjsAnalysis::Cjs { @@ -143,7 +157,7 @@ impl CjsCodeAnalyzer for CliCjsCodeAnalyzer { &self, specifier: &ModuleSpecifier, source: Option>, - ) -> Result, AnyError> { + ) -> Result, JsErrorBox> { let source = match source { Some(source) => source, None => { diff --git a/cli/npm/byonm.rs b/cli/npm/byonm.rs deleted file mode 100644 index bd29a6ec72..0000000000 --- a/cli/npm/byonm.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2018-2025 the Deno authors. MIT license. - -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; - -use deno_core::serde_json; -use deno_core::url::Url; -use deno_resolver::npm::ByonmNpmResolver; -use deno_resolver::npm::ByonmNpmResolverCreateOptions; -use deno_resolver::npm::ByonmOrManagedNpmResolver; -use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; -use deno_runtime::ops::process::NpmProcessStateProvider; -use deno_semver::package::PackageReq; -use node_resolver::NpmPackageFolderResolver; - -use super::CliNpmResolver; -use super::InnerCliNpmResolverRef; -use crate::args::NpmProcessState; -use crate::args::NpmProcessStateKind; -use crate::sys::CliSys; - -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)] -struct CliByonmWrapper(Arc); - -impl NpmProcessStateProvider for CliByonmWrapper { - fn get_npm_process_state(&self) -> String { - serde_json::to_string(&NpmProcessState { - kind: NpmProcessStateKind::Byonm, - local_node_modules_path: self - .0 - .root_node_modules_dir() - .map(|p| p.to_string_lossy().to_string()), - }) - .unwrap() - } -} - -impl CliNpmResolver for CliByonmNpmResolver { - fn into_npm_pkg_folder_resolver( - self: Arc, - ) -> Arc { - self - } - - fn into_process_state_provider( - self: Arc, - ) -> Arc { - Arc::new(CliByonmWrapper(self)) - } - - fn into_byonm_or_managed( - self: Arc, - ) -> ByonmOrManagedNpmResolver { - ByonmOrManagedNpmResolver::Byonm(self) - } - - fn clone_snapshotted(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_inner(&self) -> InnerCliNpmResolverRef { - InnerCliNpmResolverRef::Byonm(self) - } - - fn root_node_modules_path(&self) -> Option<&Path> { - self.root_node_modules_dir() - } - - fn check_state_hash(&self) -> Option { - // it is very difficult to determine the check state hash for byonm - // so we just return None to signify check caching is not supported - None - } - - fn resolve_pkg_folder_from_deno_module_req( - &self, - req: &PackageReq, - referrer: &Url, - ) -> Result { - self - .resolve_pkg_folder_from_deno_module_req(req, referrer) - .map_err(ResolvePkgFolderFromDenoReqError::Byonm) - } -} diff --git a/cli/npm/managed/installers/common/bin_entries.rs b/cli/npm/installer/common/bin_entries.rs similarity index 99% rename from cli/npm/managed/installers/common/bin_entries.rs rename to cli/npm/installer/common/bin_entries.rs index bc69786b6c..2f7bed285a 100644 --- a/cli/npm/managed/installers/common/bin_entries.rs +++ b/cli/npm/installer/common/bin_entries.rs @@ -8,8 +8,7 @@ use std::path::PathBuf; use deno_npm::resolution::NpmResolutionSnapshot; use deno_npm::NpmPackageId; - -use crate::npm::managed::NpmResolutionPackage; +use deno_npm::NpmResolutionPackage; #[derive(Default)] pub struct BinEntries<'a> { diff --git a/cli/npm/managed/installers/common/lifecycle_scripts.rs b/cli/npm/installer/common/lifecycle_scripts.rs similarity index 98% rename from cli/npm/managed/installers/common/lifecycle_scripts.rs rename to cli/npm/installer/common/lifecycle_scripts.rs index a0d821cdfc..64b06aecbf 100644 --- a/cli/npm/managed/installers/common/lifecycle_scripts.rs +++ b/cli/npm/installer/common/lifecycle_scripts.rs @@ -220,7 +220,7 @@ impl<'a> LifecycleScripts<'a> { get_package_path, ); let init_cwd = &self.config.initial_cwd; - let process_state = crate::npm::managed::npm_process_state( + let process_state = deno_lib::npm::npm_process_state( snapshot.as_valid_serialized(), Some(root_node_modules_dir_path), ); @@ -240,7 +240,7 @@ impl<'a> LifecycleScripts<'a> { // However, if we concurrently run scripts in the future we will // have to have multiple temp files. let temp_file_fd = - deno_runtime::ops::process::npm_process_state_tempfile( + deno_runtime::deno_process::npm_process_state_tempfile( process_state.as_bytes(), ) .map_err(LifecycleScriptsError::CreateNpmProcessState)?; @@ -248,7 +248,7 @@ impl<'a> LifecycleScripts<'a> { let _temp_file = unsafe { std::fs::File::from_raw_io_handle(temp_file_fd) }; // make sure the file gets closed env_vars.insert( - deno_runtime::ops::process::NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME + deno_runtime::deno_process::NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME .to_string(), (temp_file_fd as usize).to_string(), ); diff --git a/cli/npm/managed/installers/common/mod.rs b/cli/npm/installer/common/mod.rs similarity index 79% rename from cli/npm/managed/installers/common/mod.rs rename to cli/npm/installer/common/mod.rs index 9659649a2e..bd22a58f03 100644 --- a/cli/npm/managed/installers/common/mod.rs +++ b/cli/npm/installer/common/mod.rs @@ -3,14 +3,14 @@ use async_trait::async_trait; use deno_error::JsErrorBox; -use crate::npm::PackageCaching; +use super::PackageCaching; pub mod bin_entries; pub mod lifecycle_scripts; /// Part of the resolution that interacts with the file system. #[async_trait(?Send)] -pub trait NpmPackageFsInstaller: Send + Sync { +pub trait NpmPackageFsInstaller: std::fmt::Debug + Send + Sync { async fn cache_packages<'a>( &self, caching: PackageCaching<'a>, diff --git a/cli/npm/managed/installers/global.rs b/cli/npm/installer/global.rs similarity index 96% rename from cli/npm/managed/installers/global.rs rename to cli/npm/installer/global.rs index 477d73dbfb..f074c62174 100644 --- a/cli/npm/managed/installers/global.rs +++ b/cli/npm/installer/global.rs @@ -9,16 +9,16 @@ use async_trait::async_trait; use deno_core::futures::stream::FuturesUnordered; use deno_core::futures::StreamExt; use deno_error::JsErrorBox; +use deno_lib::util::hash::FastInsecureHasher; use deno_npm::NpmResolutionPackage; use deno_npm::NpmSystemInfo; -use deno_resolver::npm::managed::NpmResolution; +use deno_resolver::npm::managed::NpmResolutionCell; use super::common::lifecycle_scripts::LifecycleScriptsStrategy; use super::common::NpmPackageFsInstaller; +use super::PackageCaching; use crate::args::LifecycleScriptsConfig; -use crate::cache::FastInsecureHasher; use crate::colors; -use crate::npm::managed::PackageCaching; use crate::npm::CliNpmCache; use crate::npm::CliNpmTarballCache; @@ -27,25 +27,25 @@ use crate::npm::CliNpmTarballCache; pub struct GlobalNpmPackageInstaller { cache: Arc, tarball_cache: Arc, - resolution: Arc, - system_info: NpmSystemInfo, + resolution: Arc, lifecycle_scripts: LifecycleScriptsConfig, + system_info: NpmSystemInfo, } impl GlobalNpmPackageInstaller { pub fn new( cache: Arc, tarball_cache: Arc, - resolution: Arc, - system_info: NpmSystemInfo, + resolution: Arc, lifecycle_scripts: LifecycleScriptsConfig, + system_info: NpmSystemInfo, ) -> Self { Self { cache, tarball_cache, resolution, - system_info, lifecycle_scripts, + system_info, } } } diff --git a/cli/npm/managed/installers/local.rs b/cli/npm/installer/local.rs similarity index 99% rename from cli/npm/managed/installers/local.rs rename to cli/npm/installer/local.rs index a8efaaeb00..87288c6c8e 100644 --- a/cli/npm/managed/installers/local.rs +++ b/cli/npm/installer/local.rs @@ -25,7 +25,7 @@ use deno_npm::NpmResolutionPackage; use deno_npm::NpmSystemInfo; use deno_path_util::fs::atomic_write_file_with_retries; use deno_resolver::npm::get_package_folder_id_folder_name; -use deno_resolver::npm::managed::NpmResolution; +use deno_resolver::npm::managed::NpmResolutionCell; use deno_semver::package::PackageNv; use deno_semver::StackString; use serde::Deserialize; @@ -33,11 +33,11 @@ use serde::Serialize; use super::common::bin_entries; use super::common::NpmPackageFsInstaller; +use super::PackageCaching; use crate::args::LifecycleScriptsConfig; use crate::args::NpmInstallDepsProvider; use crate::cache::CACHE_PERM; use crate::colors; -use crate::npm::managed::PackageCaching; use crate::npm::CliNpmCache; use crate::npm::CliNpmTarballCache; use crate::sys::CliSys; @@ -54,12 +54,12 @@ pub struct LocalNpmPackageInstaller { cache: Arc, npm_install_deps_provider: Arc, progress_bar: ProgressBar, - resolution: Arc, + resolution: Arc, sys: CliSys, tarball_cache: Arc, + lifecycle_scripts: LifecycleScriptsConfig, root_node_modules_path: PathBuf, system_info: NpmSystemInfo, - lifecycle_scripts: LifecycleScriptsConfig, } impl LocalNpmPackageInstaller { @@ -68,12 +68,12 @@ impl LocalNpmPackageInstaller { cache: Arc, npm_install_deps_provider: Arc, progress_bar: ProgressBar, - resolution: Arc, + resolution: Arc, sys: CliSys, tarball_cache: Arc, node_modules_folder: PathBuf, - system_info: NpmSystemInfo, lifecycle_scripts: LifecycleScriptsConfig, + system_info: NpmSystemInfo, ) -> Self { Self { cache, @@ -82,9 +82,9 @@ impl LocalNpmPackageInstaller { resolution, tarball_cache, sys, + lifecycle_scripts, root_node_modules_path: node_modules_folder, system_info, - lifecycle_scripts, } } } diff --git a/cli/npm/installer/mod.rs b/cli/npm/installer/mod.rs new file mode 100644 index 0000000000..58b9cb1bc7 --- /dev/null +++ b/cli/npm/installer/mod.rs @@ -0,0 +1,283 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; +use std::path::PathBuf; +use std::sync::Arc; + +use deno_core::error::AnyError; +use deno_core::unsync::sync::AtomicFlag; +use deno_error::JsErrorBox; +use deno_npm::registry::NpmPackageInfo; +use deno_npm::registry::NpmRegistryPackageInfoLoadError; +use deno_npm::NpmSystemInfo; +use deno_resolver::npm::managed::NpmResolutionCell; +use deno_runtime::colors; +use deno_semver::package::PackageReq; + +pub use self::common::NpmPackageFsInstaller; +use self::global::GlobalNpmPackageInstaller; +use self::local::LocalNpmPackageInstaller; +pub use self::resolution::AddPkgReqsResult; +pub use self::resolution::NpmResolutionInstaller; +use super::NpmResolutionInitializer; +use crate::args::CliLockfile; +use crate::args::LifecycleScriptsConfig; +use crate::args::NpmInstallDepsProvider; +use crate::args::PackageJsonDepValueParseWithLocationError; +use crate::npm::CliNpmCache; +use crate::npm::CliNpmTarballCache; +use crate::sys::CliSys; +use crate::util::progress_bar::ProgressBar; + +mod common; +mod global; +mod local; +mod resolution; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PackageCaching<'a> { + Only(Cow<'a, [PackageReq]>), + All, +} + +#[derive(Debug)] +pub struct NpmInstaller { + fs_installer: Arc, + npm_install_deps_provider: Arc, + npm_resolution_initializer: Arc, + npm_resolution_installer: Arc, + maybe_lockfile: Option>, + npm_resolution: Arc, + top_level_install_flag: AtomicFlag, +} + +impl NpmInstaller { + #[allow(clippy::too_many_arguments)] + pub fn new( + npm_cache: Arc, + npm_install_deps_provider: Arc, + npm_resolution: Arc, + npm_resolution_initializer: Arc, + npm_resolution_installer: Arc, + progress_bar: &ProgressBar, + sys: CliSys, + tarball_cache: Arc, + maybe_lockfile: Option>, + maybe_node_modules_path: Option, + lifecycle_scripts: LifecycleScriptsConfig, + system_info: NpmSystemInfo, + ) -> Self { + let fs_installer: Arc = + match maybe_node_modules_path { + Some(node_modules_folder) => Arc::new(LocalNpmPackageInstaller::new( + npm_cache, + npm_install_deps_provider.clone(), + progress_bar.clone(), + npm_resolution.clone(), + sys, + tarball_cache, + node_modules_folder, + lifecycle_scripts, + system_info, + )), + None => Arc::new(GlobalNpmPackageInstaller::new( + npm_cache, + tarball_cache, + npm_resolution.clone(), + lifecycle_scripts, + system_info, + )), + }; + Self { + fs_installer, + npm_install_deps_provider, + npm_resolution, + npm_resolution_initializer, + npm_resolution_installer, + maybe_lockfile, + top_level_install_flag: Default::default(), + } + } + + /// Adds package requirements to the resolver and ensures everything is setup. + /// This includes setting up the `node_modules` directory, if applicable. + pub async fn add_and_cache_package_reqs( + &self, + packages: &[PackageReq], + ) -> Result<(), JsErrorBox> { + self.npm_resolution_initializer.ensure_initialized().await?; + self + .add_package_reqs_raw( + packages, + Some(PackageCaching::Only(packages.into())), + ) + .await + .dependencies_result + } + + pub async fn add_package_reqs_no_cache( + &self, + packages: &[PackageReq], + ) -> Result<(), JsErrorBox> { + self.npm_resolution_initializer.ensure_initialized().await?; + self + .add_package_reqs_raw(packages, None) + .await + .dependencies_result + } + + pub async fn add_package_reqs( + &self, + packages: &[PackageReq], + caching: PackageCaching<'_>, + ) -> Result<(), JsErrorBox> { + self + .add_package_reqs_raw(packages, Some(caching)) + .await + .dependencies_result + } + + pub async fn add_package_reqs_raw<'a>( + &self, + packages: &[PackageReq], + caching: Option>, + ) -> AddPkgReqsResult { + if packages.is_empty() { + return AddPkgReqsResult { + dependencies_result: Ok(()), + results: vec![], + }; + } + + #[cfg(debug_assertions)] + self.npm_resolution_initializer.debug_assert_initialized(); + + let mut result = self + .npm_resolution_installer + .add_package_reqs(packages) + .await; + + if result.dependencies_result.is_ok() { + if let Some(lockfile) = self.maybe_lockfile.as_ref() { + result.dependencies_result = lockfile.error_if_changed(); + } + } + if result.dependencies_result.is_ok() { + if let Some(caching) = caching { + result.dependencies_result = self.cache_packages(caching).await; + } + } + + result + } + + /// Sets package requirements to the resolver, removing old requirements and adding new ones. + /// + /// This will retrieve and resolve package information, but not cache any package files. + pub async fn set_package_reqs( + &self, + packages: &[PackageReq], + ) -> Result<(), AnyError> { + self + .npm_resolution_installer + .set_package_reqs(packages) + .await + } + + pub async fn inject_synthetic_types_node_package( + &self, + ) -> Result<(), JsErrorBox> { + self.npm_resolution_initializer.ensure_initialized().await?; + let reqs = &[PackageReq::from_str("@types/node").unwrap()]; + // add and ensure this isn't added to the lockfile + self + .add_package_reqs(reqs, PackageCaching::Only(reqs.into())) + .await?; + + Ok(()) + } + + pub async fn cache_package_info( + &self, + package_name: &str, + ) -> Result, NpmRegistryPackageInfoLoadError> { + self + .npm_resolution_installer + .cache_package_info(package_name) + .await + } + + pub async fn cache_packages( + &self, + caching: PackageCaching<'_>, + ) -> Result<(), JsErrorBox> { + self.npm_resolution_initializer.ensure_initialized().await?; + self.fs_installer.cache_packages(caching).await + } + + 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.as_kind() { + deno_package_json::PackageJsonDepValueParseErrorKind::VersionReq(_) => { + return Err(Box::new(err.clone())); + } + deno_package_json::PackageJsonDepValueParseErrorKind::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. + /// + /// Returns `true` if the top level packages are already installed. A + /// return value of `false` means that new packages were added to the NPM resolution. + pub async fn ensure_top_level_package_json_install( + &self, + ) -> Result { + if !self.top_level_install_flag.raise() { + return Ok(true); // already did this + } + + self.npm_resolution_initializer.ensure_initialized().await?; + + let pkg_json_remote_pkgs = self.npm_install_deps_provider.remote_pkgs(); + if pkg_json_remote_pkgs.is_empty() { + return Ok(true); + } + + // check if something needs resolving before bothering to load all + // the package information (which is slow) + if pkg_json_remote_pkgs.iter().all(|pkg| { + self + .npm_resolution + .resolve_pkg_id_from_pkg_req(&pkg.req) + .is_ok() + }) { + log::debug!( + "All package.json deps resolvable. Skipping top level install." + ); + return Ok(true); // everything is already resolvable + } + + let pkg_reqs = pkg_json_remote_pkgs + .iter() + .map(|pkg| pkg.req.clone()) + .collect::>(); + self.add_package_reqs_no_cache(&pkg_reqs).await?; + + Ok(false) + } +} diff --git a/cli/npm/managed/installer.rs b/cli/npm/installer/resolution.rs similarity index 92% rename from cli/npm/managed/installer.rs rename to cli/npm/installer/resolution.rs index 9f1cc05968..06bbcd4f76 100644 --- a/cli/npm/managed/installer.rs +++ b/cli/npm/installer/resolution.rs @@ -8,12 +8,14 @@ use deno_core::error::AnyError; use deno_error::JsErrorBox; use deno_lockfile::NpmPackageDependencyLockfileInfo; use deno_lockfile::NpmPackageLockfileInfo; +use deno_npm::registry::NpmPackageInfo; use deno_npm::registry::NpmRegistryApi; +use deno_npm::registry::NpmRegistryPackageInfoLoadError; use deno_npm::resolution::AddPkgReqsOptions; use deno_npm::resolution::NpmResolutionError; use deno_npm::resolution::NpmResolutionSnapshot; use deno_npm::NpmResolutionPackage; -use deno_resolver::npm::managed::NpmResolution; +use deno_resolver::npm::managed::NpmResolutionCell; use deno_semver::jsr::JsrDepPackageReq; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; @@ -35,9 +37,10 @@ pub struct AddPkgReqsResult { } /// Updates the npm resolution with the provided package requirements. +#[derive(Debug)] pub struct NpmResolutionInstaller { registry_info_provider: Arc, - resolution: Arc, + resolution: Arc, maybe_lockfile: Option>, update_queue: TaskQueue, } @@ -45,7 +48,7 @@ pub struct NpmResolutionInstaller { impl NpmResolutionInstaller { pub fn new( registry_info_provider: Arc, - resolution: Arc, + resolution: Arc, maybe_lockfile: Option>, ) -> Self { Self { @@ -56,6 +59,14 @@ impl NpmResolutionInstaller { } } + pub async fn cache_package_info( + &self, + package_name: &str, + ) -> Result, NpmRegistryPackageInfoLoadError> { + // this will internally cache the package information + self.registry_info_provider.package_info(package_name).await + } + pub async fn add_package_reqs( &self, package_reqs: &[PackageReq], diff --git a/cli/npm/managed.rs b/cli/npm/managed.rs new file mode 100644 index 0000000000..14ba088d89 --- /dev/null +++ b/cli/npm/managed.rs @@ -0,0 +1,203 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::path::PathBuf; +use std::sync::Arc; + +use deno_core::parking_lot::Mutex; +use deno_error::JsError; +use deno_error::JsErrorBox; +use deno_npm::registry::NpmRegistryApi; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; +use deno_resolver::npm::managed::ManagedNpmResolverCreateOptions; +use deno_resolver::npm::managed::NpmResolutionCell; +use thiserror::Error; + +use super::CliNpmRegistryInfoProvider; +use crate::args::CliLockfile; +use crate::sys::CliSys; + +pub type CliManagedNpmResolverCreateOptions = + ManagedNpmResolverCreateOptions; + +#[derive(Debug, Clone)] +pub enum CliNpmResolverManagedSnapshotOption { + ResolveFromLockfile(Arc), + Specified(Option), +} + +#[derive(Debug)] +enum SyncState { + Pending(Option), + Err(ResolveSnapshotError), + Success, +} + +#[derive(Debug)] +pub struct NpmResolutionInitializer { + npm_registry_info_provider: Arc, + npm_resolution: Arc, + queue: tokio::sync::Mutex<()>, + sync_state: Mutex, +} + +impl NpmResolutionInitializer { + pub fn new( + npm_registry_info_provider: Arc, + npm_resolution: Arc, + snapshot_option: CliNpmResolverManagedSnapshotOption, + ) -> Self { + Self { + npm_registry_info_provider, + npm_resolution, + queue: tokio::sync::Mutex::new(()), + sync_state: Mutex::new(SyncState::Pending(Some(snapshot_option))), + } + } + + #[cfg(debug_assertions)] + pub fn debug_assert_initialized(&self) { + if !matches!(*self.sync_state.lock(), SyncState::Success) { + panic!("debug assert: npm resolution must be initialized before calling this code"); + } + } + + pub async fn ensure_initialized(&self) -> Result<(), JsErrorBox> { + // fast exit if not pending + { + match &*self.sync_state.lock() { + SyncState::Pending(_) => {} + SyncState::Err(err) => return Err(JsErrorBox::from_err(err.clone())), + SyncState::Success => return Ok(()), + } + } + + // only allow one task in here at a time + let _guard = self.queue.lock().await; + + let snapshot_option = { + let mut sync_state = self.sync_state.lock(); + match &mut *sync_state { + SyncState::Pending(snapshot_option) => { + // this should never panic, but if it does it means that a + // previous future was dropped while initialization occurred... + // that should never happen because this is initialized during + // startup + snapshot_option.take().unwrap() + } + // another thread updated the state while we were waiting + SyncState::Err(resolve_snapshot_error) => { + return Err(JsErrorBox::from_err(resolve_snapshot_error.clone())); + } + SyncState::Success => { + return Ok(()); + } + } + }; + + match resolve_snapshot(&self.npm_registry_info_provider, snapshot_option) + .await + { + Ok(maybe_snapshot) => { + if let Some(snapshot) = maybe_snapshot { + self + .npm_resolution + .set_snapshot(NpmResolutionSnapshot::new(snapshot)); + } + let mut sync_state = self.sync_state.lock(); + *sync_state = SyncState::Success; + Ok(()) + } + Err(err) => { + let mut sync_state = self.sync_state.lock(); + *sync_state = SyncState::Err(err.clone()); + Err(JsErrorBox::from_err(err)) + } + } + } +} + +#[derive(Debug, Error, Clone, JsError)] +#[error("failed reading lockfile '{}'", lockfile_path.display())] +#[class(inherit)] +pub struct ResolveSnapshotError { + lockfile_path: PathBuf, + #[inherit] + #[source] + source: SnapshotFromLockfileError, +} + +impl ResolveSnapshotError { + pub fn maybe_integrity_check_error( + &self, + ) -> Option<&deno_npm::resolution::IntegrityCheckFailedError> { + match &self.source { + SnapshotFromLockfileError::SnapshotFromLockfile( + deno_npm::resolution::SnapshotFromLockfileError::IntegrityCheckFailed( + err, + ), + ) => Some(err), + _ => None, + } + } +} + +async fn resolve_snapshot( + registry_info_provider: &Arc, + snapshot: CliNpmResolverManagedSnapshotOption, +) -> Result, ResolveSnapshotError> +{ + match snapshot { + CliNpmResolverManagedSnapshotOption::ResolveFromLockfile(lockfile) => { + if !lockfile.overwrite() { + let snapshot = snapshot_from_lockfile( + lockfile.clone(), + ®istry_info_provider.as_npm_registry_api(), + ) + .await + .map_err(|source| ResolveSnapshotError { + lockfile_path: lockfile.filename.clone(), + source, + })?; + Ok(Some(snapshot)) + } else { + Ok(None) + } + } + CliNpmResolverManagedSnapshotOption::Specified(snapshot) => Ok(snapshot), + } +} + +#[derive(Debug, Error, Clone, JsError)] +pub enum SnapshotFromLockfileError { + #[error(transparent)] + #[class(inherit)] + IncompleteError( + #[from] deno_npm::resolution::IncompleteSnapshotFromLockfileError, + ), + #[error(transparent)] + #[class(inherit)] + SnapshotFromLockfile(#[from] deno_npm::resolution::SnapshotFromLockfileError), +} + +async fn snapshot_from_lockfile( + lockfile: Arc, + api: &dyn NpmRegistryApi, +) -> Result { + let (incomplete_snapshot, skip_integrity_check) = { + let lock = lockfile.lock(); + ( + deno_npm::resolution::incomplete_snapshot_from_lockfile(&lock)?, + lock.overwrite, + ) + }; + let snapshot = deno_npm::resolution::snapshot_from_lockfile( + deno_npm::resolution::SnapshotFromLockfileParams { + incomplete_snapshot, + api, + skip_integrity_check, + }, + ) + .await?; + Ok(snapshot) +} diff --git a/cli/npm/managed/installers/mod.rs b/cli/npm/managed/installers/mod.rs deleted file mode 100644 index 39e0d6f77c..0000000000 --- a/cli/npm/managed/installers/mod.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2018-2025 the Deno authors. MIT license. - -use std::path::PathBuf; -use std::sync::Arc; - -use deno_npm::NpmSystemInfo; -use deno_resolver::npm::managed::NpmResolution; - -pub use self::common::NpmPackageFsInstaller; -use self::global::GlobalNpmPackageInstaller; -use self::local::LocalNpmPackageInstaller; -use crate::args::LifecycleScriptsConfig; -use crate::args::NpmInstallDepsProvider; -use crate::npm::CliNpmCache; -use crate::npm::CliNpmTarballCache; -use crate::sys::CliSys; -use crate::util::progress_bar::ProgressBar; - -mod common; -mod global; -mod local; - -#[allow(clippy::too_many_arguments)] -pub fn create_npm_fs_installer( - npm_cache: Arc, - npm_install_deps_provider: &Arc, - progress_bar: &ProgressBar, - resolution: Arc, - sys: CliSys, - tarball_cache: Arc, - maybe_node_modules_path: Option, - system_info: NpmSystemInfo, - lifecycle_scripts: LifecycleScriptsConfig, -) -> Arc { - match maybe_node_modules_path { - Some(node_modules_folder) => Arc::new(LocalNpmPackageInstaller::new( - npm_cache, - npm_install_deps_provider.clone(), - progress_bar.clone(), - resolution, - sys, - tarball_cache, - node_modules_folder, - system_info, - lifecycle_scripts, - )), - None => Arc::new(GlobalNpmPackageInstaller::new( - npm_cache, - tarball_cache, - resolution, - system_info, - lifecycle_scripts, - )), - } -} diff --git a/cli/npm/managed/mod.rs b/cli/npm/managed/mod.rs deleted file mode 100644 index 8d2ede7e67..0000000000 --- a/cli/npm/managed/mod.rs +++ /dev/null @@ -1,768 +0,0 @@ -// Copyright 2018-2025 the Deno authors. MIT license. - -use std::borrow::Cow; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; - -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_error::JsErrorBox; -use deno_npm::npm_rc::ResolvedNpmRc; -use deno_npm::registry::NpmPackageInfo; -use deno_npm::registry::NpmRegistryApi; -use deno_npm::resolution::NpmResolutionSnapshot; -use deno_npm::resolution::PackageReqNotFoundError; -use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; -use deno_npm::NpmPackageId; -use deno_npm::NpmResolutionPackage; -use deno_npm::NpmSystemInfo; -use deno_npm_cache::NpmCacheSetting; -use deno_resolver::npm::managed::NpmResolution; -use deno_resolver::npm::managed::ResolvePkgFolderFromPkgIdError; -use deno_resolver::npm::ByonmOrManagedNpmResolver; -use deno_resolver::npm::ManagedNpmResolver; -use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; -use deno_runtime::colors; -use deno_runtime::ops::process::NpmProcessStateProvider; -use deno_semver::package::PackageNv; -use deno_semver::package::PackageReq; -use installer::AddPkgReqsResult; -use installer::NpmResolutionInstaller; -use installers::create_npm_fs_installer; -use installers::NpmPackageFsInstaller; -use node_resolver::NpmPackageFolderResolver; - -use super::CliNpmCache; -use super::CliNpmCacheHttpClient; -use super::CliNpmRegistryInfoProvider; -use super::CliNpmResolver; -use super::CliNpmTarballCache; -use super::InnerCliNpmResolverRef; -use crate::args::CliLockfile; -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::sys::CliSys; -use crate::util::progress_bar::ProgressBar; -use crate::util::sync::AtomicFlag; - -mod installer; -mod installers; - -pub enum CliNpmResolverManagedSnapshotOption { - ResolveFromLockfile(Arc), - Specified(Option), -} - -pub struct CliManagedNpmResolverCreateOptions { - pub snapshot: CliNpmResolverManagedSnapshotOption, - pub maybe_lockfile: Option>, - pub http_client_provider: Arc, - pub npm_cache_dir: Arc, - pub sys: CliSys, - pub cache_setting: deno_cache_dir::file_fetcher::CacheSetting, - pub text_only_progress_bar: crate::util::progress_bar::ProgressBar, - pub maybe_node_modules_path: Option, - pub npm_system_info: NpmSystemInfo, - pub npm_install_deps_provider: Arc, - pub npmrc: Arc, - pub lifecycle_scripts: LifecycleScriptsConfig, -} - -pub async fn create_managed_npm_resolver_for_lsp( - options: CliManagedNpmResolverCreateOptions, -) -> Arc { - let npm_cache = create_cache(&options); - let http_client = Arc::new(CliNpmCacheHttpClient::new( - options.http_client_provider.clone(), - options.text_only_progress_bar.clone(), - )); - let npm_api = create_api(npm_cache.clone(), http_client.clone(), &options); - // spawn due to the lsp's `Send` requirement - deno_core::unsync::spawn(async move { - let snapshot = match resolve_snapshot(&npm_api, options.snapshot).await { - Ok(snapshot) => snapshot, - Err(err) => { - log::warn!("failed to resolve snapshot: {}", err); - None - } - }; - create_inner( - http_client, - npm_cache, - options.npm_cache_dir, - options.npm_install_deps_provider, - npm_api, - options.sys, - options.text_only_progress_bar, - options.maybe_lockfile, - options.npmrc, - options.maybe_node_modules_path, - options.npm_system_info, - snapshot, - options.lifecycle_scripts, - ) - }) - .await - .unwrap() -} - -pub async fn create_managed_npm_resolver( - options: CliManagedNpmResolverCreateOptions, -) -> Result, AnyError> { - let npm_cache = create_cache(&options); - let http_client = Arc::new(CliNpmCacheHttpClient::new( - options.http_client_provider.clone(), - options.text_only_progress_bar.clone(), - )); - let api = create_api(npm_cache.clone(), http_client.clone(), &options); - let snapshot = resolve_snapshot(&api, options.snapshot).await?; - Ok(create_inner( - http_client, - npm_cache, - options.npm_cache_dir, - options.npm_install_deps_provider, - api, - options.sys, - options.text_only_progress_bar, - options.maybe_lockfile, - options.npmrc, - options.maybe_node_modules_path, - options.npm_system_info, - snapshot, - options.lifecycle_scripts, - )) -} - -#[allow(clippy::too_many_arguments)] -fn create_inner( - http_client: Arc, - npm_cache: Arc, - npm_cache_dir: Arc, - npm_install_deps_provider: Arc, - registry_info_provider: Arc, - sys: CliSys, - text_only_progress_bar: crate::util::progress_bar::ProgressBar, - maybe_lockfile: Option>, - npm_rc: Arc, - node_modules_dir_path: Option, - npm_system_info: NpmSystemInfo, - snapshot: Option, - lifecycle_scripts: LifecycleScriptsConfig, -) -> Arc { - let resolution = Arc::new(NpmResolution::from_serialized(snapshot)); - let tarball_cache = Arc::new(CliNpmTarballCache::new( - npm_cache.clone(), - http_client, - sys.clone(), - npm_rc.clone(), - )); - - let fs_installer = create_npm_fs_installer( - npm_cache.clone(), - &npm_install_deps_provider, - &text_only_progress_bar, - resolution.clone(), - sys.clone(), - tarball_cache.clone(), - node_modules_dir_path.clone(), - npm_system_info.clone(), - lifecycle_scripts.clone(), - ); - let managed_npm_resolver = - Arc::new(ManagedNpmResolver::::new::( - &npm_cache_dir, - &npm_rc, - resolution.clone(), - sys.clone(), - node_modules_dir_path, - )); - Arc::new(ManagedCliNpmResolver::new( - fs_installer, - maybe_lockfile, - managed_npm_resolver, - npm_cache, - npm_cache_dir, - npm_install_deps_provider, - npm_rc, - registry_info_provider, - resolution, - sys, - tarball_cache, - text_only_progress_bar, - npm_system_info, - lifecycle_scripts, - )) -} - -fn create_cache( - options: &CliManagedNpmResolverCreateOptions, -) -> Arc { - Arc::new(CliNpmCache::new( - options.npm_cache_dir.clone(), - options.sys.clone(), - NpmCacheSetting::from_cache_setting(&options.cache_setting), - options.npmrc.clone(), - )) -} - -fn create_api( - cache: Arc, - http_client: Arc, - options: &CliManagedNpmResolverCreateOptions, -) -> Arc { - Arc::new(CliNpmRegistryInfoProvider::new( - cache, - http_client, - options.npmrc.clone(), - )) -} - -async fn resolve_snapshot( - registry_info_provider: &Arc, - snapshot: CliNpmResolverManagedSnapshotOption, -) -> Result, AnyError> { - match snapshot { - CliNpmResolverManagedSnapshotOption::ResolveFromLockfile(lockfile) => { - if !lockfile.overwrite() { - let snapshot = snapshot_from_lockfile( - lockfile.clone(), - ®istry_info_provider.as_npm_registry_api(), - ) - .await - .with_context(|| { - format!("failed reading lockfile '{}'", lockfile.filename.display()) - })?; - Ok(Some(snapshot)) - } else { - Ok(None) - } - } - CliNpmResolverManagedSnapshotOption::Specified(snapshot) => Ok(snapshot), - } -} - -async fn snapshot_from_lockfile( - lockfile: Arc, - api: &dyn NpmRegistryApi, -) -> Result { - let (incomplete_snapshot, skip_integrity_check) = { - let lock = lockfile.lock(); - ( - deno_npm::resolution::incomplete_snapshot_from_lockfile(&lock)?, - lock.overwrite, - ) - }; - let snapshot = deno_npm::resolution::snapshot_from_lockfile( - deno_npm::resolution::SnapshotFromLockfileParams { - incomplete_snapshot, - api, - skip_integrity_check, - }, - ) - .await?; - Ok(snapshot) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PackageCaching<'a> { - Only(Cow<'a, [PackageReq]>), - All, -} - -#[derive(Debug, thiserror::Error, deno_error::JsError)] -pub enum ResolvePkgFolderFromDenoModuleError { - #[class(inherit)] - #[error(transparent)] - PackageNvNotFound(#[from] deno_npm::resolution::PackageNvNotFoundError), - #[class(inherit)] - #[error(transparent)] - ResolvePkgFolderFromPkgId(#[from] ResolvePkgFolderFromPkgIdError), -} - -/// 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 { - fs_installer: Arc, - maybe_lockfile: Option>, - registry_info_provider: Arc, - managed_npm_resolver: Arc>, - npm_cache: Arc, - npm_cache_dir: Arc, - npm_install_deps_provider: Arc, - npm_rc: Arc, - sys: CliSys, - resolution: Arc, - resolution_installer: NpmResolutionInstaller, - tarball_cache: Arc, - text_only_progress_bar: ProgressBar, - npm_system_info: NpmSystemInfo, - top_level_install_flag: AtomicFlag, - lifecycle_scripts: LifecycleScriptsConfig, -} - -impl std::fmt::Debug for ManagedCliNpmResolver { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ManagedCliNpmResolver") - .field("", &"") - .finish() - } -} - -impl ManagedCliNpmResolver { - #[allow(clippy::too_many_arguments)] - pub fn new( - fs_installer: Arc, - maybe_lockfile: Option>, - managed_npm_resolver: Arc>, - npm_cache: Arc, - npm_cache_dir: Arc, - npm_install_deps_provider: Arc, - npm_rc: Arc, - registry_info_provider: Arc, - resolution: Arc, - sys: CliSys, - tarball_cache: Arc, - text_only_progress_bar: ProgressBar, - npm_system_info: NpmSystemInfo, - lifecycle_scripts: LifecycleScriptsConfig, - ) -> Self { - let resolution_installer = NpmResolutionInstaller::new( - registry_info_provider.clone(), - resolution.clone(), - maybe_lockfile.clone(), - ); - Self { - fs_installer, - maybe_lockfile, - managed_npm_resolver, - npm_cache, - npm_cache_dir, - npm_install_deps_provider, - npm_rc, - registry_info_provider, - text_only_progress_bar, - resolution, - resolution_installer, - sys, - tarball_cache, - npm_system_info, - top_level_install_flag: Default::default(), - lifecycle_scripts, - } - } - - pub fn resolve_pkg_folder_from_pkg_id( - &self, - pkg_id: &NpmPackageId, - ) -> Result { - self - .managed_npm_resolver - .resolve_pkg_folder_from_pkg_id(pkg_id) - } - - /// Resolves the package id from the provided specifier. - pub fn resolve_pkg_id_from_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Result, AnyError> { - let Some(cache_folder_id) = self - .managed_npm_resolver - .resolve_package_cache_folder_id_from_specifier(specifier)? - else { - return Ok(None); - }; - Ok(Some( - self - .resolution - .resolve_pkg_id_from_pkg_cache_folder_id(&cache_folder_id)?, - )) - } - - pub fn resolve_pkg_reqs_from_pkg_id( - &self, - id: &NpmPackageId, - ) -> Vec { - self.resolution.resolve_pkg_reqs_from_pkg_id(id) - } - - /// Attempts to get the package size in bytes. - pub fn package_size( - &self, - package_id: &NpmPackageId, - ) -> Result { - let package_folder = self - .managed_npm_resolver - .resolve_pkg_folder_from_pkg_id(package_id)?; - Ok(crate::util::fs::dir_size(&package_folder)?) - } - - pub fn all_system_packages( - &self, - system_info: &NpmSystemInfo, - ) -> Vec { - self.resolution.all_system_packages(system_info) - } - - /// Checks if the provided package req's folder is cached. - pub fn is_pkg_req_folder_cached(&self, req: &PackageReq) -> bool { - self - .resolve_pkg_id_from_pkg_req(req) - .ok() - .and_then(|id| { - self - .managed_npm_resolver - .resolve_pkg_folder_from_pkg_id(&id) - .ok() - }) - .map(|folder| folder.exists()) - .unwrap_or(false) - } - - /// Adds package requirements to the resolver and ensures everything is setup. - /// This includes setting up the `node_modules` directory, if applicable. - pub async fn add_and_cache_package_reqs( - &self, - packages: &[PackageReq], - ) -> Result<(), JsErrorBox> { - self - .add_package_reqs_raw( - packages, - Some(PackageCaching::Only(packages.into())), - ) - .await - .dependencies_result - } - - pub async fn add_package_reqs_no_cache( - &self, - packages: &[PackageReq], - ) -> Result<(), JsErrorBox> { - self - .add_package_reqs_raw(packages, None) - .await - .dependencies_result - } - - pub async fn add_package_reqs( - &self, - packages: &[PackageReq], - caching: PackageCaching<'_>, - ) -> Result<(), JsErrorBox> { - self - .add_package_reqs_raw(packages, Some(caching)) - .await - .dependencies_result - } - - pub async fn add_package_reqs_raw<'a>( - &self, - packages: &[PackageReq], - caching: Option>, - ) -> AddPkgReqsResult { - if packages.is_empty() { - return AddPkgReqsResult { - dependencies_result: Ok(()), - results: vec![], - }; - } - - let mut result = self.resolution_installer.add_package_reqs(packages).await; - - if result.dependencies_result.is_ok() { - if let Some(lockfile) = self.maybe_lockfile.as_ref() { - result.dependencies_result = lockfile.error_if_changed(); - } - } - if result.dependencies_result.is_ok() { - if let Some(caching) = caching { - result.dependencies_result = self.cache_packages(caching).await; - } - } - - result - } - - /// Sets package requirements to the resolver, removing old requirements and adding new ones. - /// - /// This will retrieve and resolve package information, but not cache any package files. - pub async fn set_package_reqs( - &self, - packages: &[PackageReq], - ) -> Result<(), AnyError> { - self.resolution_installer.set_package_reqs(packages).await - } - - pub fn snapshot(&self) -> NpmResolutionSnapshot { - 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, - ) -> ValidSerializedNpmResolutionSnapshot { - self - .resolution - .serialized_valid_snapshot_for_system(system_info) - } - - pub async fn inject_synthetic_types_node_package( - &self, - ) -> Result<(), JsErrorBox> { - let reqs = &[PackageReq::from_str("@types/node").unwrap()]; - // add and ensure this isn't added to the lockfile - self - .add_package_reqs(reqs, PackageCaching::Only(reqs.into())) - .await?; - - Ok(()) - } - - pub async fn cache_packages( - &self, - caching: PackageCaching<'_>, - ) -> Result<(), JsErrorBox> { - self.fs_installer.cache_packages(caching).await - } - - pub fn resolve_pkg_folder_from_deno_module( - &self, - nv: &PackageNv, - ) -> Result { - let pkg_id = self.resolution.resolve_pkg_id_from_deno_module(nv)?; - Ok(self.resolve_pkg_folder_from_pkg_id(&pkg_id)?) - } - - pub fn resolve_pkg_id_from_pkg_req( - &self, - req: &PackageReq, - ) -> Result { - 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.as_kind() { - deno_package_json::PackageJsonDepValueParseErrorKind::VersionReq(_) => { - return Err(Box::new(err.clone())); - } - deno_package_json::PackageJsonDepValueParseErrorKind::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. - /// - /// Returns `true` if the top level packages are already installed. A - /// return value of `false` means that new packages were added to the NPM resolution. - pub async fn ensure_top_level_package_json_install( - &self, - ) -> Result { - if !self.top_level_install_flag.raise() { - return Ok(true); // already did this - } - - let pkg_json_remote_pkgs = self.npm_install_deps_provider.remote_pkgs(); - if pkg_json_remote_pkgs.is_empty() { - return Ok(true); - } - - // check if something needs resolving before bothering to load all - // the package information (which is slow) - if pkg_json_remote_pkgs.iter().all(|pkg| { - self - .resolution - .resolve_pkg_id_from_pkg_req(&pkg.req) - .is_ok() - }) { - log::debug!( - "All package.json deps resolvable. Skipping top level install." - ); - return Ok(true); // everything is already resolvable - } - - let pkg_reqs = pkg_json_remote_pkgs - .iter() - .map(|pkg| pkg.req.clone()) - .collect::>(); - self.add_package_reqs_no_cache(&pkg_reqs).await?; - - Ok(false) - } - - pub async fn cache_package_info( - &self, - package_name: &str, - ) -> Result, AnyError> { - // this will internally cache the package information - self - .registry_info_provider - .package_info(package_name) - .await - .map_err(|err| err.into()) - } - - pub fn maybe_node_modules_path(&self) -> Option<&Path> { - self.managed_npm_resolver.node_modules_path() - } - - pub fn global_cache_root_path(&self) -> &Path { - self.npm_cache_dir.root_dir() - } - - pub fn global_cache_root_url(&self) -> &Url { - self.npm_cache_dir.root_dir_url() - } -} - -fn npm_process_state( - snapshot: ValidSerializedNpmResolutionSnapshot, - node_modules_path: Option<&Path>, -) -> String { - serde_json::to_string(&NpmProcessState { - kind: NpmProcessStateKind::Snapshot(snapshot.into_serialized()), - local_node_modules_path: node_modules_path - .map(|p| p.to_string_lossy().to_string()), - }) - .unwrap() -} - -impl NpmProcessStateProvider for ManagedCliNpmResolver { - fn get_npm_process_state(&self) -> String { - npm_process_state( - self.resolution.serialized_valid_snapshot(), - self.managed_npm_resolver.node_modules_path(), - ) - } -} - -impl CliNpmResolver for ManagedCliNpmResolver { - fn into_npm_pkg_folder_resolver( - self: Arc, - ) -> Arc { - self.managed_npm_resolver.clone() - } - - fn into_process_state_provider( - self: Arc, - ) -> Arc { - self - } - - fn into_byonm_or_managed( - self: Arc, - ) -> ByonmOrManagedNpmResolver { - ByonmOrManagedNpmResolver::Managed(self.managed_npm_resolver.clone()) - } - - fn clone_snapshotted(&self) -> Arc { - // create a new snapshotted npm resolution and resolver - let npm_resolution = - Arc::new(NpmResolution::new(self.resolution.snapshot())); - - Arc::new(ManagedCliNpmResolver::new( - create_npm_fs_installer( - self.npm_cache.clone(), - &self.npm_install_deps_provider, - &self.text_only_progress_bar, - npm_resolution.clone(), - self.sys.clone(), - self.tarball_cache.clone(), - self.root_node_modules_path().map(ToOwned::to_owned), - self.npm_system_info.clone(), - self.lifecycle_scripts.clone(), - ), - self.maybe_lockfile.clone(), - Arc::new(ManagedNpmResolver::::new::( - &self.npm_cache_dir, - &self.npm_rc, - npm_resolution.clone(), - self.sys.clone(), - self.root_node_modules_path().map(ToOwned::to_owned), - )), - self.npm_cache.clone(), - self.npm_cache_dir.clone(), - self.npm_install_deps_provider.clone(), - self.npm_rc.clone(), - self.registry_info_provider.clone(), - npm_resolution, - self.sys.clone(), - self.tarball_cache.clone(), - self.text_only_progress_bar.clone(), - self.npm_system_info.clone(), - self.lifecycle_scripts.clone(), - )) - } - - fn as_inner(&self) -> InnerCliNpmResolverRef { - InnerCliNpmResolverRef::Managed(self) - } - - fn root_node_modules_path(&self) -> Option<&Path> { - self.managed_npm_resolver.node_modules_path() - } - - fn check_state_hash(&self) -> Option { - // We could go further and check all the individual - // npm packages, but that's probably overkill. - let mut package_reqs = self - .resolution - .package_reqs() - .into_iter() - .collect::>(); - package_reqs.sort_by(|a, b| a.0.cmp(&b.0)); // determinism - let mut hasher = FastInsecureHasher::new_without_deno_version(); - // ensure the cache gets busted when turning nodeModulesDir on or off - // as this could cause changes in resolution - hasher - .write_hashable(self.managed_npm_resolver.node_modules_path().is_some()); - for (pkg_req, pkg_nv) in package_reqs { - hasher.write_hashable(&pkg_req); - hasher.write_hashable(&pkg_nv); - } - Some(hasher.finish()) - } - - fn resolve_pkg_folder_from_deno_module_req( - &self, - req: &PackageReq, - referrer: &Url, - ) -> Result { - self - .managed_npm_resolver - .resolve_pkg_folder_from_deno_module_req(req, referrer) - .map_err(ResolvePkgFolderFromDenoReqError::Managed) - } -} diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index 052a98e6cf..388b007f81 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -1,39 +1,27 @@ // Copyright 2018-2025 the Deno authors. MIT license. -mod byonm; +pub mod installer; mod managed; -mod permission_checker; -use std::path::Path; -use std::path::PathBuf; use std::sync::Arc; use dashmap::DashMap; -use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::url::Url; use deno_error::JsErrorBox; +use deno_lib::version::DENO_VERSION_INFO; use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::registry::NpmPackageInfo; -use deno_resolver::npm::ByonmNpmResolver; -use deno_resolver::npm::ByonmOrManagedNpmResolver; -use deno_resolver::npm::ResolvePkgFolderFromDenoReqError; -use deno_runtime::ops::process::NpmProcessStateProvider; +use deno_resolver::npm::ByonmNpmResolverCreateOptions; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use http::HeaderName; use http::HeaderValue; -use node_resolver::NpmPackageFolderResolver; -pub use self::byonm::CliByonmNpmResolver; -pub use self::byonm::CliByonmNpmResolverCreateOptions; pub use self::managed::CliManagedNpmResolverCreateOptions; pub use self::managed::CliNpmResolverManagedSnapshotOption; -pub use self::managed::ManagedCliNpmResolver; -pub use self::managed::PackageCaching; -pub use self::managed::ResolvePkgFolderFromDenoModuleError; -pub use self::permission_checker::NpmRegistryReadPermissionChecker; -pub use self::permission_checker::NpmRegistryReadPermissionCheckerMode; +pub use self::managed::NpmResolutionInitializer; +pub use self::managed::ResolveSnapshotError; use crate::file_fetcher::CliFileFetcher; use crate::http_util::HttpClientProvider; use crate::sys::CliSys; @@ -44,6 +32,12 @@ pub type CliNpmTarballCache = pub type CliNpmCache = deno_npm_cache::NpmCache; pub type CliNpmRegistryInfoProvider = deno_npm_cache::RegistryInfoProvider; +pub type CliNpmResolver = deno_resolver::npm::NpmResolver; +pub type CliManagedNpmResolver = deno_resolver::npm::ManagedNpmResolver; +pub type CliNpmResolverCreateOptions = + deno_resolver::npm::NpmResolverCreateOptions; +pub type CliByonmNpmResolverCreateOptions = + ByonmNpmResolverCreateOptions; #[derive(Debug)] pub struct CliNpmCacheHttpClient { @@ -104,82 +98,6 @@ impl deno_npm_cache::NpmCacheHttpClient for CliNpmCacheHttpClient { } } -pub enum CliNpmResolverCreateOptions { - Managed(CliManagedNpmResolverCreateOptions), - Byonm(CliByonmNpmResolverCreateOptions), -} - -pub async fn create_cli_npm_resolver_for_lsp( - options: CliNpmResolverCreateOptions, -) -> Arc { - use CliNpmResolverCreateOptions::*; - match options { - Managed(options) => { - managed::create_managed_npm_resolver_for_lsp(options).await - } - Byonm(options) => Arc::new(ByonmNpmResolver::new(options)), - } -} - -pub async fn create_cli_npm_resolver( - options: CliNpmResolverCreateOptions, -) -> Result, AnyError> { - use CliNpmResolverCreateOptions::*; - match options { - Managed(options) => managed::create_managed_npm_resolver(options).await, - Byonm(options) => Ok(Arc::new(ByonmNpmResolver::new(options))), - } -} - -pub enum InnerCliNpmResolverRef<'a> { - Managed(&'a ManagedCliNpmResolver), - #[allow(dead_code)] - Byonm(&'a CliByonmNpmResolver), -} - -// todo(dsherret): replace with an enum -pub trait CliNpmResolver: Send + Sync + std::fmt::Debug { - fn into_npm_pkg_folder_resolver( - self: Arc, - ) -> Arc; - fn into_process_state_provider( - self: Arc, - ) -> Arc; - fn into_byonm_or_managed( - self: Arc, - ) -> ByonmOrManagedNpmResolver; - - fn clone_snapshotted(&self) -> Arc; - - fn as_inner(&self) -> InnerCliNpmResolverRef; - - fn as_managed(&self) -> Option<&ManagedCliNpmResolver> { - match self.as_inner() { - InnerCliNpmResolverRef::Managed(inner) => Some(inner), - InnerCliNpmResolverRef::Byonm(_) => None, - } - } - - fn as_byonm(&self) -> Option<&CliByonmNpmResolver> { - match self.as_inner() { - InnerCliNpmResolverRef::Managed(_) => None, - InnerCliNpmResolverRef::Byonm(inner) => Some(inner), - } - } - - fn resolve_pkg_folder_from_deno_module_req( - &self, - req: &PackageReq, - referrer: &Url, - ) -> Result; - - fn root_node_modules_path(&self) -> Option<&Path>; - - /// Returns a hash returning the state of the npm resolver - /// or `None` if the state currently can't be determined. - fn check_state_hash(&self) -> Option; -} - #[derive(Debug)] pub struct NpmFetchResolver { nv_by_req: DashMap>, @@ -265,8 +183,8 @@ 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"), + DENO_VERSION_INFO.deno, + DENO_VERSION_INFO.deno, std::env::consts::OS, std::env::consts::ARCH ) diff --git a/cli/resolver.rs b/cli/resolver.rs index 1d12d5f8b7..35ae25da9e 100644 --- a/cli/resolver.rs +++ b/cli/resolver.rs @@ -1,17 +1,11 @@ // Copyright 2018-2025 the Deno authors. MIT license. -use std::borrow::Cow; use std::sync::Arc; use async_trait::async_trait; use dashmap::DashSet; -use deno_ast::MediaType; use deno_config::workspace::MappedResolutionDiagnostic; use deno_config::workspace::MappedResolutionError; -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_error::JsErrorBox; use deno_graph::source::ResolveError; @@ -19,193 +13,69 @@ use deno_graph::source::UnknownBuiltInNodeModuleError; use deno_graph::NpmLoadError; use deno_graph::NpmResolvePkgReqsResult; use deno_npm::resolution::NpmResolutionError; +use deno_resolver::npm::DenoInNpmPackageChecker; use deno_resolver::sloppy_imports::SloppyImportsCachedFs; use deno_resolver::sloppy_imports::SloppyImportsResolver; use deno_runtime::colors; -use deno_runtime::deno_fs; use deno_runtime::deno_node::is_builtin_node_module; use deno_runtime::deno_node::RealIsBuiltInNodeModuleChecker; use deno_semver::package::PackageReq; use node_resolver::NodeResolutionKind; use node_resolver::ResolutionMode; -use thiserror::Error; use crate::args::NpmCachingStrategy; use crate::args::DENO_DISABLE_PEDANTIC_NODE_WARNINGS; -use crate::node::CliNodeCodeTranslator; +use crate::npm::installer::NpmInstaller; +use crate::npm::installer::PackageCaching; use crate::npm::CliNpmResolver; -use crate::npm::InnerCliNpmResolverRef; use crate::sys::CliSys; use crate::util::sync::AtomicFlag; -use crate::util::text_encoding::from_utf8_lossy_cow; -pub type CjsTracker = deno_resolver::cjs::CjsTracker; -pub type IsCjsResolver = deno_resolver::cjs::IsCjsResolver; +pub type CliCjsTracker = + deno_resolver::cjs::CjsTracker; +pub type CliIsCjsResolver = + deno_resolver::cjs::IsCjsResolver; pub type CliSloppyImportsCachedFs = SloppyImportsCachedFs; pub type CliSloppyImportsResolver = SloppyImportsResolver; pub type CliDenoResolver = deno_resolver::DenoResolver< + DenoInNpmPackageChecker, RealIsBuiltInNodeModuleChecker, + CliNpmResolver, CliSloppyImportsCachedFs, CliSys, >; -pub type CliNpmReqResolver = - deno_resolver::npm::NpmReqResolver; +pub type CliNpmReqResolver = deno_resolver::npm::NpmReqResolver< + DenoInNpmPackageChecker, + RealIsBuiltInNodeModuleChecker, + CliNpmResolver, + CliSys, +>; -pub struct ModuleCodeStringSource { - pub code: ModuleSourceCode, - pub found_url: ModuleSpecifier, - pub media_type: MediaType, -} - -#[derive(Debug, Error, deno_error::JsError)] -#[class(type)] -#[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_tracker: Arc, - fs: Arc, - node_code_translator: Arc, -} - -impl NpmModuleLoader { - pub fn new( - cjs_tracker: Arc, - fs: Arc, - node_code_translator: Arc, - ) -> Self { - Self { - cjs_tracker, - node_code_translator, - fs, - } - } - - pub async fn load( - &self, - specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - ) -> Result { - let file_path = specifier.to_file_path().unwrap(); - let code = self - .fs - .read_file_async(file_path.clone(), None) - .await - .map_err(AnyError::from) - .with_context(|| { - if file_path.is_dir() { - // directory imports are not allowed when importing from an - // ES module, so provide the user with a helpful error message - let dir_path = file_path; - let mut msg = "Directory import ".to_string(); - msg.push_str(&dir_path.to_string_lossy()); - if let Some(referrer) = &maybe_referrer { - msg.push_str(" is not supported resolving import from "); - msg.push_str(referrer.as_str()); - let entrypoint_name = ["index.mjs", "index.js", "index.cjs"] - .iter() - .find(|e| dir_path.join(e).is_file()); - if let Some(entrypoint_name) = entrypoint_name { - msg.push_str("\nDid you mean to import "); - msg.push_str(entrypoint_name); - msg.push_str(" within the directory?"); - } - } - msg - } else { - let mut msg = "Unable to load ".to_string(); - msg.push_str(&file_path.to_string_lossy()); - if let Some(referrer) = &maybe_referrer { - msg.push_str(" imported from "); - msg.push_str(referrer.as_str()); - } - msg - } - })?; - - let 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 = from_utf8_lossy_cow(code); - ModuleSourceCode::String( - self - .node_code_translator - .translate_cjs_to_esm(specifier, Some(code)) - .await? - .into_owned() - .into(), - ) - } else { - // esm and json code is untouched - ModuleSourceCode::Bytes(match code { - Cow::Owned(bytes) => bytes.into_boxed_slice().into(), - Cow::Borrowed(bytes) => bytes.into(), - }) - }; - - Ok(ModuleCodeStringSource { - code, - found_url: specifier.clone(), - media_type: MediaType::from_specifier(specifier), - }) - } -} - -pub struct CliResolverOptions { - pub deno_resolver: Arc, - pub npm_resolver: Option>, - pub bare_node_builtins_enabled: bool, -} +#[derive(Debug, Default)] +pub struct FoundPackageJsonDepFlag(AtomicFlag); /// A resolver that takes care of resolution, taking into account loaded /// import map, JSX settings. #[derive(Debug)] pub struct CliResolver { deno_resolver: Arc, - npm_resolver: Option>, - found_package_json_dep_flag: AtomicFlag, - bare_node_builtins_enabled: bool, + found_package_json_dep_flag: Arc, warned_pkgs: DashSet, } impl CliResolver { - pub fn new(options: CliResolverOptions) -> Self { + pub fn new( + deno_resolver: Arc, + found_package_json_dep_flag: Arc, + ) -> Self { Self { - deno_resolver: options.deno_resolver, - npm_resolver: options.npm_resolver, - found_package_json_dep_flag: Default::default(), - bare_node_builtins_enabled: options.bare_node_builtins_enabled, + deno_resolver, + found_package_json_dep_flag, warned_pkgs: Default::default(), } } - // 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, - npm_caching: NpmCachingStrategy, - ) -> WorkerCliNpmGraphResolver { - WorkerCliNpmGraphResolver { - npm_resolver: self.npm_resolver.as_ref(), - found_package_json_dep_flag: &self.found_package_json_dep_flag, - bare_node_builtins_enabled: self.bare_node_builtins_enabled, - npm_caching, - } - } - pub fn resolve( &self, raw_specifier: &str, @@ -233,7 +103,7 @@ impl CliResolver { if resolution.found_package_json_dep { // mark that we need to do an "npm install" later - self.found_package_json_dep_flag.raise(); + self.found_package_json_dep_flag.0.raise(); } if let Some(diagnostic) = resolution.maybe_diagnostic { @@ -260,15 +130,31 @@ impl CliResolver { } #[derive(Debug)] -pub struct WorkerCliNpmGraphResolver<'a> { - npm_resolver: Option<&'a Arc>, - found_package_json_dep_flag: &'a AtomicFlag, +pub struct CliNpmGraphResolver { + npm_installer: Option>, + found_package_json_dep_flag: Arc, bare_node_builtins_enabled: bool, npm_caching: NpmCachingStrategy, } +impl CliNpmGraphResolver { + pub fn new( + npm_installer: Option>, + found_package_json_dep_flag: Arc, + bare_node_builtins_enabled: bool, + npm_caching: NpmCachingStrategy, + ) -> Self { + Self { + npm_installer, + found_package_json_dep_flag, + bare_node_builtins_enabled, + npm_caching, + } + } +} + #[async_trait(?Send)] -impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> { +impl deno_graph::source::NpmResolver for CliNpmGraphResolver { fn resolve_builtin_node_module( &self, specifier: &ModuleSpecifier, @@ -298,17 +184,12 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> { } fn load_and_cache_npm_package_info(&self, package_name: &str) { - match self.npm_resolver { - Some(npm_resolver) if npm_resolver.as_managed().is_some() => { - let npm_resolver = npm_resolver.clone(); - let package_name = package_name.to_string(); - deno_core::unsync::spawn(async move { - if let Some(managed) = npm_resolver.as_managed() { - let _ignore = managed.cache_package_info(&package_name).await; - } - }); - } - _ => {} + if let Some(npm_installer) = &self.npm_installer { + let npm_installer = npm_installer.clone(); + let package_name = package_name.to_string(); + deno_core::unsync::spawn(async move { + let _ignore = npm_installer.cache_package_info(&package_name).await; + }); } } @@ -316,17 +197,11 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> { &self, package_reqs: &[PackageReq], ) -> NpmResolvePkgReqsResult { - match &self.npm_resolver { - Some(npm_resolver) => { - let npm_resolver = match npm_resolver.as_inner() { - InnerCliNpmResolverRef::Managed(npm_resolver) => npm_resolver, - // if we are using byonm, then this should never be called because - // we don't use deno_graph's npm resolution in this case - InnerCliNpmResolverRef::Byonm(_) => unreachable!(), - }; - - let top_level_result = if self.found_package_json_dep_flag.is_raised() { - npm_resolver + match &self.npm_installer { + Some(npm_installer) => { + let top_level_result = if self.found_package_json_dep_flag.0.is_raised() + { + npm_installer .ensure_top_level_package_json_install() .await .map(|_| ()) @@ -334,15 +209,13 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> { Ok(()) }; - let result = npm_resolver + let result = npm_installer .add_package_reqs_raw( package_reqs, match self.npm_caching { - NpmCachingStrategy::Eager => { - Some(crate::npm::PackageCaching::All) - } + NpmCachingStrategy::Eager => Some(PackageCaching::All), NpmCachingStrategy::Lazy => { - Some(crate::npm::PackageCaching::Only(package_reqs.into())) + Some(PackageCaching::Only(package_reqs.into())) } NpmCachingStrategy::Manual => None, }, diff --git a/cli/rt/Cargo.toml b/cli/rt/Cargo.toml new file mode 100644 index 0000000000..71d9586137 --- /dev/null +++ b/cli/rt/Cargo.toml @@ -0,0 +1,63 @@ +# Copyright 2018-2025 the Deno authors. MIT license. + +[package] +name = "denort" +version = "2.1.7" +authors.workspace = true +default-run = "denort" +edition.workspace = true +license.workspace = true +publish = false +repository.workspace = true +description = "Provides the denort executable" + +[[bin]] +name = "denort" +path = "main.rs" +doc = false + +[[test]] +name = "integration" +path = "integration_tests_runner.rs" +harness = false + +[build-dependencies] +deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting", "only_snapshotted_js_sources"] } +deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] } + +[dependencies] +deno_cache_dir.workspace = true +deno_config.workspace = true +deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] } +deno_error.workspace = true +deno_lib.workspace = true +deno_media_type = { workspace = true, features = ["data_url", "decoding"] } +deno_npm.workspace = true +deno_package_json.workspace = true +deno_path_util.workspace = true +deno_resolver = { workspace = true, features = ["sync"] } +deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] } +deno_semver.workspace = true +deno_snapshots.workspace = true +deno_terminal.workspace = true +libsui = "0.5.0" +node_resolver.workspace = true + +async-trait.workspace = true +bincode = "=1.3.3" +import_map = { version = "=0.21.0", features = ["ext"] } +indexmap.workspace = true +log = { workspace = true, features = ["serde"] } +serde.workspace = true +serde_json.workspace = true +sys_traits = { workspace = true, features = ["getrandom", "filetime", "libc", "real", "strip_unc", "winapi"] } +thiserror.workspace = true +tokio.workspace = true +tokio-util.workspace = true +twox-hash.workspace = true +url.workspace = true + +[dev-dependencies] +pretty_assertions.workspace = true +sys_traits = { workspace = true, features = ["memory"] } +test_util.workspace = true diff --git a/cli/rt/binary.rs b/cli/rt/binary.rs new file mode 100644 index 0000000000..19aad257ca --- /dev/null +++ b/cli/rt/binary.rs @@ -0,0 +1,682 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::OsString; +use std::io::ErrorKind; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; + +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_error::JsError; +use deno_error::JsErrorBox; +use deno_lib::standalone::binary::DenoRtDeserializable; +use deno_lib::standalone::binary::Metadata; +use deno_lib::standalone::binary::RemoteModuleEntry; +use deno_lib::standalone::binary::SpecifierDataStore; +use deno_lib::standalone::binary::SpecifierId; +use deno_lib::standalone::binary::MAGIC_BYTES; +use deno_lib::standalone::virtual_fs::VirtualDirectory; +use deno_lib::standalone::virtual_fs::VirtualDirectoryEntries; +use deno_media_type::MediaType; +use deno_npm::resolution::SerializedNpmResolutionSnapshot; +use deno_npm::resolution::SerializedNpmResolutionSnapshotPackage; +use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; +use deno_npm::NpmPackageId; +use deno_runtime::deno_fs::FileSystem; +use deno_runtime::deno_fs::RealFs; +use deno_runtime::deno_io::fs::FsError; +use deno_semver::package::PackageReq; +use deno_semver::StackString; +use indexmap::IndexMap; +use thiserror::Error; + +use crate::file_system::FileBackedVfs; +use crate::file_system::VfsRoot; + +pub struct StandaloneData { + pub metadata: Metadata, + pub modules: Arc, + pub npm_snapshot: Option, + pub root_path: PathBuf, + pub vfs: Arc, +} + +/// 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, +/// then checking for the magic trailer string `d3n0l4nd`. If found, +/// the bundle is executed. If not, this function exits with `Ok(None)`. +pub fn extract_standalone( + cli_args: Cow>, +) -> Result, AnyError> { + let Some(data) = libsui::find_section("d3n0l4nd") else { + return Ok(None); + }; + + 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 root_url = deno_path_util::url_from_directory_path(&root_path)?; + + let DeserializedDataSection { + mut metadata, + npm_snapshot, + modules_store: remote_modules, + vfs_root_entries, + vfs_files_data, + } = match deserialize_binary_data_section(&root_url, data)? { + Some(data_section) => data_section, + None => return Ok(None), + }; + + let cli_args = cli_args.into_owned(); + 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 = { + let fs_root = VfsRoot { + dir: VirtualDirectory { + // align the name of the directory with the root dir + name: root_path.file_name().unwrap().to_string_lossy().to_string(), + entries: vfs_root_entries, + }, + root_path: root_path.clone(), + start_file_offset: 0, + }; + Arc::new(FileBackedVfs::new( + Cow::Borrowed(vfs_files_data), + fs_root, + metadata.vfs_case_sensitivity, + )) + }; + Ok(Some(StandaloneData { + metadata, + modules: Arc::new(StandaloneModules { + modules: remote_modules, + vfs: vfs.clone(), + }), + npm_snapshot, + root_path, + vfs, + })) +} + +pub struct DeserializedDataSection { + pub metadata: Metadata, + pub npm_snapshot: Option, + pub modules_store: RemoteModulesStore, + pub vfs_root_entries: VirtualDirectoryEntries, + pub vfs_files_data: &'static [u8], +} + +pub fn deserialize_binary_data_section( + root_dir_url: &Url, + data: &'static [u8], +) -> Result, AnyError> { + 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_u64_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_u64_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. Specifiers + let (input, specifiers_store) = + SpecifierStore::deserialize(root_dir_url, input) + .context("deserializing specifiers")?; + // 4. Redirects + let (input, redirects_store) = + SpecifierDataStore::::deserialize(input) + .context("deserializing redirects")?; + // 5. Remote modules + let (input, remote_modules_store) = + SpecifierDataStore::>::deserialize(input) + .context("deserializing remote modules")?; + // 6. VFS + let (input, data) = read_bytes_with_u64_len(input).context("vfs")?; + let vfs_root_entries: VirtualDirectoryEntries = + serde_json::from_slice(data).context("deserializing vfs data")?; + let (input, vfs_files_data) = + read_bytes_with_u64_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."); + } + + let modules_store = RemoteModulesStore::new( + specifiers_store, + redirects_store, + remote_modules_store, + ); + + Ok(Some(DeserializedDataSection { + metadata, + npm_snapshot, + modules_store, + vfs_root_entries, + vfs_files_data, + })) +} + +struct SpecifierStore { + data: IndexMap, SpecifierId>, + reverse: IndexMap>, +} + +impl SpecifierStore { + pub fn deserialize<'a>( + root_dir_url: &Url, + input: &'a [u8], + ) -> std::io::Result<(&'a [u8], Self)> { + let (input, len) = read_u32_as_usize(input)?; + let mut data = IndexMap::with_capacity(len); + let mut reverse = IndexMap::with_capacity(len); + let mut input = input; + for _ in 0..len { + let (new_input, specifier_str) = read_string_lossy(input)?; + let specifier = match Url::parse(&specifier_str) { + Ok(url) => url, + Err(err) => match root_dir_url.join(&specifier_str) { + Ok(url) => url, + Err(_) => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + err, + )); + } + }, + }; + let (new_input, id) = SpecifierId::deserialize(new_input)?; + let specifier = Arc::new(specifier); + data.insert(specifier.clone(), id); + reverse.insert(id, specifier); + input = new_input; + } + Ok((input, Self { data, reverse })) + } + + pub fn get_id(&self, specifier: &Url) -> Option { + self.data.get(specifier).cloned() + } + + pub fn get_specifier(&self, specifier_id: SpecifierId) -> Option<&Url> { + self.reverse.get(&specifier_id).map(|url| url.as_ref()) + } +} + +pub struct StandaloneModules { + modules: RemoteModulesStore, + vfs: Arc, +} + +impl StandaloneModules { + pub fn resolve_specifier<'a>( + &'a self, + specifier: &'a Url, + ) -> Result, TooManyRedirectsError> { + if specifier.scheme() == "file" { + Ok(Some(specifier)) + } else { + self.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 Url, + ) -> Result>, JsErrorBox> { + if specifier.scheme() == "file" { + let path = deno_path_util::url_to_file_path(specifier) + .map_err(JsErrorBox::from_err)?; + let mut transpiled = None; + let mut source_map = None; + let mut cjs_export_analysis = None; + let bytes = match self.vfs.file_entry(&path) { + Ok(entry) => { + let bytes = self + .vfs + .read_file_all(entry) + .map_err(JsErrorBox::from_err)?; + transpiled = entry + .transpiled_offset + .and_then(|t| self.vfs.read_file_offset_with_len(t).ok()); + source_map = entry + .source_map_offset + .and_then(|t| self.vfs.read_file_offset_with_len(t).ok()); + cjs_export_analysis = entry + .cjs_export_analysis_offset + .and_then(|t| self.vfs.read_file_offset_with_len(t).ok()); + bytes + } + Err(err) if err.kind() == ErrorKind::NotFound => { + 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(JsErrorBox::from_err(err)), + } + } + Err(err) => return Err(JsErrorBox::from_err(err)), + }; + Ok(Some(DenoCompileModuleData { + media_type: MediaType::from_specifier(specifier), + specifier, + data: bytes, + transpiled, + source_map, + cjs_export_analysis, + })) + } else { + self.modules.read(specifier).map_err(JsErrorBox::from_err) + } + } +} + +pub struct DenoCompileModuleData<'a> { + pub specifier: &'a Url, + pub media_type: MediaType, + pub data: Cow<'static, [u8]>, + pub transpiled: Option>, + pub source_map: Option>, + pub cjs_export_analysis: Option>, +} + +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 data = self.transpiled.unwrap_or(self.data); + 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(data)), + MediaType::Json => (ModuleType::Json, into_string_unsafe(data)), + MediaType::Wasm => { + (ModuleType::Wasm, DenoCompileModuleSource::Bytes(data)) + } + // just assume javascript if we made it here + MediaType::Css | MediaType::SourceMap | MediaType::Unknown => { + (ModuleType::JavaScript, DenoCompileModuleSource::Bytes(data)) + } + }; + (self.specifier, media_type, source) + } +} + +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), + } + } +} + +#[derive(Debug, Error, JsError)] +#[class(generic)] +#[error("Too many redirects resolving: {0}")] +pub struct TooManyRedirectsError(Url); + +pub struct RemoteModulesStore { + specifiers: SpecifierStore, + redirects: SpecifierDataStore, + remote_modules: SpecifierDataStore>, +} + +impl RemoteModulesStore { + fn new( + specifiers: SpecifierStore, + redirects: SpecifierDataStore, + remote_modules: SpecifierDataStore>, + ) -> Self { + Self { + specifiers, + redirects, + remote_modules, + } + } + + pub fn resolve_specifier<'a>( + &'a self, + specifier: &'a Url, + ) -> Result, TooManyRedirectsError> { + let Some(mut current) = self.specifiers.get_id(specifier) else { + return Ok(None); + }; + let mut count = 0; + loop { + if count > 10 { + return Err(TooManyRedirectsError(specifier.clone())); + } + match self.redirects.get(current) { + Some(to) => { + current = *to; + count += 1; + } + None => { + if count == 0 { + return Ok(Some(specifier)); + } else { + return Ok(self.specifiers.get_specifier(current)); + } + } + } + } + } + + pub fn read<'a>( + &'a self, + original_specifier: &'a Url, + ) -> Result>, TooManyRedirectsError> { + #[allow(clippy::ptr_arg)] + fn handle_cow_ref(data: &Cow<'static, [u8]>) -> Cow<'static, [u8]> { + match data { + Cow::Borrowed(data) => Cow::Borrowed(data), + Cow::Owned(data) => { + // this variant should never happen because the data + // should always be borrowed static in denort + debug_assert!(false); + Cow::Owned(data.clone()) + } + } + } + + let mut count = 0; + let Some(mut specifier) = self.specifiers.get_id(original_specifier) else { + return Ok(None); + }; + loop { + if count > 10 { + return Err(TooManyRedirectsError(original_specifier.clone())); + } + match self.redirects.get(specifier) { + Some(to) => { + specifier = *to; + count += 1; + } + None => { + let Some(entry) = self.remote_modules.get(specifier) else { + return Ok(None); + }; + return Ok(Some(DenoCompileModuleData { + specifier: if count == 0 { + original_specifier + } else { + self.specifiers.get_specifier(specifier).unwrap() + }, + media_type: entry.media_type, + data: handle_cow_ref(&entry.data), + transpiled: entry.maybe_transpiled.as_ref().map(handle_cow_ref), + source_map: entry.maybe_source_map.as_ref().map(handle_cow_ref), + cjs_export_analysis: entry + .maybe_cjs_export_analysis + .as_ref() + .map(handle_cow_ref), + })); + } + } + } + } +} + +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], (StackString, NpmPackageId)), AnyError> + 'a + { + |input| { + let (input, req) = read_string_lossy(input)?; + let (input, id) = read_u32_as_usize(input)?; + let req = StackString::from_cow(req); + Ok((input, (req, 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 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_with_u64_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> { + let (input, len) = read_u64(input)?; + let (input, data) = read_bytes(input, len as usize)?; + Ok((input, data)) +} + +fn read_bytes_with_u32_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> { + let (input, len) = read_u32_as_usize(input)?; + let (input, data) = read_bytes(input, len)?; + Ok((input, data)) +} + +fn read_bytes(input: &[u8], len: usize) -> std::io::Result<(&[u8], &[u8])> { + check_has_len(input, len)?; + let (len_bytes, input) = input.split_at(len); + Ok((input, len_bytes)) +} + +#[inline(always)] +fn check_has_len(input: &[u8], len: usize) -> std::io::Result<()> { + if input.len() < len { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Unexpected end of data", + )) + } else { + Ok(()) + } +} + +fn read_string_lossy(input: &[u8]) -> std::io::Result<(&[u8], Cow)> { + let (input, data_bytes) = read_bytes_with_u32_len(input)?; + Ok((input, String::from_utf8_lossy(data_bytes))) +} + +fn read_u32_as_usize(input: &[u8]) -> std::io::Result<(&[u8], usize)> { + let (input, len_bytes) = read_bytes(input, 4)?; + let len = u32::from_le_bytes(len_bytes.try_into().unwrap()); + Ok((input, len as usize)) +} + +fn read_u64(input: &[u8]) -> std::io::Result<(&[u8], u64)> { + let (input, len_bytes) = read_bytes(input, 8)?; + let len = u64::from_le_bytes(len_bytes.try_into().unwrap()); + Ok((input, len)) +} diff --git a/cli/rt/build.rs b/cli/rt/build.rs new file mode 100644 index 0000000000..486e203dd6 --- /dev/null +++ b/cli/rt/build.rs @@ -0,0 +1,11 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +fn main() { + // Skip building from docs.rs. + if std::env::var_os("DOCS_RS").is_some() { + return; + } + + deno_runtime::deno_napi::print_linker_flags("denort"); + deno_runtime::deno_webgpu::print_linker_flags("denort"); +} diff --git a/cli/standalone/code_cache.rs b/cli/rt/code_cache.rs similarity index 96% rename from cli/standalone/code_cache.rs rename to cli/rt/code_cache.rs index de9ff2a141..c97638abd1 100644 --- a/cli/standalone/code_cache.rs +++ b/cli/rt/code_cache.rs @@ -1,6 +1,5 @@ // Copyright 2018-2025 the Deno authors. MIT license. -use std::collections::BTreeMap; use std::collections::HashMap; use std::io::BufReader; use std::io::BufWriter; @@ -10,17 +9,15 @@ 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::parking_lot::Mutex; use deno_core::unsync::sync::AtomicFlag; +use deno_lib::util::hash::FastInsecureHasher; use deno_path_util::get_atomic_path; use deno_runtime::code_cache::CodeCache; use deno_runtime::code_cache::CodeCacheType; - -use crate::cache::FastInsecureHasher; -use crate::worker::CliCodeCache; +use url::Url; enum CodeCacheStrategy { FirstRun(FirstRunCodeCacheStrategy), @@ -76,12 +73,27 @@ impl DenoCompileCodeCache { } } } + + pub fn for_deno_core(self: Arc) -> Arc { + self.clone() + } + + pub fn enabled(&self) -> bool { + match &self.strategy { + CodeCacheStrategy::FirstRun(strategy) => { + !strategy.is_finished.is_raised() + } + CodeCacheStrategy::SubsequentRun(strategy) => { + !strategy.is_finished.is_raised() + } + } + } } impl CodeCache for DenoCompileCodeCache { fn get_sync( &self, - specifier: &ModuleSpecifier, + specifier: &Url, code_cache_type: CodeCacheType, source_hash: u64, ) -> Option> { @@ -106,7 +118,7 @@ impl CodeCache for DenoCompileCodeCache { fn set_sync( &self, - specifier: ModuleSpecifier, + specifier: Url, code_cache_type: CodeCacheType, source_hash: u64, bytes: &[u8], @@ -152,23 +164,6 @@ impl CodeCache for DenoCompileCodeCache { } } -impl CliCodeCache for DenoCompileCodeCache { - fn enabled(&self) -> bool { - match &self.strategy { - CodeCacheStrategy::FirstRun(strategy) => { - !strategy.is_finished.is_raised() - } - CodeCacheStrategy::SubsequentRun(strategy) => { - !strategy.is_finished.is_raised() - } - } - } - - fn as_code_cache(self: Arc) -> Arc { - self - } -} - type CodeCacheKey = (String, CodeCacheType); struct FirstRunCodeCacheData { @@ -216,7 +211,7 @@ struct SubsequentRunCodeCacheStrategy { impl SubsequentRunCodeCacheStrategy { fn take_from_cache( &self, - specifier: &ModuleSpecifier, + specifier: &Url, code_cache_type: CodeCacheType, source_hash: u64, ) -> Option> { @@ -395,8 +390,6 @@ fn deserialize_with_reader( #[cfg(test)] mod test { - use std::fs::File; - use test_util::TempDir; use super::*; @@ -463,8 +456,8 @@ mod test { fn code_cache() { let temp_dir = TempDir::new(); let file_path = temp_dir.path().join("cache.bin").to_path_buf(); - let url1 = ModuleSpecifier::parse("https://deno.land/example1.js").unwrap(); - let url2 = ModuleSpecifier::parse("https://deno.land/example2.js").unwrap(); + let url1 = Url::parse("https://deno.land/example1.js").unwrap(); + let url2 = Url::parse("https://deno.land/example2.js").unwrap(); // first run { let code_cache = DenoCompileCodeCache::new(file_path.clone(), 1234); diff --git a/cli/rt/file_system.rs b/cli/rt/file_system.rs new file mode 100644 index 0000000000..097674f015 --- /dev/null +++ b/cli/rt/file_system.rs @@ -0,0 +1,1713 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashSet; +use std::io::ErrorKind; +use std::io::SeekFrom; +use std::ops::Range; +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; +use std::time::Duration; +use std::time::SystemTime; + +use deno_core::BufMutView; +use deno_core::BufView; +use deno_core::ResourceHandleFd; +use deno_lib::standalone::virtual_fs::FileSystemCaseSensitivity; +use deno_lib::standalone::virtual_fs::OffsetWithLength; +use deno_lib::standalone::virtual_fs::VfsEntry; +use deno_lib::standalone::virtual_fs::VfsEntryRef; +use deno_lib::standalone::virtual_fs::VirtualDirectory; +use deno_lib::standalone::virtual_fs::VirtualFile; +use deno_lib::sys::DenoLibSys; +use deno_runtime::deno_fs::AccessCheckCb; +use deno_runtime::deno_fs::FileSystem; +use deno_runtime::deno_fs::FsDirEntry; +use deno_runtime::deno_fs::FsFileType; +use deno_runtime::deno_fs::OpenOptions; +use deno_runtime::deno_fs::RealFs; +use deno_runtime::deno_io; +use deno_runtime::deno_io::fs::File as DenoFile; +use deno_runtime::deno_io::fs::FsError; +use deno_runtime::deno_io::fs::FsResult; +use deno_runtime::deno_io::fs::FsStat; +use deno_runtime::deno_node::ExtNodeSys; +use sys_traits::boxed::BoxedFsDirEntry; +use sys_traits::boxed::BoxedFsMetadataValue; +use sys_traits::boxed::FsMetadataBoxed; +use sys_traits::boxed::FsReadDirBoxed; +use sys_traits::FsCopy; +use url::Url; + +#[derive(Debug, Clone)] +pub struct DenoRtSys(Arc); + +impl DenoRtSys { + pub fn new(vfs: Arc) -> Self { + Self(vfs) + } + + pub fn is_specifier_in_vfs(&self, specifier: &Url) -> bool { + deno_path_util::url_to_file_path(specifier) + .map(|p| self.is_in_vfs(&p)) + .unwrap_or(false) + } + + pub fn is_in_vfs(&self, path: &Path) -> bool { + self.0.is_path_within(path) + } + + fn error_if_in_vfs(&self, path: &Path) -> FsResult<()> { + if self.0.is_path_within(path) { + Err(FsError::NotSupported) + } else { + Ok(()) + } + } + + fn copy_to_real_path( + &self, + oldpath: &Path, + newpath: &Path, + ) -> std::io::Result { + let old_file = self.0.file_entry(oldpath)?; + let old_file_bytes = self.0.read_file_all(old_file)?; + let len = old_file_bytes.len() as u64; + RealFs + .write_file_sync( + newpath, + OpenOptions { + read: false, + write: true, + create: true, + truncate: true, + append: false, + create_new: false, + mode: None, + }, + None, + &old_file_bytes, + ) + .map_err(|err| err.into_io_error())?; + Ok(len) + } +} + +#[async_trait::async_trait(?Send)] +impl FileSystem for DenoRtSys { + fn cwd(&self) -> FsResult { + RealFs.cwd() + } + + fn tmp_dir(&self) -> FsResult { + RealFs.tmp_dir() + } + + fn chdir(&self, path: &Path) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.chdir(path) + } + + fn umask(&self, mask: Option) -> FsResult { + RealFs.umask(mask) + } + + fn open_sync( + &self, + path: &Path, + options: OpenOptions, + access_check: Option, + ) -> FsResult> { + if self.0.is_path_within(path) { + Ok(Rc::new(self.0.open_file(path)?)) + } else { + RealFs.open_sync(path, options, access_check) + } + } + async fn open_async<'a>( + &'a self, + path: PathBuf, + options: OpenOptions, + access_check: Option>, + ) -> FsResult> { + if self.0.is_path_within(&path) { + Ok(Rc::new(self.0.open_file(&path)?)) + } else { + RealFs.open_async(path, options, access_check).await + } + } + + fn mkdir_sync( + &self, + path: &Path, + recursive: bool, + mode: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.mkdir_sync(path, recursive, mode) + } + async fn mkdir_async( + &self, + path: PathBuf, + recursive: bool, + mode: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.mkdir_async(path, recursive, mode).await + } + + fn chmod_sync(&self, path: &Path, mode: u32) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.chmod_sync(path, mode) + } + async fn chmod_async(&self, path: PathBuf, mode: u32) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.chmod_async(path, mode).await + } + + fn chown_sync( + &self, + path: &Path, + uid: Option, + gid: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.chown_sync(path, uid, gid) + } + async fn chown_async( + &self, + path: PathBuf, + uid: Option, + gid: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.chown_async(path, uid, gid).await + } + + fn lchown_sync( + &self, + path: &Path, + uid: Option, + gid: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.lchown_sync(path, uid, gid) + } + + async fn lchown_async( + &self, + path: PathBuf, + uid: Option, + gid: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.lchown_async(path, uid, gid).await + } + + fn remove_sync(&self, path: &Path, recursive: bool) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.remove_sync(path, recursive) + } + async fn remove_async(&self, path: PathBuf, recursive: bool) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.remove_async(path, recursive).await + } + + fn copy_file_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { + self.error_if_in_vfs(newpath)?; + if self.0.is_path_within(oldpath) { + self + .copy_to_real_path(oldpath, newpath) + .map(|_| ()) + .map_err(FsError::Io) + } else { + RealFs.copy_file_sync(oldpath, newpath) + } + } + async fn copy_file_async( + &self, + oldpath: PathBuf, + newpath: PathBuf, + ) -> FsResult<()> { + self.error_if_in_vfs(&newpath)?; + if self.0.is_path_within(&oldpath) { + let fs = self.clone(); + tokio::task::spawn_blocking(move || { + fs.copy_to_real_path(&oldpath, &newpath) + .map(|_| ()) + .map_err(FsError::Io) + }) + .await? + } else { + RealFs.copy_file_async(oldpath, newpath).await + } + } + + fn cp_sync(&self, from: &Path, to: &Path) -> FsResult<()> { + self.error_if_in_vfs(to)?; + + RealFs.cp_sync(from, to) + } + async fn cp_async(&self, from: PathBuf, to: PathBuf) -> FsResult<()> { + self.error_if_in_vfs(&to)?; + + RealFs.cp_async(from, to).await + } + + fn stat_sync(&self, path: &Path) -> FsResult { + if self.0.is_path_within(path) { + Ok(self.0.stat(path)?.as_fs_stat()) + } else { + RealFs.stat_sync(path) + } + } + async fn stat_async(&self, path: PathBuf) -> FsResult { + if self.0.is_path_within(&path) { + Ok(self.0.stat(&path)?.as_fs_stat()) + } else { + RealFs.stat_async(path).await + } + } + + fn lstat_sync(&self, path: &Path) -> FsResult { + if self.0.is_path_within(path) { + Ok(self.0.lstat(path)?.as_fs_stat()) + } else { + RealFs.lstat_sync(path) + } + } + async fn lstat_async(&self, path: PathBuf) -> FsResult { + if self.0.is_path_within(&path) { + Ok(self.0.lstat(&path)?.as_fs_stat()) + } else { + RealFs.lstat_async(path).await + } + } + + fn realpath_sync(&self, path: &Path) -> FsResult { + if self.0.is_path_within(path) { + Ok(self.0.canonicalize(path)?) + } else { + RealFs.realpath_sync(path) + } + } + async fn realpath_async(&self, path: PathBuf) -> FsResult { + if self.0.is_path_within(&path) { + Ok(self.0.canonicalize(&path)?) + } else { + RealFs.realpath_async(path).await + } + } + + fn read_dir_sync(&self, path: &Path) -> FsResult> { + if self.0.is_path_within(path) { + Ok(self.0.read_dir(path)?) + } else { + RealFs.read_dir_sync(path) + } + } + async fn read_dir_async(&self, path: PathBuf) -> FsResult> { + if self.0.is_path_within(&path) { + Ok(self.0.read_dir(&path)?) + } else { + RealFs.read_dir_async(path).await + } + } + + fn rename_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { + self.error_if_in_vfs(oldpath)?; + self.error_if_in_vfs(newpath)?; + RealFs.rename_sync(oldpath, newpath) + } + async fn rename_async( + &self, + oldpath: PathBuf, + newpath: PathBuf, + ) -> FsResult<()> { + self.error_if_in_vfs(&oldpath)?; + self.error_if_in_vfs(&newpath)?; + RealFs.rename_async(oldpath, newpath).await + } + + fn link_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { + self.error_if_in_vfs(oldpath)?; + self.error_if_in_vfs(newpath)?; + RealFs.link_sync(oldpath, newpath) + } + async fn link_async( + &self, + oldpath: PathBuf, + newpath: PathBuf, + ) -> FsResult<()> { + self.error_if_in_vfs(&oldpath)?; + self.error_if_in_vfs(&newpath)?; + RealFs.link_async(oldpath, newpath).await + } + + fn symlink_sync( + &self, + oldpath: &Path, + newpath: &Path, + file_type: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(oldpath)?; + self.error_if_in_vfs(newpath)?; + RealFs.symlink_sync(oldpath, newpath, file_type) + } + async fn symlink_async( + &self, + oldpath: PathBuf, + newpath: PathBuf, + file_type: Option, + ) -> FsResult<()> { + self.error_if_in_vfs(&oldpath)?; + self.error_if_in_vfs(&newpath)?; + RealFs.symlink_async(oldpath, newpath, file_type).await + } + + fn read_link_sync(&self, path: &Path) -> FsResult { + if self.0.is_path_within(path) { + Ok(self.0.read_link(path)?) + } else { + RealFs.read_link_sync(path) + } + } + async fn read_link_async(&self, path: PathBuf) -> FsResult { + if self.0.is_path_within(&path) { + Ok(self.0.read_link(&path)?) + } else { + RealFs.read_link_async(path).await + } + } + + fn truncate_sync(&self, path: &Path, len: u64) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.truncate_sync(path, len) + } + async fn truncate_async(&self, path: PathBuf, len: u64) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.truncate_async(path, len).await + } + + fn utime_sync( + &self, + path: &Path, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.utime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + } + async fn utime_async( + &self, + path: PathBuf, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs + .utime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + .await + } + + fn lutime_sync( + &self, + path: &Path, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.lutime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + } + async fn lutime_async( + &self, + path: PathBuf, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs + .lutime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + .await + } +} + +impl ExtNodeSys for DenoRtSys {} +impl DenoLibSys for DenoRtSys {} + +impl sys_traits::BaseFsHardLink for DenoRtSys { + #[inline] + fn base_fs_hard_link(&self, src: &Path, dst: &Path) -> std::io::Result<()> { + self.link_sync(src, dst).map_err(|err| err.into_io_error()) + } +} + +impl sys_traits::BaseFsRead for DenoRtSys { + #[inline] + fn base_fs_read(&self, path: &Path) -> std::io::Result> { + self + .read_file_sync(path, None) + .map_err(|err| err.into_io_error()) + } +} + +impl sys_traits::FsMetadataValue for FileBackedVfsMetadata { + fn file_type(&self) -> sys_traits::FileType { + self.file_type + } + + fn len(&self) -> u64 { + self.len + } + + fn accessed(&self) -> std::io::Result { + Err(not_supported("accessed time")) + } + + fn created(&self) -> std::io::Result { + Err(not_supported("created time")) + } + + fn changed(&self) -> std::io::Result { + Err(not_supported("changed time")) + } + + fn modified(&self) -> std::io::Result { + Err(not_supported("modified time")) + } + + fn dev(&self) -> std::io::Result { + Ok(0) + } + + fn ino(&self) -> std::io::Result { + Ok(0) + } + + fn mode(&self) -> std::io::Result { + Ok(0) + } + + fn nlink(&self) -> std::io::Result { + Ok(0) + } + + fn uid(&self) -> std::io::Result { + Ok(0) + } + + fn gid(&self) -> std::io::Result { + Ok(0) + } + + fn rdev(&self) -> std::io::Result { + Ok(0) + } + + fn blksize(&self) -> std::io::Result { + Ok(0) + } + + fn blocks(&self) -> std::io::Result { + Ok(0) + } + + fn is_block_device(&self) -> std::io::Result { + Ok(false) + } + + fn is_char_device(&self) -> std::io::Result { + Ok(false) + } + + fn is_fifo(&self) -> std::io::Result { + Ok(false) + } + + fn is_socket(&self) -> std::io::Result { + Ok(false) + } + + fn file_attributes(&self) -> std::io::Result { + Ok(0) + } +} + +fn not_supported(name: &str) -> std::io::Error { + std::io::Error::new( + ErrorKind::Unsupported, + format!( + "{} is not supported for an embedded deno compile file", + name + ), + ) +} + +impl sys_traits::FsDirEntry for FileBackedVfsDirEntry { + type Metadata = BoxedFsMetadataValue; + + fn file_name(&self) -> Cow { + Cow::Borrowed(self.metadata.name.as_ref()) + } + + fn file_type(&self) -> std::io::Result { + Ok(self.metadata.file_type) + } + + fn metadata(&self) -> std::io::Result { + Ok(BoxedFsMetadataValue(Box::new(self.metadata.clone()))) + } + + fn path(&self) -> Cow { + Cow::Owned(self.parent_path.join(&self.metadata.name)) + } +} + +impl sys_traits::BaseFsReadDir for DenoRtSys { + type ReadDirEntry = BoxedFsDirEntry; + + fn base_fs_read_dir( + &self, + path: &Path, + ) -> std::io::Result< + Box> + '_>, + > { + if self.0.is_path_within(path) { + let entries = self.0.read_dir_with_metadata(path)?; + Ok(Box::new( + entries.map(|entry| Ok(BoxedFsDirEntry::new(entry))), + )) + } else { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.fs_read_dir_boxed(path) + } + } +} + +impl sys_traits::BaseFsCanonicalize for DenoRtSys { + #[inline] + fn base_fs_canonicalize(&self, path: &Path) -> std::io::Result { + self.realpath_sync(path).map_err(|err| err.into_io_error()) + } +} + +impl sys_traits::BaseFsMetadata for DenoRtSys { + type Metadata = BoxedFsMetadataValue; + + #[inline] + fn base_fs_metadata(&self, path: &Path) -> std::io::Result { + if self.0.is_path_within(path) { + Ok(BoxedFsMetadataValue::new(self.0.stat(path)?)) + } else { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.fs_metadata_boxed(path) + } + } + + #[inline] + fn base_fs_symlink_metadata( + &self, + path: &Path, + ) -> std::io::Result { + if self.0.is_path_within(path) { + Ok(BoxedFsMetadataValue::new(self.0.lstat(path)?)) + } else { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.fs_symlink_metadata_boxed(path) + } + } +} + +impl sys_traits::BaseFsCopy for DenoRtSys { + #[inline] + fn base_fs_copy(&self, from: &Path, to: &Path) -> std::io::Result { + self + .error_if_in_vfs(to) + .map_err(|err| err.into_io_error())?; + if self.0.is_path_within(from) { + self.copy_to_real_path(from, to) + } else { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.fs_copy(from, to) + } + } +} + +impl sys_traits::BaseFsCloneFile for DenoRtSys { + fn base_fs_clone_file( + &self, + _from: &Path, + _to: &Path, + ) -> std::io::Result<()> { + // will cause a fallback in the code that uses this + Err(not_supported("cloning files")) + } +} + +impl sys_traits::BaseFsCreateDir for DenoRtSys { + #[inline] + fn base_fs_create_dir( + &self, + path: &Path, + options: &sys_traits::CreateDirOptions, + ) -> std::io::Result<()> { + self + .mkdir_sync(path, options.recursive, options.mode) + .map_err(|err| err.into_io_error()) + } +} + +impl sys_traits::BaseFsRemoveFile for DenoRtSys { + #[inline] + fn base_fs_remove_file(&self, path: &Path) -> std::io::Result<()> { + self + .remove_sync(path, false) + .map_err(|err| err.into_io_error()) + } +} + +impl sys_traits::BaseFsRename for DenoRtSys { + #[inline] + fn base_fs_rename(&self, from: &Path, to: &Path) -> std::io::Result<()> { + self + .rename_sync(from, to) + .map_err(|err| err.into_io_error()) + } +} + +pub enum FsFileAdapter { + Real(sys_traits::impls::RealFsFile), + Vfs(FileBackedVfsFile), +} + +impl sys_traits::FsFile for FsFileAdapter {} + +impl sys_traits::FsFileAsRaw for FsFileAdapter { + #[cfg(windows)] + fn fs_file_as_raw_handle(&self) -> Option { + match self { + Self::Real(file) => file.fs_file_as_raw_handle(), + Self::Vfs(_) => None, + } + } + + #[cfg(unix)] + fn fs_file_as_raw_fd(&self) -> Option { + match self { + Self::Real(file) => file.fs_file_as_raw_fd(), + Self::Vfs(_) => None, + } + } +} + +impl sys_traits::FsFileSyncData for FsFileAdapter { + fn fs_file_sync_data(&mut self) -> std::io::Result<()> { + match self { + Self::Real(file) => file.fs_file_sync_data(), + Self::Vfs(_) => Ok(()), + } + } +} + +impl sys_traits::FsFileSyncAll for FsFileAdapter { + fn fs_file_sync_all(&mut self) -> std::io::Result<()> { + match self { + Self::Real(file) => file.fs_file_sync_all(), + Self::Vfs(_) => Ok(()), + } + } +} + +impl sys_traits::FsFileSetPermissions for FsFileAdapter { + #[inline] + fn fs_file_set_permissions(&mut self, mode: u32) -> std::io::Result<()> { + match self { + Self::Real(file) => file.fs_file_set_permissions(mode), + Self::Vfs(_) => Ok(()), + } + } +} + +impl std::io::Read for FsFileAdapter { + #[inline] + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + Self::Real(file) => file.read(buf), + Self::Vfs(file) => file.read_to_buf(buf), + } + } +} + +impl std::io::Seek for FsFileAdapter { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + match self { + Self::Real(file) => file.seek(pos), + Self::Vfs(file) => file.seek(pos), + } + } +} + +impl std::io::Write for FsFileAdapter { + #[inline] + fn write(&mut self, buf: &[u8]) -> std::io::Result { + match self { + Self::Real(file) => file.write(buf), + Self::Vfs(_) => Err(not_supported("writing files")), + } + } + + #[inline] + fn flush(&mut self) -> std::io::Result<()> { + match self { + Self::Real(file) => file.flush(), + Self::Vfs(_) => Err(not_supported("writing files")), + } + } +} + +impl sys_traits::FsFileSetLen for FsFileAdapter { + #[inline] + fn fs_file_set_len(&mut self, len: u64) -> std::io::Result<()> { + match self { + Self::Real(file) => file.fs_file_set_len(len), + Self::Vfs(_) => Err(not_supported("setting file length")), + } + } +} + +impl sys_traits::FsFileSetTimes for FsFileAdapter { + fn fs_file_set_times( + &mut self, + times: sys_traits::FsFileTimes, + ) -> std::io::Result<()> { + match self { + Self::Real(file) => file.fs_file_set_times(times), + Self::Vfs(_) => Err(not_supported("setting file times")), + } + } +} + +impl sys_traits::FsFileLock for FsFileAdapter { + fn fs_file_lock( + &mut self, + mode: sys_traits::FsFileLockMode, + ) -> std::io::Result<()> { + match self { + Self::Real(file) => file.fs_file_lock(mode), + Self::Vfs(_) => Err(not_supported("locking files")), + } + } + + fn fs_file_try_lock( + &mut self, + mode: sys_traits::FsFileLockMode, + ) -> std::io::Result<()> { + match self { + Self::Real(file) => file.fs_file_try_lock(mode), + Self::Vfs(_) => Err(not_supported("locking files")), + } + } + + fn fs_file_unlock(&mut self) -> std::io::Result<()> { + match self { + Self::Real(file) => file.fs_file_unlock(), + Self::Vfs(_) => Err(not_supported("unlocking files")), + } + } +} + +impl sys_traits::FsFileIsTerminal for FsFileAdapter { + #[inline] + fn fs_file_is_terminal(&self) -> bool { + match self { + Self::Real(file) => file.fs_file_is_terminal(), + Self::Vfs(_) => false, + } + } +} + +impl sys_traits::BaseFsOpen for DenoRtSys { + type File = FsFileAdapter; + + fn base_fs_open( + &self, + path: &Path, + options: &sys_traits::OpenOptions, + ) -> std::io::Result { + if self.0.is_path_within(path) { + Ok(FsFileAdapter::Vfs(self.0.open_file(path)?)) + } else { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + Ok(FsFileAdapter::Real( + sys_traits::impls::RealSys.base_fs_open(path, options)?, + )) + } + } +} + +impl sys_traits::BaseFsSymlinkDir for DenoRtSys { + fn base_fs_symlink_dir(&self, src: &Path, dst: &Path) -> std::io::Result<()> { + self + .symlink_sync(src, dst, Some(FsFileType::Directory)) + .map_err(|err| err.into_io_error()) + } +} + +impl sys_traits::SystemRandom for DenoRtSys { + #[inline] + fn sys_random(&self, buf: &mut [u8]) -> std::io::Result<()> { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.sys_random(buf) + } +} + +impl sys_traits::SystemTimeNow for DenoRtSys { + #[inline] + fn sys_time_now(&self) -> SystemTime { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.sys_time_now() + } +} + +impl sys_traits::ThreadSleep for DenoRtSys { + #[inline] + fn thread_sleep(&self, dur: Duration) { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.thread_sleep(dur) + } +} + +impl sys_traits::EnvCurrentDir for DenoRtSys { + fn env_current_dir(&self) -> std::io::Result { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.env_current_dir() + } +} + +impl sys_traits::BaseEnvVar for DenoRtSys { + fn base_env_var_os( + &self, + key: &std::ffi::OsStr, + ) -> Option { + #[allow(clippy::disallowed_types)] // ok because we're implementing the fs + sys_traits::impls::RealSys.base_env_var_os(key) + } +} + +#[derive(Debug)] +pub struct VfsRoot { + pub dir: VirtualDirectory, + pub root_path: PathBuf, + pub start_file_offset: u64, +} + +impl VfsRoot { + fn find_entry<'a>( + &'a self, + path: &Path, + case_sensitivity: FileSystemCaseSensitivity, + ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { + self.find_entry_inner(path, &mut HashSet::new(), case_sensitivity) + } + + fn find_entry_inner<'a>( + &'a self, + path: &Path, + seen: &mut HashSet, + case_sensitivity: FileSystemCaseSensitivity, + ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { + let mut path = Cow::Borrowed(path); + loop { + let (resolved_path, entry) = + self.find_entry_no_follow_inner(&path, seen, case_sensitivity)?; + match entry { + VfsEntryRef::Symlink(symlink) => { + if !seen.insert(path.to_path_buf()) { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "circular symlinks", + )); + } + path = Cow::Owned(symlink.resolve_dest_from_root(&self.root_path)); + } + _ => { + return Ok((resolved_path, entry)); + } + } + } + } + + fn find_entry_no_follow( + &self, + path: &Path, + case_sensitivity: FileSystemCaseSensitivity, + ) -> std::io::Result<(PathBuf, VfsEntryRef)> { + self.find_entry_no_follow_inner(path, &mut HashSet::new(), case_sensitivity) + } + + fn find_entry_no_follow_inner<'a>( + &'a self, + path: &Path, + seen: &mut HashSet, + case_sensitivity: FileSystemCaseSensitivity, + ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { + let relative_path = match path.strip_prefix(&self.root_path) { + Ok(p) => p, + Err(_) => { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "path not found", + )); + } + }; + let mut final_path = self.root_path.clone(); + let mut current_entry = VfsEntryRef::Dir(&self.dir); + for component in relative_path.components() { + let component = component.as_os_str(); + let current_dir = match current_entry { + VfsEntryRef::Dir(dir) => { + final_path.push(component); + dir + } + VfsEntryRef::Symlink(symlink) => { + let dest = symlink.resolve_dest_from_root(&self.root_path); + let (resolved_path, entry) = + self.find_entry_inner(&dest, seen, case_sensitivity)?; + final_path = resolved_path; // overwrite with the new resolved path + match entry { + VfsEntryRef::Dir(dir) => { + final_path.push(component); + dir + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "path not found", + )); + } + } + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "path not found", + )); + } + }; + let component = component.to_string_lossy(); + current_entry = current_dir + .entries + .get_by_name(&component, case_sensitivity) + .ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "path not found") + })? + .as_ref(); + } + + Ok((final_path, current_entry)) + } +} + +pub struct FileBackedVfsFile { + file: VirtualFile, + pos: RefCell, + vfs: Arc, +} + +impl FileBackedVfsFile { + pub fn seek(&self, pos: SeekFrom) -> std::io::Result { + match pos { + SeekFrom::Start(pos) => { + *self.pos.borrow_mut() = pos; + Ok(pos) + } + SeekFrom::End(offset) => { + if offset < 0 && -offset as u64 > self.file.offset.len { + let msg = "An attempt was made to move the file pointer before the beginning of the file."; + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + msg, + )) + } else { + let mut current_pos = self.pos.borrow_mut(); + *current_pos = if offset >= 0 { + self.file.offset.len - (offset as u64) + } else { + self.file.offset.len + (-offset as u64) + }; + Ok(*current_pos) + } + } + SeekFrom::Current(offset) => { + let mut current_pos = self.pos.borrow_mut(); + if offset >= 0 { + *current_pos += offset as u64; + } else if -offset as u64 > *current_pos { + return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "An attempt was made to move the file pointer before the beginning of the file.")); + } else { + *current_pos -= -offset as u64; + } + Ok(*current_pos) + } + } + } + + pub fn read_to_buf(&self, buf: &mut [u8]) -> std::io::Result { + let read_pos = { + let mut pos = self.pos.borrow_mut(); + let read_pos = *pos; + // advance the position due to the read + *pos = std::cmp::min(self.file.offset.len, *pos + buf.len() as u64); + read_pos + }; + self.vfs.read_file(&self.file, read_pos, buf) + } + + fn read_to_end(&self) -> FsResult> { + let read_pos = { + let mut pos = self.pos.borrow_mut(); + let read_pos = *pos; + // todo(dsherret): should this always set it to the end of the file? + if *pos < self.file.offset.len { + // advance the position due to the read + *pos = self.file.offset.len; + } + read_pos + }; + if read_pos > self.file.offset.len { + return Ok(Cow::Borrowed(&[])); + } + if read_pos == 0 { + Ok(self.vfs.read_file_all(&self.file)?) + } else { + let size = (self.file.offset.len - read_pos) as usize; + let mut buf = vec![0; size]; + self.vfs.read_file(&self.file, read_pos, &mut buf)?; + Ok(Cow::Owned(buf)) + } + } +} + +#[async_trait::async_trait(?Send)] +impl deno_io::fs::File for FileBackedVfsFile { + fn read_sync(self: Rc, buf: &mut [u8]) -> FsResult { + self.read_to_buf(buf).map_err(Into::into) + } + async fn read_byob( + self: Rc, + mut buf: BufMutView, + ) -> FsResult<(usize, BufMutView)> { + // this is fast, no need to spawn a task + let nread = self.read_to_buf(&mut buf)?; + Ok((nread, buf)) + } + + fn write_sync(self: Rc, _buf: &[u8]) -> FsResult { + Err(FsError::NotSupported) + } + async fn write( + self: Rc, + _buf: BufView, + ) -> FsResult { + Err(FsError::NotSupported) + } + + fn write_all_sync(self: Rc, _buf: &[u8]) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn write_all(self: Rc, _buf: BufView) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn read_all_sync(self: Rc) -> FsResult> { + self.read_to_end() + } + async fn read_all_async(self: Rc) -> FsResult> { + // this is fast, no need to spawn a task + self.read_to_end() + } + + fn chmod_sync(self: Rc, _pathmode: u32) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn chmod_async(self: Rc, _mode: u32) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn seek_sync(self: Rc, pos: SeekFrom) -> FsResult { + self.seek(pos).map_err(|err| err.into()) + } + async fn seek_async(self: Rc, pos: SeekFrom) -> FsResult { + self.seek(pos).map_err(|err| err.into()) + } + + fn datasync_sync(self: Rc) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn datasync_async(self: Rc) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn sync_sync(self: Rc) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn sync_async(self: Rc) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn stat_sync(self: Rc) -> FsResult { + Err(FsError::NotSupported) + } + async fn stat_async(self: Rc) -> FsResult { + Err(FsError::NotSupported) + } + + fn lock_sync(self: Rc, _exclusive: bool) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn lock_async(self: Rc, _exclusive: bool) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn unlock_sync(self: Rc) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn unlock_async(self: Rc) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn truncate_sync(self: Rc, _len: u64) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn truncate_async(self: Rc, _len: u64) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn utime_sync( + self: Rc, + _atime_secs: i64, + _atime_nanos: u32, + _mtime_secs: i64, + _mtime_nanos: u32, + ) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn utime_async( + self: Rc, + _atime_secs: i64, + _atime_nanos: u32, + _mtime_secs: i64, + _mtime_nanos: u32, + ) -> FsResult<()> { + Err(FsError::NotSupported) + } + + // lower level functionality + fn as_stdio(self: Rc) -> FsResult { + Err(FsError::NotSupported) + } + fn backing_fd(self: Rc) -> Option { + None + } + fn try_clone_inner(self: Rc) -> FsResult> { + Ok(self) + } +} + +#[derive(Debug, Clone)] +pub struct FileBackedVfsDirEntry { + pub parent_path: PathBuf, + pub metadata: FileBackedVfsMetadata, +} + +#[derive(Debug, Clone)] +pub struct FileBackedVfsMetadata { + pub name: String, + pub file_type: sys_traits::FileType, + pub len: u64, +} + +impl FileBackedVfsMetadata { + pub fn from_vfs_entry_ref(vfs_entry: VfsEntryRef) -> Self { + FileBackedVfsMetadata { + file_type: match vfs_entry { + VfsEntryRef::Dir(_) => sys_traits::FileType::Dir, + VfsEntryRef::File(_) => sys_traits::FileType::File, + VfsEntryRef::Symlink(_) => sys_traits::FileType::Symlink, + }, + name: vfs_entry.name().to_string(), + len: match vfs_entry { + VfsEntryRef::Dir(_) => 0, + VfsEntryRef::File(file) => file.offset.len, + VfsEntryRef::Symlink(_) => 0, + }, + } + } + pub fn as_fs_stat(&self) -> FsStat { + FsStat { + is_directory: self.file_type == sys_traits::FileType::Dir, + is_file: self.file_type == sys_traits::FileType::File, + is_symlink: self.file_type == sys_traits::FileType::Symlink, + atime: None, + birthtime: None, + mtime: None, + ctime: None, + blksize: 0, + size: self.len, + dev: 0, + ino: 0, + mode: 0, + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + blocks: 0, + is_block_device: false, + is_char_device: false, + is_fifo: false, + is_socket: false, + } + } +} + +#[derive(Debug)] +pub struct FileBackedVfs { + vfs_data: Cow<'static, [u8]>, + fs_root: VfsRoot, + case_sensitivity: FileSystemCaseSensitivity, +} + +impl FileBackedVfs { + pub fn new( + data: Cow<'static, [u8]>, + fs_root: VfsRoot, + case_sensitivity: FileSystemCaseSensitivity, + ) -> Self { + Self { + vfs_data: data, + fs_root, + case_sensitivity, + } + } + + pub fn root(&self) -> &Path { + &self.fs_root.root_path + } + + pub fn is_path_within(&self, path: &Path) -> bool { + path.starts_with(&self.fs_root.root_path) + } + + pub fn open_file( + self: &Arc, + path: &Path, + ) -> std::io::Result { + let file = self.file_entry(path)?; + Ok(FileBackedVfsFile { + file: file.clone(), + vfs: self.clone(), + pos: Default::default(), + }) + } + + pub fn read_dir(&self, path: &Path) -> std::io::Result> { + let dir = self.dir_entry(path)?; + Ok( + dir + .entries + .iter() + .map(|entry| FsDirEntry { + name: entry.name().to_string(), + is_file: matches!(entry, VfsEntry::File(_)), + is_directory: matches!(entry, VfsEntry::Dir(_)), + is_symlink: matches!(entry, VfsEntry::Symlink(_)), + }) + .collect(), + ) + } + + pub fn read_dir_with_metadata<'a>( + &'a self, + path: &Path, + ) -> std::io::Result + 'a> { + let dir = self.dir_entry(path)?; + let path = path.to_path_buf(); + Ok(dir.entries.iter().map(move |entry| FileBackedVfsDirEntry { + parent_path: path.to_path_buf(), + metadata: FileBackedVfsMetadata::from_vfs_entry_ref(entry.as_ref()), + })) + } + + pub fn read_link(&self, path: &Path) -> std::io::Result { + let (_, entry) = self + .fs_root + .find_entry_no_follow(path, self.case_sensitivity)?; + match entry { + VfsEntryRef::Symlink(symlink) => { + Ok(symlink.resolve_dest_from_root(&self.fs_root.root_path)) + } + VfsEntryRef::Dir(_) | VfsEntryRef::File(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "not a symlink", + )), + } + } + + pub fn lstat(&self, path: &Path) -> std::io::Result { + let (_, entry) = self + .fs_root + .find_entry_no_follow(path, self.case_sensitivity)?; + Ok(FileBackedVfsMetadata::from_vfs_entry_ref(entry)) + } + + pub fn stat(&self, path: &Path) -> std::io::Result { + let (_, entry) = self.fs_root.find_entry(path, self.case_sensitivity)?; + Ok(FileBackedVfsMetadata::from_vfs_entry_ref(entry)) + } + + pub fn canonicalize(&self, path: &Path) -> std::io::Result { + let (path, _) = self.fs_root.find_entry(path, self.case_sensitivity)?; + Ok(path) + } + + pub fn read_file_all( + &self, + file: &VirtualFile, + ) -> std::io::Result> { + self.read_file_offset_with_len(file.offset) + } + + pub fn read_file_offset_with_len( + &self, + offset_with_len: OffsetWithLength, + ) -> std::io::Result> { + let read_range = + self.get_read_range(offset_with_len, 0, offset_with_len.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( + &self, + file: &VirtualFile, + pos: u64, + buf: &mut [u8], + ) -> std::io::Result { + let read_range = self.get_read_range(file.offset, pos, buf.len() as u64)?; + let read_len = read_range.len(); + buf[..read_len].copy_from_slice(&self.vfs_data[read_range]); + Ok(read_len) + } + + fn get_read_range( + &self, + file_offset_and_len: OffsetWithLength, + pos: u64, + len: u64, + ) -> std::io::Result> { + if pos > file_offset_and_len.len { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "unexpected EOF", + )); + } + let file_offset = + self.fs_root.start_file_offset + file_offset_and_len.offset; + let start = file_offset + pos; + let end = file_offset + std::cmp::min(pos + len, file_offset_and_len.len); + Ok(start as usize..end as usize) + } + + pub fn dir_entry(&self, path: &Path) -> std::io::Result<&VirtualDirectory> { + let (_, entry) = self.fs_root.find_entry(path, self.case_sensitivity)?; + match entry { + VfsEntryRef::Dir(dir) => Ok(dir), + VfsEntryRef::Symlink(_) => unreachable!(), + VfsEntryRef::File(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "path is a file", + )), + } + } + + pub fn file_entry(&self, path: &Path) -> std::io::Result<&VirtualFile> { + let (_, entry) = self.fs_root.find_entry(path, self.case_sensitivity)?; + match entry { + VfsEntryRef::Dir(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "path is a directory", + )), + VfsEntryRef::Symlink(_) => unreachable!(), + VfsEntryRef::File(file) => Ok(file), + } + } +} + +#[cfg(test)] +mod test { + use std::io::Write; + + use deno_lib::standalone::virtual_fs::VfsBuilder; + use test_util::assert_contains; + use test_util::TempDir; + + use super::*; + + #[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().into_owned()).unwrap() + } + + #[test] + fn builds_and_uses_virtual_fs() { + let temp_dir = TempDir::new(); + // we canonicalize the temp directory because the vfs builder + // will canonicalize the root path + let src_path = temp_dir.path().canonicalize().join("src"); + src_path.create_dir_all(); + src_path.join("sub_dir").create_dir_all(); + src_path.join("e.txt").write("e"); + src_path.symlink_file("e.txt", "sub_dir/e.txt"); + let src_path = src_path.to_path_buf(); + let mut builder = VfsBuilder::new(); + builder + .add_file_with_data_raw(&src_path.join("a.txt"), "data".into()) + .unwrap(); + builder + .add_file_with_data_raw(&src_path.join("b.txt"), "data".into()) + .unwrap(); + assert_eq!(builder.files_len(), 1); // because duplicate data + builder + .add_file_with_data_raw(&src_path.join("c.txt"), "c".into()) + .unwrap(); + builder + .add_file_with_data_raw( + &src_path.join("sub_dir").join("d.txt"), + "d".into(), + ) + .unwrap(); + builder.add_file_at_path(&src_path.join("e.txt")).unwrap(); + builder + .add_symlink(&src_path.join("sub_dir").join("e.txt")) + .unwrap(); + + // get the virtual fs + let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); + + assert_eq!(read_file(&virtual_fs, &dest_path.join("a.txt")), "data"); + assert_eq!(read_file(&virtual_fs, &dest_path.join("b.txt")), "data"); + + // attempt reading a symlink + assert_eq!( + read_file(&virtual_fs, &dest_path.join("sub_dir").join("e.txt")), + "e", + ); + + // canonicalize symlink + assert_eq!( + virtual_fs + .canonicalize(&dest_path.join("sub_dir").join("e.txt")) + .unwrap(), + dest_path.join("e.txt"), + ); + + // metadata + assert_eq!( + virtual_fs + .lstat(&dest_path.join("sub_dir").join("e.txt")) + .unwrap() + .file_type, + sys_traits::FileType::Symlink, + ); + assert_eq!( + virtual_fs + .stat(&dest_path.join("sub_dir").join("e.txt")) + .unwrap() + .file_type, + sys_traits::FileType::File, + ); + assert_eq!( + virtual_fs + .stat(&dest_path.join("sub_dir")) + .unwrap() + .file_type, + sys_traits::FileType::Dir, + ); + assert_eq!( + virtual_fs.stat(&dest_path.join("e.txt")).unwrap().file_type, + sys_traits::FileType::File + ); + } + + #[test] + fn test_include_dir_recursive() { + let temp_dir = TempDir::new(); + let temp_dir_path = temp_dir.path().canonicalize(); + temp_dir.create_dir_all("src/nested/sub_dir"); + temp_dir.write("src/a.txt", "data"); + temp_dir.write("src/b.txt", "data"); + temp_dir.path().symlink_dir( + temp_dir_path.join("src/nested/sub_dir"), + temp_dir_path.join("src/sub_dir_link"), + ); + temp_dir.write("src/nested/sub_dir/c.txt", "c"); + + // build and create the virtual fs + let src_path = temp_dir_path.join("src").to_path_buf(); + let mut builder = VfsBuilder::new(); + builder.add_dir_recursive(&src_path).unwrap(); + let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); + + assert_eq!(read_file(&virtual_fs, &dest_path.join("a.txt")), "data",); + assert_eq!(read_file(&virtual_fs, &dest_path.join("b.txt")), "data",); + + assert_eq!( + read_file( + &virtual_fs, + &dest_path.join("nested").join("sub_dir").join("c.txt") + ), + "c", + ); + assert_eq!( + read_file(&virtual_fs, &dest_path.join("sub_dir_link").join("c.txt")), + "c", + ); + assert_eq!( + virtual_fs + .lstat(&dest_path.join("sub_dir_link")) + .unwrap() + .file_type, + sys_traits::FileType::Symlink, + ); + + assert_eq!( + virtual_fs + .canonicalize(&dest_path.join("sub_dir_link").join("c.txt")) + .unwrap(), + dest_path.join("nested").join("sub_dir").join("c.txt"), + ); + } + + fn into_virtual_fs( + builder: VfsBuilder, + temp_dir: &TempDir, + ) -> (PathBuf, FileBackedVfs) { + let virtual_fs_file = temp_dir.path().join("virtual_fs"); + let vfs = builder.build(); + { + let mut file = std::fs::File::create(&virtual_fs_file).unwrap(); + for file_data in &vfs.files { + file.write_all(file_data).unwrap(); + } + } + let dest_path = temp_dir.path().join("dest"); + let data = std::fs::read(&virtual_fs_file).unwrap(); + ( + dest_path.to_path_buf(), + FileBackedVfs::new( + Cow::Owned(data), + VfsRoot { + dir: VirtualDirectory { + name: "".to_string(), + entries: vfs.entries, + }, + root_path: dest_path.to_path_buf(), + start_file_offset: 0, + }, + FileSystemCaseSensitivity::Sensitive, + ), + ) + } + + #[test] + fn circular_symlink() { + let temp_dir = TempDir::new(); + let src_path = temp_dir.path().canonicalize().join("src"); + src_path.create_dir_all(); + src_path.symlink_file("a.txt", "b.txt"); + src_path.symlink_file("b.txt", "c.txt"); + src_path.symlink_file("c.txt", "a.txt"); + let src_path = src_path.to_path_buf(); + let mut builder = VfsBuilder::new(); + let err = builder + .add_symlink(src_path.join("a.txt").as_path()) + .unwrap_err(); + assert_contains!(err.to_string(), "Circular symlink detected",); + } + + #[tokio::test] + async fn test_open_file() { + let temp_dir = TempDir::new(); + let temp_path = temp_dir.path().canonicalize(); + let mut builder = VfsBuilder::new(); + builder + .add_file_with_data_raw( + temp_path.join("a.txt").as_path(), + "0123456789".to_string().into_bytes(), + ) + .unwrap(); + let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); + let virtual_fs = Arc::new(virtual_fs); + let file = virtual_fs.open_file(&dest_path.join("a.txt")).unwrap(); + file.seek(SeekFrom::Current(2)).unwrap(); + let mut buf = vec![0; 2]; + file.read_to_buf(&mut buf).unwrap(); + assert_eq!(buf, b"23"); + file.read_to_buf(&mut buf).unwrap(); + assert_eq!(buf, b"45"); + file.seek(SeekFrom::Current(-4)).unwrap(); + file.read_to_buf(&mut buf).unwrap(); + assert_eq!(buf, b"23"); + file.seek(SeekFrom::Start(2)).unwrap(); + file.read_to_buf(&mut buf).unwrap(); + assert_eq!(buf, b"23"); + file.seek(SeekFrom::End(2)).unwrap(); + file.read_to_buf(&mut buf).unwrap(); + assert_eq!(buf, b"89"); + file.seek(SeekFrom::Current(-8)).unwrap(); + file.read_to_buf(&mut buf).unwrap(); + assert_eq!(buf, b"23"); + assert_eq!( + file + .seek(SeekFrom::Current(-5)) + .unwrap_err() + .to_string(), + "An attempt was made to move the file pointer before the beginning of the file." + ); + // go beyond the file length, then back + file.seek(SeekFrom::Current(40)).unwrap(); + file.seek(SeekFrom::Current(-38)).unwrap(); + let file = Rc::new(file); + let read_buf = file.clone().read(2).await.unwrap(); + assert_eq!(read_buf.to_vec(), b"67"); + file.clone().seek_sync(SeekFrom::Current(-2)).unwrap(); + + // read to the end of the file + let all_buf = file.clone().read_all_sync().unwrap(); + assert_eq!(all_buf.to_vec(), b"6789"); + file.clone().seek_sync(SeekFrom::Current(-9)).unwrap(); + + // try try_clone_inner and read_all_async + let all_buf = file + .try_clone_inner() + .unwrap() + .read_all_async() + .await + .unwrap(); + assert_eq!(all_buf.to_vec(), b"123456789"); + } +} diff --git a/cli/rt/integration_tests_runner.rs b/cli/rt/integration_tests_runner.rs new file mode 100644 index 0000000000..63f2abe460 --- /dev/null +++ b/cli/rt/integration_tests_runner.rs @@ -0,0 +1,5 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +pub fn main() { + // this file exists to cause the executable to be built when running cargo test +} diff --git a/cli/mainrt.rs b/cli/rt/main.rs similarity index 66% rename from cli/mainrt.rs rename to cli/rt/main.rs index 8eea3f85ed..60b5a2fb96 100644 --- a/cli/mainrt.rs +++ b/cli/rt/main.rs @@ -1,46 +1,27 @@ // Copyright 2018-2025 the Deno authors. MIT license. -// Allow unused code warnings because we share -// code between the two bin targets. -#![allow(dead_code)] -#![allow(unused_imports)] - -mod standalone; - -mod args; -mod cache; -mod emit; -mod file_fetcher; -mod http_util; -mod js; -mod node; -mod npm; -mod resolver; -mod shared; -mod sys; -mod task_runner; -mod util; -mod version; -mod worker; - use std::borrow::Cow; -use std::collections::HashMap; use std::env; -use std::env::current_exe; use std::sync::Arc; use deno_core::error::AnyError; use deno_core::error::CoreError; -use deno_core::error::JsError; +use deno_lib::util::result::any_and_jserrorbox_downcast_ref; +use deno_lib::version::otel_runtime_config; +use deno_runtime::deno_telemetry::OtelConfig; use deno_runtime::fmt_errors::format_js_error; use deno_runtime::tokio_util::create_and_run_current_thread_with_maybe_metrics; -pub use deno_runtime::UNSTABLE_GRANULAR_FLAGS; use deno_terminal::colors; use indexmap::IndexMap; -use standalone::DenoCompileFileSystem; -use crate::args::Flags; -use crate::util::result::any_and_jserrorbox_downcast_ref; +use self::binary::extract_standalone; +use self::file_system::DenoRtSys; + +mod binary; +mod code_cache; +mod file_system; +mod node; +mod run; pub(crate) fn unstable_exit_cb(feature: &str, api_name: &str) { log::error!( @@ -87,27 +68,26 @@ fn load_env_vars(env_vars: &IndexMap) { fn main() { deno_runtime::deno_permissions::mark_standalone(); let args: Vec<_> = env::args_os().collect(); - let standalone = standalone::extract_standalone(Cow::Owned(args)); + let standalone = extract_standalone(Cow::Owned(args)); let future = async move { match standalone { Ok(Some(data)) => { - deno_telemetry::init( - crate::args::otel_runtime_config(), + deno_runtime::deno_telemetry::init( + otel_runtime_config(), &data.metadata.otel_config, )?; - util::logger::init( + init_logging( data.metadata.log_level, Some(data.metadata.otel_config.clone()), ); load_env_vars(&data.metadata.env_vars_from_env_file); - let fs = DenoCompileFileSystem::new(data.vfs.clone()); - let sys = crate::sys::CliSys::DenoCompile(fs.clone()); - let exit_code = standalone::run(Arc::new(fs), sys, data).await?; + let sys = DenoRtSys::new(data.vfs.clone()); + let exit_code = run::run(Arc::new(sys.clone()), sys, data).await?; deno_runtime::exit(exit_code); } Ok(None) => Ok(()), Err(err) => { - util::logger::init(None, None); + init_logging(None, None); Err(err) } } @@ -115,3 +95,15 @@ fn main() { unwrap_or_exit(create_and_run_current_thread_with_maybe_metrics(future)); } + +fn init_logging( + maybe_level: Option, + otel_config: Option, +) { + deno_lib::util::logger::init(deno_lib::util::logger::InitLoggingOptions { + maybe_level, + otel_config, + on_log_start: || {}, + on_log_end: || {}, + }) +} diff --git a/cli/rt/node.rs b/cli/rt/node.rs new file mode 100644 index 0000000000..ef4f99cc8a --- /dev/null +++ b/cli/rt/node.rs @@ -0,0 +1,165 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; +use std::sync::Arc; + +use deno_core::url::Url; +use deno_error::JsErrorBox; +use deno_lib::loader::NpmModuleLoader; +use deno_lib::standalone::binary::CjsExportAnalysisEntry; +use deno_media_type::MediaType; +use deno_resolver::npm::DenoInNpmPackageChecker; +use deno_resolver::npm::NpmReqResolver; +use deno_runtime::deno_fs::FileSystem; +use deno_runtime::deno_node::RealIsBuiltInNodeModuleChecker; +use node_resolver::analyze::CjsAnalysis; +use node_resolver::analyze::CjsAnalysisExports; +use node_resolver::analyze::NodeCodeTranslator; + +use crate::binary::StandaloneModules; +use crate::file_system::DenoRtSys; + +pub type DenoRtCjsTracker = + deno_resolver::cjs::CjsTracker; +pub type DenoRtNpmResolver = deno_resolver::npm::NpmResolver; +pub type DenoRtNpmModuleLoader = NpmModuleLoader< + CjsCodeAnalyzer, + DenoInNpmPackageChecker, + RealIsBuiltInNodeModuleChecker, + DenoRtNpmResolver, + DenoRtSys, +>; +pub type DenoRtNodeCodeTranslator = NodeCodeTranslator< + CjsCodeAnalyzer, + DenoInNpmPackageChecker, + RealIsBuiltInNodeModuleChecker, + DenoRtNpmResolver, + DenoRtSys, +>; +pub type DenoRtNodeResolver = deno_runtime::deno_node::NodeResolver< + DenoInNpmPackageChecker, + DenoRtNpmResolver, + DenoRtSys, +>; +pub type DenoRtNpmReqResolver = NpmReqResolver< + DenoInNpmPackageChecker, + RealIsBuiltInNodeModuleChecker, + DenoRtNpmResolver, + DenoRtSys, +>; + +pub struct CjsCodeAnalyzer { + cjs_tracker: Arc, + modules: Arc, + sys: DenoRtSys, +} + +impl CjsCodeAnalyzer { + pub fn new( + cjs_tracker: Arc, + modules: Arc, + sys: DenoRtSys, + ) -> Self { + Self { + cjs_tracker, + modules, + sys, + } + } + + fn inner_cjs_analysis<'a>( + &self, + specifier: &Url, + source: Cow<'a, str>, + ) -> Result, JsErrorBox> { + let media_type = MediaType::from_specifier(specifier); + if media_type == MediaType::Json { + return Ok(CjsAnalysis::Cjs(CjsAnalysisExports { + exports: vec![], + reexports: vec![], + })); + } + + let cjs_tracker = self.cjs_tracker.clone(); + let is_maybe_cjs = cjs_tracker + .is_maybe_cjs(specifier, media_type) + .map_err(JsErrorBox::from_err)?; + let analysis = if is_maybe_cjs { + let data = self + .modules + .read(specifier)? + .and_then(|d| d.cjs_export_analysis); + match data { + Some(data) => { + let data: CjsExportAnalysisEntry = bincode::deserialize(&data) + .map_err(|err| JsErrorBox::generic(err.to_string()))?; + match data { + CjsExportAnalysisEntry::Esm => { + cjs_tracker.set_is_known_script(specifier, false); + CjsAnalysis::Esm(source) + } + CjsExportAnalysisEntry::Cjs(analysis) => { + cjs_tracker.set_is_known_script(specifier, true); + CjsAnalysis::Cjs(analysis) + } + } + } + None => { + if log::log_enabled!(log::Level::Debug) { + if self.sys.is_specifier_in_vfs(specifier) { + log::debug!( + "No CJS export analysis was stored for '{}'. Assuming ESM. This might indicate a bug in Deno.", + specifier + ); + } else { + log::debug!( + "Analyzing potentially CommonJS files is not supported at runtime in a compiled executable ({}). Assuming ESM.", + specifier + ); + } + } + // assume ESM as we don't have access to swc here + CjsAnalysis::Esm(source) + } + } + } else { + CjsAnalysis::Esm(source) + }; + + Ok(analysis) + } +} + +#[async_trait::async_trait(?Send)] +impl node_resolver::analyze::CjsCodeAnalyzer for CjsCodeAnalyzer { + async fn analyze_cjs<'a>( + &self, + specifier: &Url, + source: Option>, + ) -> Result, JsErrorBox> { + let source = match source { + Some(source) => source, + None => { + if let Ok(path) = deno_path_util::url_to_file_path(specifier) { + // todo(dsherret): should this use the sync method instead? + if let Ok(source_from_file) = + self.sys.read_text_file_lossy_async(path, None).await + { + source_from_file + } else { + return Ok(CjsAnalysis::Cjs(CjsAnalysisExports { + exports: vec![], + reexports: vec![], + })); + } + } else { + return Ok(CjsAnalysis::Cjs(CjsAnalysisExports { + exports: vec![], + reexports: vec![], + })); + } + } + }; + self.inner_cjs_analysis(specifier, source) + } +} diff --git a/cli/rt/run.rs b/cli/rt/run.rs new file mode 100644 index 0000000000..6f5c05b467 --- /dev/null +++ b/cli/rt/run.rs @@ -0,0 +1,990 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::borrow::Cow; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::OnceLock; + +use deno_cache_dir::npm::NpmCacheDir; +use deno_config::workspace::MappedResolution; +use deno_config::workspace::ResolverWorkspaceJsrPackage; +use deno_config::workspace::WorkspaceResolver; +use deno_core::error::AnyError; +use deno_core::error::ModuleLoaderError; +use deno_core::futures::future::LocalBoxFuture; +use deno_core::futures::FutureExt; +use deno_core::url::Url; +use deno_core::v8_set_flags; +use deno_core::FastString; +use deno_core::FeatureChecker; +use deno_core::ModuleLoader; +use deno_core::ModuleSourceCode; +use deno_core::ModuleType; +use deno_core::RequestedModuleType; +use deno_core::ResolutionKind; +use deno_core::SourceCodeCacheInfo; +use deno_error::JsErrorBox; +use deno_lib::args::get_root_cert_store; +use deno_lib::args::npm_pkg_req_ref_to_binary_command; +use deno_lib::args::CaData; +use deno_lib::args::RootCertStoreLoadError; +use deno_lib::loader::NpmModuleLoader; +use deno_lib::npm::create_npm_process_state_provider; +use deno_lib::npm::NpmRegistryReadPermissionChecker; +use deno_lib::npm::NpmRegistryReadPermissionCheckerMode; +use deno_lib::standalone::binary::NodeModules; +use deno_lib::util::hash::FastInsecureHasher; +use deno_lib::util::text_encoding::from_utf8_lossy_cow; +use deno_lib::util::text_encoding::from_utf8_lossy_owned; +use deno_lib::util::v8::construct_v8_flags; +use deno_lib::worker::CreateModuleLoaderResult; +use deno_lib::worker::LibMainWorkerFactory; +use deno_lib::worker::LibMainWorkerOptions; +use deno_lib::worker::ModuleLoaderFactory; +use deno_lib::worker::StorageKeyResolver; +use deno_media_type::MediaType; +use deno_npm::npm_rc::ResolvedNpmRc; +use deno_npm::resolution::NpmResolutionSnapshot; +use deno_package_json::PackageJsonDepValue; +use deno_resolver::cjs::CjsTracker; +use deno_resolver::cjs::IsCjsResolutionMode; +use deno_resolver::npm::managed::ManagedInNpmPkgCheckerCreateOptions; +use deno_resolver::npm::managed::ManagedNpmResolverCreateOptions; +use deno_resolver::npm::managed::NpmResolutionCell; +use deno_resolver::npm::ByonmNpmResolverCreateOptions; +use deno_resolver::npm::CreateInNpmPkgCheckerOptions; +use deno_resolver::npm::DenoInNpmPackageChecker; +use deno_resolver::npm::NpmReqResolver; +use deno_resolver::npm::NpmReqResolverOptions; +use deno_resolver::npm::NpmResolver; +use deno_resolver::npm::NpmResolverCreateOptions; +use deno_runtime::code_cache::CodeCache; +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::RealIsBuiltInNodeModuleChecker; +use deno_runtime::deno_permissions::Permissions; +use deno_runtime::deno_permissions::PermissionsContainer; +use deno_runtime::deno_tls::rustls::RootCertStore; +use deno_runtime::deno_tls::RootCertStoreProvider; +use deno_runtime::deno_web::BlobStore; +use deno_runtime::permissions::RuntimePermissionDescriptorParser; +use deno_runtime::WorkerExecutionMode; +use deno_runtime::WorkerLogLevel; +use deno_semver::npm::NpmPackageReqReference; +use node_resolver::analyze::NodeCodeTranslator; +use node_resolver::errors::ClosestPkgJsonError; +use node_resolver::NodeResolutionKind; +use node_resolver::NodeResolver; +use node_resolver::PackageJsonResolver; +use node_resolver::ResolutionMode; + +use crate::binary::DenoCompileModuleSource; +use crate::binary::StandaloneData; +use crate::binary::StandaloneModules; +use crate::code_cache::DenoCompileCodeCache; +use crate::file_system::DenoRtSys; +use crate::file_system::FileBackedVfs; +use crate::node::CjsCodeAnalyzer; +use crate::node::DenoRtCjsTracker; +use crate::node::DenoRtNodeCodeTranslator; +use crate::node::DenoRtNodeResolver; +use crate::node::DenoRtNpmModuleLoader; +use crate::node::DenoRtNpmReqResolver; + +struct SharedModuleLoaderState { + cjs_tracker: Arc, + code_cache: Option>, + modules: Arc, + node_code_translator: Arc, + node_resolver: Arc, + npm_module_loader: Arc, + npm_registry_permission_checker: NpmRegistryReadPermissionChecker, + npm_req_resolver: Arc, + vfs: Arc, + workspace_resolver: WorkspaceResolver, +} + +impl SharedModuleLoaderState { + fn get_code_cache( + &self, + specifier: &Url, + source: &[u8], + ) -> Option { + let Some(code_cache) = &self.code_cache else { + return None; + }; + if !code_cache.enabled() { + return None; + } + // deno version is already included in the root cache key + let hash = FastInsecureHasher::new_without_deno_version() + .write_hashable(source) + .finish(); + let data = code_cache.get_sync( + specifier, + deno_runtime::code_cache::CodeCacheType::EsModule, + hash, + ); + Some(SourceCodeCacheInfo { + hash, + data: data.map(Cow::Owned), + }) + } +} + +#[derive(Clone)] +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() + } +} + +impl ModuleLoader for EmbeddedModuleLoader { + fn resolve( + &self, + raw_specifier: &str, + referrer: &str, + kind: ResolutionKind, + ) -> Result { + let referrer = if referrer == "." { + if kind != ResolutionKind::MainModule { + return Err( + JsErrorBox::generic(format!( + "Expected to resolve main module, got {:?} instead.", + kind + )) + .into(), + ); + } + let current_dir = std::env::current_dir().unwrap(); + deno_core::resolve_path(".", ¤t_dir)? + } else { + Url::parse(referrer).map_err(|err| { + JsErrorBox::type_error(format!( + "Referrer uses invalid specifier: {}", + err + )) + })? + }; + let referrer_kind = if self + .shared + .cjs_tracker + .is_maybe_cjs(&referrer, MediaType::from_specifier(&referrer)) + .map_err(JsErrorBox::from_err)? + { + ResolutionMode::Require + } else { + ResolutionMode::Import + }; + + if self.shared.node_resolver.in_npm_package(&referrer) { + return Ok( + self + .shared + .node_resolver + .resolve( + raw_specifier, + &referrer, + referrer_kind, + NodeResolutionKind::Execution, + ) + .map_err(JsErrorBox::from_err)? + .into_url(), + ); + } + + let mapped_resolution = self + .shared + .workspace_resolver + .resolve(raw_specifier, &referrer); + + match mapped_resolution { + Ok(MappedResolution::WorkspaceJsrPackage { specifier, .. }) => { + Ok(specifier) + } + Ok(MappedResolution::WorkspaceNpmPackage { + target_pkg_json: pkg_json, + sub_path, + .. + }) => Ok( + self + .shared + .node_resolver + .resolve_package_subpath_from_deno_module( + pkg_json.dir_path(), + sub_path.as_deref(), + Some(&referrer), + referrer_kind, + NodeResolutionKind::Execution, + ) + .map_err(JsErrorBox::from_err)?, + ), + Ok(MappedResolution::PackageJson { + dep_result, + sub_path, + alias, + .. + }) => match dep_result + .as_ref() + .map_err(|e| JsErrorBox::from_err(e.clone()))? + { + PackageJsonDepValue::Req(req) => self + .shared + .npm_req_resolver + .resolve_req_with_sub_path( + req, + sub_path.as_deref(), + &referrer, + referrer_kind, + NodeResolutionKind::Execution, + ) + .map_err(|e| JsErrorBox::from_err(e).into()), + PackageJsonDepValue::Workspace(version_req) => { + let pkg_folder = self + .shared + .workspace_resolver + .resolve_workspace_pkg_json_folder_for_pkg_json_dep( + alias, + version_req, + ) + .map_err(JsErrorBox::from_err)?; + Ok( + self + .shared + .node_resolver + .resolve_package_subpath_from_deno_module( + pkg_folder, + sub_path.as_deref(), + Some(&referrer), + referrer_kind, + NodeResolutionKind::Execution, + ) + .map_err(JsErrorBox::from_err)?, + ) + } + }, + Ok(MappedResolution::Normal { specifier, .. }) + | Ok(MappedResolution::ImportMap { specifier, .. }) => { + if let Ok(reference) = + NpmPackageReqReference::from_specifier(&specifier) + { + return Ok( + self + .shared + .npm_req_resolver + .resolve_req_reference( + &reference, + &referrer, + referrer_kind, + NodeResolutionKind::Execution, + ) + .map_err(JsErrorBox::from_err)?, + ); + } + + if specifier.scheme() == "jsr" { + if let Some(specifier) = self + .shared + .modules + .resolve_specifier(&specifier) + .map_err(JsErrorBox::from_err)? + { + return Ok(specifier.clone()); + } + } + + Ok( + self + .shared + .node_resolver + .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 + .npm_req_resolver + .resolve_if_for_npm_pkg( + raw_specifier, + &referrer, + referrer_kind, + NodeResolutionKind::Execution, + ) + .map_err(JsErrorBox::from_err)?; + if let Some(res) = maybe_res { + return Ok(res.into_url()); + } + Err(JsErrorBox::from_err(err).into()) + } + Err(err) => Err(JsErrorBox::from_err(err).into()), + } + } + + fn get_host_defined_options<'s>( + &self, + scope: &mut deno_core::v8::HandleScope<'s>, + name: &str, + ) -> Option> { + let name = Url::parse(name).ok()?; + if self.shared.node_resolver.in_npm_package(&name) { + Some(create_host_defined_options(scope)) + } else { + None + } + } + + fn load( + &self, + original_specifier: &Url, + maybe_referrer: Option<&Url>, + _is_dynamic: bool, + _requested_module_type: RequestedModuleType, + ) -> deno_core::ModuleLoadResponse { + if original_specifier.scheme() == "data" { + let data_url_text = + match deno_media_type::data_url::RawDataUrl::parse(original_specifier) + .and_then(|url| url.decode()) + { + Ok(response) => response, + Err(err) => { + return deno_core::ModuleLoadResponse::Sync(Err( + JsErrorBox::type_error(format!("{:#}", err)).into(), + )); + } + }; + return deno_core::ModuleLoadResponse::Sync(Ok( + deno_core::ModuleSource::new( + deno_core::ModuleType::JavaScript, + ModuleSourceCode::String(data_url_text.into()), + original_specifier, + None, + ), + )); + } + + if self.shared.node_resolver.in_npm_package(original_specifier) { + let shared = self.shared.clone(); + let original_specifier = original_specifier.clone(); + let maybe_referrer = maybe_referrer.cloned(); + return deno_core::ModuleLoadResponse::Async( + async move { + let code_source = shared + .npm_module_loader + .load(&original_specifier, maybe_referrer.as_ref()) + .await + .map_err(JsErrorBox::from_err)?; + let code_cache_entry = shared.get_code_cache( + &code_source.found_url, + code_source.code.as_bytes(), + ); + Ok(deno_core::ModuleSource::new_with_redirect( + match code_source.media_type { + MediaType::Json => ModuleType::Json, + _ => ModuleType::JavaScript, + }, + code_source.code, + &original_specifier, + &code_source.found_url, + code_cache_entry, + )) + } + .boxed_local(), + ); + } + + 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( + JsErrorBox::type_error(format!("{:?}", err)).into(), + )); + } + }; + 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(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 + .map_err(JsErrorBox::from_err)?; + let module_source = match source { + Cow::Owned(source) => ModuleSourceCode::String(source.into()), + Cow::Borrowed(source) => { + ModuleSourceCode::String(FastString::from_static(source)) + } + }; + let code_cache_entry = shared + .get_code_cache(&module_specifier, module_source.as_bytes()); + Ok(deno_core::ModuleSource::new_with_redirect( + module_type, + module_source, + &original_specifier, + &module_specifier, + code_cache_entry, + )) + } + .boxed_local(), + ) + } else { + let module_source = module_source.into_for_v8(); + let code_cache_entry = self + .shared + .get_code_cache(module_specifier, module_source.as_bytes()); + deno_core::ModuleLoadResponse::Sync(Ok( + deno_core::ModuleSource::new_with_redirect( + module_type, + module_source, + original_specifier, + module_specifier, + code_cache_entry, + ), + )) + } + } + Ok(None) => deno_core::ModuleLoadResponse::Sync(Err( + JsErrorBox::type_error(format!( + "Module not found: {}", + original_specifier + )) + .into(), + )), + Err(err) => deno_core::ModuleLoadResponse::Sync(Err( + JsErrorBox::type_error(format!("{:?}", err)).into(), + )), + } + } + + fn code_cache_ready( + &self, + specifier: Url, + source_hash: u64, + code_cache_data: &[u8], + ) -> LocalBoxFuture<'static, ()> { + if let Some(code_cache) = &self.shared.code_cache { + code_cache.set_sync( + specifier, + deno_runtime::code_cache::CodeCacheType::EsModule, + source_hash, + code_cache_data, + ); + } + std::future::ready(()).boxed_local() + } + + fn get_source_map(&self, file_name: &str) -> Option> { + let url = Url::parse(file_name).ok()?; + let data = self.shared.modules.read(&url).ok()??; + data.source_map + } + + fn get_source_mapped_source_line( + &self, + file_name: &str, + line_number: usize, + ) -> Option { + let specifier = Url::parse(file_name).ok()?; + let data = self.shared.modules.read(&specifier).ok()??; + + let source = String::from_utf8_lossy(&data.data); + // Do NOT use .lines(): it skips the terminating empty line. + // (due to internally using_terminator() instead of .split()) + let lines: Vec<&str> = source.split('\n').collect(); + if line_number >= lines.len() { + Some(format!( + "{} Couldn't format source line: Line {} is out of bounds (source may have changed at runtime)", + crate::colors::yellow("Warning"), line_number + 1, + )) + } else { + Some(lines[line_number].to_string()) + } + } +} + +impl NodeRequireLoader for EmbeddedModuleLoader { + fn ensure_read_permission<'a>( + &self, + permissions: &mut dyn deno_runtime::deno_node::NodePermissions, + path: &'a std::path::Path, + ) -> Result, JsErrorBox> { + if self.shared.modules.has_file(path) { + // allow reading if the file is in the snapshot + return Ok(Cow::Borrowed(path)); + } + + self + .shared + .npm_registry_permission_checker + .ensure_read_permission(permissions, path) + .map_err(JsErrorBox::from_err) + } + + fn load_text_file_lossy( + &self, + path: &std::path::Path, + ) -> Result, JsErrorBox> { + let file_entry = self + .shared + .vfs + .file_entry(path) + .map_err(JsErrorBox::from_err)?; + let file_bytes = self + .shared + .vfs + .read_file_offset_with_len( + file_entry.transpiled_offset.unwrap_or(file_entry.offset), + ) + .map_err(JsErrorBox::from_err)?; + Ok(from_utf8_lossy_cow(file_bytes)) + } + + fn is_maybe_cjs(&self, specifier: &Url) -> 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, + ) -> CreateModuleLoaderResult { + self.create_result() + } + + fn create_for_worker( + &self, + _parent_permissions: PermissionsContainer, + _permissions: PermissionsContainer, + ) -> CreateModuleLoaderResult { + self.create_result() + } +} + +struct StandaloneRootCertStoreProvider { + ca_stores: Option>, + ca_data: Option, + cell: OnceLock>, +} + +impl RootCertStoreProvider for StandaloneRootCertStoreProvider { + fn get_or_try_init(&self) -> Result<&RootCertStore, JsErrorBox> { + self + .cell + // get_or_try_init was not stable yet when this was written + .get_or_init(|| { + get_root_cert_store(None, self.ca_stores.clone(), self.ca_data.clone()) + }) + .as_ref() + .map_err(|err| JsErrorBox::from_err(err.clone())) + } +} + +pub async fn run( + fs: Arc, + sys: DenoRtSys, + data: StandaloneData, +) -> Result { + let StandaloneData { + metadata, + modules, + npm_snapshot, + root_path, + vfs, + } = data; + let root_cert_store_provider = Arc::new(StandaloneRootCertStoreProvider { + ca_stores: metadata.ca_stores, + ca_data: metadata.ca_data.map(CaData::Bytes), + cell: Default::default(), + }); + // use a dummy npm registry url + let npm_registry_url = Url::parse("https://localhost/").unwrap(); + let root_dir_url = Arc::new(Url::from_directory_path(&root_path).unwrap()); + let main_module = root_dir_url.join(&metadata.entrypoint_key).unwrap(); + let npm_global_cache_dir = root_path.join(".deno_compile_node_modules"); + let pkg_json_resolver = Arc::new(PackageJsonResolver::new(sys.clone())); + let npm_registry_permission_checker = { + let mode = match &metadata.node_modules { + Some(NodeModules::Managed { + node_modules_dir: Some(path), + }) => NpmRegistryReadPermissionCheckerMode::Local(PathBuf::from(path)), + Some(NodeModules::Byonm { .. }) => { + NpmRegistryReadPermissionCheckerMode::Byonm + } + Some(NodeModules::Managed { + node_modules_dir: None, + }) + | None => NpmRegistryReadPermissionCheckerMode::Global( + npm_global_cache_dir.clone(), + ), + }; + NpmRegistryReadPermissionChecker::new(sys.clone(), mode) + }; + let (in_npm_pkg_checker, npm_resolver) = match metadata.node_modules { + Some(NodeModules::Managed { node_modules_dir }) => { + // 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( + &sys, + 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| root_path.join(node_modules_dir)); + let in_npm_pkg_checker = + DenoInNpmPackageChecker::new(CreateInNpmPkgCheckerOptions::Managed( + ManagedInNpmPkgCheckerCreateOptions { + root_cache_dir_url: npm_cache_dir.root_dir_url(), + maybe_node_modules_path: maybe_node_modules_path.as_deref(), + }, + )); + let npm_resolution = + Arc::new(NpmResolutionCell::new(NpmResolutionSnapshot::new(snapshot))); + let npm_resolver = NpmResolver::::new::( + NpmResolverCreateOptions::Managed(ManagedNpmResolverCreateOptions { + npm_resolution, + npm_cache_dir, + sys: sys.clone(), + maybe_node_modules_path, + npm_system_info: Default::default(), + npmrc, + }), + ); + (in_npm_pkg_checker, npm_resolver) + } + Some(NodeModules::Byonm { + root_node_modules_dir, + }) => { + let root_node_modules_dir = + root_node_modules_dir.map(|p| vfs.root().join(p)); + let in_npm_pkg_checker = + DenoInNpmPackageChecker::new(CreateInNpmPkgCheckerOptions::Byonm); + let npm_resolver = NpmResolver::::new::( + NpmResolverCreateOptions::Byonm(ByonmNpmResolverCreateOptions { + sys: sys.clone(), + pkg_json_resolver: pkg_json_resolver.clone(), + root_node_modules_dir, + }), + ); + (in_npm_pkg_checker, npm_resolver) + } + None => { + // 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( + &sys, + npm_global_cache_dir, + npmrc.get_all_known_registries_urls(), + )); + let in_npm_pkg_checker = + DenoInNpmPackageChecker::new(CreateInNpmPkgCheckerOptions::Managed( + ManagedInNpmPkgCheckerCreateOptions { + root_cache_dir_url: npm_cache_dir.root_dir_url(), + maybe_node_modules_path: None, + }, + )); + let npm_resolution = Arc::new(NpmResolutionCell::default()); + let npm_resolver = NpmResolver::::new::( + NpmResolverCreateOptions::Managed(ManagedNpmResolverCreateOptions { + npm_resolution, + sys: sys.clone(), + npm_cache_dir, + maybe_node_modules_path: None, + npm_system_info: Default::default(), + npmrc: create_default_npmrc(), + }), + ); + (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( + in_npm_pkg_checker.clone(), + RealIsBuiltInNodeModuleChecker, + npm_resolver.clone(), + pkg_json_resolver.clone(), + sys.clone(), + node_resolver::ConditionsFromResolutionMode::default(), + )); + let cjs_tracker = Arc::new(CjsTracker::new( + in_npm_pkg_checker.clone(), + pkg_json_resolver.clone(), + if metadata.unstable_config.detect_cjs { + IsCjsResolutionMode::ImplicitTypeCommonJs + } else if metadata.workspace_resolver.package_jsons.is_empty() { + IsCjsResolutionMode::Disabled + } else { + IsCjsResolutionMode::ExplicitTypeCommonJs + }, + )); + let npm_req_resolver = Arc::new(NpmReqResolver::new(NpmReqResolverOptions { + sys: sys.clone(), + in_npm_pkg_checker: in_npm_pkg_checker.clone(), + node_resolver: node_resolver.clone(), + npm_resolver: npm_resolver.clone(), + })); + let cjs_esm_code_analyzer = + CjsCodeAnalyzer::new(cjs_tracker.clone(), modules.clone(), sys.clone()); + let node_code_translator = Arc::new(NodeCodeTranslator::new( + cjs_esm_code_analyzer, + in_npm_pkg_checker, + node_resolver.clone(), + npm_resolver.clone(), + pkg_json_resolver.clone(), + sys.clone(), + )); + let workspace_resolver = { + let import_map = match metadata.workspace_resolver.import_map { + Some(import_map) => Some( + import_map::parse_from_json_with_options( + root_dir_url.join(&import_map.specifier).unwrap(), + &import_map.json, + import_map::ImportMapOptions { + address_hook: None, + expand_imports: true, + }, + )? + .import_map, + ), + None => None, + }; + let pkg_jsons = metadata + .workspace_resolver + .package_jsons + .into_iter() + .map(|(relative_path, json)| { + let path = root_dir_url + .join(&relative_path) + .unwrap() + .to_file_path() + .unwrap(); + let pkg_json = + deno_package_json::PackageJson::load_from_value(path, json); + Arc::new(pkg_json) + }) + .collect(); + WorkspaceResolver::new_raw( + root_dir_url.clone(), + import_map, + metadata + .workspace_resolver + .jsr_pkgs + .iter() + .map(|pkg| ResolverWorkspaceJsrPackage { + is_patch: false, // only used for enhancing the diagnostic, which isn't shown in deno compile + base: root_dir_url.join(&pkg.relative_base).unwrap(), + name: pkg.name.clone(), + version: pkg.version.clone(), + exports: pkg.exports.clone(), + }) + .collect(), + pkg_jsons, + metadata.workspace_resolver.pkg_json_resolution, + ) + }; + let code_cache = match metadata.code_cache_key { + Some(code_cache_key) => Some(Arc::new(DenoCompileCodeCache::new( + root_path.with_file_name(format!( + "{}.cache", + root_path.file_name().unwrap().to_string_lossy() + )), + code_cache_key, + ))), + None => { + log::debug!("Code cache disabled."); + None + } + }; + let module_loader_factory = StandaloneModuleLoaderFactory { + shared: Arc::new(SharedModuleLoaderState { + cjs_tracker: cjs_tracker.clone(), + code_cache: code_cache.clone(), + modules, + node_code_translator: node_code_translator.clone(), + node_resolver: node_resolver.clone(), + npm_module_loader: Arc::new(NpmModuleLoader::new( + cjs_tracker.clone(), + node_code_translator, + sys.clone(), + )), + npm_registry_permission_checker, + npm_req_resolver, + vfs: vfs.clone(), + workspace_resolver, + }), + }; + + let permissions = { + let mut permissions = metadata.permissions; + // 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()]); + } + } + + let desc_parser = + Arc::new(RuntimePermissionDescriptorParser::new(sys.clone())); + let permissions = + Permissions::from_options(desc_parser.as_ref(), &permissions)?; + PermissionsContainer::new(desc_parser, permissions) + }; + let feature_checker = Arc::new({ + let mut checker = FeatureChecker::default(); + checker.set_exit_cb(Box::new(crate::unstable_exit_cb)); + for feature in metadata.unstable_config.features { + // `metadata` is valid for the whole lifetime of the program, so we + // can leak the string here. + checker.enable_feature(feature.leak()); + } + checker + }); + let lib_main_worker_options = LibMainWorkerOptions { + argv: metadata.argv, + log_level: WorkerLogLevel::Info, + enable_op_summary_metrics: false, + enable_testing_features: false, + has_node_modules_dir, + inspect_brk: false, + inspect_wait: false, + strace_ops: None, + is_inspecting: false, + skip_op_registration: true, + location: metadata.location, + argv0: NpmPackageReqReference::from_specifier(&main_module) + .ok() + .map(|req_ref| npm_pkg_req_ref_to_binary_command(&req_ref)) + .or(std::env::args().next()), + node_debug: std::env::var("NODE_DEBUG").ok(), + origin_data_folder_path: None, + seed: metadata.seed, + unsafely_ignore_certificate_errors: metadata + .unsafely_ignore_certificate_errors, + node_ipc: None, + serve_port: None, + serve_host: None, + otel_config: metadata.otel_config, + startup_snapshot: deno_snapshots::CLI_SNAPSHOT, + }; + let worker_factory = LibMainWorkerFactory::new( + Arc::new(BlobStore::default()), + code_cache.map(|c| c.for_deno_core()), + feature_checker, + fs, + None, + Box::new(module_loader_factory), + node_resolver.clone(), + create_npm_process_state_provider(&npm_resolver), + pkg_json_resolver, + root_cert_store_provider, + StorageKeyResolver::empty(), + sys.clone(), + lib_main_worker_options, + ); + + // Initialize v8 once from the main thread. + v8_set_flags(construct_v8_flags(&[], &metadata.v8_flags, vec![])); + // TODO(bartlomieju): remove last argument once Deploy no longer needs it + deno_core::JsRuntime::init_platform(None, true); + + let main_module = match NpmPackageReqReference::from_specifier(&main_module) { + Ok(package_ref) => { + let pkg_folder = npm_resolver.resolve_pkg_folder_from_deno_module_req( + package_ref.req(), + &deno_path_util::url_from_file_path(&vfs.root().join("package.json"))?, + )?; + worker_factory + .resolve_npm_binary_entrypoint(&pkg_folder, package_ref.sub_path())? + } + Err(_) => main_module, + }; + + let mut worker = worker_factory.create_main_worker( + WorkerExecutionMode::Run, + permissions, + main_module, + )?; + + let exit_code = worker.run().await?; + Ok(exit_code) +} + +fn create_default_npmrc() -> Arc { + // this is fine because multiple registries are combined into + // one when compiling the binary + Arc::new(ResolvedNpmRc { + default_config: deno_npm::npm_rc::RegistryConfigWithUrl { + registry_url: Url::parse("https://registry.npmjs.org").unwrap(), + config: Default::default(), + }, + scopes: Default::default(), + registry_configs: Default::default(), + }) +} diff --git a/cli/snapshot/Cargo.toml b/cli/snapshot/Cargo.toml new file mode 100644 index 0000000000..c023af04a3 --- /dev/null +++ b/cli/snapshot/Cargo.toml @@ -0,0 +1,20 @@ +# Copyright 2018-2025 the Deno authors. MIT license. + +[package] +name = "deno_snapshots" +version = "0.2.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true +description = "v8 snapshot used by the Deno CLI" + +[lib] +path = "lib.rs" + +[features] +disable = [] + +[build-dependencies] +deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting", "only_snapshotted_js_sources", "snapshot"] } diff --git a/cli/snapshot/README.md b/cli/snapshot/README.md new file mode 100644 index 0000000000..d52dead6b2 --- /dev/null +++ b/cli/snapshot/README.md @@ -0,0 +1,3 @@ +# deno_snapshots + +v8 snapshot used in the Deno CLI. diff --git a/cli/snapshot/build.rs b/cli/snapshot/build.rs new file mode 100644 index 0000000000..9f08ac0e9e --- /dev/null +++ b/cli/snapshot/build.rs @@ -0,0 +1,30 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +#[cfg(not(feature = "disable"))] +mod shared; + +fn main() { + #[cfg(not(feature = "disable"))] + { + let o = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + let cli_snapshot_path = o.join("CLI_SNAPSHOT.bin"); + create_cli_snapshot(cli_snapshot_path); + } +} + +#[cfg(not(feature = "disable"))] +fn create_cli_snapshot(snapshot_path: std::path::PathBuf) { + use deno_runtime::ops::bootstrap::SnapshotOptions; + + let snapshot_options = SnapshotOptions { + ts_version: shared::TS_VERSION.to_string(), + v8_version: deno_runtime::deno_core::v8::VERSION_STRING, + target: std::env::var("TARGET").unwrap(), + }; + + deno_runtime::snapshot::create_runtime_snapshot( + snapshot_path, + snapshot_options, + vec![], + ); +} diff --git a/cli/snapshot/lib.rs b/cli/snapshot/lib.rs new file mode 100644 index 0000000000..e5af4bcf6b --- /dev/null +++ b/cli/snapshot/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +#[cfg(not(feature = "disable"))] +pub static CLI_SNAPSHOT: Option<&[u8]> = Some(include_bytes!(concat!( + env!("OUT_DIR"), + "/CLI_SNAPSHOT.bin" +))); +#[cfg(feature = "disable")] +pub static CLI_SNAPSHOT: Option<&[u8]> = None; + +mod shared; + +pub use shared::TS_VERSION; diff --git a/cli/snapshot/shared.rs b/cli/snapshot/shared.rs new file mode 100644 index 0000000000..eec982776a --- /dev/null +++ b/cli/snapshot/shared.rs @@ -0,0 +1,3 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +pub static TS_VERSION: &str = "5.6.2"; diff --git a/cli/standalone/binary.rs b/cli/standalone/binary.rs index f07c645d89..4cb30316c1 100644 --- a/cli/standalone/binary.rs +++ b/cli/standalone/binary.rs @@ -1,109 +1,70 @@ // Copyright 2018-2025 the Deno authors. MIT license. use std::borrow::Cow; -use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::VecDeque; use std::env; -use std::env::current_exe; 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::Component; use std::path::Path; use std::path::PathBuf; -use std::process::Command; -use std::sync::Arc; +use capacity_builder::BytesAppendable; use deno_ast::MediaType; use deno_ast::ModuleKind; use deno_ast::ModuleSpecifier; -use deno_config::workspace::PackageJsonDepResolution; -use deno_config::workspace::ResolverWorkspaceJsrPackage; -use deno_config::workspace::Workspace; use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; -use deno_core::futures::io::AllowStdIo; -use deno_core::futures::AsyncReadExt; -use deno_core::futures::AsyncSeekExt; use deno_core::serde_json; use deno_core::url::Url; use deno_graph::ModuleGraph; +use deno_lib::args::CaData; +use deno_lib::args::UnstableConfig; +use deno_lib::shared::ReleaseChannel; +use deno_lib::standalone::binary::CjsExportAnalysisEntry; +use deno_lib::standalone::binary::Metadata; +use deno_lib::standalone::binary::NodeModules; +use deno_lib::standalone::binary::RemoteModuleEntry; +use deno_lib::standalone::binary::SerializedResolverWorkspaceJsrPackage; +use deno_lib::standalone::binary::SerializedWorkspaceResolver; +use deno_lib::standalone::binary::SerializedWorkspaceResolverImportMap; +use deno_lib::standalone::binary::SpecifierDataStore; +use deno_lib::standalone::binary::SpecifierId; +use deno_lib::standalone::binary::MAGIC_BYTES; +use deno_lib::standalone::virtual_fs::BuiltVfs; +use deno_lib::standalone::virtual_fs::VfsBuilder; +use deno_lib::standalone::virtual_fs::VfsEntry; +use deno_lib::standalone::virtual_fs::VirtualDirectory; +use deno_lib::standalone::virtual_fs::VirtualDirectoryEntries; +use deno_lib::standalone::virtual_fs::WindowsSystemRootablePath; +use deno_lib::standalone::virtual_fs::DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME; +use deno_lib::util::hash::FastInsecureHasher; +use deno_lib::version::DENO_VERSION_INFO; 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_path_util::url_from_directory_path; -use deno_path_util::url_from_file_path; use deno_path_util::url_to_file_path; -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::deno_permissions::PermissionsOptions; -use deno_semver::npm::NpmVersionReqParseError; -use deno_semver::package::PackageReq; -use deno_semver::Version; -use deno_semver::VersionReqSpecifierParseError; -use deno_telemetry::OtelConfig; use indexmap::IndexMap; -use log::Level; -use serde::Deserialize; -use serde::Serialize; +use node_resolver::analyze::CjsAnalysis; +use node_resolver::analyze::CjsCodeAnalyzer; -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::serialization::SourceMapStore; use super::virtual_fs::output_vfs; -use super::virtual_fs::BuiltVfs; -use super::virtual_fs::FileBackedVfs; -use super::virtual_fs::VfsBuilder; -use super::virtual_fs::VfsFileSubDataKind; -use super::virtual_fs::VfsRoot; -use super::virtual_fs::VirtualDirectory; -use super::virtual_fs::VirtualDirectoryEntries; -use super::virtual_fs::WindowsSystemRootablePath; -use crate::args::CaData; use crate::args::CliOptions; use crate::args::CompileFlags; -use crate::args::NpmInstallDepsProvider; -use crate::args::PermissionFlags; -use crate::args::UnstableConfig; use crate::cache::DenoDir; -use crate::cache::FastInsecureHasher; use crate::emit::Emitter; -use crate::file_fetcher::CliFileFetcher; use crate::http_util::HttpClientProvider; +use crate::node::CliCjsCodeAnalyzer; use crate::npm::CliNpmResolver; -use crate::npm::InnerCliNpmResolverRef; -use crate::resolver::CjsTracker; -use crate::shared::ReleaseChannel; -use crate::standalone::virtual_fs::VfsEntry; +use crate::resolver::CliCjsTracker; use crate::util::archive; -use crate::util::fs::canonicalize_path; -use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; -pub static DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME: &str = - ".deno_compile_node_modules"; - /// 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 @@ -149,103 +110,60 @@ impl<'a> StandaloneRelativeFileBaseUrl<'a> { } } -#[derive(Deserialize, Serialize)] -pub enum NodeModules { - Managed { - /// Relative path for the node_modules directory in the vfs. - node_modules_dir: Option, - }, - Byonm { - root_node_modules_dir: Option, - }, +struct SpecifierStore<'a> { + data: IndexMap<&'a Url, SpecifierId>, } -#[derive(Deserialize, Serialize)] -pub struct SerializedWorkspaceResolverImportMap { - pub specifier: String, - pub json: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializedResolverWorkspaceJsrPackage { - pub relative_base: String, - pub name: String, - pub version: Option, - pub exports: IndexMap, -} - -#[derive(Deserialize, Serialize)] -pub struct SerializedWorkspaceResolver { - pub import_map: Option, - pub jsr_pkgs: Vec, - pub package_jsons: BTreeMap, - pub pkg_json_resolution: PackageJsonDepResolution, -} - -// Note: Don't use hashmaps/hashsets. Ensure the serialization -// is deterministic. -#[derive(Deserialize, Serialize)] -pub struct Metadata { - pub argv: Vec, - pub seed: Option, - pub code_cache_key: Option, - pub permissions: PermissionsOptions, - pub location: Option, - pub v8_flags: Vec, - pub log_level: Option, - pub ca_stores: Option>, - pub ca_data: Option>, - pub unsafely_ignore_certificate_errors: Option>, - pub env_vars_from_env_file: IndexMap, - pub workspace_resolver: SerializedWorkspaceResolver, - pub entrypoint_key: String, - pub node_modules: Option, - pub unstable_config: UnstableConfig, - pub otel_config: OtelConfig, -} - -#[allow(clippy::too_many_arguments)] -fn write_binary_bytes( - mut file_writer: File, - original_bin: Vec, - metadata: &Metadata, - npm_snapshot: Option, - remote_modules: &RemoteModulesStoreBuilder, - source_map_store: &SourceMapStore, - vfs: &BuiltVfs, - compile_flags: &CompileFlags, -) -> Result<(), AnyError> { - let data_section_bytes = serialize_binary_data_section( - metadata, - npm_snapshot, - remote_modules, - source_map_store, - vfs, - ) - .context("Serializing binary data section.")?; - - let target = compile_flags.resolve_target(); - if target.contains("linux") { - libsui::Elf::new(&original_bin).append( - "d3n0l4nd", - &data_section_bytes, - &mut file_writer, - )?; - } else if target.contains("windows") { - let mut pe = libsui::PortableExecutable::from(&original_bin)?; - if let Some(icon) = compile_flags.icon.as_ref() { - let icon = std::fs::read(icon)?; - pe = pe.set_icon(&icon)?; +impl<'a> SpecifierStore<'a> { + pub fn with_capacity(capacity: usize) -> Self { + Self { + data: IndexMap::with_capacity(capacity), + } + } + + pub fn get_or_add(&mut self, specifier: &'a Url) -> SpecifierId { + let len = self.data.len(); + let entry = self.data.entry(specifier); + match entry { + indexmap::map::Entry::Occupied(occupied_entry) => *occupied_entry.get(), + indexmap::map::Entry::Vacant(vacant_entry) => { + let new_id = SpecifierId::new(len as u32); + vacant_entry.insert(new_id); + new_id + } + } + } + + pub fn for_serialization( + self, + base_url: &StandaloneRelativeFileBaseUrl<'a>, + ) -> SpecifierStoreForSerialization<'a> { + SpecifierStoreForSerialization { + data: self + .data + .into_iter() + .map(|(specifier, id)| (base_url.specifier_key(specifier), id)) + .collect(), + } + } +} + +struct SpecifierStoreForSerialization<'a> { + data: Vec<(Cow<'a, str>, SpecifierId)>, +} + +impl<'a> BytesAppendable<'a> for &'a SpecifierStoreForSerialization<'a> { + fn append_to_builder( + self, + builder: &mut capacity_builder::BytesBuilder<'a, TBytes>, + ) { + builder.append_le(self.data.len() as u32); + for (specifier_str, id) in &self.data { + builder.append_le(specifier_str.len() as u32); + builder.append(specifier_str.as_ref()); + builder.append(*id); } - - pe.write_resource("d3n0l4nd", data_section_bytes)? - .build(&mut file_writer)?; - } else if target.contains("darwin") { - libsui::Macho::from(original_bin)? - .write_section("d3n0l4nd", data_section_bytes)? - .build_and_sign(&mut file_writer)?; } - Ok(()) } pub fn is_standalone_binary(exe_path: &Path) -> bool { @@ -258,142 +176,6 @@ pub fn is_standalone_binary(exe_path: &Path) -> bool { || libsui::utils::is_macho(&data) } -pub struct StandaloneData { - pub metadata: Metadata, - pub modules: StandaloneModules, - pub npm_snapshot: Option, - pub root_path: PathBuf, - pub source_maps: SourceMapStore, - 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, - kind: VfsFileSubDataKind, - ) -> 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, kind)?, - Err(err) if err.kind() == ErrorKind::NotFound => { - 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()), - } - } - Err(err) => return Err(err.into()), - }; - Ok(Some(DenoCompileModuleData { - media_type: MediaType::from_specifier(specifier), - specifier, - data: bytes, - })) - } else { - self.remote_modules.read(specifier).map(|maybe_entry| { - maybe_entry.map(|entry| DenoCompileModuleData { - media_type: entry.media_type, - specifier: entry.specifier, - data: match kind { - VfsFileSubDataKind::Raw => entry.data, - VfsFileSubDataKind::ModuleGraph => { - entry.transpiled_data.unwrap_or(entry.data) - } - }, - }) - }) - } - } -} - -/// 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, -/// then checking for the magic trailer string `d3n0l4nd`. If found, -/// the bundle is executed. If not, this function exits with `Ok(None)`. -pub fn extract_standalone( - cli_args: Cow>, -) -> Result, AnyError> { - let Some(data) = libsui::find_section("d3n0l4nd") else { - return Ok(None); - }; - - let DeserializedDataSection { - mut metadata, - npm_snapshot, - remote_modules, - source_maps, - vfs_root_entries, - vfs_files_data, - } = match deserialize_binary_data_section(data)? { - Some(data_section) => data_section, - None => return Ok(None), - }; - - 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(); - 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 = { - let fs_root = VfsRoot { - dir: VirtualDirectory { - // align the name of the directory with the root dir - name: root_path.file_name().unwrap().to_string_lossy().to_string(), - entries: vfs_root_entries, - }, - root_path: root_path.clone(), - start_file_offset: 0, - }; - Arc::new(FileBackedVfs::new(Cow::Borrowed(vfs_files_data), fs_root)) - }; - Ok(Some(StandaloneData { - metadata, - modules: StandaloneModules { - remote_modules, - vfs: vfs.clone(), - }, - npm_snapshot, - root_path, - source_maps, - vfs, - })) -} - pub struct WriteBinOptions<'a> { pub writer: File, pub display_output_filename: &'a str, @@ -404,13 +186,13 @@ pub struct WriteBinOptions<'a> { } pub struct DenoCompileBinaryWriter<'a> { - cjs_tracker: &'a CjsTracker, + cjs_code_analyzer: CliCjsCodeAnalyzer, + cjs_tracker: &'a CliCjsTracker, cli_options: &'a CliOptions, deno_dir: &'a DenoDir, emitter: &'a Emitter, - file_fetcher: &'a CliFileFetcher, http_client_provider: &'a HttpClientProvider, - npm_resolver: &'a dyn CliNpmResolver, + npm_resolver: &'a CliNpmResolver, workspace_resolver: &'a WorkspaceResolver, npm_system_info: NpmSystemInfo, } @@ -418,22 +200,22 @@ pub struct DenoCompileBinaryWriter<'a> { impl<'a> DenoCompileBinaryWriter<'a> { #[allow(clippy::too_many_arguments)] pub fn new( - cjs_tracker: &'a CjsTracker, + cjs_code_analyzer: CliCjsCodeAnalyzer, + cjs_tracker: &'a CliCjsTracker, cli_options: &'a CliOptions, deno_dir: &'a DenoDir, emitter: &'a Emitter, - file_fetcher: &'a CliFileFetcher, http_client_provider: &'a HttpClientProvider, - npm_resolver: &'a dyn CliNpmResolver, + npm_resolver: &'a CliNpmResolver, workspace_resolver: &'a WorkspaceResolver, npm_system_info: NpmSystemInfo, ) -> Self { Self { + cjs_code_analyzer, cjs_tracker, cli_options, deno_dir, emitter, - file_fetcher, http_client_provider, npm_resolver, workspace_resolver, @@ -469,7 +251,7 @@ impl<'a> DenoCompileBinaryWriter<'a> { ) } } - self.write_standalone_binary(options, original_binary) + self.write_standalone_binary(options, original_binary).await } async fn get_base_binary( @@ -489,19 +271,14 @@ impl<'a> DenoCompileBinaryWriter<'a> { let target = compile_flags.resolve_target(); let binary_name = format!("denort-{target}.zip"); - let binary_path_suffix = - match crate::version::DENO_VERSION_INFO.release_channel { - ReleaseChannel::Canary => { - format!( - "canary/{}/{}", - crate::version::DENO_VERSION_INFO.git_hash, - binary_name - ) - } - _ => { - format!("release/v{}/{}", env!("CARGO_PKG_VERSION"), binary_name) - } - }; + let binary_path_suffix = match DENO_VERSION_INFO.release_channel { + ReleaseChannel::Canary => { + format!("canary/{}/{}", DENO_VERSION_INFO.git_hash, binary_name) + } + _ => { + format!("release/v{}/{}", DENO_VERSION_INFO.deno, binary_name) + } + }; let download_directory = self.deno_dir.dl_folder_path(); let binary_path = download_directory.join(&binary_path_suffix); @@ -572,7 +349,7 @@ 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, options: WriteBinOptions<'_>, original_bin: Vec, @@ -593,10 +370,11 @@ impl<'a> DenoCompileBinaryWriter<'a> { None => None, }; let mut vfs = VfsBuilder::new(); - let npm_snapshot = match self.npm_resolver.as_inner() { - InnerCliNpmResolverRef::Managed(managed) => { - let snapshot = - managed.serialized_valid_snapshot_for_system(&self.npm_system_info); + let npm_snapshot = match &self.npm_resolver { + CliNpmResolver::Managed(managed) => { + let snapshot = managed + .resolution() + .serialized_valid_snapshot_for_system(&self.npm_system_info); if !snapshot.as_serialized().packages.is_empty() { self.fill_npm_vfs(&mut vfs).context("Building npm vfs.")?; Some(snapshot) @@ -604,7 +382,7 @@ impl<'a> DenoCompileBinaryWriter<'a> { None } } - InnerCliNpmResolverRef::Byonm(_) => { + CliNpmResolver::Byonm(_) => { self.fill_npm_vfs(&mut vfs)?; None } @@ -615,23 +393,50 @@ impl<'a> DenoCompileBinaryWriter<'a> { .add_file_at_path(&path) .with_context(|| format!("Including {}", path.display()))?; } - let mut remote_modules_store = RemoteModulesStoreBuilder::default(); - let mut source_maps = Vec::with_capacity(graph.specifiers_count()); - // todo(dsherret): transpile in parallel + let specifiers_count = graph.specifiers_count(); + let mut specifier_store = SpecifierStore::with_capacity(specifiers_count); + let mut remote_modules_store = + SpecifierDataStore::with_capacity(specifiers_count); + // todo(dsherret): transpile and analyze CJS in parallel 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_original_source, maybe_transpiled, media_type) = match module { + let mut maybe_source_map = None; + let mut maybe_transpiled = None; + let mut maybe_cjs_analysis = None; + let (maybe_original_source, media_type) = match module { deno_graph::Module::Js(m) => { - let original_bytes = m.source.as_bytes().to_vec(); - let maybe_transpiled = if m.media_type.is_emittable() { - let is_cjs = self.cjs_tracker.is_cjs_with_known_is_script( - &m.specifier, + let specifier = &m.specifier; + let original_bytes = m.source.as_bytes(); + if self.cjs_tracker.is_maybe_cjs(specifier, m.media_type)? { + if self.cjs_tracker.is_cjs_with_known_is_script( + specifier, m.media_type, m.is_script, - )?; - let module_kind = ModuleKind::from_is_cjs(is_cjs); + )? { + let cjs_analysis = self + .cjs_code_analyzer + .analyze_cjs( + module.specifier(), + Some(Cow::Borrowed(m.source.as_ref())), + ) + .await?; + maybe_cjs_analysis = Some(match cjs_analysis { + CjsAnalysis::Esm(_) => CjsExportAnalysisEntry::Esm, + CjsAnalysis::Cjs(exports) => { + CjsExportAnalysisEntry::Cjs(exports) + } + }); + } else { + maybe_cjs_analysis = Some(CjsExportAnalysisEntry::Esm); + } + } + if m.media_type.is_emittable() { + let module_kind = match maybe_cjs_analysis.as_ref() { + Some(CjsExportAnalysisEntry::Cjs(_)) => ModuleKind::Cjs, + _ => ModuleKind::Esm, + }; let (source, source_map) = self.emitter.emit_parsed_source_for_deno_compile( &m.specifier, @@ -640,60 +445,67 @@ impl<'a> DenoCompileBinaryWriter<'a> { &m.source, )?; if source != m.source.as_ref() { - source_maps.push((&m.specifier, source_map)); - Some(source.into_bytes()) - } else { - None + maybe_source_map = Some(source_map.into_bytes()); + maybe_transpiled = Some(source.into_bytes()); } - } else { - None - }; - (Some(original_bytes), maybe_transpiled, m.media_type) + } + (Some(original_bytes), m.media_type) } deno_graph::Module::Json(m) => { - (Some(m.source.as_bytes().to_vec()), None, m.media_type) + (Some(m.source.as_bytes()), m.media_type) } deno_graph::Module::Wasm(m) => { - (Some(m.source.to_vec()), None, MediaType::Wasm) + (Some(m.source.as_ref()), MediaType::Wasm) } deno_graph::Module::Npm(_) | deno_graph::Module::Node(_) - | deno_graph::Module::External(_) => (None, None, MediaType::Unknown), + | deno_graph::Module::External(_) => (None, MediaType::Unknown), }; if let Some(original_source) = maybe_original_source { + let maybe_cjs_export_analysis = maybe_cjs_analysis + .as_ref() + .map(bincode::serialize) + .transpose()?; if module.specifier().scheme() == "file" { let file_path = deno_path_util::url_to_file_path(module.specifier())?; vfs .add_file_with_data( &file_path, - original_source, - VfsFileSubDataKind::Raw, + deno_lib::standalone::virtual_fs::AddFileDataOptions { + data: original_source.to_vec(), + maybe_transpiled, + maybe_source_map, + maybe_cjs_export_analysis, + }, ) .with_context(|| { format!("Failed adding '{}'", file_path.display()) })?; - if let Some(transpiled_source) = maybe_transpiled { - vfs - .add_file_with_data( - &file_path, - transpiled_source, - VfsFileSubDataKind::ModuleGraph, - ) - .with_context(|| { - format!("Failed adding '{}'", file_path.display()) - })?; - } } else { + let specifier_id = specifier_store.get_or_add(module.specifier()); remote_modules_store.add( - module.specifier(), - media_type, - original_source, - maybe_transpiled, + specifier_id, + RemoteModuleEntry { + media_type, + data: Cow::Borrowed(original_source), + maybe_transpiled: maybe_transpiled.map(Cow::Owned), + maybe_source_map: maybe_source_map.map(Cow::Owned), + maybe_cjs_export_analysis: maybe_cjs_export_analysis + .map(Cow::Owned), + }, ); } } } - remote_modules_store.add_redirects(&graph.redirects); + + let mut redirects_store = + SpecifierDataStore::with_capacity(graph.redirects.len()); + for (from, to) in &graph.redirects { + redirects_store.add( + specifier_store.get_or_add(from), + specifier_store.get_or_add(to), + ); + } if let Some(import_map) = self.workspace_resolver.maybe_import_map() { if let Ok(file_path) = url_to_file_path(import_map.base_url()) { @@ -711,7 +523,48 @@ impl<'a> DenoCompileBinaryWriter<'a> { } } + // do CJS export analysis on all the files in the VFS + // todo(dsherret): analyze cjs in parallel + let mut to_add = Vec::new(); + for (file_path, file) in vfs.iter_files() { + if file.cjs_export_analysis_offset.is_some() { + continue; // already analyzed + } + let specifier = deno_path_util::url_from_file_path(&file_path)?; + let media_type = MediaType::from_specifier(&specifier); + if self.cjs_tracker.is_maybe_cjs(&specifier, media_type)? { + let maybe_source = vfs + .file_bytes(file.offset) + .map(|text| String::from_utf8_lossy(text)); + let cjs_analysis_result = self + .cjs_code_analyzer + .analyze_cjs(&specifier, maybe_source) + .await; + let maybe_analysis = match cjs_analysis_result { + Ok(CjsAnalysis::Esm(_)) => Some(CjsExportAnalysisEntry::Esm), + Ok(CjsAnalysis::Cjs(exports)) => { + Some(CjsExportAnalysisEntry::Cjs(exports)) + } + Err(err) => { + log::debug!( + "Ignoring cjs export analysis for '{}': {}", + specifier, + err + ); + None + } + }; + if let Some(analysis) = &maybe_analysis { + to_add.push((file_path, bincode::serialize(analysis)?)); + } + } + } + for (file_path, analysis) in to_add { + vfs.add_cjs_export_analysis(&file_path, analysis); + } + let vfs = self.build_vfs_consolidating_global_npm_cache(vfs); + let root_dir_url = match &vfs.root_path { WindowsSystemRootablePath::Path(dir) => { Some(url_from_directory_path(dir)?) @@ -737,16 +590,8 @@ impl<'a> DenoCompileBinaryWriter<'a> { None }; - let mut source_map_store = SourceMapStore::with_capacity(source_maps.len()); - for (specifier, source_map) in source_maps { - source_map_store.add( - Cow::Owned(root_dir_url.specifier_key(specifier).into_owned()), - Cow::Owned(source_map.into_bytes()), - ); - } - - let node_modules = match self.npm_resolver.as_inner() { - InnerCliNpmResolverRef::Managed(_) => { + let node_modules = match &self.npm_resolver { + CliNpmResolver::Managed(_) => { npm_snapshot.as_ref().map(|_| NodeModules::Managed { node_modules_dir: self.npm_resolver.root_node_modules_path().map( |path| { @@ -759,7 +604,7 @@ impl<'a> DenoCompileBinaryWriter<'a> { ), }) } - InnerCliNpmResolverRef::Byonm(resolver) => Some(NodeModules::Byonm { + CliNpmResolver::Byonm(resolver) => Some(NodeModules::Byonm { root_node_modules_dir: resolver.root_node_modules_path().map( |node_modules_dir| { root_dir_url @@ -851,19 +696,21 @@ impl<'a> DenoCompileBinaryWriter<'a> { npm_lazy_caching: self.cli_options.unstable_npm_lazy_caching(), }, otel_config: self.cli_options.otel_config(), + vfs_case_sensitivity: vfs.case_sensitivity, }; - write_binary_bytes( - writer, - original_bin, + let data_section_bytes = serialize_binary_data_section( &metadata, npm_snapshot.map(|s| s.into_serialized()), + &specifier_store.for_serialization(&root_dir_url), + &redirects_store, &remote_modules_store, - &source_map_store, &vfs, - compile_flags, ) - .context("Writing binary bytes") + .context("Serializing binary data section.")?; + + write_binary_bytes(writer, original_bin, data_section_bytes, compile_flags) + .context("Writing binary bytes") } fn fill_npm_vfs(&self, builder: &mut VfsBuilder) -> Result<(), AnyError> { @@ -873,16 +720,17 @@ impl<'a> DenoCompileBinaryWriter<'a> { } } - match self.npm_resolver.as_inner() { - InnerCliNpmResolverRef::Managed(npm_resolver) => { + match &self.npm_resolver { + CliNpmResolver::Managed(npm_resolver) => { if let Some(node_modules_path) = npm_resolver.root_node_modules_path() { maybe_warn_different_system(&self.npm_system_info); builder.add_dir_recursive(node_modules_path)?; Ok(()) } else { // we'll flatten to remove any custom registries later - let mut packages = - npm_resolver.all_system_packages(&self.npm_system_info); + let mut packages = npm_resolver + .resolution() + .all_system_packages(&self.npm_system_info); packages.sort_by(|a, b| a.id.cmp(&b.id)); // determinism for package in packages { let folder = @@ -892,7 +740,7 @@ impl<'a> DenoCompileBinaryWriter<'a> { Ok(()) } } - InnerCliNpmResolverRef::Byonm(_) => { + CliNpmResolver::Byonm(_) => { maybe_warn_different_system(&self.npm_system_info); for pkg_json in self.cli_options.workspace().package_jsons() { builder.add_file_at_path(&pkg_json.path)?; @@ -935,8 +783,8 @@ impl<'a> DenoCompileBinaryWriter<'a> { &self, mut vfs: VfsBuilder, ) -> BuiltVfs { - match self.npm_resolver.as_inner() { - InnerCliNpmResolverRef::Managed(npm_resolver) => { + match &self.npm_resolver { + CliNpmResolver::Managed(npm_resolver) => { if npm_resolver.root_node_modules_path().is_some() { return vfs.build(); } @@ -984,11 +832,15 @@ impl<'a> DenoCompileBinaryWriter<'a> { // it's better to not expose the user's cache directory, so take it out // of there + let case_sensitivity = vfs.case_sensitivity(); let parent = global_cache_root_path.parent().unwrap(); let parent_dir = vfs.get_dir_mut(parent).unwrap(); let index = parent_dir .entries - .binary_search(DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME) + .binary_search( + DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME, + case_sensitivity, + ) .unwrap(); let npm_global_cache_dir_entry = parent_dir.entries.remove(index); @@ -996,9 +848,19 @@ impl<'a> DenoCompileBinaryWriter<'a> { // this is not as optimized as it could be let mut last_name = Cow::Borrowed(DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME); - for ancestor in parent.ancestors() { - let dir = vfs.get_dir_mut(ancestor).unwrap(); - if let Ok(index) = dir.entries.binary_search(&last_name) { + for ancestor in + parent.ancestors().map(Some).chain(std::iter::once(None)) + { + let dir = if let Some(ancestor) = ancestor { + vfs.get_dir_mut(ancestor).unwrap() + } else if cfg!(windows) { + vfs.get_system_root_dir_mut() + } else { + break; + }; + if let Ok(index) = + dir.entries.binary_search(&last_name, case_sensitivity) + { dir.entries.remove(index); } last_name = Cow::Owned(dir.name.clone()); @@ -1009,14 +871,156 @@ impl<'a> DenoCompileBinaryWriter<'a> { // now build the vfs and add the global cache dir entry there let mut built_vfs = vfs.build(); - built_vfs.entries.insert(npm_global_cache_dir_entry); + built_vfs + .entries + .insert(npm_global_cache_dir_entry, case_sensitivity); built_vfs } - InnerCliNpmResolverRef::Byonm(_) => vfs.build(), + CliNpmResolver::Byonm(_) => vfs.build(), } } } +#[allow(clippy::too_many_arguments)] +fn write_binary_bytes( + mut file_writer: File, + original_bin: Vec, + data_section_bytes: Vec, + compile_flags: &CompileFlags, +) -> Result<(), AnyError> { + let target = compile_flags.resolve_target(); + if target.contains("linux") { + libsui::Elf::new(&original_bin).append( + "d3n0l4nd", + &data_section_bytes, + &mut file_writer, + )?; + } else if target.contains("windows") { + let mut pe = libsui::PortableExecutable::from(&original_bin)?; + if let Some(icon) = compile_flags.icon.as_ref() { + let icon = std::fs::read(icon)?; + pe = pe.set_icon(&icon)?; + } + + pe.write_resource("d3n0l4nd", data_section_bytes)? + .build(&mut file_writer)?; + } else if target.contains("darwin") { + libsui::Macho::from(original_bin)? + .write_section("d3n0l4nd", data_section_bytes)? + .build_and_sign(&mut file_writer)?; + } + Ok(()) +} + +/// Binary format: +/// * d3n0l4nd +/// * +/// * +/// * +/// * +/// * +/// * +/// * +/// * d3n0l4nd +#[allow(clippy::too_many_arguments)] +fn serialize_binary_data_section( + metadata: &Metadata, + npm_snapshot: Option, + specifiers: &SpecifierStoreForSerialization, + redirects: &SpecifierDataStore, + remote_modules: &SpecifierDataStore>, + vfs: &BuiltVfs, +) -> Result, AnyError> { + let metadata = serde_json::to_string(metadata)?; + let npm_snapshot = + npm_snapshot.map(serialize_npm_snapshot).unwrap_or_default(); + let serialized_vfs = serde_json::to_string(&vfs.entries)?; + + let bytes = capacity_builder::BytesBuilder::build(|builder| { + builder.append(MAGIC_BYTES); + // 1. Metadata + { + builder.append_le(metadata.len() as u64); + builder.append(&metadata); + } + // 2. Npm snapshot + { + builder.append_le(npm_snapshot.len() as u64); + builder.append(&npm_snapshot); + } + // 3. Specifiers + builder.append(specifiers); + // 4. Redirects + redirects.serialize(builder); + // 5. Remote modules + remote_modules.serialize(builder); + // 6. VFS + { + builder.append_le(serialized_vfs.len() as u64); + builder.append(&serialized_vfs); + let vfs_bytes_len = vfs.files.iter().map(|f| f.len() as u64).sum::(); + builder.append_le(vfs_bytes_len); + for file in &vfs.files { + builder.append(file); + } + } + + // write the magic bytes at the end so we can use it + // to make sure we've deserialized correctly + builder.append(MAGIC_BYTES); + })?; + + Ok(bytes) +} + +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 get_denort_path(deno_exe: PathBuf) -> Option { let mut denort = deno_exe; denort.set_file_name(if cfg!(windows) { diff --git a/cli/standalone/file_system.rs b/cli/standalone/file_system.rs deleted file mode 100644 index b04db88c90..0000000000 --- a/cli/standalone/file_system.rs +++ /dev/null @@ -1,884 +0,0 @@ -// Copyright 2018-2025 the Deno authors. MIT license. - -use std::borrow::Cow; -use std::io::ErrorKind; -use std::path::Path; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; -use std::time::Duration; -use std::time::SystemTime; - -use deno_runtime::deno_fs::AccessCheckCb; -use deno_runtime::deno_fs::FileSystem; -use deno_runtime::deno_fs::FsDirEntry; -use deno_runtime::deno_fs::FsFileType; -use deno_runtime::deno_fs::OpenOptions; -use deno_runtime::deno_fs::RealFs; -use deno_runtime::deno_io::fs::File; -use deno_runtime::deno_io::fs::FsError; -use deno_runtime::deno_io::fs::FsResult; -use deno_runtime::deno_io::fs::FsStat; -use sys_traits::boxed::BoxedFsDirEntry; -use sys_traits::boxed::BoxedFsMetadataValue; -use sys_traits::boxed::FsMetadataBoxed; -use sys_traits::boxed::FsReadDirBoxed; -use sys_traits::FsCopy; -use sys_traits::FsMetadata; - -use super::virtual_fs::FileBackedVfs; -use super::virtual_fs::FileBackedVfsDirEntry; -use super::virtual_fs::FileBackedVfsFile; -use super::virtual_fs::FileBackedVfsMetadata; -use super::virtual_fs::VfsFileSubDataKind; - -#[derive(Debug, Clone)] -pub struct DenoCompileFileSystem(Arc); - -impl DenoCompileFileSystem { - pub fn new(vfs: Arc) -> Self { - Self(vfs) - } - - fn error_if_in_vfs(&self, path: &Path) -> FsResult<()> { - if self.0.is_path_within(path) { - Err(FsError::NotSupported) - } else { - Ok(()) - } - } - - fn copy_to_real_path( - &self, - oldpath: &Path, - newpath: &Path, - ) -> std::io::Result { - let old_file = self.0.file_entry(oldpath)?; - let old_file_bytes = - self.0.read_file_all(old_file, VfsFileSubDataKind::Raw)?; - let len = old_file_bytes.len() as u64; - RealFs - .write_file_sync( - newpath, - OpenOptions { - read: false, - write: true, - create: true, - truncate: true, - append: false, - create_new: false, - mode: None, - }, - None, - &old_file_bytes, - ) - .map_err(|err| err.into_io_error())?; - Ok(len) - } -} - -#[async_trait::async_trait(?Send)] -impl FileSystem for DenoCompileFileSystem { - fn cwd(&self) -> FsResult { - RealFs.cwd() - } - - fn tmp_dir(&self) -> FsResult { - RealFs.tmp_dir() - } - - fn chdir(&self, path: &Path) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.chdir(path) - } - - fn umask(&self, mask: Option) -> FsResult { - RealFs.umask(mask) - } - - fn open_sync( - &self, - path: &Path, - options: OpenOptions, - access_check: Option, - ) -> FsResult> { - if self.0.is_path_within(path) { - Ok(Rc::new(self.0.open_file(path)?)) - } else { - RealFs.open_sync(path, options, access_check) - } - } - async fn open_async<'a>( - &'a self, - path: PathBuf, - options: OpenOptions, - access_check: Option>, - ) -> FsResult> { - if self.0.is_path_within(&path) { - Ok(Rc::new(self.0.open_file(&path)?)) - } else { - RealFs.open_async(path, options, access_check).await - } - } - - fn mkdir_sync( - &self, - path: &Path, - recursive: bool, - mode: Option, - ) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.mkdir_sync(path, recursive, mode) - } - async fn mkdir_async( - &self, - path: PathBuf, - recursive: bool, - mode: Option, - ) -> FsResult<()> { - self.error_if_in_vfs(&path)?; - RealFs.mkdir_async(path, recursive, mode).await - } - - fn chmod_sync(&self, path: &Path, mode: u32) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.chmod_sync(path, mode) - } - async fn chmod_async(&self, path: PathBuf, mode: u32) -> FsResult<()> { - self.error_if_in_vfs(&path)?; - RealFs.chmod_async(path, mode).await - } - - fn chown_sync( - &self, - path: &Path, - uid: Option, - gid: Option, - ) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.chown_sync(path, uid, gid) - } - async fn chown_async( - &self, - path: PathBuf, - uid: Option, - gid: Option, - ) -> FsResult<()> { - self.error_if_in_vfs(&path)?; - RealFs.chown_async(path, uid, gid).await - } - - fn lchown_sync( - &self, - path: &Path, - uid: Option, - gid: Option, - ) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.lchown_sync(path, uid, gid) - } - - async fn lchown_async( - &self, - path: PathBuf, - uid: Option, - gid: Option, - ) -> FsResult<()> { - self.error_if_in_vfs(&path)?; - RealFs.lchown_async(path, uid, gid).await - } - - fn remove_sync(&self, path: &Path, recursive: bool) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.remove_sync(path, recursive) - } - async fn remove_async(&self, path: PathBuf, recursive: bool) -> FsResult<()> { - self.error_if_in_vfs(&path)?; - RealFs.remove_async(path, recursive).await - } - - fn copy_file_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { - self.error_if_in_vfs(newpath)?; - if self.0.is_path_within(oldpath) { - self - .copy_to_real_path(oldpath, newpath) - .map(|_| ()) - .map_err(FsError::Io) - } else { - RealFs.copy_file_sync(oldpath, newpath) - } - } - async fn copy_file_async( - &self, - oldpath: PathBuf, - newpath: PathBuf, - ) -> FsResult<()> { - self.error_if_in_vfs(&newpath)?; - if self.0.is_path_within(&oldpath) { - let fs = self.clone(); - tokio::task::spawn_blocking(move || { - fs.copy_to_real_path(&oldpath, &newpath) - .map(|_| ()) - .map_err(FsError::Io) - }) - .await? - } else { - RealFs.copy_file_async(oldpath, newpath).await - } - } - - fn cp_sync(&self, from: &Path, to: &Path) -> FsResult<()> { - self.error_if_in_vfs(to)?; - - RealFs.cp_sync(from, to) - } - async fn cp_async(&self, from: PathBuf, to: PathBuf) -> FsResult<()> { - self.error_if_in_vfs(&to)?; - - RealFs.cp_async(from, to).await - } - - fn stat_sync(&self, path: &Path) -> FsResult { - if self.0.is_path_within(path) { - Ok(self.0.stat(path)?.as_fs_stat()) - } else { - RealFs.stat_sync(path) - } - } - async fn stat_async(&self, path: PathBuf) -> FsResult { - if self.0.is_path_within(&path) { - Ok(self.0.stat(&path)?.as_fs_stat()) - } else { - RealFs.stat_async(path).await - } - } - - fn lstat_sync(&self, path: &Path) -> FsResult { - if self.0.is_path_within(path) { - Ok(self.0.lstat(path)?.as_fs_stat()) - } else { - RealFs.lstat_sync(path) - } - } - async fn lstat_async(&self, path: PathBuf) -> FsResult { - if self.0.is_path_within(&path) { - Ok(self.0.lstat(&path)?.as_fs_stat()) - } else { - RealFs.lstat_async(path).await - } - } - - fn realpath_sync(&self, path: &Path) -> FsResult { - if self.0.is_path_within(path) { - Ok(self.0.canonicalize(path)?) - } else { - RealFs.realpath_sync(path) - } - } - async fn realpath_async(&self, path: PathBuf) -> FsResult { - if self.0.is_path_within(&path) { - Ok(self.0.canonicalize(&path)?) - } else { - RealFs.realpath_async(path).await - } - } - - fn read_dir_sync(&self, path: &Path) -> FsResult> { - if self.0.is_path_within(path) { - Ok(self.0.read_dir(path)?) - } else { - RealFs.read_dir_sync(path) - } - } - async fn read_dir_async(&self, path: PathBuf) -> FsResult> { - if self.0.is_path_within(&path) { - Ok(self.0.read_dir(&path)?) - } else { - RealFs.read_dir_async(path).await - } - } - - fn rename_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { - self.error_if_in_vfs(oldpath)?; - self.error_if_in_vfs(newpath)?; - RealFs.rename_sync(oldpath, newpath) - } - async fn rename_async( - &self, - oldpath: PathBuf, - newpath: PathBuf, - ) -> FsResult<()> { - self.error_if_in_vfs(&oldpath)?; - self.error_if_in_vfs(&newpath)?; - RealFs.rename_async(oldpath, newpath).await - } - - fn link_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { - self.error_if_in_vfs(oldpath)?; - self.error_if_in_vfs(newpath)?; - RealFs.link_sync(oldpath, newpath) - } - async fn link_async( - &self, - oldpath: PathBuf, - newpath: PathBuf, - ) -> FsResult<()> { - self.error_if_in_vfs(&oldpath)?; - self.error_if_in_vfs(&newpath)?; - RealFs.link_async(oldpath, newpath).await - } - - fn symlink_sync( - &self, - oldpath: &Path, - newpath: &Path, - file_type: Option, - ) -> FsResult<()> { - self.error_if_in_vfs(oldpath)?; - self.error_if_in_vfs(newpath)?; - RealFs.symlink_sync(oldpath, newpath, file_type) - } - async fn symlink_async( - &self, - oldpath: PathBuf, - newpath: PathBuf, - file_type: Option, - ) -> FsResult<()> { - self.error_if_in_vfs(&oldpath)?; - self.error_if_in_vfs(&newpath)?; - RealFs.symlink_async(oldpath, newpath, file_type).await - } - - fn read_link_sync(&self, path: &Path) -> FsResult { - if self.0.is_path_within(path) { - Ok(self.0.read_link(path)?) - } else { - RealFs.read_link_sync(path) - } - } - async fn read_link_async(&self, path: PathBuf) -> FsResult { - if self.0.is_path_within(&path) { - Ok(self.0.read_link(&path)?) - } else { - RealFs.read_link_async(path).await - } - } - - fn truncate_sync(&self, path: &Path, len: u64) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.truncate_sync(path, len) - } - async fn truncate_async(&self, path: PathBuf, len: u64) -> FsResult<()> { - self.error_if_in_vfs(&path)?; - RealFs.truncate_async(path, len).await - } - - fn utime_sync( - &self, - path: &Path, - atime_secs: i64, - atime_nanos: u32, - mtime_secs: i64, - mtime_nanos: u32, - ) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.utime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) - } - async fn utime_async( - &self, - path: PathBuf, - atime_secs: i64, - atime_nanos: u32, - mtime_secs: i64, - mtime_nanos: u32, - ) -> FsResult<()> { - self.error_if_in_vfs(&path)?; - RealFs - .utime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) - .await - } - - fn lutime_sync( - &self, - path: &Path, - atime_secs: i64, - atime_nanos: u32, - mtime_secs: i64, - mtime_nanos: u32, - ) -> FsResult<()> { - self.error_if_in_vfs(path)?; - RealFs.lutime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) - } - async fn lutime_async( - &self, - path: PathBuf, - atime_secs: i64, - atime_nanos: u32, - mtime_secs: i64, - mtime_nanos: u32, - ) -> FsResult<()> { - self.error_if_in_vfs(&path)?; - RealFs - .lutime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) - .await - } -} - -impl sys_traits::BaseFsHardLink for DenoCompileFileSystem { - #[inline] - fn base_fs_hard_link(&self, src: &Path, dst: &Path) -> std::io::Result<()> { - self.link_sync(src, dst).map_err(|err| err.into_io_error()) - } -} - -impl sys_traits::BaseFsRead for DenoCompileFileSystem { - #[inline] - fn base_fs_read(&self, path: &Path) -> std::io::Result> { - self - .read_file_sync(path, None) - .map_err(|err| err.into_io_error()) - } -} - -impl sys_traits::FsMetadataValue for FileBackedVfsMetadata { - fn file_type(&self) -> sys_traits::FileType { - self.file_type - } - - fn len(&self) -> u64 { - self.len - } - - fn accessed(&self) -> std::io::Result { - Err(not_supported("accessed time")) - } - - fn created(&self) -> std::io::Result { - Err(not_supported("created time")) - } - - fn changed(&self) -> std::io::Result { - Err(not_supported("changed time")) - } - - fn modified(&self) -> std::io::Result { - Err(not_supported("modified time")) - } - - fn dev(&self) -> std::io::Result { - Ok(0) - } - - fn ino(&self) -> std::io::Result { - Ok(0) - } - - fn mode(&self) -> std::io::Result { - Ok(0) - } - - fn nlink(&self) -> std::io::Result { - Ok(0) - } - - fn uid(&self) -> std::io::Result { - Ok(0) - } - - fn gid(&self) -> std::io::Result { - Ok(0) - } - - fn rdev(&self) -> std::io::Result { - Ok(0) - } - - fn blksize(&self) -> std::io::Result { - Ok(0) - } - - fn blocks(&self) -> std::io::Result { - Ok(0) - } - - fn is_block_device(&self) -> std::io::Result { - Ok(false) - } - - fn is_char_device(&self) -> std::io::Result { - Ok(false) - } - - fn is_fifo(&self) -> std::io::Result { - Ok(false) - } - - fn is_socket(&self) -> std::io::Result { - Ok(false) - } - - fn file_attributes(&self) -> std::io::Result { - Ok(0) - } -} - -fn not_supported(name: &str) -> std::io::Error { - std::io::Error::new( - ErrorKind::Unsupported, - format!( - "{} is not supported for an embedded deno compile file", - name - ), - ) -} - -impl sys_traits::FsDirEntry for FileBackedVfsDirEntry { - type Metadata = BoxedFsMetadataValue; - - fn file_name(&self) -> Cow { - Cow::Borrowed(self.metadata.name.as_ref()) - } - - fn file_type(&self) -> std::io::Result { - Ok(self.metadata.file_type) - } - - fn metadata(&self) -> std::io::Result { - Ok(BoxedFsMetadataValue(Box::new(self.metadata.clone()))) - } - - fn path(&self) -> Cow { - Cow::Owned(self.parent_path.join(&self.metadata.name)) - } -} - -impl sys_traits::BaseFsReadDir for DenoCompileFileSystem { - type ReadDirEntry = BoxedFsDirEntry; - - fn base_fs_read_dir( - &self, - path: &Path, - ) -> std::io::Result< - Box> + '_>, - > { - if self.0.is_path_within(path) { - let entries = self.0.read_dir_with_metadata(path)?; - Ok(Box::new( - entries.map(|entry| Ok(BoxedFsDirEntry::new(entry))), - )) - } else { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.fs_read_dir_boxed(path) - } - } -} - -impl sys_traits::BaseFsCanonicalize for DenoCompileFileSystem { - #[inline] - fn base_fs_canonicalize(&self, path: &Path) -> std::io::Result { - self.realpath_sync(path).map_err(|err| err.into_io_error()) - } -} - -impl sys_traits::BaseFsMetadata for DenoCompileFileSystem { - type Metadata = BoxedFsMetadataValue; - - #[inline] - fn base_fs_metadata(&self, path: &Path) -> std::io::Result { - if self.0.is_path_within(path) { - Ok(BoxedFsMetadataValue::new(self.0.stat(path)?)) - } else { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.fs_metadata_boxed(path) - } - } - - #[inline] - fn base_fs_symlink_metadata( - &self, - path: &Path, - ) -> std::io::Result { - if self.0.is_path_within(path) { - Ok(BoxedFsMetadataValue::new(self.0.lstat(path)?)) - } else { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.fs_symlink_metadata_boxed(path) - } - } -} - -impl sys_traits::BaseFsCopy for DenoCompileFileSystem { - #[inline] - fn base_fs_copy(&self, from: &Path, to: &Path) -> std::io::Result { - self - .error_if_in_vfs(to) - .map_err(|err| err.into_io_error())?; - if self.0.is_path_within(from) { - self.copy_to_real_path(from, to) - } else { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.fs_copy(from, to) - } - } -} - -impl sys_traits::BaseFsCloneFile for DenoCompileFileSystem { - fn base_fs_clone_file( - &self, - _from: &Path, - _to: &Path, - ) -> std::io::Result<()> { - // will cause a fallback in the code that uses this - Err(not_supported("cloning files")) - } -} - -impl sys_traits::BaseFsCreateDir for DenoCompileFileSystem { - #[inline] - fn base_fs_create_dir( - &self, - path: &Path, - options: &sys_traits::CreateDirOptions, - ) -> std::io::Result<()> { - self - .mkdir_sync(path, options.recursive, options.mode) - .map_err(|err| err.into_io_error()) - } -} - -impl sys_traits::BaseFsRemoveFile for DenoCompileFileSystem { - #[inline] - fn base_fs_remove_file(&self, path: &Path) -> std::io::Result<()> { - self - .remove_sync(path, false) - .map_err(|err| err.into_io_error()) - } -} - -impl sys_traits::BaseFsRename for DenoCompileFileSystem { - #[inline] - fn base_fs_rename(&self, from: &Path, to: &Path) -> std::io::Result<()> { - self - .rename_sync(from, to) - .map_err(|err| err.into_io_error()) - } -} - -pub enum FsFileAdapter { - Real(sys_traits::impls::RealFsFile), - Vfs(FileBackedVfsFile), -} - -impl sys_traits::FsFile for FsFileAdapter {} - -impl sys_traits::FsFileAsRaw for FsFileAdapter { - #[cfg(windows)] - fn fs_file_as_raw_handle(&self) -> Option { - match self { - Self::Real(file) => file.fs_file_as_raw_handle(), - Self::Vfs(_) => None, - } - } - - #[cfg(unix)] - fn fs_file_as_raw_fd(&self) -> Option { - match self { - Self::Real(file) => file.fs_file_as_raw_fd(), - Self::Vfs(_) => None, - } - } -} - -impl sys_traits::FsFileSyncData for FsFileAdapter { - fn fs_file_sync_data(&mut self) -> std::io::Result<()> { - match self { - Self::Real(file) => file.fs_file_sync_data(), - Self::Vfs(_) => Ok(()), - } - } -} - -impl sys_traits::FsFileSyncAll for FsFileAdapter { - fn fs_file_sync_all(&mut self) -> std::io::Result<()> { - match self { - Self::Real(file) => file.fs_file_sync_all(), - Self::Vfs(_) => Ok(()), - } - } -} - -impl sys_traits::FsFileSetPermissions for FsFileAdapter { - #[inline] - fn fs_file_set_permissions(&mut self, mode: u32) -> std::io::Result<()> { - match self { - Self::Real(file) => file.fs_file_set_permissions(mode), - Self::Vfs(_) => Ok(()), - } - } -} - -impl std::io::Read for FsFileAdapter { - #[inline] - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - match self { - Self::Real(file) => file.read(buf), - Self::Vfs(file) => file.read_to_buf(buf), - } - } -} - -impl std::io::Seek for FsFileAdapter { - fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { - match self { - Self::Real(file) => file.seek(pos), - Self::Vfs(file) => file.seek(pos), - } - } -} - -impl std::io::Write for FsFileAdapter { - #[inline] - fn write(&mut self, buf: &[u8]) -> std::io::Result { - match self { - Self::Real(file) => file.write(buf), - Self::Vfs(_) => Err(not_supported("writing files")), - } - } - - #[inline] - fn flush(&mut self) -> std::io::Result<()> { - match self { - Self::Real(file) => file.flush(), - Self::Vfs(_) => Err(not_supported("writing files")), - } - } -} - -impl sys_traits::FsFileSetLen for FsFileAdapter { - #[inline] - fn fs_file_set_len(&mut self, len: u64) -> std::io::Result<()> { - match self { - Self::Real(file) => file.fs_file_set_len(len), - Self::Vfs(_) => Err(not_supported("setting file length")), - } - } -} - -impl sys_traits::FsFileSetTimes for FsFileAdapter { - fn fs_file_set_times( - &mut self, - times: sys_traits::FsFileTimes, - ) -> std::io::Result<()> { - match self { - Self::Real(file) => file.fs_file_set_times(times), - Self::Vfs(_) => Err(not_supported("setting file times")), - } - } -} - -impl sys_traits::FsFileLock for FsFileAdapter { - fn fs_file_lock( - &mut self, - mode: sys_traits::FsFileLockMode, - ) -> std::io::Result<()> { - match self { - Self::Real(file) => file.fs_file_lock(mode), - Self::Vfs(_) => Err(not_supported("locking files")), - } - } - - fn fs_file_try_lock( - &mut self, - mode: sys_traits::FsFileLockMode, - ) -> std::io::Result<()> { - match self { - Self::Real(file) => file.fs_file_try_lock(mode), - Self::Vfs(_) => Err(not_supported("locking files")), - } - } - - fn fs_file_unlock(&mut self) -> std::io::Result<()> { - match self { - Self::Real(file) => file.fs_file_unlock(), - Self::Vfs(_) => Err(not_supported("unlocking files")), - } - } -} - -impl sys_traits::FsFileIsTerminal for FsFileAdapter { - #[inline] - fn fs_file_is_terminal(&self) -> bool { - match self { - Self::Real(file) => file.fs_file_is_terminal(), - Self::Vfs(_) => false, - } - } -} - -impl sys_traits::BaseFsOpen for DenoCompileFileSystem { - type File = FsFileAdapter; - - fn base_fs_open( - &self, - path: &Path, - options: &sys_traits::OpenOptions, - ) -> std::io::Result { - if self.0.is_path_within(path) { - Ok(FsFileAdapter::Vfs(self.0.open_file(path)?)) - } else { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - Ok(FsFileAdapter::Real( - sys_traits::impls::RealSys.base_fs_open(path, options)?, - )) - } - } -} - -impl sys_traits::BaseFsSymlinkDir for DenoCompileFileSystem { - fn base_fs_symlink_dir(&self, src: &Path, dst: &Path) -> std::io::Result<()> { - self - .symlink_sync(src, dst, Some(FsFileType::Directory)) - .map_err(|err| err.into_io_error()) - } -} - -impl sys_traits::SystemRandom for DenoCompileFileSystem { - #[inline] - fn sys_random(&self, buf: &mut [u8]) -> std::io::Result<()> { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.sys_random(buf) - } -} - -impl sys_traits::SystemTimeNow for DenoCompileFileSystem { - #[inline] - fn sys_time_now(&self) -> SystemTime { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.sys_time_now() - } -} - -impl sys_traits::ThreadSleep for DenoCompileFileSystem { - #[inline] - fn thread_sleep(&self, dur: Duration) { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.thread_sleep(dur) - } -} - -impl sys_traits::EnvCurrentDir for DenoCompileFileSystem { - fn env_current_dir(&self) -> std::io::Result { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.env_current_dir() - } -} - -impl sys_traits::BaseEnvVar for DenoCompileFileSystem { - fn base_env_var_os( - &self, - key: &std::ffi::OsStr, - ) -> Option { - #[allow(clippy::disallowed_types)] // ok because we're implementing the fs - sys_traits::impls::RealSys.base_env_var_os(key) - } -} diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 961e8d6b51..81ca2b4ff1 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -1,1047 +1,4 @@ // Copyright 2018-2025 the Deno authors. MIT license. -// Allow unused code warnings because we share -// code between the two bin targets. -#![allow(dead_code)] -#![allow(unused_imports)] - -use std::borrow::Cow; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; - -use binary::StandaloneData; -use binary::StandaloneModules; -use code_cache::DenoCompileCodeCache; -use deno_ast::MediaType; -use deno_cache_dir::file_fetcher::CacheSetting; -use deno_cache_dir::npm::NpmCacheDir; -use deno_config::workspace::MappedResolution; -use deno_config::workspace::MappedResolutionError; -use deno_config::workspace::ResolverWorkspaceJsrPackage; -use deno_config::workspace::WorkspaceResolver; -use deno_core::anyhow::Context; -use deno_core::error::AnyError; -use deno_core::error::ModuleLoaderError; -use deno_core::futures::future::LocalBoxFuture; -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; -use deno_core::ModuleSpecifier; -use deno_core::ModuleType; -use deno_core::RequestedModuleType; -use deno_core::ResolutionKind; -use deno_core::SourceCodeCacheInfo; -use deno_error::JsErrorBox; -use deno_npm::npm_rc::ResolvedNpmRc; -use deno_package_json::PackageJsonDepValue; -use deno_resolver::cjs::IsCjsResolutionMode; -use deno_resolver::npm::create_in_npm_pkg_checker; -use deno_resolver::npm::managed::ManagedInNpmPkgCheckerCreateOptions; -use deno_resolver::npm::CreateInNpmPkgCheckerOptions; -use deno_resolver::npm::NpmReqResolverOptions; -use deno_runtime::deno_fs; -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_node::RealIsBuiltInNodeModuleChecker; -use deno_runtime::deno_permissions::Permissions; -use deno_runtime::deno_permissions::PermissionsContainer; -use deno_runtime::deno_tls::rustls::RootCertStore; -use deno_runtime::deno_tls::RootCertStoreProvider; -use deno_runtime::deno_web::BlobStore; -use deno_runtime::permissions::RuntimePermissionDescriptorParser; -use deno_runtime::WorkerExecutionMode; -use deno_runtime::WorkerLogLevel; -use deno_semver::npm::NpmPackageReqReference; -use import_map::parse_from_json; -use node_resolver::analyze::NodeCodeTranslator; -use node_resolver::errors::ClosestPkgJsonError; -use node_resolver::NodeResolutionKind; -use node_resolver::ResolutionMode; -use serialization::DenoCompileModuleSource; -use serialization::SourceMapStore; -use virtual_fs::FileBackedVfs; -use virtual_fs::VfsFileSubDataKind; - -use crate::args::create_default_npmrc; -use crate::args::get_root_cert_store; -use crate::args::npm_pkg_req_ref_to_binary_command; -use crate::args::CaData; -use crate::args::NpmInstallDepsProvider; -use crate::args::StorageKeyResolver; -use crate::cache::Caches; -use crate::cache::DenoDirProvider; -use crate::cache::FastInsecureHasher; -use crate::cache::NodeAnalysisCache; -use crate::http_util::HttpClientProvider; -use crate::node::CliCjsCodeAnalyzer; -use crate::node::CliNodeCodeTranslator; -use crate::node::CliNodeResolver; -use crate::node::CliPackageJsonResolver; -use crate::npm::create_cli_npm_resolver; -use crate::npm::CliByonmNpmResolverCreateOptions; -use crate::npm::CliManagedNpmResolverCreateOptions; -use crate::npm::CliNpmResolver; -use crate::npm::CliNpmResolverCreateOptions; -use crate::npm::CliNpmResolverManagedSnapshotOption; -use crate::npm::NpmRegistryReadPermissionChecker; -use crate::npm::NpmRegistryReadPermissionCheckerMode; -use crate::resolver::CjsTracker; -use crate::resolver::CliNpmReqResolver; -use crate::resolver::NpmModuleLoader; -use crate::sys::CliSys; -use crate::util::progress_bar::ProgressBar; -use crate::util::progress_bar::ProgressBarStyle; -use crate::util::text_encoding::from_utf8_lossy_cow; -use crate::util::v8::construct_v8_flags; -use crate::worker::CliCodeCache; -use crate::worker::CliMainWorkerFactory; -use crate::worker::CliMainWorkerOptions; -use crate::worker::CreateModuleLoaderResult; -use crate::worker::ModuleLoaderFactory; - pub mod binary; -mod code_cache; -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::Metadata; -pub use self::file_system::DenoCompileFileSystem; - -struct SharedModuleLoaderState { - cjs_tracker: Arc, - code_cache: Option>, - fs: Arc, - modules: StandaloneModules, - node_code_translator: Arc, - node_resolver: Arc, - npm_module_loader: Arc, - npm_registry_permission_checker: NpmRegistryReadPermissionChecker, - npm_req_resolver: Arc, - npm_resolver: Arc, - source_maps: SourceMapStore, - vfs: Arc, - workspace_resolver: WorkspaceResolver, -} - -impl SharedModuleLoaderState { - fn get_code_cache( - &self, - specifier: &ModuleSpecifier, - source: &[u8], - ) -> Option { - let Some(code_cache) = &self.code_cache else { - return None; - }; - if !code_cache.enabled() { - return None; - } - // deno version is already included in the root cache key - let hash = FastInsecureHasher::new_without_deno_version() - .write_hashable(source) - .finish(); - let data = code_cache.get_sync( - specifier, - deno_runtime::code_cache::CodeCacheType::EsModule, - hash, - ); - Some(SourceCodeCacheInfo { - hash, - data: data.map(Cow::Owned), - }) - } -} - -#[derive(Clone)] -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"; - -impl ModuleLoader for EmbeddedModuleLoader { - fn resolve( - &self, - raw_specifier: &str, - referrer: &str, - kind: ResolutionKind, - ) -> Result { - let referrer = if referrer == "." { - if kind != ResolutionKind::MainModule { - return Err( - JsErrorBox::generic(format!( - "Expected to resolve main module, got {:?} instead.", - kind - )) - .into(), - ); - } - let current_dir = std::env::current_dir().unwrap(); - deno_core::resolve_path(".", ¤t_dir)? - } else { - ModuleSpecifier::parse(referrer).map_err(|err| { - JsErrorBox::type_error(format!( - "Referrer uses invalid specifier: {}", - err - )) - })? - }; - let referrer_kind = if self - .shared - .cjs_tracker - .is_maybe_cjs(&referrer, MediaType::from_specifier(&referrer)) - .map_err(JsErrorBox::from_err)? - { - ResolutionMode::Require - } else { - ResolutionMode::Import - }; - - if self.shared.node_resolver.in_npm_package(&referrer) { - return Ok( - self - .shared - .node_resolver - .resolve( - raw_specifier, - &referrer, - referrer_kind, - NodeResolutionKind::Execution, - ) - .map_err(JsErrorBox::from_err)? - .into_url(), - ); - } - - let mapped_resolution = self - .shared - .workspace_resolver - .resolve(raw_specifier, &referrer); - - match mapped_resolution { - Ok(MappedResolution::WorkspaceJsrPackage { specifier, .. }) => { - Ok(specifier) - } - Ok(MappedResolution::WorkspaceNpmPackage { - target_pkg_json: pkg_json, - sub_path, - .. - }) => Ok( - self - .shared - .node_resolver - .resolve_package_subpath_from_deno_module( - pkg_json.dir_path(), - sub_path.as_deref(), - Some(&referrer), - referrer_kind, - NodeResolutionKind::Execution, - ) - .map_err(JsErrorBox::from_err)?, - ), - Ok(MappedResolution::PackageJson { - dep_result, - sub_path, - alias, - .. - }) => match dep_result - .as_ref() - .map_err(|e| JsErrorBox::from_err(e.clone()))? - { - PackageJsonDepValue::Req(req) => self - .shared - .npm_req_resolver - .resolve_req_with_sub_path( - req, - sub_path.as_deref(), - &referrer, - referrer_kind, - NodeResolutionKind::Execution, - ) - .map_err(|e| JsErrorBox::from_err(e).into()), - PackageJsonDepValue::Workspace(version_req) => { - let pkg_folder = self - .shared - .workspace_resolver - .resolve_workspace_pkg_json_folder_for_pkg_json_dep( - alias, - version_req, - ) - .map_err(JsErrorBox::from_err)?; - Ok( - self - .shared - .node_resolver - .resolve_package_subpath_from_deno_module( - pkg_folder, - sub_path.as_deref(), - Some(&referrer), - referrer_kind, - NodeResolutionKind::Execution, - ) - .map_err(JsErrorBox::from_err)?, - ) - } - }, - Ok(MappedResolution::Normal { specifier, .. }) - | Ok(MappedResolution::ImportMap { specifier, .. }) => { - if let Ok(reference) = - NpmPackageReqReference::from_specifier(&specifier) - { - return Ok( - self - .shared - .npm_req_resolver - .resolve_req_reference( - &reference, - &referrer, - referrer_kind, - NodeResolutionKind::Execution, - ) - .map_err(JsErrorBox::from_err)?, - ); - } - - if specifier.scheme() == "jsr" { - if let Some(specifier) = - self.shared.modules.resolve_specifier(&specifier)? - { - return Ok(specifier.clone()); - } - } - - Ok( - self - .shared - .node_resolver - .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 - .npm_req_resolver - .resolve_if_for_npm_pkg( - raw_specifier, - &referrer, - referrer_kind, - NodeResolutionKind::Execution, - ) - .map_err(JsErrorBox::from_err)?; - if let Some(res) = maybe_res { - return Ok(res.into_url()); - } - Err(JsErrorBox::from_err(err).into()) - } - Err(err) => Err(JsErrorBox::from_err(err).into()), - } - } - - fn get_host_defined_options<'s>( - &self, - scope: &mut deno_core::v8::HandleScope<'s>, - name: &str, - ) -> Option> { - let name = deno_core::ModuleSpecifier::parse(name).ok()?; - if self.shared.node_resolver.in_npm_package(&name) { - Some(create_host_defined_options(scope)) - } else { - None - } - } - - fn load( - &self, - original_specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - _is_dynamic: bool, - _requested_module_type: RequestedModuleType, - ) -> deno_core::ModuleLoadResponse { - if original_specifier.scheme() == "data" { - let data_url_text = - match deno_graph::source::RawDataUrl::parse(original_specifier) - .and_then(|url| url.decode()) - { - Ok(response) => response, - Err(err) => { - return deno_core::ModuleLoadResponse::Sync(Err( - JsErrorBox::type_error(format!("{:#}", err)).into(), - )); - } - }; - return deno_core::ModuleLoadResponse::Sync(Ok( - deno_core::ModuleSource::new( - deno_core::ModuleType::JavaScript, - ModuleSourceCode::String(data_url_text.into()), - original_specifier, - None, - ), - )); - } - - if self.shared.node_resolver.in_npm_package(original_specifier) { - let shared = self.shared.clone(); - let original_specifier = original_specifier.clone(); - let maybe_referrer = maybe_referrer.cloned(); - return deno_core::ModuleLoadResponse::Async( - async move { - let code_source = shared - .npm_module_loader - .load(&original_specifier, maybe_referrer.as_ref()) - .await?; - let code_cache_entry = shared.get_code_cache( - &code_source.found_url, - code_source.code.as_bytes(), - ); - Ok(deno_core::ModuleSource::new_with_redirect( - match code_source.media_type { - MediaType::Json => ModuleType::Json, - _ => ModuleType::JavaScript, - }, - code_source.code, - &original_specifier, - &code_source.found_url, - code_cache_entry, - )) - } - .boxed_local(), - ); - } - - match self - .shared - .modules - .read(original_specifier, VfsFileSubDataKind::ModuleGraph) - { - 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( - JsErrorBox::type_error(format!("{:?}", err)).into(), - )); - } - }; - 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)) - } - }; - let code_cache_entry = shared - .get_code_cache(&module_specifier, module_source.as_bytes()); - Ok(deno_core::ModuleSource::new_with_redirect( - module_type, - module_source, - &original_specifier, - &module_specifier, - code_cache_entry, - )) - } - .boxed_local(), - ) - } else { - let module_source = module_source.into_for_v8(); - let code_cache_entry = self - .shared - .get_code_cache(module_specifier, module_source.as_bytes()); - deno_core::ModuleLoadResponse::Sync(Ok( - deno_core::ModuleSource::new_with_redirect( - module_type, - module_source, - original_specifier, - module_specifier, - code_cache_entry, - ), - )) - } - } - Ok(None) => deno_core::ModuleLoadResponse::Sync(Err( - JsErrorBox::type_error(format!( - "{MODULE_NOT_FOUND}: {}", - original_specifier - )) - .into(), - )), - Err(err) => deno_core::ModuleLoadResponse::Sync(Err( - JsErrorBox::type_error(format!("{:?}", err)).into(), - )), - } - } - - fn code_cache_ready( - &self, - specifier: ModuleSpecifier, - source_hash: u64, - code_cache_data: &[u8], - ) -> LocalBoxFuture<'static, ()> { - if let Some(code_cache) = &self.shared.code_cache { - code_cache.set_sync( - specifier, - deno_runtime::code_cache::CodeCacheType::EsModule, - source_hash, - code_cache_data, - ); - } - std::future::ready(()).boxed_local() - } - - fn get_source_map(&self, file_name: &str) -> Option> { - if file_name.starts_with("file:///") { - let url = - deno_path_util::url_from_directory_path(self.shared.vfs.root()).ok()?; - let file_url = ModuleSpecifier::parse(file_name).ok()?; - let relative_path = url.make_relative(&file_url)?; - self.shared.source_maps.get(&relative_path) - } else { - self.shared.source_maps.get(file_name) - } - .map(Cow::Borrowed) - } - - fn get_source_mapped_source_line( - &self, - file_name: &str, - line_number: usize, - ) -> Option { - let specifier = ModuleSpecifier::parse(file_name).ok()?; - let data = self - .shared - .modules - .read(&specifier, VfsFileSubDataKind::Raw) - .ok()??; - - let source = String::from_utf8_lossy(&data.data); - // Do NOT use .lines(): it skips the terminating empty line. - // (due to internally using_terminator() instead of .split()) - let lines: Vec<&str> = source.split('\n').collect(); - if line_number >= lines.len() { - Some(format!( - "{} Couldn't format source line: Line {} is out of bounds (source may have changed at runtime)", - crate::colors::yellow("Warning"), line_number + 1, - )) - } else { - Some(lines[line_number].to_string()) - } - } -} - -impl NodeRequireLoader for EmbeddedModuleLoader { - fn ensure_read_permission<'a>( - &self, - permissions: &mut dyn deno_runtime::deno_node::NodePermissions, - path: &'a std::path::Path, - ) -> Result, JsErrorBox> { - if self.shared.modules.has_file(path) { - // allow reading if the file is in the snapshot - return Ok(Cow::Borrowed(path)); - } - - self - .shared - .npm_registry_permission_checker - .ensure_read_permission(permissions, path) - .map_err(JsErrorBox::from_err) - } - - fn load_text_file_lossy( - &self, - path: &std::path::Path, - ) -> Result, JsErrorBox> { - let file_entry = self - .shared - .vfs - .file_entry(path) - .map_err(JsErrorBox::from_err)?; - let file_bytes = self - .shared - .vfs - .read_file_all(file_entry, VfsFileSubDataKind::ModuleGraph) - .map_err(JsErrorBox::from_err)?; - Ok(from_utf8_lossy_cow(file_bytes)) - } - - 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, - ) -> CreateModuleLoaderResult { - self.create_result() - } - - fn create_for_worker( - &self, - _parent_permissions: PermissionsContainer, - _permissions: PermissionsContainer, - ) -> CreateModuleLoaderResult { - self.create_result() - } -} - -struct StandaloneRootCertStoreProvider { - ca_stores: Option>, - ca_data: Option, - cell: once_cell::sync::OnceCell, -} - -impl RootCertStoreProvider for StandaloneRootCertStoreProvider { - fn get_or_try_init(&self) -> Result<&RootCertStore, JsErrorBox> { - self.cell.get_or_try_init(|| { - get_root_cert_store(None, self.ca_stores.clone(), self.ca_data.clone()) - .map_err(JsErrorBox::from_err) - }) - } -} - -pub async fn run( - fs: Arc, - sys: CliSys, - data: StandaloneData, -) -> Result { - let StandaloneData { - metadata, - modules, - npm_snapshot, - root_path, - source_maps, - vfs, - } = data; - let deno_dir_provider = Arc::new(DenoDirProvider::new(sys.clone(), None)); - let root_cert_store_provider = Arc::new(StandaloneRootCertStoreProvider { - ca_stores: metadata.ca_stores, - ca_data: metadata.ca_data.map(CaData::Bytes), - cell: Default::default(), - }); - let progress_bar = ProgressBar::new(ProgressBarStyle::TextOnly); - let http_client_provider = Arc::new(HttpClientProvider::new( - Some(root_cert_store_provider.clone()), - metadata.unsafely_ignore_certificate_errors.clone(), - )); - // use a dummy npm registry url - let npm_registry_url = ModuleSpecifier::parse("https://localhost/").unwrap(); - let root_dir_url = - Arc::new(ModuleSpecifier::from_directory_path(&root_path).unwrap()); - let main_module = root_dir_url.join(&metadata.entrypoint_key).unwrap(); - let npm_global_cache_dir = root_path.join(".deno_compile_node_modules"); - let cache_setting = CacheSetting::Only; - let pkg_json_resolver = Arc::new(CliPackageJsonResolver::new(sys.clone())); - let npm_registry_permission_checker = { - let mode = match &metadata.node_modules { - Some(binary::NodeModules::Managed { - node_modules_dir: Some(path), - }) => NpmRegistryReadPermissionCheckerMode::Local(PathBuf::from(path)), - Some(binary::NodeModules::Byonm { .. }) => { - NpmRegistryReadPermissionCheckerMode::Byonm - } - Some(binary::NodeModules::Managed { - node_modules_dir: None, - }) - | None => NpmRegistryReadPermissionCheckerMode::Global( - npm_global_cache_dir.clone(), - ), - }; - NpmRegistryReadPermissionChecker::new(sys.clone(), mode) - }; - let (in_npm_pkg_checker, npm_resolver) = match metadata.node_modules { - Some(binary::NodeModules::Managed { node_modules_dir }) => { - // 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( - &sys, - 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| root_path.join(node_modules_dir)); - let in_npm_pkg_checker = - create_in_npm_pkg_checker(CreateInNpmPkgCheckerOptions::Managed( - ManagedInNpmPkgCheckerCreateOptions { - 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( - CliManagedNpmResolverCreateOptions { - snapshot: CliNpmResolverManagedSnapshotOption::Specified(Some( - snapshot, - )), - maybe_lockfile: None, - http_client_provider: http_client_provider.clone(), - npm_cache_dir, - npm_install_deps_provider: Arc::new( - // this is only used for installing packages, which isn't necessary with deno compile - NpmInstallDepsProvider::empty(), - ), - sys: sys.clone(), - text_only_progress_bar: progress_bar, - cache_setting, - maybe_node_modules_path, - npm_system_info: Default::default(), - npmrc, - lifecycle_scripts: Default::default(), - }, - )) - .await?; - (in_npm_pkg_checker, npm_resolver) - } - Some(binary::NodeModules::Byonm { - root_node_modules_dir, - }) => { - let root_node_modules_dir = - root_node_modules_dir.map(|p| vfs.root().join(p)); - let in_npm_pkg_checker = - create_in_npm_pkg_checker(CreateInNpmPkgCheckerOptions::Byonm); - let npm_resolver = create_cli_npm_resolver( - CliNpmResolverCreateOptions::Byonm(CliByonmNpmResolverCreateOptions { - sys: sys.clone(), - pkg_json_resolver: pkg_json_resolver.clone(), - root_node_modules_dir, - }), - ) - .await?; - (in_npm_pkg_checker, npm_resolver) - } - None => { - // 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( - &sys, - npm_global_cache_dir, - npmrc.get_all_known_registries_urls(), - )); - let in_npm_pkg_checker = - create_in_npm_pkg_checker(CreateInNpmPkgCheckerOptions::Managed( - ManagedInNpmPkgCheckerCreateOptions { - root_cache_dir_url: npm_cache_dir.root_dir_url(), - maybe_node_modules_path: None, - }, - )); - let npm_resolver = - create_cli_npm_resolver(CliNpmResolverCreateOptions::Managed( - CliManagedNpmResolverCreateOptions { - snapshot: CliNpmResolverManagedSnapshotOption::Specified(None), - http_client_provider: http_client_provider.clone(), - npm_install_deps_provider: Arc::new( - // this is only used for installing packages, which isn't necessary with deno compile - NpmInstallDepsProvider::empty(), - ), - sys: sys.clone(), - cache_setting, - text_only_progress_bar: progress_bar, - npm_cache_dir, - maybe_lockfile: None, - maybe_node_modules_path: None, - npm_system_info: Default::default(), - npmrc: create_default_npmrc(), - lifecycle_scripts: Default::default(), - }, - )) - .await?; - (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( - in_npm_pkg_checker.clone(), - RealIsBuiltInNodeModuleChecker, - npm_resolver.clone().into_npm_pkg_folder_resolver(), - pkg_json_resolver.clone(), - sys.clone(), - )); - let cjs_tracker = Arc::new(CjsTracker::new( - in_npm_pkg_checker.clone(), - pkg_json_resolver.clone(), - if metadata.unstable_config.detect_cjs { - IsCjsResolutionMode::ImplicitTypeCommonJs - } else if metadata.workspace_resolver.package_jsons.is_empty() { - IsCjsResolutionMode::Disabled - } else { - IsCjsResolutionMode::ExplicitTypeCommonJs - }, - )); - let cache_db = Caches::new(deno_dir_provider.clone()); - let node_analysis_cache = NodeAnalysisCache::new(cache_db.node_analysis_db()); - let npm_req_resolver = - Arc::new(CliNpmReqResolver::new(NpmReqResolverOptions { - sys: sys.clone(), - in_npm_pkg_checker: in_npm_pkg_checker.clone(), - node_resolver: node_resolver.clone(), - npm_resolver: npm_resolver.clone().into_byonm_or_managed(), - })); - let cjs_esm_code_analyzer = CliCjsCodeAnalyzer::new( - node_analysis_cache, - cjs_tracker.clone(), - fs.clone(), - None, - ); - let node_code_translator = Arc::new(NodeCodeTranslator::new( - cjs_esm_code_analyzer, - in_npm_pkg_checker, - node_resolver.clone(), - npm_resolver.clone().into_npm_pkg_folder_resolver(), - pkg_json_resolver.clone(), - sys.clone(), - )); - let workspace_resolver = { - let import_map = match metadata.workspace_resolver.import_map { - Some(import_map) => Some( - import_map::parse_from_json_with_options( - root_dir_url.join(&import_map.specifier).unwrap(), - &import_map.json, - import_map::ImportMapOptions { - address_hook: None, - expand_imports: true, - }, - )? - .import_map, - ), - None => None, - }; - let pkg_jsons = metadata - .workspace_resolver - .package_jsons - .into_iter() - .map(|(relative_path, json)| { - let path = root_dir_url - .join(&relative_path) - .unwrap() - .to_file_path() - .unwrap(); - let pkg_json = - deno_package_json::PackageJson::load_from_value(path, json); - Arc::new(pkg_json) - }) - .collect(); - WorkspaceResolver::new_raw( - root_dir_url.clone(), - import_map, - metadata - .workspace_resolver - .jsr_pkgs - .iter() - .map(|pkg| ResolverWorkspaceJsrPackage { - is_patch: false, // only used for enhancing the diagnostic, which isn't shown in deno compile - base: root_dir_url.join(&pkg.relative_base).unwrap(), - name: pkg.name.clone(), - version: pkg.version.clone(), - exports: pkg.exports.clone(), - }) - .collect(), - pkg_jsons, - metadata.workspace_resolver.pkg_json_resolution, - ) - }; - let code_cache = match metadata.code_cache_key { - Some(code_cache_key) => Some(Arc::new(DenoCompileCodeCache::new( - root_path.with_file_name(format!( - "{}.cache", - root_path.file_name().unwrap().to_string_lossy() - )), - code_cache_key, - )) as Arc), - None => { - log::debug!("Code cache disabled."); - None - } - }; - let module_loader_factory = StandaloneModuleLoaderFactory { - shared: Arc::new(SharedModuleLoaderState { - cjs_tracker: cjs_tracker.clone(), - code_cache: code_cache.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_tracker.clone(), - fs.clone(), - node_code_translator, - )), - npm_registry_permission_checker, - npm_resolver: npm_resolver.clone(), - npm_req_resolver, - source_maps, - vfs, - workspace_resolver, - }), - }; - - let permissions = { - let mut permissions = metadata.permissions; - // 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()]); - } - } - - let desc_parser = - Arc::new(RuntimePermissionDescriptorParser::new(sys.clone())); - let permissions = - Permissions::from_options(desc_parser.as_ref(), &permissions)?; - PermissionsContainer::new(desc_parser, permissions) - }; - let feature_checker = Arc::new({ - let mut checker = FeatureChecker::default(); - checker.set_exit_cb(Box::new(crate::unstable_exit_cb)); - for feature in metadata.unstable_config.features { - // `metadata` is valid for the whole lifetime of the program, so we - // can leak the string here. - checker.enable_feature(feature.leak()); - } - checker - }); - let worker_factory = CliMainWorkerFactory::new( - Arc::new(BlobStore::default()), - code_cache, - feature_checker, - fs, - None, - None, - None, - Box::new(module_loader_factory), - node_resolver, - npm_resolver, - pkg_json_resolver, - root_cert_store_provider, - permissions, - StorageKeyResolver::empty(), - sys, - crate::args::DenoSubcommand::Run(Default::default()), - CliMainWorkerOptions { - argv: metadata.argv, - log_level: WorkerLogLevel::Info, - enable_op_summary_metrics: false, - enable_testing_features: false, - has_node_modules_dir, - hmr: false, - inspect_brk: false, - inspect_wait: false, - strace_ops: None, - is_inspecting: false, - skip_op_registration: true, - location: metadata.location, - argv0: NpmPackageReqReference::from_specifier(&main_module) - .ok() - .map(|req_ref| npm_pkg_req_ref_to_binary_command(&req_ref)) - .or(std::env::args().next()), - node_debug: std::env::var("NODE_DEBUG").ok(), - origin_data_folder_path: None, - seed: metadata.seed, - unsafely_ignore_certificate_errors: metadata - .unsafely_ignore_certificate_errors, - create_hmr_runner: None, - create_coverage_collector: None, - node_ipc: None, - serve_port: None, - serve_host: None, - }, - metadata.otel_config, - crate::args::NpmCachingStrategy::Lazy, - ); - - // Initialize v8 once from the main thread. - v8_set_flags(construct_v8_flags(&[], &metadata.v8_flags, vec![])); - // TODO(bartlomieju): remove last argument once Deploy no longer needs it - deno_core::JsRuntime::init_platform(None, true); - - let mut worker = worker_factory - .create_main_worker(WorkerExecutionMode::Run, main_module) - .await?; - - let exit_code = worker.run().await?; - Ok(exit_code) -} diff --git a/cli/standalone/serialization.rs b/cli/standalone/serialization.rs deleted file mode 100644 index 238ef44fd0..0000000000 --- a/cli/standalone/serialization.rs +++ /dev/null @@ -1,785 +0,0 @@ -// Copyright 2018-2025 the Deno authors. MIT license. - -use std::borrow::Cow; -use std::cell::Cell; -use std::collections::BTreeMap; -use std::collections::HashMap; -use std::io::Write; - -use capacity_builder::BytesAppendable; -use deno_ast::swc::common::source_map; -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 deno_semver::StackString; -use indexmap::IndexMap; - -use super::binary::Metadata; -use super::virtual_fs::BuiltVfs; -use super::virtual_fs::VfsBuilder; -use super::virtual_fs::VirtualDirectoryEntries; -use crate::standalone::virtual_fs::VirtualDirectory; - -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, - source_map_store: &SourceMapStore, - vfs: &BuiltVfs, -) -> Result, AnyError> { - let metadata = serde_json::to_string(metadata)?; - let npm_snapshot = - npm_snapshot.map(serialize_npm_snapshot).unwrap_or_default(); - let serialized_vfs = serde_json::to_string(&vfs.entries)?; - - let bytes = capacity_builder::BytesBuilder::build(|builder| { - builder.append(MAGIC_BYTES); - // 1. Metadata - { - builder.append_le(metadata.len() as u64); - builder.append(&metadata); - } - // 2. Npm snapshot - { - builder.append_le(npm_snapshot.len() as u64); - builder.append(&npm_snapshot); - } - // 3. Remote modules - { - remote_modules.write(builder); - } - // 4. VFS - { - builder.append_le(serialized_vfs.len() as u64); - builder.append(&serialized_vfs); - let vfs_bytes_len = vfs.files.iter().map(|f| f.len() as u64).sum::(); - builder.append_le(vfs_bytes_len); - for file in &vfs.files { - builder.append(file); - } - } - // 5. Source maps - { - builder.append_le(source_map_store.data.len() as u32); - for (specifier, source_map) in &source_map_store.data { - builder.append_le(specifier.len() as u32); - builder.append(specifier); - builder.append_le(source_map.len() as u32); - builder.append(source_map.as_ref()); - } - } - - // write the magic bytes at the end so we can use it - // to make sure we've deserialized correctly - builder.append(MAGIC_BYTES); - })?; - - Ok(bytes) -} - -pub struct DeserializedDataSection { - pub metadata: Metadata, - pub npm_snapshot: Option, - pub remote_modules: RemoteModulesStore, - pub source_maps: SourceMapStore, - pub vfs_root_entries: VirtualDirectoryEntries, - pub vfs_files_data: &'static [u8], -} - -pub fn deserialize_binary_data_section( - data: &'static [u8], -) -> Result, AnyError> { - 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)) - } - - #[allow(clippy::type_complexity)] - fn read_source_map_entry( - input: &[u8], - ) -> Result<(&[u8], (Cow, &[u8])), AnyError> { - let (input, specifier) = read_string_lossy(input)?; - let (input, source_map) = read_bytes_with_u32_len(input)?; - Ok((input, (specifier, source_map))) - } - - let (input, found) = read_magic_bytes(data)?; - if !found { - return Ok(None); - } - - // 1. Metadata - let (input, data) = - read_bytes_with_u64_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_u64_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, remote_modules) = - RemoteModulesStore::build(input).context("deserializing remote modules")?; - // 4. VFS - let (input, data) = read_bytes_with_u64_len(input).context("vfs")?; - let vfs_root_entries: VirtualDirectoryEntries = - serde_json::from_slice(data).context("deserializing vfs data")?; - let (input, vfs_files_data) = - read_bytes_with_u64_len(input).context("reading vfs files data")?; - // 5. Source maps - let (mut input, source_map_data_len) = read_u32_as_usize(input)?; - let mut source_maps = SourceMapStore::with_capacity(source_map_data_len); - for _ in 0..source_map_data_len { - let (current_input, (specifier, source_map)) = - read_source_map_entry(input)?; - input = current_input; - source_maps.add(specifier, Cow::Borrowed(source_map)); - } - - // 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, - source_maps, - vfs_root_entries, - vfs_files_data, - })) -} - -#[derive(Default)] -pub struct RemoteModulesStoreBuilder { - specifiers: Vec<(String, u64)>, - data: Vec<(MediaType, Vec, Option>)>, - 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, - maybe_transpiled: Option>, - ) { - log::debug!("Adding '{}' ({})", specifier, media_type); - let specifier = specifier.to_string(); - self.specifiers.push((specifier, self.data_byte_len)); - let maybe_transpiled_len = match &maybe_transpiled { - // data length (4 bytes), data - Some(data) => 4 + data.len() as u64, - None => 0, - }; - // media type (1 byte), data length (4 bytes), data, has transpiled (1 byte), transpiled length - self.data_byte_len += 1 + 4 + data.len() as u64 + 1 + maybe_transpiled_len; - self.data.push((media_type, data, maybe_transpiled)); - } - - 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<'a, TBytes: capacity_builder::BytesType>( - &'a self, - builder: &mut capacity_builder::BytesBuilder<'a, TBytes>, - ) { - builder.append_le(self.specifiers.len() as u32); - builder.append_le(self.redirects.len() as u32); - for (specifier, offset) in &self.specifiers { - builder.append_le(specifier.len() as u32); - builder.append(specifier); - builder.append_le(*offset); - } - for (from, to) in &self.redirects { - builder.append_le(from.len() as u32); - builder.append(from); - builder.append_le(to.len() as u32); - builder.append(to); - } - builder.append_le( - self - .data - .iter() - .map(|(_, data, maybe_transpiled)| { - 1 + 4 - + (data.len() as u64) - + 1 - + match maybe_transpiled { - Some(transpiled) => 4 + (transpiled.len() as u64), - None => 0, - } - }) - .sum::(), - ); - for (media_type, data, maybe_transpiled) in &self.data { - builder.append(serialize_media_type(*media_type)); - builder.append_le(data.len() as u32); - builder.append(data); - if let Some(transpiled) = maybe_transpiled { - builder.append(1); - builder.append_le(transpiled.len() as u32); - builder.append(transpiled); - } else { - builder.append(0); - } - } - } -} - -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 SourceMapStore { - data: IndexMap, Cow<'static, [u8]>>, -} - -impl SourceMapStore { - pub fn with_capacity(capacity: usize) -> Self { - Self { - data: IndexMap::with_capacity(capacity), - } - } - - pub fn add( - &mut self, - specifier: Cow<'static, str>, - source_map: Cow<'static, [u8]>, - ) { - self.data.insert(specifier, source_map); - } - - pub fn get(&self, specifier: &str) -> Option<&[u8]> { - self.data.get(specifier).map(|v| v.as_ref()) - } -} - -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) - } -} - -pub struct RemoteModuleEntry<'a> { - pub specifier: &'a Url, - pub media_type: MediaType, - pub data: Cow<'static, [u8]>, - pub transpiled_data: Option>, -} - -enum RemoteModulesStoreSpecifierValue { - Data(usize), - Redirect(Url), -} - -pub struct RemoteModulesStore { - specifiers: HashMap, - files_data: &'static [u8], -} - -impl RemoteModulesStore { - fn build(input: &'static [u8]) -> Result<(&'static [u8], Self), AnyError> { - 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 (input, specifiers) = read_headers(input)?; - let (input, files_data) = read_bytes_with_u64_len(input)?; - - Ok(( - input, - 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, data) = read_bytes_with_u32_len(input)?; - check_has_len(input, 1)?; - let (input, has_transpiled) = (&input[1..], input[0]); - let (_, transpiled_data) = match has_transpiled { - 0 => (input, None), - 1 => { - let (input, data) = read_bytes_with_u32_len(input)?; - (input, Some(data)) - } - value => bail!( - "Invalid transpiled data flag: {}. Compiled data is corrupt.", - value - ), - }; - return Ok(Some(RemoteModuleEntry { - specifier, - media_type, - data: Cow::Borrowed(data), - transpiled_data: transpiled_data.map(Cow::Borrowed), - })); - } - 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], (StackString, NpmPackageId)), AnyError> + 'a - { - |input| { - let (input, req) = read_string_lossy(input)?; - let (input, id) = read_u32_as_usize(input)?; - let req = StackString::from_cow(req); - Ok((input, (req, 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_with_u64_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_bytes_with_u32_len(input: &[u8]) -> Result<(&[u8], &[u8]), AnyError> { - let (input, len) = read_u32_as_usize(input)?; - let (input, data) = read_bytes(input, len)?; - Ok((input, data)) -} - -fn read_bytes(input: &[u8], len: usize) -> Result<(&[u8], &[u8]), AnyError> { - check_has_len(input, len)?; - let (len_bytes, input) = input.split_at(len); - Ok((input, len_bytes)) -} - -#[inline(always)] -fn check_has_len(input: &[u8], len: usize) -> Result<(), AnyError> { - if input.len() < len { - bail!("Unexpected end of data."); - } - Ok(()) -} - -fn read_string_lossy(input: &[u8]) -> Result<(&[u8], Cow), AnyError> { - let (input, data_bytes) = read_bytes_with_u32_len(input)?; - 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 e167b153a7..fa79b784dc 100644 --- a/cli/standalone/virtual_fs.rs +++ b/cli/standalone/virtual_fs.rs @@ -1,503 +1,21 @@ // Copyright 2018-2025 the Deno authors. MIT license. use std::borrow::Cow; -use std::cell::RefCell; -use std::collections::HashMap; use std::collections::HashSet; -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; -use std::sync::Arc; -use deno_core::anyhow::anyhow; -use deno_core::anyhow::bail; -use deno_core::anyhow::Context; -use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; -use deno_core::BufMutView; -use deno_core::BufView; -use deno_core::ResourceHandleFd; -use deno_path_util::normalize_path; -use deno_path_util::strip_unc_prefix; -use deno_runtime::deno_fs::FsDirEntry; -use deno_runtime::deno_io; -use deno_runtime::deno_io::fs::FsError; -use deno_runtime::deno_io::fs::FsResult; -use deno_runtime::deno_io::fs::FsStat; -use indexmap::IndexSet; -use serde::Deserialize; -use serde::Serialize; -use thiserror::Error; +use deno_lib::standalone::virtual_fs::BuiltVfs; +use deno_lib::standalone::virtual_fs::OffsetWithLength; +use deno_lib::standalone::virtual_fs::VfsEntry; +use deno_lib::standalone::virtual_fs::VirtualDirectory; +use deno_lib::standalone::virtual_fs::VirtualDirectoryEntries; +use deno_lib::standalone::virtual_fs::VirtualFile; +use deno_lib::standalone::virtual_fs::VirtualSymlinkParts; +use deno_lib::standalone::virtual_fs::WindowsSystemRootablePath; +use deno_lib::standalone::virtual_fs::DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME; -use super::binary::DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME; -use crate::util; use crate::util::display::human_size; use crate::util::display::DisplayTreeNode; -use crate::util::fs::canonicalize_path; - -#[derive(Debug, PartialEq, Eq)] -pub enum WindowsSystemRootablePath { - /// The root of the system above any drive letters. - WindowSystemRoot, - Path(PathBuf), -} - -impl WindowsSystemRootablePath { - pub fn join(&self, name_component: &str) -> PathBuf { - // this method doesn't handle multiple components - debug_assert!( - !name_component.contains('\\'), - "Invalid component: {}", - name_component - ); - debug_assert!( - !name_component.contains('/'), - "Invalid component: {}", - name_component - ); - - match self { - WindowsSystemRootablePath::WindowSystemRoot => { - // windows drive letter - PathBuf::from(&format!("{}\\", name_component)) - } - WindowsSystemRootablePath::Path(path) => path.join(name_component), - } - } -} - -#[derive(Debug)] -pub struct BuiltVfs { - pub root_path: WindowsSystemRootablePath, - pub entries: VirtualDirectoryEntries, - pub files: Vec>, -} - -#[derive(Debug, Copy, Clone)] -pub enum VfsFileSubDataKind { - /// Raw bytes of the file. - Raw, - /// Bytes to use for module loading. For example, for TypeScript - /// files this will be the transpiled JavaScript source. - ModuleGraph, -} - -#[derive(Debug)] -pub struct VfsBuilder { - executable_root: VirtualDirectory, - files: Vec>, - current_offset: u64, - file_offsets: HashMap, - /// The minimum root directory that should be included in the VFS. - min_root_dir: Option, -} - -impl VfsBuilder { - pub fn new() -> Self { - Self { - executable_root: VirtualDirectory { - name: "/".to_string(), - entries: Default::default(), - }, - files: Vec::new(), - current_offset: 0, - file_offsets: Default::default(), - min_root_dir: Default::default(), - } - } - - /// Add a directory that might be the minimum root directory - /// of the VFS. - /// - /// For example, say the user has a deno.json and specifies an - /// import map in a parent directory. The import map won't be - /// included in the VFS, but its base will meaning we need to - /// tell the VFS builder to include the base of the import map - /// by calling this method. - pub fn add_possible_min_root_dir(&mut self, path: &Path) { - self.add_dir_raw(path); - - match &self.min_root_dir { - Some(WindowsSystemRootablePath::WindowSystemRoot) => { - // already the root dir - } - Some(WindowsSystemRootablePath::Path(current_path)) => { - let mut common_components = Vec::new(); - for (a, b) in current_path.components().zip(path.components()) { - if a != b { - break; - } - common_components.push(a); - } - if common_components.is_empty() { - if cfg!(windows) { - self.min_root_dir = - Some(WindowsSystemRootablePath::WindowSystemRoot); - } else { - self.min_root_dir = - Some(WindowsSystemRootablePath::Path(PathBuf::from("/"))); - } - } else { - self.min_root_dir = Some(WindowsSystemRootablePath::Path( - common_components.iter().collect(), - )); - } - } - None => { - self.min_root_dir = - Some(WindowsSystemRootablePath::Path(path.to_path_buf())); - } - } - } - - pub fn add_dir_recursive(&mut self, path: &Path) -> Result<(), AnyError> { - let target_path = self.resolve_target_path(path)?; - self.add_dir_recursive_not_symlink(&target_path) - } - - fn add_dir_recursive_not_symlink( - &mut self, - path: &Path, - ) -> Result<(), AnyError> { - self.add_dir_raw(path); - let read_dir = std::fs::read_dir(path) - .with_context(|| format!("Reading {}", path.display()))?; - - let mut dir_entries = - read_dir.into_iter().collect::, _>>()?; - dir_entries.sort_by_cached_key(|entry| entry.file_name()); // determinism - - for entry in dir_entries { - let file_type = entry.file_type()?; - let path = entry.path(); - - if file_type.is_dir() { - self.add_dir_recursive_not_symlink(&path)?; - } else if file_type.is_file() { - self.add_file_at_path_not_symlink(&path)?; - } else if file_type.is_symlink() { - match self.add_symlink(&path) { - Ok(target) => match target { - SymlinkTarget::File(target) => { - self.add_file_at_path_not_symlink(&target)? - } - SymlinkTarget::Dir(target) => { - self.add_dir_recursive_not_symlink(&target)?; - } - }, - Err(err) => { - log::warn!( - "{} Failed resolving symlink. Ignoring.\n Path: {}\n Message: {:#}", - crate::colors::yellow("Warning"), - path.display(), - err - ); - } - } - } - } - - Ok(()) - } - - fn add_dir_raw(&mut self, path: &Path) -> &mut VirtualDirectory { - log::debug!("Ensuring directory '{}'", path.display()); - debug_assert!(path.is_absolute()); - let mut current_dir = &mut self.executable_root; - - for component in path.components() { - if matches!(component, std::path::Component::RootDir) { - continue; - } - let name = component.as_os_str().to_string_lossy(); - let index = match current_dir.entries.binary_search(&name) { - Ok(index) => index, - Err(insert_index) => { - current_dir.entries.0.insert( - insert_index, - VfsEntry::Dir(VirtualDirectory { - name: name.to_string(), - entries: Default::default(), - }), - ); - insert_index - } - }; - match &mut current_dir.entries.0[index] { - VfsEntry::Dir(dir) => { - current_dir = dir; - } - _ => unreachable!(), - }; - } - - current_dir - } - - pub fn get_system_root_dir_mut(&mut self) -> &mut VirtualDirectory { - &mut self.executable_root - } - - pub fn get_dir_mut(&mut self, path: &Path) -> Option<&mut VirtualDirectory> { - debug_assert!(path.is_absolute()); - let mut current_dir = &mut self.executable_root; - - for component in path.components() { - if matches!(component, std::path::Component::RootDir) { - continue; - } - let name = component.as_os_str().to_string_lossy(); - let entry = current_dir.entries.get_mut_by_name(&name)?; - match entry { - VfsEntry::Dir(dir) => { - current_dir = dir; - } - _ => unreachable!(), - }; - } - - Some(current_dir) - } - - pub fn add_file_at_path(&mut self, path: &Path) -> Result<(), AnyError> { - let file_bytes = std::fs::read(path) - .with_context(|| format!("Reading {}", path.display()))?; - self.add_file_with_data(path, file_bytes, VfsFileSubDataKind::Raw) - } - - 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_with_data_inner(path, file_bytes, VfsFileSubDataKind::Raw) - } - - pub fn add_file_with_data( - &mut self, - path: &Path, - data: Vec, - sub_data_kind: VfsFileSubDataKind, - ) -> Result<(), AnyError> { - let metadata = std::fs::symlink_metadata(path).with_context(|| { - format!("Resolving target path for '{}'", path.display()) - })?; - if metadata.is_symlink() { - let target = self.add_symlink(path)?.into_path_buf(); - self.add_file_with_data_inner(&target, data, sub_data_kind) - } else { - self.add_file_with_data_inner(path, data, sub_data_kind) - } - } - - fn add_file_with_data_inner( - &mut self, - path: &Path, - data: Vec, - sub_data_kind: VfsFileSubDataKind, - ) -> 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) { - // duplicate file, reuse an old offset - *offset - } else { - self.file_offsets.insert(checksum, self.current_offset); - self.current_offset - }; - - let dir = self.add_dir_raw(path.parent().unwrap()); - let name = path.file_name().unwrap().to_string_lossy(); - let offset_and_len = OffsetWithLength { - offset, - len: data.len() as u64, - }; - match dir.entries.binary_search(&name) { - Ok(index) => { - let entry = &mut dir.entries.0[index]; - match entry { - VfsEntry::File(virtual_file) => match sub_data_kind { - VfsFileSubDataKind::Raw => { - virtual_file.offset = offset_and_len; - } - VfsFileSubDataKind::ModuleGraph => { - virtual_file.module_graph_offset = offset_and_len; - } - }, - VfsEntry::Dir(_) | VfsEntry::Symlink(_) => unreachable!(), - } - } - Err(insert_index) => { - dir.entries.0.insert( - insert_index, - VfsEntry::File(VirtualFile { - name: name.to_string(), - offset: offset_and_len, - module_graph_offset: offset_and_len, - }), - ); - } - } - - // new file, update the list of files - if self.current_offset == offset { - self.files.push(data); - self.current_offset += offset_and_len.len; - } - - Ok(()) - } - - fn resolve_target_path(&mut self, path: &Path) -> Result { - let metadata = std::fs::symlink_metadata(path).with_context(|| { - format!("Resolving target path for '{}'", path.display()) - })?; - if metadata.is_symlink() { - Ok(self.add_symlink(path)?.into_path_buf()) - } else { - Ok(path.to_path_buf()) - } - } - - fn add_symlink(&mut self, path: &Path) -> Result { - self.add_symlink_inner(path, &mut IndexSet::new()) - } - - fn add_symlink_inner( - &mut self, - path: &Path, - visited: &mut IndexSet, - ) -> Result { - log::debug!("Adding symlink '{}'", path.display()); - let target = strip_unc_prefix( - std::fs::read_link(path) - .with_context(|| format!("Reading symlink '{}'", path.display()))?, - ); - let target = normalize_path(path.parent().unwrap().join(&target)); - let dir = self.add_dir_raw(path.parent().unwrap()); - let name = path.file_name().unwrap().to_string_lossy(); - match dir.entries.binary_search(&name) { - Ok(_) => {} // previously inserted - Err(insert_index) => { - dir.entries.0.insert( - insert_index, - VfsEntry::Symlink(VirtualSymlink { - name: name.to_string(), - dest_parts: VirtualSymlinkParts::from_path(&target), - }), - ); - } - } - let target_metadata = - std::fs::symlink_metadata(&target).with_context(|| { - format!("Reading symlink target '{}'", target.display()) - })?; - if target_metadata.is_symlink() { - if !visited.insert(target.clone()) { - // todo: probably don't error in this scenario - bail!( - "Circular symlink detected: {} -> {}", - visited - .iter() - .map(|p| p.display().to_string()) - .collect::>() - .join(" -> "), - target.display() - ); - } - self.add_symlink_inner(&target, visited) - } else if target_metadata.is_dir() { - Ok(SymlinkTarget::Dir(target)) - } else { - Ok(SymlinkTarget::File(target)) - } - } - - pub fn build(self) -> BuiltVfs { - fn strip_prefix_from_symlinks( - dir: &mut VirtualDirectory, - parts: &[String], - ) { - for entry in &mut dir.entries.0 { - match entry { - VfsEntry::Dir(dir) => { - strip_prefix_from_symlinks(dir, parts); - } - VfsEntry::File(_) => {} - VfsEntry::Symlink(symlink) => { - let old_parts = std::mem::take(&mut symlink.dest_parts.0); - symlink.dest_parts.0 = - old_parts.into_iter().skip(parts.len()).collect(); - } - } - } - } - - let mut current_dir = self.executable_root; - let mut current_path = if cfg!(windows) { - WindowsSystemRootablePath::WindowSystemRoot - } else { - WindowsSystemRootablePath::Path(PathBuf::from("/")) - }; - loop { - if current_dir.entries.len() != 1 { - break; - } - if self.min_root_dir.as_ref() == Some(¤t_path) { - break; - } - match ¤t_dir.entries.0[0] { - VfsEntry::Dir(dir) => { - if dir.name == DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME { - // special directory we want to maintain - break; - } - match current_dir.entries.0.remove(0) { - VfsEntry::Dir(dir) => { - current_path = - WindowsSystemRootablePath::Path(current_path.join(&dir.name)); - current_dir = dir; - } - _ => unreachable!(), - }; - } - VfsEntry::File(_) | VfsEntry::Symlink(_) => break, - } - } - if let WindowsSystemRootablePath::Path(path) = ¤t_path { - strip_prefix_from_symlinks( - &mut current_dir, - &VirtualSymlinkParts::from_path(path).0, - ); - } - BuiltVfs { - root_path: current_path, - entries: current_dir.entries, - files: self.files, - } - } -} - -#[derive(Debug)] -enum SymlinkTarget { - File(PathBuf), - Dir(PathBuf), -} - -impl SymlinkTarget { - pub fn into_path_buf(self) -> PathBuf { - match self { - Self::File(path) => path, - Self::Dir(path) => path, - } - } -} pub fn output_vfs(vfs: &BuiltVfs, executable_name: &str) { if !log::log_enabled!(log::Level::Info) { @@ -552,7 +70,7 @@ fn vfs_as_display_tree( All(Size), Subset(Vec>), File(Size), - Symlink(&'a [String]), + Symlink(&'a VirtualSymlinkParts), } impl<'a> EntryOutput<'a> { @@ -601,7 +119,7 @@ fn vfs_as_display_tree( format!("{} ({})", name, format_size(*size)) } EntryOutput::Symlink(parts) => { - format!("{} --> {}", name, parts.join("/")) + format!("{} --> {}", name, parts.display()) } }, children: match self { @@ -686,8 +204,13 @@ fn vfs_as_display_tree( let mut size = Size::default(); add_offset_to_size(file.offset, &mut size, seen_offsets); - if file.module_graph_offset.offset != file.offset.offset { - add_offset_to_size(file.module_graph_offset, &mut size, seen_offsets); + let maybe_offsets = [ + file.transpiled_offset, + file.source_map_offset, + file.cjs_export_analysis_offset, + ]; + for offset in maybe_offsets.into_iter().flatten() { + add_offset_to_size(offset, &mut size, seen_offsets); } size } @@ -744,7 +267,7 @@ fn vfs_as_display_tree( EntryOutput::File(file_size(file, seen_offsets)) } VfsEntry::Symlink(virtual_symlink) => { - EntryOutput::Symlink(&virtual_symlink.dest_parts.0) + EntryOutput::Symlink(&virtual_symlink.dest_parts) } }, }) @@ -781,7 +304,7 @@ fn vfs_as_display_tree( } VfsEntry::File(file) => EntryOutput::File(file_size(file, seen_offsets)), VfsEntry::Symlink(virtual_symlink) => { - EntryOutput::Symlink(&virtual_symlink.dest_parts.0) + EntryOutput::Symlink(&virtual_symlink.dest_parts) } } } @@ -847,996 +370,14 @@ fn vfs_as_display_tree( } } -#[derive(Debug)] -enum VfsEntryRef<'a> { - Dir(&'a VirtualDirectory), - File(&'a VirtualFile), - Symlink(&'a VirtualSymlink), -} - -impl VfsEntryRef<'_> { - pub fn as_metadata(&self) -> FileBackedVfsMetadata { - FileBackedVfsMetadata { - file_type: match self { - Self::Dir(_) => sys_traits::FileType::Dir, - Self::File(_) => sys_traits::FileType::File, - Self::Symlink(_) => sys_traits::FileType::Symlink, - }, - name: self.name().to_string(), - len: match self { - Self::Dir(_) => 0, - Self::File(file) => file.offset.len, - Self::Symlink(_) => 0, - }, - } - } - - pub fn name(&self) -> &str { - match self { - Self::Dir(dir) => &dir.name, - Self::File(file) => &file.name, - Self::Symlink(symlink) => &symlink.name, - } - } -} - -// todo(dsherret): we should store this more efficiently in the binary -#[derive(Debug, Serialize, Deserialize)] -pub enum VfsEntry { - Dir(VirtualDirectory), - File(VirtualFile), - Symlink(VirtualSymlink), -} - -impl VfsEntry { - pub fn name(&self) -> &str { - match self { - Self::Dir(dir) => &dir.name, - Self::File(file) => &file.name, - Self::Symlink(symlink) => &symlink.name, - } - } - - fn as_ref(&self) -> VfsEntryRef { - match self { - VfsEntry::Dir(dir) => VfsEntryRef::Dir(dir), - VfsEntry::File(file) => VfsEntryRef::File(file), - VfsEntry::Symlink(symlink) => VfsEntryRef::Symlink(symlink), - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct VirtualDirectoryEntries(Vec); - -impl VirtualDirectoryEntries { - pub fn new(mut entries: Vec) -> Self { - // needs to be sorted by name - entries.sort_by(|a, b| a.name().cmp(b.name())); - Self(entries) - } - - pub fn take_inner(&mut self) -> Vec { - std::mem::take(&mut self.0) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn get_by_name(&self, name: &str) -> Option<&VfsEntry> { - self.binary_search(name).ok().map(|index| &self.0[index]) - } - - pub fn get_mut_by_name(&mut self, name: &str) -> Option<&mut VfsEntry> { - self - .binary_search(name) - .ok() - .map(|index| &mut self.0[index]) - } - - pub fn binary_search(&self, name: &str) -> Result { - self.0.binary_search_by(|e| e.name().cmp(name)) - } - - pub fn insert(&mut self, entry: VfsEntry) { - match self.binary_search(entry.name()) { - Ok(index) => { - self.0[index] = entry; - } - Err(insert_index) => { - self.0.insert(insert_index, entry); - } - } - } - - pub fn remove(&mut self, index: usize) -> VfsEntry { - self.0.remove(index) - } - - pub fn iter(&self) -> std::slice::Iter<'_, VfsEntry> { - self.0.iter() - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VirtualDirectory { - #[serde(rename = "n")] - pub name: String, - // should be sorted by name - #[serde(rename = "e")] - pub entries: VirtualDirectoryEntries, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct OffsetWithLength { - #[serde(rename = "o")] - pub offset: u64, - #[serde(rename = "l")] - pub len: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VirtualFile { - #[serde(rename = "n")] - pub name: String, - #[serde(rename = "o")] - pub offset: OffsetWithLength, - /// Offset file to use for module loading when it differs from the - /// raw file. Often this will be the same offset as above for data - /// such as JavaScript files, but for TypeScript files the `offset` - /// will be the original raw bytes when included as an asset and this - /// offset will be to the transpiled JavaScript source. - #[serde(rename = "m")] - pub module_graph_offset: OffsetWithLength, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VirtualSymlinkParts(Vec); - -impl VirtualSymlinkParts { - pub fn from_path(path: &Path) -> Self { - Self( - path - .components() - .filter(|c| !matches!(c, std::path::Component::RootDir)) - .map(|c| c.as_os_str().to_string_lossy().to_string()) - .collect(), - ) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VirtualSymlink { - #[serde(rename = "n")] - pub name: String, - #[serde(rename = "p")] - pub dest_parts: VirtualSymlinkParts, -} - -impl VirtualSymlink { - pub fn resolve_dest_from_root(&self, root: &Path) -> PathBuf { - let mut dest = root.to_path_buf(); - for part in &self.dest_parts.0 { - dest.push(part); - } - dest - } -} - -#[derive(Debug)] -pub struct VfsRoot { - pub dir: VirtualDirectory, - pub root_path: PathBuf, - pub start_file_offset: u64, -} - -impl VfsRoot { - fn find_entry<'a>( - &'a self, - path: &Path, - ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { - self.find_entry_inner(path, &mut HashSet::new()) - } - - fn find_entry_inner<'a>( - &'a self, - path: &Path, - seen: &mut HashSet, - ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { - let mut path = Cow::Borrowed(path); - loop { - let (resolved_path, entry) = - self.find_entry_no_follow_inner(&path, seen)?; - match entry { - VfsEntryRef::Symlink(symlink) => { - if !seen.insert(path.to_path_buf()) { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "circular symlinks", - )); - } - path = Cow::Owned(symlink.resolve_dest_from_root(&self.root_path)); - } - _ => { - return Ok((resolved_path, entry)); - } - } - } - } - - fn find_entry_no_follow( - &self, - path: &Path, - ) -> std::io::Result<(PathBuf, VfsEntryRef)> { - self.find_entry_no_follow_inner(path, &mut HashSet::new()) - } - - fn find_entry_no_follow_inner<'a>( - &'a self, - path: &Path, - seen: &mut HashSet, - ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { - let relative_path = match path.strip_prefix(&self.root_path) { - Ok(p) => p, - Err(_) => { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "path not found", - )); - } - }; - let mut final_path = self.root_path.clone(); - let mut current_entry = VfsEntryRef::Dir(&self.dir); - for component in relative_path.components() { - let component = component.as_os_str(); - let current_dir = match current_entry { - VfsEntryRef::Dir(dir) => { - final_path.push(component); - dir - } - VfsEntryRef::Symlink(symlink) => { - let dest = symlink.resolve_dest_from_root(&self.root_path); - let (resolved_path, entry) = self.find_entry_inner(&dest, seen)?; - final_path = resolved_path; // overwrite with the new resolved path - match entry { - VfsEntryRef::Dir(dir) => { - final_path.push(component); - dir - } - _ => { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "path not found", - )); - } - } - } - _ => { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "path not found", - )); - } - }; - let component = component.to_string_lossy(); - current_entry = current_dir - .entries - .get_by_name(&component) - .ok_or_else(|| { - std::io::Error::new(std::io::ErrorKind::NotFound, "path not found") - })? - .as_ref(); - } - - Ok((final_path, current_entry)) - } -} - -pub struct FileBackedVfsFile { - file: VirtualFile, - pos: RefCell, - vfs: Arc, -} - -impl FileBackedVfsFile { - pub fn seek(&self, pos: SeekFrom) -> std::io::Result { - match pos { - SeekFrom::Start(pos) => { - *self.pos.borrow_mut() = pos; - Ok(pos) - } - SeekFrom::End(offset) => { - if offset < 0 && -offset as u64 > self.file.offset.len { - let msg = "An attempt was made to move the file pointer before the beginning of the file."; - Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - msg, - )) - } else { - let mut current_pos = self.pos.borrow_mut(); - *current_pos = if offset >= 0 { - self.file.offset.len - (offset as u64) - } else { - self.file.offset.len + (-offset as u64) - }; - Ok(*current_pos) - } - } - SeekFrom::Current(offset) => { - let mut current_pos = self.pos.borrow_mut(); - if offset >= 0 { - *current_pos += offset as u64; - } else if -offset as u64 > *current_pos { - return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "An attempt was made to move the file pointer before the beginning of the file.")); - } else { - *current_pos -= -offset as u64; - } - Ok(*current_pos) - } - } - } - - pub fn read_to_buf(&self, buf: &mut [u8]) -> std::io::Result { - let read_pos = { - let mut pos = self.pos.borrow_mut(); - let read_pos = *pos; - // advance the position due to the read - *pos = std::cmp::min(self.file.offset.len, *pos + buf.len() as u64); - read_pos - }; - self.vfs.read_file(&self.file, read_pos, buf) - } - - fn read_to_end(&self) -> FsResult> { - let read_pos = { - let mut pos = self.pos.borrow_mut(); - let read_pos = *pos; - // todo(dsherret): should this always set it to the end of the file? - if *pos < self.file.offset.len { - // advance the position due to the read - *pos = self.file.offset.len; - } - read_pos - }; - if read_pos > self.file.offset.len { - return Ok(Cow::Borrowed(&[])); - } - if read_pos == 0 { - Ok( - self - .vfs - .read_file_all(&self.file, VfsFileSubDataKind::Raw)?, - ) - } else { - let size = (self.file.offset.len - read_pos) as usize; - let mut buf = vec![0; size]; - self.vfs.read_file(&self.file, read_pos, &mut buf)?; - Ok(Cow::Owned(buf)) - } - } -} - -#[async_trait::async_trait(?Send)] -impl deno_io::fs::File for FileBackedVfsFile { - fn read_sync(self: Rc, buf: &mut [u8]) -> FsResult { - self.read_to_buf(buf).map_err(Into::into) - } - async fn read_byob( - self: Rc, - mut buf: BufMutView, - ) -> FsResult<(usize, BufMutView)> { - // this is fast, no need to spawn a task - let nread = self.read_to_buf(&mut buf)?; - Ok((nread, buf)) - } - - fn write_sync(self: Rc, _buf: &[u8]) -> FsResult { - Err(FsError::NotSupported) - } - async fn write( - self: Rc, - _buf: BufView, - ) -> FsResult { - Err(FsError::NotSupported) - } - - fn write_all_sync(self: Rc, _buf: &[u8]) -> FsResult<()> { - Err(FsError::NotSupported) - } - async fn write_all(self: Rc, _buf: BufView) -> FsResult<()> { - Err(FsError::NotSupported) - } - - fn read_all_sync(self: Rc) -> FsResult> { - self.read_to_end() - } - async fn read_all_async(self: Rc) -> FsResult> { - // this is fast, no need to spawn a task - self.read_to_end() - } - - fn chmod_sync(self: Rc, _pathmode: u32) -> FsResult<()> { - Err(FsError::NotSupported) - } - async fn chmod_async(self: Rc, _mode: u32) -> FsResult<()> { - Err(FsError::NotSupported) - } - - fn seek_sync(self: Rc, pos: SeekFrom) -> FsResult { - self.seek(pos).map_err(|err| err.into()) - } - async fn seek_async(self: Rc, pos: SeekFrom) -> FsResult { - self.seek(pos).map_err(|err| err.into()) - } - - fn datasync_sync(self: Rc) -> FsResult<()> { - Err(FsError::NotSupported) - } - async fn datasync_async(self: Rc) -> FsResult<()> { - Err(FsError::NotSupported) - } - - fn sync_sync(self: Rc) -> FsResult<()> { - Err(FsError::NotSupported) - } - async fn sync_async(self: Rc) -> FsResult<()> { - Err(FsError::NotSupported) - } - - fn stat_sync(self: Rc) -> FsResult { - Err(FsError::NotSupported) - } - async fn stat_async(self: Rc) -> FsResult { - Err(FsError::NotSupported) - } - - fn lock_sync(self: Rc, _exclusive: bool) -> FsResult<()> { - Err(FsError::NotSupported) - } - async fn lock_async(self: Rc, _exclusive: bool) -> FsResult<()> { - Err(FsError::NotSupported) - } - - fn unlock_sync(self: Rc) -> FsResult<()> { - Err(FsError::NotSupported) - } - async fn unlock_async(self: Rc) -> FsResult<()> { - Err(FsError::NotSupported) - } - - fn truncate_sync(self: Rc, _len: u64) -> FsResult<()> { - Err(FsError::NotSupported) - } - async fn truncate_async(self: Rc, _len: u64) -> FsResult<()> { - Err(FsError::NotSupported) - } - - fn utime_sync( - self: Rc, - _atime_secs: i64, - _atime_nanos: u32, - _mtime_secs: i64, - _mtime_nanos: u32, - ) -> FsResult<()> { - Err(FsError::NotSupported) - } - async fn utime_async( - self: Rc, - _atime_secs: i64, - _atime_nanos: u32, - _mtime_secs: i64, - _mtime_nanos: u32, - ) -> FsResult<()> { - Err(FsError::NotSupported) - } - - // lower level functionality - fn as_stdio(self: Rc) -> FsResult { - Err(FsError::NotSupported) - } - fn backing_fd(self: Rc) -> Option { - None - } - fn try_clone_inner(self: Rc) -> FsResult> { - Ok(self) - } -} - -#[derive(Debug, Clone)] -pub struct FileBackedVfsDirEntry { - pub parent_path: PathBuf, - pub metadata: FileBackedVfsMetadata, -} - -#[derive(Debug, Clone)] -pub struct FileBackedVfsMetadata { - pub name: String, - pub file_type: sys_traits::FileType, - pub len: u64, -} - -impl FileBackedVfsMetadata { - pub fn as_fs_stat(&self) -> FsStat { - FsStat { - is_directory: self.file_type == sys_traits::FileType::Dir, - is_file: self.file_type == sys_traits::FileType::File, - is_symlink: self.file_type == sys_traits::FileType::Symlink, - atime: None, - birthtime: None, - mtime: None, - ctime: None, - blksize: 0, - size: self.len, - dev: 0, - ino: 0, - mode: 0, - nlink: 0, - uid: 0, - gid: 0, - rdev: 0, - blocks: 0, - is_block_device: false, - is_char_device: false, - is_fifo: false, - is_socket: false, - } - } -} - -#[derive(Debug)] -pub struct FileBackedVfs { - vfs_data: Cow<'static, [u8]>, - fs_root: VfsRoot, -} - -impl FileBackedVfs { - pub fn new(data: Cow<'static, [u8]>, fs_root: VfsRoot) -> Self { - Self { - vfs_data: data, - fs_root, - } - } - - pub fn root(&self) -> &Path { - &self.fs_root.root_path - } - - pub fn is_path_within(&self, path: &Path) -> bool { - path.starts_with(&self.fs_root.root_path) - } - - pub fn open_file( - self: &Arc, - path: &Path, - ) -> std::io::Result { - let file = self.file_entry(path)?; - Ok(FileBackedVfsFile { - file: file.clone(), - vfs: self.clone(), - pos: Default::default(), - }) - } - - pub fn read_dir(&self, path: &Path) -> std::io::Result> { - let dir = self.dir_entry(path)?; - Ok( - dir - .entries - .iter() - .map(|entry| FsDirEntry { - name: entry.name().to_string(), - is_file: matches!(entry, VfsEntry::File(_)), - is_directory: matches!(entry, VfsEntry::Dir(_)), - is_symlink: matches!(entry, VfsEntry::Symlink(_)), - }) - .collect(), - ) - } - - pub fn read_dir_with_metadata<'a>( - &'a self, - path: &Path, - ) -> std::io::Result + 'a> { - let dir = self.dir_entry(path)?; - let path = path.to_path_buf(); - Ok(dir.entries.iter().map(move |entry| FileBackedVfsDirEntry { - parent_path: path.to_path_buf(), - metadata: entry.as_ref().as_metadata(), - })) - } - - pub fn read_link(&self, path: &Path) -> std::io::Result { - let (_, entry) = self.fs_root.find_entry_no_follow(path)?; - match entry { - VfsEntryRef::Symlink(symlink) => { - Ok(symlink.resolve_dest_from_root(&self.fs_root.root_path)) - } - VfsEntryRef::Dir(_) | VfsEntryRef::File(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "not a symlink", - )), - } - } - - pub fn lstat(&self, path: &Path) -> std::io::Result { - let (_, entry) = self.fs_root.find_entry_no_follow(path)?; - Ok(entry.as_metadata()) - } - - pub fn stat(&self, path: &Path) -> std::io::Result { - let (_, entry) = self.fs_root.find_entry(path)?; - Ok(entry.as_metadata()) - } - - pub fn canonicalize(&self, path: &Path) -> std::io::Result { - let (path, _) = self.fs_root.find_entry(path)?; - Ok(path) - } - - pub fn read_file_all( - &self, - file: &VirtualFile, - sub_data_kind: VfsFileSubDataKind, - ) -> std::io::Result> { - let read_len = match sub_data_kind { - VfsFileSubDataKind::Raw => file.offset.len, - VfsFileSubDataKind::ModuleGraph => file.module_graph_offset.len, - }; - let read_range = self.get_read_range(file, sub_data_kind, 0, read_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( - &self, - file: &VirtualFile, - pos: u64, - buf: &mut [u8], - ) -> std::io::Result { - let read_range = self.get_read_range( - file, - VfsFileSubDataKind::Raw, - pos, - buf.len() as u64, - )?; - let read_len = read_range.len(); - buf[..read_len].copy_from_slice(&self.vfs_data[read_range]); - Ok(read_len) - } - - fn get_read_range( - &self, - file: &VirtualFile, - sub_data_kind: VfsFileSubDataKind, - pos: u64, - len: u64, - ) -> std::io::Result> { - let file_offset_and_len = match sub_data_kind { - VfsFileSubDataKind::Raw => file.offset, - VfsFileSubDataKind::ModuleGraph => file.module_graph_offset, - }; - if pos > file_offset_and_len.len { - return Err(std::io::Error::new( - std::io::ErrorKind::UnexpectedEof, - "unexpected EOF", - )); - } - let file_offset = - self.fs_root.start_file_offset + file_offset_and_len.offset; - let start = file_offset + pos; - let end = file_offset + std::cmp::min(pos + len, file_offset_and_len.len); - Ok(start as usize..end as usize) - } - - pub fn dir_entry(&self, path: &Path) -> std::io::Result<&VirtualDirectory> { - let (_, entry) = self.fs_root.find_entry(path)?; - match entry { - VfsEntryRef::Dir(dir) => Ok(dir), - VfsEntryRef::Symlink(_) => unreachable!(), - VfsEntryRef::File(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "path is a file", - )), - } - } - - pub fn file_entry(&self, path: &Path) -> std::io::Result<&VirtualFile> { - let (_, entry) = self.fs_root.find_entry(path)?; - match entry { - VfsEntryRef::Dir(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "path is a directory", - )), - VfsEntryRef::Symlink(_) => unreachable!(), - VfsEntryRef::File(file) => Ok(file), - } - } -} - #[cfg(test)] mod test { - use std::io::Write; - use console_static_text::ansi::strip_ansi_codes; - use deno_io::fs::File; - use test_util::assert_contains; + use deno_lib::standalone::virtual_fs::VfsBuilder; use test_util::TempDir; use super::*; - #[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, VfsFileSubDataKind::Raw) - .unwrap() - .into_owned(), - ) - .unwrap() - } - - #[test] - fn builds_and_uses_virtual_fs() { - let temp_dir = TempDir::new(); - // we canonicalize the temp directory because the vfs builder - // will canonicalize the root path - let src_path = temp_dir.path().canonicalize().join("src"); - src_path.create_dir_all(); - src_path.join("sub_dir").create_dir_all(); - src_path.join("e.txt").write("e"); - src_path.symlink_file("e.txt", "sub_dir/e.txt"); - let src_path = src_path.to_path_buf(); - let mut builder = VfsBuilder::new(); - builder - .add_file_with_data_inner( - &src_path.join("a.txt"), - "data".into(), - VfsFileSubDataKind::Raw, - ) - .unwrap(); - builder - .add_file_with_data_inner( - &src_path.join("b.txt"), - "data".into(), - VfsFileSubDataKind::Raw, - ) - .unwrap(); - assert_eq!(builder.files.len(), 1); // because duplicate data - builder - .add_file_with_data_inner( - &src_path.join("c.txt"), - "c".into(), - VfsFileSubDataKind::Raw, - ) - .unwrap(); - builder - .add_file_with_data_inner( - &src_path.join("sub_dir").join("d.txt"), - "d".into(), - VfsFileSubDataKind::Raw, - ) - .unwrap(); - builder.add_file_at_path(&src_path.join("e.txt")).unwrap(); - builder - .add_symlink(&src_path.join("sub_dir").join("e.txt")) - .unwrap(); - - // get the virtual fs - let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); - - assert_eq!(read_file(&virtual_fs, &dest_path.join("a.txt")), "data"); - assert_eq!(read_file(&virtual_fs, &dest_path.join("b.txt")), "data"); - - // attempt reading a symlink - assert_eq!( - read_file(&virtual_fs, &dest_path.join("sub_dir").join("e.txt")), - "e", - ); - - // canonicalize symlink - assert_eq!( - virtual_fs - .canonicalize(&dest_path.join("sub_dir").join("e.txt")) - .unwrap(), - dest_path.join("e.txt"), - ); - - // metadata - assert_eq!( - virtual_fs - .lstat(&dest_path.join("sub_dir").join("e.txt")) - .unwrap() - .file_type, - sys_traits::FileType::Symlink, - ); - assert_eq!( - virtual_fs - .stat(&dest_path.join("sub_dir").join("e.txt")) - .unwrap() - .file_type, - sys_traits::FileType::File, - ); - assert_eq!( - virtual_fs - .stat(&dest_path.join("sub_dir")) - .unwrap() - .file_type, - sys_traits::FileType::Dir, - ); - assert_eq!( - virtual_fs.stat(&dest_path.join("e.txt")).unwrap().file_type, - sys_traits::FileType::File - ); - } - - #[test] - fn test_include_dir_recursive() { - let temp_dir = TempDir::new(); - let temp_dir_path = temp_dir.path().canonicalize(); - temp_dir.create_dir_all("src/nested/sub_dir"); - temp_dir.write("src/a.txt", "data"); - temp_dir.write("src/b.txt", "data"); - util::fs::symlink_dir( - &crate::sys::CliSys::default(), - temp_dir_path.join("src/nested/sub_dir").as_path(), - temp_dir_path.join("src/sub_dir_link").as_path(), - ) - .unwrap(); - temp_dir.write("src/nested/sub_dir/c.txt", "c"); - - // build and create the virtual fs - let src_path = temp_dir_path.join("src").to_path_buf(); - let mut builder = VfsBuilder::new(); - builder.add_dir_recursive(&src_path).unwrap(); - let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); - - assert_eq!(read_file(&virtual_fs, &dest_path.join("a.txt")), "data",); - assert_eq!(read_file(&virtual_fs, &dest_path.join("b.txt")), "data",); - - assert_eq!( - read_file( - &virtual_fs, - &dest_path.join("nested").join("sub_dir").join("c.txt") - ), - "c", - ); - assert_eq!( - read_file(&virtual_fs, &dest_path.join("sub_dir_link").join("c.txt")), - "c", - ); - assert_eq!( - virtual_fs - .lstat(&dest_path.join("sub_dir_link")) - .unwrap() - .file_type, - sys_traits::FileType::Symlink, - ); - - assert_eq!( - virtual_fs - .canonicalize(&dest_path.join("sub_dir_link").join("c.txt")) - .unwrap(), - dest_path.join("nested").join("sub_dir").join("c.txt"), - ); - } - - fn into_virtual_fs( - builder: VfsBuilder, - temp_dir: &TempDir, - ) -> (PathBuf, FileBackedVfs) { - let virtual_fs_file = temp_dir.path().join("virtual_fs"); - let vfs = builder.build(); - { - let mut file = std::fs::File::create(&virtual_fs_file).unwrap(); - for file_data in &vfs.files { - file.write_all(file_data).unwrap(); - } - } - let dest_path = temp_dir.path().join("dest"); - let data = std::fs::read(&virtual_fs_file).unwrap(); - ( - dest_path.to_path_buf(), - FileBackedVfs::new( - Cow::Owned(data), - VfsRoot { - dir: VirtualDirectory { - name: "".to_string(), - entries: vfs.entries, - }, - root_path: dest_path.to_path_buf(), - start_file_offset: 0, - }, - ), - ) - } - - #[test] - fn circular_symlink() { - let temp_dir = TempDir::new(); - let src_path = temp_dir.path().canonicalize().join("src"); - src_path.create_dir_all(); - src_path.symlink_file("a.txt", "b.txt"); - src_path.symlink_file("b.txt", "c.txt"); - src_path.symlink_file("c.txt", "a.txt"); - let src_path = src_path.to_path_buf(); - let mut builder = VfsBuilder::new(); - let err = builder - .add_symlink(src_path.join("a.txt").as_path()) - .unwrap_err(); - assert_contains!(err.to_string(), "Circular symlink detected",); - } - - #[tokio::test] - async fn test_open_file() { - let temp_dir = TempDir::new(); - let temp_path = temp_dir.path().canonicalize(); - let mut builder = VfsBuilder::new(); - builder - .add_file_with_data_inner( - temp_path.join("a.txt").as_path(), - "0123456789".to_string().into_bytes(), - VfsFileSubDataKind::Raw, - ) - .unwrap(); - let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); - let virtual_fs = Arc::new(virtual_fs); - let file = virtual_fs.open_file(&dest_path.join("a.txt")).unwrap(); - file.seek(SeekFrom::Current(2)).unwrap(); - let mut buf = vec![0; 2]; - file.read_to_buf(&mut buf).unwrap(); - assert_eq!(buf, b"23"); - file.read_to_buf(&mut buf).unwrap(); - assert_eq!(buf, b"45"); - file.seek(SeekFrom::Current(-4)).unwrap(); - file.read_to_buf(&mut buf).unwrap(); - assert_eq!(buf, b"23"); - file.seek(SeekFrom::Start(2)).unwrap(); - file.read_to_buf(&mut buf).unwrap(); - assert_eq!(buf, b"23"); - file.seek(SeekFrom::End(2)).unwrap(); - file.read_to_buf(&mut buf).unwrap(); - assert_eq!(buf, b"89"); - file.seek(SeekFrom::Current(-8)).unwrap(); - file.read_to_buf(&mut buf).unwrap(); - assert_eq!(buf, b"23"); - assert_eq!( - file - .seek(SeekFrom::Current(-5)) - .unwrap_err() - .to_string(), - "An attempt was made to move the file pointer before the beginning of the file." - ); - // go beyond the file length, then back - file.seek(SeekFrom::Current(40)).unwrap(); - file.seek(SeekFrom::Current(-38)).unwrap(); - let file = Rc::new(file); - let read_buf = file.clone().read(2).await.unwrap(); - assert_eq!(read_buf.to_vec(), b"67"); - file.clone().seek_sync(SeekFrom::Current(-2)).unwrap(); - - // read to the end of the file - let all_buf = file.clone().read_all_sync().unwrap(); - assert_eq!(all_buf.to_vec(), b"6789"); - file.clone().seek_sync(SeekFrom::Current(-9)).unwrap(); - - // try try_clone_inner and read_all_async - let all_buf = file - .try_clone_inner() - .unwrap() - .read_all_async() - .await - .unwrap(); - assert_eq!(all_buf.to_vec(), b"123456789"); - } - #[test] fn test_vfs_as_display_tree() { let temp_dir = TempDir::new(); diff --git a/cli/sys.rs b/cli/sys.rs deleted file mode 100644 index 718e9981e2..0000000000 --- a/cli/sys.rs +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright 2018-2025 the Deno authors. MIT license. - -// todo(dsherret): this should instead use conditional compilation and directly -// surface the underlying implementation. -// -// The problem atm is that there's no way to have conditional compilation for -// denort or the deno binary. We should extract out denort to a separate binary. - -use std::borrow::Cow; -use std::path::Path; -use std::path::PathBuf; - -use sys_traits::boxed::BoxedFsDirEntry; -use sys_traits::boxed::BoxedFsFile; -use sys_traits::boxed::BoxedFsMetadataValue; -use sys_traits::boxed::FsMetadataBoxed; -use sys_traits::boxed::FsOpenBoxed; -use sys_traits::boxed::FsReadDirBoxed; -use sys_traits::CreateDirOptions; - -use crate::standalone::DenoCompileFileSystem; - -#[derive(Debug, Clone)] -pub enum CliSys { - #[allow(dead_code)] // will be dead code for denort - #[allow(clippy::disallowed_types)] // ok because sys impl - Real(sys_traits::impls::RealSys), - #[allow(dead_code)] // will be dead code for deno - DenoCompile(DenoCompileFileSystem), -} - -impl Default for CliSys { - fn default() -> Self { - Self::Real(sys_traits::impls::RealSys) - } -} - -impl deno_runtime::deno_node::ExtNodeSys for CliSys {} - -impl sys_traits::BaseFsCloneFile for CliSys { - fn base_fs_clone_file(&self, src: &Path, dst: &Path) -> std::io::Result<()> { - match self { - Self::Real(sys) => sys.base_fs_clone_file(src, dst), - Self::DenoCompile(sys) => sys.base_fs_clone_file(src, dst), - } - } -} - -impl sys_traits::BaseFsSymlinkDir for CliSys { - fn base_fs_symlink_dir(&self, src: &Path, dst: &Path) -> std::io::Result<()> { - match self { - Self::Real(sys) => sys.base_fs_symlink_dir(src, dst), - Self::DenoCompile(sys) => sys.base_fs_symlink_dir(src, dst), - } - } -} - -impl sys_traits::BaseFsCopy for CliSys { - fn base_fs_copy(&self, src: &Path, dst: &Path) -> std::io::Result { - match self { - Self::Real(sys) => sys.base_fs_copy(src, dst), - Self::DenoCompile(sys) => sys.base_fs_copy(src, dst), - } - } -} - -impl sys_traits::BaseFsHardLink for CliSys { - fn base_fs_hard_link(&self, src: &Path, dst: &Path) -> std::io::Result<()> { - match self { - Self::Real(sys) => sys.base_fs_hard_link(src, dst), - Self::DenoCompile(sys) => sys.base_fs_hard_link(src, dst), - } - } -} - -impl sys_traits::BaseFsRead for CliSys { - fn base_fs_read(&self, p: &Path) -> std::io::Result> { - match self { - Self::Real(sys) => sys.base_fs_read(p), - Self::DenoCompile(sys) => sys.base_fs_read(p), - } - } -} - -impl sys_traits::BaseFsReadDir for CliSys { - type ReadDirEntry = BoxedFsDirEntry; - - fn base_fs_read_dir( - &self, - p: &Path, - ) -> std::io::Result< - Box> + '_>, - > { - match self { - Self::Real(sys) => sys.fs_read_dir_boxed(p), - Self::DenoCompile(sys) => sys.fs_read_dir_boxed(p), - } - } -} - -impl sys_traits::BaseFsCanonicalize for CliSys { - fn base_fs_canonicalize(&self, p: &Path) -> std::io::Result { - match self { - Self::Real(sys) => sys.base_fs_canonicalize(p), - Self::DenoCompile(sys) => sys.base_fs_canonicalize(p), - } - } -} - -impl sys_traits::BaseFsMetadata for CliSys { - type Metadata = BoxedFsMetadataValue; - - fn base_fs_metadata(&self, path: &Path) -> std::io::Result { - match self { - Self::Real(sys) => sys.fs_metadata_boxed(path), - Self::DenoCompile(sys) => sys.fs_metadata_boxed(path), - } - } - - fn base_fs_symlink_metadata( - &self, - path: &Path, - ) -> std::io::Result { - match self { - Self::Real(sys) => sys.fs_symlink_metadata_boxed(path), - Self::DenoCompile(sys) => sys.fs_symlink_metadata_boxed(path), - } - } -} - -impl sys_traits::BaseFsCreateDir for CliSys { - fn base_fs_create_dir( - &self, - p: &Path, - options: &CreateDirOptions, - ) -> std::io::Result<()> { - match self { - Self::Real(sys) => sys.base_fs_create_dir(p, options), - Self::DenoCompile(sys) => sys.base_fs_create_dir(p, options), - } - } -} - -impl sys_traits::BaseFsOpen for CliSys { - type File = BoxedFsFile; - - fn base_fs_open( - &self, - path: &Path, - options: &sys_traits::OpenOptions, - ) -> std::io::Result { - match self { - Self::Real(sys) => sys.fs_open_boxed(path, options), - Self::DenoCompile(sys) => sys.fs_open_boxed(path, options), - } - } -} - -impl sys_traits::BaseFsRemoveFile for CliSys { - fn base_fs_remove_file(&self, p: &Path) -> std::io::Result<()> { - match self { - Self::Real(sys) => sys.base_fs_remove_file(p), - Self::DenoCompile(sys) => sys.base_fs_remove_file(p), - } - } -} - -impl sys_traits::BaseFsRename for CliSys { - fn base_fs_rename(&self, old: &Path, new: &Path) -> std::io::Result<()> { - match self { - Self::Real(sys) => sys.base_fs_rename(old, new), - Self::DenoCompile(sys) => sys.base_fs_rename(old, new), - } - } -} - -impl sys_traits::SystemRandom for CliSys { - fn sys_random(&self, buf: &mut [u8]) -> std::io::Result<()> { - match self { - Self::Real(sys) => sys.sys_random(buf), - Self::DenoCompile(sys) => sys.sys_random(buf), - } - } -} - -impl sys_traits::SystemTimeNow for CliSys { - fn sys_time_now(&self) -> std::time::SystemTime { - match self { - Self::Real(sys) => sys.sys_time_now(), - Self::DenoCompile(sys) => sys.sys_time_now(), - } - } -} - -impl sys_traits::ThreadSleep for CliSys { - fn thread_sleep(&self, dur: std::time::Duration) { - match self { - Self::Real(sys) => sys.thread_sleep(dur), - Self::DenoCompile(sys) => sys.thread_sleep(dur), - } - } -} - -impl sys_traits::EnvCurrentDir for CliSys { - fn env_current_dir(&self) -> std::io::Result { - match self { - Self::Real(sys) => sys.env_current_dir(), - Self::DenoCompile(sys) => sys.env_current_dir(), - } - } -} - -impl sys_traits::BaseEnvVar for CliSys { - fn base_env_var_os( - &self, - key: &std::ffi::OsStr, - ) -> Option { - match self { - Self::Real(sys) => sys.base_env_var_os(key), - Self::DenoCompile(sys) => sys.base_env_var_os(key), - } - } -} - -impl sys_traits::EnvHomeDir for CliSys { - fn env_home_dir(&self) -> Option { - #[allow(clippy::disallowed_types)] // ok because sys impl - sys_traits::impls::RealSys.env_home_dir() - } -} diff --git a/cli/task_runner.rs b/cli/task_runner.rs index 8510a650e7..14e850ee76 100644 --- a/cli/task_runner.rs +++ b/cli/task_runner.rs @@ -25,9 +25,8 @@ use tokio::task::LocalSet; use tokio_util::sync::CancellationToken; use crate::node::CliNodeResolver; +use crate::npm::CliManagedNpmResolver; use crate::npm::CliNpmResolver; -use crate::npm::InnerCliNpmResolverRef; -use crate::npm::ManagedCliNpmResolver; pub fn get_script_with_args(script: &str, argv: &[String]) -> String { let additional_args = argv @@ -414,15 +413,15 @@ impl ShellCommand for NodeModulesFileRunCommand { } pub fn resolve_custom_commands( - npm_resolver: &dyn CliNpmResolver, + npm_resolver: &CliNpmResolver, node_resolver: &CliNodeResolver, ) -> Result>, AnyError> { - let mut commands = match npm_resolver.as_inner() { - InnerCliNpmResolverRef::Byonm(npm_resolver) => { + let mut commands = match npm_resolver { + CliNpmResolver::Byonm(npm_resolver) => { let node_modules_dir = npm_resolver.root_node_modules_path().unwrap(); resolve_npm_commands_from_bin_dir(node_modules_dir) } - InnerCliNpmResolverRef::Managed(npm_resolver) => { + CliNpmResolver::Managed(npm_resolver) => { resolve_managed_npm_commands(npm_resolver, node_resolver)? } }; @@ -521,13 +520,12 @@ fn resolve_execution_path_from_npx_shim( } fn resolve_managed_npm_commands( - npm_resolver: &ManagedCliNpmResolver, + npm_resolver: &CliManagedNpmResolver, node_resolver: &CliNodeResolver, ) -> Result>, AnyError> { let mut result = HashMap::new(); - let snapshot = npm_resolver.snapshot(); - for id in snapshot.top_level_packages() { - let package_folder = npm_resolver.resolve_pkg_folder_from_pkg_id(id)?; + for id in npm_resolver.resolution().top_level_packages() { + let package_folder = npm_resolver.resolve_pkg_folder_from_pkg_id(&id)?; let bin_commands = node_resolver.resolve_binary_commands(&package_folder)?; for bin_command in bin_commands { @@ -598,7 +596,7 @@ async fn listen_ctrl_c(kill_signal: KillSignal) { #[cfg(unix)] async fn listen_and_forward_all_signals(kill_signal: KillSignal) { use deno_core::futures::FutureExt; - use deno_runtime::signal::SIGNAL_NUMS; + use deno_runtime::deno_os::signal::SIGNAL_NUMS; // listen and forward every signal we support let mut futures = Vec::with_capacity(SIGNAL_NUMS.len()); diff --git a/cli/tools/bench/mod.rs b/cli/tools/bench/mod.rs index 6a57c4ce6c..a316e60b52 100644 --- a/cli/tools/bench/mod.rs +++ b/cli/tools/bench/mod.rs @@ -48,6 +48,7 @@ use crate::util::fs::collect_specifiers; use crate::util::path::is_script_ext; use crate::util::path::matches_pattern_or_exact_path; use crate::worker::CliMainWorkerFactory; +use crate::worker::CreateCustomWorkerError; mod mitata; mod reporters; @@ -164,7 +165,7 @@ async fn bench_specifier( .await { Ok(()) => Ok(()), - Err(CoreError::Js(error)) => { + Err(CreateCustomWorkerError::Core(CoreError::Js(error))) => { sender.send(BenchEvent::UncaughtError( specifier.to_string(), Box::new(error), @@ -182,7 +183,7 @@ async fn bench_specifier_inner( specifier: ModuleSpecifier, sender: &UnboundedSender, filter: TestFilter, -) -> Result<(), CoreError> { +) -> Result<(), CreateCustomWorkerError> { let mut worker = worker_factory .create_custom_worker( WorkerExecutionMode::Bench, @@ -201,7 +202,7 @@ async fn bench_specifier_inner( // Ensure that there are no pending exceptions before we start running tests worker.run_up_to_duration(Duration::from_millis(0)).await?; - worker.dispatch_load_event()?; + worker.dispatch_load_event().map_err(CoreError::Js)?; let benchmarks = { let state_rc = worker.js_runtime.op_state(); @@ -236,11 +237,13 @@ async fn bench_specifier_inner( used_only, names: benchmarks.iter().map(|(d, _)| d.name.clone()).collect(), })) - .map_err(JsErrorBox::from_err)?; + .map_err(JsErrorBox::from_err) + .map_err(CoreError::JsBox)?; for (desc, function) in benchmarks { sender .send(BenchEvent::Wait(desc.id)) - .map_err(JsErrorBox::from_err)?; + .map_err(JsErrorBox::from_err) + .map_err(CoreError::JsBox)?; let call = worker.js_runtime.call(&function); let result = worker .js_runtime @@ -249,18 +252,26 @@ async fn bench_specifier_inner( let scope = &mut worker.js_runtime.handle_scope(); let result = v8::Local::new(scope, result); let result = serde_v8::from_v8::(scope, result) - .map_err(JsErrorBox::from_err)?; + .map_err(JsErrorBox::from_err) + .map_err(CoreError::JsBox)?; sender .send(BenchEvent::Result(desc.id, result)) - .map_err(JsErrorBox::from_err)?; + .map_err(JsErrorBox::from_err) + .map_err(CoreError::JsBox)?; } // Ignore `defaultPrevented` of the `beforeunload` event. We don't allow the // event loop to continue beyond what's needed to await results. - worker.dispatch_beforeunload_event()?; - worker.dispatch_process_beforeexit_event()?; - worker.dispatch_unload_event()?; - worker.dispatch_process_exit_event()?; + worker + .dispatch_beforeunload_event() + .map_err(CoreError::Js)?; + worker + .dispatch_process_beforeexit_event() + .map_err(CoreError::Js)?; + worker.dispatch_unload_event().map_err(CoreError::Js)?; + worker + .dispatch_process_exit_event() + .map_err(CoreError::Js)?; // Ensure the worker has settled so we can catch any remaining unhandled rejections. We don't // want to wait forever here. diff --git a/cli/tools/bench/reporters.rs b/cli/tools/bench/reporters.rs index 68a0c56bce..c3df53b76a 100644 --- a/cli/tools/bench/reporters.rs +++ b/cli/tools/bench/reporters.rs @@ -1,10 +1,10 @@ // Copyright 2018-2025 the Deno authors. MIT license. +use deno_lib::version::DENO_VERSION_INFO; use serde::Serialize; use super::*; use crate::tools::test::TestFailureFormatOptions; -use crate::version; pub trait BenchReporter { fn report_group_summary(&mut self); @@ -31,11 +31,7 @@ impl Default for JsonReporterOutput { fn default() -> Self { Self { version: JSON_SCHEMA_VERSION, - runtime: format!( - "{} {}", - version::DENO_VERSION_INFO.user_agent, - env!("TARGET") - ), + runtime: format!("{} {}", DENO_VERSION_INFO.user_agent, env!("TARGET")), cpu: mitata::cpu::name(), benches: vec![], } @@ -163,7 +159,7 @@ impl BenchReporter for ConsoleReporter { "{}\n", colors::gray(format!( "Runtime | Deno {} ({})", - crate::version::DENO_VERSION_INFO.deno, + DENO_VERSION_INFO.deno, env!("TARGET") )) ); diff --git a/cli/tools/check.rs b/cli/tools/check.rs index 53fd5c5db9..d8128432b3 100644 --- a/cli/tools/check.rs +++ b/cli/tools/check.rs @@ -13,6 +13,8 @@ use deno_graph::Module; use deno_graph::ModuleError; use deno_graph::ModuleGraph; use deno_graph::ModuleLoadError; +use deno_lib::util::hash::FastInsecureHasher; +use deno_semver::npm::NpmPackageNvReference; use deno_terminal::colors; use once_cell::sync::Lazy; use regex::Regex; @@ -27,13 +29,13 @@ use crate::args::TsTypeLib; use crate::args::TypeCheckMode; use crate::cache::CacheDBHash; use crate::cache::Caches; -use crate::cache::FastInsecureHasher; use crate::cache::TypeCheckCache; use crate::factory::CliFactory; use crate::graph_util::maybe_additional_sloppy_imports_message; use crate::graph_util::BuildFastCheckGraphOptions; use crate::graph_util::ModuleGraphBuilder; use crate::node::CliNodeResolver; +use crate::npm::installer::NpmInstaller; use crate::npm::CliNpmResolver; use crate::sys::CliSys; use crate::tsc; @@ -109,8 +111,9 @@ pub struct TypeChecker { cjs_tracker: Arc, cli_options: Arc, module_graph_builder: Arc, + npm_installer: Option>, node_resolver: Arc, - npm_resolver: Arc, + npm_resolver: CliNpmResolver, sys: CliSys, } @@ -136,13 +139,15 @@ pub enum CheckError { } impl TypeChecker { + #[allow(clippy::too_many_arguments)] pub fn new( caches: Arc, cjs_tracker: Arc, cli_options: Arc, module_graph_builder: Arc, node_resolver: Arc, - npm_resolver: Arc, + npm_installer: Option>, + npm_resolver: CliNpmResolver, sys: CliSys, ) -> Self { Self { @@ -151,6 +156,7 @@ impl TypeChecker { cli_options, module_graph_builder, node_resolver, + npm_installer, npm_resolver, sys, } @@ -184,6 +190,29 @@ impl TypeChecker { mut graph: ModuleGraph, options: CheckOptions, ) -> Result<(Arc, Diagnostics), CheckError> { + fn check_state_hash(resolver: &CliNpmResolver) -> Option { + match resolver { + CliNpmResolver::Byonm(_) => { + // not feasible and probably slower to compute + None + } + CliNpmResolver::Managed(resolver) => { + // we should probably go further and check all the individual npm packages + let mut package_reqs = resolver.resolution().package_reqs(); + package_reqs.sort_by(|a, b| a.0.cmp(&b.0)); // determinism + let mut hasher = FastInsecureHasher::new_without_deno_version(); + // ensure the cache gets busted when turning nodeModulesDir on or off + // as this could cause changes in resolution + hasher.write_hashable(resolver.root_node_modules_path().is_some()); + for (pkg_req, pkg_nv) in package_reqs { + hasher.write_hashable(&pkg_req); + hasher.write_hashable(&pkg_nv); + } + Some(hasher.finish()) + } + } + } + if !options.type_check_mode.is_true() || graph.roots.is_empty() { return Ok((graph.into(), Default::default())); } @@ -191,9 +220,9 @@ impl TypeChecker { // node built-in specifiers use the @types/node package to determine // types, so inject that now (the caller should do this after the lockfile // has been written) - if let Some(npm_resolver) = self.npm_resolver.as_managed() { + if let Some(npm_installer) = &self.npm_installer { if graph.has_node_specifier { - npm_resolver.inject_synthetic_types_node_package().await?; + npm_installer.inject_synthetic_types_node_package().await?; } } @@ -233,9 +262,11 @@ impl TypeChecker { maybe_check_hash, } = get_tsc_roots( &self.sys, + &self.npm_resolver, + &self.node_resolver, &graph, check_js, - self.npm_resolver.check_state_hash(), + check_state_hash(&self.npm_resolver), type_check_mode, &ts_config, ); @@ -345,8 +376,11 @@ struct TscRoots { /// redirects resolved. We need to include all the emittable files in /// the roots, so they get type checked and optionally emitted, /// otherwise they would be ignored if only imported into JavaScript. +#[allow(clippy::too_many_arguments)] fn get_tsc_roots( sys: &CliSys, + npm_resolver: &CliNpmResolver, + node_resolver: &CliNodeResolver, graph: &ModuleGraph, check_js: bool, npm_cache_state_hash: Option, @@ -429,6 +463,7 @@ fn get_tsc_roots( if let Some(hasher) = hasher { hasher.write_str(module.specifier.as_str()); } + None } } @@ -465,17 +500,33 @@ fn get_tsc_roots( let mut pending = VecDeque::new(); // put in the global types first so that they're resolved before anything else - let get_import_specifiers = || { - graph - .imports + for (referrer, import) in graph.imports.iter() { + for specifier in import + .dependencies .values() - .flat_map(|i| i.dependencies.values()) .filter_map(|dep| dep.get_type().or_else(|| dep.get_code())) - }; - for specifier in get_import_specifiers() { - let specifier = graph.resolve(specifier); - if seen.insert(specifier) { - pending.push_back((specifier, false)); + { + let specifier = graph.resolve(specifier); + if seen.insert(specifier) { + if let Ok(nv_ref) = NpmPackageNvReference::from_specifier(specifier) { + let Some(resolved) = + resolve_npm_nv_ref(npm_resolver, node_resolver, &nv_ref, referrer) + else { + result.missing_diagnostics.push( + tsc::Diagnostic::from_missing_error( + specifier, + None, + maybe_additional_sloppy_imports_message(sys, specifier), + ), + ); + continue; + }; + let mt = MediaType::from_specifier(&resolved); + result.roots.push((resolved, mt)); + } else { + pending.push_back((specifier, false)); + } + } } } @@ -596,6 +647,29 @@ fn get_tsc_roots( result } +fn resolve_npm_nv_ref( + npm_resolver: &CliNpmResolver, + node_resolver: &CliNodeResolver, + nv_ref: &NpmPackageNvReference, + referrer: &ModuleSpecifier, +) -> Option { + let pkg_dir = npm_resolver + .as_managed() + .unwrap() + .resolve_pkg_folder_from_deno_module(nv_ref.nv()) + .ok()?; + let resolved = node_resolver + .resolve_package_subpath_from_deno_module( + &pkg_dir, + nv_ref.sub_path(), + Some(referrer), + node_resolver::ResolutionMode::Import, + node_resolver::NodeResolutionKind::Types, + ) + .ok()?; + Some(resolved) +} + /// Matches the `@ts-check` pragma. static TS_CHECK_RE: Lazy = lazy_regex::lazy_regex!(r#"(?i)^\s*@ts-check(?:\s+|$)"#); diff --git a/cli/tools/coverage/mod.rs b/cli/tools/coverage/mod.rs index 06afa5fac2..53c08191f7 100644 --- a/cli/tools/coverage/mod.rs +++ b/cli/tools/coverage/mod.rs @@ -18,10 +18,13 @@ use deno_config::glob::PathOrPatternSet; use deno_core::anyhow::anyhow; use deno_core::anyhow::Context; use deno_core::error::AnyError; +use deno_core::error::CoreError; use deno_core::serde_json; use deno_core::sourcemap::SourceMap; use deno_core::url::Url; use deno_core::LocalInspectorSession; +use deno_error::JsErrorBox; +use deno_resolver::npm::DenoInNpmPackageChecker; use node_resolver::InNpmPackageChecker; use regex::Regex; use text_lines::TextLines; @@ -52,7 +55,7 @@ pub struct CoverageCollector { #[async_trait::async_trait(?Send)] impl crate::worker::CoverageCollector for CoverageCollector { - async fn start_collecting(&mut self) -> Result<(), AnyError> { + async fn start_collecting(&mut self) -> Result<(), CoreError> { self.enable_debugger().await?; self.enable_profiler().await?; self @@ -66,7 +69,7 @@ impl crate::worker::CoverageCollector for CoverageCollector { Ok(()) } - async fn stop_collecting(&mut self) -> Result<(), AnyError> { + async fn stop_collecting(&mut self) -> Result<(), CoreError> { fs::create_dir_all(&self.dir)?; let script_coverages = self.take_precise_coverage().await?.result; @@ -87,7 +90,8 @@ impl crate::worker::CoverageCollector for CoverageCollector { let filepath = self.dir.join(filename); let mut out = BufWriter::new(File::create(&filepath)?); - let coverage = serde_json::to_string(&script_coverage)?; + let coverage = serde_json::to_string(&script_coverage) + .map_err(JsErrorBox::from_err)?; let formatted_coverage = format_json(&filepath, &coverage, &Default::default()) .ok() @@ -110,7 +114,7 @@ impl CoverageCollector { Self { dir, session } } - async fn enable_debugger(&mut self) -> Result<(), AnyError> { + async fn enable_debugger(&mut self) -> Result<(), CoreError> { self .session .post_message::<()>("Debugger.enable", None) @@ -118,7 +122,7 @@ impl CoverageCollector { Ok(()) } - async fn enable_profiler(&mut self) -> Result<(), AnyError> { + async fn enable_profiler(&mut self) -> Result<(), CoreError> { self .session .post_message::<()>("Profiler.enable", None) @@ -126,7 +130,7 @@ impl CoverageCollector { Ok(()) } - async fn disable_debugger(&mut self) -> Result<(), AnyError> { + async fn disable_debugger(&mut self) -> Result<(), CoreError> { self .session .post_message::<()>("Debugger.disable", None) @@ -134,7 +138,7 @@ impl CoverageCollector { Ok(()) } - async fn disable_profiler(&mut self) -> Result<(), AnyError> { + async fn disable_profiler(&mut self) -> Result<(), CoreError> { self .session .post_message::<()>("Profiler.disable", None) @@ -145,26 +149,28 @@ impl CoverageCollector { async fn start_precise_coverage( &mut self, parameters: cdp::StartPreciseCoverageArgs, - ) -> Result { + ) -> Result { let return_value = self .session .post_message("Profiler.startPreciseCoverage", Some(parameters)) .await?; - let return_object = serde_json::from_value(return_value)?; + let return_object = + serde_json::from_value(return_value).map_err(JsErrorBox::from_err)?; Ok(return_object) } async fn take_precise_coverage( &mut self, - ) -> Result { + ) -> Result { let return_value = self .session .post_message::<()>("Profiler.takePreciseCoverage", None) .await?; - let return_object = serde_json::from_value(return_value)?; + let return_object = + serde_json::from_value(return_value).map_err(JsErrorBox::from_err)?; Ok(return_object) } @@ -464,7 +470,7 @@ fn filter_coverages( coverages: Vec, include: Vec, exclude: Vec, - in_npm_pkg_checker: &dyn InNpmPackageChecker, + in_npm_pkg_checker: &DenoInNpmPackageChecker, ) -> Vec { let include: Vec = include.iter().map(|e| Regex::new(e).unwrap()).collect(); @@ -532,7 +538,7 @@ pub fn cover_files( script_coverages, coverage_flags.include, coverage_flags.exclude, - in_npm_pkg_checker.as_ref(), + in_npm_pkg_checker, ); if script_coverages.is_empty() { return Err(anyhow!("No covered files included in the report")); diff --git a/cli/tools/coverage/reporter.rs b/cli/tools/coverage/reporter.rs index bc6e85b47e..4f0325117c 100644 --- a/cli/tools/coverage/reporter.rs +++ b/cli/tools/coverage/reporter.rs @@ -11,6 +11,7 @@ use std::path::PathBuf; use deno_core::error::AnyError; use deno_core::url::Url; +use deno_lib::version::DENO_VERSION_INFO; use super::util; use super::CoverageReport; @@ -559,7 +560,7 @@ impl HtmlCoverageReporter { /// Creates footer part of the contents for html report. pub fn create_html_footer(&self, now: &str) -> String { - let version = env!("CARGO_PKG_VERSION"); + let version = DENO_VERSION_INFO.deno; format!( "