Skip to main content

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}