use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Manifest(HashMap<String, ManifestChunk>);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ManifestChunk {
#[serde(skip_serializing_if = "Option::is_none")]
pub src: Option<String>,
pub file: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub css: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assets: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "isEntry")]
pub is_entry: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "isDynamicEntry")]
pub is_dynamic_entry: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub imports: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "dynamicImports")]
pub dynamic_imports: Option<Vec<String>>,
}
impl std::str::FromStr for Manifest {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
impl Manifest {
pub fn manifest(&self) -> &HashMap<String, ManifestChunk> {
&self.0
}
pub fn imported_chunks(&self, name: &str) -> Vec<ManifestChunk> {
imported_chunks(&self.0, name)
}
}
pub fn parse_manifest(json: &str) -> Result<Manifest, serde_json::Error> {
serde_json::from_str(json)
}
pub fn manifest_to_json(manifest: &Manifest) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(manifest)
}
fn imported_chunks(manifest: &HashMap<String, ManifestChunk>, name: &str) -> Vec<ManifestChunk> {
let mut seen: HashSet<String> = HashSet::new();
fn get_imported_chunks(
chunk: &ManifestChunk,
manifest: &HashMap<String, ManifestChunk>,
seen: &mut HashSet<String>,
) -> Vec<ManifestChunk> {
let mut chunks: Vec<ManifestChunk> = Vec::new();
if let Some(imports) = &chunk.imports {
for file in imports.iter() {
if !seen.insert(file.clone()) {
continue;
}
if let Some(chunk) = manifest.get(file) {
chunks.extend(get_imported_chunks(chunk, manifest, seen));
chunks.push(chunk.clone());
}
}
}
chunks
}
match manifest.get(name) {
Some(chunk) => get_imported_chunks(chunk, manifest, &mut seen),
None => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_chunk_serialization() {
let chunk = ManifestChunk {
src: Some("src/main.js".to_string()),
file: "assets/main-abc123.js".to_string(),
css: Some(vec!["assets/main-def456.css".to_string()]),
assets: None,
is_entry: Some(true),
name: None,
names: None,
is_dynamic_entry: None,
imports: None,
dynamic_imports: None,
};
let json = serde_json::to_string(&chunk).unwrap();
let parsed_chunk: ManifestChunk = serde_json::from_str(&json).unwrap();
assert_eq!(chunk, parsed_chunk);
assert_eq!(chunk.file, "assets/main-abc123.js");
assert_eq!(chunk.src, Some("src/main.js".to_string()));
assert_eq!(chunk.is_entry, Some(true));
assert_eq!(chunk.css, Some(vec!["assets/main-def456.css".to_string()]));
}
#[test]
fn test_manifest_serialization() {
let json = r#"{
"src/main.js": {
"file": "assets/main-abc123.js",
"src": "src/main.js",
"isEntry": true,
"css": ["assets/main-def456.css"]
},
"src/utils.js": {
"file": "assets/utils-def456.js"
}
}"#;
let manifest = parse_manifest(json).unwrap();
let serialized = manifest_to_json(&manifest).unwrap();
let parsed_again = parse_manifest(&serialized).unwrap();
assert_eq!(manifest, parsed_again);
assert_eq!(manifest.manifest().len(), 2);
let main_chunk = manifest.manifest().get("src/main.js").unwrap();
assert_eq!(main_chunk.file, "assets/main-abc123.js");
assert_eq!(main_chunk.is_entry, Some(true));
let utils_chunk = manifest.manifest().get("src/utils.js").unwrap();
assert_eq!(utils_chunk.file, "assets/utils-def456.js");
assert_eq!(utils_chunk.is_entry, None);
}
#[test]
fn test_json_field_names() {
let json = r#"{
"file": "assets/main.js",
"isEntry": true,
"isDynamicEntry": false,
"dynamicImports": ["./lazy.js"]
}"#;
let chunk: ManifestChunk = serde_json::from_str(json).unwrap();
assert_eq!(chunk.is_entry, Some(true));
assert_eq!(chunk.is_dynamic_entry, Some(false));
assert_eq!(chunk.dynamic_imports, Some(vec!["./lazy.js".to_string()]));
}
#[test]
fn test_imported_chunks() {
let json = r#"{
"entry.js": {
"file": "entry.mjs",
"src": "entry.js",
"isEntry": true,
"imports": ["utils.js", "components.js"]
},
"utils.js": {
"file": "utils.mjs",
"src": "utils.js",
"imports": ["helpers.js"]
},
"components.js": {
"file": "components.mjs",
"src": "components.js",
"imports": ["helpers.js"]
},
"helpers.js": {
"file": "helpers.mjs",
"src": "helpers.js"
},
"standalone.js": {
"file": "standalone.mjs",
"src": "standalone.js"
}
}"#;
let manifest = parse_manifest(json).unwrap();
let imported = manifest.imported_chunks("entry.js");
assert_eq!(imported.len(), 3);
let files: Vec<_> = imported.iter().map(|c| &c.file).collect();
assert!(files.contains(&&"helpers.mjs".to_string()));
assert!(files.contains(&&"utils.mjs".to_string()));
assert!(files.contains(&&"components.mjs".to_string()));
let utils_imported = manifest.imported_chunks("utils.js");
assert_eq!(utils_imported.len(), 1);
assert_eq!(utils_imported[0].file, "helpers.mjs");
let helpers_imported = manifest.imported_chunks("helpers.js");
assert_eq!(helpers_imported.len(), 0);
let standalone_imported = manifest.imported_chunks("standalone.js");
assert_eq!(standalone_imported.len(), 0);
let missing_imported = manifest.imported_chunks("missing.js");
assert_eq!(missing_imported.len(), 0);
}
}