Skip to content
This repository has been archived by the owner on Jul 17, 2024. It is now read-only.

new: Support globals and proto v0.15. #5

Merged
merged 16 commits into from
Aug 22, 2023
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
- uses: moonrepo/setup-rust@v1
with:
bins: cargo-wasi, cargo-nextest
- uses: moonrepo/setup-proto@v1
- run: cargo wasi build -p node_plugin
- run: cargo wasi build -p node_depman_plugin
- run: cargo nextest run --workspace
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
# Changelog

## 0.2.0

#### 🚀 Updates

- Added support for `install_global` and `uninstall_global`.
- Added `post_install` hook for installing the bundled npm.
- Updated to support proto v0.15 release.

#### 🐞 Fixes

- **npm**
- Will no longer crash when parsing an invalid `package.json`.

## 0.1.0

#### 💥 Breaking

- Will now longer check `engines` in `package.json` when detecting a version.
- Will no longer check `engines` in `package.json` when detecting a version.

#### 🚀 Updates

Expand Down
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ members = ["crates/*"]

[workspace.dependencies]
extism-pdk = "0.3.3"
# proto_pdk = { version = "0.4.1", path = "../proto/crates/pdk" }
# proto_pdk_test_utils = { version = "0.3.3", path = "../proto/crates/pdk-test-utils" }
proto_pdk = "0.4.2"
proto_pdk_test_utils = "0.3.4"
proto_pdk = { version = "0.6.2" } # , path = "../../proto/crates/pdk" }
proto_pdk_api = { version = "0.6.0" } # , path = "../../proto/crates/pdk-api" }
proto_pdk_test_utils = { version = "0.5.5" } # , path = "../../proto/crates/pdk-test-utils" }
serde = "1.0.183"
serde_json = "1.0.105"
starbase_sandbox = "0.1.8"
tokio = "1.29.1"
tokio = "1.31.0"
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ license = "MIT"
publish = false

[dependencies]
proto_pdk_api = { workspace = true }
serde = { workspace = true }
46 changes: 46 additions & 0 deletions crates/common/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use proto_pdk_api::ExecCommandInput;
use std::path::Path;

pub fn install_global(dependency: &str, globals_dir: &Path) -> ExecCommandInput {
let mut cmd = ExecCommandInput::inherit(
"npm",
[
"install",
"--global",
"--loglevel",
"warn",
"--no-audit",
"--no-update-notifier",
dependency,
],
);

cmd.env_vars
.insert("PROTO_INSTALL_GLOBAL".into(), "true".into());

// Remove the /bin component
cmd.env_vars.insert(
"PREFIX".into(),
globals_dir.parent().unwrap().to_string_lossy().to_string(),
);

cmd
}

pub fn uninstall_global(dependency: &str, globals_dir: &Path) -> ExecCommandInput {
let mut cmd = ExecCommandInput::inherit(
"npm",
["uninstall", "--global", "--loglevel", "warn", dependency],
);

cmd.env_vars
.insert("PROTO_INSTALL_GLOBAL".into(), "true".into());

// Remove the /bin component
cmd.env_vars.insert(
"PREFIX".into(),
globals_dir.parent().unwrap().to_string_lossy().to_string(),
);

cmd
}
1 change: 1 addition & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod commands;
mod node_dist;
mod package_json;

Expand Down
3 changes: 2 additions & 1 deletion crates/node-depman/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "node_depman_plugin"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "MIT"
publish = false
Expand All @@ -16,5 +16,6 @@ serde = { workspace = true }

[dev-dependencies]
proto_pdk_test_utils = { workspace = true }
serde_json = { workspace = true }
starbase_sandbox = { workspace = true }
tokio = { workspace = true }
163 changes: 120 additions & 43 deletions crates/node-depman/src/proto.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use extism_pdk::*;
use node_common::{BinField, NodeDistVersion, PackageJson};
use node_common::{commands, BinField, NodeDistVersion, PackageJson};
use proto_pdk::*;
use serde::Deserialize;
use std::collections::HashMap;
Expand All @@ -9,8 +9,8 @@ use std::path::PathBuf;

#[host_fn]
extern "ExtismHost" {
fn host_log(input: Json<HostLogInput>);
fn exec_command(input: Json<ExecCommandInput>) -> Json<ExecCommandOutput>;
fn host_log(input: Json<HostLogInput>);
}

