Skip to content

Commit c2ddc60

Browse files
committed
chore: ci
2 parents 3ab20ff + 3fe8caf commit c2ddc60

File tree

13 files changed

+315
-39
lines changed

13 files changed

+315
-39
lines changed

crates/pm/src/cmd/install.rs

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use anyhow::{Context, Result};
22
use std::fs;
3-
use std::path::{Path, PathBuf};
3+
use std::path::Path;
44
use std::sync::Arc;
55
use std::thread;
66
use tokio::sync::Semaphore;
77

88
use crate::cmd::rebuild::rebuild;
9+
use crate::helper::global_bin::get_global_bin_dir;
910
use crate::helper::lock::update_package_json;
1011
use crate::helper::lock::{
1112
PackageLock, ensure_package_lock, group_by_depth, prepare_global_package_json,
@@ -112,7 +113,7 @@ pub async fn install(ignore_scripts: bool, root_path: &Path) -> Result<()> {
112113
}
113114
}
114115

115-
pub async fn install_global_package(npm_spec: &str, prefix: &Option<String>) -> Result<()> {
116+
pub async fn install_global_package(npm_spec: &str, prefix: Option<&str>) -> Result<()> {
116117
// Prepare global package directory and package.json
117118
let package_path = prepare_global_package_json(npm_spec, prefix)
118119
.await
@@ -129,17 +130,9 @@ pub async fn install_global_package(npm_spec: &str, prefix: &Option<String>) ->
129130
let package_info =
130131
PackageInfo::from_path(&package_path).context("Failed to create package info from path")?;
131132

132-
let target_bin_dir = match prefix {
133-
Some(prefix) => PathBuf::from(prefix).join("bin"),
134-
135-
// If prefix is not set, link to the bin directory of the current executable
136-
// ~/.nvm/versions/node/v20.15.0/bin/utoo -> ~/.nvm/versions/node/v20.15.0/bin
137-
None => std::env::current_exe()
138-
.context("Failed to get current executable path")?
139-
.parent()
140-
.context("Failed to get executable parent directory")?
141-
.to_path_buf(),
142-
};
133+
// Get global bin directory using the common helper
134+
let target_bin_dir =
135+
get_global_bin_dir(&prefix).context("Failed to get global bin directory")?;
143136

144137
// Link binary files to global
145138
log_verbose(&format!(
@@ -161,7 +154,7 @@ mod tests {
161154
#[tokio::test]
162155
async fn test_install_global_package_invalid_spec() {
163156
// Test installing with invalid package spec
164-
let result = install_global_package("invalid-package-that-does-not-exist", &None).await;
157+
let result = install_global_package("invalid-package-that-does-not-exist", None).await;
165158
assert!(result.is_err(), "Should fail with invalid package spec");
166159
}
167160
}

crates/pm/src/cmd/link.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use crate::cmd::install::install;
2+
use crate::helper::global_bin::get_global_package_dir;
3+
use crate::helper::workspace::update_cwd_to_project;
4+
use crate::model::package::PackageInfo;
5+
use crate::util::linker::link;
6+
use crate::util::logger::log_verbose;
7+
use anyhow::{Context, Result};
8+
9+
/// Link current package to global (equivalent to npm link without args)
10+
pub async fn link_current_to_global(prefix: Option<&str>) -> Result<()> {
11+
let cwd = std::env::current_dir().context("Failed to get current directory")?;
12+
let project_path = update_cwd_to_project(&cwd).await?;
13+
14+
// Create package info from path
15+
let package_info = PackageInfo::from_path(&project_path).with_context(|| {
16+
format!(
17+
"Failed to parse package info from path: {}",
18+
project_path.display()
19+
)
20+
})?;
21+
22+
// Install dependencies
23+
install(false, &project_path).await.map_err(|e| {
24+
anyhow::anyhow!("Failed to prepare dependencies for package to link: {}", e)
25+
})?;
26+
27+
let global_package_path = get_global_package_dir(&prefix)?.join(package_info.name.clone());
28+
// link local project to global package
29+
link(&project_path, &global_package_path).with_context(|| {
30+
format!(
31+
"Failed to link local project to global package: {} -> {}",
32+
project_path.display(),
33+
global_package_path.display()
34+
)
35+
})?;
36+
// If the package has binary files, also link them to global bin directory
37+
if package_info.has_bin_files() {
38+
let global_bin_dir = global_package_path.join("bin");
39+
package_info
40+
.link_to_target(&global_bin_dir)
41+
.await
42+
.with_context(|| {
43+
format!(
44+
"Failed to link binary files to global bin directory: {}",
45+
global_bin_dir.display()
46+
)
47+
})?;
48+
}
49+
50+
Ok(())
51+
}
52+
53+
/// Link a global package to local node_modules (equivalent to npm link pkg_name)
54+
pub async fn link_global_to_local(package_name: &str, prefix: Option<&str>) -> Result<()> {
55+
let cwd = std::env::current_dir().context("Failed to get current directory")?;
56+
let project_path = update_cwd_to_project(&cwd).await?;
57+
58+
let global_package_path = get_global_package_dir(&prefix)?.join(package_name);
59+
60+
// Create package info from path
61+
let package_info = PackageInfo::from_path(&global_package_path).with_context(|| {
62+
format!(
63+
"Failed to parse package info from path: {}",
64+
global_package_path.display()
65+
)
66+
})?;
67+
68+
// Create symlink from global package to local project
69+
let local_link_path = project_path.join("node_modules/").join(package_name);
70+
link(&global_package_path, &local_link_path).with_context(|| {
71+
format!(
72+
"Failed to link global package to local: {} -> {}",
73+
global_package_path.display(),
74+
local_link_path.display()
75+
)
76+
})?;
77+
78+
log_verbose(&format!(
79+
"link global package to local: {} -> {}",
80+
global_package_path.display(),
81+
project_path.display()
82+
));
83+
84+
// If the package has binary files, also link them to target project bin directory
85+
if package_info.has_bin_files() {
86+
log_verbose(&format!(
87+
"Linking binary files to project bin directory: {}",
88+
package_info.name
89+
));
90+
let bin_dir = project_path.join("node_modules/.bin");
91+
package_info
92+
.link_to_target(&bin_dir)
93+
.await
94+
.with_context(|| {
95+
format!(
96+
"Failed to link binary files to project bin directory: {}",
97+
bin_dir.display()
98+
)
99+
})?;
100+
}
101+
102+
Ok(())
103+
}
104+
105+
#[cfg(test)]
106+
mod tests {
107+
use super::*;
108+
use std::fs;
109+
use tempfile::TempDir;
110+
111+
// helper to write minimal package.json and lock to bypass install flow side effects
112+
async fn write_minimal_project_files(path: &std::path::Path, name: &str) {
113+
let pkg = format!(
114+
r#"{{
115+
"name": "{name}",
116+
"version": "1.0.0"
117+
}}"#
118+
);
119+
fs::write(path.join("package.json"), pkg).unwrap();
120+
let lock = r#"{
121+
"name": "test",
122+
"version": "1.0.0",
123+
"lockfileVersion": 3,
124+
"requires": true,
125+
"packages": {
126+
"": {"name": "test", "version": "1.0.0"}
127+
}
128+
}"#;
129+
fs::write(path.join("package-lock.json"), lock).unwrap();
130+
fs::create_dir_all(path.join("node_modules")).unwrap();
131+
}
132+
133+
#[tokio::test]
134+
async fn test_link_global_to_local_basic() {
135+
let temp_dir = TempDir::new().unwrap();
136+
let project = temp_dir.path().join("proj");
137+
let global = temp_dir.path().join("global");
138+
fs::create_dir_all(&project).unwrap();
139+
fs::create_dir_all(&global).unwrap();
140+
141+
// minimal project files to bypass install
142+
write_minimal_project_files(&project, "consumer").await;
143+
144+
// create fake global package dir with package.json
145+
let pkg_name = "lib-a";
146+
let global_pkg_dir = global.join("lib/node_modules").join(pkg_name);
147+
fs::create_dir_all(&global_pkg_dir).unwrap();
148+
let pkg_json = format!(
149+
r#"{{
150+
"name": "{pkg_name}",
151+
"version": "1.0.0"
152+
}}"#
153+
);
154+
fs::write(global_pkg_dir.join("package.json"), pkg_json).unwrap();
155+
156+
// set cwd to project so local node_modules path is resolved
157+
std::env::set_current_dir(&project).unwrap();
158+
159+
let prefix_str = global.to_string_lossy().to_string();
160+
let result = link_global_to_local(pkg_name, Some(&prefix_str)).await;
161+
assert!(result.is_ok(), "link_global_to_local should succeed");
162+
163+
// verify node_modules symlink
164+
let local_link = project.join("node_modules").join(pkg_name);
165+
assert!(local_link.exists());
166+
assert!(local_link.is_symlink() || local_link.is_dir());
167+
}
168+
}

