1use serde::Deserialize;
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Clone, Deserialize, Default)]
10pub struct HuskConfig {
11 pub package: Option<PackageConfig>,
13 #[serde(default)]
15 pub build: BuildConfig,
16 #[serde(default)]
18 pub dts: Vec<DtsEntry>,
19 #[serde(rename = "dts.options", alias = "dts_options")]
21 pub dts_options: Option<DtsOptions>,
22 pub run: Option<RunConfig>,
24 pub watch: Option<WatchConfig>,
26}
27
28#[derive(Debug, Clone, Deserialize)]
30pub struct PackageConfig {
31 pub name: String,
33 pub version: String,
35 pub description: Option<String>,
37 pub authors: Option<Vec<String>>,
39 pub license: Option<String>,
41 pub repository: Option<String>,
43}
44
45#[derive(Debug, Clone, Deserialize, Default)]
47pub struct BuildConfig {
48 pub entry: Option<String>,
50 pub output: Option<String>,
52 pub target: Option<String>,
54 pub lib: Option<bool>,
56 pub emit_dts: Option<bool>,
58 pub source_maps: Option<bool>,
60 pub prelude: Option<bool>,
62}
63
64#[derive(Debug, Clone, Deserialize)]
66pub struct DtsEntry {
67 pub package: String,
69 pub types: Option<String>,
71 pub version: Option<String>,
73 pub output: String,
75 pub include: Option<Vec<String>>,
77 pub exclude: Option<Vec<String>>,
79}
80
81#[derive(Debug, Clone, Deserialize, Default)]
83pub struct DtsOptions {
84 pub output_dir: Option<String>,
86 pub auto_update: Option<bool>,
88 pub warn_level: Option<String>,
90}
91
92#[derive(Debug, Clone, Deserialize, Default)]
94pub struct RunConfig {
95 pub node_args: Option<Vec<String>>,
97 pub env: Option<HashMap<String, String>>,
99}
100
101#[derive(Debug, Clone, Deserialize, Default)]
103pub struct WatchConfig {
104 pub debounce_ms: Option<u64>,
106 pub ignore: Option<Vec<String>>,
108 pub run: Option<bool>,
110 pub exec: Option<String>,
112}
113
114impl HuskConfig {
115 pub fn load() -> Option<Self> {
118 Self::load_from_path(Path::new("husk.toml"))
119 }
120
121 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 pub fn parse(content: &str) -> Result<Self, toml::de::Error> {
132 toml::from_str(content)
133 }
134
135 pub fn load_or_default() -> Self {
137 Self::load().unwrap_or_default()
138 }
139}
140
141impl BuildConfig {
142 pub fn output_dir(&self) -> &str {
144 self.output.as_deref().unwrap_or("dist")
145 }
146
147 pub fn source_maps(&self) -> bool {
149 self.source_maps.unwrap_or(true)
150 }
151
152 pub fn prelude(&self) -> bool {
154 self.prelude.unwrap_or(true)
155 }
156
157 pub fn lib(&self) -> bool {
159 self.lib.unwrap_or(false)
160 }
161
162 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}