1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 08:09:08 -05:00

feat: "deno task" subcommand (#13725)

Co-authored-by: David Sherret <dsherret@gmail.com>
This commit is contained in:
Bartek Iwańczuk 2022-03-11 02:56:14 +01:00 committed by GitHub
parent 808f797633
commit 47f22777be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 468 additions and 5 deletions

31
Cargo.lock generated
View file

@ -750,6 +750,7 @@ dependencies = [
"deno_lint",
"deno_net",
"deno_runtime",
"deno_task_shell",
"deno_url",
"deno_web",
"deno_webgpu",
@ -778,7 +779,7 @@ dependencies = [
"os_pipe",
"percent-encoding",
"pin-project",
"pretty_assertions",
"pretty_assertions 0.7.2",
"rand 0.8.4",
"regex",
"ring",
@ -1065,6 +1066,18 @@ dependencies = [
"winres",
]
[[package]]
name = "deno_task_shell"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad23aacc3db4f37eb88fb9c874a85a4abe5eab14a98fc070ee8df9e204f243e6"
dependencies = [
"anyhow",
"futures",
"pretty_assertions 1.1.0",
"tokio",
]
[[package]]
name = "deno_tls"
version = "0.27.0"
@ -2252,9 +2265,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "lock_api"
version = "0.4.5"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
dependencies = [
"scopeguard",
]
@ -2896,6 +2909,18 @@ dependencies = [
"output_vt100",
]
[[package]]
name = "pretty_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50"
dependencies = [
"ansi_term",
"ctor",
"diff",
"output_vt100",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"

View file

@ -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"

View file

@ -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]

View file

@ -139,6 +139,11 @@ pub struct RunFlags {
pub script: String,
}
#[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>,
@ -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()
}
);
}
}

View file

@ -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()
}

View file

@ -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
}
}
}

View file

@ -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"]

View file

@ -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())],
});

8
cli/tests/testdata/task/deno.json vendored Normal file
View file

@ -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"
}
}

View file

@ -0,0 +1 @@
1 2

View file

@ -0,0 +1 @@
1 string "quoted string"

View file

@ -0,0 +1 @@
1 || echo 5

View file

@ -0,0 +1 @@
1 $(echo 5)

View file

@ -0,0 +1,4 @@
1
2
3
4

View file

@ -0,0 +1 @@
10

View file

@ -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\")"

View file

@ -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\")"

View file

@ -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;

165
cli/tools/task.rs Normal file
View file

@ -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,
);
}
}

View file

@ -1705,6 +1705,7 @@ pub fn run_powershell_script_file(
#[derive(Debug, Default)]
pub struct CheckOutputIntegrationTest {
pub args: &'static str,
pub args_vec: Vec<&'static str>,
pub output: &'static str,
pub input: Option<&'static str>,
pub output_str: Option<&'static str>,
@ -1715,7 +1716,15 @@ pub struct CheckOutputIntegrationTest {
impl CheckOutputIntegrationTest {
pub fn run(&self) {
let args = self.args.split_whitespace();
let args = if self.args_vec.is_empty() {
std::borrow::Cow::Owned(self.args.split_whitespace().collect::<Vec<_>>())
} else {
assert!(
self.args.is_empty(),
"Do not provide args when providing args_vec."
);
std::borrow::Cow::Borrowed(&self.args_vec)
};
let deno_exe = deno_exe_path();
println!("deno_exe path {}", deno_exe.display());
@ -1730,7 +1739,7 @@ impl CheckOutputIntegrationTest {
let mut command = deno_cmd();
println!("deno_exe args {}", self.args);
println!("deno_exe testdata path {:?}", &testdata_dir);
command.args(args);
command.args(args.iter());
command.envs(self.envs.clone());
command.current_dir(&testdata_dir);
command.stdin(Stdio::piped());