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 std::path::PathBuf;
5use std::time::Instant;
6
7#[derive(Args, Clone, Debug)]
8pub struct WorkspaceArgs {
9    /// Path to the crate or workspace root (defaults to current directory).
10    #[arg(short, long)]
11    pub path: Option<PathBuf>,
12    /// Package name to filter (if in a workspace, only process this package).
13    #[arg(short = 'P', long)]
14    pub package: Option<String>,
15}
16
17/// Common arguments for locale-based processing commands.
18///
19/// Used by format, check, and sync commands.
20#[derive(Args, Clone, Debug)]
21pub struct LocaleProcessingArgs {
22    /// Process all locales, not just the fallback language.
23    #[arg(long)]
24    pub all: bool,
25
26    /// Dry run - show what would change without making changes.
27    #[arg(long)]
28    pub dry_run: bool,
29}
30
31/// Represents a resolved set of crates for a command to operate on.
32#[derive(Clone, Debug)]
33pub struct WorkspaceCrates {
34    /// The user-supplied (or default) root path.
35    pub path: PathBuf,
36    /// Workspace information (root dir, target dir, all crates).
37    pub workspace_info: WorkspaceInfo,
38    /// All crates discovered (after optional package filtering).
39    pub crates: Vec<CrateInfo>,
40    /// Crates that are eligible for operations (contain `lib.rs`).
41    pub valid: Vec<CrateInfo>,
42    /// Crates that were skipped (missing `lib.rs`).
43    pub skipped: Vec<CrateInfo>,
44}
45
46impl WorkspaceCrates {
47    /// Discover crates for a command, applying the common filtering and partitioning logic.
48    pub fn discover(args: WorkspaceArgs) -> Result<Self, CliError> {
49        use crate::utils::discover_workspace;
50
51        let path = args.path.unwrap_or_else(|| PathBuf::from("."));
52        let workspace_info = discover_workspace(&path)?;
53        let crates = filter_crates_by_package(workspace_info.crates.clone(), args.package.as_ref());
54        let (valid_refs, skipped_refs) = partition_by_lib_rs(&crates);
55        let valid = valid_refs.into_iter().cloned().collect();
56        let skipped = skipped_refs.into_iter().cloned().collect();
57
58        Ok(Self {
59            path,
60            workspace_info,
61            crates,
62            valid,
63            skipped,
64        })
65    }
66
67    /// Print a standardized discovery summary, including skipped crates.
68    ///
69    /// Returns `false` when no crates were discovered to allow early-exit flows.
70    pub fn print_discovery(&self, header: impl Fn()) -> bool {
71        header();
72
73        if self.crates.is_empty() {
74            ui::print_discovered(&[]);
75            return false;
76        }
77
78        ui::print_discovered(&self.crates);
79
80        for krate in &self.skipped {
81            ui::print_missing_lib_rs(&krate.name);
82        }
83
84        true
85    }
86}
87
88/// Read the changed status from the runner crate's result.json file.
89///
90/// Returns `true` if the file indicates changes were made, `false` otherwise.
91/// Read the changed status from the runner crate's result.json file.
92///
93/// Returns `true` if the file indicates changes were made, `false` otherwise.
94fn read_changed_status(temp_dir: &std::path::Path, crate_name: &str) -> bool {
95    let result_json_path = temp_dir
96        .join("metadata")
97        .join(crate_name)
98        .join("result.json");
99
100    if !result_json_path.exists() {
101        return false;
102    }
103
104    match std::fs::read_to_string(&result_json_path) {
105        Ok(json_str) => match serde_json::from_str::<serde_json::Value>(&json_str) {
106            Ok(json) => json["changed"].as_bool().unwrap_or(false),
107            Err(_) => false,
108        },
109        Err(_) => false,
110    }
111}
112
113/// Run generation-like work using the monolithic temp crate approach.
114///
115/// This prepares a single temp crate at workspace root that links all workspace crates,
116/// then runs the binary for each crate. Much faster on subsequent runs.
117///
118/// If `force_run` is true, the staleness check is skipped and the runner is always rebuilt.
119pub fn parallel_generate(
120    workspace: &WorkspaceInfo,
121    crates: &[CrateInfo],
122    action: &GenerationAction,
123    force_run: bool,
124) -> Vec<GenerateResult> {
125    use crate::generation::{generate_for_crate_monolithic, prepare_monolithic_runner_crate};
126
127    // Prepare the monolithic temp crate once upfront
128    if let Err(e) = prepare_monolithic_runner_crate(workspace) {
129        // If preparation fails, return error results for all crates
130        return crates
131            .iter()
132            .map(|k| {
133                GenerateResult::failure(k.name.clone(), std::time::Duration::ZERO, e.to_string())
134            })
135            .collect();
136    }
137
138    let pb = ui::create_progress_bar(crates.len() as u64, "Processing crates...");
139
140    // Process sequentially since they share the same binary
141    // (parallel could cause contention on first build)
142    crates
143        .iter()
144        .map(|krate| {
145            let start = Instant::now();
146            let result = generate_for_crate_monolithic(krate, workspace, action, force_run);
147            let duration = start.elapsed();
148
149            pb.inc(1);
150
151            let resource_count = result
152                .as_ref()
153                .ok()
154                .map(|_| count_ftl_resources(&krate.ftl_output_dir, &krate.name))
155                .unwrap_or(0);
156
157            match result {
158                Ok(output) => {
159                    // For monolithic, result.json is at workspace root
160                    let temp_dir = workspace.root_dir.join(".es-fluent");
161                    let changed = read_changed_status(&temp_dir, &krate.name);
162
163                    let output_opt = if output.is_empty() {
164                        None
165                    } else {
166                        Some(output.to_string())
167                    };
168
169                    GenerateResult::success(
170                        krate.name.clone(),
171                        duration,
172                        resource_count,
173                        output_opt,
174                        changed,
175                    )
176                },
177                Err(e) => GenerateResult::failure(krate.name.clone(), duration, e.to_string()),
178            }
179        })
180        .collect()
181}
182
183/// Render a list of `GenerateResult`s with custom success/error handlers.
184///
185/// Returns `true` when any errors were encountered.
186pub fn render_generation_results(
187    results: &[GenerateResult],
188    on_success: impl Fn(&GenerateResult),
189    on_error: impl Fn(&GenerateResult),
190) -> bool {
191    let mut has_errors = false;
192
193    for result in results {
194        if result.error.is_some() {
195            has_errors = true;
196            on_error(result);
197        } else {
198            on_success(result);
199        }
200    }
201
202    has_errors
203}