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#[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: DynManifest<'static>,
39    links: HtmlLinks<'static>,
40}
41
42impl HtmlIntegration {
43    /// Create new [`HtmlIntegration`] builder.
44    ///
45    /// Takes [`DynManifest`] (boxed [`Manifest`]) and returns [`HtmlIntegration`].
46    #[must_use]
47    pub fn new(manifest: DynManifest<'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        let base = if self.manifest.base() == "/" {
120            ""
121        } else {
122            &self.manifest.base()
123        };
124
125        for style in links.stylesheets {
126            html.push(format!(
127                r#"<link rel="stylesheet" href="{base}/{style}" />"#
128            ));
129        }
130        for script in links.scripts {
131            html.push(format!(
132                r#"<script type="module" src="{base}/{script}"></script>"#
133            ));
134        }
135        for preload in links.preloads {
136            html.push(format!(
137                r#"<link rel="modulepreload" href="{base}/{preload}" />"#
138            ));
139        }
140
141        html
142    }
143
144    /// Builds HTML and returns [`String`].
145    ///
146    /// ```rust
147    /// let html = HtmlIntegration::new(MyViteStatic.boxed())
148    ///     .import("src/entry1.js")
149    ///     .build();
150    /// ```
151    #[must_use]
152    pub fn build(self) -> String {
153        self.build_lines().join("\n")
154    }
155}
156
157impl Clone for HtmlIntegration {
158    fn clone(&self) -> Self {
159        Self {
160            manifest: self.manifest.boxed(),
161            links: self.links.clone(),
162        }
163    }
164}