use std::fs;
use std::sync::Arc;
use std::{any::Any, collections::BTreeMap};
use camino::{Utf8Path, Utf8PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::BuildError;
pub(crate) type Dynamic = Arc<dyn Any + Send + Sync>;
pub(crate) type ArcStr = std::sync::Arc<str>;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub(crate) struct Hash32([u8; 32]);
impl<T> From<T> for Hash32
where
T: Into<[u8; 32]>,
{
fn from(value: T) -> Self {
Hash32(value.into())
}
}
impl Hash32 {
pub(crate) fn hash(buffer: impl AsRef<[u8]>) -> Self {
blake3::Hasher::new()
.update(buffer.as_ref())
.finalize()
.into()
}
pub(crate) fn hash_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
Ok(blake3::Hasher::new().update_mmap(path)?.finalize().into())
}
pub(crate) fn to_hex(self) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut acc = vec![0u8; 64];
for (i, &byte) in self.0.iter().enumerate() {
acc[i * 2] = HEX[(byte >> 4) as usize];
acc[i * 2 + 1] = HEX[(byte & 0xF) as usize];
}
String::from_utf8(acc).unwrap()
}
}
impl std::fmt::Debug for Hash32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Hash32({})", self.to_hex())
}
}
#[derive(Default)]
pub(crate) struct Blake3Hasher(blake3::Hasher);
impl From<Blake3Hasher> for Hash32 {
fn from(value: Blake3Hasher) -> Self {
let bytes: [u8; 32] = value.0.finalize().into();
Hash32::from(bytes)
}
}
impl std::hash::Hasher for Blake3Hasher {
fn finish(&self) -> u64 {
let mut output = [0u8; 8];
self.0.finalize_xof().fill(&mut output);
u64::from_le_bytes(output)
}
fn write(&mut self, bytes: &[u8]) {
self.0.update(bytes);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Build,
Watch,
}
#[derive(Clone)]
pub struct Environment<D: Send + Sync = ()> {
pub generator: &'static str,
pub mode: Mode,
pub port: Option<u16>,
pub data: D,
}
impl<G: Send + Sync> std::fmt::Debug for Environment<G>
where
G: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Environment")
.field("generator", &self.generator)
.field("mode", &self.mode)
.field("port", &self.port)
.field("data", &self.data)
.finish()
}
}
impl<G: Send + Sync> Environment<G> {
pub fn get_refresh_script(&self) -> Option<String> {
self.port.map(|port| {
format!(
r#"
const socket = new WebSocket("ws://localhost:{port}");
socket.addEventListener("message", event => {{
window.location.reload();
}});
"#
)
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ImportMap {
imports: BTreeMap<String, String>,
}
impl ImportMap {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.imports.insert(key.into(), value.into());
self
}
pub fn merge(&mut self, other: ImportMap) {
for (key, value) in other.imports {
self.imports.insert(key, value);
}
}
pub fn to_json(&self) -> serde_json::Result<String> {
serde_json::to_string(self)
}
pub fn to_html(&self) -> serde_json::Result<String> {
self.to_json()
.map(|json| format!(r#"<script type="importmap">{json}</script>"#))
}
}
pub struct TaskContext<'a, G: Send + Sync = ()> {
pub env: &'a Environment<G>,
pub importmap: &'a ImportMap,
pub(crate) span: tracing::Span,
}
#[derive(Clone)]
pub struct Store {
pub(crate) imports: ImportMap,
}
impl Store {
pub fn new() -> Self {
Self {
imports: ImportMap::new(),
}
}
pub fn save(&self, data: &[u8], ext: &str) -> Result<Utf8PathBuf, BuildError> {
let hash = Hash32::hash(data);
let hash = hash.to_hex();
let path_temp = Utf8Path::new(".cache/hash").join(&hash);
let path_dist = Utf8Path::new("dist/hash").join(&hash).with_extension(ext);
let path_root = Utf8Path::new("/hash/").join(&hash).with_extension(ext);
if !path_temp.exists() {
fs::create_dir_all(".cache/hash")?;
fs::write(&path_temp, data)?;
}
let dir = path_dist.parent().unwrap_or(&path_dist);
fs::create_dir_all(dir)?;
if path_dist.exists() {
fs::remove_file(&path_dist)?;
}
fs::copy(&path_temp, &path_dist)?;
Ok(path_root)
}
pub fn register(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.imports.register(key, value);
}
}
impl Default for Store {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct FileMetadata {
pub file: Utf8PathBuf,
pub area: Utf8PathBuf,
pub info: Option<crate::git::GitInfo>,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_importmap() {
let mut map = ImportMap::new();
map.register("svelte", "/_app/svelte.js");
assert_eq!(
map.to_html().unwrap(),
r#"<script type="importmap">{"imports":{"svelte":"/_app/svelte.js"}}</script>"#
);
}
#[test]
fn test_default_importmap() {
let map = ImportMap::default();
assert!(map.imports.is_empty());
}
#[test]
fn test_merge() {
let mut map1 = ImportMap::new();
map1.register("a", "path/a");
map1.register("b", "path/b");
let mut map2 = ImportMap::new();
map2.register("b", "path/b2");
map2.register("c", "path/c");
map1.merge(map2);
let json = map1.to_json().unwrap();
assert!(json.contains(r#""a":"path/a""#));
assert!(json.contains(r#""b":"path/b2""#));
assert!(json.contains(r#""c":"path/c""#));
}
}