1use std::collections::BTreeSet;
63use std::fs;
64use std::io::Read;
65use std::path::{Path, PathBuf};
66
67use anyhow::{Context, bail};
68use flate2::read::GzDecoder;
69use hex_literal::hex;
70use sha2::{Digest, Sha256};
71use walkdir::WalkDir;
72
73struct NpmPackage {
74 name: &'static str,
75 version: &'static str,
76 digest: [u8; 32],
77 css_file: Option<&'static str>,
78 js_prod_file: &'static str,
79 js_dev_file: &'static str,
80 extra_file: Option<(&'static str, &'static str)>,
81}
82
83const NPM_PACKAGES: &[NpmPackage] = &[
84 NpmPackage {
85 name: "@hpcc-js/wasm",
86 version: "0.3.14",
87 digest: hex!("b1628f561790925e58d33dcc5552aa2d1e8316a14b8436999a3c9c86df7c514a"),
88 css_file: None,
89 js_prod_file: "dist/index.min.js",
90 js_dev_file: "dist/index.js",
91 extra_file: Some((
92 "dist/graphvizlib.wasm",
93 "js/vendor/@hpcc-js/graphvizlib.wasm",
94 )),
95 },
96 NpmPackage {
97 name: "@babel/standalone",
98 version: "7.23.3",
99 digest: hex!("0f7b0deb19f1d0d77ff5dd05205be5063a930f2a6a56a6df92a6ed1e0cb73440"),
100 css_file: None,
101 js_prod_file: "babel.min.js",
102 js_dev_file: "babel.js",
103 extra_file: None,
104 },
105 NpmPackage {
106 name: "d3",
107 version: "5.16.0",
108 digest: hex!("85aa224591310c3cdd8a2ab8d3f8421bb7b0035926190389e790497c5b1d0f0b"),
109 css_file: None,
110 js_prod_file: "dist/d3.min.js",
111 js_dev_file: "dist/d3.js",
112 extra_file: None,
113 },
114 NpmPackage {
115 name: "d3-flame-graph",
116 version: "3.1.1",
117 digest: hex!("603120d8f1badfde6155816585d8e4c494f9783ae8fd40a3974928df707b1889"),
118 css_file: Some("dist/d3-flamegraph.css"),
119 js_prod_file: "dist/d3-flamegraph.min.js",
120 js_dev_file: "dist/d3-flamegraph.js",
121 extra_file: None,
122 },
123 NpmPackage {
124 name: "pako",
125 version: "1.0.11",
126 digest: hex!("1243d3fd9710c9b4e04d9528db02bfa55a4055bebc24743628fdddf59c83fa95"),
127 css_file: None,
128 js_prod_file: "dist/pako.min.js",
129 js_dev_file: "dist/pako.js",
130 extra_file: None,
131 },
132 NpmPackage {
133 name: "react",
134 version: "16.14.0",
135 digest: hex!("2fd361cfad2e0f8df36b67a0fdd43bd8064822b077fc7d70a84388918c663089"),
136 css_file: None,
137 js_prod_file: "umd/react.production.min.js",
138 js_dev_file: "umd/react.development.js",
139 extra_file: None,
140 },
141 NpmPackage {
142 name: "react-dom",
143 version: "16.14.0",
144 digest: hex!("27f6addacabaa5e5b9aa36ef443d4e79a947f3245c7d6c77f310f9c9fc944e25"),
145 css_file: None,
146 js_prod_file: "umd/react-dom.production.min.js",
147 js_dev_file: "umd/react-dom.development.js",
148 extra_file: None,
149 },
150];
151
152const STATIC: &str = "src/http/static";
153const CSS_VENDOR: &str = "src/http/static/css/vendor";
154const JS_PROD_VENDOR: &str = "src/http/static/js/vendor";
155const JS_DEV_VENDOR: &str = "src/http/static-dev/js/vendor";
156
157impl NpmPackage {
158 fn css_path(&self) -> PathBuf {
159 Path::new(CSS_VENDOR).join(format!("{}.css", self.name))
160 }
161
162 fn js_prod_path(&self) -> PathBuf {
163 Path::new(JS_PROD_VENDOR).join(format!("{}.js", self.name))
164 }
165
166 fn js_dev_path(&self) -> PathBuf {
167 Path::new(JS_DEV_VENDOR).join(format!("{}.js", self.name))
168 }
169
170 fn extra_path(&self) -> PathBuf {
171 let dst = self.extra_file.map(|(_src, dst)| dst);
172 Path::new(STATIC).join(dst.unwrap_or(""))
173 }
174
175 fn compute_digest(&self) -> Result<Vec<u8>, anyhow::Error> {
176 let css_data = if self.css_file.is_some() {
177 fs::read(self.css_path())?
178 } else {
179 vec![]
180 };
181 let js_prod_data = fs::read(self.js_prod_path())?;
182 let js_dev_data = fs::read(self.js_dev_path())?;
183 let extra_data = if self.extra_file.is_some() {
184 fs::read(self.extra_path())?
185 } else {
186 vec![]
187 };
188 Ok(Sha256::new()
189 .chain_update(Sha256::digest(css_data))
190 .chain_update(Sha256::digest(js_prod_data))
191 .chain_update(Sha256::digest(js_dev_data))
192 .chain_update(Sha256::digest(extra_data))
193 .finalize()
194 .as_slice()
195 .into())
196 }
197}
198
199pub fn ensure(out_dir: Option<PathBuf>) -> Result<(), anyhow::Error> {
200 println!("ensuring all npm packages are up-to-date...");
201
202 let client = reqwest::blocking::Client::new();
203 for pkg in NPM_PACKAGES {
204 if pkg.compute_digest().ok().as_deref() == Some(&pkg.digest) {
205 println!("{} is up-to-date", pkg.name);
206 continue;
207 } else {
208 println!("{} needs updating...", pkg.name);
209 }
210
211 let url = format!(
212 "https://registry.npmjs.org/{}/-/{}-{}.tgz",
213 pkg.name,
214 pkg.name.split('/').next_back().unwrap(),
215 pkg.version,
216 );
217 let res = client
218 .get(url)
219 .send()
220 .and_then(|res| res.error_for_status())
221 .with_context(|| format!("downloading {}", pkg.name))?;
222 let mut archive = tar::Archive::new(GzDecoder::new(res));
223 for entry in archive.entries()? {
224 let mut entry = entry?;
225 let path = entry.path()?.strip_prefix("package")?.to_owned();
226 if let Some(css_file) = &pkg.css_file {
227 if path == Path::new(css_file) {
228 unpack_entry(&mut entry, &pkg.css_path())?;
229 }
230 }
231 if path == Path::new(pkg.js_prod_file) {
232 unpack_entry(&mut entry, &pkg.js_prod_path())?;
233 }
234 if path == Path::new(pkg.js_dev_file) {
235 unpack_entry(&mut entry, &pkg.js_dev_path())?;
236 }
237 if let Some((extra_src, _extra_dst)) = &pkg.extra_file {
238 if path == Path::new(extra_src) {
239 unpack_entry(&mut entry, &pkg.extra_path())?;
240 }
241 }
242 }
243
244 let digest = pkg
245 .compute_digest()
246 .with_context(|| format!("computing digest for {}", pkg.name))?;
247 if digest != pkg.digest {
248 bail!(
249 "npm package {} did not match expected digest
250expected: {}
251 actual: {}",
252 pkg.name,
253 hex::encode(pkg.digest),
254 hex::encode(digest),
255 );
256 }
257 }
258
259 let mut known_paths = BTreeSet::new();
263 for pkg in NPM_PACKAGES {
264 if pkg.css_file.is_some() {
265 known_paths.insert(pkg.css_path());
266 }
267 known_paths.insert(pkg.js_prod_path());
268 known_paths.insert(pkg.js_dev_path());
269 if pkg.extra_file.is_some() {
270 known_paths.insert(pkg.extra_path());
271 }
272 }
273 for dir in &[CSS_VENDOR, JS_PROD_VENDOR, JS_DEV_VENDOR] {
274 for entry in WalkDir::new(dir) {
275 let entry = entry?;
276 if entry.file_type().is_file() {
277 if !known_paths.contains(entry.path()) {
278 println!("removing stray vendor file {}", entry.path().display());
279 fs::remove_file(entry.path())?;
280 } else if let Some(out_dir) = &out_dir {
281 let dst_path = out_dir.join(entry.path());
282 println!(
283 "copying path to OUT_DIR, src {}, dst {}",
284 entry.path().display(),
285 dst_path.display(),
286 );
287 if let Some(parent) = dst_path.parent() {
288 fs::create_dir_all(parent)?;
289 }
290 fs::copy(entry.path(), dst_path)?;
291 }
292 }
293 }
294 }
295
296 Ok(())
297}
298
299fn unpack_entry<T>(entry: &mut tar::Entry<T>, target: &Path) -> Result<(), anyhow::Error>
300where
301 T: Read,
302{
303 if let Some(parent) = target.parent() {
304 fs::create_dir_all(parent)
305 .with_context(|| format!("creating directory {}", parent.display()))?;
306 }
307 entry.unpack(target)?;
308 Ok(())
309}