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}