use std::path::Path;
use oxc_ast::ast::{Expression, ObjectExpression};
use super::{PackageSelector, RunLocation};
use crate::plugins::config_parser;
use crate::plugins::vite_plus::VITE_CONFIG_FILES;
const VP: &str = "vp";
pub(super) const VPR: &str = "vpr";
const VPX: &str = "vpx";
const SUBCOMMAND_TOOLS: &[(&str, &str)] = &[
("build", "vite"),
("check", "oxlint"),
("dev", "vite"),
("fmt", "oxfmt"),
("format", "oxfmt"),
("lint", "oxlint"),
("pack", "tsdown"),
("preview", "vite"),
("test", "vitest"),
];
const RUN_BOOLEAN_FLAGS: &[&str] = &[
"-t",
"--transitive",
"--parallel",
"--cache",
"--no-cache",
"-v",
"--verbose",
"--last-details",
];
const RUN_VALUE_FLAGS: &[&str] = &["--concurrency-limit"];
const EXEC_VALUE_FLAGS: &[&str] = &["-p", "--package"];
const EXEC_SELECTION_FLAGS: &[&str] = &["-r", "--recursive", "-F", "--filter"];
pub(super) fn subcommand_tool(
tokens: &[&str],
idx: usize,
) -> Option<(Option<&'static str>, usize)> {
if super::tool_name(tokens.get(idx)?) != VP {
return None;
}
let sub_idx = idx + 1;
let tool = tokens.get(sub_idx).and_then(|sub| {
SUBCOMMAND_TOOLS
.iter()
.find_map(|(name, tool)| (name == sub).then_some(*tool))
});
Some((tool, sub_idx))
}
#[must_use]
pub fn scan_command<'a>(tokens: &[&'a str], idx: usize) -> Option<(&'a str, usize)> {
let unchanged = Some((*tokens.get(idx)?, idx));
if matches!(tokens.get(idx + 1), Some(&("exec" | "dlx"))) {
return unchanged;
}
match subcommand_tool(tokens, idx) {
Some((Some(tool), sub_idx)) => Some((tool, sub_idx)),
Some((None, _)) => None,
None => unchanged,
}
}
pub(super) fn skip_run_flags(tokens: &[&str], mut idx: usize, location: &mut RunLocation) -> usize {
while let Some(&token) = tokens.get(idx) {
match token {
"-r" | "--recursive" => location.select_all_packages(),
"-w" | "--workspace-root" => location.select_package(PackageSelector::root()),
"-F" | "--filter" => {
select_filter(location, tokens.get(idx + 1).copied());
idx += 1;
}
_ if RUN_VALUE_FLAGS.contains(&token) => idx += 1,
_ if RUN_BOOLEAN_FLAGS.contains(&token) => {}
_ => {
if let Some(("--filter", value)) = token.split_once('=') {
select_filter(location, Some(value));
} else if !token.starts_with("--concurrency-limit=") {
break;
}
}
}
idx += 1;
}
idx.min(tokens.len())
}
fn select_filter(location: &mut RunLocation, value: Option<&str>) {
location.select_package(PackageSelector::pnpm_filter(value.unwrap_or_default()));
}
pub(super) fn exec_child_index(tokens: &[&str], idx: usize) -> Option<usize> {
let mut next = match (*tokens.get(idx)?, tokens.get(idx + 1).copied()) {
(VPX, _) => idx + 1,
(VP, Some("exec" | "dlx")) => idx + 2,
_ => return None,
};
while let Some(&token) = tokens.get(next) {
if token == "--" {
next += 1;
break;
}
if !token.starts_with('-') {
break;
}
let flag = token.split_once('=').map_or(token, |(flag, _)| flag);
if EXEC_SELECTION_FLAGS.contains(&flag) {
return None;
}
next += if EXEC_VALUE_FLAGS.contains(&token) {
2
} else {
1
};
}
(next < tokens.len()).then_some(next)
}
fn read_vite_config(dir: &Path) -> Option<(std::path::PathBuf, String)> {
VITE_CONFIG_FILES.iter().find_map(|name| {
let path = dir.join(name);
let source = std::fs::read_to_string(&path).ok()?;
Some((path, source))
})
}
#[must_use]
pub fn staged_commands(dir: &Path) -> Vec<String> {
let Some((path, source)) = read_vite_config(dir) else {
return Vec::new();
};
config_parser::extract_from_source(&source, &path, |program| {
let config = config_parser::find_config_object(program)?;
let staged = config_parser::property_object(config, "staged")?;
Some(
staged
.properties
.iter()
.filter_map(|property| property.as_property())
.flat_map(|property| command_values(&property.value))
.collect(),
)
})
.unwrap_or_default()
}
#[must_use]
pub fn task_commands(dir: &Path) -> Vec<(String, String)> {
let Some((path, source)) = read_vite_config(dir) else {
return Vec::new();
};
config_parser::extract_from_source(&source, &path, |program| {
let config = config_parser::find_config_object(program)?;
let tasks = config_parser::property_object(config, "run")
.and_then(|run| config_parser::property_object(run, "tasks"))?;
Some(
tasks
.properties
.iter()
.filter_map(|property| property.as_property())
.filter_map(|property| {
let name = property.key.static_name()?.to_string();
let command = task_command(&property.value)?;
Some((name, command))
})
.collect(),
)
})
.unwrap_or_default()
}
fn task_command(value: &Expression<'_>) -> Option<String> {
let command = match config_parser::object_expression(value) {
Some(task) => task_object_command(task)?,
None => command_values(value),
};
(!command.is_empty()).then(|| command.join(" && "))
}
fn task_object_command(task: &ObjectExpression<'_>) -> Option<Vec<String>> {
if config_parser::find_property(task, "cwd").is_some() {
return None;
}
config_parser::property_expr(task, "command").map(command_values)
}
fn command_values(value: &Expression<'_>) -> Vec<String> {
match config_parser::array_expression(value) {
Some(array) => array
.elements
.iter()
.filter_map(|element| element.as_expression())
.filter_map(config_parser::expression_to_string)
.collect(),
None => config_parser::expression_to_string(value)
.into_iter()
.collect(),
}
}
#[cfg(test)]
#[path = "vite_plus_tests.rs"]
mod tests;