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 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(["--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 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 rename(&f!("{lib_path}_bg.wasm"), out_path("sw.wasm"))?;
109
110 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 write(out_path(".webmanifest"), gen_manifest(opts))?;
122
123 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
155pub 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}