use crate::{Error, GitSource, LocalSource, RemoteTarballSource};
use aube_util::path::normalize_lexical;
use std::collections::BTreeMap;
use std::path::{Component, Path};
pub(super) fn bun_key_to_alias_name(key: &str) -> String {
if let Some(last_slash) = key.rfind('/') {
let tail_start = key[..last_slash].rfind('/').map(|i| i + 1).unwrap_or(0);
if key[tail_start..last_slash].starts_with('@') {
key[tail_start..].to_string()
} else {
key[last_slash + 1..].to_string()
}
} else {
key.to_string()
}
}
pub(super) fn classify_bun_ident(
alias_name: &str,
raw_name: &str,
raw_version: &str,
integrity: Option<&str>,
) -> Result<(String, String, Option<LocalSource>, Option<String>), Error> {
let alias_of = if alias_name != raw_name {
Some(raw_name.to_string())
} else {
None
};
let name = alias_name.to_string();
if raw_version.starts_with("workspace:") {
let rel = raw_version.strip_prefix("workspace:").unwrap_or("");
let is_path = rel.starts_with('.') || rel.starts_with('/') || rel.contains('/');
let path_buf = std::path::PathBuf::from(if rel.is_empty() || !is_path { "." } else { rel });
return Ok((
name,
raw_version.to_string(),
Some(LocalSource::Link(path_buf)),
alias_of,
));
}
if let Some(rest) = raw_version.strip_prefix("github:") {
let (url, committish) = split_committish(rest);
return Ok((
name,
raw_version.to_string(),
Some(LocalSource::Git(GitSource {
url: format!("https://github.com/{url}.git"),
committish: committish.clone(),
resolved: committish.unwrap_or_default(),
integrity: None,
subpath: None,
})),
alias_of,
));
}
if (raw_version.starts_with("git+")
|| raw_version.starts_with("git://")
|| raw_version.starts_with("git@"))
&& let Some((url, committish, subpath)) = crate::parse_git_spec(raw_version)
{
return Ok((
name,
raw_version.to_string(),
Some(LocalSource::Git(GitSource {
url,
committish: committish.clone(),
resolved: committish.unwrap_or_default(),
integrity: None,
subpath,
})),
alias_of,
));
}
if raw_version.starts_with("http://") || raw_version.starts_with("https://") {
return Ok((
name,
raw_version.to_string(),
Some(LocalSource::RemoteTarball(RemoteTarballSource {
url: raw_version.to_string(),
integrity: integrity.map(str::to_string).unwrap_or_default(),
git_hosted: false,
})),
alias_of,
));
}
if let Some(rest) = raw_version.strip_prefix("file:") {
let rel = std::path::PathBuf::from(rest);
let kind = if LocalSource::path_looks_like_tarball(&rel) {
LocalSource::Tarball(rel)
} else {
LocalSource::Directory(rel)
};
return Ok((name, raw_version.to_string(), Some(kind), alias_of));
}
let raw_path = std::path::PathBuf::from(raw_version);
if LocalSource::path_looks_like_tarball(&raw_path) {
return Ok((
name,
raw_version.to_string(),
Some(LocalSource::Tarball(raw_path)),
alias_of,
));
}
if let Some(rest) = raw_version.strip_prefix("link:") {
return Ok((
name,
raw_version.to_string(),
Some(LocalSource::Link(std::path::PathBuf::from(rest))),
alias_of,
));
}
Ok((name, raw_version.to_string(), None, alias_of))
}
pub(super) fn rebase_workspace_scoped_local_source(
key: &str,
local: LocalSource,
workspace_scopes: &[(&str, &str)],
) -> LocalSource {
let Some(local_path) = local.path() else {
return local;
};
if !local_path
.components()
.any(|c| matches!(c, Component::ParentDir))
{
return local;
}
let Some((_, ws_path)) = workspace_scopes.iter().find(|(name, _)| {
key.strip_prefix(*name)
.is_some_and(|suffix| suffix.starts_with('/'))
}) else {
return local;
};
let rebased = normalize_lexical(&Path::new(ws_path).join(local_path));
match local {
LocalSource::Directory(_) => LocalSource::Directory(rebased),
LocalSource::Tarball(_) => LocalSource::Tarball(rebased),
LocalSource::Link(_) => LocalSource::Link(rebased),
LocalSource::Portal(_) => LocalSource::Portal(rebased),
LocalSource::Exec(_) => LocalSource::Exec(rebased),
LocalSource::Git(_) | LocalSource::RemoteTarball(_) => local,
}
}
pub(super) fn split_committish(spec: &str) -> (String, Option<String>) {
match spec.rfind('#') {
Some(i) => (spec[..i].to_string(), Some(spec[i + 1..].to_string())),
None => (spec.to_string(), None),
}
}
pub(super) fn bin_value_to_map(
default_name: &str,
value: &serde_json::Value,
) -> BTreeMap<String, String> {
match value {
serde_json::Value::String(s) => {
let mut map = BTreeMap::new();
map.insert(default_name.to_string(), s.clone());
map
}
serde_json::Value::Object(obj) => obj
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect(),
_ => BTreeMap::new(),
}
}
pub(super) fn resolve_nested_bun(
pkg_key: &str,
dep_name: &str,
key_info: &BTreeMap<String, (String, String)>,
) -> Option<String> {
let mut base = pkg_key.to_string();
loop {
let candidate = if base.is_empty() {
dep_name.to_string()
} else {
format!("{base}/{dep_name}")
};
if key_info.contains_key(&candidate) {
return Some(candidate);
}
if base.is_empty() {
return None;
}
if let Some(idx) = base.rfind('/') {
let tail_start = base[..idx].rfind('/').map(|i| i + 1).unwrap_or(0);
if base[tail_start..idx].starts_with('@') {
base.truncate(tail_start.saturating_sub(1));
} else {
base.truncate(idx);
}
} else {
base.clear();
}
}
}
pub(super) fn resolve_workspace_dep(
ws_path: &str,
ws_name: Option<&str>,
dep_name: &str,
key_info: &BTreeMap<String, (String, String)>,
) -> Option<String> {
if let Some(ws_name) = ws_name {
let ws_specific = format!("{ws_name}/{dep_name}");
if key_info.contains_key(&ws_specific) {
return Some(ws_specific);
}
}
if !ws_path.is_empty() {
let ws_specific = format!("{ws_path}/{dep_name}");
if key_info.contains_key(&ws_specific) {
return Some(ws_specific);
}
}
if key_info.contains_key(dep_name) {
return Some(dep_name.to_string());
}
None
}
pub(super) fn split_ident(ident: &str) -> Option<(String, String)> {
if let Some(rest) = ident.strip_prefix('@') {
let slash = rest.find('/')?;
let after_slash = &rest[slash + 1..];
let at = after_slash.find('@')?;
let name = format!("@{}", &rest[..slash + 1 + at]);
let version = after_slash[at + 1..].to_string();
Some((name, version))
} else {
let at = ident.find('@')?;
Some((ident[..at].to_string(), ident[at + 1..].to_string()))
}
}