glory_cli/compile/
front.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::Write;
4use std::sync::Arc;
5
6use brotli::CompressorWriter;
7use camino::{Utf8Path, Utf8PathBuf};
8use flate2::write::GzEncoder;
9use tokio::process::Child;
10use tokio::{process::Command, sync::broadcast, task::JoinHandle};
11use wasm_bindgen_cli_support::Bindgen;
12
13use super::ChangeSet;
14use crate::config::Project;
15use crate::ext::fs;
16use crate::ext::sync::{wait_interruptible, CommandResult};
17use crate::service::site::SiteFile;
18use crate::signal::{Interrupt, Outcome, Product};
19use crate::{
20    ext::{
21        anyhow::{Context, Result},
22        exe::Exe,
23    },
24    logger::GRAY,
25};
26
27pub async fn front(proj: &Arc<Project>, changes: &ChangeSet) -> JoinHandle<Result<Outcome<Product>>> {
28    let proj = proj.clone();
29    let changes = changes.clone();
30    tokio::spawn(async move {
31        if !changes.need_front_build() {
32            log::trace!("Front no changes to rebuild");
33            return Ok(Outcome::Success(Product::None));
34        }
35
36        fs::create_dir_all(&proj.site.root_relative_pkg_dir()).await?;
37
38        let (envs, line, process) = front_cargo_process("build", true, &proj)?;
39
40        match wait_interruptible("Cargo", process, Interrupt::subscribe_any()).await? {
41            CommandResult::Interrupted => return Ok(Outcome::Stopped),
42            CommandResult::Failure(_) => return Ok(Outcome::Failed),
43            _ => {}
44        }
45        log::debug!("Cargo envs: {}", GRAY.paint(envs));
46        log::info!("Cargo finished {}", GRAY.paint(line));
47
48        bindgen(&proj).await.dot()
49    })
50}
51
52pub fn front_cargo_process(cmd: &str, wasm: bool, proj: &Project) -> Result<(String, String, Child)> {
53    let mut command = Command::new("cargo");
54    let (envs, line) = build_cargo_front_cmd(cmd, wasm, proj, &mut command);
55    Ok((envs, line, command.spawn()?))
56}
57
58pub fn build_cargo_front_cmd(cmd: &str, wasm: bool, proj: &Project, command: &mut Command) -> (String, String) {
59    let mut args = vec![
60        cmd.to_string(),
61        format!("--package={}", proj.lib.name.as_str()),
62        // "--lib".to_string(),
63        "--target-dir=target/front".to_string(),
64    ];
65    if wasm {
66        args.push("--target=wasm32-unknown-unknown".to_string());
67    }
68
69    if !proj.lib.default_features {
70        args.push("--no-default-features".to_string());
71    }
72
73    if !proj.lib.features.is_empty() {
74        args.push(format!("--features={}", proj.lib.features.join(",")));
75    }
76
77    proj.lib.profile.add_to_args(&mut args);
78
79    let envs = proj.to_envs();
80
81    let envs_str = envs.iter().map(|(name, val)| format!("{name}={val}")).collect::<Vec<_>>().join(" ");
82
83    command.args(&args).envs(envs);
84    let line = format!("cargo {}", args.join(" "));
85    println!("Build front: {}", line);
86    (envs_str, line)
87}
88
89async fn bindgen(proj: &Project) -> Result<Outcome<Product>> {
90    let wasm_file = &proj.lib.wasm_file;
91    let interrupt = Interrupt::subscribe_any();
92
93    log::info!("Front compiling WASM");
94
95    // see:
96    // https://github.com/rustwasm/wasm-bindgen/blob/main/crates/cli-support/src/lib.rs#L95
97    // https://github.com/rustwasm/wasm-bindgen/blob/main/crates/cli/src/bin/wasm-bindgen.rs#L13
98    let mut bindgen = Bindgen::new().input_path(&wasm_file.source).web(true).dot()?.generate_output().dot()?;
99
100    bindgen.wasm_mut().emit_wasm_file(&wasm_file.dest).dot()?;
101    log::info!("Front wrote wasm to {:?}", wasm_file.dest.as_str());
102    if proj.release {
103        match optimize(&wasm_file.dest, interrupt).await.dot()? {
104            CommandResult::Interrupted => return Ok(Outcome::Stopped),
105            CommandResult::Failure(_) => return Ok(Outcome::Failed),
106            _ => {}
107        }
108    }
109
110    let data = fs::read(&wasm_file.dest).await?;
111    let br_file = File::create(format!("{}.br", wasm_file.dest.as_str()))?;
112    let mut br_writer = CompressorWriter::new(
113        br_file,
114        32 * 1024, // 32 KiB buffer
115        11,        // BROTLI_PARAM_QUALITY
116        22,        // BROTLI_PARAM_LGWIN
117    );
118    br_writer.write_all(&data)?;
119
120    let zstd_data = zstd::encode_all(&*data, 21)?;
121    let mut zstd_file = File::create(format!("{}.zst", wasm_file.dest.as_str()))?;
122    zstd_file.write_all(&zstd_data)?;
123
124    let gzip_file = File::create(format!("{}.gz", wasm_file.dest.as_str()))?;
125    let mut gzip_encoder = GzEncoder::new(gzip_file, flate2::Compression::best());
126    gzip_encoder.write_all(&data)?;
127
128    let mut js_changed = false;
129
130    js_changed |= write_snippets(proj, bindgen.snippets()).await?;
131
132    js_changed |= write_modules(proj, bindgen.local_modules()).await?;
133
134    let wasm_changed = proj.site.did_file_change(&proj.lib.wasm_file.as_site_file()).await.dot()?;
135    js_changed |= proj.site.updated_with(&proj.lib.js_file, bindgen.js().as_bytes()).await.dot()?;
136    log::info!("Front js changed: {js_changed}");
137    log::info!("Front wasm changed: {wasm_changed}");
138
139    if js_changed || wasm_changed {
140        Ok(Outcome::Success(Product::Front))
141    } else {
142        Ok(Outcome::Success(Product::None))
143    }
144}
145
146async fn optimize(file: &Utf8Path, interrupt: broadcast::Receiver<()>) -> Result<CommandResult<()>> {
147    let wasm_opt = Exe::WasmOpt.get().await.dot()?;
148
149    let args = [file.as_str(), "-Os", "-o", file.as_str()];
150    log::info!("Front optimizing wasm: {} {}", wasm_opt.display(), args.join(" "));
151    let process = Command::new(wasm_opt).args(args).spawn().context("Could not spawn command")?;
152    wait_interruptible("wasm-opt", process, interrupt).await
153}
154
155async fn write_snippets(proj: &Project, snippets: &HashMap<String, Vec<String>>) -> Result<bool> {
156    let mut js_changed = false;
157
158    // Provide inline JS files
159    for (identifier, list) in snippets.iter() {
160        for (i, js) in list.iter().enumerate() {
161            let name = format!("inline{}.js", i);
162            let site_path = Utf8PathBuf::from("snippets").join(identifier).join(name);
163            let file_path = proj.site.root_relative_pkg_dir().join(&site_path);
164
165            fs::create_dir_all(file_path.parent().unwrap()).await?;
166
167            let site_file = SiteFile {
168                dest: file_path,
169                site: site_path,
170            };
171
172            js_changed |= proj.site.updated_with(&site_file, js.as_bytes()).await?;
173        }
174    }
175    Ok(js_changed)
176}
177
178async fn write_modules(proj: &Project, modules: &HashMap<String, String>) -> Result<bool> {
179    let mut js_changed = false;
180    // Provide snippet files from JS snippets
181    for (path, js) in modules.iter() {
182        let site_path = Utf8PathBuf::from("snippets").join(path);
183        let file_path = proj.site.root_relative_pkg_dir().join(&site_path);
184
185        fs::create_dir_all(file_path.parent().unwrap()).await?;
186
187        let site_file = SiteFile {
188            dest: file_path,
189            site: site_path,
190        };
191
192        js_changed |= proj.site.updated_with(&site_file, js.as_bytes()).await?;
193    }
194    Ok(js_changed)
195}