#[derive(PartialEq)]
Expand All @@ -21,10 +21,12 @@ enum PackageManager {
}

impl PackageManager {
pub fn from(env: &Environment) -> PackageManager {
if env.id.to_lowercase().contains("yarn") {
pub fn detect() -> PackageManager {
let id = get_tool_id();

if id.to_lowercase().contains("yarn") {
PackageManager::Yarn
} else if env.id.to_lowercase().contains("pnpm") {
} else if id.to_lowercase().contains("pnpm") {
PackageManager::Pnpm
} else {
PackageManager::Npm
Expand Down Expand Up @@ -61,18 +63,19 @@ impl fmt::Display for PackageManager {
}

#[plugin_fn]
pub fn register_tool(Json(input): Json<ToolMetadataInput>) -> FnResult<Json<ToolMetadataOutput>> {
let manager = PackageManager::from(&input.env);
pub fn register_tool(Json(_): Json<ToolMetadataInput>) -> FnResult<Json<ToolMetadataOutput>> {
let manager = PackageManager::detect();

Ok(Json(ToolMetadataOutput {
name: manager.to_string(),
type_of: PluginType::DependencyManager,
env_vars: vec!["PROTO_NODE_VERSION".into()],
env_vars: vec!["PROTO_NODE_VERSION".into(), "PROTO_INSTALL_GLOBAL".into()],
default_version: if manager == PackageManager::Npm {
Some("bundled".into())
} else {
None
},
plugin_version: Some(env!("CARGO_PKG_VERSION").into()),
..ToolMetadataOutput::default()
}))
}
Expand All @@ -81,8 +84,8 @@ pub fn register_tool(Json(input): Json<ToolMetadataInput>) -> FnResult<Json<Tool
pub fn download_prebuilt(
Json(input): Json<DownloadPrebuiltInput>,
) -> FnResult<Json<DownloadPrebuiltOutput>> {
let version = &input.env.version;
let manager = PackageManager::from(&input.env);
let version = &input.context.version;
let manager = PackageManager::detect();
let package_name = manager.get_package_name(version);

// Derive values based on package manager
Expand Down Expand Up @@ -110,30 +113,30 @@ pub fn download_prebuilt(
#[plugin_fn]
pub fn locate_bins(Json(input): Json<LocateBinsInput>) -> FnResult<Json<LocateBinsOutput>> {
let mut bin_path = None;
let package_path = input.tool_dir.join("package.json");
let manager = PackageManager::from(&input.env);
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() {
let package_json: PackageJson = 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 let Ok(package_json) = json::from_slice::<PackageJson>(&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() {
if let Some(main_field) = package_json.main {
bin_path = Some(main_field);
}
}
}
}
Expand Down Expand Up @@ -172,7 +175,7 @@ struct RegistryResponse {
#[plugin_fn]
pub fn load_versions(Json(input): Json<LoadVersionsInput>) -> FnResult<Json<LoadVersionsOutput>> {
let mut output = LoadVersionsOutput::default();
let manager = PackageManager::from(&input.env);
let manager = PackageManager::detect();
let package_name = manager.get_package_name(&input.initial);

let mut map_output = |res: RegistryResponse| -> Result<(), Error> {
Expand Down Expand Up @@ -202,6 +205,10 @@ pub fn load_versions(Json(input): Json<LoadVersionsInput>) -> FnResult<Json<Load
// Yarn is managed by 2 different packages, so we need to request versions from both of them!
if manager.is_yarn_berry(&input.initial) {
map_output(fetch_url_with_cache("https://registry.npmjs.org/yarn/")?)?;
} else if manager.is_yarn_classic(&input.initial) {
map_output(fetch_url_with_cache(
"https://registry.npmjs.org/@yarnpkg/cli-dist/",
)?)?;
}

output
Expand All @@ -215,7 +222,7 @@ pub fn load_versions(Json(input): Json<LoadVersionsInput>) -> FnResult<Json<Load
pub fn resolve_version(
Json(input): Json<ResolveVersionInput>,
) -> FnResult<Json<ResolveVersionOutput>> {
let manager = PackageManager::from(&input.env);
let manager = PackageManager::detect();
let mut output = ResolveVersionOutput::default();

match manager {
Expand All @@ -228,7 +235,7 @@ pub fn resolve_version(
let mut found_version = false;

// Infer from proto's environment variable
if let Some(node_version) = input.env.vars.get("PROTO_NODE_VERSION") {
if let Some(node_version) = input.context.env_vars.get("PROTO_NODE_VERSION") {
for node_release in &response {
// Theirs starts with v, ours does not
if &node_release.version[1..] == node_version {
Expand Down Expand Up @@ -280,8 +287,9 @@ pub fn resolve_version(
}

#[plugin_fn]
pub fn create_shims(Json(input): Json<CreateShimsInput>) -> FnResult<Json<CreateShimsOutput>> {
let manager = PackageManager::from(&input.env);
pub fn create_shims(Json(_): Json<CreateShimsInput>) -> FnResult<Json<CreateShimsOutput>> {
let env = get_proto_environment()?;
let manager = PackageManager::detect();
let mut global_shims = HashMap::<String, ShimConfig>::new();
let mut local_shims = HashMap::<String, ShimConfig>::new();

Expand All @@ -295,7 +303,7 @@ pub fn create_shims(Json(input): Json<CreateShimsInput>) -> FnResult<Json<Create
// node-gyp
global_shims.insert(
"node-gyp".into(),
ShimConfig::global_with_alt_bin(if input.env.os == HostOS::Windows {
ShimConfig::global_with_alt_bin(if env.os == HostOS::Windows {
"bin/node-gyp-bin/node-gyp.cmd"
} else {
"bin/node-gyp-bin/node-gyp"
Expand Down Expand Up @@ -345,21 +353,90 @@ pub fn parse_version_file(
Json(input): Json<ParseVersionFileInput>,
) -> FnResult<Json<ParseVersionFileOutput>> {
let mut version = None;
let manager = PackageManager::from(&input.env);

if input.file == "package.json" {
let package_json: PackageJson = json::from_str(&input.content)?;
let manager_name = manager.to_string();

if let Some(manager) = package_json.package_manager {
let mut parts = manager.split('@');
let name = parts.next().unwrap_or_default();
if let Ok(package_json) = json::from_str::<PackageJson>(&input.content) {
if let Some(pm) = package_json.package_manager {
let mut parts = pm.split('@');
let name = parts.next().unwrap_or_default();

if name == manager_name {
version = Some(parts.next().unwrap_or("latest").to_owned());
if name == PackageManager::detect().to_string() {
version = Some(parts.next().unwrap_or("latest").to_owned());
}
}
}
}

Ok(Json(ParseVersionFileOutput { version }))
}

#[plugin_fn]
pub fn install_global(
Json(input): Json<InstallGlobalInput>,
) -> FnResult<Json<InstallGlobalOutput>> {
let result = exec_command!(commands::install_global(
&input.dependency,
&input.globals_dir.real_path(),
));

Ok(Json(InstallGlobalOutput::from_exec_command(result)))
}

#[plugin_fn]
pub fn uninstall_global(
Json(input): Json<UninstallGlobalInput>,
) -> FnResult<Json<UninstallGlobalOutput>> {
let result = exec_command!(commands::uninstall_global(
&input.dependency,
&input.globals_dir.real_path(),
));

Ok(Json(UninstallGlobalOutput::from_exec_command(result)))
}

#[plugin_fn]
pub fn pre_run(Json(input): Json<RunHook>) -> FnResult<()> {
let args = &input.passthrough_args;
let user_config = get_proto_user_config()?;

if args.len() < 3
|| input.context.env_vars.get("PROTO_INSTALL_GLOBAL").is_some()
|| !user_config.node_intercept_globals
{
return Ok(());
}

let manager = PackageManager::detect();
let mut is_install_command = false;
let mut is_global = false;

// npm install -g <dep>
// pnpm add -g <dep>
if manager == PackageManager::Npm || manager == PackageManager::Pnpm {
is_install_command = args[0] == "install" || args[0] == "i" || args[0] == "add";

for arg in args {
if arg == "--global" || arg == "-g" || arg == "--location=global" {
is_global = true;
break;
}
}
}

// yarn global add <dep>
if manager == PackageManager::Yarn {
is_global = args[0] == "global";
is_install_command = args[1] == "add";
}

if is_install_command && is_global {
return err!(format!(
"Global binaries must be installed with `proto install-global {}`!\nLearn more: {}\n\nOpt-out of this functionality with `{}`.",
manager.to_string(),
"https://moonrepo.dev/docs/proto/faq#how-can-i-install-a-global-binary-for-a-language",
"node-intercept-globals = false",
));
}

Ok(())
}
Loading