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.
117pub fn parallel_generate(
118    workspace: &WorkspaceInfo,
119    crates: &[CrateInfo],
120    action: &GenerationAction,
121) -> Vec<GenerateResult> {
122    use crate::generation::{generate_for_crate_monolithic, prepare_monolithic_runner_crate};
123
124    // Prepare the monolithic temp crate once upfront
125    if let Err(e) = prepare_monolithic_runner_crate(workspace) {
126        // If preparation fails, return error results for all crates
127        return crates
128            .iter()
129            .map(|k| {
130                GenerateResult::failure(k.name.clone(), std::time::Duration::ZERO, e.to_string())
131            })
132            .collect();
133    }
134
135    let pb = ui::create_progress_bar(crates.len() as u64, "Processing crates...");
136
137    // Process sequentially since they share the same binary
138    // (parallel could cause contention on first build)
139    crates
140        .iter()
141        .map(|krate| {
142            let start = Instant::now();
143            let result = generate_for_crate_monolithic(krate, workspace, action);
144            let duration = start.elapsed();
145
146            pb.inc(1);
147
148            let resource_count = result
149                .as_ref()
150                .ok()
151                .map(|_| count_ftl_resources(&krate.ftl_output_dir, &krate.name))
152                .unwrap_or(0);
153
154            match result {
155                Ok(output) => {
156                    // For monolithic, result.json is at workspace root
157                    let temp_dir = workspace.root_dir.join(".es-fluent");
158                    let changed = read_changed_status(&temp_dir, &krate.name);
159
160                    let output_opt = if output.is_empty() {
161                        None
162                    } else {
163                        Some(output.to_string())
164                    };
165
166                    GenerateResult::success(
167                        krate.name.clone(),
168                        duration,
169                        resource_count,
170                        output_opt,
171                        changed,
172                    )
173                },
174                Err(e) => GenerateResult::failure(krate.name.clone(), duration, e.to_string()),
175            }
176        })
177        .collect()
178}
179
180/// Render a list of `GenerateResult`s with custom success/error handlers.
181///
182/// Returns `true` when any errors were encountered.
183pub fn render_generation_results(
184    results: &[GenerateResult],
185    on_success: impl Fn(&GenerateResult),
186    on_error: impl Fn(&GenerateResult),
187) -> bool {
188    let mut has_errors = false;
189
190    for result in results {
191        if result.error.is_some() {
192            has_errors = true;
193            on_error(result);
194        } else {
195            on_success(result);
196        }
197    }
198
199    has_errors
200}