1use std::collections::{BTreeSet, HashMap};
8
9use camino::{Utf8Path, Utf8PathBuf};
10use thiserror::Error;
11
12#[derive(serde::Deserialize, Debug, Clone)]
13#[serde(rename_all = "camelCase", deny_unknown_fields)]
14pub struct ManifestEntry {
15 #[allow(dead_code)]
16 name: Option<String>,
17
18 #[allow(dead_code)]
19 src: Option<Utf8PathBuf>,
20
21 file: Utf8PathBuf,
22
23 css: Option<Vec<Utf8PathBuf>>,
24
25 assets: Option<Vec<Utf8PathBuf>>,
26
27 #[allow(dead_code)]
28 is_entry: Option<bool>,
29
30 #[allow(dead_code)]
31 is_dynamic_entry: Option<bool>,
32
33 imports: Option<Vec<Utf8PathBuf>>,
34
35 #[allow(dead_code)]
36 dynamic_imports: Option<Vec<Utf8PathBuf>>,
37
38 integrity: Option<String>,
39}
40
41#[derive(serde::Deserialize, Debug, Clone)]
42pub struct Manifest {
43 #[serde(flatten)]
44 inner: HashMap<Utf8PathBuf, ManifestEntry>,
45}
46
47#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
48enum FileType {
49 Script,
50 Stylesheet,
51 Woff,
52 Woff2,
53 Json,
54 Png,
55}
56
57impl FileType {
58 fn from_name(name: &Utf8Path) -> Option<Self> {
59 match name.extension() {
60 Some("css") => Some(Self::Stylesheet),
61 Some("js") => Some(Self::Script),
62 Some("woff") => Some(Self::Woff),
63 Some("woff2") => Some(Self::Woff2),
64 Some("json") => Some(Self::Json),
65 Some("png") => Some(Self::Png),
66 _ => None,
67 }
68 }
69}
70
71#[derive(Debug, Error)]
72#[error("Invalid Vite manifest")]
73pub enum InvalidManifest<'a> {
74 #[error("Can't find asset for name {name:?}")]
75 CantFindAssetByName { name: &'a Utf8Path },
76
77 #[error("Can't find asset for file {file:?}")]
78 CantFindAssetByFile { file: &'a Utf8Path },
79
80 #[error("Invalid file type")]
81 InvalidFileType,
82}
83
84#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
86pub struct Asset<'a> {
87 file_type: FileType,
88 name: &'a Utf8Path,
89 integrity: Option<&'a str>,
90}
91
92impl<'a> Asset<'a> {
93 fn new(entry: &'a ManifestEntry) -> Result<Self, InvalidManifest<'a>> {
94 let name = &entry.file;
95 let integrity = entry.integrity.as_deref();
96 let file_type = FileType::from_name(name).ok_or(InvalidManifest::InvalidFileType)?;
97 Ok(Self {
98 file_type,
99 name,
100 integrity,
101 })
102 }
103
104 fn src(&self, assets_base: &Utf8Path) -> Utf8PathBuf {
105 assets_base.join(self.name)
106 }
107
108 pub fn preload_tag(&self, assets_base: &Utf8Path) -> String {
110 let href = self.src(assets_base);
111 let integrity = self
112 .integrity
113 .map(|i| format!(r#"integrity="{i}" "#))
114 .unwrap_or_default();
115 match self.file_type {
116 FileType::Stylesheet => {
117 format!(r#"<link rel="preload" href="{href}" as="style" crossorigin {integrity}/>"#)
118 }
119 FileType::Script => {
120 format!(r#"<link rel="modulepreload" href="{href}" crossorigin {integrity}/>"#)
121 }
122 FileType::Woff | FileType::Woff2 => {
123 format!(r#"<link rel="preload" href="{href}" as="font" crossorigin {integrity}/>"#,)
124 }
125 FileType::Json => {
126 format!(r#"<link rel="preload" href="{href}" as="fetch" crossorigin {integrity}/>"#,)
127 }
128 FileType::Png => {
129 format!(r#"<link rel="preload" href="{href}" as="image" crossorigin {integrity}/>"#,)
130 }
131 }
132 }
133
134 pub fn include_tag(&self, assets_base: &Utf8Path) -> Option<String> {
136 let src = self.src(assets_base);
137 let integrity = self
138 .integrity
139 .map(|i| format!(r#"integrity="{i}" "#))
140 .unwrap_or_default();
141
142 match self.file_type {
143 FileType::Stylesheet => Some(format!(
144 r#"<link rel="stylesheet" href="{src}" crossorigin {integrity}/>"#
145 )),
146 FileType::Script => Some(format!(
147 r#"<script type="module" src="{src}" crossorigin {integrity}></script>"#
148 )),
149 FileType::Woff | FileType::Woff2 | FileType::Json | FileType::Png => None,
150 }
151 }
152
153 #[must_use]
155 pub fn is_script(&self) -> bool {
156 self.file_type == FileType::Script
157 }
158
159 #[must_use]
161 pub fn is_stylesheet(&self) -> bool {
162 self.file_type == FileType::Stylesheet
163 }
164
165 #[must_use]
167 pub fn is_json(&self) -> bool {
168 self.file_type == FileType::Json
169 }
170
171 #[must_use]
173 pub fn is_font(&self) -> bool {
174 self.file_type == FileType::Woff || self.file_type == FileType::Woff2
175 }
176
177 #[must_use]
179 pub fn is_image(&self) -> bool {
180 self.file_type == FileType::Png
181 }
182}
183
184impl Manifest {
185 pub fn find_assets<'a>(
193 &'a self,
194 entrypoint: &'a Utf8Path,
195 ) -> Result<(Asset<'a>, BTreeSet<Asset<'a>>), InvalidManifest<'a>> {
196 let entry = self.lookup_by_name(entrypoint)?;
197 let mut entries = BTreeSet::new();
198 let main_asset = self.find_imported_chunks(entry, &mut entries)?;
199
200 entries.remove(&main_asset);
203
204 Ok((main_asset, entries))
205 }
206
207 fn lookup_by_name<'a>(
209 &self,
210 name: &'a Utf8Path,
211 ) -> Result<&ManifestEntry, InvalidManifest<'a>> {
212 self.inner
213 .get(name)
214 .ok_or(InvalidManifest::CantFindAssetByName { name })
215 }
216
217 fn lookup_by_file<'a>(
219 &self,
220 file: &'a Utf8Path,
221 ) -> Result<&ManifestEntry, InvalidManifest<'a>> {
222 self.inner
223 .values()
224 .find(|e| e.file == file)
225 .ok_or(InvalidManifest::CantFindAssetByFile { file })
226 }
227
228 fn find_imported_chunks<'a>(
229 &'a self,
230 current_entry: &'a ManifestEntry,
231 entries: &mut BTreeSet<Asset<'a>>,
232 ) -> Result<Asset<'a>, InvalidManifest<'a>> {
233 let asset = Asset::new(current_entry)?;
234 let inserted = entries.insert(asset);
235
236 if inserted {
238 if let Some(css) = ¤t_entry.css {
239 for file in css {
240 let entry = self.lookup_by_file(file)?;
241 self.find_imported_chunks(entry, entries)?;
242 }
243 }
244
245 if let Some(assets) = ¤t_entry.assets {
246 for file in assets {
247 let entry = self.lookup_by_file(file)?;
248 self.find_imported_chunks(entry, entries)?;
249 }
250 }
251
252 if let Some(imports) = ¤t_entry.imports {
253 for import in imports {
254 let entry = self.lookup_by_name(import)?;
255 self.find_imported_chunks(entry, entries)?;
256 }
257 }
258 }
259
260 Ok(asset)
261 }
262}