Skip to main content

vite_static_html/
lib.rs

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