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