vite_static_html/lib.rs
1#![warn(clippy::pedantic)]
2
3use std::borrow::Cow;
4
5use vite_static_shared::Manifest;
6
7// This type is used in documentation.
8#[allow(unused_imports)]
9use vite_static_shared::ManifestChunk;
10
11#[derive(Clone, Debug, Default)]
12struct HtmlLinks<'a> {
13 stylesheets: Vec<Cow<'a, str>>,
14 scripts: Vec<Cow<'a, str>>,
15 preloads: Vec<Cow<'a, str>>,
16}
17
18// TODO: add importmap.json support
19
20/// HTML integration.
21///
22/// Builds links, preloads for specified chunks.
23///
24/// ```rust
25/// // Create `HtmlIntegration` struct:
26/// let html = HtmlIntegration::new(MyViteStatic.boxed())
27/// // import all required chunks:
28/// .import("src/entry1.js")
29/// .import("src/entry2.js")
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>` for CSS and `<link>` preloads for dependencies.
36/// ```
37pub struct HtmlIntegration {
38 manifest: Box<dyn Manifest<'static>>,
39 links: HtmlLinks<'static>,
40}
41
42impl HtmlIntegration {
43 /// Create new [`HtmlIntegration`] builder.
44 ///
45 /// Takes boxed [`Manifest`] and returns [`HtmlIntegration`].
46 #[must_use]
47 pub fn new(manifest: Box<dyn Manifest<'static>>) -> Self {
48 Self {
49 manifest,
50 links: HtmlLinks::default(),
51 }
52 }
53
54 /// Internal function to recursively import chunks.
55 fn import_as(&mut self, chunk: &str, is_dependency: bool) {
56 let chunk = self
57 .manifest
58 .chunk_by_key(chunk)
59 .unwrap_or_else(|| panic!(r#"failed to find chunk "{chunk}""#));
60
61 for css in chunk.css.as_ref() {
62 if !self.links.stylesheets.contains(css) {
63 self.links.stylesheets.push(css.clone());
64 }
65 }
66
67 for import in chunk.imports.as_ref() {
68 let import_file = self
69 .manifest
70 .resolve_output(import)
71 .unwrap_or_else(|| panic!(r#"failed to find imported chunk "{import}""#));
72
73 if !self.links.preloads.contains(&import_file) {
74 self.import_as(import, true);
75 }
76 }
77
78 if is_dependency {
79 self.links.preloads.push(chunk.file.clone());
80 } else {
81 self.links.scripts.push(chunk.file.clone());
82 }
83 }
84
85 /// Import [`ManifestChunk`] by key (input filename).
86 ///
87 /// ```rust
88 /// .import("src/this-chunk.js")
89 /// .import("src/components/MyCoolComponent.tsx")
90 /// .import("src/or/some/styles.scss")
91 /// ```
92 ///
93 /// # Panics
94 ///
95 /// This function will panic if:
96 ///
97 /// - Failed to find chunk by specified key
98 /// - Failed to find dependency (AKA imported chunk)
99 #[must_use]
100 pub fn import(mut self, chunk: &str) -> Self {
101 self.import_as(chunk, false);
102 self
103 }
104
105 /// Builds HTML and returns array of lines.
106 ///
107 /// ```rust
108 /// let html_lines = HtmlIntegration::new(MyViteStatic.boxed())
109 /// .import("src/entry1.js")
110 /// .build_lines();
111 ///
112 /// assert_eq!(html_lines.len(), 2); // `src/entry1.js` script tag AND `src/shared.js` preload
113 /// ```
114 #[must_use]
115 pub fn build_lines(self) -> Vec<String> {
116 let links = self.links;
117 let mut html = Vec::new();
118
119 for style in links.stylesheets {
120 html.push(format!(r#"<link rel="stylesheet" href="/{style}" />"#));
121 }
122 for script in links.scripts {
123 html.push(format!(
124 r#"<script type="module" src="/{script}"></script>"#
125 ));
126 }
127 for preload in links.preloads {
128 html.push(format!(r#"<link rel="modulepreload" href="/{preload}" />"#));
129 }
130
131 html
132 }
133
134 /// Builds HTML and returns [`String`].
135 ///
136 /// ```rust
137 /// let html = HtmlIntegration::new(MyViteStatic.boxed())
138 /// .import("src/entry1.js")
139 /// .build();
140 /// ```
141 #[must_use]
142 pub fn build(self) -> String {
143 self.build_lines().join("\n")
144 }
145}
146
147impl Clone for HtmlIntegration {
148 fn clone(&self) -> Self {
149 Self {
150 manifest: self.manifest.boxed(),
151 links: self.links.clone(),
152 }
153 }
154}