crates/pm/src/cmd/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod config;
33
pub mod deps;
44
pub mod execute;
55
pub mod install;
6+
pub mod link;
67
pub mod list;
78
pub mod rebuild;
89
pub mod run;

crates/pm/src/constants.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,8 @@ pub mod cmd {
4646
pub const CONFIG_NAME: &str = "config";
4747
pub const CONFIG_ALIAS: &str = "cfg";
4848
pub const CONFIG_ABOUT: &str = "Manage configuration";
49+
50+
pub const LINK_NAME: &str = "link";
51+
pub const LINK_ALIAS: &str = "ln";
52+
pub const LINK_ABOUT: &str = "Link a package like npm link";
4953
}

crates/pm/src/helper/auto_update.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub async fn init_auto_update() -> Result<()> {
4141
"New version found: {} (current version: {}), updating automatically...",
4242
cache.version, current_version
4343
));
44-
log_info(&format!("utoo i @utoo/utoo{} -g", cache.version));
44+
log_info(&format!("utoo i utoo@{} -g", cache.version));
4545

4646
execute_update(&cache.version).await?;
4747
}
@@ -60,7 +60,7 @@ async fn check_and_update_cache() -> Result<VersionCache> {
6060

6161
async fn execute_update(version: &str) -> Result<()> {
6262
let status = Command::new("utoo")
63-
.args(["i", &format!("@utoo/utoo@{version}"), "-g"])
63+
.args(["i", &format!("utoo@{version}"), "-g"])
6464
.env("CI", "1")
6565
.stdout(Stdio::inherit())
6666
.stderr(Stdio::inherit())
@@ -77,7 +77,7 @@ async fn execute_update(version: &str) -> Result<()> {
7777
}
7878

7979
async fn check_remote_version() -> Result<()> {
80-
let registry_url = format!("{}/@utoo/utoo/latest", get_registry());
80+
let registry_url = format!("{}/utoo/latest", get_registry());
8181
let client = reqwest::Client::builder()
8282
.timeout(std::time::Duration::from_millis(2000))
8383
.build()

crates/pm/src/helper/global_bin.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use anyhow::{Context, Result};
2+
use std::path::PathBuf;
3+
4+
// Get global bin directory based on prefix
5+
//
6+
// This function determines the global binary directory where executables should be linked.
7+
// It's used by both link and install commands to ensure consistent binary linking behavior.
8+
//
9+
// # Arguments
10+
// * `prefix` - Optional custom prefix path. If None, uses the current executable's directory.
11+
//
12+
// # Returns
13+
// * `PathBuf` - Path to the global bin directory
14+
//
15+
// # Examples
16+
//
17+
// ```rust
18+
// // Use default (current executable directory)
19+
// let bin_dir = get_global_bin_dir(&None)?;
20+
//
21+
// // Use custom prefix
22+
// let bin_dir = get_global_bin_dir(&Some("/usr/local"))?;
23+
// ```
24+
pub fn get_global_bin_dir(prefix: &Option<&str>) -> Result<PathBuf> {
25+
Ok(get_global_package_dir(prefix)?.join("bin"))
26+
}
27+
28+
pub fn get_global_package_dir(prefix: &Option<&str>) -> Result<PathBuf> {
29+
let lib_path = match prefix {
30+
Some(prefix) => PathBuf::from(prefix).join("lib/node_modules"),
31+
None => std::env::current_exe()
32+
.context("Failed to get current executable path")?
33+
.parent()
34+
.context("Failed to get executable parent directory")?
35+
.to_path_buf()
36+
.join("lib/node_modules"),
37+
};
38+
Ok(lib_path)
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use super::*;
44+
45+
#[test]
46+
fn test_get_global_bin_dir_with_prefix() {
47+
let prefix: Option<&str> = Some("/usr/local");
48+
let result = get_global_bin_dir(&prefix).unwrap();
49+
assert_eq!(result, PathBuf::from("/usr/local/lib/node_modules/bin"));
50+
}
51+
52+
#[test]
53+
fn test_get_global_bin_dir_without_prefix() {
54+
let expected = std::env::current_exe()
55+
.expect("current_exe should exist")
56+
.parent()
57+
.expect("exe should have a parent directory")
58+
.to_path_buf()
59+
.join("lib/node_modules")
60+
.join("bin");
61+
let result = get_global_bin_dir(&None).unwrap();
62+
assert_eq!(result, expected);
63+
}
64+
65+
#[test]
66+
fn test_get_global_bin_dir_with_empty_prefix() {
67+
let prefix: Option<&str> = Some("");
68+
let result = get_global_bin_dir(&prefix).unwrap();
69+
assert_eq!(result, PathBuf::from("lib/node_modules/bin"));
70+
}
71+
}

crates/pm/src/helper/lock.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,7 @@ pub async fn parse_package_spec(spec: &str) -> Result<(String, String, String)>
174174
Ok((name, resolved.version, version_spec))
175175
}
176176

177-
pub async fn prepare_global_package_json(
178-
npm_spec: &str,
179-
prefix: &Option<String>,
180-
) -> Result<PathBuf> {
177+
pub async fn prepare_global_package_json(npm_spec: &str, prefix: Option<&str>) -> Result<PathBuf> {
181178
// Parse package name and version
182179
let (name, _version, version_spec) = parse_package_spec(npm_spec).await?;
183180
let lib_path = match prefix {

crates/pm/src/helper/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod auto_update;
22
pub mod cli;
33
pub mod compatibility;
44
pub mod deps;
5+
pub mod global_bin;
56
pub mod install_runtime;
67
pub mod lock;
78
pub mod package;

0 commit comments

Comments
 (0)