0
0
Fork 0
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:
Marvin Hagemeister 2025-01-30 14:37:10 +01:00 committed by GitHub
parent 5c2fc88ba6
commit c44b05cab5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 additions and 64 deletions

View file

@ -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(&regex_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(&regex_str)
}
fn arg_to_task_name_filter(input: &str) -> Result<TaskNameFilter, AnyError> {
if !input.contains("*") {
return Ok(TaskNameFilter::Exact(input));

View file

@ -0,0 +1,12 @@
{
"tests": {
"wildcard": {
"args": "task foo-*",
"output": "wildcard.out"
},
"wildcard_deps": {
"args": "task dep-*",
"output": "wildcard_deps.out"
}
}
}

View 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"]
}
}
}

View 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

View 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