use std::path::{Path, PathBuf};
use std::thread;
use serde::Deserialize;
use super::node_versions::{self, Version};
use crate::manager::{Package, capture};
pub fn incompatible(bin: &Path, packages: &[Package]) -> Vec<(Package, String)> {
let Some(node) = node_version(bin) else {
return Vec::new();
};
thread::scope(|scope| {
let handles: Vec<_> = packages
.iter()
.filter(|p| !p.latest.is_empty())
.map(|p| scope.spawn(move || (p, required(bin, p))))
.collect();
handles
.into_iter()
.filter_map(|h| h.join().ok())
.filter_map(|(p, range)| {
let range = range?;
(!satisfies(&range, node)).then(|| {
let reason = format!(
"{} needs Node {range}, and this is Node {}",
p.latest,
node_versions::show(node)
);
(p.clone(), reason)
})
})
.collect()
})
}
fn node_version(bin: &Path) -> Option<Version> {
let beside = bin.with_file_name(if cfg!(windows) { "node.exe" } else { "node" });
let node = if beside.exists() {
beside
} else {
PathBuf::from("node")
};
node_versions::parse(&capture(node, ["--version"]).ok()?.expect(&[0]).ok()?)
}
fn required(bin: &Path, package: &Package) -> Option<String> {
let spec = format!("{}@{}", package.name, package.latest);
let json = capture(bin, ["view", &spec, "engines.node", "--json"])
.ok()?
.expect(&[0])
.ok()?;
#[derive(Deserialize)]
#[serde(untagged)]
enum Engines {
One(String),
Many(Vec<String>),
}
match serde_json::from_str(json.trim()).ok()? {
Engines::One(range) => Some(range),
Engines::Many(ranges) => ranges.into_iter().last(),
}
}
pub fn satisfies(range: &str, version: Version) -> bool {
range
.split("||")
.map(|set| in_set(set, version))
.try_fold(false, |any, set| set.map(|ok| any || ok))
.unwrap_or(true)
}
fn in_set(set: &str, v: Version) -> Option<bool> {
let set = set.trim();
if let Some((low, high)) = set.split_once(" - ") {
let (low, high) = (partial(low)?, partial(high)?);
return Some(v >= floor(&low) && below_ceiling(&high, v));
}
let mut comparators = Vec::new();
let mut pending = String::new();
for token in set.split_whitespace() {
pending.push_str(token);
if !matches!(token, ">" | ">=" | "<" | "<=" | "=" | "^" | "~") {
comparators.push(std::mem::take(&mut pending));
}
}
comparators
.iter()
.map(|c| comparator(c, v))
.try_fold(true, |all, c| c.map(|ok| all && ok))
}
fn comparator(text: &str, v: Version) -> Option<bool> {
let ops = [">=", "<=", ">", "<", "=", "^", "~"];
let op = ops.iter().find(|op| text.starts_with(**op)).copied();
let p = partial(&text[op.map_or(0, str::len)..])?;
let low = floor(&p);
Some(match op {
Some(">=") => v >= low,
Some(">") if p.len() == 3 => v > low,
Some(">") => !below_ceiling(&p, v),
Some("<") => v < low,
Some("<=") if p.len() == 3 => v <= low,
Some("<=") => below_ceiling(&p, v),
Some("^") => {
let fixed = p.iter().position(|&n| n != 0).unwrap_or(p.len() - 1);
v >= low && below_ceiling(&p[..=fixed], v)
}
Some("~") => v >= low && below_ceiling(&p[..p.len().min(2)], v),
_ if p.is_empty() => true,
_ => v >= low && below_ceiling(&p, v),
})
}
fn partial(text: &str) -> Option<Vec<u64>> {
let text = text.trim().trim_start_matches('v');
let text = text.split(['-', '+']).next()?;
let mut parts = Vec::new();
for part in text.split('.') {
if matches!(part, "x" | "X" | "*" | "") {
break;
}
parts.push(part.parse().ok()?);
}
(parts.len() <= 3).then_some(parts)
}
fn floor(p: &[u64]) -> Version {
[0, 1, 2].map(|i| p.get(i).copied().unwrap_or(0))
}
fn below_ceiling(p: &[u64], v: Version) -> bool {
match p.len() {
0 => true,
3 => v <= floor(p),
n => {
let mut ceiling = floor(p);
ceiling[n - 1] += 1;
v < ceiling
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_npm_ranges() {
let eslint = "^18.18.0 || ^20.9.0 || >=21.1.0";
assert!(satisfies(eslint, [20, 11, 1]));
assert!(satisfies(eslint, [24, 0, 0]));
assert!(!satisfies(eslint, [20, 8, 0]));
assert!(!satisfies(eslint, [19, 0, 0]));
assert!(!satisfies(">=22.9.0", [20, 19, 5]));
assert!(satisfies(">= 18", [18, 0, 0]));
assert!(satisfies(">=18.0.0 <19", [18, 20, 4]));
assert!(!satisfies(">=18.0.0 <19", [19, 0, 0]));
assert!(satisfies("18.x || 20.x", [20, 3, 0]));
assert!(!satisfies("~18.17", [18, 18, 0]));
assert!(satisfies("^0.10.0", [0, 10, 5]));
assert!(!satisfies("^0.10.0", [0, 11, 0]));
assert!(satisfies("16 - 20", [20, 9, 9]));
assert!(!satisfies("16 - 20", [21, 0, 0]));
assert!(satisfies("*", [4, 0, 0]));
assert!(!satisfies(">16.2", [16, 2, 9]));
}
#[test]
fn unknown_syntax_does_not_hold_anything_back() {
assert!(satisfies("node >= lts", [8, 0, 0]));
}
}