Skip to main content

husk_lang/
config.rs

1//! Husk project configuration (husk.toml) parsing and types.
2
3use serde::Deserialize;
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8/// Root configuration structure for husk.toml.
9#[derive(Debug, Clone, Deserialize, Default)]
10pub struct HuskConfig {
11    /// Package metadata.
12    pub package: Option<PackageConfig>,
13    /// Build configuration.
14    #[serde(default)]
15    pub build: BuildConfig,
16    /// DTS entries for TypeScript definition imports.
17    #[serde(default)]
18    pub dts: Vec<DtsEntry>,
19    /// Global DTS options.
20    #[serde(rename = "dts.options", alias = "dts_options")]
21    pub dts_options: Option<DtsOptions>,
22    /// Runtime configuration.
23    pub run: Option<RunConfig>,
24    /// Watch mode configuration.
25    pub watch: Option<WatchConfig>,
26}
27
28/// Package metadata section.
29#[derive(Debug, Clone, Deserialize)]
30pub struct PackageConfig {
31    /// Project name.
32    pub name: String,
33    /// Project version (semver).
34    pub version: String,
35    /// Project description.
36    pub description: Option<String>,
37    /// List of authors.
38    pub authors: Option<Vec<String>>,
39    /// License identifier.
40    pub license: Option<String>,
41    /// Repository URL.
42    pub repository: Option<String>,
43}
44
45/// Build configuration section.
46#[derive(Debug, Clone, Deserialize, Default)]
47pub struct BuildConfig {
48    /// Entry point file path.
49    pub entry: Option<String>,
50    /// Output directory.
51    pub output: Option<String>,
52    /// JavaScript target: "esm", "cjs", or "auto".
53    pub target: Option<String>,
54    /// Library mode (don't call main()).
55    pub lib: Option<bool>,
56    /// Emit TypeScript declaration file.
57    pub emit_dts: Option<bool>,
58    /// Generate source maps.
59    pub source_maps: Option<bool>,
60    /// Inject stdlib prelude (Option/Result types).
61    pub prelude: Option<bool>,
62}
63
64/// A single DTS dependency entry.
65#[derive(Debug, Clone, Deserialize)]
66pub struct DtsEntry {
67    /// NPM package name.
68    pub package: String,
69    /// TypeScript types package (e.g., "@types/express").
70    pub types: Option<String>,
71    /// Version constraint (semver).
72    pub version: Option<String>,
73    /// Output path for generated .hk file.
74    pub output: String,
75    /// Types to include (filter).
76    pub include: Option<Vec<String>>,
77    /// Types to exclude (filter).
78    pub exclude: Option<Vec<String>>,
79}
80
81/// Global options for DTS management.
82#[derive(Debug, Clone, Deserialize, Default)]
83pub struct DtsOptions {
84    /// Default output directory for generated .hk files.
85    pub output_dir: Option<String>,
86    /// Auto-update .hk files on build.
87    pub auto_update: Option<bool>,
88    /// Warning level: "all", "simplified", "none".
89    pub warn_level: Option<String>,
90}
91
92/// Runtime configuration for `huskc run`.
93#[derive(Debug, Clone, Deserialize, Default)]
94pub struct RunConfig {
95    /// Additional arguments to pass to Node.js.
96    pub node_args: Option<Vec<String>>,
97    /// Environment variables.
98    pub env: Option<HashMap<String, String>>,
99}
100
101/// Watch mode configuration.
102#[derive(Debug, Clone, Deserialize, Default)]
103pub struct WatchConfig {
104    /// Debounce delay in milliseconds.
105    pub debounce_ms: Option<u64>,
106    /// Glob patterns to ignore.
107    pub ignore: Option<Vec<String>>,
108    /// Run the compiled output after each change.
109    pub run: Option<bool>,
110    /// Custom command to execute after compilation.
111    pub exec: Option<String>,
112}
113
114impl HuskConfig {
115    /// Load configuration from husk.toml in the current directory.
116    /// Returns None if file doesn't exist.
117    pub fn load() -> Option<Self> {
118        Self::load_from_path(Path::new("husk.toml"))
119    }
120
121    /// Load configuration from a specific path.
122    pub fn load_from_path(path: &Path) -> Option<Self> {
123        if !path.exists() {
124            return None;
125        }
126        let content = fs::read_to_string(path).ok()?;
127        Self::parse(&content).ok()
128    }
129
130    /// Parse configuration from a TOML string.
131    pub fn parse(content: &str) -> Result<Self, toml::de::Error> {
132        toml::from_str(content)
133    }
134
135    /// Load configuration or return default if not found.
136    pub fn load_or_default() -> Self {
137        Self::load().unwrap_or_default()
138    }
139}
140
141impl BuildConfig {
142    /// Get the output directory, defaulting to "dist".
143    pub fn output_dir(&self) -> &str {
144        self.output.as_deref().unwrap_or("dist")
145    }
146
147    /// Get whether source maps should be generated (default: true).
148    pub fn source_maps(&self) -> bool {
149        self.source_maps.unwrap_or(true)
150    }
151
152    /// Get whether prelude should be injected (default: true).
153    pub fn prelude(&self) -> bool {
154        self.prelude.unwrap_or(true)
155    }
156
157    /// Get whether library mode is enabled (default: false).
158    pub fn lib(&self) -> bool {
159        self.lib.unwrap_or(false)
160    }
161
162    /// Get whether to emit .d.ts (default: false).
163    pub fn emit_dts(&self) -> bool {
164        self.emit_dts.unwrap_or(false)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_parse_minimal_config() {
174        let toml = r#"
175[package]
176name = "test-project"
177version = "0.1.0"
178"#;
179        let config = HuskConfig::parse(toml).unwrap();
180        assert_eq!(config.package.as_ref().unwrap().name, "test-project");
181        assert_eq!(config.package.as_ref().unwrap().version, "0.1.0");
182    }
183
184    #[test]
185    fn test_parse_build_config() {
186        let toml = r#"
187[build]
188entry = "src/main.hk"
189output = "out"
190target = "esm"
191lib = true
192emit_dts = true
193source_maps = false
194prelude = false
195"#;
196        let config = HuskConfig::parse(toml).unwrap();
197        assert_eq!(config.build.entry, Some("src/main.hk".to_string()));
198        assert_eq!(config.build.output_dir(), "out");
199        assert_eq!(config.build.target, Some("esm".to_string()));
200        assert!(config.build.lib());
201        assert!(config.build.emit_dts());
202        assert!(!config.build.source_maps());
203        assert!(!config.build.prelude());
204    }
205
206    #[test]
207    fn test_parse_dts_entries() {
208        let toml = r#"
209[[dts]]
210package = "express"
211types = "@types/express"
212version = "^5.0.0"
213output = "src/extern/express.hk"
214
215[[dts]]
216package = "better-sqlite3"
217types = "@types/better-sqlite3"
218output = "src/extern/sqlite.hk"
219include = ["Database", "Statement"]
220"#;
221        let config = HuskConfig::parse(toml).unwrap();
222        assert_eq!(config.dts.len(), 2);
223
224        let express = &config.dts[0];
225        assert_eq!(express.package, "express");
226        assert_eq!(express.types, Some("@types/express".to_string()));
227        assert_eq!(express.version, Some("^5.0.0".to_string()));
228        assert_eq!(express.output, "src/extern/express.hk");
229
230        let sqlite = &config.dts[1];
231        assert_eq!(sqlite.package, "better-sqlite3");
232        assert_eq!(
233            sqlite.include,
234            Some(vec!["Database".to_string(), "Statement".to_string()])
235        );
236    }
237
238    #[test]
239    fn test_parse_run_config() {
240        let toml = r#"
241[run]
242node_args = ["--enable-source-maps", "--experimental-modules"]
243"#;
244        let config = HuskConfig::parse(toml).unwrap();
245        let run = config.run.unwrap();
246        assert_eq!(
247            run.node_args,
248            Some(vec![
249                "--enable-source-maps".to_string(),
250                "--experimental-modules".to_string()
251            ])
252        );
253    }
254
255    #[test]
256    fn test_parse_watch_config() {
257        let toml = r#"
258[watch]
259debounce_ms = 300
260ignore = ["dist/**", "node_modules/**"]
261"#;
262        let config = HuskConfig::parse(toml).unwrap();
263        let watch = config.watch.unwrap();
264        assert_eq!(watch.debounce_ms, Some(300));
265        assert_eq!(
266            watch.ignore,
267            Some(vec!["dist/**".to_string(), "node_modules/**".to_string()])
268        );
269    }
270
271    #[test]
272    fn test_parse_empty_config() {
273        let config = HuskConfig::parse("").unwrap();
274        assert!(config.package.is_none());
275        assert!(config.build.entry.is_none());
276        assert!(config.dts.is_empty());
277    }
278
279    #[test]
280    fn test_build_config_defaults() {
281        let config = BuildConfig::default();
282        assert_eq!(config.output_dir(), "dist");
283        assert!(config.source_maps());
284        assert!(config.prelude());
285        assert!(!config.lib());
286        assert!(!config.emit_dts());
287    }
288
289    #[test]
290    fn test_full_config() {
291        let toml = r#"
292[package]
293name = "my-app"
294version = "1.0.0"
295description = "My awesome app"
296authors = ["Alice <alice@example.com>"]
297license = "MIT"
298
299[build]
300entry = "src/main.hk"
301output = "dist"
302target = "auto"
303source_maps = true
304
305[[dts]]
306package = "express"
307types = "@types/express"
308output = "src/extern/express.hk"
309
310[run]
311node_args = ["--enable-source-maps"]
312
313[watch]
314debounce_ms = 200
315"#;
316        let config = HuskConfig::parse(toml).unwrap();
317        assert_eq!(config.package.as_ref().unwrap().name, "my-app");
318        assert_eq!(config.build.entry, Some("src/main.hk".to_string()));
319        assert_eq!(config.dts.len(), 1);
320        assert!(config.run.is_some());
321        assert!(config.watch.is_some());
322    }
323}