Skip to main content

esbuild_metafile/
lib.rs

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                // Static files use the same extension as the input file, and no entry point
183                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}