From f8f4e776325efe0d8dd50207beecb425f0875999 Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Thu, 2 Nov 2023 02:21:13 +1100 Subject: [PATCH] feat(unstable): `deno run --env` (#20300) This change adds the `--env=[FILE]` flag to the `run`, `compile`, `eval`, `install` and `repl` subcommands. Environment variables set in the CLI overwrite those defined in the `.env` file. --- Cargo.lock | 7 +++ cli/Cargo.toml | 1 + cli/args/flags.rs | 70 +++++++++++++++++++++++++++-- cli/args/mod.rs | 7 +++ cli/tests/integration/eval_tests.rs | 13 ++++++ cli/tests/integration/repl_tests.rs | 16 +++++++ cli/tests/integration/run_tests.rs | 13 ++++++ cli/tests/testdata/env | 4 ++ cli/tests/testdata/run/env_file.out | 4 ++ cli/tests/testdata/run/env_file.ts | 3 ++ 10 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 cli/tests/testdata/env create mode 100644 cli/tests/testdata/run/env_file.out create mode 100644 cli/tests/testdata/run/env_file.ts diff --git a/Cargo.lock b/Cargo.lock index dcb1e7d0cb..5d3f231b4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,6 +1029,7 @@ dependencies = [ "deno_semver", "deno_task_shell", "dissimilar", + "dotenvy", "dprint-plugin-json", "dprint-plugin-markdown", "dprint-plugin-typescript", @@ -2085,6 +2086,12 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dprint-core" version = "0.62.1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 03ae55420a..50920cdd97 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -76,6 +76,7 @@ dashmap = "5.5.3" data-encoding.workspace = true data-url.workspace = true dissimilar = "=1.0.4" +dotenvy = "0.15.7" dprint-plugin-json = "=0.19.0" dprint-plugin-markdown = "=0.16.2" dprint-plugin-typescript = "=0.88.3" diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 0e4621d674..b3fe61abd4 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -395,6 +395,7 @@ pub struct Flags { pub ext: Option, pub ignore: Vec, pub import_map_path: Option, + pub env_file: Option, pub inspect_brk: Option, pub inspect_wait: Option, pub inspect: Option, @@ -1030,6 +1031,7 @@ glob {*_,*.,}bench.{js,mjs,ts,mts,jsx,tsx}: .arg(watch_arg(false)) .arg(no_clear_screen_arg()) .arg(script_arg().last(true)) + .arg(env_file_arg()) }) } @@ -1191,6 +1193,7 @@ supported in canary. .action(ArgAction::SetTrue), ) .arg(executable_ext_arg()) + .arg(env_file_arg()) .arg(script_arg().required(true).trailing_var_arg(true)) }) } @@ -1434,6 +1437,7 @@ This command has implicit access to all permissions (--allow-all).", .value_name("CODE_ARG") .required(true), ) + .arg(env_file_arg()) }) } @@ -1667,6 +1671,7 @@ These must be added to the path manually if required.") .help("Forcefully overwrite existing installation") .action(ArgAction::SetTrue)) ) + .arg(env_file_arg()) } fn jupyter_subcommand() -> Command { @@ -1868,6 +1873,7 @@ fn repl_subcommand() -> Command { .help("Evaluates the provided code when the REPL starts.") .value_name("code"), )) + .arg(env_file_arg()) } fn run_subcommand() -> Command { @@ -1882,6 +1888,7 @@ fn run_subcommand() -> Command { .required_unless_present("v8-flags") .trailing_var_arg(true), ) + .arg(env_file_arg()) .about("Run a JavaScript or TypeScript program") .long_about( "Run a JavaScript or TypeScript program @@ -2063,6 +2070,7 @@ Directory arguments are expanded to all contained files matching the glob .help("Select reporter to use. Default to 'pretty'.") .value_parser(["pretty", "dot", "junit", "tap"]) ) + .arg(env_file_arg()) ) } @@ -2643,6 +2651,18 @@ fn import_map_arg() -> Arg { .value_hint(ValueHint::FilePath) } +fn env_file_arg() -> Arg { + Arg::new("env") + .long("env") + .value_name("FILE") + .help("Load .env file") + .long_help("UNSTABLE: Load environment variables from local file. Only the first environment variable with a given key is used. Existing process environment variables are not overwritten.") + .value_hint(ValueHint::FilePath) + .default_missing_value(".env") + .require_equals(true) + .num_args(0..=1) +} + fn reload_arg() -> Arg { Arg::new("reload") .short('r') @@ -3708,6 +3728,7 @@ fn runtime_args_parse( v8_flags_arg_parse(flags, matches); seed_arg_parse(flags, matches); enable_testing_features_arg_parse(flags, matches); + env_file_arg_parse(flags, matches); } fn inspect_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) { @@ -3745,6 +3766,10 @@ fn import_map_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) { flags.import_map_path = matches.remove_one::("import-map"); } +fn env_file_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) { + flags.env_file = matches.remove_one::("env"); +} + fn reload_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) { if let Some(cache_bl) = matches.remove_many::("reload") { let raw_cache_blocklist: Vec = cache_bl.collect(); @@ -5175,7 +5200,7 @@ mod tests { #[test] fn eval_with_flags() { #[rustfmt::skip] - let r = flags_from_vec(svec!["deno", "eval", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--v8-flags=--help", "--seed", "1", "--inspect=127.0.0.1:9229", "42"]); + let r = flags_from_vec(svec!["deno", "eval", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--v8-flags=--help", "--seed", "1", "--inspect=127.0.0.1:9229", "--env=.example.env", "42"]); assert_eq!( r.unwrap(), Flags { @@ -5204,6 +5229,7 @@ mod tests { allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, + env_file: Some(".example.env".to_owned()), ..Flags::default() } ); @@ -5273,7 +5299,7 @@ mod tests { #[test] fn repl_with_flags() { #[rustfmt::skip] - let r = flags_from_vec(svec!["deno", "repl", "-A", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--v8-flags=--help", "--seed", "1", "--inspect=127.0.0.1:9229", "--unsafely-ignore-certificate-errors"]); + let r = flags_from_vec(svec!["deno", "repl", "-A", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--v8-flags=--help", "--seed", "1", "--inspect=127.0.0.1:9229", "--unsafely-ignore-certificate-errors", "--env=.example.env"]); assert_eq!( r.unwrap(), Flags { @@ -5304,6 +5330,7 @@ mod tests { allow_write: Some(vec![]), allow_ffi: Some(vec![]), allow_hrtime: true, + env_file: Some(".example.env".to_owned()), unsafely_ignore_certificate_errors: Some(vec![]), ..Flags::default() } @@ -6067,6 +6094,39 @@ mod tests { ); } + #[test] + fn run_env_file_default() { + let r = flags_from_vec(svec!["deno", "run", "--env", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + watch: Default::default(), + }), + env_file: Some(".env".to_owned()), + ..Flags::default() + } + ); + } + + #[test] + fn run_env_file_defined() { + let r = + flags_from_vec(svec!["deno", "run", "--env=.another_env", "script.ts"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + watch: Default::default(), + }), + env_file: Some(".another_env".to_owned()), + ..Flags::default() + } + ); + } + #[test] fn cache_multiple() { let r = @@ -6148,7 +6208,7 @@ mod tests { #[test] fn install_with_flags() { #[rustfmt::skip] - let r = flags_from_vec(svec!["deno", "install", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--unsafely-ignore-certificate-errors", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--allow-read", "--allow-net", "--v8-flags=--help", "--seed", "1", "--inspect=127.0.0.1:9229", "--name", "file_server", "--root", "/foo", "--force", "https://deno.land/std/http/file_server.ts", "foo", "bar"]); + let r = flags_from_vec(svec!["deno", "install", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--unsafely-ignore-certificate-errors", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--allow-read", "--allow-net", "--v8-flags=--help", "--seed", "1", "--inspect=127.0.0.1:9229", "--name", "file_server", "--root", "/foo", "--force", "--env=.example.env", "https://deno.land/std/http/file_server.ts", "foo", "bar"]); assert_eq!( r.unwrap(), Flags { @@ -6174,6 +6234,7 @@ mod tests { allow_net: Some(vec![]), unsafely_ignore_certificate_errors: Some(vec![]), allow_read: Some(vec![]), + env_file: Some(".example.env".to_owned()), ..Flags::default() } ); @@ -7557,7 +7618,7 @@ mod tests { #[test] fn compile_with_flags() { #[rustfmt::skip] - let r = flags_from_vec(svec!["deno", "compile", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--unsafely-ignore-certificate-errors", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--allow-read", "--allow-net", "--v8-flags=--help", "--seed", "1", "--no-terminal", "--output", "colors", "https://deno.land/std/examples/colors.ts", "foo", "bar", "-p", "8080"]); + let r = flags_from_vec(svec!["deno", "compile", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--unsafely-ignore-certificate-errors", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--allow-read", "--allow-net", "--v8-flags=--help", "--seed", "1", "--no-terminal", "--output", "colors", "--env=.example.env", "https://deno.land/std/examples/colors.ts", "foo", "bar", "-p", "8080"]); assert_eq!( r.unwrap(), Flags { @@ -7584,6 +7645,7 @@ mod tests { allow_net: Some(vec![]), v8_flags: svec!["--help", "--random-seed=1"], seed: Some(1), + env_file: Some(".example.env".to_owned()), ..Flags::default() } ); diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 94eaf2a3e7..b8b0a81f52 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -52,6 +52,7 @@ use deno_runtime::deno_tls::rustls_pemfile; use deno_runtime::deno_tls::webpki_roots; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::permissions::PermissionsOptions; +use dotenvy::from_filename; use once_cell::sync::Lazy; use once_cell::sync::OnceCell; use serde::Deserialize; @@ -651,6 +652,12 @@ impl CliOptions { let maybe_vendor_folder = resolve_vendor_folder(&initial_cwd, &flags, maybe_config_file.as_ref()); + if let Some(env_file_name) = &flags.env_file { + if (from_filename(env_file_name)).is_err() { + bail!("Unable to load '{env_file_name}' environment variable file") + } + } + Ok(Self { flags, initial_cwd, diff --git a/cli/tests/integration/eval_tests.rs b/cli/tests/integration/eval_tests.rs index df2362c7f7..99a0d674f4 100644 --- a/cli/tests/integration/eval_tests.rs +++ b/cli/tests/integration/eval_tests.rs @@ -77,3 +77,16 @@ itest!(check_local_by_default2 { output: "eval/check_local_by_default2.out", http_server: true, }); + +itest!(env_file { + args: "eval --env=env console.log(Deno.env.get(\"ANOTHER_FOO\"))", + output_str: Some("ANOTHER_BAR\n"), +}); + +itest!(env_file_missing { + args: "eval --env=missing console.log(Deno.env.get(\"ANOTHER_FOO\"))", + output_str: Some( + "error: Unable to load 'missing' environment variable file\n" + ), + exit_code: 1, +}); diff --git a/cli/tests/integration/repl_tests.rs b/cli/tests/integration/repl_tests.rs index 1cdc625b25..fe075d37c8 100644 --- a/cli/tests/integration/repl_tests.rs +++ b/cli/tests/integration/repl_tests.rs @@ -1058,3 +1058,19 @@ fn closed_file_pre_load_does_not_occur() { assert_contains!(console.all_output(), "Skipping document preload.",); }); } + +#[test] +fn env_file() { + TestContext::default() + .new_command() + .args_vec([ + "repl", + "--env=env", + "--allow-env", + "--eval", + "console.log(Deno.env.get('FOO'))", + ]) + .with_pty(|console| { + assert_contains!(console.all_output(), "BAR",); + }); +} diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index f709252892..64df31818d 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -808,6 +808,19 @@ fn permissions_cache() { }); } +itest!(env_file { + args: "run --env=env --allow-env run/env_file.ts", + output: "run/env_file.out", +}); + +itest!(env_file_missing { + args: "run --env=missing --allow-env run/env_file.ts", + output_str: Some( + "error: Unable to load 'missing' environment variable file\n" + ), + exit_code: 1, +}); + itest!(_091_use_define_for_class_fields { args: "run --check run/091_use_define_for_class_fields.ts", output: "run/091_use_define_for_class_fields.ts.out", diff --git a/cli/tests/testdata/env b/cli/tests/testdata/env new file mode 100644 index 0000000000..c41732d30b --- /dev/null +++ b/cli/tests/testdata/env @@ -0,0 +1,4 @@ +FOO=BAR +ANOTHER_FOO=ANOTHER_${FOO} +MULTILINE="First Line +Second Line" \ No newline at end of file diff --git a/cli/tests/testdata/run/env_file.out b/cli/tests/testdata/run/env_file.out new file mode 100644 index 0000000000..54a0bf25df --- /dev/null +++ b/cli/tests/testdata/run/env_file.out @@ -0,0 +1,4 @@ +BAR +ANOTHER_BAR +First Line +Second Line diff --git a/cli/tests/testdata/run/env_file.ts b/cli/tests/testdata/run/env_file.ts new file mode 100644 index 0000000000..48488ce721 --- /dev/null +++ b/cli/tests/testdata/run/env_file.ts @@ -0,0 +1,3 @@ +console.log(Deno.env.get("FOO")); +console.log(Deno.env.get("ANOTHER_FOO")); +console.log(Deno.env.get("MULTILINE"));