#![doc = include_str!("../README.md")]
#![deny(
unsafe_code,
// clippy::unwrap_used,
// clippy::expect_used,
clippy::panic,
)]
pub mod error;
mod executor;
mod graph;
pub mod importmap;
pub mod loader;
pub mod page;
mod utils;
use std::{any::type_name, fmt::Debug, sync::Arc};
use camino::Utf8PathBuf;
use graph::TaskDependencies;
use petgraph::{Graph, graph::NodeIndex};
pub use camino;
pub use gitscan as git;
pub use crate::executor::Diagnostics;
pub use crate::graph::Handle;
pub use crate::importmap::ImportMap;
pub use crate::loader::Store;
pub use crate::page::Output;
use crate::graph::{Dynamic, Task, TypedTask};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
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 {
fn hash(buffer: impl AsRef<[u8]>) -> Self {
blake3::Hasher::new()
.update(buffer.as_ref())
.finalize()
.into()
}
fn hash_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
Ok(blake3::Hasher::new()
.update_mmap_rayon(path)?
.finalize()
.into())
}
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 Debug for Hash32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Hash32({})", self.to_hex())
}
}
#[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> 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();
}});
"#
)
})
}
}
pub struct TaskContext<'a, G: Send + Sync = ()> {
pub env: &'a Environment<G>,
pub importmap: &'a ImportMap,
}
#[derive(Debug)]
pub struct FileMetadata {
pub file: Utf8PathBuf,
pub area: Utf8PathBuf,
pub info: Option<crate::git::GitInfo>,
}
struct TaskNode<G, R, D, F>
where
G: Send + Sync,
R: Send + Sync + 'static,
D: TaskDependencies,
F: for<'a> Fn(&TaskContext<'a, G>, D::Output<'a>) -> anyhow::Result<R> + Send + Sync,
{
name: &'static str,
dependencies: D,
callback: F,
_phantom: std::marker::PhantomData<G>,
}
impl<G, R, D, F> TypedTask<G> for TaskNode<G, R, D, F>
where
G: Send + Sync + 'static,
R: Send + Sync + 'static,
D: TaskDependencies + Send + Sync,
F: for<'a> Fn(&TaskContext<'a, G>, D::Output<'a>) -> anyhow::Result<R> + Send + Sync + 'static,
{
type Output = R;
fn get_name(&self) -> String {
self.name.to_string()
}
fn dependencies(&self) -> Vec<NodeIndex> {
self.dependencies.dependencies()
}
fn get_watched(&self) -> Vec<camino::Utf8PathBuf> {
vec![]
}
fn execute(
&self,
context: &TaskContext<G>,
_: &mut Store,
dependencies: &[Dynamic],
) -> anyhow::Result<Self::Output> {
let dependencies = self.dependencies.resolve(dependencies);
(self.callback)(context, dependencies)
}
}
pub struct Blueprint<G: Send + Sync = ()> {
graph: Graph<Arc<dyn Task<G>>, ()>,
}
impl<G: Send + Sync + 'static> Blueprint<G> {
pub fn new() -> Self {
Self {
graph: Graph::new(),
}
}
pub fn finish(self) -> Website<G> {
Website { graph: self.graph }
}
pub fn add_task<D, F, R>(&mut self, dependencies: D, callback: F) -> graph::Handle<R>
where
D: TaskDependencies + Send + Sync + 'static,
F: for<'a> Fn(&TaskContext<'a, G>, D::Output<'a>) -> anyhow::Result<R>
+ Send
+ Sync
+ 'static,
R: Send + Sync + 'static,
{
self.add_task_opaque(TaskNode {
name: type_name::<F>(),
dependencies,
callback,
_phantom: std::marker::PhantomData,
})
}
pub(crate) fn add_task_opaque<O, T>(&mut self, task: T) -> graph::Handle<O>
where
O: 'static,
T: TypedTask<G, Output = O> + 'static,
{
let dependencies = task.dependencies();
let index = self.graph.add_node(Arc::new(task));
for dependency in dependencies {
self.graph.add_edge(dependency, index, ());
}
graph::Handle::new(index)
}
}
impl<G: Send + Sync + 'static> Default for Blueprint<G> {
fn default() -> Self {
Self::new()
}
}
impl<G> std::fmt::Display for Blueprint<G>
where
G: Send + Sync + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "graph LR")?;
for index in self.graph.node_indices() {
let task = &self.graph[index];
let name = task.get_name().replace('"', "\\\""); writeln!(f, " {:?}[\"{}\"]", index.index(), name)?;
if task.is_output() {
writeln!(f, " {:?} --> Output", index.index())?;
}
}
writeln!(f, " Output[Output]")?;
for edge in self.graph.edge_indices() {
let (source, target) = self.graph.edge_endpoints(edge).unwrap();
let source_task = &self.graph[source];
let type_name = source_task
.get_output_type_name()
.replace('<', "<")
.replace('>', ">");
writeln!(
f,
" {:?} -- \"{}\" --> {:?}",
source.index(),
type_name,
target.index()
)?;
}
Ok(())
}
}
pub struct Website<G: Send + Sync = ()> {
graph: Graph<Arc<dyn Task<G>>, ()>,
}
impl<G> Website<G>
where
G: Send + Sync + 'static,
{
pub fn design() -> Blueprint<G> {
Blueprint::default()
}
pub fn build(&mut self, data: G) -> anyhow::Result<Diagnostics> {
let globals = Environment {
generator: "hauchiwa",
mode: Mode::Build,
port: None,
data,
};
utils::clear_dist().expect("Failed to clear dist directory");
utils::clone_static().expect("Failed to copy static files");
let (_, pages, diagnostics) = crate::executor::run_once_parallel(self, &globals)?;
crate::page::save_pages_to_dist(&pages).expect("Failed to save pages");
Ok(diagnostics)
}
#[cfg(feature = "live")]
pub fn watch(&mut self, data: G) -> anyhow::Result<()> {
utils::clear_dist().expect("Failed to clear dist directory");
utils::clone_static().expect("Failed to copy static files");
crate::executor::watch(self, data)?;
Ok(())
}
}
#[macro_export]
macro_rules! task {
($config:expr, |$ctx:pat_param $(, $($dep:ident $( : $ty:ty )? ),* )? | $body:block) => {
$config.add_task(
( $( $($dep),* )? ),
|$ctx, ( $( $($dep),* )? )| {
$( $( $( let _: $ty = $dep; )? )* )?
$body
}
)
};
}