use std::path::{Path, PathBuf};
use crate::resolver::ResolutionOverrides;
use crate::tool;
use crate::types::{PackageManager, ProjectContext, TaskSource};
pub(crate) fn select_task_entry<'a>(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
found: &[&'a crate::types::Task],
) -> &'a crate::types::Task {
if overrides.pm.is_none()
&& overrides.runner.is_none()
&& let Some(name) = found.first().map(|t| t.name.as_str())
&& let Some(pinned) = overrides.task_source_overrides.get(name)
{
for source in pinned {
if let Some(task) = found
.iter()
.copied()
.filter(|t| t.source == *source)
.min_by_key(|t| t.alias_of.is_some())
{
return task;
}
}
}
found
.iter()
.min_by_key(|task| {
(
source_priority(overrides, task.source),
source_depth(ctx, task.source),
task.source.display_order(),
task.alias_of.is_some(),
)
})
.copied()
.expect("task selection should have at least one match")
}
pub(crate) fn source_priority(overrides: &ResolutionOverrides, source: TaskSource) -> u16 {
if let Some(forced) = forced_pm(overrides) {
let owned = forced.owned_task_sources();
if let Some(idx) = owned
.iter()
.position(|owned_source| *owned_source == source)
{
return u16::try_from(idx).unwrap_or(u16::MAX);
}
let bump = u16::try_from(owned.len()).unwrap_or(u16::MAX);
return base_source_priority(overrides, source).saturating_add(bump);
}
base_source_priority(overrides, source)
}
fn base_source_priority(overrides: &ResolutionOverrides, source: TaskSource) -> u16 {
let default_tier: u16 = match source {
TaskSource::TurboJson => 0,
TaskSource::PackageJson => 1,
_ => 2,
};
if !overrides.prefer_sources.is_empty() {
if let Some(idx) = overrides.prefer_sources.iter().position(|s| *s == source) {
return u16::try_from(idx).unwrap_or(u16::MAX);
}
return u16::try_from(overrides.prefer_sources.len()).unwrap_or(u16::MAX) + default_tier;
}
if overrides.prefer_runners.is_empty() {
return default_tier;
}
if let Some(idx) = overrides
.prefer_runners
.iter()
.position(|r| r.task_source() == Some(source))
{
return u16::try_from(idx).unwrap_or(u16::MAX);
}
u16::try_from(overrides.prefer_runners.len()).unwrap_or(u16::MAX) + default_tier
}
fn forced_pm(overrides: &ResolutionOverrides) -> Option<PackageManager> {
overrides.pm.as_ref().map(|forced| forced.pm)
}
pub(crate) fn source_depth(ctx: &ProjectContext, source: TaskSource) -> usize {
source_dir(source, &ctx.root)
.and_then(|dir| {
ctx.root
.ancestors()
.position(|ancestor| ancestor == dir.as_path())
.or_else(|| {
dir.starts_with(&ctx.root).then_some(0)
})
})
.unwrap_or(usize::MAX)
}
fn source_dir(source: TaskSource, root: &Path) -> Option<PathBuf> {
let path = match source {
TaskSource::PackageJson => tool::node::find_manifest_upwards(root),
TaskSource::DenoJson => tool::deno::find_config_upwards(root),
TaskSource::CargoAliases => tool::cargo_aliases::find_anchor(root),
TaskSource::GoPackage => tool::go_pm::find_file(root),
TaskSource::TurboJson => tool::files::find_first_upwards(root, tool::turbo::FILENAMES),
TaskSource::Makefile => tool::files::find_first_upwards(root, tool::make::FILENAMES),
TaskSource::Justfile => tool::just::find_file(root),
TaskSource::Taskfile => tool::files::find_first_upwards(root, tool::go_task::FILENAMES),
TaskSource::BaconToml => tool::files::find_first_upwards(root, tool::bacon::FILENAMES),
TaskSource::MiseToml => tool::files::find_first_upwards(root, tool::mise::FILENAMES),
TaskSource::PyprojectScripts => tool::files::find_first_upwards(root, &["pyproject.toml"]),
};
path.and_then(|path| path.parent().map(Path::to_path_buf))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::path::PathBuf;
use super::{select_task_entry, source_priority};
use crate::resolver::{OverrideOrigin, PmOverride, ResolutionOverrides};
use crate::types::{PackageManager, ProjectContext, Task, TaskSource};
fn context(tasks: Vec<Task>) -> ProjectContext {
ProjectContext {
root: PathBuf::from("."),
package_managers: Vec::new(),
task_runners: Vec::new(),
tasks,
node_version: None,
current_node: None,
is_monorepo: false,
warnings: Vec::new(),
}
}
fn task(name: &str, source: TaskSource) -> Task {
Task {
name: name.to_string(),
source,
run_target: None,
description: None,
alias_of: None,
passthrough_to: None,
}
}
fn pm_override(pm: PackageManager, origin: OverrideOrigin) -> ResolutionOverrides {
ResolutionOverrides {
pm: Some(PmOverride { pm, origin }),
..ResolutionOverrides::default()
}
}
#[test]
fn forced_deno_pm_wins_name_conflict_over_package_json() {
let ctx = context(vec![
task("check", TaskSource::PackageJson),
task("check", TaskSource::DenoJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
for origin in [OverrideOrigin::CliFlag, OverrideOrigin::EnvVar] {
let overrides = pm_override(PackageManager::Deno, origin);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::DenoJson);
}
}
#[test]
fn no_override_keeps_package_json_winning_the_conflict() {
let ctx = context(vec![
task("check", TaskSource::PackageJson),
task("check", TaskSource::DenoJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let entry = select_task_entry(&ctx, &ResolutionOverrides::default(), &found);
assert_eq!(entry.source, TaskSource::PackageJson);
}
#[test]
fn forced_deno_beats_turbo_in_conflict() {
let ctx = context(vec![
task("check", TaskSource::TurboJson),
task("check", TaskSource::DenoJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = pm_override(PackageManager::Deno, OverrideOrigin::EnvVar);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::DenoJson);
}
#[test]
fn forced_bun_biases_toward_its_own_package_json() {
let ctx = context(vec![
task("check", TaskSource::PackageJson),
task("check", TaskSource::DenoJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = pm_override(PackageManager::Bun, OverrideOrigin::CliFlag);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::PackageJson);
}
#[test]
fn forced_bun_beats_turbo_in_conflict() {
let ctx = context(vec![
task("check", TaskSource::TurboJson),
task("check", TaskSource::PackageJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = pm_override(PackageManager::Bun, OverrideOrigin::CliFlag);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::PackageJson);
}
#[test]
fn forced_deno_falls_back_to_package_json_when_no_deno_task() {
let ctx = context(vec![task("check", TaskSource::PackageJson)]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = pm_override(PackageManager::Deno, OverrideOrigin::EnvVar);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::PackageJson);
}
#[test]
fn forced_pm_without_owned_source_does_not_reorder() {
let ctx = context(vec![
task("check", TaskSource::PackageJson),
task("check", TaskSource::DenoJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = pm_override(PackageManager::Composer, OverrideOrigin::CliFlag);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::PackageJson);
assert_eq!(
source_priority(&overrides, TaskSource::PackageJson),
source_priority(&ResolutionOverrides::default(), TaskSource::PackageJson),
);
}
#[test]
fn forced_deno_priority_promotes_deno_source_to_zero() {
let overrides = pm_override(PackageManager::Deno, OverrideOrigin::CliFlag);
assert_eq!(source_priority(&overrides, TaskSource::DenoJson), 0);
assert!(
source_priority(&overrides, TaskSource::PackageJson)
> source_priority(&overrides, TaskSource::DenoJson)
);
}
#[test]
fn forced_bun_priority_promotes_package_json_to_zero() {
let overrides = pm_override(PackageManager::Bun, OverrideOrigin::CliFlag);
assert_eq!(source_priority(&overrides, TaskSource::PackageJson), 0);
assert!(
source_priority(&overrides, TaskSource::DenoJson)
> source_priority(&overrides, TaskSource::PackageJson)
);
}
#[test]
fn unforced_priority_matches_default_tier() {
let overrides = ResolutionOverrides::default();
assert_eq!(source_priority(&overrides, TaskSource::TurboJson), 0);
assert_eq!(source_priority(&overrides, TaskSource::PackageJson), 1);
assert_eq!(source_priority(&overrides, TaskSource::DenoJson), 2);
}
fn tasks_prefer(sources: Vec<TaskSource>) -> ResolutionOverrides {
ResolutionOverrides {
prefer_sources: sources,
..ResolutionOverrides::default()
}
}
#[test]
fn tasks_prefer_flips_package_json_ahead_of_turbo() {
let ctx = context(vec![
task("build", TaskSource::TurboJson),
task("build", TaskSource::PackageJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = tasks_prefer(vec![TaskSource::PackageJson, TaskSource::TurboJson]);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::PackageJson);
}
#[test]
fn tasks_prefer_turbo_first_keeps_turbo() {
let ctx = context(vec![
task("build", TaskSource::TurboJson),
task("build", TaskSource::PackageJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = tasks_prefer(vec![TaskSource::TurboJson, TaskSource::PackageJson]);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::TurboJson);
}
#[test]
fn tasks_prefer_is_rank_only_unlisted_sources_still_win_when_alone() {
let ctx = context(vec![task("build", TaskSource::Makefile)]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = tasks_prefer(vec![TaskSource::TurboJson]);
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::Makefile);
}
#[test]
fn tasks_override_pins_a_specific_name() {
let ctx = context(vec![
task("build", TaskSource::TurboJson),
task("build", TaskSource::PackageJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = ResolutionOverrides {
task_source_overrides: BTreeMap::from([(
"build".to_string(),
vec![TaskSource::PackageJson],
)]),
..ResolutionOverrides::default()
};
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::PackageJson);
}
#[test]
fn tasks_override_only_affects_the_named_task() {
let ctx = context(vec![
task("build", TaskSource::TurboJson),
task("build", TaskSource::PackageJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = ResolutionOverrides {
task_source_overrides: BTreeMap::from([(
"dev".to_string(),
vec![TaskSource::PackageJson],
)]),
..ResolutionOverrides::default()
};
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::TurboJson);
}
#[test]
fn cli_pm_override_outranks_a_per_task_pin() {
let ctx = context(vec![
task("build", TaskSource::TurboJson),
task("build", TaskSource::PackageJson),
]);
let found: Vec<_> = ctx.tasks.iter().collect();
let overrides = ResolutionOverrides {
pm: Some(PmOverride {
pm: PackageManager::Bun,
origin: OverrideOrigin::CliFlag,
}),
task_source_overrides: BTreeMap::from([(
"build".to_string(),
vec![TaskSource::TurboJson],
)]),
..ResolutionOverrides::default()
};
let entry = select_task_entry(&ctx, &overrides, &found);
assert_eq!(entry.source, TaskSource::PackageJson);
}
}