Skip to main content

es_fluent_cli/commands/
common.rs

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    /// Path to the crate or workspace root (defaults to current directory).
11    #[arg(short, long)]
12    pub path: Option<PathBuf>,
13    /// Package name to filter (if in a workspace, only process this package).
14    #[arg(short = 'P', long)]
15    pub package: Option<String>,
16}
17
18/// Common arguments for locale-based processing commands.
19///
20/// Used by format, check, and sync commands.
21#[derive(Args, Clone, Debug)]
22pub struct LocaleProcessingArgs {
23    /// Process all locales, not just the fallback language.
24    #[arg(long)]
25    pub all: bool,
26
27    /// Dry run - show what would change without making changes.
28    #[arg(long)]
29    pub dry_run: bool,
30}
31
32/// Represents a resolved set of crates for a command to operate on.
33#[derive(Clone, Debug)]
34pub struct WorkspaceCrates {
35    /// The user-supplied (or default) root path.
36    pub path: PathBuf,
37    /// Workspace information (root dir, target dir, all crates).
38    pub workspace_info: WorkspaceInfo,
39    /// All crates discovered (after optional package filtering).
40    pub crates: Vec<CrateInfo>,
41    /// Crates that are eligible for operations (contain `lib.rs`).
42    pub valid: Vec<CrateInfo>,
43    /// Crates that were skipped (missing `lib.rs`).
44    pub skipped: Vec<CrateInfo>,
45}
46
47impl WorkspaceCrates {
48    /// Discover crates for a command, applying the common filtering and partitioning logic.
49    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    /// Print a standardized discovery summary, including skipped crates.
69    ///
70    /// Returns `false` when no crates were discovered to allow early-exit flows.
71    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
89/// Read the changed status from the runner crate's result.json file.
90///
91/// Returns `true` if the file indicates changes were made, `false` otherwise.
92fn 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
108/// Run generation-like work using the monolithic temp crate approach.
109///
110/// This prepares a single temp crate at workspace root that links all workspace crates,
111/// then runs the binary for each crate. Much faster on subsequent runs.
112///
113/// If `force_run` is true, the staleness check is skipped and the runner is always rebuilt.
114pub 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    // Prepare the monolithic temp crate once upfront
123    if let Err(e) = prepare_monolithic_runner_crate(workspace) {
124        // If preparation fails, return error results for all crates
125        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    // Process sequentially since they share the same binary
136    // (parallel could cause contention on first build)
137    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                    // For monolithic, result.json is at workspace root
155                    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
179/// Render a list of `GenerateResult`s with custom success/error handlers.
180///
181/// Returns `true` when any errors were encountered.
182pub 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
227/// Render generation-like results with the standard dry-run output.
228///
229/// Returns `true` when any errors were encountered.
230pub 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}