1pub mod asset;
2mod filesystem;
3pub mod filters;
4pub mod http_preloader;
5pub mod instance;
6pub mod path_renderer;
7pub mod preloadable_asset;
8pub mod renders_path;
9
10#[cfg(test)]
11mod test;
12
13use std::collections::HashMap;
14use std::collections::HashSet;
15use std::str::FromStr;
16
17use anyhow::Result;
18pub use http_preloader::HttpPreloader;
19use serde::Deserialize;
20
21#[derive(Deserialize)]
22struct EsbuildMetaFileLoader {
23 outputs: HashMap<String, Output>,
24}
25
26#[derive(Debug, Deserialize)]
27pub struct InputInOutput {}
28
29#[derive(Debug, Deserialize)]
30pub struct Output {
31 imports: Vec<Import>,
32 #[serde(rename = "cssBundle")]
33 css_bundle: Option<String>,
34 #[serde(rename = "entryPoint")]
35 entry_point: Option<String>,
36 #[serde(default)]
37 inputs: HashMap<String, InputInOutput>,
38}
39
40#[derive(Debug, Deserialize)]
41pub struct Import {
42 path: String,
43}
44
45#[derive(Debug, Default)]
46pub struct EsbuildMetaFile {
47 input_to_outputs: HashMap<String, Vec<String>>,
48 output_paths: HashSet<String>,
49 output_to_preloads: HashMap<String, Vec<String>>,
50 static_paths: HashMap<String, Vec<String>>,
51}
52
53impl EsbuildMetaFile {
54 pub fn find_static_paths_for_input(&self, input_path: &str) -> Option<Vec<String>> {
55 self.static_paths.get(input_path).cloned()
56 }
57
58 pub fn find_outputs_for_input(&self, input_path: &str) -> Option<Vec<String>> {
59 self.input_to_outputs.get(input_path).cloned()
60 }
61
62 pub fn get_output_paths(&self) -> HashSet<String> {
63 self.output_paths.clone()
64 }
65
66 pub fn get_preloads(&self, output_path: &str) -> Vec<String> {
67 self.output_to_preloads
68 .get(output_path)
69 .cloned()
70 .unwrap_or_default()
71 }
72
73 fn register_preloads_for_output<'preloads>(
74 metafile: &'preloads EsbuildMetaFileLoader,
75 outputs: &'preloads mut Vec<String>,
76 preloads: &'preloads mut Vec<String>,
77 remaining_outputs: &'preloads mut HashSet<String>,
78 output_path: &'preloads str,
79 ) -> Result<()> {
80 if let Some(output) = metafile.outputs.get(output_path) {
81 remaining_outputs.remove(output_path);
82
83 let output_path_str = output_path.to_string();
84
85 if !outputs.contains(&output_path_str) {
86 outputs.push(output_path_str);
87
88 Self::register_preloads_from_imports(
89 metafile,
90 outputs,
91 preloads,
92 remaining_outputs,
93 &output.imports,
94 )?;
95 }
96 }
97
98 Ok(())
99 }
100
101 fn register_preloads_from_imports<'preloads>(
102 metafile: &'preloads EsbuildMetaFileLoader,
103 outputs: &'preloads mut Vec<String>,
104 preloads: &'preloads mut Vec<String>,
105 remaining_outputs: &'preloads mut HashSet<String>,
106 imports: &'preloads [Import],
107 ) -> Result<()> {
108 for Import {
109 path,
110 } in imports
111 {
112 if !preloads.contains(path) {
113 remaining_outputs.remove(path);
114 preloads.push(path.clone());
115
116 Self::register_preloads_for_output(
117 metafile,
118 outputs,
119 preloads,
120 remaining_outputs,
121 path,
122 )?;
123 }
124 }
125
126 Ok(())
127 }
128}
129
130impl FromStr for EsbuildMetaFile {
131 type Err = anyhow::Error;
132
133 fn from_str(json: &str) -> Result<EsbuildMetaFile> {
134 let metafile: EsbuildMetaFileLoader = serde_json::from_str(json)?;
135 let mut input_to_outputs: HashMap<String, Vec<String>> = HashMap::new();
136 let mut output_to_preloads: HashMap<String, Vec<String>> = HashMap::new();
137 let mut static_paths: HashMap<String, Vec<String>> = HashMap::new();
138
139 let mut remaining_outputs: HashSet<String> = metafile
140 .outputs
141 .keys()
142 .filter(|path| !path.ends_with(".map"))
143 .cloned()
144 .collect();
145
146 for (
147 output_path,
148 Output {
149 imports,
150 css_bundle,
151 entry_point,
152 inputs,
153 },
154 ) in &metafile.outputs
155 {
156 if let Some(entry_point) = &entry_point {
157 remaining_outputs.remove(output_path);
158
159 let outputs = input_to_outputs.entry(entry_point.clone()).or_default();
160 let preloads = output_to_preloads.entry(output_path.clone()).or_default();
161
162 outputs.push(output_path.clone());
163
164 if let Some(css_bundle) = css_bundle {
165 Self::register_preloads_for_output(
166 &metafile,
167 outputs,
168 preloads,
169 &mut remaining_outputs,
170 css_bundle,
171 )?;
172 }
173
174 Self::register_preloads_from_imports(
175 &metafile,
176 outputs,
177 preloads,
178 &mut remaining_outputs,
179 imports,
180 )?;
181 } else {
182 for input_path in inputs.keys() {
184 remaining_outputs.remove(output_path);
185 static_paths
186 .entry(input_path.to_string())
187 .or_default()
188 .push(output_path.to_string());
189 }
190 }
191 }
192
193 if !remaining_outputs.is_empty() {
194 log::warn!("Some outputs were not processed: {remaining_outputs:?}");
195 }
196
197 Ok(Self {
198 input_to_outputs,
199 output_paths: metafile
200 .outputs
201 .keys()
202 .map(|key| key.to_string())
203 .collect::<HashSet<String>>(),
204 output_to_preloads,
205 static_paths,
206 })
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::test::get_metafile_basic;
214 use crate::test::get_metafile_fonts;
215 use crate::test::get_metafile_glb;
216 use crate::test::get_metafile_svg;
217
218 #[test]
219 fn test_get_output_paths() -> Result<()> {
220 let metafile = get_metafile_basic()?;
221 let outputs = metafile.get_output_paths();
222
223 assert_eq!(outputs.len(), 2);
224 assert!(outputs.contains("dist/main.css"));
225 assert!(outputs.contains("dist/main.js"));
226
227 Ok(())
228 }
229
230 #[test]
231 fn test_find_outputs_for_css_input() -> Result<()> {
232 let metafile = get_metafile_fonts()?;
233 let outputs = metafile
234 .find_outputs_for_input("resources/css/page-common.css")
235 .unwrap();
236
237 assert_eq!(outputs.len(), 2);
238 assert!(outputs.contains(&"static/page-common_DO3RNJ3I.css".to_string()));
239 assert!(outputs.contains(&"static/test_6D5OPEBZ.svg".to_string()));
240
241 Ok(())
242 }
243
244 #[test]
245 fn test_find_outputs_for_tsx_input() -> Result<()> {
246 let metafile = get_metafile_fonts()?;
247 let outputs = metafile
248 .find_outputs_for_input("resources/ts/controller_foo.tsx")
249 .unwrap();
250
251 assert_eq!(outputs.len(), 2);
252 assert!(outputs.contains(&"static/controller_foo_CTJMZK66.js".to_string()));
253 assert!(outputs.contains(&"static/controller_foo_CX2Z63ZH.css".to_string()));
254
255 Ok(())
256 }
257
258 #[test]
259 fn test_get_preloads_for_js() -> Result<()> {
260 let metafile = get_metafile_fonts()?;
261 let preloads = metafile.get_preloads("static/controller_foo_CTJMZK66.js");
262
263 assert_eq!(preloads.len(), 5);
264 assert!(preloads.contains(&"https://fonts/font1.woff2".to_string()));
265 assert!(preloads.contains(&"https://fonts/font3.woff2".to_string()));
266 assert!(preloads.contains(&"static/chunk-EMZKCXNJ.js".to_string()));
267 assert!(preloads.contains(&"static/chunk-PI4ZFSEL.js".to_string()));
268 assert!(preloads.contains(&"static/logo_XSTJPNLH.png".to_string()));
269
270 Ok(())
271 }
272
273 #[test]
274 fn test_get_preloads_for_css() -> Result<()> {
275 let metafile = get_metafile_fonts()?;
276 let preloads = metafile.get_preloads("static/page-common_DO3RNJ3I.css");
277
278 assert_eq!(preloads.len(), 3);
279 assert!(preloads.contains(&"https://fonts/font1.woff2".to_string()));
280 assert!(preloads.contains(&"https://fonts/font2.woff2".to_string()));
281 assert!(preloads.contains(&"static/test_6D5OPEBZ.svg".to_string()));
282
283 Ok(())
284 }
285
286 #[test]
287 fn test_get_file_path_for_glb() -> Result<()> {
288 let metafile = get_metafile_glb()?;
289 let outputs = metafile
290 .find_static_paths_for_input("resources/media/models/model.glb")
291 .unwrap();
292
293 assert_eq!(outputs.len(), 1);
294 assert!(outputs.contains(&"dist/model_123.glb".to_string()));
295
296 let preloads = metafile.get_preloads("dist/main.js");
297
298 println!("preloads: {preloads:?}");
299
300 assert_eq!(preloads.len(), 3);
301 assert!(preloads.contains(&"dist/chunk-ABC.js".to_string()));
302 assert!(preloads.contains(&"dist/chunk-DEF.js".to_string()));
303 assert!(preloads.contains(&"dist/model_123.glb".to_string()));
304
305 Ok(())
306 }
307
308 #[test]
309 fn test_get_file_path_for_svg() -> Result<()> {
310 let metafile = get_metafile_svg()?;
311 let outputs = metafile
312 .find_static_paths_for_input("resources/images/image.svg")
313 .unwrap();
314
315 assert_eq!(outputs.len(), 1);
316 assert!(outputs.contains(&"dist/image_123.svg".to_string()));
317
318 Ok(())
319 }
320}