diff --git a/Cargo.lock b/Cargo.lock index a4e0c55f7bb..d818ac795c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,6 +632,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dotenvy" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e851a83c30366fd01d75b913588e95e74a1705c1ecc5d58b1f8e1a6d556525f" +dependencies = [ + "dirs", +] + [[package]] name = "dunce" version = "1.0.2" @@ -1533,6 +1542,7 @@ name = "moon_task" version = "0.1.0" dependencies = [ "common-path", + "dotenvy", "moon_config", "moon_error", "moon_logger", diff --git a/crates/config/src/project/task.rs b/crates/config/src/project/task.rs index b8181e693c4..81c80603098 100644 --- a/crates/config/src/project/task.rs +++ b/crates/config/src/project/task.rs @@ -1,6 +1,8 @@ use crate::project::{ProjectConfig, ProjectLanguage}; use crate::types::{FilePath, InputValue, TargetID}; -use crate::validators::{skip_if_default, validate_child_or_root_path, validate_target}; +use crate::validators::{ + skip_if_default, validate_child_or_root_path, validate_child_relative_path, validate_target, +}; use moon_utils::process::split_args; use moon_utils::regex::{ENV_VAR, NODE_COMMAND, UNIX_SYSTEM_COMMAND, WINDOWS_SYSTEM_COMMAND}; use schemars::gen::SchemaGenerator; @@ -42,6 +44,14 @@ fn validate_outputs(list: &[String]) -> Result<(), ValidationError> { Ok(()) } +fn validate_env_file(file: &TaskOptionEnvFile) -> Result<(), ValidationError> { + if let TaskOptionEnvFile::File(path) = file { + validate_child_relative_path("env_file", path)?; + } + + Ok(()) +} + #[derive(Clone, Debug, Default, Deserialize, Display, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum PlatformType { @@ -56,6 +66,23 @@ pub enum PlatformType { Unknown, } +#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +#[serde(untagged)] +pub enum TaskOptionEnvFile { + Enabled(bool), + File(String), +} + +impl TaskOptionEnvFile { + pub fn to_option(&self) -> Option { + match self { + TaskOptionEnvFile::Enabled(true) => Some(".env".to_owned()), + TaskOptionEnvFile::Enabled(false) => None, + TaskOptionEnvFile::File(path) => Some(path.to_owned()), + } + } +} + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum TaskMergeStrategy { @@ -78,6 +105,10 @@ pub struct TaskOptionsConfig { #[serde(skip_serializing_if = "Option::is_none")] pub cache: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[validate(custom = "validate_env_file")] + pub env_file: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub merge_args: Option, @@ -674,5 +705,47 @@ options: Ok(()) }); } + + #[test] + #[should_panic( + expected = "data did not match any variant of untagged enum TaskOptionEnvFile for key \"default.options.envFile\"" + )] + fn invalid_env_file_type() { + figment::Jail::expect_with(|jail| { + jail.create_file( + super::CONFIG_FILENAME, + r#" +command: foo +options: + envFile: 123 +"#, + )?; + + super::load_jailed_config()?; + + Ok(()) + }); + } + + // Enums validation is currently not supported: + // https://github.com/Keats/validator/issues/77 + // #[test] + // #[should_panic(expected = "todo")] + // fn invalid_env_file_path() { + // figment::Jail::expect_with(|jail| { + // jail.create_file( + // super::CONFIG_FILENAME, + // r#" + // command: foo + // options: + // envFile: '../.env' + // "#, + // )?; + + // super::load_jailed_config()?; + + // Ok(()) + // }); + // } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 528396af557..1c2a83ecb2c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5,7 +5,7 @@ use moon_config::{ }; use moon_constants::CONFIG_PROJECT_FILENAME; use moon_logger::{color, debug, trace, Logable}; -use moon_task::{FileGroup, Target, Task, TokenResolver, TokenSharedData}; +use moon_task::{FileGroup, ResolverData, Target, Task, TokenResolver}; use moon_utils::path; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -109,7 +109,7 @@ fn create_tasks_from_config( project_config: &ProjectConfig, global_config: &GlobalProjectConfig, dependencies: &[DependencyConfig], - token_data: &TokenSharedData, + resolver_data: &ResolverData, implicit_inputs: &[String], ) -> Result { let mut tasks = BTreeMap::::new(); @@ -222,12 +222,13 @@ fn create_tasks_from_config( task.inputs.extend(implicit_inputs.iter().cloned()); // Resolve in order! + task.expand_env(resolver_data)?; task.expand_deps(project_id, dependencies)?; - task.expand_inputs(TokenResolver::for_inputs(token_data))?; - task.expand_outputs(TokenResolver::for_outputs(token_data))?; + task.expand_inputs(TokenResolver::for_inputs(resolver_data))?; + task.expand_outputs(TokenResolver::for_outputs(resolver_data))?; // Must be last as it references inputs/outputs - task.expand_args(TokenResolver::for_args(token_data))?; + task.expand_args(TokenResolver::for_args(resolver_data))?; } Ok(tasks) @@ -309,14 +310,14 @@ impl Project { let config = load_project_config(&log_target, &root, source)?; let file_groups = create_file_groups_from_config(&log_target, &config, global_config); let dependencies = create_dependencies_from_config(&log_target, &config); - let token_data = TokenSharedData::new(&file_groups, workspace_root, &root, &config); + let resolver_data = ResolverData::new(&file_groups, workspace_root, &root, &config); let tasks = create_tasks_from_config( &log_target, id, &config, global_config, &dependencies, - &token_data, + &resolver_data, implicit_inputs, )?; diff --git a/crates/project/tests/project_test.rs b/crates/project/tests/project_test.rs index 17eb2463605..43651bfabd8 100644 --- a/crates/project/tests/project_test.rs +++ b/crates/project/tests/project_test.rs @@ -221,6 +221,7 @@ mod tasks { fn mock_merged_task_options_config(strategy: TaskMergeStrategy) -> TaskOptionsConfig { TaskOptionsConfig { cache: None, + env_file: None, merge_args: Some(strategy.clone()), merge_deps: Some(strategy.clone()), merge_env: Some(strategy.clone()), @@ -237,6 +238,7 @@ mod tasks { fn mock_local_task_options_config(strategy: TaskMergeStrategy) -> TaskOptionsConfig { TaskOptionsConfig { cache: None, + env_file: None, merge_args: Some(strategy.clone()), merge_deps: Some(strategy.clone()), merge_env: Some(strategy.clone()), @@ -253,6 +255,7 @@ mod tasks { fn stub_global_task_options_config() -> TaskOptionsConfig { TaskOptionsConfig { cache: Some(true), + env_file: None, merge_args: None, merge_deps: None, merge_env: None, @@ -733,6 +736,7 @@ mod tasks { outputs: Some(string_vec!["a.ts", "b.ts"]), options: TaskOptionsConfig { cache: Some(true), + env_file: None, merge_args: Some(TaskMergeStrategy::Append), merge_deps: Some(TaskMergeStrategy::Prepend), merge_env: Some(TaskMergeStrategy::Replace), @@ -768,6 +772,7 @@ mod tasks { outputs: Some(string_vec!["b.ts"]), options: TaskOptionsConfig { cache: None, + env_file: None, merge_args: Some(TaskMergeStrategy::Append), merge_deps: Some(TaskMergeStrategy::Prepend), merge_env: Some(TaskMergeStrategy::Replace), diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml index 6a356ca5f8d..52010464a27 100644 --- a/crates/task/Cargo.toml +++ b/crates/task/Cargo.toml @@ -9,5 +9,6 @@ moon_error = { path = "../error" } moon_logger = { path = "../logger" } moon_utils = { path = "../utils" } common-path = "1.0.0" +dotenvy = "0.15.1" serde = { version = "1.0.140", features = ["derive"] } thiserror = "1.0.31" diff --git a/crates/task/src/errors.rs b/crates/task/src/errors.rs index 688b0837bf2..1c5d821d3b6 100644 --- a/crates/task/src/errors.rs +++ b/crates/task/src/errors.rs @@ -1,10 +1,14 @@ use moon_error::MoonError; use moon_utils::glob::GlobError; use moon_utils::process::ArgsParseError; +use std::path::PathBuf; use thiserror::Error; #[derive(Error, Debug)] pub enum TaskError { + #[error("Failed to parse env file {0}: {1}")] + InvalidEnvFile(PathBuf, String), + #[error( "Task outputs do not support file globs. Found {0} in {1}." )] diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 2af0c8098aa..ef03066c876 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -12,5 +12,5 @@ pub use errors::*; pub use file_group::FileGroup; pub use target::{Target, TargetProjectScope}; pub use task::{Task, TaskOptions}; -pub use token::{ResolverType, TokenResolver, TokenSharedData, TokenType}; +pub use token::{ResolverData, ResolverType, TokenResolver, TokenType}; pub use types::*; diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index 9530c2f99f2..dcafb0d9897 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -1,10 +1,10 @@ use crate::errors::{TargetError, TaskError}; use crate::target::{Target, TargetProjectScope}; -use crate::token::TokenResolver; +use crate::token::{ResolverData, TokenResolver}; use crate::types::{EnvVars, TouchedFilePaths}; use moon_config::{ DependencyConfig, FileGlob, FilePath, InputValue, PlatformType, TargetID, TaskConfig, - TaskMergeStrategy, TaskOptionsConfig, TaskOutputStyle, + TaskMergeStrategy, TaskOptionEnvFile, TaskOptionsConfig, TaskOutputStyle, }; use moon_logger::{color, debug, trace, Logable}; use moon_utils::{glob, path, regex::ENV_VAR, string_vec}; @@ -18,6 +18,8 @@ use std::path::PathBuf; pub struct TaskOptions { pub cache: bool, + pub env_file: Option, + pub merge_args: TaskMergeStrategy, pub merge_deps: TaskMergeStrategy, @@ -43,6 +45,7 @@ impl Default for TaskOptions { fn default() -> Self { TaskOptions { cache: true, + env_file: None, merge_args: TaskMergeStrategy::Append, merge_deps: TaskMergeStrategy::Append, merge_env: TaskMergeStrategy::Append, @@ -59,6 +62,10 @@ impl Default for TaskOptions { impl TaskOptions { pub fn merge(&mut self, config: &TaskOptionsConfig) { + if let Some(env_file) = &config.env_file { + self.env_file = env_file.to_option(); + } + if let Some(merge_args) = &config.merge_args { self.merge_args = merge_args.clone(); } @@ -106,6 +113,14 @@ impl TaskOptions { // Skip merge options until we need them + if let Some(env_file) = &self.env_file { + config.env_file = Some(if env_file == ".env" { + TaskOptionEnvFile::Enabled(true) + } else { + TaskOptionEnvFile::File(env_file.clone()) + }); + } + if let Some(output_style) = &self.output_style { config.output_style = Some(output_style.clone()); } @@ -201,6 +216,9 @@ impl Task { log_target, options: TaskOptions { cache: cloned_options.cache.unwrap_or(!is_long_running), + env_file: cloned_options + .env_file + .map(|env_file| env_file.to_option().unwrap()), merge_args: cloned_options.merge_args.unwrap_or_default(), merge_deps: cloned_options.merge_deps.unwrap_or_default(), merge_env: cloned_options.merge_env.unwrap_or_default(), @@ -392,6 +410,24 @@ impl Task { Ok(()) } + /// Expand environment variables by loading a `.env` file if configured. + pub fn expand_env(&mut self, data: &ResolverData) -> Result<(), TaskError> { + if let Some(env_file) = &self.options.env_file { + let env_path = data.project_root.join(env_file); + let error_handler = + |e: dotenvy::Error| TaskError::InvalidEnvFile(env_path.clone(), e.to_string()); + + for entry in dotenvy::from_path_iter(&env_path).map_err(error_handler)? { + let (key, value) = entry.map_err(error_handler)?; + + // Vars defined in `env` take precedence over those in the env file + self.env.entry(key).or_insert(value); + } + } + + Ok(()) + } + /// Expand the inputs list to a set of absolute file paths, while resolving tokens. pub fn expand_inputs(&mut self, token_resolver: TokenResolver) -> Result<(), TaskError> { if self.inputs.is_empty() { diff --git a/crates/task/src/test.rs b/crates/task/src/test.rs index f507260f767..3e85407d40f 100644 --- a/crates/task/src/test.rs +++ b/crates/task/src/test.rs @@ -1,4 +1,4 @@ -use crate::{FileGroup, Target, Task, TaskError, TokenResolver, TokenSharedData}; +use crate::{FileGroup, ResolverData, Target, Task, TaskError, TokenResolver}; use moon_config::{ProjectConfig, TaskConfig}; use moon_utils::string_vec; use std::collections::HashMap; @@ -84,9 +84,9 @@ pub fn create_expanded_task( let mut task = create_initial_task(config); let file_groups = create_file_groups(); let project_config = ProjectConfig::new(project_root); - let metadata = - TokenSharedData::new(&file_groups, workspace_root, project_root, &project_config); + let metadata = ResolverData::new(&file_groups, workspace_root, project_root, &project_config); + task.expand_env(&metadata)?; task.expand_inputs(TokenResolver::for_inputs(&metadata))?; task.expand_outputs(TokenResolver::for_outputs(&metadata))?; task.expand_args(TokenResolver::for_args(&metadata))?; // Must be last diff --git a/crates/task/src/token.rs b/crates/task/src/token.rs index 3881448fb0e..ff17685e0e1 100644 --- a/crates/task/src/token.rs +++ b/crates/task/src/token.rs @@ -31,6 +31,32 @@ impl ResolverType { } } +pub struct ResolverData<'a> { + pub file_groups: &'a HashMap, + + pub project_config: &'a ProjectConfig, + + pub project_root: &'a Path, + + pub workspace_root: &'a Path, +} + +impl<'a> ResolverData<'a> { + pub fn new( + file_groups: &'a HashMap, + workspace_root: &'a Path, + project_root: &'a Path, + project_config: &'a ProjectConfig, + ) -> ResolverData<'a> { + ResolverData { + file_groups, + project_config, + project_root, + workspace_root, + } + } +} + #[derive(Debug, PartialEq)] pub enum TokenType { Var(String), @@ -87,54 +113,28 @@ impl TokenType { } } -pub struct TokenSharedData<'a> { - pub file_groups: &'a HashMap, - - pub project_config: &'a ProjectConfig, - - pub project_root: &'a Path, - - pub workspace_root: &'a Path, -} - -impl<'a> TokenSharedData<'a> { - pub fn new( - file_groups: &'a HashMap, - workspace_root: &'a Path, - project_root: &'a Path, - project_config: &'a ProjectConfig, - ) -> TokenSharedData<'a> { - TokenSharedData { - file_groups, - project_config, - project_root, - workspace_root, - } - } -} - pub struct TokenResolver<'a> { context: ResolverType, - pub data: &'a TokenSharedData<'a>, + pub data: &'a ResolverData<'a>, } impl<'a> TokenResolver<'a> { - pub fn for_args(data: &'a TokenSharedData<'a>) -> TokenResolver<'a> { + pub fn for_args(data: &'a ResolverData<'a>) -> TokenResolver<'a> { TokenResolver { context: ResolverType::Args, data, } } - pub fn for_inputs(data: &'a TokenSharedData<'a>) -> TokenResolver<'a> { + pub fn for_inputs(data: &'a ResolverData<'a>) -> TokenResolver<'a> { TokenResolver { context: ResolverType::Inputs, data, } } - pub fn for_outputs(data: &'a TokenSharedData<'a>) -> TokenResolver<'a> { + pub fn for_outputs(data: &'a ResolverData<'a>) -> TokenResolver<'a> { TokenResolver { context: ResolverType::Outputs, data, diff --git a/crates/task/tests/task_test.rs b/crates/task/tests/task_test.rs index 653757c4997..13b1e806884 100644 --- a/crates/task/tests/task_test.rs +++ b/crates/task/tests/task_test.rs @@ -1,8 +1,8 @@ -use moon_config::TaskConfig; +use moon_config::{TaskConfig, TaskOptionEnvFile, TaskOptionsConfig}; use moon_task::test::create_expanded_task; -use moon_utils::test::get_fixtures_dir; +use moon_utils::test::{create_sandbox, get_fixtures_dir}; use moon_utils::{glob, string_vec}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::env; #[test] @@ -45,6 +45,7 @@ mod is_affected { env::remove_var("FOO"); } + #[test] fn returns_false_if_var_missing() { let workspace_root = get_fixtures_dir("base"); @@ -184,6 +185,143 @@ mod is_affected { } } +mod expand_env { + use super::*; + use std::fs; + + #[test] + #[should_panic(expected = "Error parsing line: 'FOO', error at line index: 3")] + fn errors_on_invalid_file() { + let fixture = create_sandbox("cases"); + let project_root = fixture.path().join("base"); + + fs::write(project_root.join(".env"), "FOO").unwrap(); + + create_expanded_task( + fixture.path(), + &project_root, + Some(TaskConfig { + options: TaskOptionsConfig { + env_file: Some(TaskOptionEnvFile::Enabled(true)), + ..TaskOptionsConfig::default() + }, + ..TaskConfig::default() + }), + ) + .unwrap(); + } + + #[test] + // Windows = "The system cannot find the file specified" + // Unix = "No such file or directory" + #[should_panic(expected = "InvalidEnvFile")] + fn errors_on_missing_file() { + let fixture = create_sandbox("cases"); + let project_root = fixture.path().join("base"); + + create_expanded_task( + fixture.path(), + &project_root, + Some(TaskConfig { + options: TaskOptionsConfig { + env_file: Some(TaskOptionEnvFile::Enabled(true)), + ..TaskOptionsConfig::default() + }, + ..TaskConfig::default() + }), + ) + .unwrap(); + } + + #[test] + fn loads_using_bool() { + let fixture = create_sandbox("cases"); + let project_root = fixture.path().join("base"); + + fs::write(project_root.join(".env"), "FOO=foo\nBAR=123").unwrap(); + + let task = create_expanded_task( + fixture.path(), + &project_root, + Some(TaskConfig { + options: TaskOptionsConfig { + env_file: Some(TaskOptionEnvFile::Enabled(true)), + ..TaskOptionsConfig::default() + }, + ..TaskConfig::default() + }), + ) + .unwrap(); + + assert_eq!( + task.env, + HashMap::from([ + ("FOO".to_owned(), "foo".to_owned()), + ("BAR".to_owned(), "123".to_owned()) + ]) + ); + } + + #[test] + fn loads_using_custom_path() { + let fixture = create_sandbox("cases"); + let project_root = fixture.path().join("base"); + + fs::write(project_root.join(".env.production"), "FOO=foo\nBAR=123").unwrap(); + + let task = create_expanded_task( + fixture.path(), + &project_root, + Some(TaskConfig { + options: TaskOptionsConfig { + env_file: Some(TaskOptionEnvFile::File(".env.production".to_owned())), + ..TaskOptionsConfig::default() + }, + ..TaskConfig::default() + }), + ) + .unwrap(); + + assert_eq!( + task.env, + HashMap::from([ + ("FOO".to_owned(), "foo".to_owned()), + ("BAR".to_owned(), "123".to_owned()) + ]) + ); + } + + #[test] + fn doesnt_override_other_env() { + let fixture = create_sandbox("cases"); + let project_root = fixture.path().join("base"); + + fs::write(project_root.join(".env"), "FOO=foo\nBAR=123").unwrap(); + + let task = create_expanded_task( + fixture.path(), + &project_root, + Some(TaskConfig { + env: Some(HashMap::from([("FOO".to_owned(), "original".to_owned())])), + options: TaskOptionsConfig { + env_file: Some(TaskOptionEnvFile::Enabled(true)), + ..TaskOptionsConfig::default() + }, + ..TaskConfig::default() + }), + ) + .unwrap(); + + assert_eq!( + task.env, + HashMap::from([ + ("FOO".to_owned(), "original".to_owned()), + ("BAR".to_owned(), "123".to_owned()) + ]) + ); + } +} + mod expand_inputs { use super::*; diff --git a/crates/task/tests/token_test.rs b/crates/task/tests/token_test.rs index e4904caec4b..eda87eed136 100644 --- a/crates/task/tests/token_test.rs +++ b/crates/task/tests/token_test.rs @@ -1,6 +1,6 @@ use moon_config::{ProjectConfig, ProjectLanguage, ProjectType, TaskConfig}; use moon_task::test::{create_expanded_task, create_file_groups, create_initial_task}; -use moon_task::{TokenResolver, TokenSharedData}; +use moon_task::{ResolverData, TokenResolver}; use moon_utils::test::get_fixtures_dir; use moon_utils::{glob, string_vec}; use std::path::PathBuf; @@ -20,7 +20,7 @@ fn errors_for_unknown_file_group() { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -41,7 +41,7 @@ fn errors_if_no_globs_in_file_group() { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -61,7 +61,7 @@ fn doesnt_match_when_not_alone() { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -88,7 +88,7 @@ mod in_token { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -116,7 +116,7 @@ mod in_token { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -147,7 +147,7 @@ mod out_token { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -175,7 +175,7 @@ mod out_token { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -207,7 +207,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -233,7 +233,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -259,7 +259,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -292,7 +292,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -325,7 +325,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -354,7 +354,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -384,7 +384,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -417,7 +417,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -452,7 +452,7 @@ mod args { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -479,7 +479,7 @@ mod args { type_of: ProjectType::Tool, ..ProjectConfig::default() }; - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -542,7 +542,7 @@ mod inputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -568,7 +568,7 @@ mod inputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -594,7 +594,7 @@ mod inputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -627,7 +627,7 @@ mod inputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -660,7 +660,7 @@ mod inputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -690,7 +690,7 @@ mod inputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -709,7 +709,7 @@ mod inputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -727,7 +727,7 @@ mod inputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -755,7 +755,7 @@ mod outputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -776,7 +776,7 @@ mod outputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -797,7 +797,7 @@ mod outputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -818,7 +818,7 @@ mod outputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -839,7 +839,7 @@ mod outputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -858,7 +858,7 @@ mod outputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -877,7 +877,7 @@ mod outputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, @@ -898,7 +898,7 @@ mod outputs { let project_config = ProjectConfig::new(&project_root); let workspace_root = get_workspace_root(); let file_groups = create_file_groups(); - let metadata = TokenSharedData::new( + let metadata = ResolverData::new( &file_groups, &workspace_root, &project_root, diff --git a/website/docs/config/project.mdx b/website/docs/config/project.mdx index 3c29c33b6e5..2b6c0b54d7e 100644 --- a/website/docs/config/project.mdx +++ b/website/docs/config/project.mdx @@ -270,6 +270,7 @@ tasks: > `Record` The `env` field is map of strings that are passed as environment variables when running the command. +Variables defined here will take precedence over those loaded with [`envFile`](#envfile). ```yaml title="moon.yml" {4,5} tasks: @@ -375,6 +376,25 @@ tasks: cache: false ``` +#### `envFile` + +> `boolean | string` + +A boolean or path to a project relative file (also know as dotenv file) that defines a collection of +[environment variables](#env) for the current task. Variables will be loaded on project creation, +but will _not_ override those defined in [`env`](#env). + +```yaml title="moon.yml" {6} +tasks: + build: + command: 'webpack' + options: + # Defaults to .env + envFile: true + # Or + envFile: '.env.production' +``` + #### `mergeArgs` > `TaskMergeStrategy` diff --git a/website/static/schemas/global-project.json b/website/static/schemas/global-project.json index 5e952ca2b68..10df1d327f6 100644 --- a/website/static/schemas/global-project.json +++ b/website/static/schemas/global-project.json @@ -112,6 +112,16 @@ "replace" ] }, + "TaskOptionEnvFile": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, "TaskOptionsConfig": { "type": "object", "properties": { @@ -121,6 +131,16 @@ "null" ] }, + "envFile": { + "anyOf": [ + { + "$ref": "#/definitions/TaskOptionEnvFile" + }, + { + "type": "null" + } + ] + }, "mergeArgs": { "anyOf": [ { @@ -171,6 +191,16 @@ } ] }, + "outputStyle": { + "anyOf": [ + { + "$ref": "#/definitions/TaskOutputStyle" + }, + { + "type": "null" + } + ] + }, "retryCount": { "type": [ "integer", @@ -198,6 +228,13 @@ ] } } + }, + "TaskOutputStyle": { + "type": "string", + "enum": [ + "on-exit", + "stream" + ] } } } \ No newline at end of file diff --git a/website/static/schemas/project.json b/website/static/schemas/project.json index ff86ebf990a..352e44f99f9 100644 --- a/website/static/schemas/project.json +++ b/website/static/schemas/project.json @@ -253,6 +253,16 @@ "replace" ] }, + "TaskOptionEnvFile": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, "TaskOptionsConfig": { "type": "object", "properties": { @@ -262,6 +272,16 @@ "null" ] }, + "envFile": { + "anyOf": [ + { + "$ref": "#/definitions/TaskOptionEnvFile" + }, + { + "type": "null" + } + ] + }, "mergeArgs": { "anyOf": [ { @@ -312,6 +332,16 @@ } ] }, + "outputStyle": { + "anyOf": [ + { + "$ref": "#/definitions/TaskOutputStyle" + }, + { + "type": "null" + } + ] + }, "retryCount": { "type": [ "integer", @@ -339,6 +369,13 @@ ] } } + }, + "TaskOutputStyle": { + "type": "string", + "enum": [ + "on-exit", + "stream" + ] } } } \ No newline at end of file