prest_build/
pwa.rs

1use crate::*;
2use anyhow::Result;
3use std::{
4    env, format as f,
5    fs::{read_to_string, rename, write},
6    process::Command,
7    time::Instant,
8};
9use webmanifest::{DisplayMode, Icon, Manifest};
10
11pub struct PWAOptions<'a> {
12    pub listeners: Vec<(&'a str, &'a str)>,
13    pub name: String,
14    pub desc: String,
15    pub background: String,
16    pub theme: String,
17    pub start: String,
18    pub display: DisplayMode,
19    pub icons: Vec<Icon<'a>>,
20    pub debug_pwa: bool,
21}
22impl PWAOptions<'_> {
23    pub fn new() -> Self {
24        Self::default()
25    }
26}
27impl Default for PWAOptions<'_> {
28    fn default() -> Self {
29        Self {
30            listeners: vec![
31                (
32                    "install",
33                    "event.waitUntil(Promise.all([__wbg_init('/sw.wasm'), self.skipWaiting()]))",
34                ),
35                ("activate", "event.waitUntil(self.clients.claim())"),
36                ("fetch", "handle_fetch(self, event)"),
37            ],
38            name: std::env::var("CARGO_PKG_NAME").unwrap(),
39            desc: if let Ok(desc) = std::env::var("CARGO_PKG_DESCRIPTION") {
40                desc
41            } else {
42                "An installable web application".to_owned()
43            },
44            background: "#1e293b".to_owned(),
45            theme: "#a21caf".to_owned(),
46            start: "/".to_owned(),
47            display: DisplayMode::Standalone,
48            icons: vec![Icon::new("logo.png", "512x512")],
49            debug_pwa: false,
50        }
51    }
52}
53
54impl PWAOptions<'_> {
55    pub fn debug_pwa(mut self) -> Self {
56        self.debug_pwa = true;
57        self
58    }
59}
60
61const SW_TARGET: &str = "service-worker";
62
63static LOGO: &[u8] = include_bytes!("default-logo.png");
64
65static LISTENER_TEMPLATE: &str = "self.addEventListener('NAME', event => LISTENER);\n";
66
67pub fn build_pwa(opts: PWAOptions) -> Result<()> {
68    if env::var("SELF_PWA_BUILD").is_ok() || (!opts.debug_pwa && !is_pwa()) {
69        return Ok(());
70    }
71    let start = Instant::now();
72    let lib_name = &read_lib_name()?;
73    let target_dir = sw_target_dir();
74    let profile_dir = match cfg!(debug_assertions) {
75        true => "debug",
76        false => "release",
77    };
78    let profile_path = &f!("{target_dir}/wasm32-unknown-unknown/{profile_dir}");
79    let lib_path = &f!("{profile_path}/{lib_name}");
80
81    // build in a separate target dir to avoid build deadlock with the host
82    let mut cmd = Command::new("cargo");
83    cmd.env("SELF_PWA_BUILD", "true")
84        .arg("rustc")
85        .arg("--lib")
86        .args(["--crate-type", "cdylib"])
87        //.args(["--features", "traces html embed"])
88        .args(["--target", "wasm32-unknown-unknown"])
89        .args(["--target-dir", &target_dir]);
90
91    if !cfg!(debug_assertions) {
92        cmd.arg("--release");
93    }
94
95    assert!(cmd.status()?.success());
96
97    // generate bindings for the wasm binary
98    wasm_bindgen_cli_support::Bindgen::new()
99        .input_path(f!("{lib_path}.wasm"))
100        .web(true)?
101        .remove_name_section(cfg!(not(debug_assertions)))
102        .remove_producers_section(cfg!(not(debug_assertions)))
103        .keep_debug(cfg!(debug_assertions))
104        .omit_default_module_path(true)
105        .generate(profile_path)?;
106
107    // move the processed wasm binary into final dist
108    rename(&f!("{lib_path}_bg.wasm"), out_path("sw.wasm"))?;
109
110    // append event listeners and save js bindings
111    let mut js = read_to_string(&f!("{lib_path}.js"))?;
112    for listener in opts.listeners.iter() {
113        js += LISTENER_TEMPLATE
114            .replace("NAME", listener.0)
115            .replace("LISTENER", listener.1)
116            .as_str();
117    }
118    write(out_path("sw.js"), &js)?;
119
120    // compose .webmanifest with app metadata
121    write(out_path(".webmanifest"), gen_manifest(opts))?;
122
123    // at least one logo is required for PWA installability
124    write(out_path("logo.png"), LOGO)?;
125
126    println!(
127        "cargo:warning={}",
128        f!("composed PWA in {}ms", start.elapsed().as_millis())
129    );
130
131    Ok(())
132}
133
134fn gen_manifest(opts: PWAOptions) -> String {
135    let mut manifest = Manifest::builder(&opts.name)
136        .description(&opts.desc)
137        .bg_color(&opts.background)
138        .theme_color(&opts.theme)
139        .start_url(&opts.theme)
140        .display_mode(opts.display.clone());
141    for icon in &opts.icons {
142        manifest = manifest.icon(icon);
143    }
144    manifest.build().unwrap()
145}
146
147fn sw_target_dir() -> String {
148    if let Some(dir) = find_target_dir() {
149        dir + "/" + SW_TARGET
150    } else {
151        "target/".to_owned() + SW_TARGET
152    }
153}
154
155// SHOULD BE SYNCED WITH THE SAME FN IN ../lib.rs
156pub fn is_pwa() -> bool {
157    #[cfg(target_arch = "wasm32")]
158    return true;
159    #[cfg(not(target_arch = "wasm32"))]
160    {
161        #[cfg(debug_assertions)]
162        return std::env::var("PWA").map_or(false, |v| v == "debug");
163        #[cfg(not(debug_assertions))]
164        return std::env::var("PWA").map_or(true, |v| v == "release");
165    }
166}