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 mut tracker = ChangeTracker::new();
190 if let Err(e) = tracker.compute_changes_with_base(&build_info, ¤t_files, root_dir) {
191 if args.build_verbose {
192 warn!("Failed to compute changes: {}", e);
193 }
194 return false;
195 }
196
197 if tracker.has_changes() {
198 if args.build_verbose {
199 info!(
200 "Project has changes: {} changed, {} new, {} deleted",
201 tracker.changed_files().len(),
202 tracker.new_files().len(),
203 tracker.deleted_files().len()
204 );
205 }
206 return false;
207 }
208
209 if !are_referenced_projects_uptodate(project, &build_info, args) {
211 return false;
212 }
213
214 true
215}
216
217fn are_referenced_projects_uptodate(
220 project: &ResolvedProject,
221 build_info: &BuildInfo,
222 args: &CliArgs,
223) -> bool {
224 for reference in &project.resolved_references {
226 let project_dir = reference
227 .config_path
228 .parent()
229 .unwrap_or(reference.config_path.as_path());
230
231 let ref_build_info_path = project_dir.join("tsconfig.tsbuildinfo");
232
233 if !ref_build_info_path.exists() {
234 if args.build_verbose {
235 let project_name = reference
236 .config_path
237 .file_stem()
238 .and_then(|s| s.to_str())
239 .unwrap_or("unknown");
240 info!("Referenced project not built: {}", project_name);
241 }
242 return false;
243 }
244
245 match BuildInfo::load(&ref_build_info_path) {
246 Ok(Some(ref_build_info)) => {
247 if let Some(ref latest_dts) = ref_build_info.latest_changed_dts_file {
250 let dts_absolute_path = project_dir.join(latest_dts);
252
253 if let Ok(metadata) = std::fs::metadata(&dts_absolute_path)
255 && let Ok(dts_modified) = metadata.modified()
256 {
257 if let Ok(dts_secs) = dts_modified.duration_since(std::time::UNIX_EPOCH) {
259 let dts_timestamp = dts_secs.as_secs();
260
261 if dts_timestamp > build_info.build_time {
263 if args.build_verbose {
264 let project_name = reference
265 .config_path
266 .file_stem()
267 .and_then(|s| s.to_str())
268 .unwrap_or("unknown");
269 info!(
270 "Referenced project's .d.ts is newer: {} ({} > {})",
271 project_name, dts_timestamp, build_info.build_time
272 );
273 }
274 return false;
275 }
276 }
277 }
278 }
279 }
280 Ok(None) => {
281 if args.build_verbose {
282 let project_name = reference
283 .config_path
284 .file_stem()
285 .and_then(|s| s.to_str())
286 .unwrap_or("unknown");
287 info!("Referenced project has version mismatch: {}", project_name);
288 }
289 return false;
290 }
291 Err(e) => {
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 warn!("Failed to load BuildInfo for {}: {}", project_name, e);
299 }
300 return false;
301 }
302 }
303 }
304
305 true
306}
307
308fn get_build_info_path(project: &ResolvedProject) -> Option<PathBuf> {
310 use crate::incremental::default_build_info_path;
311
312 let out_dir = project.out_dir.as_deref();
314 Some(default_build_info_path(&project.config_path, out_dir))
315}
316
317fn find_tsconfig(dir: &Path) -> Option<PathBuf> {
319 let config = dir.join("tsconfig.json");
320 config.exists().then_some(config)
321}