summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/Cargo.toml1
-rw-r--r--cli/config_file.rs28
-rw-r--r--cli/flags.rs103
-rw-r--r--cli/main.rs11
-rw-r--r--cli/schemas/config-file.v1.json11
-rw-r--r--cli/tests/integration/mod.rs2
-rw-r--r--cli/tests/integration/task_tests.rs71
-rw-r--r--cli/tests/testdata/task/deno.json8
-rw-r--r--cli/tests/testdata/task/task_additional_args.out1
-rw-r--r--cli/tests/testdata/task/task_additional_args_nested_strings.out1
-rw-r--r--cli/tests/testdata/task/task_additional_args_no_logic.out1
-rw-r--r--cli/tests/testdata/task/task_additional_args_no_shell_expansion.out1
-rw-r--r--cli/tests/testdata/task/task_boolean_logic.out4
-rw-r--r--cli/tests/testdata/task/task_exit_code_5.out1
-rw-r--r--cli/tests/testdata/task/task_no_args.out9
-rw-r--r--cli/tests/testdata/task/task_non_existent.out10
-rw-r--r--cli/tools/mod.rs1
-rw-r--r--cli/tools/task.rs165
18 files changed, 429 insertions, 0 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index c42088e8a..a31f18b84 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -51,6 +51,7 @@ deno_doc = "0.32.0"
deno_graph = "0.24.0"
deno_lint = { version = "0.26.0", features = ["docs"] }
deno_runtime = { version = "0.48.0", path = "../runtime" }
+deno_task_shell = "0.1.6"
atty = "=0.2.14"
base64 = "=0.13.0"
diff --git a/cli/config_file.rs b/cli/config_file.rs
index 9d5ae84a0..a2bdbe1d3 100644
--- a/cli/config_file.rs
+++ b/cli/config_file.rs
@@ -530,6 +530,7 @@ pub struct ConfigFileJson {
pub import_map: Option<String>,
pub lint: Option<Value>,
pub fmt: Option<Value>,
+ pub tasks: Option<Value>,
}
#[derive(Clone, Debug)]
@@ -648,6 +649,19 @@ impl ConfigFile {
}
}
+ pub fn to_tasks_config(
+ &self,
+ ) -> Result<Option<BTreeMap<String, String>>, AnyError> {
+ if let Some(config) = self.json.tasks.clone() {
+ let tasks_config: BTreeMap<String, String> =
+ serde_json::from_value(config)
+ .context("Failed to parse \"tasks\" configuration")?;
+ Ok(Some(tasks_config))
+ } else {
+ Ok(None)
+ }
+ }
+
/// If the configuration file contains "extra" modules (like TypeScript
/// `"types"`) options, return them as imports to be added to a module graph.
pub fn to_maybe_imports(&self) -> MaybeImportsResult {
@@ -784,6 +798,10 @@ mod tests {
"singleQuote": true,
"proseWrap": "preserve"
}
+ },
+ "tasks": {
+ "build": "deno run --allow-read --allow-write build.ts",
+ "server": "deno run --allow-net --allow-read server.ts"
}
}"#;
let config_dir = ModuleSpecifier::parse("file:///deno/").unwrap();
@@ -841,6 +859,16 @@ mod tests {
assert_eq!(fmt_config.options.line_width, Some(80));
assert_eq!(fmt_config.options.indent_width, Some(4));
assert_eq!(fmt_config.options.single_quote, Some(true));
+
+ let tasks_config = config_file.to_tasks_config().unwrap().unwrap();
+ assert_eq!(
+ tasks_config["build"],
+ "deno run --allow-read --allow-write build.ts",
+ );
+ assert_eq!(
+ tasks_config["server"],
+ "deno run --allow-net --allow-read server.ts"
+ );
}
#[test]
diff --git a/cli/flags.rs b/cli/flags.rs
index 614a975b1..dfa617462 100644
--- a/cli/flags.rs
+++ b/cli/flags.rs
@@ -140,6 +140,11 @@ pub struct RunFlags {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
+pub struct TaskFlags {
+ pub task: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct TestFlags {
pub ignore: Vec<PathBuf>,
pub doc: bool,
@@ -187,6 +192,7 @@ pub enum DenoSubcommand {
Lint(LintFlags),
Repl(ReplFlags),
Run(RunFlags),
+ Task(TaskFlags),
Test(TestFlags),
Types,
Upgrade(UpgradeFlags),
@@ -500,6 +506,7 @@ pub fn flags_from_vec(args: Vec<String>) -> clap::Result<Flags> {
Some(("compile", m)) => compile_parse(&mut flags, m),
Some(("lsp", m)) => lsp_parse(&mut flags, m),
Some(("vendor", m)) => vendor_parse(&mut flags, m),
+ Some(("task", m)) => task_parse(&mut flags, m),
_ => handle_repl_flags(&mut flags, ReplFlags { eval: None }),
}
@@ -568,6 +575,7 @@ If the flag is set, restrict these messages to errors.",
.subcommand(lint_subcommand())
.subcommand(repl_subcommand())
.subcommand(run_subcommand())
+ .subcommand(task_subcommand())
.subcommand(test_subcommand())
.subcommand(types_subcommand())
.subcommand(upgrade_subcommand())
@@ -1256,6 +1264,25 @@ Deno allows specifying the filename '-' to read the file from stdin.
)
}
+fn task_subcommand<'a>() -> App<'a> {
+ App::new("task")
+ .setting(AppSettings::TrailingVarArg)
+ .arg(config_arg())
+ .arg(Arg::new("task").help("Task to be executed"))
+ .arg(
+ Arg::new("task_args")
+ .multiple_values(true)
+ .multiple_occurrences(true)
+ .help("Additional arguments passed to the task"),
+ )
+ .about("Run a task defined in the configuration file")
+ .long_about(
+ "Run a task defined in the configuration file
+
+ deno task build",
+ )
+}
+
fn test_subcommand<'a>() -> App<'a> {
runtime_args(App::new("test"), true, true)
.setting(AppSettings::TrailingVarArg)
@@ -2197,6 +2224,26 @@ fn run_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
flags.subcommand = DenoSubcommand::Run(RunFlags { script });
}
+fn task_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
+ config_arg_parse(flags, matches);
+
+ let mut task_name = "".to_string();
+ if let Some(task) = matches.value_of("task") {
+ task_name = task.to_string();
+
+ let task_args: Vec<String> = matches
+ .values_of("task_args")
+ .unwrap_or_default()
+ .map(String::from)
+ .collect();
+ for v in task_args {
+ flags.argv.push(v);
+ }
+ }
+
+ flags.subcommand = DenoSubcommand::Task(TaskFlags { task: task_name });
+}
+
fn test_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
runtime_args_parse(flags, matches, true, true);
// NOTE: `deno test` always uses `--no-prompt`, tests shouldn't ever do
@@ -5063,4 +5110,60 @@ mod tests {
}
);
}
+
+ #[test]
+ fn task_subcommand() {
+ let r =
+ flags_from_vec(svec!["deno", "task", "build", "--", "hello", "world",]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Task(TaskFlags {
+ task: "build".to_string(),
+ }),
+ argv: svec!["hello", "world"],
+ ..Flags::default()
+ }
+ );
+
+ let r = flags_from_vec(svec!["deno", "task", "build"]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Task(TaskFlags {
+ task: "build".to_string(),
+ }),
+ ..Flags::default()
+ }
+ );
+ }
+
+ #[test]
+ fn task_subcommand_empty() {
+ let r = flags_from_vec(svec!["deno", "task",]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Task(TaskFlags {
+ task: "".to_string(),
+ }),
+ ..Flags::default()
+ }
+ );
+ }
+
+ #[test]
+ fn task_subcommand_config() {
+ let r = flags_from_vec(svec!["deno", "task", "--config", "deno.jsonc"]);
+ assert_eq!(
+ r.unwrap(),
+ Flags {
+ subcommand: DenoSubcommand::Task(TaskFlags {
+ task: "".to_string(),
+ }),
+ config_path: Some("deno.jsonc".to_string()),
+ ..Flags::default()
+ }
+ );
+ }
}
diff --git a/cli/main.rs b/cli/main.rs
index bb2a0afb3..916070a83 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -56,6 +56,7 @@ use crate::flags::InstallFlags;
use crate::flags::LintFlags;
use crate::flags::ReplFlags;
use crate::flags::RunFlags;
+use crate::flags::TaskFlags;
use crate::flags::TestFlags;
use crate::flags::UninstallFlags;
use crate::flags::UpgradeFlags;
@@ -1228,6 +1229,13 @@ async fn run_command(
Ok(worker.get_exit_code())
}
+async fn task_command(
+ flags: Flags,
+ task_flags: TaskFlags,
+) -> Result<i32, AnyError> {
+ tools::task::execute_script(flags, task_flags).await
+}
+
async fn coverage_command(
flags: Flags,
coverage_flags: CoverageFlags,
@@ -1360,6 +1368,9 @@ fn get_subcommand(
DenoSubcommand::Run(run_flags) => {
run_command(flags, run_flags).boxed_local()
}
+ DenoSubcommand::Task(task_flags) => {
+ task_command(flags, task_flags).boxed_local()
+ }
DenoSubcommand::Test(test_flags) => {
test_command(flags, test_flags).boxed_local()
}
diff --git a/cli/schemas/config-file.v1.json b/cli/schemas/config-file.v1.json
index 55ea417f0..2ab06aead 100644
--- a/cli/schemas/config-file.v1.json
+++ b/cli/schemas/config-file.v1.json
@@ -310,6 +310,17 @@
}
}
}
+ },
+ "tasks": {
+ "description": "Configuration for deno task",
+ "type": "object",
+ "patternProperties": {
+ "^[A-Za-z][A-Za-z0-9_\\-]*$": {
+ "type": "string",
+ "description": "Command to execute for this task name."
+ }
+ },
+ "additionalProperties": false
}
}
}
diff --git a/cli/tests/integration/mod.rs b/cli/tests/integration/mod.rs
index 2a1e69bd1..8d934dc0f 100644
--- a/cli/tests/integration/mod.rs
+++ b/cli/tests/integration/mod.rs
@@ -80,6 +80,8 @@ mod lsp;
mod repl;
#[path = "run_tests.rs"]
mod run;
+#[path = "task_tests.rs"]
+mod task;
#[path = "test_tests.rs"]
mod test;
#[path = "upgrade_tests.rs"]
diff --git a/cli/tests/integration/task_tests.rs b/cli/tests/integration/task_tests.rs
new file mode 100644
index 000000000..5d5887469
--- /dev/null
+++ b/cli/tests/integration/task_tests.rs
@@ -0,0 +1,71 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use crate::itest;
+
+// Most of the tests for this are in deno_task_shell.
+// These tests are intended to only test integration.
+
+itest!(task_no_args {
+ args: "task --config task/deno.json",
+ output: "task/task_no_args.out",
+ envs: vec![("NO_COLOR".to_string(), "1".to_string())],
+ exit_code: 1,
+});
+
+itest!(task_non_existent {
+ args: "task --config task/deno.json non_existent",
+ output: "task/task_non_existent.out",
+ envs: vec![("NO_COLOR".to_string(), "1".to_string())],
+ exit_code: 1,
+});
+
+itest!(task_boolean_logic {
+ args: "task --config task/deno.json boolean_logic",
+ output: "task/task_boolean_logic.out",
+ envs: vec![("NO_COLOR".to_string(), "1".to_string())],
+});
+
+itest!(task_exit_code_1 {
+ args: "task --config task/deno.json exit_code_5",
+ output: "task/task_exit_code_5.out",
+ envs: vec![("NO_COLOR".to_string(), "1".to_string())],
+ exit_code: 5,
+});
+
+itest!(task_additional_args {
+ args: "task --config task/deno.json echo 2",
+ output: "task/task_additional_args.out",
+ envs: vec![("NO_COLOR".to_string(), "1".to_string())],
+});
+
+itest!(task_additional_args_no_shell_expansion {
+ args_vec: vec!["task", "--config", "task/deno.json", "echo", "$(echo 5)"],
+ output: "task/task_additional_args_no_shell_expansion.out",
+ envs: vec![("NO_COLOR".to_string(), "1".to_string())],
+});
+
+itest!(task_additional_args_nested_strings {
+ args_vec: vec![
+ "task",
+ "--config",
+ "task/deno.json",
+ "echo",
+ "string \"quoted string\""
+ ],
+ output: "task/task_additional_args_nested_strings.out",
+ envs: vec![("NO_COLOR".to_string(), "1".to_string())],
+});
+
+itest!(task_additional_args_no_logic {
+ args_vec: vec![
+ "task",
+ "--config",
+ "task/deno.json",
+ "echo",
+ "||",
+ "echo",
+ "5"
+ ],
+ output: "task/task_additional_args_no_logic.out",
+ envs: vec![("NO_COLOR".to_string(), "1".to_string())],
+});
diff --git a/cli/tests/testdata/task/deno.json b/cli/tests/testdata/task/deno.json
new file mode 100644
index 000000000..c26e143e1
--- /dev/null
+++ b/cli/tests/testdata/task/deno.json
@@ -0,0 +1,8 @@
+{
+ "tasks": {
+ "boolean_logic": "sleep 0.1 && echo 3 && echo 4 & echo 1 && echo 2 || echo NOPE",
+ "echo": "echo 1",
+ "strings": "deno run main.ts && deno eval \"console.log(\\\"test\\\")\"",
+ "exit_code_5": "echo $(echo 10 ; exit 2) && exit 5"
+ }
+}
diff --git a/cli/tests/testdata/task/task_additional_args.out b/cli/tests/testdata/task/task_additional_args.out
new file mode 100644
index 000000000..8d04f961a
--- /dev/null
+++ b/cli/tests/testdata/task/task_additional_args.out
@@ -0,0 +1 @@
+1 2
diff --git a/cli/tests/testdata/task/task_additional_args_nested_strings.out b/cli/tests/testdata/task/task_additional_args_nested_strings.out
new file mode 100644
index 000000000..0e5f35c7f
--- /dev/null
+++ b/cli/tests/testdata/task/task_additional_args_nested_strings.out
@@ -0,0 +1 @@
+1 string "quoted string"
diff --git a/cli/tests/testdata/task/task_additional_args_no_logic.out b/cli/tests/testdata/task/task_additional_args_no_logic.out
new file mode 100644
index 000000000..a4886a60d
--- /dev/null
+++ b/cli/tests/testdata/task/task_additional_args_no_logic.out
@@ -0,0 +1 @@
+1 || echo 5
diff --git a/cli/tests/testdata/task/task_additional_args_no_shell_expansion.out b/cli/tests/testdata/task/task_additional_args_no_shell_expansion.out
new file mode 100644
index 000000000..826a3aaf1
--- /dev/null
+++ b/cli/tests/testdata/task/task_additional_args_no_shell_expansion.out
@@ -0,0 +1 @@
+1 $(echo 5)
diff --git a/cli/tests/testdata/task/task_boolean_logic.out b/cli/tests/testdata/task/task_boolean_logic.out
new file mode 100644
index 000000000..94ebaf900
--- /dev/null
+++ b/cli/tests/testdata/task/task_boolean_logic.out
@@ -0,0 +1,4 @@
+1
+2
+3
+4
diff --git a/cli/tests/testdata/task/task_exit_code_5.out b/cli/tests/testdata/task/task_exit_code_5.out
new file mode 100644
index 000000000..f599e28b8
--- /dev/null
+++ b/cli/tests/testdata/task/task_exit_code_5.out
@@ -0,0 +1 @@
+10
diff --git a/cli/tests/testdata/task/task_no_args.out b/cli/tests/testdata/task/task_no_args.out
new file mode 100644
index 000000000..edf028649
--- /dev/null
+++ b/cli/tests/testdata/task/task_no_args.out
@@ -0,0 +1,9 @@
+Available tasks:
+- boolean_logic
+ sleep 0.1 && echo 3 && echo 4 & echo 1 && echo 2 || echo NOPE
+- echo
+ echo 1
+- exit_code_5
+ echo $(echo 10 ; exit 2) && exit 5
+- strings
+ deno run main.ts && deno eval "console.log(\"test\")"
diff --git a/cli/tests/testdata/task/task_non_existent.out b/cli/tests/testdata/task/task_non_existent.out
new file mode 100644
index 000000000..916a85706
--- /dev/null
+++ b/cli/tests/testdata/task/task_non_existent.out
@@ -0,0 +1,10 @@
+Task not found: non_existent
+Available tasks:
+- boolean_logic
+ sleep 0.1 && echo 3 && echo 4 & echo 1 && echo 2 || echo NOPE
+- echo
+ echo 1
+- exit_code_5
+ echo $(echo 10 ; exit 2) && exit 5
+- strings
+ deno run main.ts && deno eval "console.log(\"test\")"
diff --git a/cli/tools/mod.rs b/cli/tools/mod.rs
index ffea76e1d..0c52725b6 100644
--- a/cli/tools/mod.rs
+++ b/cli/tools/mod.rs
@@ -7,6 +7,7 @@ pub mod installer;
pub mod lint;
pub mod repl;
pub mod standalone;
+pub mod task;
pub mod test;
pub mod upgrade;
pub mod vendor;
diff --git a/cli/tools/task.rs b/cli/tools/task.rs
new file mode 100644
index 000000000..3cfce107e
--- /dev/null
+++ b/cli/tools/task.rs
@@ -0,0 +1,165 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use crate::colors;
+use crate::config_file::ConfigFile;
+use crate::flags::Flags;
+use crate::flags::TaskFlags;
+use crate::proc_state::ProcState;
+use deno_core::anyhow::bail;
+use deno_core::anyhow::Context;
+use deno_core::error::AnyError;
+use std::collections::BTreeMap;
+use std::collections::HashMap;
+use std::sync::Arc;
+
+fn get_tasks_config(
+ maybe_config_file: Option<&ConfigFile>,
+) -> Result<BTreeMap<String, String>, AnyError> {
+ if let Some(config_file) = maybe_config_file {
+ let maybe_tasks_config = config_file.to_tasks_config()?;
+ if let Some(tasks_config) = maybe_tasks_config {
+ for key in tasks_config.keys() {
+ if key.is_empty() {
+ bail!("Configuration file task names cannot be empty");
+ } else if !key
+ .chars()
+ .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'))
+ {
+ bail!("Configuration file task names must only contain alpha-numeric characters, underscores (_), or dashes (-). Task: {}", key);
+ } else if !key.chars().next().unwrap().is_ascii_alphabetic() {
+ bail!("Configuration file task names must start with an alphabetic character. Task: {}", key);
+ }
+ }
+ Ok(tasks_config)
+ } else {
+ bail!("No tasks found in configuration file")
+ }
+ } else {
+ bail!("No config file found")
+ }
+}
+
+fn print_available_tasks(tasks_config: BTreeMap<String, String>) {
+ eprintln!("{}", colors::green("Available tasks:"));
+
+ for name in tasks_config.keys() {
+ eprintln!("- {}", colors::cyan(name));
+ eprintln!(" {}", tasks_config[name])
+ }
+}
+
+pub async fn execute_script(
+ flags: Flags,
+ task_flags: TaskFlags,
+) -> Result<i32, AnyError> {
+ let flags = Arc::new(flags);
+ let ps = ProcState::build(flags.clone()).await?;
+ let tasks_config = get_tasks_config(ps.maybe_config_file.as_ref())?;
+ let config_file_url = &ps.maybe_config_file.as_ref().unwrap().specifier;
+ let config_file_path = if config_file_url.scheme() == "file" {
+ config_file_url.to_file_path().unwrap()
+ } else {
+ bail!("Only local configuration files are supported")
+ };
+
+ if task_flags.task.is_empty() {
+ print_available_tasks(tasks_config);
+ return Ok(1);
+ }
+
+ let cwd = config_file_path.parent().unwrap();
+ let task_name = task_flags.task;
+ let maybe_script = tasks_config.get(&task_name);
+
+ if let Some(script) = maybe_script {
+ let additional_args = flags
+ .argv
+ .iter()
+ // surround all the additional arguments in double quotes
+ // and santize any command substition
+ .map(|a| format!("\"{}\"", a.replace('"', "\\\"").replace('$', "\\$")))
+ .collect::<Vec<_>>()
+ .join(" ");
+ let script = format!("{} {}", script, additional_args);
+ let seq_list = deno_task_shell::parser::parse(&script)
+ .with_context(|| format!("Error parsing script '{}'.", task_name))?;
+ let env_vars = std::env::vars().collect::<HashMap<String, String>>();
+ let exit_code = deno_task_shell::execute(seq_list, env_vars, cwd).await;
+ Ok(exit_code)
+ } else {
+ eprintln!("Task not found: {}", task_name);
+ print_available_tasks(tasks_config);
+ Ok(1)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use deno_ast::ModuleSpecifier;
+ use pretty_assertions::assert_eq;
+
+ use super::*;
+
+ #[test]
+ fn tasks_no_tasks() {
+ run_task_error_test(r#"{}"#, "No tasks found in configuration file");
+ }
+
+ #[test]
+ fn task_name_invalid_chars() {
+ run_task_error_test(
+ r#"{
+ "tasks": {
+ "build": "deno test",
+ "some%test": "deno bundle mod.ts"
+ }
+ }"#,
+ concat!(
+ "Configuration file task names must only contain alpha-numeric ",
+ "characters, underscores (_), or dashes (-). Task: some%test",
+ ),
+ );
+ }
+
+ #[test]
+ fn task_name_non_alpha_starting_char() {
+ run_task_error_test(
+ r#"{
+ "tasks": {
+ "build": "deno test",
+ "1test": "deno bundle mod.ts"
+ }
+ }"#,
+ concat!(
+ "Configuration file task names must start with an ",
+ "alphabetic character. Task: 1test",
+ ),
+ );
+ }
+
+ #[test]
+ fn task_name_empty() {
+ run_task_error_test(
+ r#"{
+ "tasks": {
+ "build": "deno test",
+ "": "deno bundle mod.ts"
+ }
+ }"#,
+ "Configuration file task names cannot be empty",
+ );
+ }
+
+ fn run_task_error_test(config_text: &str, expected_error: &str) {
+ let config_dir = ModuleSpecifier::parse("file:///deno/").unwrap();
+ let config_specifier = config_dir.join("tsconfig.json").unwrap();
+ let config_file = ConfigFile::new(config_text, &config_specifier).unwrap();
+ assert_eq!(
+ get_tasks_config(Some(&config_file))
+ .err()
+ .unwrap()
+ .to_string(),
+ expected_error,
+ );
+ }
+}