Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Inspired by [antfu/ni](https://github.com/antfu/ni).
- make: `Makefile`
- just: `justfile` / `Justfile`
- task: `Taskfile.yml` / `Taskfile.yaml` ...
- vite+: `vite.config.ts` / `vite.config.js` ...
- cargo-make: `Makefile.toml`
- mise: `mise.toml`
- mask: `maskfile.md`
Expand Down
133 changes: 130 additions & 3 deletions src/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};

use crate::RtError;

const RUNNER_CANDIDATES: [(&str, Runner); 15] = [
const RUNNER_CANDIDATES: [(&str, Runner); 22] = [
("Justfile", Runner::Justfile),
("justfile", Runner::Justfile),
("Taskfile.yml", Runner::Taskfile),
Expand All @@ -15,6 +15,13 @@ const RUNNER_CANDIDATES: [(&str, Runner); 15] = [
("taskfile.dist.yaml", Runner::Taskfile),
("maskfile.md", Runner::Maskfile),
("Maskfile.md", Runner::Maskfile),
("vite.config.ts", Runner::VitePlus),
("vite.config.mts", Runner::VitePlus),
("vite.config.cts", Runner::VitePlus),
("vite.config.js", Runner::VitePlus),
("vite.config.mjs", Runner::VitePlus),
("vite.config.cjs", Runner::VitePlus),
("package.json", Runner::VitePlus),
("mise.toml", Runner::Mise),
("Makefile.toml", Runner::CargoMake),
("Makefile", Runner::Makefile),
Expand All @@ -25,6 +32,7 @@ pub enum Runner {
Justfile,
Taskfile,
Maskfile,
VitePlus,
Mise,
CargoMake,
Makefile,
Expand All @@ -40,7 +48,7 @@ pub struct Detection {
pub fn detect_runner(dir_path: &Path) -> Result<Detection, RtError> {
for (name, runner) in RUNNER_CANDIDATES {
let path = dir_path.join(name);
if path.is_file() {
if candidate_matches(runner, &path) {
return Ok(Detection {
runner,
runner_file: path,
Expand All @@ -63,7 +71,7 @@ pub fn detect_runners(dir_path: &Path) -> Result<Vec<Detection>, RtError> {
continue;
}
let path = dir_path.join(name);
if path.is_file() {
if candidate_matches(runner, &path) {
seen.insert(runner);
detections.push(Detection {
runner,
Expand All @@ -81,12 +89,68 @@ pub fn detect_runners(dir_path: &Path) -> Result<Vec<Detection>, RtError> {
}
}

fn candidate_matches(runner: Runner, path: &Path) -> bool {
if !path.is_file() {
return false;
}

match runner {
Runner::VitePlus => vite_plus_marker_matches(path),
_ => true,
}
}

fn vite_plus_marker_matches(path: &Path) -> bool {
match path.file_name().and_then(|name| name.to_str()) {
Some("package.json") => package_json_declares_vite_plus(path),
Some(name) if name.starts_with("vite.config.") => vite_config_declares_vite_plus(path),
_ => true,
}
}

fn vite_config_declares_vite_plus(path: &Path) -> bool {
std::fs::read_to_string(path)
.map(|content| content.contains("vite-plus"))
.unwrap_or(false)
}

fn package_json_declares_vite_plus(path: &Path) -> bool {
#[derive(serde::Deserialize)]
struct PackageJson {
#[serde(default)]
dependencies: std::collections::BTreeMap<String, serde_json::Value>,
#[serde(default, rename = "devDependencies")]
dev_dependencies: std::collections::BTreeMap<String, serde_json::Value>,
#[serde(default, rename = "peerDependencies")]
peer_dependencies: std::collections::BTreeMap<String, serde_json::Value>,
#[serde(default, rename = "optionalDependencies")]
optional_dependencies: std::collections::BTreeMap<String, serde_json::Value>,
}

let Ok(content) = std::fs::read_to_string(path) else {
return false;
};
let Ok(package_json) = serde_json::from_str::<PackageJson>(&content) else {
return false;
};

[
&package_json.dependencies,
&package_json.dev_dependencies,
&package_json.peer_dependencies,
&package_json.optional_dependencies,
]
.into_iter()
.any(|deps| deps.contains_key("vite-plus"))
}

/// Returns the command name for the given runner.
pub fn runner_command(runner: Runner) -> &'static str {
match runner {
Runner::Justfile => "just",
Runner::Taskfile => "task",
Runner::Maskfile => "mask",
Runner::VitePlus => "vp",
Runner::Mise => "mise",
// cargo-make is a subcommand of cargo, so we need to check cargo
Runner::CargoMake => "cargo",
Expand All @@ -105,6 +169,12 @@ mod tests {
path
}

fn write(dir: &Path, name: &str, contents: &str) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, contents).unwrap();
path
}

#[test]
fn detect_none_returns_error() {
let dir = tempdir().unwrap();
Expand Down Expand Up @@ -152,11 +222,42 @@ mod tests {
assert_eq!(detection.runner_file, yml);
}

#[test]
fn detect_ignores_plain_vite_config_without_vite_plus_marker() {
let dir = tempdir().unwrap();
write(
dir.path(),
"vite.config.ts",
"import { defineConfig } from 'vite'; export default defineConfig({});",
);

let err = detect_runner(dir.path()).unwrap_err();
match err {
RtError::NoRunnerFound { .. } => {}
other => panic!("unexpected error: {other:?}"),
}
}

#[test]
fn detect_package_json_with_vite_plus_dependency() {
let dir = tempdir().unwrap();
let package_json = write(
dir.path(),
"package.json",
r#"{"devDependencies":{"vite-plus":"^1.0.0"}}"#,
);

let detection = detect_runner(dir.path()).unwrap();
assert_eq!(detection.runner, Runner::VitePlus);
assert_eq!(detection.runner_file, package_json);
}

#[test]
fn runner_command_mapping() {
assert_eq!(runner_command(Runner::Justfile), "just");
assert_eq!(runner_command(Runner::Taskfile), "task");
assert_eq!(runner_command(Runner::Maskfile), "mask");
assert_eq!(runner_command(Runner::VitePlus), "vp");
assert_eq!(runner_command(Runner::Mise), "mise");
assert_eq!(runner_command(Runner::CargoMake), "cargo");
assert_eq!(runner_command(Runner::Makefile), "make");
Expand All @@ -168,6 +269,11 @@ mod tests {
touch(dir.path(), "Makefile");
touch(dir.path(), "Makefile.toml");
touch(dir.path(), "mise.toml");
write(
dir.path(),
"vite.config.ts",
"import { defineConfig } from 'vite-plus'; export default defineConfig({});",
);
touch(dir.path(), "maskfile.md");
touch(dir.path(), "Taskfile.yml");
touch(dir.path(), "justfile");
Expand All @@ -181,13 +287,34 @@ mod tests {
Runner::Justfile,
Runner::Taskfile,
Runner::Maskfile,
Runner::VitePlus,
Runner::Mise,
Runner::CargoMake,
Runner::Makefile,
]
);
}

#[test]
fn detect_runners_deduplicates_vite_plus_config_variants() {
let dir = tempdir().unwrap();
write(
dir.path(),
"vite.config.ts",
"import { defineConfig } from 'vite-plus'; export default defineConfig({});",
);
write(
dir.path(),
"vite.config.mjs",
"import { defineConfig } from 'vite-plus'; export default defineConfig({});",
);

let detections = detect_runners(dir.path()).unwrap();
let runners: Vec<Runner> = detections.into_iter().map(|d| d.runner).collect();

assert_eq!(runners, vec![Runner::VitePlus]);
}

#[test]
fn detect_runners_deduplicates_case_variants() {
let dir = tempdir().unwrap();
Expand Down
50 changes: 34 additions & 16 deletions src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ pub fn run(
cwd: &Path,
) -> Result<RunResult, RtError> {
let program = runner_command(runner).to_string();
let mut args = Vec::new();
if runner == Runner::CargoMake {
args.push("make".to_string());
}
if runner == Runner::Mise {
args.push("run".to_string());
}
let mut args = runner_prefix_args(runner);
args.push(task.to_string());
args.extend(passthrough.iter().cloned());

Expand Down Expand Up @@ -67,8 +61,8 @@ pub fn base_command(runner: Runner) -> Result<Command, RtError> {
let program = runner_command(runner);
ensure_tool(program)?;
let mut command = Command::new(program);
if runner == Runner::CargoMake {
command.arg("make");
for arg in runner_prefix_args(runner) {
command.arg(arg);
}
Ok(command)
}
Expand All @@ -81,20 +75,22 @@ pub fn ensure_tool(tool: &'static str) -> Result<(), RtError> {
}

pub fn preview_command(runner: Runner, task: &str, passthrough: &[String]) -> String {
let mut parts = Vec::new();
let program = runner_command(runner);
if runner == Runner::CargoMake {
parts.push("make".to_string());
}
if runner == Runner::Mise {
parts.push("run".to_string());
}
let mut parts = runner_prefix_args(runner);
parts.push(task.to_string());
parts.extend(passthrough.iter().cloned());

format_program_args(program, &parts)
}

fn runner_prefix_args(runner: Runner) -> Vec<String> {
match runner {
Runner::CargoMake => vec!["make".to_string()],
Runner::VitePlus | Runner::Mise => vec!["run".to_string()],
_ => Vec::new(),
}
}

pub fn format_program_args(program: &str, args: &[String]) -> String {
let mut parts = Vec::new();
parts.push(program.to_string());
Expand Down Expand Up @@ -137,6 +133,24 @@ mod tests {
assert_eq!(args, vec!["make".to_string()]);
}

#[test]
fn runner_prefix_args_include_vite_plus_run_subcommand() {
assert_eq!(
runner_prefix_args(Runner::VitePlus),
vec!["run".to_string()]
);
}

#[test]
fn runner_prefix_args_include_runner_specific_prefixes() {
assert_eq!(
runner_prefix_args(Runner::CargoMake),
vec!["make".to_string()]
);
assert_eq!(runner_prefix_args(Runner::Mise), vec!["run".to_string()]);
assert!(runner_prefix_args(Runner::Justfile).is_empty());
}

#[test]
fn ensure_tool_returns_error_for_missing_binary() {
let err = ensure_tool("__rt_missing_tool_for_test__").unwrap_err();
Expand Down Expand Up @@ -172,6 +186,10 @@ mod tests {
preview_command(Runner::Mise, "build", &[]),
"mise run build"
);
assert_eq!(
preview_command(Runner::VitePlus, "build", &[]),
"vp run build"
);
assert_eq!(
preview_command(Runner::CargoMake, "build", &[]),
"cargo make build"
Expand Down
2 changes: 2 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ mod makefile;
mod mask;
mod mise;
mod taskfile;
mod vite_plus;

/// Returns parsed tasks from the output of the given runner's list command.
pub fn parse_tasks(runner: Runner, output: &str) -> Vec<TaskItem> {
match runner {
Runner::Justfile => justfile::parse(output),
Runner::Taskfile => taskfile::parse(output),
Runner::Maskfile => mask::parse(output),
Runner::VitePlus => vite_plus::parse(output),
Runner::Mise => mise::parse(output),
Runner::CargoMake => cargo_make::parse(output),
Runner::Makefile => makefile::parse(output),
Expand Down
60 changes: 60 additions & 0 deletions src/parser/vite_plus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::tasks::TaskItem;

pub(super) fn parse(output: &str) -> Vec<TaskItem> {
output
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
return None;
}

let (name, description) = line.split_once(':')?;
let name = name.trim();
if name.is_empty() {
return None;
}

let description = description.trim();
Some(TaskItem {
name: name.to_string(),
description: (!description.is_empty()).then(|| description.to_string()),
})
})
.collect()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_vite_plus_task_list() {
let output = "\
check: echo check root
app#build: echo build app
";
let tasks = parse(output);
assert_eq!(
tasks,
vec![
TaskItem {
name: "check".to_string(),
description: Some("echo check root".to_string()),
},
TaskItem {
name: "app#build".to_string(),
description: Some("echo build app".to_string()),
},
]
);
}

#[test]
fn parse_vite_plus_ignores_blank_lines() {
let output = "\n\n build: echo build\n";
let tasks = parse(output);
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].name, "build");
}
}
Loading
Loading