From f5140a3e71da1a657d9bf41ae9bd76574a33b701 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 4 Nov 2023 16:05:12 -0700 Subject: [PATCH] new: Support proto v0.22 release. (#17) --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 17 + Cargo.lock | 32 +- Cargo.toml | 6 +- crates/node-depman/Cargo.toml | 2 +- crates/node-depman/src/lib.rs | 6 + crates/node-depman/src/npm_registry.rs | 32 ++ crates/node-depman/src/package_manager.rs | 63 +++ crates/node-depman/src/proto.rs | 487 +++++++++--------- crates/node-depman/tests/download_test.rs | 119 +---- crates/node-depman/tests/shims_test.rs | 6 +- ...ims_test__npm__creates_global_shims-2.snap | 4 +- ...ims_test__npm__creates_global_shims-3.snap | 13 + ...ms_test__pnpm__creates_global_shims-2.snap | 2 +- ...ms_test__yarn__creates_global_shims-2.snap | 2 +- crates/node/Cargo.toml | 2 +- crates/node/src/proto.rs | 243 +++++---- crates/node/tests/download_test.rs | 12 +- crates/node/tests/shims_test.rs | 2 +- .../shims_test__creates_global_shims-2.snap | 13 - .../shims_test__creates_global_shims.snap | 2 +- 21 files changed, 540 insertions(+), 526 deletions(-) create mode 100644 crates/node-depman/src/npm_registry.rs create mode 100644 crates/node-depman/src/package_manager.rs create mode 100644 crates/node-depman/tests/snapshots/shims_test__npm__creates_global_shims-3.snap delete mode 100644 crates/node/tests/snapshots/shims_test__creates_global_shims-2.snap diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1836a51..6519476 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,7 @@ jobs: - uses: moonrepo/setup-rust@v1 with: bins: cargo-wasi, cargo-nextest + cache: false - uses: moonrepo/setup-toolchain@v0 - run: cargo wasi build -p node_plugin - run: cargo wasi build -p node_depman_plugin diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a9bf0..f828849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.5.0 + +#### 💥 Breaking + +- Updated the `npm` tool to create the `npx` shim instead of the `node` tool. +- Updated executable detection for package managers to use the shell scripts instead of the source `.js` files (when applicable). + - Previously we would execute the JS file with node: `node ./bin/npm-cli.js` + - Now we execute the shell script: `./bin/npm` (unix), `./bin/npm.cmd` (windows) + +#### 🚀 Updates + +- Updated to support proto v0.22 release. + +#### ⚙️ Internal + +- Updated dependencies. + ## 0.4.3 #### 🐞 Fixes diff --git a/Cargo.lock b/Cargo.lock index 8720e8b..da0ec08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1629,9 +1629,9 @@ dependencies = [ [[package]] name = "json_comments" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ee439ee368ba4a77ac70d04f14015415af8600d6c894dc1f11bd79758c57d5" +checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105" [[package]] name = "lazy_static" @@ -1850,7 +1850,7 @@ dependencies = [ [[package]] name = "node_depman_plugin" -version = "0.4.3" +version = "0.5.0" dependencies = [ "extism-pdk", "node_common", @@ -1865,7 +1865,7 @@ dependencies = [ [[package]] name = "node_plugin" -version = "0.4.3" +version = "0.5.0" dependencies = [ "extism-pdk", "node_common", @@ -2104,9 +2104,9 @@ dependencies = [ [[package]] name = "proto_core" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515eabc6b74a9a75d4b5e3db86f96697609d4202c31e0e6cb0cb92e5d253b775" +checksum = "857645ab2fe78d570056938494f135a33edd8788a6ec898188325b385d29671e" dependencies = [ "cached", "extism", @@ -2135,9 +2135,9 @@ dependencies = [ [[package]] name = "proto_pdk" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0b4fe64801579bbf1c341db7c6bf9b67cd2d58e3bf32d28b6fd69908cfe102" +checksum = "a67a488f4dfb26b6fc53378526de346d09ba2b511883204992fdf3acebb24d16" dependencies = [ "anyhow", "extism-pdk", @@ -2147,9 +2147,9 @@ dependencies = [ [[package]] name = "proto_pdk_api" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d49bf81e0c2033a4660270a2c8ff6f331ba0cb61500c6d3dc41d8f18cd5ce3" +checksum = "2c4cafcba5b79560a3bfba87ac7fe6fca1e7970e7459a2b60989dc41e152a9ad" dependencies = [ "anyhow", "semver", @@ -2163,9 +2163,9 @@ dependencies = [ [[package]] name = "proto_pdk_test_utils" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb47a6bf9be024ae8690f62e6e9c3e0c2537e9071c1562a74564bf70378b8a80" +checksum = "2e6c0a69d1302bd65f95043434d90b6e5cb9ff0aeb7bef7cd91f7fc49b9973ad" dependencies = [ "extism", "proto_core", @@ -2845,9 +2845,9 @@ dependencies = [ [[package]] name = "starbase_utils" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf731068b919569efae7f6c7c6c3d2e29411c1cba666ef0c6fe20b82f50a1" +checksum = "d948dcd63f1dd11f2b7a9ab15c53e7866faa11ccd31377dca1625c106d40fcee" dependencies = [ "dirs 5.0.1", "fs4", @@ -2943,9 +2943,9 @@ dependencies = [ [[package]] name = "system_env" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9eb72230d28d8116aab631232e98675a7fd3e69b81b046d54205bd21e16d5" +checksum = "c1545ee71941615742defd942059f99d35bfd6e1e079d0f6502e020c4b60a928" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index a76d4e1..559bb3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,9 @@ members = ["crates/*"] [workspace.dependencies] extism-pdk = "0.3.4" -proto_pdk = { version = "0.9.0" } # , path = "../../proto/crates/pdk" } -proto_pdk_api = { version = "0.9.0" } # , path = "../../proto/crates/pdk-api" } -proto_pdk_test_utils = { version = "0.9.1" } # , path = "../../proto/crates/pdk-test-utils" } +proto_pdk = { version = "0.10.0" } # , path = "../../proto/crates/pdk" } +proto_pdk_api = { version = "0.10.0" } # , path = "../../proto/crates/pdk-api" } +proto_pdk_test_utils = { version = "0.10.0" } # , path = "../../proto/crates/pdk-test-utils" } regex = { version = "1.10.2", default-features = false, features = [ "std", "unicode", diff --git a/crates/node-depman/Cargo.toml b/crates/node-depman/Cargo.toml index e9a4d7b..4870a36 100644 --- a/crates/node-depman/Cargo.toml +++ b/crates/node-depman/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node_depman_plugin" -version = "0.4.3" +version = "0.5.0" edition = "2021" license = "MIT" publish = false diff --git a/crates/node-depman/src/lib.rs b/crates/node-depman/src/lib.rs index 4311d51..2a3d839 100644 --- a/crates/node-depman/src/lib.rs +++ b/crates/node-depman/src/lib.rs @@ -1,6 +1,12 @@ // WASM cannot be executed through the test runner and we need to avoid building // WASM code for non-WASM targets. We can solve both of these with a cfg flag. +#[cfg(not(test))] +mod npm_registry; + +#[cfg(not(test))] +mod package_manager; + #[cfg(not(test))] mod proto; diff --git a/crates/node-depman/src/npm_registry.rs b/crates/node-depman/src/npm_registry.rs new file mode 100644 index 0000000..952f045 --- /dev/null +++ b/crates/node-depman/src/npm_registry.rs @@ -0,0 +1,32 @@ +#![allow(dead_code)] + +use extism_pdk::{json, Error, HttpResponse}; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Deserialize)] +pub struct RegistryVersion { + pub version: String, // No v prefix +} + +#[derive(Deserialize)] +pub struct RegistryResponse { + #[serde(rename = "dist-tags")] + pub dist_tags: HashMap, + pub versions: HashMap, +} + +pub fn parse_registry_response( + res: HttpResponse, + is_yarn: bool, +) -> Result { + if !is_yarn { + return res.json(); + } + + // https://github.com/moonrepo/proto/issues/257 + let pattern = regex::bytes::Regex::new("[\u{0000}-\u{001F}]+").unwrap(); + let body = res.body(); + + Ok(json::from_slice(&pattern.replace_all(&body, b""))?) +} diff --git a/crates/node-depman/src/package_manager.rs b/crates/node-depman/src/package_manager.rs new file mode 100644 index 0000000..2b1446f --- /dev/null +++ b/crates/node-depman/src/package_manager.rs @@ -0,0 +1,63 @@ +#![allow(dead_code)] + +use proto_pdk::{get_tool_id, UnresolvedVersionSpec}; +use std::fmt; + +#[derive(PartialEq)] +pub enum PackageManager { + Npm, + Pnpm, + Yarn, +} + +impl PackageManager { + pub fn detect() -> PackageManager { + let id = get_tool_id(); + + if id.to_lowercase().contains("yarn") { + PackageManager::Yarn + } else if id.to_lowercase().contains("pnpm") { + PackageManager::Pnpm + } else { + PackageManager::Npm + } + } + + pub fn get_package_name(&self, version: impl AsRef) -> String { + if self.is_yarn_berry(version.as_ref()) { + "@yarnpkg/cli-dist".into() + } else { + self.to_string() + } + } + + pub fn is_yarn_classic(&self, version: impl AsRef) -> bool { + matches!(self, PackageManager::Yarn) + && match version.as_ref() { + UnresolvedVersionSpec::Alias(alias) => alias == "legacy" || alias == "classic", + UnresolvedVersionSpec::Version(ver) => ver.major == 1, + UnresolvedVersionSpec::Req(req) => req.comparators.iter().any(|c| c.major == 1), + _ => false, + } + } + + pub fn is_yarn_berry(&self, version: impl AsRef) -> bool { + matches!(self, PackageManager::Yarn) + && match version.as_ref() { + UnresolvedVersionSpec::Alias(alias) => alias == "berry" || alias == "latest", + UnresolvedVersionSpec::Version(ver) => ver.major > 1, + UnresolvedVersionSpec::Req(req) => req.comparators.iter().any(|c| c.major > 1), + _ => false, + } + } +} + +impl fmt::Display for PackageManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PackageManager::Npm => write!(f, "npm"), + PackageManager::Pnpm => write!(f, "pnpm"), + PackageManager::Yarn => write!(f, "yarn"), + } + } +} diff --git a/crates/node-depman/src/proto.rs b/crates/node-depman/src/proto.rs index 91b4588..d074167 100644 --- a/crates/node-depman/src/proto.rs +++ b/crates/node-depman/src/proto.rs @@ -1,9 +1,9 @@ +use crate::npm_registry::parse_registry_response; +use crate::package_manager::PackageManager; use extism_pdk::*; use node_common::{commands, BinField, NodeDistVersion, PackageJson}; use proto_pdk::*; -use serde::Deserialize; use std::collections::HashMap; -use std::fmt; use std::fs; use std::path::PathBuf; @@ -14,65 +14,6 @@ extern "ExtismHost" { fn host_log(input: Json); } -#[derive(PartialEq)] -enum PackageManager { - Npm, - Pnpm, - Yarn, -} - -impl PackageManager { - pub fn detect() -> PackageManager { - let id = get_tool_id(); - - if id.to_lowercase().contains("yarn") { - PackageManager::Yarn - } else if id.to_lowercase().contains("pnpm") { - PackageManager::Pnpm - } else { - PackageManager::Npm - } - } - - pub fn get_package_name(&self, version: impl AsRef) -> String { - if self.is_yarn_berry(version.as_ref()) { - "@yarnpkg/cli-dist".into() - } else { - self.to_string() - } - } - - pub fn is_yarn_classic(&self, version: impl AsRef) -> bool { - matches!(self, PackageManager::Yarn) - && match version.as_ref() { - UnresolvedVersionSpec::Alias(alias) => alias == "legacy" || alias == "classic", - UnresolvedVersionSpec::Version(ver) => ver.major == 1, - UnresolvedVersionSpec::Req(req) => req.comparators.iter().any(|c| c.major == 1), - _ => false, - } - } - - pub fn is_yarn_berry(&self, version: impl AsRef) -> bool { - matches!(self, PackageManager::Yarn) - && match version.as_ref() { - UnresolvedVersionSpec::Alias(alias) => alias == "berry" || alias == "latest", - UnresolvedVersionSpec::Version(ver) => ver.major > 1, - UnresolvedVersionSpec::Req(req) => req.comparators.iter().any(|c| c.major > 1), - _ => false, - } - } -} - -impl fmt::Display for PackageManager { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PackageManager::Npm => write!(f, "npm"), - PackageManager::Pnpm => write!(f, "pnpm"), - PackageManager::Yarn => write!(f, "yarn"), - } - } -} - #[plugin_fn] pub fn register_tool(Json(_): Json) -> FnResult> { let manager = PackageManager::detect(); @@ -91,116 +32,53 @@ pub fn register_tool(Json(_): Json) -> FnResult, -) -> FnResult> { - let version = &input.context.version; - let manager = PackageManager::detect(); - - if version.is_canary() { - return err!(PluginError::UnsupportedCanary { - tool: manager.to_string() - } - .into()); - } - - let package_name = manager.get_package_name(version.to_unresolved_spec()); - - // Derive values based on package manager - let archive_prefix = if manager.is_yarn_classic(version.to_unresolved_spec()) { - format!("yarn-v{version}") - } else { - "package".into() - }; - - let package_without_scope = if package_name.contains('/') { - package_name.split('/').nth(1).unwrap() - } else { - &package_name - }; - - Ok(Json(DownloadPrebuiltOutput { - archive_prefix: Some(archive_prefix), - download_url: format!( - "https://registry.npmjs.org/{package_name}/-/{package_without_scope}-{version}.tgz", - ), - ..DownloadPrebuiltOutput::default() +pub fn detect_version_files(_: ()) -> FnResult> { + Ok(Json(DetectVersionOutput { + files: vec!["package.json".into()], })) } #[plugin_fn] -pub fn locate_bins(Json(input): Json) -> FnResult> { - let mut bin_path = None; - let package_path = input.context.tool_dir.join("package.json"); - let manager = PackageManager::detect(); - let manager_name = manager.to_string(); +pub fn parse_version_file( + Json(input): Json, +) -> FnResult> { + let mut version = None; - // Extract the binary from the `package.json` - if package_path.exists() { - if let Ok(package_json) = json::from_slice::(&fs::read(package_path)?) { - if let Some(bin_field) = package_json.bin { - match bin_field { - BinField::String(bin) => { - bin_path = Some(bin); - } - BinField::Object(map) => { - if let Some(bin) = map.get(&manager_name) { - bin_path = Some(bin.to_owned()); + if input.file == "package.json" { + if let Ok(package_json) = json::from_str::(&input.content) { + let manager_name = PackageManager::detect().to_string(); + + if let Some(pm) = package_json.package_manager { + let mut parts = pm.split('@'); + let name = parts.next().unwrap_or_default(); + + if name == manager_name { + let value = if let Some(value) = parts.next() { + // Remove corepack build metadata hash + if let Some(index) = value.find('+') { + &value[0..index] + } else { + value } - } - }; - } + } else { + "latest" + }; - if bin_path.is_none() { - if let Some(main_field) = package_json.main { - bin_path = Some(main_field); + version = Some(UnresolvedVersionSpec::parse(value)?); } } - } - } - if bin_path.is_none() { - bin_path = Some(format!( - "bin/{}", - if manager == PackageManager::Pnpm { - "pnpm.cjs".to_owned() - } else { - manager_name + if version.is_none() { + if let Some(engines) = package_json.engines { + if let Some(constraint) = engines.get(&manager_name) { + version = Some(UnresolvedVersionSpec::parse(constraint)?); + } + } } - )); - } - - Ok(Json(LocateBinsOutput { - bin_path: bin_path.map(PathBuf::from), - fallback_last_globals_dir: true, - globals_lookup_dirs: vec!["$PROTO_HOME/tools/node/globals/bin".into()], - ..LocateBinsOutput::default() - })) -} - -#[derive(Deserialize)] -struct RegistryVersion { - version: String, // No v prefix -} - -#[derive(Deserialize)] -struct RegistryResponse { - #[serde(rename = "dist-tags")] - dist_tags: HashMap, - versions: HashMap, -} - -// https://github.com/moonrepo/proto/issues/257 -fn parse_registry_response(res: HttpResponse, is_yarn: bool) -> Result { - if !is_yarn { - return res.json(); + } } - let pattern = regex::bytes::Regex::new("[\u{0000}-\u{001F}]+").unwrap(); - let body = res.body(); - // let text = String::from_bytes(res.body())?; - - Ok(json::from_slice(&pattern.replace_all(&body, b""))?) + Ok(Json(ParseVersionFileOutput { version })) } #[plugin_fn] @@ -280,7 +158,7 @@ pub fn resolve_version( if let Some(node_version) = host_env!("PROTO_NODE_VERSION") { for node_release in &response { // Theirs starts with v, ours does not - if node_release.version[1..] == node_version { + if node_release.version[1..] == node_version && node_release.npm.is_some() { output.version = Some(VersionSpec::parse(node_release.npm.as_ref().unwrap())?); found_version = true; @@ -291,20 +169,16 @@ pub fn resolve_version( // Otherwise call the current `node` binary and infer from that if !found_version { - if let Ok(result) = exec_command!(raw, "node", ["--version"]) { - if result.0.exit_code == 0 { - let node_version = result.0.stdout.trim(); - - for node_release in &response { - // Both start with v - if node_release.version == node_version { - output.version = Some(VersionSpec::parse( - node_release.npm.as_ref().unwrap(), - )?); - found_version = true; - break; - } - } + let result = exec_command!("node", ["--version"]); + let node_version = result.stdout.trim(); + + for node_release in &response { + // Both start with v + if node_release.version == node_version && node_release.npm.is_some() { + output.version = + Some(VersionSpec::parse(node_release.npm.as_ref().unwrap())?); + found_version = true; + break; } } } @@ -336,110 +210,97 @@ pub fn resolve_version( } #[plugin_fn] -pub fn create_shims(Json(_): Json) -> FnResult> { +pub fn download_prebuilt( + Json(input): Json, +) -> FnResult> { + let version = &input.context.version; + let manager = PackageManager::detect(); + + if version.is_canary() { + return err!(PluginError::UnsupportedCanary { + tool: manager.to_string() + } + .into()); + } + + let package_name = manager.get_package_name(version.to_unresolved_spec()); + + // Derive values based on package manager + let archive_prefix = if manager.is_yarn_classic(version.to_unresolved_spec()) { + format!("yarn-v{version}") + } else { + "package".into() + }; + + let package_without_scope = if package_name.contains('/') { + package_name.split('/').nth(1).unwrap() + } else { + &package_name + }; + + Ok(Json(DownloadPrebuiltOutput { + archive_prefix: Some(archive_prefix), + download_url: format!( + "https://registry.npmjs.org/{package_name}/-/{package_without_scope}-{version}.tgz", + ), + ..DownloadPrebuiltOutput::default() + })) +} + +#[plugin_fn] +pub fn locate_executables( + Json(_): Json, +) -> FnResult> { let env = get_proto_environment()?; let manager = PackageManager::detect(); - let mut global_shims = HashMap::::new(); - let mut local_shims = HashMap::::new(); + let mut secondary = HashMap::default(); + let mut primary; - match manager { + match &manager { PackageManager::Npm => { - local_shims.insert( - "npm".into(), - ShimConfig::local_with_parent("bin/npm-cli.js", "node"), + primary = ExecutableConfig::new(env.os.get_file_name("bin/npm", "cmd")); + + // npx + secondary.insert( + "npx".into(), + ExecutableConfig::new(env.os.get_file_name("bin/npx", "cmd")), ); // node-gyp - global_shims.insert( + secondary.insert( "node-gyp".into(), - ShimConfig::global_with_alt_bin(if env.os == HostOS::Windows { - "bin/node-gyp-bin/node-gyp.cmd" - } else { - "bin/node-gyp-bin/node-gyp" - }), + ExecutableConfig::new(env.os.get_file_name("bin/node-gyp-bin/node-gyp", "cmd")), ); } PackageManager::Pnpm => { - local_shims.insert( - "pnpm".into(), - ShimConfig::local_with_parent("bin/pnpm.cjs", "node"), - ); + primary = ExecutableConfig::new("bin/pnpm.cjs"); + primary.no_bin = true; // Can't execute a JS file // pnpx - global_shims.insert("pnpx".into(), ShimConfig::global_with_sub_command("dlx")); + secondary.insert( + "pnpx".into(), + ExecutableConfig { + shim_before_args: Some("dlx".into()), + ..ExecutableConfig::default() + }, + ); } PackageManager::Yarn => { - local_shims.insert( - "yarn".into(), - ShimConfig::local_with_parent("bin/yarn.js", "node"), - ); + primary = ExecutableConfig::new(env.os.get_file_name("bin/yarn", "cmd")); // yarnpkg - global_shims.insert("yarnpkg".into(), ShimConfig::default()); + secondary.insert("yarnpkg".into(), ExecutableConfig::default()); } }; - Ok(Json(CreateShimsOutput { - primary: Some(ShimConfig { - parent_bin: Some("node".into()), - ..ShimConfig::default() - }), - global_shims, - local_shims, - ..CreateShimsOutput::default() - })) -} - -#[plugin_fn] -pub fn detect_version_files(_: ()) -> FnResult> { - Ok(Json(DetectVersionOutput { - files: vec!["package.json".into()], + Ok(Json(LocateExecutablesOutput { + globals_lookup_dirs: vec!["$PROTO_HOME/tools/node/globals/bin".into()], + primary: Some(primary), + secondary, + ..LocateExecutablesOutput::default() })) } -#[plugin_fn] -pub fn parse_version_file( - Json(input): Json, -) -> FnResult> { - let mut version = None; - - if input.file == "package.json" { - if let Ok(package_json) = json::from_str::(&input.content) { - let manager_name = PackageManager::detect().to_string(); - - if let Some(pm) = package_json.package_manager { - let mut parts = pm.split('@'); - let name = parts.next().unwrap_or_default(); - - if name == manager_name { - let value = if let Some(value) = parts.next() { - // Remove corepack build metadata hash - if let Some(index) = value.find('+') { - &value[0..index] - } else { - value - } - } else { - "latest" - }; - - version = Some(UnresolvedVersionSpec::parse(value)?); - } - } - - if version.is_none() { - if let Some(engines) = package_json.engines { - if let Some(constraint) = engines.get(&manager_name) { - version = Some(UnresolvedVersionSpec::parse(constraint)?); - } - } - } - } - } - - Ok(Json(ParseVersionFileOutput { version })) -} - #[plugin_fn] pub fn install_global( Json(input): Json, @@ -510,3 +371,121 @@ pub fn pre_run(Json(input): Json) -> FnResult<()> { Ok(()) } + +// DEPRECATED +// Remove in v0.23! + +#[plugin_fn] +pub fn locate_bins(Json(input): Json) -> FnResult> { + let mut bin_path = None; + let package_path = input.context.tool_dir.join("package.json"); + let manager = PackageManager::detect(); + let manager_name = manager.to_string(); + + // Extract the binary from the `package.json` + if package_path.exists() { + if let Ok(package_json) = json::from_slice::(&fs::read(package_path)?) { + if let Some(bin_field) = package_json.bin { + match bin_field { + BinField::String(bin) => { + bin_path = Some(bin); + } + BinField::Object(map) => { + if let Some(bin) = map.get(&manager_name) { + bin_path = Some(bin.to_owned()); + } + } + }; + } + + if bin_path.is_none() { + if let Some(main_field) = package_json.main { + bin_path = Some(main_field); + } + } + } + } + + if bin_path.is_none() { + bin_path = Some(format!( + "bin/{}", + if manager == PackageManager::Pnpm { + "pnpm.cjs".to_owned() + } else { + manager_name + } + )); + } + + Ok(Json(LocateBinsOutput { + bin_path: bin_path.map(PathBuf::from), + fallback_last_globals_dir: true, + globals_lookup_dirs: vec!["$PROTO_HOME/tools/node/globals/bin".into()], + ..LocateBinsOutput::default() + })) +} + +#[plugin_fn] +pub fn create_shims(Json(_): Json) -> FnResult> { + let env = get_proto_environment()?; + let manager = PackageManager::detect(); + let mut global_shims = HashMap::::new(); + let mut local_shims = HashMap::::new(); + + match manager { + PackageManager::Npm => { + local_shims.insert( + "npm".into(), + ShimConfig::local_with_parent("bin/npm-cli.js", "node"), + ); + + // node-gyp + global_shims.insert( + "node-gyp".into(), + ShimConfig::global_with_alt_bin(if env.os == HostOS::Windows { + "bin/node-gyp-bin/node-gyp.cmd" + } else { + "bin/node-gyp-bin/node-gyp" + }), + ); + + // npx + global_shims.insert( + "npx".into(), + ShimConfig::global_with_alt_bin(if env.os == HostOS::Windows { + "bin/npx.cmd" + } else { + "bin/npx" + }), + ); + } + PackageManager::Pnpm => { + local_shims.insert( + "pnpm".into(), + ShimConfig::local_with_parent("bin/pnpm.cjs", "node"), + ); + + // pnpx + global_shims.insert("pnpx".into(), ShimConfig::global_with_sub_command("dlx")); + } + PackageManager::Yarn => { + local_shims.insert( + "yarn".into(), + ShimConfig::local_with_parent("bin/yarn.js", "node"), + ); + + // yarnpkg + global_shims.insert("yarnpkg".into(), ShimConfig::default()); + } + }; + + Ok(Json(CreateShimsOutput { + primary: Some(ShimConfig { + parent_bin: Some("node".into()), + ..ShimConfig::default() + }), + global_shims, + local_shims, + ..CreateShimsOutput::default() + })) +} diff --git a/crates/node-depman/tests/download_test.rs b/crates/node-depman/tests/download_test.rs index 84171c2..24b1775 100644 --- a/crates/node-depman/tests/download_test.rs +++ b/crates/node-depman/tests/download_test.rs @@ -46,13 +46,15 @@ mod npm { assert_eq!( plugin - .locate_bins(LocateBinsInput { + .locate_executables(LocateExecutablesInput { context: ToolContext { version: VersionSpec::parse("9.0.0").unwrap(), ..Default::default() }, }) - .bin_path, + .primary + .unwrap() + .exe_path, Some("bin/npm".into()) ); } @@ -103,13 +105,15 @@ mod pnpm { assert_eq!( plugin - .locate_bins(LocateBinsInput { + .locate_executables(LocateExecutablesInput { context: ToolContext { version: VersionSpec::parse("8.0.0").unwrap(), ..Default::default() }, }) - .bin_path, + .primary + .unwrap() + .exe_path, Some("bin/pnpm.cjs".into()) ); } @@ -160,13 +164,15 @@ mod yarn { assert_eq!( plugin - .locate_bins(LocateBinsInput { + .locate_executables(LocateExecutablesInput { context: ToolContext { version: VersionSpec::parse("1.22.0").unwrap(), ..Default::default() }, }) - .bin_path, + .primary + .unwrap() + .exe_path, Some("bin/yarn".into()) ); } @@ -218,109 +224,16 @@ mod yarn_berry { assert_eq!( plugin - .locate_bins(LocateBinsInput { + .locate_executables(LocateExecutablesInput { context: ToolContext { version: VersionSpec::parse("3.6.1").unwrap(), ..Default::default() }, }) - .bin_path, + .primary + .unwrap() + .exe_path, Some("bin/yarn".into()) ); } } - -#[test] -fn locates_bin_from_package_json_bin() { - let sandbox = create_empty_sandbox(); - - sandbox.create_file( - ".proto/tools/npm-test/latest/package.json", - r#"{ - "main": "./index.js", - "bin": "./file.js" -}"#, - ); - - let mut plugin = create_plugin("npm-test", sandbox.path()); - - plugin.set_environment(HostEnvironment { - arch: HostArch::X64, - os: HostOS::Windows, - ..Default::default() - }); - - assert_eq!( - plugin - .locate_bins(LocateBinsInput { - context: ToolContext { - version: VersionSpec::parse("20.0.0").unwrap(), - ..Default::default() - }, - }) - .bin_path, - Some("./file.js".into()) - ); - - plugin.set_environment(HostEnvironment { - arch: HostArch::Arm64, - os: HostOS::Linux, - ..Default::default() - }); - - sandbox.create_file( - ".proto/tools/npm-test/latest/package.json", - r#"{ - "main": "./index.js", - "bin": { - "npm": "./npm.js", - "pnpm": "./pnpm.js", - "yarn": "./yarn.js" - } -}"#, - ); - - assert_eq!( - plugin - .locate_bins(LocateBinsInput { - context: ToolContext { - version: VersionSpec::parse("9.0.0").unwrap(), - ..Default::default() - }, - }) - .bin_path, - Some("./npm.js".into()) - ); -} - -#[test] -fn locates_bin_from_package_json_main() { - let sandbox = create_empty_sandbox(); - - sandbox.create_file( - ".proto/tools/npm-test/latest/package.json", - r#"{ - "main": "./index.js" -}"#, - ); - - let mut plugin = create_plugin("npm-test", sandbox.path()); - - plugin.set_environment(HostEnvironment { - arch: HostArch::X64, - os: HostOS::MacOS, - ..Default::default() - }); - - assert_eq!( - plugin - .locate_bins(LocateBinsInput { - context: ToolContext { - version: VersionSpec::parse("8.0.0").unwrap(), - ..Default::default() - }, - }) - .bin_path, - Some("./index.js".into()) - ); -} diff --git a/crates/node-depman/tests/shims_test.rs b/crates/node-depman/tests/shims_test.rs index 0b52b95..06f2ea1 100644 --- a/crates/node-depman/tests/shims_test.rs +++ b/crates/node-depman/tests/shims_test.rs @@ -4,19 +4,19 @@ use proto_pdk_test_utils::*; mod npm { use super::*; - generate_global_shims_test!("npm-test", ["node-gyp"]); + generate_shims_test!("npm-test", ["npx", "node-gyp"]); } #[cfg(not(windows))] mod pnpm { use super::*; - generate_global_shims_test!("pnpm-test", ["pnpx"]); + generate_shims_test!("pnpm-test", ["pnpx"]); } #[cfg(not(windows))] mod yarn { use super::*; - generate_global_shims_test!("yarn-test", ["yarnpkg"]); + generate_shims_test!("yarn-test", ["yarnpkg"]); } diff --git a/crates/node-depman/tests/snapshots/shims_test__npm__creates_global_shims-2.snap b/crates/node-depman/tests/snapshots/shims_test__npm__creates_global_shims-2.snap index b48f085..46c07b1 100644 --- a/crates/node-depman/tests/snapshots/shims_test__npm__creates_global_shims-2.snap +++ b/crates/node-depman/tests/snapshots/shims_test__npm__creates_global_shims-2.snap @@ -1,6 +1,6 @@ --- source: crates/node-depman/tests/shims_test.rs -expression: "std::fs::read_to_string(sandbox.path().join(\".proto/bin\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"node-gyp\")\n } else { \"node-gyp\".to_string() })).unwrap()" +expression: "std::fs::read_to_string(sandbox.path().join(\".proto/shims\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"npx\")\n } else { \"npx\".to_string() })).unwrap()" --- #!/usr/bin/env bash set -e @@ -8,6 +8,6 @@ set -e -exec proto run npm-test --bin "bin/node-gyp-bin/node-gyp" -- "$@" +exec proto run npm-test --bin "bin/npx" -- "$@" diff --git a/crates/node-depman/tests/snapshots/shims_test__npm__creates_global_shims-3.snap b/crates/node-depman/tests/snapshots/shims_test__npm__creates_global_shims-3.snap new file mode 100644 index 0000000..b480fc5 --- /dev/null +++ b/crates/node-depman/tests/snapshots/shims_test__npm__creates_global_shims-3.snap @@ -0,0 +1,13 @@ +--- +source: crates/node-depman/tests/shims_test.rs +expression: "std::fs::read_to_string(sandbox.path().join(\".proto/shims\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"node-gyp\")\n } else { \"node-gyp\".to_string() })).unwrap()" +--- +#!/usr/bin/env bash +set -e +[ -n "$PROTO_DEBUG" ] && set -x + + + +exec proto run npm-test --bin "bin/node-gyp-bin/node-gyp" -- "$@" + + diff --git a/crates/node-depman/tests/snapshots/shims_test__pnpm__creates_global_shims-2.snap b/crates/node-depman/tests/snapshots/shims_test__pnpm__creates_global_shims-2.snap index 21b7077..68b39bc 100644 --- a/crates/node-depman/tests/snapshots/shims_test__pnpm__creates_global_shims-2.snap +++ b/crates/node-depman/tests/snapshots/shims_test__pnpm__creates_global_shims-2.snap @@ -1,6 +1,6 @@ --- source: crates/node-depman/tests/shims_test.rs -expression: "std::fs::read_to_string(sandbox.path().join(\".proto/bin\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"pnpx\")\n } else { \"pnpx\".to_string() })).unwrap()" +expression: "std::fs::read_to_string(sandbox.path().join(\".proto/shims\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"pnpx\")\n } else { \"pnpx\".to_string() })).unwrap()" --- #!/usr/bin/env bash set -e diff --git a/crates/node-depman/tests/snapshots/shims_test__yarn__creates_global_shims-2.snap b/crates/node-depman/tests/snapshots/shims_test__yarn__creates_global_shims-2.snap index d2bb0a1..3f2ba54 100644 --- a/crates/node-depman/tests/snapshots/shims_test__yarn__creates_global_shims-2.snap +++ b/crates/node-depman/tests/snapshots/shims_test__yarn__creates_global_shims-2.snap @@ -1,6 +1,6 @@ --- source: crates/node-depman/tests/shims_test.rs -expression: "std::fs::read_to_string(sandbox.path().join(\".proto/bin\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"yarnpkg\")\n } else { \"yarnpkg\".to_string() })).unwrap()" +expression: "std::fs::read_to_string(sandbox.path().join(\".proto/shims\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"yarnpkg\")\n } else { \"yarnpkg\".to_string() })).unwrap()" --- #!/usr/bin/env bash set -e diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index ac32415..0dd9bee 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node_plugin" -version = "0.4.3" +version = "0.5.0" edition = "2021" license = "MIT" publish = false diff --git a/crates/node/src/proto.rs b/crates/node/src/proto.rs index 1556bd5..1b597ac 100644 --- a/crates/node/src/proto.rs +++ b/crates/node/src/proto.rs @@ -1,7 +1,6 @@ use extism_pdk::*; use node_common::{commands, NodeDistLTS, NodeDistVersion, PackageJson}; use proto_pdk::*; -use std::collections::HashMap; #[host_fn] extern "ExtismHost" { @@ -22,6 +21,99 @@ pub fn register_tool(Json(_): Json) -> FnResult FnResult> { + Ok(Json(DetectVersionOutput { + files: vec![ + ".nvmrc".into(), + ".node-version".into(), + "package.json".into(), + ], + })) +} + +#[plugin_fn] +pub fn parse_version_file( + Json(input): Json, +) -> FnResult> { + let mut version = None; + + if input.file == "package.json" { + if let Ok(package_json) = json::from_str::(&input.content) { + if let Some(engines) = package_json.engines { + if let Some(constraint) = engines.get(BIN) { + version = Some(UnresolvedVersionSpec::parse(constraint)?); + } + } + } + } else { + version = Some(UnresolvedVersionSpec::parse(input.content)?); + } + + Ok(Json(ParseVersionFileOutput { version })) +} + +#[plugin_fn] +pub fn load_versions(Json(_): Json) -> FnResult> { + let mut output = LoadVersionsOutput::default(); + let response: Vec = + fetch_url("https://nodejs.org/download/release/index.json")?; + + for (index, item) in response.iter().enumerate() { + let version = Version::parse(&item.version[1..])?; + + // First item is always the latest + if index == 0 { + output.latest = Some(version.clone()); + } + + if let NodeDistLTS::Name(alias) = &item.lts { + let alias = alias.to_lowercase(); + + // The first encounter of an lts is the latest stable + if !output.aliases.contains_key("stable") { + output.aliases.insert("stable".into(), version.clone()); + } + + // The first encounter of an lts is the latest version for that alias + if !output.aliases.contains_key(&alias) { + output.aliases.insert(alias.clone(), version.clone()); + } + } + + output.versions.push(version); + } + + output + .aliases + .insert("latest".into(), output.latest.clone().unwrap()); + + Ok(Json(output)) +} + +#[plugin_fn] +pub fn resolve_version( + Json(input): Json, +) -> FnResult> { + let mut output = ResolveVersionOutput::default(); + + if let UnresolvedVersionSpec::Alias(alias) = input.initial { + let candidate = if alias == "node" { + "latest" + } else if alias == "lts-*" || alias == "lts/*" { + "stable" + } else if alias.starts_with("lts-") || alias.starts_with("lts/") { + &alias[4..] + } else { + return Ok(Json(output)); + }; + + output.candidate = Some(UnresolvedVersionSpec::Alias(candidate.to_owned())); + } + + Ok(Json(output)) +} + fn map_arch(os: HostOS, arch: HostArch) -> Result { let arch = match arch { HostArch::Arm => "armv7l".into(), @@ -107,134 +199,22 @@ pub fn download_prebuilt( } #[plugin_fn] -pub fn locate_bins(Json(_): Json) -> FnResult> { +pub fn locate_executables( + Json(_): Json, +) -> FnResult> { let env = get_proto_environment()?; - Ok(Json(LocateBinsOutput { - bin_path: Some(if env.os == HostOS::Windows { - format!("{}.exe", BIN).into() - } else { - format!("bin/{}", BIN).into() - }), - fallback_last_globals_dir: true, + Ok(Json(LocateExecutablesOutput { globals_lookup_dirs: vec!["$PROTO_HOME/tools/node/globals/bin".into()], - ..LocateBinsOutput::default() - })) -} - -#[plugin_fn] -pub fn load_versions(Json(_): Json) -> FnResult> { - let mut output = LoadVersionsOutput::default(); - let response: Vec = - fetch_url("https://nodejs.org/download/release/index.json")?; - - for (index, item) in response.iter().enumerate() { - let version = Version::parse(&item.version[1..])?; - - // First item is always the latest - if index == 0 { - output.latest = Some(version.clone()); - } - - if let NodeDistLTS::Name(alias) = &item.lts { - let alias = alias.to_lowercase(); - - // The first encounter of an lts is the latest stable - if !output.aliases.contains_key("stable") { - output.aliases.insert("stable".into(), version.clone()); - } - - // The first encounter of an lts is the latest version for that alias - if !output.aliases.contains_key(&alias) { - output.aliases.insert(alias.clone(), version.clone()); - } - } - - output.versions.push(version); - } - - output - .aliases - .insert("latest".into(), output.latest.clone().unwrap()); - - Ok(Json(output)) -} - -#[plugin_fn] -pub fn resolve_version( - Json(input): Json, -) -> FnResult> { - let mut output = ResolveVersionOutput::default(); - - if let UnresolvedVersionSpec::Alias(alias) = input.initial { - let candidate = if alias == "node" { - "latest" - } else if alias == "lts-*" || alias == "lts/*" { - "stable" - } else if alias.starts_with("lts-") || alias.starts_with("lts/") { - &alias[4..] - } else { - return Ok(Json(output)); - }; - - output.candidate = Some(UnresolvedVersionSpec::Alias(candidate.to_owned())); - } - - Ok(Json(output)) -} - -#[plugin_fn] -pub fn create_shims(Json(_): Json) -> FnResult> { - let env = get_proto_environment()?; - let mut global_shims = HashMap::new(); - - global_shims.insert( - "npx".into(), - ShimConfig::global_with_alt_bin(if env.os == HostOS::Windows { - "npx.cmd" + primary: Some(ExecutableConfig::new(if env.os == HostOS::Windows { + format!("{}.exe", BIN) } else { - "bin/npx" - }), - ); - - Ok(Json(CreateShimsOutput { - global_shims, - ..CreateShimsOutput::default() - })) -} - -#[plugin_fn] -pub fn detect_version_files(_: ()) -> FnResult> { - Ok(Json(DetectVersionOutput { - files: vec![ - ".nvmrc".into(), - ".node-version".into(), - "package.json".into(), - ], + format!("bin/{}", BIN) + })), + ..LocateExecutablesOutput::default() })) } -#[plugin_fn] -pub fn parse_version_file( - Json(input): Json, -) -> FnResult> { - let mut version = None; - - if input.file == "package.json" { - if let Ok(package_json) = json::from_str::(&input.content) { - if let Some(engines) = package_json.engines { - if let Some(constraint) = engines.get(BIN) { - version = Some(UnresolvedVersionSpec::parse(constraint)?); - } - } - } - } else { - version = Some(UnresolvedVersionSpec::parse(input.content)?); - } - - Ok(Json(ParseVersionFileOutput { version })) -} - #[plugin_fn] pub fn install_global( Json(input): Json, @@ -292,3 +272,22 @@ pub fn post_install(Json(input): Json) -> FnResult<()> { Ok(()) } + +// DEPRECATED +// Remove in v0.23! + +#[plugin_fn] +pub fn locate_bins(Json(_): Json) -> FnResult> { + let env = get_proto_environment()?; + + Ok(Json(LocateBinsOutput { + bin_path: Some(if env.os == HostOS::Windows { + format!("{}.exe", BIN).into() + } else { + format!("bin/{}", BIN).into() + }), + fallback_last_globals_dir: true, + globals_lookup_dirs: vec!["$PROTO_HOME/tools/node/globals/bin".into()], + ..LocateBinsOutput::default() + })) +} diff --git a/crates/node/tests/download_test.rs b/crates/node/tests/download_test.rs index fec4110..41888ca 100644 --- a/crates/node/tests/download_test.rs +++ b/crates/node/tests/download_test.rs @@ -327,13 +327,15 @@ fn locates_unix_bin() { assert_eq!( plugin - .locate_bins(LocateBinsInput { + .locate_executables(LocateExecutablesInput { context: ToolContext { version: VersionSpec::parse("20.0.0").unwrap(), ..Default::default() }, }) - .bin_path, + .primary + .unwrap() + .exe_path, Some("bin/node".into()) ); } @@ -351,13 +353,15 @@ fn locates_windows_bin() { assert_eq!( plugin - .locate_bins(LocateBinsInput { + .locate_executables(LocateExecutablesInput { context: ToolContext { version: VersionSpec::parse("20.0.0").unwrap(), ..Default::default() }, }) - .bin_path, + .primary + .unwrap() + .exe_path, Some("node.exe".into()) ); } diff --git a/crates/node/tests/shims_test.rs b/crates/node/tests/shims_test.rs index ce4a326..87bbfd1 100644 --- a/crates/node/tests/shims_test.rs +++ b/crates/node/tests/shims_test.rs @@ -1,4 +1,4 @@ use proto_pdk_test_utils::*; #[cfg(not(windows))] -generate_global_shims_test!("node-test", ["npx"]); +generate_shims_test!("node-test"); diff --git a/crates/node/tests/snapshots/shims_test__creates_global_shims-2.snap b/crates/node/tests/snapshots/shims_test__creates_global_shims-2.snap deleted file mode 100644 index 8bc565b..0000000 --- a/crates/node/tests/snapshots/shims_test__creates_global_shims-2.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/node/tests/shims_test.rs -expression: "std::fs::read_to_string(sandbox.path().join(\".proto/bin\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"npx\")\n } else { \"npx\".to_string() })).unwrap()" ---- -#!/usr/bin/env bash -set -e -[ -n "$PROTO_DEBUG" ] && set -x - - - -exec proto run node-test --bin "bin/npx" -- "$@" - - diff --git a/crates/node/tests/snapshots/shims_test__creates_global_shims.snap b/crates/node/tests/snapshots/shims_test__creates_global_shims.snap index 68846d6..2b04074 100644 --- a/crates/node/tests/snapshots/shims_test__creates_global_shims.snap +++ b/crates/node/tests/snapshots/shims_test__creates_global_shims.snap @@ -1,6 +1,6 @@ --- source: crates/node/tests/shims_test.rs -expression: "std::fs::read_to_string(sandbox.path().join(\".proto/bin\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"node-test\")\n } else { \"node-test\".to_string() })).unwrap()" +expression: "std::fs::read_to_string(sandbox.path().join(\".proto/shims\").join(if cfg!(windows)\n {\n format!(\"{}.cmd\", \"node-test\")\n } else { \"node-test\".to_string() })).unwrap()" --- #!/usr/bin/env bash set -e