waterui_cli/toolchain/
web.rs1use crate::{
4 toolchain::{Host, Installation, Toolchain, ToolchainError, rust::SelectedToolchainTargets},
5 utils::CommandError,
6 web::PackageManager,
7};
8
9#[must_use]
12pub fn wasm32_target() -> SelectedToolchainTargets {
13 SelectedToolchainTargets::new(vec![String::from("wasm32-unknown-unknown")])
14}
15
16#[derive(Debug, Clone, Copy, Default)]
18pub struct WasmPack;
19
20#[derive(Debug, Clone, Copy, Default)]
22pub struct WasmPackInstallation;
23
24impl Toolchain for WasmPack {
25 type Installation = WasmPackInstallation;
26
27 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
28 if host.which("wasm-pack").await.is_ok() {
29 return Ok(());
30 }
31 if host.which("cargo").await.is_err() {
32 return Err(ToolchainError::unfixable(
33 "wasm-pack is not installed and cargo is not on PATH",
34 "Install Rust via rustup first (see the `rust` doctor item), then run `cargo install wasm-pack`.",
35 ));
36 }
37 Err(ToolchainError::Fixable(WasmPackInstallation))
38 }
39}
40
41impl Installation for WasmPackInstallation {
42 type Error = CommandError;
43
44 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
45 host.run("cargo", ["install", "--locked", "wasm-pack"])
46 .await
47 .map(|_| ())
48 }
49}
50
51#[derive(Debug, Clone, Copy)]
55pub struct PackageManagerToolchain(pub PackageManager);
56
57#[derive(Debug, Clone, Copy)]
60pub struct PackageManagerInstallation(pub PackageManager);
61
62impl Toolchain for PackageManagerToolchain {
63 type Installation = PackageManagerInstallation;
64
65 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
66 let package_manager = self.0;
67 if host.which(package_manager.binary()).await.is_ok() {
68 return Ok(());
69 }
70 match package_manager {
71 PackageManager::Npm => Err(ToolchainError::unfixable(
74 "npm is not installed",
75 package_manager.install_hint(),
76 )),
77 _ => Err(ToolchainError::fixable(PackageManagerInstallation(
78 package_manager,
79 ))),
80 }
81 }
82}
83
84impl Installation for PackageManagerInstallation {
85 type Error = eyre::Report;
86
87 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
88 match self.0 {
89 PackageManager::Yarn => host
91 .run("corepack", ["enable"])
92 .await
93 .map(|_| ())
94 .map_err(Into::into),
95 PackageManager::Npm => Err(eyre::eyre!(
96 "npm ships with Node.js; install Node.js from https://nodejs.org/"
97 )),
98 package_manager => {
99 #[cfg(unix)]
100 {
101 let script = match package_manager {
102 PackageManager::Bun => "curl -fsSL https://bun.sh/install | bash",
103 PackageManager::Pnpm => "curl -fsSL https://get.pnpm.io/install.sh | sh -",
104 _ => unreachable!(),
105 };
106 host.run("sh", ["-c", script])
107 .await
108 .map(|_| ())
109 .map_err(Into::into)
110 }
111 #[cfg(windows)]
112 {
113 let script = match package_manager {
114 PackageManager::Bun => "irm bun.sh/install.ps1 | iex",
115 PackageManager::Pnpm => "iwr https://get.pnpm.io/install.ps1 -useb | iex",
116 _ => unreachable!(),
117 };
118 host.run("powershell", ["-c", script])
119 .await
120 .map(|_| ())
121 .map_err(Into::into)
122 }
123 #[cfg(not(any(unix, windows)))]
124 {
125 Err(eyre::eyre!(
126 "no automatic installer for {} on this platform; run: {}",
127 package_manager.binary(),
128 package_manager.install_hint()
129 ))
130 }
131 }
132 }
133 }
134}
135
136pub type WebToolchain = (SelectedToolchainTargets, WasmPack);
138
139#[must_use]
141pub fn web_toolchain() -> WebToolchain {
142 (wasm32_target(), WasmPack)
143}
144
145#[cfg(test)]
146mod tests {
147 use super::{WasmPack, wasm32_target};
148 use crate::toolchain::testing::TestMachine;
149 use crate::toolchain::{Toolchain, ToolchainError};
150
151 fn machine_with_rustup() -> TestMachine {
153 let machine = TestMachine::new();
154 machine.install("rustup");
155 machine.respond(
156 "RUSTUP_ACTIVE_TOOLCHAIN",
157 "stable-aarch64-apple-darwin (default)",
158 );
159 machine
160 }
161
162 #[test]
163 fn wasm32_target_unfixable_without_rustup() {
164 let machine = TestMachine::new();
165 let host = machine.host(Vec::<(String, String)>::new());
166 let result = smol::block_on(wasm32_target().check(&host));
167 assert!(
168 matches!(result, Err(ToolchainError::Unfixable(_))),
169 "missing rustup must be unfixable: {result:?}"
170 );
171 }
172
173 #[test]
174 fn wasm32_target_fixable_when_not_installed() {
175 let machine = machine_with_rustup();
176 let host = machine.host([(
177 String::from("WATERUI_FAKE_RUSTUP_INSTALLED_TARGETS"),
178 String::from("aarch64-apple-darwin"),
179 )]);
180 let result = smol::block_on(wasm32_target().check(&host));
181 assert!(
182 matches!(result, Err(ToolchainError::Fixable(_))),
183 "absent wasm32 target must be fixable: {result:?}"
184 );
185 }
186
187 #[test]
188 fn wasm32_target_ok_when_installed() {
189 let machine = machine_with_rustup();
190 machine.respond(
192 "RUSTUP_INSTALLED_TARGETS",
193 &["aarch64-apple-darwin", "wasm32-unknown-unknown"].join("\n"),
194 );
195 let host = machine.host(Vec::<(String, String)>::new());
196 smol::block_on(wasm32_target().check(&host)).expect("installed wasm32 target must be ok");
197 }
198
199 #[test]
202 fn wasm32_target_checks_pinned_toolchain() {
203 let machine = machine_with_rustup();
204 machine.file(
205 "rust-toolchain.toml",
206 "[toolchain]\nchannel = \"nightly\"\n",
207 );
208 machine.respond(
209 "RUSTUP_ACTIVE_TOOLCHAIN",
210 "nightly-aarch64-apple-darwin (overridden by rust-toolchain.toml)",
211 );
212 machine.respond(
213 "RUSTUP_INSTALLED_TARGETS",
214 "aarch64-apple-darwin\nwasm32-unknown-unknown",
215 );
216 let host = machine.host(Vec::<(String, String)>::new());
217 smol::block_on(wasm32_target().check(&host))
218 .expect("installed wasm32 target on the pinned toolchain must be ok");
219 }
220
221 #[test]
222 fn wasm_pack_ok_when_on_path() {
223 let machine = TestMachine::new();
224 machine.install("wasm-pack");
225 let host = machine.host(Vec::<(String, String)>::new());
226 smol::block_on(WasmPack.check(&host)).expect("wasm-pack on PATH must be ok");
227 }
228
229 #[test]
230 fn wasm_pack_fixable_when_missing() {
231 let machine = TestMachine::new();
232 machine.install("cargo");
233 let host = machine.host(Vec::<(String, String)>::new());
234 let result = smol::block_on(WasmPack.check(&host));
235 assert!(
236 matches!(result, Err(ToolchainError::Fixable(_))),
237 "missing wasm-pack must be fixable via cargo install: {result:?}"
238 );
239 }
240
241 #[test]
242 fn wasm_pack_unfixable_without_cargo() {
243 let machine = TestMachine::new();
244 let host = machine.host(Vec::<(String, String)>::new());
245 let result = smol::block_on(WasmPack.check(&host));
246 assert!(
247 matches!(result, Err(ToolchainError::Unfixable(_))),
248 "missing wasm-pack without cargo must be manual: {result:?}"
249 );
250 }
251}