mirror of
https://github.com/denoland/deno.git
synced 2025-02-07 23:06:50 -05:00
feat(task): add support for task wildcards (#27007)
This PR adds support for passing wildcard tasks. All matched tasks are sorted in case they have dependencies. Tasks already in the dependency tree will be pruned so that every task only runs once. ```json { "tasks": { "foo-1": "echo 'foo-1'", "foo-2": "echo 'foo-2'" } } ``` ```sh $ deno task "foo-*" Task foo-1 echo 'foo-1' foo-1 Task foo-2 echo 'foo-2' foo-2 ``` The changes in the PR look a little bigger than they really are due to formatting. For the most part, I've only needed to hoist up the task matching logic. Closes https://github.com/denoland/deno/issues/26944 Closes https://github.com/denoland/deno/issues/21530 --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
parent
5c2fc88ba6
commit
c44b05cab5
5 changed files with 118 additions and 64 deletions
|
@ -28,6 +28,7 @@ use deno_path_util::normalize_path;
|
|||
use deno_task_shell::KillSignal;
|
||||
use deno_task_shell::ShellCommand;
|
||||
use indexmap::IndexMap;
|
||||
use indexmap::IndexSet;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::args::CliOptions;
|
||||
|
@ -70,19 +71,10 @@ pub async fn execute_script(
|
|||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
fn arg_to_regex(input: &str) -> Result<regex::Regex, regex::Error> {
|
||||
let mut regex_str = regex::escape(input);
|
||||
regex_str = regex_str.replace("\\*", ".*");
|
||||
|
||||
Regex::new(®ex_str)
|
||||
}
|
||||
|
||||
let packages_task_configs: Vec<PackageTaskInfo> = if let Some(filter) =
|
||||
&task_flags.filter
|
||||
{
|
||||
// TODO(bartlomieju): this whole huge if statement should be a separate function, preferably with unit tests
|
||||
let (packages_task_configs, name) = if let Some(filter) = &task_flags.filter {
|
||||
// Filter based on package name
|
||||
let package_regex = arg_to_regex(filter)?;
|
||||
let workspace = cli_options.workspace();
|
||||
let package_regex = package_filter_to_regex(filter)?;
|
||||
|
||||
let Some(task_name) = &task_flags.task else {
|
||||
print_available_tasks_workspace(
|
||||
|
@ -95,10 +87,11 @@ pub async fn execute_script(
|
|||
|
||||
return Ok(0);
|
||||
};
|
||||
let task_regex = arg_to_task_name_filter(task_name)?;
|
||||
|
||||
let task_name_filter = arg_to_task_name_filter(task_name)?;
|
||||
let mut packages_task_info: Vec<PackageTaskInfo> = vec![];
|
||||
|
||||
let workspace = cli_options.workspace();
|
||||
for folder in workspace.config_folders() {
|
||||
if !task_flags.recursive
|
||||
&& !matches_package(folder.1, force_use_pkg_json, &package_regex)
|
||||
|
@ -112,52 +105,14 @@ pub async fn execute_script(
|
|||
tasks_config = tasks_config.with_only_pkg_json();
|
||||
}
|
||||
|
||||
// Any of the matched tasks could be a child task of another matched
|
||||
// one. Therefore we need to filter these out to ensure that every
|
||||
// task is only run once.
|
||||
let mut matched: HashSet<String> = HashSet::new();
|
||||
let mut visited: HashSet<String> = HashSet::new();
|
||||
let matched_tasks = match_tasks(&tasks_config, &task_regex);
|
||||
|
||||
fn visit_task(
|
||||
tasks_config: &WorkspaceTasksConfig,
|
||||
visited: &mut HashSet<String>,
|
||||
name: &str,
|
||||
) {
|
||||
if visited.contains(name) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.insert(name.to_string());
|
||||
|
||||
if let Some((_, TaskOrScript::Task(_, task))) = &tasks_config.task(name)
|
||||
{
|
||||
for dep in &task.dependencies {
|
||||
visit_task(tasks_config, visited, dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match tasks in deno.json
|
||||
for name in tasks_config.task_names() {
|
||||
let matches_filter = match &task_name_filter {
|
||||
TaskNameFilter::Exact(n) => *n == name,
|
||||
TaskNameFilter::Regex(re) => re.is_match(name),
|
||||
};
|
||||
if matches_filter && !visited.contains(name) {
|
||||
matched.insert(name.to_string());
|
||||
visit_task(&tasks_config, &mut visited, name);
|
||||
}
|
||||
}
|
||||
|
||||
if matched.is_empty() {
|
||||
if matched_tasks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
packages_task_info.push(PackageTaskInfo {
|
||||
matched_tasks: matched
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
matched_tasks,
|
||||
tasks_config,
|
||||
});
|
||||
}
|
||||
|
@ -178,10 +133,7 @@ pub async fn execute_script(
|
|||
return Ok(0);
|
||||
}
|
||||
|
||||
// FIXME: Sort packages topologically
|
||||
//
|
||||
|
||||
packages_task_info
|
||||
(packages_task_info, task_name)
|
||||
} else {
|
||||
let mut tasks_config = start_dir.to_tasks_config()?;
|
||||
|
||||
|
@ -199,10 +151,16 @@ pub async fn execute_script(
|
|||
return Ok(0);
|
||||
};
|
||||
|
||||
vec![PackageTaskInfo {
|
||||
tasks_config,
|
||||
matched_tasks: vec![task_name.to_string()],
|
||||
}]
|
||||
let task_regex = arg_to_task_name_filter(task_name)?;
|
||||
let matched_tasks = match_tasks(&tasks_config, &task_regex);
|
||||
|
||||
(
|
||||
vec![PackageTaskInfo {
|
||||
tasks_config,
|
||||
matched_tasks,
|
||||
}],
|
||||
task_name,
|
||||
)
|
||||
};
|
||||
|
||||
let npm_installer = factory.npm_installer_if_managed()?;
|
||||
|
@ -247,7 +205,7 @@ pub async fn execute_script(
|
|||
|
||||
for task_config in &packages_task_configs {
|
||||
let exit_code = task_runner
|
||||
.run_tasks(task_config, &kill_signal, cli_options.argv())
|
||||
.run_tasks(task_config, name, &kill_signal, cli_options.argv())
|
||||
.await?;
|
||||
if exit_code > 0 {
|
||||
return Ok(exit_code);
|
||||
|
@ -282,10 +240,11 @@ impl<'a> TaskRunner<'a> {
|
|||
pub async fn run_tasks(
|
||||
&self,
|
||||
pkg_tasks_config: &PackageTaskInfo,
|
||||
task_name: &str,
|
||||
kill_signal: &KillSignal,
|
||||
argv: &[String],
|
||||
) -> Result<i32, deno_core::anyhow::Error> {
|
||||
match sort_tasks_topo(pkg_tasks_config) {
|
||||
match sort_tasks_topo(pkg_tasks_config, task_name) {
|
||||
Ok(sorted) => self.run_tasks_in_parallel(sorted, kill_signal, argv).await,
|
||||
Err(err) => match err {
|
||||
TaskError::NotFound(name) => {
|
||||
|
@ -601,6 +560,7 @@ struct ResolvedTask<'a> {
|
|||
|
||||
fn sort_tasks_topo<'a>(
|
||||
pkg_task_config: &'a PackageTaskInfo,
|
||||
task_name: &str,
|
||||
) -> Result<Vec<ResolvedTask<'a>>, TaskError> {
|
||||
trait TasksConfig {
|
||||
fn task(
|
||||
|
@ -695,6 +655,10 @@ fn sort_tasks_topo<'a>(
|
|||
sort_visit(name, &mut sorted, Vec::new(), &pkg_task_config.tasks_config)?;
|
||||
}
|
||||
|
||||
if sorted.is_empty() {
|
||||
return Err(TaskError::NotFound(task_name.to_string()));
|
||||
}
|
||||
|
||||
Ok(sorted)
|
||||
}
|
||||
|
||||
|
@ -928,6 +892,57 @@ fn strip_ansi_codes_and_escape_control_chars(s: &str) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn visit_task_and_dependencies(
|
||||
tasks_config: &WorkspaceTasksConfig,
|
||||
visited: &mut HashSet<String>,
|
||||
name: &str,
|
||||
) {
|
||||
if visited.contains(name) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.insert(name.to_string());
|
||||
|
||||
if let Some((_, TaskOrScript::Task(_, task))) = &tasks_config.task(name) {
|
||||
for dep in &task.dependencies {
|
||||
visit_task_and_dependencies(tasks_config, visited, dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any of the matched tasks could be a child task of another matched
|
||||
// one. Therefore we need to filter these out to ensure that every
|
||||
// task is only run once.
|
||||
fn match_tasks(
|
||||
tasks_config: &WorkspaceTasksConfig,
|
||||
task_name_filter: &TaskNameFilter,
|
||||
) -> Vec<String> {
|
||||
let mut matched: IndexSet<String> = IndexSet::new();
|
||||
let mut visited: HashSet<String> = HashSet::new();
|
||||
|
||||
// Match tasks in deno.json
|
||||
for name in tasks_config.task_names() {
|
||||
let matches_filter = match &task_name_filter {
|
||||
TaskNameFilter::Exact(n) => *n == name,
|
||||
TaskNameFilter::Regex(re) => re.is_match(name),
|
||||
};
|
||||
|
||||
if matches_filter && !visited.contains(name) {
|
||||
matched.insert(name.to_string());
|
||||
visit_task_and_dependencies(tasks_config, &mut visited, name);
|
||||
}
|
||||
}
|
||||
|
||||
matched.iter().map(|s| s.to_string()).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn package_filter_to_regex(input: &str) -> Result<regex::Regex, regex::Error> {
|
||||
let mut regex_str = regex::escape(input);
|
||||
regex_str = regex_str.replace("\\*", ".*");
|
||||
|
||||
Regex::new(®ex_str)
|
||||
}
|
||||
|
||||
fn arg_to_task_name_filter(input: &str) -> Result<TaskNameFilter, AnyError> {
|
||||
if !input.contains("*") {
|
||||
return Ok(TaskNameFilter::Exact(input));
|
||||
|
|
12
tests/specs/task/wildcard/__test__.jsonc
Normal file
12
tests/specs/task/wildcard/__test__.jsonc
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"tests": {
|
||||
"wildcard": {
|
||||
"args": "task foo-*",
|
||||
"output": "wildcard.out"
|
||||
},
|
||||
"wildcard_deps": {
|
||||
"args": "task dep-*",
|
||||
"output": "wildcard_deps.out"
|
||||
}
|
||||
}
|
||||
}
|
15
tests/specs/task/wildcard/deno.json
Normal file
15
tests/specs/task/wildcard/deno.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"tasks": {
|
||||
"foo-1": "echo 'foo-1'",
|
||||
"foo-2": "echo 'foo-2'",
|
||||
"foo-3": "echo 'foo-3'",
|
||||
"dep-1": {
|
||||
"command": "echo 'dep-1'",
|
||||
"dependencies": ["dep-2", "foo-1"]
|
||||
},
|
||||
"dep-2": {
|
||||
"command": "echo 'dep-2'",
|
||||
"dependencies": ["foo-1"]
|
||||
}
|
||||
}
|
||||
}
|
6
tests/specs/task/wildcard/wildcard.out
Normal file
6
tests/specs/task/wildcard/wildcard.out
Normal file
|
@ -0,0 +1,6 @@
|
|||
Task foo-1 echo 'foo-1'
|
||||
foo-1
|
||||
Task foo-2 echo 'foo-2'
|
||||
foo-2
|
||||
Task foo-3 echo 'foo-3'
|
||||
foo-3
|
6
tests/specs/task/wildcard/wildcard_deps.out
Normal file
6
tests/specs/task/wildcard/wildcard_deps.out
Normal file
|
@ -0,0 +1,6 @@
|
|||
Task foo-1 echo 'foo-1'
|
||||
foo-1
|
||||
Task dep-2 echo 'dep-2'
|
||||
dep-2
|
||||
Task dep-1 echo 'dep-1'
|
||||
dep-1
|
Loading…
Add table
Reference in a new issue