1use serde::{Deserialize, Serialize};
28use std::collections::{HashMap, HashSet};
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct Manifest(HashMap<String, ManifestChunk>);
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct ManifestChunk {
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub src: Option<String>,
38
39 pub file: String,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub css: Option<Vec<String>>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub assets: Option<Vec<String>>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 #[serde(rename = "isEntry")]
53 pub is_entry: Option<bool>,
54
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub name: Option<String>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub names: Option<Vec<String>>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 #[serde(rename = "isDynamicEntry")]
66 pub is_dynamic_entry: Option<bool>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub imports: Option<Vec<String>>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 #[serde(rename = "dynamicImports")]
75 pub dynamic_imports: Option<Vec<String>>,
76}
77
78impl std::str::FromStr for Manifest {
79 type Err = serde_json::Error;
80
81 fn from_str(s: &str) -> Result<Self, Self::Err> {
82 serde_json::from_str(s)
83 }
84}
85
86impl Manifest {
87 pub fn manifest(&self) -> &HashMap<String, ManifestChunk> {
88 &self.0
89 }
90
91 pub fn imported_chunks(&self, name: &str) -> Vec<ManifestChunk> {
92 imported_chunks(&self.0, name)
93 }
94}
95
96pub fn parse_manifest(json: &str) -> Result<Manifest, serde_json::Error> {
98 serde_json::from_str(json)
99}
100
101pub fn manifest_to_json(manifest: &Manifest) -> Result<String, serde_json::Error> {
103 serde_json::to_string_pretty(manifest)
104}
105
106fn imported_chunks(manifest: &HashMap<String, ManifestChunk>, name: &str) -> Vec<ManifestChunk> {
107 let mut seen: HashSet<String> = HashSet::new();
108
109 fn get_imported_chunks(
110 chunk: &ManifestChunk,
111 manifest: &HashMap<String, ManifestChunk>,
112 seen: &mut HashSet<String>,
113 ) -> Vec<ManifestChunk> {
114 let mut chunks: Vec<ManifestChunk> = Vec::new();
115 if let Some(imports) = &chunk.imports {
116 for file in imports.iter() {
117 if !seen.insert(file.clone()) {
118 continue;
119 }
120
121 if let Some(chunk) = manifest.get(file) {
122 chunks.extend(get_imported_chunks(chunk, manifest, seen));
123 chunks.push(chunk.clone());
124 }
125 }
126 }
127 chunks
128 }
129
130 match manifest.get(name) {
131 Some(chunk) => get_imported_chunks(chunk, manifest, &mut seen),
132 None => Vec::new(),
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_manifest_chunk_serialization() {
142 let chunk = ManifestChunk {
143 src: Some("src/main.js".to_string()),
144 file: "assets/main-abc123.js".to_string(),
145 css: Some(vec!["assets/main-def456.css".to_string()]),
146 assets: None,
147 is_entry: Some(true),
148 name: None,
149 names: None,
150 is_dynamic_entry: None,
151 imports: None,
152 dynamic_imports: None,
153 };
154
155 let json = serde_json::to_string(&chunk).unwrap();
156 let parsed_chunk: ManifestChunk = serde_json::from_str(&json).unwrap();
157
158 assert_eq!(chunk, parsed_chunk);
159 assert_eq!(chunk.file, "assets/main-abc123.js");
160 assert_eq!(chunk.src, Some("src/main.js".to_string()));
161 assert_eq!(chunk.is_entry, Some(true));
162 assert_eq!(chunk.css, Some(vec!["assets/main-def456.css".to_string()]));
163 }
164
165 #[test]
166 fn test_manifest_serialization() {
167 let json = r#"{
168 "src/main.js": {
169 "file": "assets/main-abc123.js",
170 "src": "src/main.js",
171 "isEntry": true,
172 "css": ["assets/main-def456.css"]
173 },
174 "src/utils.js": {
175 "file": "assets/utils-def456.js"
176 }
177 }"#;
178
179 let manifest = parse_manifest(json).unwrap();
180 let serialized = manifest_to_json(&manifest).unwrap();
181 let parsed_again = parse_manifest(&serialized).unwrap();
182
183 assert_eq!(manifest, parsed_again);
184 assert_eq!(manifest.manifest().len(), 2);
185
186 let main_chunk = manifest.manifest().get("src/main.js").unwrap();
187 assert_eq!(main_chunk.file, "assets/main-abc123.js");
188 assert_eq!(main_chunk.is_entry, Some(true));
189
190 let utils_chunk = manifest.manifest().get("src/utils.js").unwrap();
191 assert_eq!(utils_chunk.file, "assets/utils-def456.js");
192 assert_eq!(utils_chunk.is_entry, None);
193 }
194
195 #[test]
196 fn test_json_field_names() {
197 let json = r#"{
198 "file": "assets/main.js",
199 "isEntry": true,
200 "isDynamicEntry": false,
201 "dynamicImports": ["./lazy.js"]
202 }"#;
203
204 let chunk: ManifestChunk = serde_json::from_str(json).unwrap();
205 assert_eq!(chunk.is_entry, Some(true));
206 assert_eq!(chunk.is_dynamic_entry, Some(false));
207 assert_eq!(chunk.dynamic_imports, Some(vec!["./lazy.js".to_string()]));
208 }
209
210 #[test]
211 fn test_imported_chunks() {
212 let json = r#"{
213 "entry.js": {
214 "file": "entry.mjs",
215 "src": "entry.js",
216 "isEntry": true,
217 "imports": ["utils.js", "components.js"]
218 },
219 "utils.js": {
220 "file": "utils.mjs",
221 "src": "utils.js",
222 "imports": ["helpers.js"]
223 },
224 "components.js": {
225 "file": "components.mjs",
226 "src": "components.js",
227 "imports": ["helpers.js"]
228 },
229 "helpers.js": {
230 "file": "helpers.mjs",
231 "src": "helpers.js"
232 },
233 "standalone.js": {
234 "file": "standalone.mjs",
235 "src": "standalone.js"
236 }
237 }"#;
238
239 let manifest = parse_manifest(json).unwrap();
240
241 let imported = manifest.imported_chunks("entry.js");
243 assert_eq!(imported.len(), 3); let files: Vec<_> = imported.iter().map(|c| &c.file).collect();
247 assert!(files.contains(&&"helpers.mjs".to_string()));
248 assert!(files.contains(&&"utils.mjs".to_string()));
249 assert!(files.contains(&&"components.mjs".to_string()));
250
251 let utils_imported = manifest.imported_chunks("utils.js");
253 assert_eq!(utils_imported.len(), 1);
254 assert_eq!(utils_imported[0].file, "helpers.mjs");
255
256 let helpers_imported = manifest.imported_chunks("helpers.js");
258 assert_eq!(helpers_imported.len(), 0);
259
260 let standalone_imported = manifest.imported_chunks("standalone.js");
262 assert_eq!(standalone_imported.len(), 0);
263
264 let missing_imported = manifest.imported_chunks("missing.js");
266 assert_eq!(missing_imported.len(), 0);
267 }
268}