Skip to main content

vite_static_html/
lib.rs

1#![warn(clippy::pedantic)]
2
3use std::borrow::Cow;
4
5use vite_static_shared::DynManifest;
6
7// These types are used in documentation.
8#[allow(unused_imports)]
9use vite_static_shared::{Manifest, ManifestChunk};
10
11// TODO: add importmap.json support
12
13/// HTML integration.
14///
15/// Builds links, preloads for specified chunks.
16///
17/// ```rust
18/// # use vite_static_shared::__tests::*;
19/// # use vite_static_html::*;
20/// #
21/// // Create `HtmlIntegration` struct:
22/// let html = HtmlIntegration::new(MyViteStatic.boxed())
23///     // add imports of chunks: (<script> tags)
24///     .import("src/main.tsx")
25///     // add preloads of assets (fonts, images, etc).
26///     // arguments: (preload's as="..." attribute, chunk input filename),
27///     .preload("image", "src/images/500GB_image.png")
28///     // add stylesheets
29///     .stylesheet("src/style.scss")
30///     // and then, you can build `String` of HTML (tags, joined by '\n')
31///     .build();
32///     // or you can build array of tags/lines
33///     // .build_lines();
34///
35/// // The result will contain `<script>` tags, `<link rel="stylesheet" ...>` for CSS and `<link rel="preload" ...>` preloads for dependencies.
36/// ```
37pub struct HtmlIntegration {
38    manifest: DynManifest<'static>,
39
40    stylesheets: Vec<Cow<'static, str>>,
41    scripts: Vec<Cow<'static, str>>,
42    preloads: Vec<(&'static str, Cow<'static, str>)>,
43}
44
45impl HtmlIntegration {
46    /// Create new [`HtmlIntegration`] builder.
47    ///
48    /// Takes [`DynManifest`] (boxed [`Manifest`]) and returns [`HtmlIntegration`].
49    #[must_use]
50    pub fn new(manifest: DynManifest<'static>) -> Self {
51        Self {
52            manifest,
53            stylesheets: Vec::new(),
54            scripts: Vec::new(),
55            preloads: Vec::new(),
56        }
57    }
58
59    /// Import ESM [`ManifestChunk`] by key (input filename).
60    ///
61    /// This function:
62    ///  - adds `<script type="module" ...>` for specified chunk;
63    ///  - adds module preloads for all script dependencies.
64    ///
65    /// ```ignore
66    /// .import("src/main.tsx")
67    /// .import("src/components/MyCoolComponent.tsx")
68    /// .import("src/or/some/styles.scss")
69    /// ```
70    ///
71    /// # Panics
72    ///
73    /// This function will panic if:
74    ///
75    ///   - Failed to find chunk by specified key
76    ///   - Failed to find dependency (AKA imported chunk)
77    #[must_use]
78    pub fn import(mut self, chunk: &str) -> Self {
79        self.import_as(chunk, false);
80        self
81    }
82
83    // TODO: add script() method to _just_ add script tag (e.g. UMD or whatever)
84
85    /// Preload [`ManifestChunk`] as type by key (input filename).
86    ///
87    /// ```ignore
88    /// .preload("font", "src/assets/CoolFancyFont.ttf")
89    /// .preload("image", "src/assets/500GB_image.png")
90    /// .preload("script", "src/importantScript.js")
91    /// ```
92    ///
93    /// # Panics
94    ///
95    /// This function will panic if specified chunk was not found.
96    #[must_use]
97    pub fn preload(mut self, as_filetype: &'static str, chunk: &str) -> Self {
98        let chunk = self
99            .manifest
100            .chunk_by_key(chunk)
101            .unwrap_or_else(|| panic!(r#"failed to find chunk "{chunk}""#));
102
103        self.preloads.push((as_filetype, chunk.file.clone()));
104
105        self
106    }
107
108    /// Import stylesheet by key (input filename).
109    ///
110    /// ```ignore
111    /// .stylesheet("src/style.css")
112    /// .stylesheet("src/style.scss")
113    /// .stylesheet("src/style.less")
114    /// .stylesheet("src/style.whatever")
115    /// ```
116    ///
117    /// # Panics
118    ///
119    /// This function will panic if specified chunk was not found.
120    #[must_use]
121    pub fn stylesheet(mut self, chunk: &str) -> Self {
122        let chunk = self
123            .manifest
124            .chunk_by_key(chunk)
125            .unwrap_or_else(|| panic!(r#"failed to find chunk "{chunk}""#));
126        self.stylesheets.push(chunk.file.clone());
127        self
128    }
129
130    /// Builds HTML and returns array of lines.
131    ///
132    /// ```rust
133    /// # use vite_static_shared::__tests::*;
134    /// # use vite_static_html::*;
135    /// #
136    /// let html_lines = HtmlIntegration::new(MyViteStatic.boxed())
137    ///     .import("src/main.tsx")
138    ///     .build_lines();
139    ///
140    /// assert_eq!(html_lines.len(), 3);
141    /// // `src/main.tsx` script tag, `src/shared.ts` preload and `src/style.scss` stylesheet.
142    /// ```
143    #[must_use]
144    pub fn build_lines(self) -> Vec<String> {
145        let mut html = Vec::new();
146
147        let base = if self.manifest.base() == "/" {
148            ""
149        } else {
150            &self.manifest.base()
151        };
152
153        for style in self.stylesheets {
154            html.push(format!(
155                r#"<link rel="stylesheet" href="{base}/{style}" />"#
156            ));
157        }
158        for script in self.scripts {
159            html.push(format!(
160                r#"<script type="module" src="{base}/{script}"></script>"#
161            ));
162        }
163        for (filetype, preload) in self.preloads {
164            html.push(format!(
165                r#"<link rel="{filetype}" href="{base}/{preload}" />"#
166            ));
167        }
168
169        html
170    }
171
172    /// Builds HTML and returns [`String`].
173    ///
174    /// ```rust
175    /// # use vite_static_shared::__tests::*;
176    /// # use vite_static_html::*;
177    /// #
178    /// let html = HtmlIntegration::new(MyViteStatic.boxed())
179    ///     .import("src/main.tsx")
180    ///     .build();
181    /// ```
182    #[must_use]
183    pub fn build(self) -> String {
184        self.build_lines().join("\n")
185    }
186}
187
188/// Internal functions for [`HtmlIntegration`].
189impl HtmlIntegration {
190    /// Internal import chunk function.
191    fn import_as(&mut self, chunk: &str, is_dependency: bool) {
192        let chunk = self
193            .manifest
194            .chunk_by_key(chunk)
195            .unwrap_or_else(|| panic!(r#"failed to find chunk "{chunk}""#));
196
197        for css in chunk.css.as_ref() {
198            if !self.stylesheets.contains(css) {
199                self.stylesheets.push(css.clone());
200            }
201        }
202
203        for import in chunk.imports.as_ref() {
204            let import_file = self
205                .manifest
206                .resolve_output(import)
207                .unwrap_or_else(|| panic!(r#"failed to find imported chunk "{import}""#));
208
209            if !self.preloads.contains(&("modulepreload", import_file)) {
210                self.import_as(import, true);
211            }
212        }
213
214        if is_dependency {
215            self.preloads.push(("modulepreload", chunk.file.clone()));
216        } else {
217            self.scripts.push(chunk.file.clone());
218        }
219    }
220}
221
222impl Clone for HtmlIntegration {
223    fn clone(&self) -> Self {
224        Self {
225            manifest: self.manifest.boxed(),
226            stylesheets: self.stylesheets.clone(),
227            scripts: self.scripts.clone(),
228            preloads: self.preloads.clone(),
229        }
230    }
231}