use super::*;
use std::path::PathBuf;
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use tokio;
#[derive(Debug)]
pub struct YarnCompatibility {
config: PackageManagerConfig,
registry_client: RegistryClient,
yarn_lock_parser: YarnLockParser,
plug_n_play: PlugNPlayManager,
auth_manager: AuthManager,
}
impl YarnCompatibility {
pub fn new(config: PackageManagerConfig) -> Self {
Self {
registry_client: RegistryClient::new(config.registry_url.clone(), config.timeout_ms),
yarn_lock_parser: YarnLockParser::new(),
plug_n_play: PlugNPlayManager::new(),
auth_manager: AuthManager::new(),
config,
}
}
pub async fn init(&self, project_name: &str) -> Result<(), Box<dyn std::error::Error>> {
let package_json: _ = PackageJson {
name: project_name.to_string(),
version: "1.0.0".to_string(),
description: None,
main: "index.js".to_string(),
scripts: HashMap::new(),
keywords: vec![],
author: None,
license: "ISC".to_string(),
dependencies: HashMap::new(),
dev_dependencies: HashMap::new(),
peer_dependencies: HashMap::new(),
optional_dependencies: HashMap::new(),
engines: None,
os: vec![],
cpu: vec![],
private: false,
workspaces: None,
publish_config: None,
exports: None,
types: None,
typesVersions: None,
typings: None,
files: None,
bin: None,
man: None,
directories: None,
repository: None,
bugs: None,
homepage: None,
readme: None,
funding: None,
overrides: None,
resolutions: None,
bundle_dependencies: None,
deprecated: None,
description_map: HashMap::new(),
};
let mut file = tokio::fs::File::create("package.json").await?;
file.write_all(&serde_json::to_string_pretty(&package_json)?.into_bytes()).await?;
let yarnrc_content: _ = r#"# Yarn 2+ 配置文件
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.x.x.cjs
"#;
let mut file = tokio::fs::File::create(".yarnrc.yml").await?;
file.write_all(yarnrc_content.as_bytes()).await?;
tokio::fs::create_dir_all(".yarn/releases").await?;
Ok(())
}
pub async fn install(&self, options: &InstallOptions) -> Result<(), Box<dyn std::error::Error>> {
if PathBuf::from("yarn.lock").exists() {
self.install_from_lockfile().await?;
} else {
self.install_from_package_json().await?;
}
Ok(())
}
async fn install_from_lockfile(&self) -> Result<(), Box<dyn std::error::Error>> {
let lockfile_content: _ = tokio::fs::read_to_string("yarn.lock").await?;
let lockfile: _ = self.yarn_lock_parser.parse(&lockfile_content)?;
for (package_key, entry) in lockfile.entries {
let (package_name, version) = self.yarn_lock_parser.parse_package_key(&package_key)?;
let resolution: _ = PackageResolution {
package_name,
version,
resolved_url: entry.resolved,
integrity: entry.integrity,
dependencies: entry.dependencies,
peer_dependencies: HashMap::new(),
optional_dependencies: HashMap::new(),
bins: HashMap::new(),
main: "index.js".to_string(),
types: None,
exports: None,
};
self.download_and_install_package(&resolution).await?;
}
Ok(())
}
async fn install_from_package_json(&self) -> Result<(), Box<dyn std::error::Error>> {
let content: _ = tokio::fs::read_to_string("package.json").await?;
let package_json: PackageJson = serde_json::from_str(&content)?;
let mut resolutions = HashMap::new();
for (name, version_spec) in package_json.dependencies {
let version_range: _ = VersionRange::parse(&version_spec)?;
let resolution: _ = self.resolve_package(&PackageSpec::NameRange(name.clone(), version_spec)).await?;
resolutions.insert(name, resolution);
}
for (name, version_spec) in package_json.dev_dependencies {
let resolution: _ = self.resolve_package(&PackageSpec::NameRange(name.clone(), version_spec)).await?;
resolutions.insert(name, resolution);
}
for resolution in resolutions.values() {
self.download_and_install_package(resolution).await?;
}
self.generate_yarn_lock(&resolutions).await?;
Ok(())
}
async fn resolve_package(&self, spec: &PackageSpec) -> Result<PackageResolution, Box<dyn std::error::Error>> {
let (package_name, version_range) = match spec {
PackageSpec::Name(name) => (name.clone(), VersionRange::Wildcard),
PackageSpec::NameVersion(name, version) => (name.clone(), VersionRange::Exact(version.clone()),
PackageSpec::NameRange(name, range) => (name.clone(), VersionRange::parse(range)?),
_ => return Err("Unsupported package spec for Yarn".into()),
};
let package_info: _ = self.registry_client.get_package_info(&package_name).await?;
let selected_version: _ = self.select_version(&package_info, &version_range)?;
let resolution: _ = PackageResolution {
package_name,
version: selected_version.clone(),
resolved_url: format!("https://registry.yarnpkg.com/{}/-/{}-{}.tgz", package_name, package_name, selected_version),
integrity: "sha512-...".to_string(), dependencies: package_info.dependencies,
peer_dependencies: package_info.peer_dependencies,
optional_dependencies: package_info.optional_dependencies,
bins: package_info.bins,
main: package_info.main,
types: package_info.types,
exports: package_info.exports,
};
Ok(resolution)
}
fn select_version(&self, package_info: &NpmPackageInfo, range: &VersionRange) -> Result<String, String> {
for version in &package_info.versions {
if range.matches(version) {
return Ok(version.clone());
}
}
Err(format!("No version found matching range: {}", range))
}
async fn download_and_install_package(&self, resolution: &PackageResolution) -> Result<(), Box<dyn std::error::Error>> {
let package_dir: _ = PathBuf::from("node_modules").join(&resolution.package_name);
tokio::fs::create_dir_all(&package_dir).await?;
let package_json: _ = PackageJson {
name: resolution.package_name.clone(),
version: resolution.version.clone(),
main: resolution.main.clone(),
types: resolution.types.clone(),
bin: resolution.bins.clone(),
dependencies: resolution.dependencies.clone(),
peer_dependencies: resolution.peer_dependencies.clone(),
optional_dependencies: resolution.optional_dependencies.clone(),
..Default::default()
};
let package_json_path: _ = package_dir.join("package.json");
let mut file = tokio::fs::File::create(&package_json_path).await?;
file.write_all(&serde_json::to_string_pretty(&package_json)?.into_bytes()).await?;
Ok(())
}
async fn generate_yarn_lock(&self, resolutions: &HashMap<String, PackageResolution>) -> Result<(), Box<dyn std::error::Error>> {
let mut content = String::new();
content.push_str("# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n");
content.push_str("# yarn lockfile v1\n\n");
for (name, resolution) in resolutions {
let package_key: _ = format!("\{}@{}\"", name, resolution.version"));
content.push_str(&format!("{}:\n", package_key));
content.push_str(&format!(" version \"{}\"\n", resolution.version));
content.push_str(&format!(" resolved \"{}\"\n", resolution.resolved_url));
content.push_str(&format!(" integrity {}\n", resolution.integrity));
content.push('\n');
}
tokio::fs::write("yarn.lock", content).await?;
Ok(())
}
pub async fn enable_plug_n_play(&self) -> Result<(), Box<dyn std::error::Error>> {
self.plug_n_play.enable().await?;
Ok(())
}
pub async fn run_script(&self, script_name: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content: _ = tokio::fs::read_to_string("package.json").await?;
let package_json: PackageJson = serde_json::from_str(&content)?;
if let Some(command) = package_json.scripts.get(script_name) {
println!("Running script '{}': {}", script_name, command);
Ok(0)
} else {
eprintln!("Script '{}' not found", script_name);
Ok(1)
}
}
pub async fn add(&self, packages: &[PackageSpec], dev: bool) -> Result<(), Box<dyn std::error::Error>> {
let mut resolutions = HashMap::new();
for spec in packages {
let resolution: _ = self.resolve_package(spec).await?;
self.download_and_install_package(&resolution).await?;
resolutions.insert(resolution.package_name.clone(), resolution);
}
self.update_package_json(&resolutions, dev).await?;
self.generate_yarn_lock(&resolutions).await?;
Ok(())
}
async fn update_package_json(
&self,
resolutions: &HashMap<String, PackageResolution>,
dev: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let content: _ = tokio::fs::read_to_string("package.json").await?;
let mut package_json: PackageJson = serde_json::from_str(&content)?;
for (name, resolution) in resolutions {
if dev {
package_json.dev_dependencies.insert(name.clone(), format!("^{}", resolution.version));
} else {
package_json.dependencies.insert(name.clone(), format!("^{}", resolution.version));
}
}
let mut file = tokio::fs::File::create("package.json").await?;
file.write_all(&serde_json::to_string_pretty(&package_json)?.into_bytes()).await?;
Ok(())
}
}
#[derive(Debug)]
pub struct YarnLockParser {
}
impl YarnLockParser {
pub fn new() -> Self {
Self {}
}
pub fn parse(&self, content: &str) -> Result<YarnLockfile, Box<dyn std::error::Error>> {
let lines: Vec<&str> = content.lines().collect();
let mut entries = HashMap::new();
let mut current_package = None;
let mut current_entry = None;
for line in lines {
if line.starts_with('"') && line.contains('@') {
if let Some(entry) = current_entry.take() {
if let Some(pkg_name) = current_package.take() {
entries.insert(pkg_name, entry);
}
}
current_package = Some(line.trim().trim_matches('"').to_string());
current_entry = Some(YarnLockEntry {
version: "".to_string(),
resolved: "".to_string(),
integrity: "".to_string(),
dependencies: HashMap::new(),
});
} else if let Some(ref mut entry) = current_entry {
if line.trim().starts_with("version") {
entry.version = line.split('"').nth(1).unwrap_or("").to_string();
} else if line.trim().starts_with("resolved") {
entry.resolved = line.split('"').nth(1).unwrap_or("").to_string();
} else if line.trim().starts_with("integrity") {
entry.integrity = line.split('"').nth(1).unwrap_or("").to_string();
}
}
}
if let Some(entry) = current_entry.take() {
if let Some(pkg_name) = current_package.take() {
entries.insert(pkg_name, entry);
}
}
Ok(YarnLockfile { entries })
}
pub fn parse_package_key(&self, key: &str) -> Result<(String, String), Box<dyn std::error::Error>> {
let key: _ = key.trim().trim_matches('"');
if let Some(at_pos) = key.rfind('@') {
let (name_part, version_part) = key.split_at(at_pos);
let name: _ = if name_part.starts_with("@scope/") {
name_part.to_string()
} else {
name_part.trim_start_matches('@').to_string()
};
let version: _ = version_part.trim_start_matches('@').to_string();
Ok((name, version))
} else {
Err("Invalid package key format".into())
}
}
}
#[derive(Debug)]
pub struct YarnLockfile {
pub entries: HashMap<String, YarnLockEntry>,
}
#[derive(Debug, Clone)]
pub struct YarnLockEntry {
pub version: String,
pub resolved: String,
pub integrity: String,
pub dependencies: HashMap<String, String>,
}
#[derive(Debug)]
pub struct PlugNPlayManager {
enabled: bool,
}
impl PlugNPlayManager {
pub fn new() -> Self {
Self { enabled: false }
}
pub async fn enable(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let pnp_content: _ = r#"
// 自动生成的 Plug'n'Play 文件
const path = require('path');
module.exports = {
// PnP 实现
};
"#;
tokio::fs::write(".pnp.cjs", pnp_content).await?;
self.enabled = true;
Ok(())
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
}