1use anyhow::{Context, Result};
5use std::path::{Path, PathBuf};
6use tracing::{info, warn};
7
8use crate::args::CliArgs;
9use crate::incremental::BuildInfo;
10use crate::project_refs::{ProjectReferenceGraph, ResolvedProject};
11use tsz::checker::diagnostics::DiagnosticCategory;
12
13pub fn build_solution(args: &CliArgs, cwd: &Path, _root_names: &[String]) -> Result<bool> {
21 let root_config = if let Some(project) = args.project.as_deref() {
23 cwd.join(project)
24 } else {
25 find_tsconfig(cwd)
27 .ok_or_else(|| anyhow::anyhow!("No tsconfig.json found in {}", cwd.display()))?
28 };
29
30 info!(
31 "Loading project reference graph from: {}",
32 root_config.display()
33 );
34
35 let graph = ProjectReferenceGraph::load(&root_config)
37 .context("Failed to load project reference graph")?;
38
39 let build_order = graph
41 .build_order()
42 .context("Failed to determine build order (circular dependencies?)")?;
43
44 info!("Build order: {} projects", build_order.len());
45
46 let mut all_success = true;
48 let mut all_diagnostics = Vec::new();
49
50 for project_id in build_order {
52 let project = graph
53 .get_project(project_id)
54 .ok_or_else(|| anyhow::anyhow!("Project not found: {project_id:?}"))?;
55
56 if !args.force && is_project_up_to_date(project, args) {
58 info!("✓ Project is up to date: {}", project.config_path.display());
59 continue;
60 }
61
62 info!("Building project: {}", project.config_path.display());
63
64 let result = crate::driver::compile_project(args, &project.root_dir, &project.config_path)
66 .with_context(|| {
67 format!("Failed to build project: {}", project.config_path.display())
68 })?;
69
70 if !result.diagnostics.is_empty() {
72 all_diagnostics.extend(result.diagnostics.clone());
73
74 let has_errors = result
76 .diagnostics
77 .iter()
78 .any(|d| d.category == DiagnosticCategory::Error);
79
80 if has_errors {
81 all_success = false;
82 warn!("✗ Project has errors: {}", project.config_path.display());
83
84 if !args.force {
86 for diag in &result.diagnostics {
88 warn!(" {:?}", diag);
89 }
90 return Ok(false);
91 }
92 } else {
93 info!(
94 "✓ Project built with warnings: {}",
95 project.config_path.display()
96 );
97 }
98 } else {
99 info!(
100 "✓ Project built successfully: {}",
101 project.config_path.display()
102 );
103 }
104 }
105
106 if !all_diagnostics.is_empty() {
108 warn!("\n=== Diagnostics ===");
109 for diag in &all_diagnostics {
110 warn!("{:?}", diag);
111 }
112 }
113
114 Ok(all_success)
115}
116
117pub fn is_project_up_to_date(project: &ResolvedProject, args: &CliArgs) -> bool {
120 use crate::fs::{FileDiscoveryOptions, discover_ts_files};
121 use crate::incremental::ChangeTracker;
122
123 let build_info_path = match get_build_info_path(project) {
125 Some(path) => path,
126 None => return false,
127 };
128
129 if !build_info_path.exists() {
130 if args.build_verbose {
131 info!("No .tsbuildinfo found at {}", build_info_path.display());
132 }
133 return false;
134 }
135
136 let build_info = match BuildInfo::load(&build_info_path) {
138 Ok(Some(info)) => info,
139 Ok(None) => {
140 if args.build_verbose {
141 info!("BuildInfo version mismatch, needs rebuild");
142 }
143 return false;
144 }
145 Err(e) => {
146 if args.build_verbose {
147 warn!(
148 "Failed to load BuildInfo from {}: {}",
149 build_info_path.display(),
150 e
151 );
152 }
153 return false;
154 }
155 };
156
157 let root_dir = &project.root_dir;
159
160 let discovery_options = FileDiscoveryOptions {
163 base_dir: root_dir.clone(),
164 files: Vec::new(),
165 include: None,
166 exclude: None,
167 out_dir: project.out_dir.clone(),
168 follow_links: false,
169 allow_js: false,
170 };
171
172 let current_files = match discover_ts_files(&discovery_options) {
173 Ok(files) => files,
174 Err(e) => {
175 if args.build_verbose {
176 warn!(
177 "Failed to discover source files in {}: {}",
178 root_dir.display(),
179 e
180 );
181 }
182 return false;
184 }
185 };
186
187 let _current_files_relative: Vec<PathBuf> = current_files
190 .iter()
191 .filter_map(|path| {
192 path.strip_prefix(root_dir)
193 .ok()
194 .map(std::path::Path::to_path_buf)
195 })
196 .collect();
197
198 let mut tracker = ChangeTracker::new();
201 if let Err(e) = tracker.compute_changes_with_base(&build_info, ¤t_files, root_dir) {
202 if args.build_verbose {
203 warn!("Failed to compute changes: {}", e);
204 }
205 return false;
206 }
207
208 if tracker.has_changes() {
209 if args.build_verbose {
210 info!(
211 "Project has changes: {} changed, {} new, {} deleted",
212 tracker.changed_files().len(),
213 tracker.new_files().len(),
214 tracker.deleted_files().len()
215 );
216 }
217 return false;
218 }
219
220 if !are_referenced_projects_uptodate(project, &build_info, args) {
222 return false;
223 }
224
225 true
226}
227
228fn are_referenced_projects_uptodate(
231 project: &ResolvedProject,
232 build_info: &BuildInfo,
233 args: &CliArgs,
234) -> bool {
235 for reference in &project.resolved_references {
237 let project_dir = reference
238 .config_path
239 .parent()
240 .unwrap_or(reference.config_path.as_path());
241
242 let ref_build_info_path = project_dir.join("tsconfig.tsbuildinfo");
243
244 if !ref_build_info_path.exists() {
245 if args.build_verbose {
246 let project_name = reference
247 .config_path
248 .file_stem()
249 .and_then(|s| s.to_str())
250 .unwrap_or("unknown");
251 info!("Referenced project not built: {}", project_name);
252 }
253 return false;
254 }
255
256 match BuildInfo::load(&ref_build_info_path) {
257 Ok(Some(ref_build_info)) => {
258 if let Some(ref latest_dts) = ref_build_info.latest_changed_dts_file {
261 let dts_absolute_path = project_dir.join(latest_dts);
263
264 if let Ok(metadata) = std::fs::metadata(&dts_absolute_path)
266 && let Ok(dts_modified) = metadata.modified()
267 {
268 if let Ok(dts_secs) = dts_modified.duration_since(std::time::UNIX_EPOCH) {
270 let dts_timestamp = dts_secs.as_secs();
271
272 if dts_timestamp > build_info.build_time {
274 if args.build_verbose {
275 let project_name = reference
276 .config_path
277 .file_stem()
278 .and_then(|s| s.to_str())
279 .unwrap_or("unknown");
280 info!(
281 "Referenced project's .d.ts is newer: {} ({} > {})",
282 project_name, dts_timestamp, build_info.build_time
283 );
284 }
285 return false;
286 }
287 }
288 }
289 }
290 }
291 Ok(None) => {
292 if args.build_verbose {
293 let project_name = reference
294 .config_path
295 .file_stem()
296 .and_then(|s| s.to_str())
297 .unwrap_or("unknown");
298 info!("Referenced project has version mismatch: {}", project_name);
299 }
300 return false;
301 }
302 Err(e) => {
303 if args.build_verbose {
304 let project_name = reference
305 .config_path
306 .file_stem()
307 .and_then(|s| s.to_str())
308 .unwrap_or("unknown");
309 warn!("Failed to load BuildInfo for {}: {}", project_name, e);
310 }
311 return false;
312 }
313 }
314 }
315
316 true
317}
318
319fn get_build_info_path(project: &ResolvedProject) -> Option<PathBuf> {
321 use crate::incremental::default_build_info_path;
322
323 let out_dir = project.out_dir.as_deref();
325 Some(default_build_info_path(&project.config_path, out_dir))
326}
327
328fn find_tsconfig(dir: &Path) -> Option<PathBuf> {
330 let config = dir.join("tsconfig.json");
331 config.exists().then_some(config)
332}