use md5::{Digest, Md5};
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Read;
use crate::asset::Asset;
use crate::chunk::Chunk;
use crate::error::{ViteError, ViteErrorKind};
use crate::vite::Entrypoints;
#[derive(Deserialize, Debug)]
pub(crate) struct Manifest {
manifest: HashMap<String, Chunk>,
hash: String,
}
impl Manifest {
pub fn new(path: &str) -> Result<Self, ViteError> {
let mut file = match File::open(path) {
Err(err) => {
return Err(ViteError::new(
format!("Failed to open manifest at {}: {}", path, err),
ViteErrorKind::Manifest,
));
}
Ok(file) => file,
};
let mut manifest_content = String::new();
if let Err(err) = file.read_to_string(&mut manifest_content) {
return Err(ViteError::new(
format!("Failed to read manifest.json content: {}", err),
ViteErrorKind::Manifest,
));
};
let hash = match Manifest::get_hash_from_manifest(&manifest_content) {
Ok(hash) => hash,
Err(err) => {
return Err(ViteError::new(
format!("Failed to generate hash for manifest: {err}"),
ViteErrorKind::Manifest,
))
}
};
let manifest = serde_json::from_str(&manifest_content);
match manifest {
Err(err) => Err(ViteError::new(
format!("Failed to parse manifest json: {}", err),
ViteErrorKind::Manifest,
)),
Ok(manifest) => Ok(Manifest { manifest, hash }),
}
}
fn get_hash_from_manifest(content: &str) -> Result<String, std::io::Error> {
let mut hasher = Md5::new();
hasher.update(content.as_bytes());
let hash = hasher.finalize();
let mut buffer = Vec::new();
for byte in hash.bytes() {
buffer.push(byte?);
}
Ok(hex::encode(&buffer))
}
#[inline]
pub fn get_hash(&self) -> &str {
&self.hash
}
pub fn generate_html_tags(
&self,
entrypoints: &Entrypoints,
prefix: Option<&'static str>,
app_url: &'static str,
) -> String {
if self.manifest.is_empty() {
log::error!("Manifest is empty. Empty string being returned from `Manifest::generate_html_tags`.");
return "".into();
}
let mut discovered_assets = HashSet::<Asset>::new();
for entry in entrypoints {
let entry_chunk = match self.manifest.get(entry.as_ref()) {
None => {
log::error!(r#"Skipping invalid or unexisting entry "{entry}"."#);
continue;
}
Some(chunk) => chunk,
};
let entry_as_asset = if entry.ends_with(".css") {
Asset::style_sheet(entry_chunk.file.clone(), prefix, app_url)
} else {
Asset::entry_point(entry_chunk.file.clone(), prefix, app_url)
};
if !discovered_assets.contains(&entry_as_asset) {
discovered_assets.insert(entry_as_asset);
self.iterate_over_chunk_assets(
&mut discovered_assets,
entry_chunk,
prefix,
app_url,
);
}
}
let mut assets = discovered_assets.into_iter().collect::<Vec<Asset>>();
assets.sort();
assets
.into_iter()
.map(|asset| asset.into_html())
.collect::<Vec<String>>()
.join("\n")
}
fn iterate_over_chunk_assets(
&self,
set: &mut HashSet<Asset>,
chunk: &Chunk,
prefix: Option<&'static str>,
app_url: &'static str,
) {
for asset in chunk.assets_iter(prefix, app_url) {
if !set.contains(&asset) {
set.insert(asset);
}
}
if chunk.is_entry {
chunk.imports.iter().for_each(|import| {
let import_chunk = &self.manifest[import];
set.insert(Asset::pre_load(import_chunk.file.clone(), prefix, app_url));
self.iterate_over_chunk_assets(set, import_chunk, prefix, app_url);
});
}
}
pub(crate) fn get_manifest_entries(&self) -> Vec<&str> {
let mut entries = Vec::new();
for (key, chunk) in self.manifest.iter() {
if chunk.is_entry {
entries.push(key.as_str());
}
}
entries
}
pub(crate) fn get_asset_url<'a>(
&'a self,
asset: &'a str,
prefix: Option<&str>,
app_url: &str,
) -> String {
match self.manifest.get(asset) {
None => String::new(),
Some(chunk) => Asset::resolve_asset_path(chunk.file.clone(), prefix, app_url),
}
}
}
#[cfg(test)]
mod test {
use super::Manifest;
use crate::{
test_utils::NormalizeHtmlStrings,
vite::{resolve_app_url, resolve_prefix},
};
#[test]
fn test_generate_html_tags_1() {
let manifest = Manifest::new("tests/test-manifest.json").unwrap();
let expected = r#"<link rel="stylesheet" href="/assets/foo-5UjPuW-k.css" />
<link rel="stylesheet" href="/assets/shared-ChJ_j-JJ.css" />
<script type="module" src="/assets/foo-BRBmoGS9.js"></script>
<link rel="modulepreload" href="/assets/shared-B7PI925R.js" />"#
.__normalize_html_strings();
let generated = manifest.generate_html_tags(&vec!["views/foo.js".into()], None, "");
assert_eq!(expected, generated);
}
#[test]
fn test_generate_html_tags_2() {
let manifest = Manifest::new("tests/test-manifest.json").unwrap();
let expected = r#"<link rel="stylesheet" href="/assets/shared-ChJ_j-JJ.css" />
<script type="module" src="/assets/bar-gkvgaI9m.js"></script>
<link rel="modulepreload" href="/assets/shared-B7PI925R.js" />"#
.__normalize_html_strings();
let generated = manifest.generate_html_tags(&vec!["views/bar.js".into()], None, "");
assert_eq!(expected, generated);
}
#[test]
fn test_generate_html_tags_with_prefix() {
let manifest = Manifest::new("tests/test-manifest.json").unwrap();
let generated = manifest.generate_html_tags(
&vec!["views/bar.js".into()],
resolve_prefix(Some("bundle/")),
"",
);
let expected = r#"<link rel="stylesheet" href="/bundle/assets/shared-ChJ_j-JJ.css" />
<script type="module" src="/bundle/assets/bar-gkvgaI9m.js"></script>
<link rel="modulepreload" href="/bundle/assets/shared-B7PI925R.js" />"#
.__normalize_html_strings();
assert_eq!(expected, generated);
}
#[test]
fn test_generate_html_tags_with_app_url() {
let manifest = Manifest::new("tests/test-manifest.json").unwrap();
let generated = manifest.generate_html_tags(
&vec!["views/bar.js".into()],
None,
resolve_app_url(Some("http://foo.baz")),
);
let expected =
r#"<link rel="stylesheet" href="http://foo.baz/assets/shared-ChJ_j-JJ.css" />
<script type="module" src="http://foo.baz/assets/bar-gkvgaI9m.js"></script>
<link rel="modulepreload" href="http://foo.baz/assets/shared-B7PI925R.js" />"#
.__normalize_html_strings();
assert_eq!(expected, generated);
}
#[test]
fn test_generate_html_tags_with_prefix_and_app_url() {
let manifest = Manifest::new("tests/test-manifest.json").unwrap();
let generated = manifest.generate_html_tags(
&vec!["views/bar.js".into()],
resolve_prefix(Some("bundle/")),
resolve_app_url(Some("http://foo.baz")),
);
let expected =
r#"<link rel="stylesheet" href="http://foo.baz/bundle/assets/shared-ChJ_j-JJ.css" />
<script type="module" src="http://foo.baz/bundle/assets/bar-gkvgaI9m.js"></script>
<link rel="modulepreload" href="http://foo.baz/bundle/assets/shared-B7PI925R.js" />"#
.__normalize_html_strings();
assert_eq!(expected, generated);
}
#[test]
fn test_get_asset_url() {
let manifest = Manifest::new("tests/test-manifest.json").unwrap();
let generated = manifest.get_asset_url("baz.js", None, "");
let expected = "/assets/baz-B2H3sXNv.js";
assert_eq!(expected, generated);
}
#[test]
fn test_get_asset_url_with_prefix() {
let manifest = Manifest::new("tests/test-manifest.json").unwrap();
let generated = manifest.get_asset_url("baz.js", resolve_prefix(Some("bundle/")), "");
let expected = "/bundle/assets/baz-B2H3sXNv.js";
assert_eq!(expected, generated);
}
}