From 9cf2eee6cef787ca526a659b39310dabf0e215b7 Mon Sep 17 00:00:00 2001 From: Jeremy Moeglich Date: Mon, 7 Oct 2024 16:28:19 +0200 Subject: [PATCH] many fixes --- Cargo.lock | 2 +- Cargo.toml | 4 +- src/apply_resolved.rs | 90 +++++++--- src/bin/weaveconfig.rs | 4 +- src/file_graph.rs | 27 ++- src/get_environment_value.rs | 24 +++ src/lib.rs | 1 + src/resolve_spaces.rs | 28 ++- src/space_graph.rs | 32 +++- src/template_file/mod.rs | 2 +- src/template_file/segment.rs | 54 +++--- src/ts_binding/bun.lockb | Bin 3135 -> 2044 bytes src/ts_binding/generate_binding.rs | 4 +- src/ts_binding/global.d.ts | 23 +-- src/ts_binding/multi_env.ts | 263 +++++++++++++++-------------- src/ts_binding/package.json | 2 +- 16 files changed, 352 insertions(+), 208 deletions(-) create mode 100644 src/get_environment_value.rs diff --git a/Cargo.lock b/Cargo.lock index fb3c124..75356da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1266,7 +1266,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "weaveconfig" -version = "0.1.2" +version = "0.2.0" dependencies = [ "anyhow", "biome_formatter", diff --git a/Cargo.toml b/Cargo.toml index 46db302..fd3e4f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "weaveconfig" -version = "0.1.2" +version = "0.2.0" edition = "2021" description = "A unified configuration tool for monorepos" readme = "README.md" @@ -32,4 +32,4 @@ lto = "fat" [profile.dev] opt-level = 0 -debug = true \ No newline at end of file +debug = true diff --git a/src/apply_resolved.rs b/src/apply_resolved.rs index 69dbb27..1165be6 100644 --- a/src/apply_resolved.rs +++ b/src/apply_resolved.rs @@ -5,10 +5,10 @@ use std::{ use anyhow::Context; use futures::{stream::FuturesUnordered, StreamExt}; -use serde_json::Map; use crate::{ - map_path::map_path, resolve_spaces::ResolvedSpace, template_file::template_file, + get_environment_value::get_environment_value, map_path::map_path, + resolve_spaces::ResolvedSpace, template_file::template_file, ts_binding::generate_binding::generate_binding, write_json_file::write_json_file, }; @@ -42,10 +42,14 @@ async fn apply_space(space: ResolvedSpace, real_path: PathBuf) -> Result<(), any real_path.display() )); } - let gen_folder = gen_folder(&real_path).await?; - write_gitignore(&gen_folder).await?; - write_json_file(&space, &gen_folder).await?; - generate_binding(&space, &gen_folder).await?; + if space.generate.generate && space.variables.is_some() { + let gen_folder = gen_folder(&real_path).await?; + write_gitignore(&gen_folder).await?; + write_json_file(&space, &gen_folder).await?; + if space.generate.typescript { + generate_binding(&space, &gen_folder).await?; + } + } write_to_copy(&space, &real_path).await?; Ok(()) } @@ -58,25 +62,65 @@ async fn write_gitignore(gen_folder: &PathBuf) -> Result<(), anyhow::Error> { Ok(()) } -async fn write_to_copy(space: &ResolvedSpace, real_path: &PathBuf) -> Result<(), anyhow::Error> { - for origin in &space.files_to_copy { - let dest_relative = origin.strip_prefix(&space.path).unwrap(); - let dest = real_path.join(dest_relative); - - let content = tokio::fs::read_to_string(origin).await?; - let content = { - let variables = space.variables.clone().unwrap_or(Map::new()); - template_file(&content, &variables) - .with_context(|| format!("Failed to template file: {}", origin.display()))? - }; - - // create parent dirs - if let Some(parent) = dest.parent() { - if !parent.exists() { - tokio::fs::create_dir_all(parent).await?; +async fn write_to_copy(space: &ResolvedSpace, real_path: &Path) -> Result<(), anyhow::Error> { + // Prepare variables once to avoid cloning multiple times + let variables = space.variables.clone().unwrap_or_default(); + + for to_copy in &space.files_to_copy { + // Determine the relative destination path + let dest_relative = to_copy + .path + .strip_prefix(&space.path) + .with_context(|| format!("Failed to strip prefix for {}", to_copy.path.display()))?; + + // Read the file content asynchronously + let content = tokio::fs::read_to_string(&to_copy.path) + .await + .with_context(|| format!("Failed to read file: {}", to_copy.path.display()))?; + + // Compute the full destination path + let mapped_dist = real_path.join(dest_relative); + + // Create parent directories if they don't exist + if let Some(parent) = mapped_dist.parent() { + tokio::fs::create_dir_all(parent) + .await + .with_context(|| format!("Failed to create directories: {}", parent.display()))?; + } + + if to_copy.for_each_env { + // Iterate over each environment and write templated files + for env in &space.environments { + let env_variables = get_environment_value(&variables, env) + .with_context(|| format!("Failed to get variables for environment: {}", env))?; + + let templated_content = + template_file(&content, &env_variables).with_context(|| { + format!( + "Failed to template file for environment {}: {}", + env, + to_copy.path.display() + ) + })?; + + let dest = mapped_dist.with_file_name(format!("{}.{}", env, to_copy.dest_filename)); + + tokio::fs::write(&dest, templated_content) + .await + .with_context(|| format!("Failed to write file: {}", dest.display()))?; } + } else { + // Template the content once and write to the destination + let templated_content = template_file(&content, &variables) + .with_context(|| format!("Failed to template file: {}", to_copy.path.display()))?; + + let dest = mapped_dist.with_file_name(&to_copy.dest_filename); + + tokio::fs::write(&dest, templated_content) + .await + .with_context(|| format!("Failed to write file: {}", dest.display()))?; } - tokio::fs::write(dest, content).await?; } + Ok(()) } diff --git a/src/bin/weaveconfig.rs b/src/bin/weaveconfig.rs index 21f137c..4c19b63 100644 --- a/src/bin/weaveconfig.rs +++ b/src/bin/weaveconfig.rs @@ -10,8 +10,8 @@ use weaveconfig::generate_weaveconfig; #[derive(Parser)] #[command( - name = "weaveconfig-cli", - version = "0.1.2", + name = "weaveconfig", + version = "0.1.3", author = "Jeremy Moeglich ", about = "A CLI to manage weaveconfig configurations" )] diff --git a/src/file_graph.rs b/src/file_graph.rs index b5d1559..e1f4ccc 100644 --- a/src/file_graph.rs +++ b/src/file_graph.rs @@ -11,7 +11,7 @@ pub struct Directory { pub path: PathBuf, pub parent_directory: Option, pub space: Option, - pub rest_to_copy: Vec, + pub rest_to_copy: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -20,6 +20,13 @@ pub struct SpaceNode { pub variables: Option>, } +#[derive(Debug, Clone, PartialEq)] +pub struct FileToCopy { + pub path: PathBuf, + pub dest_filename: String, + pub for_each_env: bool, +} + /// Creates a graph of the weaveconfig configuration. /// The root of the graph is typically the `weaveconfig` directory within the project root. pub async fn traverse_directory( @@ -136,7 +143,7 @@ async fn locate_directories(directory: &mut Directory) -> Result<(), anyhow::Err enum FileType { Space(SpaceSchema), Variables(serde_json::Map), - Rest(PathBuf), + Rest(FileToCopy), } async fn process_file(file_path: PathBuf) -> Result { @@ -188,13 +195,25 @@ async fn process_file(file_path: PathBuf) -> Result { map.insert(prefix, serde_json::Value::Object(variables)); Ok(FileType::Variables(map)) } + segments if segments.first() == Some(&"_forenv") => { + let dest_filename = segments[1..].join("."); + Ok(FileType::Rest(FileToCopy { + path: file_path, + dest_filename, + for_each_env: true, + })) + } _ => Err(anyhow!( - "Invalid file name format: '{}'. Expected '_space.json', '_env.json', or '__env.json'.", + "Invalid file name format: '{}'. Expected '_space.json', '_env.json', '__env.json' or '_forenv.'.", file_name )), } } else { - Ok(FileType::Rest(file_path)) + Ok(FileType::Rest(FileToCopy { + path: file_path.clone(), + dest_filename: file_name.to_string(), + for_each_env: false, + })) } } diff --git a/src/get_environment_value.rs b/src/get_environment_value.rs new file mode 100644 index 0000000..a2da108 --- /dev/null +++ b/src/get_environment_value.rs @@ -0,0 +1,24 @@ +use serde_json::{Map, Value}; + +use crate::template_file::value_type; + +pub fn get_environment_value( + variables: &Map, + environment: &str, +) -> Result, anyhow::Error> { + let environment_variables = variables.get(environment).ok_or(anyhow::anyhow!( + "Environment {} not found in variables, this is an internal error", + environment + ))?; + let mut variables = variables.clone(); + if let Value::Object(environment_variables) = environment_variables { + for (key, value) in environment_variables { + variables.insert(key.clone(), value.clone()); + } + return Ok(variables); + } + Err(anyhow::anyhow!( + "Expected an object, got {}", + value_type(environment_variables) + )) +} diff --git a/src/lib.rs b/src/lib.rs index 544b0e2..2d9059c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ mod ts_binding; mod write_json_file; mod template_file; mod parse_jsonc; +mod get_environment_value; pub async fn generate_weaveconfig(weaveconfig_config_root: &Path) -> Result<()> { let directory = traverse_directory(weaveconfig_config_root).await?; diff --git a/src/resolve_spaces.rs b/src/resolve_spaces.rs index 47de3ab..e3ae46b 100644 --- a/src/resolve_spaces.rs +++ b/src/resolve_spaces.rs @@ -1,4 +1,8 @@ -use crate::{merging::merge_map_consume, space_graph::SpaceGraph}; +use crate::{ + file_graph::FileToCopy, + merging::merge_map_consume, + space_graph::{GenerateSpace, SpaceGraph}, +}; use anyhow::{Context, Result}; use serde_json::{Map, Value}; use std::{ @@ -11,7 +15,8 @@ pub struct ResolvedSpace { pub variables: Option>, pub environments: HashSet, pub path: PathBuf, - pub files_to_copy: Vec, + pub files_to_copy: Vec, + pub generate: GenerateSpace, } pub fn resolve_spaces(space_graph: SpaceGraph) -> Result> { @@ -91,7 +96,11 @@ fn resolve_space( for env in &space.environments { variables .entry(env.clone()) - .or_insert(Value::Object(Map::new())); + .or_insert_with(|| Value::Object(Map::new())); + if let Some(Value::Object(obj)) = variables.get_mut(env) { + obj.entry("env".to_string()) + .or_insert(Value::String(env.clone())); + } } } @@ -102,6 +111,7 @@ fn resolve_space( environments: space.environments.clone(), path: space.path.clone(), files_to_copy: space.files_to_copy.clone(), + generate: space.generate.clone(), }, ); @@ -138,7 +148,7 @@ fn resolve_dependency( } if let Some(ref mut value) = to_merge { - move_key(value, from_env, to_env); + copy_key(value, from_env, to_env); } else { return Err(anyhow::anyhow!( "No variables present to move from '{}' to '{}'", @@ -150,7 +160,7 @@ fn resolve_dependency( } } - if let Some(to_merge) = to_merge { + if let Some(mut to_merge) = to_merge { if let Some(ref mut value) = variables { merge_map_consume(value, to_merge).with_context(|| { format!( @@ -166,9 +176,9 @@ fn resolve_dependency( Ok(()) } -fn move_key(value: &mut Map, from_key: &str, to_key: &str) { - let current = value.remove(from_key); - if let Some(current) = current { - value.insert(to_key.to_string(), current); +fn copy_key(value: &mut Map, from_key: &str, to_key: &str) { + if let Some(current) = value.get(from_key) { + let copied = current.clone(); + value.insert(to_key.to_string(), copied); } } diff --git a/src/space_graph.rs b/src/space_graph.rs index 363cb91..73da5fe 100644 --- a/src/space_graph.rs +++ b/src/space_graph.rs @@ -1,6 +1,9 @@ use std::{collections::HashMap, path::PathBuf}; -use crate::file_graph::Directory; +use crate::{ + file_graph::{Directory, FileToCopy}, + schemas::GenerateSchema, +}; use std::collections::HashSet; #[derive(Debug, Clone, PartialEq)] @@ -11,8 +14,15 @@ pub struct Space { pub mapping: Option>>, pub environments: HashSet, pub variables: Option>, - pub files_to_copy: Vec, + pub files_to_copy: Vec, pub parent_space: Option, + pub generate: GenerateSpace, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct GenerateSpace { + pub generate: bool, + pub typescript: bool, } pub type SpaceGraph = HashMap; @@ -53,6 +63,22 @@ fn add_to_spaces_graph( variables: space.variables, files_to_copy: vec![], parent_space: closest_parent_space, + generate: { + match space.schema.generate { + Some(GenerateSchema::Generate(generate)) => GenerateSpace { + generate: true, + typescript: generate.typescript, + }, + Some(GenerateSchema::ShouldGenerate(generate)) => GenerateSpace { + generate, + typescript: true, + }, + None => GenerateSpace { + generate: true, + typescript: true, + }, + } + }, }; resolve_files_to_copy(&dir, &mut space.files_to_copy); space_graph.insert(space.name.clone(), space); @@ -63,7 +89,7 @@ fn add_to_spaces_graph( } } -fn resolve_files_to_copy(dir: &Directory, files: &mut Vec) { +fn resolve_files_to_copy(dir: &Directory, files: &mut Vec) { for file in &dir.rest_to_copy { files.push(file.clone()); } diff --git a/src/template_file/mod.rs b/src/template_file/mod.rs index 7f6e526..1e7fcf8 100644 --- a/src/template_file/mod.rs +++ b/src/template_file/mod.rs @@ -27,7 +27,7 @@ pub enum VariableError { InvalidType(String, String), } -fn value_type(value: &Value) -> String { +pub fn value_type(value: &Value) -> String { match value { Value::String(_) => "string", Value::Number(_) => "number", diff --git a/src/template_file/segment.rs b/src/template_file/segment.rs index c82a3cf..6601d60 100644 --- a/src/template_file/segment.rs +++ b/src/template_file/segment.rs @@ -47,10 +47,10 @@ pub fn parse_segment(input: &str) -> Result<(String, &str), ParseSegmentError> { // If we reach here, the quoted string was not closed Err(ParseSegmentError::UnclosedQuote) - } else if first_char.is_alphanumeric() || first_char == '_' { - // It's an alphanumeric or underscore segment + } else if first_char.is_alphanumeric() || first_char == '_' || first_char == '-' { + // It's an alphanumeric, underscore, or hyphen segment for (idx, c) in chars { - if c.is_alphanumeric() || c == '_' { + if c.is_alphanumeric() || c == '_' || c == '-' { result.push(c); } else { // Found a non-alphanumeric character, return up to this point @@ -61,7 +61,7 @@ pub fn parse_segment(input: &str) -> Result<(String, &str), ParseSegmentError> { // Reached the end of the input Ok((result, "")) } else { - // The segment does not start with a quote or an alphanumeric character + // The segment does not start with a quote, alphanumeric, underscore, or hyphen // Return an error as zero characters are parsed Err(ParseSegmentError::NoSegment) } @@ -88,10 +88,10 @@ mod tests { } #[test] - fn test_alphanumeric_with_underscores() { - let input = "hello_world_123!@#"; + fn test_alphanumeric_with_underscores_and_hyphens() { + let input = "hello_world-123!@#"; let (segment, remaining) = parse_segment(input).unwrap(); - assert_eq!(segment, "hello_world_123"); + assert_eq!(segment, "hello_world-123"); assert_eq!(remaining, "!@#"); } @@ -143,16 +143,16 @@ mod tests { #[test] fn test_mixed_input() { - let input = "start \"quoted \\\"string\\\"\" middle 'another \\'test\\'' end"; + let input = "start-1 \"quoted \\\"string\\\"\" middle-2 'another \\'test\\'' end-3"; // Parse first segment let (segment, remaining) = parse_segment(input).unwrap(); - assert_eq!(segment, "start"); + assert_eq!(segment, "start-1"); // Skip leading whitespace before parsing next segment let remaining = skip_whitespace(remaining); assert_eq!( remaining, - "\"quoted \\\"string\\\"\" middle 'another \\'test\\'' end" + "\"quoted \\\"string\\\"\" middle-2 'another \\'test\\'' end-3" ); // Parse second segment @@ -160,42 +160,46 @@ mod tests { assert_eq!(segment, "quoted \"string\""); // Skip leading whitespace before parsing next segment let remaining = skip_whitespace(remaining); - assert_eq!(remaining, "middle 'another \\'test\\'' end"); + assert_eq!(remaining, "middle-2 'another \\'test\\'' end-3"); // Parse third segment let (segment, remaining) = parse_segment(remaining).unwrap(); - assert_eq!(segment, "middle"); + assert_eq!(segment, "middle-2"); // Skip leading whitespace before parsing next segment let remaining = skip_whitespace(remaining); - assert_eq!(remaining, "'another \\'test\\'' end"); + assert_eq!(remaining, "'another \\'test\\'' end-3"); // Parse fourth segment let (segment, remaining) = parse_segment(remaining).unwrap(); assert_eq!(segment, "another 'test'"); // Skip leading whitespace before parsing next segment let remaining = skip_whitespace(remaining); - assert_eq!(remaining, "end"); + assert_eq!(remaining, "end-3"); // Parse fifth segment let (segment, remaining) = parse_segment(remaining).unwrap(); - assert_eq!(segment, "end"); + assert_eq!(segment, "end-3"); assert_eq!(remaining, ""); } #[test] fn test_zero_characters_parsed_error() { - let inputs = vec![" ", "!", "@#", "_invalid_start"]; + let inputs = vec![" ", "!", "@#"]; for input in inputs { - if input.starts_with('_') { - // If the segment starts with an underscore, it should be parsed as a valid segment - let (segment, remaining) = parse_segment(input).unwrap(); - assert_eq!(segment, "_invalid_start"); - assert_eq!(remaining, ""); - } else { - let result = parse_segment(input); - assert_eq!(result, Err(ParseSegmentError::NoSegment)); - } + let result = parse_segment(input); + assert_eq!(result, Err(ParseSegmentError::NoSegment)); + } + } + + #[test] + fn test_valid_segment_starts() { + let inputs = vec!["_valid_start", "-valid-start", "valid-middle-1"]; + + for input in inputs { + let (segment, remaining) = parse_segment(input).unwrap(); + assert_eq!(segment, input); + assert_eq!(remaining, ""); } } } diff --git a/src/ts_binding/bun.lockb b/src/ts_binding/bun.lockb index 5e3af018bc8da17a01771714baa37296b59ca5d6..86547d2b71a1ecc8781da27212efe3df65bcc7ff 100755 GIT binary patch delta 602 zcmdll@rQqcp5{4?S!cJjB(|KLrDA>Y^XG!YJ?UMad4sAnj&SdY3bX3XVP^mXmWko= zP9-c54us@lU}z8lGP!|tIFRN6(m*i=kodJz&wn@{x6PcWAJwb2)z*?T-{#n>z*)N| zz6cPM2dc3Esu2UyAk$eUepH`qz{nw(4CFWhCD?$pFc5PS5%;G+K-*(wA|F2ed zo+V~QPU5?&4SSmdBc$q&eya73wBbFXXk5NXex;3w|F*18xU7X45R{RE&~$~7eJ-Fn|zbYgAHt?#pG!2CjjjGt9$?e delta 1298 zcmeyvzh7d4p606U#?h>c=1yN^Q6R1HW>H<5$ZB!F0)}N1rYK5pEB-CMo|^#-*e8a| z*JrasI1rMHfuTVJ$OO?LK$;UsF9gy+9SzTbG!KxT0TliG}nXrF>5E> znrOLurtX%zk6y%wA6($J-8O%gr#>6gQqMQt_Ot6GpLKBma9G~4&18pJlReNmj0_Oh zF~gh&q**qeJpBAhW`cd`u?lTRrehQQcLi;{VsGA+nvvKeGV4ysl0#+}itFv_SC?cc z1{xe?oOrm{u75_R3A5ca*7CwA?O8|$vmhI+7SLn;d|`@H(_Y(m)qUNOn4)cx4ADCMvUf}220L=|Y*eBPq*hw?| z`wsyi7l6{l2dF#;$7DWMrO7{7c*H;<3i3QCWm$ld7!ZVjrL{mzSW1GWGMI7@-2qi@ z$2r-J)uSGi>0p_NgDEGmB(=B%XckZ*C>z4E6%(VOo}r$RB?AL2Bf>Hk6Jwo)o~fRp z0mDL|9#EErWkH~fk%69}k)FjfpbRJz!!jjM##GP9OwWX2p)!Uhum^ym9YFg*-~{L7 z6RZVN>A)loGNZ-9^bBJK-@hMF>E+y$gV-FIlS=a@-)57Rb111SNG;X}a`kmVq%O$a zh6Z{i=~W;lSJ@ObL81^<5EWoHSe=n2NFCIO^5V%oY*uVw9~e$P#pb~UcC#r+3CJo@ TxH_Os<;A+-P%xS-!TtmQ<&|#V diff --git a/src/ts_binding/generate_binding.rs b/src/ts_binding/generate_binding.rs index e66cdac..4e2dace 100644 --- a/src/ts_binding/generate_binding.rs +++ b/src/ts_binding/generate_binding.rs @@ -19,11 +19,13 @@ pub async fn generate_binding( let ts_type = json_value_to_ts_type(&Value::Object(variables.clone())); content.push_str(&format!("type ConfigType = {};\n\n", ts_type)); - content.push_str("const environments = "); + content.push_str("export const environments = "); content.push_str(&format!( "{} as const;", serde_json::to_string(&resolved_space.environments)? )); + content.push_str("\n"); + content.push_str("export type Environments = typeof environments[number];"); content.push_str("\n\n// static code starts here, using variant: "); if resolved_space.environments.len() == 0 { diff --git a/src/ts_binding/global.d.ts b/src/ts_binding/global.d.ts index da2cf2e..ccf7ccd 100644 --- a/src/ts_binding/global.d.ts +++ b/src/ts_binding/global.d.ts @@ -1,14 +1,15 @@ declare global { - var environments: ["sample_env1", "sample_env2"]; - type ConfigType = { - sample_env1: { - sample_key1: string; - }; - sample_env2: { - sample_key2: string; - }; - sample_shared: number; - }; + var environments: ["sample_env1", "sample_env2"]; + type ConfigType = { + sample_env1: { + sample_key1: string; + }; + sample_env2: { + sample_key2: string; + }; + sample_shared: number; + }; + type Environments = "sample_env1" | "sample_env2"; } -export {}; +export type {}; diff --git a/src/ts_binding/multi_env.ts b/src/ts_binding/multi_env.ts index ccd1967..31179be 100644 --- a/src/ts_binding/multi_env.ts +++ b/src/ts_binding/multi_env.ts @@ -5,41 +5,41 @@ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"; // https://github.com/motdotla/dotenv/blob/master/lib/main.js // https://github.com/motdotla/dotenv/blob/master/LICENSE const LINE = - /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; + /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; function parse(src: string): Record { - const obj: Record = {}; + const obj: Record = {}; - // Convert line breaks to same format - const lines = src.replace(/\r\n?/gm, "\n"); - while (true) { - const match = LINE.exec(lines); - if (match === null) break; + // Convert line breaks to same format + const lines = src.replace(/\r\n?/gm, "\n"); + while (true) { + const match = LINE.exec(lines); + if (match === null) break; - const key = match[1]; + const key = match[1]; - // Default undefined or null to empty string - let value = match[2] || ""; + // Default undefined or null to empty string + let value = match[2] || ""; - // Remove whitespace - value = value.trim(); + // Remove whitespace + value = value.trim(); - // Check if double quoted - const maybeQuote = value[0]; + // Check if double quoted + const maybeQuote = value[0]; - // Remove surrounding quotes - value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); + // Remove surrounding quotes + value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2"); - // Expand newlines if double quoted - if (maybeQuote === '"') { - value = value.replace(/\\n/g, "\n"); - value = value.replace(/\\r/g, "\r"); - } + // Expand newlines if double quoted + if (maybeQuote === '"') { + value = value.replace(/\\n/g, "\n"); + value = value.replace(/\\r/g, "\r"); + } - // Add to object - obj[key] = value; - } + // Add to object + obj[key] = value; + } - return obj; + return obj; } /** @@ -50,74 +50,77 @@ function parse(src: string): Record { * @returns An array of absolute paths to `.env` files found. Ordered from closest to farthest. */ function findEnvFiles(startDir: string): string[] { - const envFiles: string[] = []; - const visitedDirs: Set = new Set(); - let currentDir = resolve(startDir); - - while (true) { - let realCurrentDir: string; - try { - realCurrentDir = realpathSync(currentDir); - } catch (err) { - console.warn( - `Warning: Unable to resolve real path for directory "${currentDir}". Skipping.`, - err, - ); - break; - } - - if (visitedDirs.has(realCurrentDir)) { - console.warn( - `Detected a symlink loop at "${realCurrentDir}". Stopping search to prevent infinite loop.`, - ); - break; - } - - visitedDirs.add(realCurrentDir); - - const envFilePath = join(currentDir, ".env"); - try { - const stat = statSync(envFilePath); - if (stat.isFile()) { - envFiles.push(envFilePath); - } - } catch (err) { - // If the file does not exist, ignore and continue - if ((err as NodeJS.ErrnoException).code !== "ENOENT") { - console.warn(`Warning: Unable to access "${envFilePath}".`, err); - } - } - - const parentDir = dirname(currentDir); - if (parentDir === currentDir) { - // Reached the root directory - break; - } - currentDir = parentDir; - } - - return envFiles; + const envFiles: string[] = []; + const visitedDirs: Set = new Set(); + let currentDir = resolve(startDir); + + while (true) { + let realCurrentDir: string; + try { + realCurrentDir = realpathSync(currentDir); + } catch (err) { + console.warn( + `Warning: Unable to resolve real path for directory "${currentDir}". Skipping.`, + err, + ); + break; + } + + if (visitedDirs.has(realCurrentDir)) { + console.warn( + `Detected a symlink loop at "${realCurrentDir}". Stopping search to prevent infinite loop.`, + ); + break; + } + + visitedDirs.add(realCurrentDir); + + const envFilePath = join(currentDir, ".env"); + try { + const stat = statSync(envFilePath); + if (stat.isFile()) { + envFiles.push(envFilePath); + } + } catch (err) { + // If the file does not exist, ignore and continue + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { + console.warn( + `Warning: Unable to access "${envFilePath}".`, + err, + ); + } + } + + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + // Reached the root directory + break; + } + currentDir = parentDir; + } + + return envFiles; } function load_env_variable( - dotenv_start_dir: string, - key: string, + dotenv_start_dir: string, + key: string, ): string | undefined { - // check if the key is already set - if (process.env[key]) { - return process.env[key]; - } - - // check if the key is in the .env file - const envFiles = findEnvFiles(dotenv_start_dir); - for (const envFile of envFiles) { - const file = readFileSync(envFile, "utf8"); - const env = parse(file); - if (env[key]) { - return env[key]; - } - } - return undefined; + // check if the key is already set + if (process.env[key]) { + return process.env[key]; + } + + // check if the key is in the .env file + const envFiles = findEnvFiles(dotenv_start_dir); + for (const envFile of envFiles) { + const file = readFileSync(envFile, "utf8"); + const env = parse(file); + if (env[key]) { + return env[key]; + } + } + return undefined; } // Get the current file URL and convert it to a path @@ -126,48 +129,58 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); function locate_config_file(): string | null { - const name = "config.json"; - const path = join(__dirname, name); - if (existsSync(path)) { - return path; - } - return null; + const name = "config.json"; + const path = join(__dirname, name); + if (existsSync(path)) { + return path; + } + return null; } function read_config_file(): ConfigType | null { - const path = locate_config_file(); - if (!path) { - return null; - } - const file = readFileSync(path, "utf8"); - return JSON.parse(file); + const path = locate_config_file(); + if (!path) { + return null; + } + const file = readFileSync(path, "utf8"); + return JSON.parse(file); } const config = read_config_file(); -const used_environment = load_env_variable(__dirname, "ENV"); +export const usedEnvironment = load_env_variable( + __dirname, + "ENV", +) as Environments; +if (!usedEnvironment) { + throw new Error( + `ENV is not set, please set ENV variable to one of ${environments.join(", ")}, you can pass this environment variable directly or in a .env file`, + ); +} +if (!environments.includes(usedEnvironment)) { + throw new Error( + `ENV must be one of ${environments.join(", ")} got ${usedEnvironment}`, + ); +} + +type ObjectKeys = { + [K in keyof T]: T[K] extends object ? K : never; +}[keyof T]; -export function env( - use_environment: T | undefined = used_environment as T | undefined, +export function env(): ConfigType & ConfigType[Environments]; +export function env>( + use_environment: T, +): ConfigType & ConfigType[T]; +export function env>( + use_environment = usedEnvironment as T, ): ConfigType & ConfigType[T] { - if (!use_environment) { - throw new Error( - `ENV is not set, please set ENV variable to one of ${environments.join( - ", ", - )}, you can pass this variable directly or in a .env file`, - ); - } - if (!environments.includes(use_environment)) { - throw new Error(`ENV must be one of ${environments.join(", ")}`); - } - - if (!config) { - throw new Error( - "Config file not found, if ./config.json does not exist, run weaveconfig, if it does this is likely a bundler issue with import.meta.url", - ); - } - - return { - ...config, - ...config[use_environment], - }; + if (!config) { + throw new Error( + "Config file not found, if ./config.json does not exist, run weaveconfig, if it does this is likely a bundler issue with import.meta.url", + ); + } + + return { + ...config, + ...config[use_environment], + }; } diff --git a/src/ts_binding/package.json b/src/ts_binding/package.json index 2e2d9ae..7d8543a 100644 --- a/src/ts_binding/package.json +++ b/src/ts_binding/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "devDependencies": { - "@types/bun": "latest" + "@types/node": "latest" }, "peerDependencies": { "typescript": "^5.0.0"