1use crate::core::{CliError, CrateInfo, GenerateResult, GenerationAction, WorkspaceInfo};
2use crate::utils::{count_ftl_resources, filter_crates_by_package, partition_by_lib_rs, ui};
3use clap::Args;
4use colored::Colorize as _;
5use std::path::PathBuf;
6use std::time::Instant;
7
8#[derive(Args, Clone, Debug)]
9pub struct WorkspaceArgs {
10 #[arg(short, long)]
12 pub path: Option<PathBuf>,
13 #[arg(short = 'P', long)]
15 pub package: Option<String>,
16}
17
18#[derive(Args, Clone, Debug)]
22pub struct LocaleProcessingArgs {
23 #[arg(long)]
25 pub all: bool,
26
27 #[arg(long)]
29 pub dry_run: bool,
30}
31
32#[derive(Clone, Debug)]
34pub struct WorkspaceCrates {
35 pub path: PathBuf,
37 pub workspace_info: WorkspaceInfo,
39 pub crates: Vec<CrateInfo>,
41 pub valid: Vec<CrateInfo>,
43 pub skipped: Vec<CrateInfo>,
45}
46
47impl WorkspaceCrates {
48 pub fn discover(args: WorkspaceArgs) -> Result<Self, CliError> {
50 use crate::utils::discover_workspace;
51
52 let path = args.path.unwrap_or_else(|| PathBuf::from("."));
53 let workspace_info = discover_workspace(&path)?;
54 let crates = filter_crates_by_package(workspace_info.crates.clone(), args.package.as_ref());
55 let (valid_refs, skipped_refs) = partition_by_lib_rs(&crates);
56 let valid = valid_refs.into_iter().cloned().collect();
57 let skipped = skipped_refs.into_iter().cloned().collect();
58
59 Ok(Self {
60 path,
61 workspace_info,
62 crates,
63 valid,
64 skipped,
65 })
66 }
67
68 pub fn print_discovery(&self, header: impl Fn()) -> bool {
72 header();
73
74 if self.crates.is_empty() {
75 ui::print_discovered(&[]);
76 return false;
77 }
78
79 ui::print_discovered(&self.crates);
80
81 for krate in &self.skipped {
82 ui::print_missing_lib_rs(&krate.name);
83 }
84
85 true
86 }
87}
88
89fn read_changed_status(temp_dir: &std::path::Path, crate_name: &str) -> bool {
93 let result_json_path = es_fluent_derive_core::get_metadata_result_path(temp_dir, crate_name);
94
95 if !result_json_path.exists() {
96 return false;
97 }
98
99 match std::fs::read_to_string(&result_json_path) {
100 Ok(json_str) => match serde_json::from_str::<serde_json::Value>(&json_str) {
101 Ok(json) => json["changed"].as_bool().unwrap_or(false),
102 Err(_) => false,
103 },
104 Err(_) => false,
105 }
106}
107
108pub fn parallel_generate(
115 workspace: &WorkspaceInfo,
116 crates: &[CrateInfo],
117 action: &GenerationAction,
118 force_run: bool,
119) -> Vec<GenerateResult> {
120 use crate::generation::{generate_for_crate_monolithic, prepare_monolithic_runner_crate};
121
122 if let Err(e) = prepare_monolithic_runner_crate(workspace) {
124 return crates
126 .iter()
127 .map(|k| {
128 GenerateResult::failure(k.name.clone(), std::time::Duration::ZERO, e.to_string())
129 })
130 .collect();
131 }
132
133 let pb = ui::create_progress_bar(crates.len() as u64, "Processing crates...");
134
135 crates
138 .iter()
139 .map(|krate| {
140 let start = Instant::now();
141 let result = generate_for_crate_monolithic(krate, workspace, action, force_run);
142 let duration = start.elapsed();
143
144 pb.inc(1);
145
146 let resource_count = result
147 .as_ref()
148 .ok()
149 .map(|_| count_ftl_resources(&krate.ftl_output_dir, &krate.name))
150 .unwrap_or(0);
151
152 match result {
153 Ok(output) => {
154 let temp_dir =
156 es_fluent_derive_core::get_es_fluent_temp_dir(&workspace.root_dir);
157 let changed = read_changed_status(&temp_dir, &krate.name);
158
159 let output_opt = if output.is_empty() {
160 None
161 } else {
162 Some(output.to_string())
163 };
164
165 GenerateResult::success(
166 krate.name.clone(),
167 duration,
168 resource_count,
169 output_opt,
170 changed,
171 )
172 },
173 Err(e) => GenerateResult::failure(krate.name.clone(), duration, e.to_string()),
174 }
175 })
176 .collect()
177}
178
179pub fn render_generation_results(
183 results: &[GenerateResult],
184 on_success: impl Fn(&GenerateResult),
185 on_error: impl Fn(&GenerateResult),
186) -> bool {
187 let mut has_errors = false;
188
189 for result in results {
190 if result.error.is_some() {
191 has_errors = true;
192 on_error(result);
193 } else {
194 on_success(result);
195 }
196 }
197
198 has_errors
199}
200
201#[derive(Clone, Copy, Debug)]
202pub enum GenerationVerb {
203 Generate,
204 Clean,
205}
206
207impl GenerationVerb {
208 fn dry_run_label(self) -> &'static str {
209 match self {
210 GenerationVerb::Generate => "would be generated in",
211 GenerationVerb::Clean => "would be cleaned in",
212 }
213 }
214
215 fn print_changed(self, result: &GenerateResult) {
216 match self {
217 GenerationVerb::Generate => {
218 ui::print_generated(&result.name, result.duration, result.resource_count);
219 },
220 GenerationVerb::Clean => {
221 ui::print_cleaned(&result.name, result.duration, result.resource_count);
222 },
223 }
224 }
225}
226
227pub fn render_generation_results_with_dry_run(
231 results: &[GenerateResult],
232 dry_run: bool,
233 verb: GenerationVerb,
234) -> bool {
235 render_generation_results(
236 results,
237 |result| {
238 if dry_run {
239 if let Some(output) = &result.output {
240 print!("{}", output);
241 } else if result.changed {
242 println!(
243 "{} {} ({} resources)",
244 format!("{} {}", result.name, verb.dry_run_label()).yellow(),
245 ui::format_duration(result.duration).green(),
246 result.resource_count.to_string().cyan()
247 );
248 } else {
249 println!("{} {}", "Unchanged:".dimmed(), result.name.bold());
250 }
251 } else if result.changed {
252 verb.print_changed(result);
253 } else {
254 println!("{} {}", "Unchanged:".dimmed(), result.name.bold());
255 }
256 },
257 |result| ui::print_generation_error(&result.name, result.error.as_ref().unwrap()),
258 )
259}