use super::probe;
use super::types::{FallbackPolicy, MismatchPolicy, ResolutionStep, ResolvedPm, Resolver};
use super::{DevEnginesFailReason, ResolveError};
use crate::tool::node::{
ManifestPmDecl, ManifestSource, OnFail, VersionCheck, check_version_constraint,
detect_pm_from_manifest, find_manifest_upwards,
};
use crate::types::{DetectionWarning, Ecosystem, PackageManager, ProjectContext};
impl<'ctx> Resolver<'ctx> {
pub(crate) const fn new(
ctx: &'ctx ProjectContext,
overrides: &'ctx super::types::ResolutionOverrides,
) -> Self {
Self { ctx, overrides }
}
pub(crate) fn resolve_node_pm(&self) -> Result<ResolvedPm, ResolveError> {
let mut warnings = Vec::new();
if let Some(o) = self.overrides.pm.as_ref() {
if !o.pm.can_dispatch_node_scripts() {
return Err(ResolveError::InvalidOverride {
value: o.pm.label().to_string(),
reason: "cannot dispatch package.json scripts (use a Node-ecosystem PM, or \
`--pm deno` for Deno tasks)",
});
}
return Ok(ResolvedPm {
pm: o.pm,
via: ResolutionStep::Override(o.origin.clone()),
warnings,
});
}
if let Some(o) = self
.overrides
.pm_by_ecosystem
.get(&Ecosystem::Node)
.or_else(|| self.overrides.pm_by_ecosystem.get(&Ecosystem::Deno))
{
return Ok(ResolvedPm {
pm: o.pm,
via: ResolutionStep::Override(o.origin.clone()),
warnings,
});
}
if let Some(decl) = detect_pm_from_manifest(&self.ctx.root) {
cross_check_against_lockfile(
&decl,
self.ctx,
self.overrides.on_mismatch,
&mut warnings,
)?;
apply_manifest_on_fail(
&decl,
&mut warnings,
real_binary_check,
check_version_constraint,
)?;
let via = match decl.source {
ManifestSource::PackageManager => ResolutionStep::ManifestPackageManager,
ManifestSource::DevEngines => ResolutionStep::ManifestDevEngines {
on_fail: decl.on_fail,
},
};
return Ok(ResolvedPm {
pm: decl.pm,
via,
warnings,
});
}
if let Some(pm) = self.ctx.primary_node_pm().or_else(|| {
self.ctx
.primary_pm()
.filter(|pm| pm.can_dispatch_node_scripts())
}) {
return Ok(ResolvedPm {
pm,
via: ResolutionStep::Lockfile,
warnings,
});
}
match self.overrides.fallback {
FallbackPolicy::Probe => {
if find_manifest_upwards(&self.ctx.root).is_none() {
return Err(no_pm_found_soft());
}
let mut found = probe::probe_all(probe::NODE_PROBE_ORDER);
if found.is_empty() {
return Err(no_pm_found_soft());
}
let (picked, binary) = found.remove(0);
warnings.push(DetectionWarning::PathProbeFallback {
picked,
ecosystem: Ecosystem::Node,
others_available: found.into_iter().map(|(pm, _)| pm).collect(),
});
Ok(ResolvedPm {
pm: picked,
via: ResolutionStep::PathProbe { binary },
warnings,
})
}
FallbackPolicy::Npm => {
warnings.push(DetectionWarning::LegacyNpmFallbackUsed {
ecosystem: Ecosystem::Node,
});
Ok(ResolvedPm {
pm: PackageManager::Npm,
via: ResolutionStep::LegacyNpmFallback,
warnings,
})
}
FallbackPolicy::Error => Err(no_pm_found_hard()),
}
}
}
pub(super) fn apply_manifest_on_fail<P, V>(
decl: &ManifestPmDecl,
warnings: &mut Vec<DetectionWarning>,
is_present: P,
check_version: V,
) -> Result<(), ResolveError>
where
P: FnOnce(PackageManager) -> bool,
V: FnOnce(PackageManager, &str) -> VersionCheck,
{
if matches!(decl.on_fail, OnFail::Ignore) {
return Ok(());
}
if !is_present(decl.pm) {
return on_fail_missing_binary(decl, warnings);
}
if let Some(range) = decl.version.as_deref()
&& let VersionCheck::Mismatch { declared, actual } = check_version(decl.pm, range)
{
return on_fail_version_mismatch(decl, &declared, &actual, warnings);
}
Ok(())
}
fn real_binary_check(pm: PackageManager) -> bool {
probe::probe(pm).is_some()
}
fn on_fail_missing_binary(
decl: &ManifestPmDecl,
warnings: &mut Vec<DetectionWarning>,
) -> Result<(), ResolveError> {
match decl.on_fail {
OnFail::Ignore => Ok(()),
OnFail::Warn => {
warnings.push(DetectionWarning::DevEnginesBinaryMissing { pm: decl.pm });
Ok(())
}
OnFail::Error => Err(ResolveError::DevEnginesFailHard {
pm: decl.pm,
reason: DevEnginesFailReason::BinaryMissing,
}),
}
}
fn on_fail_version_mismatch(
decl: &ManifestPmDecl,
declared: &str,
actual: &str,
warnings: &mut Vec<DetectionWarning>,
) -> Result<(), ResolveError> {
match decl.on_fail {
OnFail::Ignore => Ok(()),
OnFail::Warn => {
warnings.push(DetectionWarning::DevEnginesVersionMismatch {
pm: decl.pm,
declared: declared.to_string(),
actual: actual.to_string(),
});
Ok(())
}
OnFail::Error => Err(ResolveError::DevEnginesFailHard {
pm: decl.pm,
reason: DevEnginesFailReason::VersionMismatch {
declared: declared.to_string(),
actual: actual.to_string(),
},
}),
}
}
const fn no_pm_found_soft() -> ResolveError {
ResolveError::NoSignalsFound {
ecosystem: Ecosystem::Node,
soft: true,
}
}
const fn no_pm_found_hard() -> ResolveError {
ResolveError::NoSignalsFound {
ecosystem: Ecosystem::Node,
soft: false,
}
}
fn cross_check_against_lockfile(
decl: &ManifestPmDecl,
ctx: &ProjectContext,
policy: MismatchPolicy,
warnings: &mut Vec<DetectionWarning>,
) -> Result<(), ResolveError> {
let Some(lockfile_pm) = ctx.primary_node_pm() else {
return Ok(());
};
if lockfile_pm == decl.pm {
return Ok(());
}
let field = match decl.source {
ManifestSource::PackageManager => "packageManager",
ManifestSource::DevEngines => "devEngines.packageManager",
};
match policy {
MismatchPolicy::Ignore => Ok(()),
MismatchPolicy::Warn => {
warnings.push(DetectionWarning::PmMismatch {
declared: decl.pm,
field,
lockfile: lockfile_pm,
});
Ok(())
}
MismatchPolicy::Error => Err(ResolveError::MismatchPolicyError {
declared: decl.pm,
field,
lockfile: lockfile_pm,
}),
}
}