use std::collections::{HashMap, HashSet};
use serde::Deserialize;
#[derive(Debug, Clone)]
pub struct Chunks {
chunks: HashMap<String, Chunk>,
islands: HashMap<String, String>,
base: String,
}
#[derive(Debug)]
pub struct Preloads<'c> {
chunks: &'c Chunks,
linked: HashSet<&'c str>,
}
#[derive(Debug)]
pub struct ManifestError {
what: &'static str,
error: serde_json::Error,
}
#[derive(Debug, Clone, Deserialize)]
struct Chunk {
file: String,
#[serde(default)]
imports: Vec<String>,
#[serde(default, rename = "isEntry")]
is_entry: bool,
}
impl Chunks {
pub fn from_manifest(manifest: &str, islands: &str) -> Result<Self, ManifestError> {
let chunks: HashMap<String, Chunk> =
serde_json::from_str(manifest).map_err(|error| ManifestError {
what: "a Vite manifest",
error,
})?;
let files: HashMap<String, String> =
serde_json::from_str(islands).map_err(|error| ManifestError {
what: "ferrovue's islands",
error,
})?;
let keys: HashMap<&str, &str> = chunks
.iter()
.map(|(key, chunk)| (chunk.file.as_str(), key.as_str()))
.collect();
let islands = files
.into_iter()
.filter_map(|(name, file)| Some((name, (*keys.get(file.as_str())?).to_owned())))
.collect();
Ok(Chunks {
chunks,
islands,
base: String::from("/"),
})
}
#[must_use]
pub fn with_base(mut self, base: impl Into<String>) -> Self {
self.base = base.into();
self
}
#[must_use]
pub fn for_islands<'n>(&self, names: impl IntoIterator<Item = &'n str>) -> Vec<String> {
let mut seen = HashSet::new();
names
.into_iter()
.filter_map(|name| self.islands.get(name))
.flat_map(|key| self.needed(key, &mut seen))
.map(|file| self.url(file))
.collect()
}
#[must_use]
pub fn for_source(&self, key: &str) -> Vec<String> {
let Some((key, _)) = self.chunks.get_key_value(key) else {
return Vec::new();
};
self.needed(key, &mut HashSet::new())
.into_iter()
.map(|file| self.url(file))
.collect()
}
#[must_use]
pub fn preloads(&self) -> Preloads<'_> {
Preloads {
chunks: self,
linked: HashSet::new(),
}
}
fn needed<'c>(&'c self, key: &'c str, seen: &mut HashSet<&'c str>) -> Vec<&'c str> {
let mut files = Vec::new();
let mut stack = vec![key];
while let Some(key) = stack.pop() {
if !seen.insert(key) {
continue;
}
let Some(chunk) = self.chunks.get(key) else {
continue;
};
if chunk.is_entry {
continue;
}
files.push(chunk.file.as_str());
stack.extend(chunk.imports.iter().rev().map(String::as_str));
}
files
}
fn url(&self, file: &str) -> String {
format!("{}{file}", self.base)
}
}
impl<'c> Preloads<'c> {
pub fn islands_into<'n>(&mut self, out: &mut String, names: impl IntoIterator<Item = &'n str>) {
let chunks = self.chunks;
for name in names {
if let Some(key) = chunks.islands.get(name) {
self.key_into(out, key);
}
}
}
pub fn source_into(&mut self, out: &mut String, key: &str) {
if let Some((key, _)) = self.chunks.chunks.get_key_value(key) {
self.key_into(out, key);
}
}
fn key_into(&mut self, out: &mut String, key: &'c str) {
let chunks = self.chunks;
for file in chunks.needed(key, &mut HashSet::new()) {
if self.linked.insert(file) {
out.push_str("<link rel=\"modulepreload\" href=\"");
crate::escape_into(out, &chunks.url(file));
out.push_str("\">");
}
}
}
}
impl std::fmt::Display for ManifestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not {}: {}", self.what, self.error)
}
}
impl std::error::Error for ManifestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
#[cfg(test)]
mod tests;