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}