fob_cli/commands/
check.rs1use crate::cli::CheckArgs;
6use crate::commands::utils;
7use crate::config::{FobConfig, Format};
8use crate::error::{ConfigError, Result};
9use crate::ui;
10use std::path::Path;
11
12pub async fn execute(args: CheckArgs) -> Result<()> {
30 ui::info("Checking configuration...");
31
32 let config_path = args.config.as_deref();
34 let config_content = if let Some(path) = config_path {
35 std::fs::read_to_string(path).map_err(|_| ConfigError::NotFound(path.to_path_buf()))?
36 } else {
37 let default_path = Path::new("fob.config.json");
38 if default_path.exists() {
39 std::fs::read_to_string(default_path)
40 .map_err(|_| ConfigError::NotFound(default_path.to_path_buf()))?
41 } else {
42 ui::warning("No fob.config.json found, using defaults");
43 return Ok(());
44 }
45 };
46
47 let config: FobConfig = serde_json::from_str(&config_content)?;
49 config.validate()?;
50
51 ui::success("Configuration is valid!");
52
53 ui::info("Checking entry points...");
55 let cwd = if let Some(ref cwd_path) = config.cwd {
56 cwd_path.clone()
57 } else {
58 utils::get_cwd()?
59 };
60
61 for entry in &config.entry {
62 let entry_path = utils::resolve_path(Path::new(entry), &cwd);
63 if !entry_path.exists() {
64 ui::error(&format!("Entry point not found: {}", entry_path.display()));
65 return Err(ConfigError::MissingField {
66 field: "entry".to_string(),
67 hint: format!("File does not exist: {}", entry_path.display()),
68 }
69 .into());
70 }
71 ui::success(&format!(" {} exists", entry));
72 }
73
74 validate_options(&config)?;
76
77 if args.deps {
79 ui::info("Checking dependencies...");
80 check_dependencies(&cwd)?;
81 }
82
83 if args.warnings {
85 ui::info("Checking for warnings...");
86 check_warnings(&config);
87 }
88
89 ui::success("All checks passed!");
90 Ok(())
91}
92
93fn validate_options(config: &FobConfig) -> Result<()> {
95 if config.format == Format::Iife && config.global_name.is_none() {
97 return Err(ConfigError::MissingField {
98 field: "globalName".to_string(),
99 hint: "IIFE format requires a global variable name".to_string(),
100 }
101 .into());
102 }
103
104 if config.splitting && config.format != Format::Esm {
106 return Err(ConfigError::ConflictingOptions(
107 "Code splitting requires ESM format".to_string(),
108 )
109 .into());
110 }
111
112 if config.dts_bundle == Some(true) && !config.dts {
114 return Err(ConfigError::InvalidValue {
115 field: "dtsBundle".to_string(),
116 value: "true".to_string(),
117 hint: "Requires dts: true".to_string(),
118 }
119 .into());
120 }
121
122 Ok(())
123}
124
125fn check_dependencies(cwd: &Path) -> Result<()> {
127 let package_json_path = cwd.join("package.json");
128
129 if !package_json_path.exists() {
130 ui::warning("No package.json found");
131 return Ok(());
132 }
133
134 let package_json_content = std::fs::read_to_string(&package_json_path)?;
135 let package_json: serde_json::Value = serde_json::from_str(&package_json_content)?;
136
137 if let Some(deps) = package_json.get("dependencies") {
139 if let Some(obj) = deps.as_object() {
140 ui::info(&format!("Found {} dependencies", obj.len()));
141 }
142 }
143
144 if let Some(dev_deps) = package_json.get("devDependencies") {
145 if let Some(obj) = dev_deps.as_object() {
146 ui::info(&format!("Found {} dev dependencies", obj.len()));
147 }
148 }
149
150 ui::success("Dependencies look good");
151 Ok(())
152}
153
154fn check_warnings(config: &FobConfig) {
156 let mut warnings = Vec::new();
157
158 if config.format == Format::Iife && config.global_name.is_none() {
160 warnings.push("IIFE format should have a globalName");
161 }
162
163 if config.splitting && config.format != Format::Esm {
165 warnings.push("Code splitting works best with ESM format");
166 }
167
168 if config.dts
170 && !config
171 .entry
172 .iter()
173 .any(|e| e.ends_with(".ts") || e.ends_with(".tsx"))
174 {
175 warnings.push("DTS generation enabled but no TypeScript entry points found");
176 }
177
178 if !config.external.is_empty() {
180 warnings.push("Ensure external packages are listed in package.json dependencies");
181 }
182
183 if !config.minify {
185 warnings.push("Consider enabling minification for production builds");
186 }
187
188 if config.sourcemap.is_none() {
190 warnings.push("Consider enabling source maps for better debugging");
191 }
192
193 if warnings.is_empty() {
194 ui::info("No warnings found");
195 } else {
196 ui::warning(&format!("Found {} potential issues:", warnings.len()));
197 for warning in warnings {
198 ui::warning(&format!(" - {}", warning));
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::config::{EsTarget, Platform};
207 use std::path::PathBuf;
208
209 fn test_config() -> FobConfig {
210 FobConfig {
211 entry: vec!["src/index.ts".to_string()],
212 format: Format::Esm,
213 out_dir: PathBuf::from("dist"),
214 bundle: true,
215 dts: false,
216 dts_bundle: None,
217 external: vec![],
218 platform: Platform::Browser,
219 sourcemap: None,
220 minify: false,
221 target: EsTarget::Es2020,
222 global_name: None,
223 splitting: false,
224 no_treeshake: false,
225 clean: false,
226 cwd: None,
227 }
228 }
229
230 #[test]
231 fn test_validate_options_valid_esm() {
232 let config = test_config();
233 assert!(validate_options(&config).is_ok());
234 }
235
236 #[test]
237 fn test_validate_options_iife_without_global_name() {
238 let mut config = test_config();
239 config.format = Format::Iife;
240 config.global_name = None;
241
242 assert!(validate_options(&config).is_err());
243 }
244
245 #[test]
246 fn test_validate_options_iife_with_global_name() {
247 let mut config = test_config();
248 config.format = Format::Iife;
249 config.global_name = Some("MyLib".to_string());
250
251 assert!(validate_options(&config).is_ok());
252 }
253
254 #[test]
255 fn test_validate_options_splitting_with_cjs() {
256 let mut config = test_config();
257 config.format = Format::Cjs;
258 config.splitting = true;
259
260 assert!(validate_options(&config).is_err());
261 }
262
263 #[test]
264 fn test_validate_options_splitting_with_esm() {
265 let mut config = test_config();
266 config.format = Format::Esm;
267 config.splitting = true;
268
269 assert!(validate_options(&config).is_ok());
270 }
271
272 #[test]
273 fn test_validate_options_dts_bundle_without_dts() {
274 let mut config = test_config();
275 config.dts = false;
276 config.dts_bundle = Some(true);
277
278 assert!(validate_options(&config).is_err());
279 }
280
281 #[test]
282 fn test_validate_options_dts_bundle_with_dts() {
283 let mut config = test_config();
284 config.dts = true;
285 config.dts_bundle = Some(true);
286
287 assert!(validate_options(&config).is_ok());
288 }
289
290 #[test]
291 fn test_check_warnings_generates_warnings() {
292 let mut config = test_config();
293 config.dts = true; check_warnings(&config);
297 }
298}