cache busting
for faster load times, and seamless updates. because for some reason ServeDir can't do it correctly, and it tells the client "yeah hey nothing changed" even if something changed
This commit is contained in:
parent
9b190165ff
commit
5e6b84bed5
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -213,6 +213,19 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
@ -254,12 +267,13 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.8"
|
||||
version = "1.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549"
|
||||
checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -309,6 +323,12 @@ dependencies = [
|
|||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "copy_dir"
|
||||
version = "0.1.3"
|
||||
|
@ -780,6 +800,7 @@ checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
|||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1250,6 +1271,7 @@ dependencies = [
|
|||
"argon2",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"blake3",
|
||||
"chrono",
|
||||
"color-eyre",
|
||||
"copy_dir",
|
||||
|
@ -1260,6 +1282,7 @@ dependencies = [
|
|||
"handlebars",
|
||||
"indexmap",
|
||||
"jotdown",
|
||||
"mime_guess",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rayon",
|
||||
|
@ -1427,6 +1450,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
|
|
|
@ -4,7 +4,6 @@ use core::{
|
|||
};
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
ast::{Ast, NodeId, NodeKind},
|
||||
|
|
|
@ -7,6 +7,7 @@ edition = "2021"
|
|||
argon2 = "0.5.3"
|
||||
axum = { version = "0.7.5", features = ["macros", "ws"] }
|
||||
base64 = "0.22.1"
|
||||
blake3 = "1.5.4"
|
||||
chrono = "0.4.38"
|
||||
color-eyre = "0.6.3"
|
||||
copy_dir = "0.1.3"
|
||||
|
@ -15,8 +16,9 @@ derive_more = { version = "1.0.0", features = ["try_from"] }
|
|||
eyre = "0.6.12"
|
||||
haku.workspace = true
|
||||
handlebars = "6.0.0"
|
||||
indexmap = "2.4.0"
|
||||
indexmap = { version = "2.4.0", features = ["serde"] }
|
||||
jotdown = "0.5.0"
|
||||
mime_guess = "2.0.5"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rayon = "1.10.0"
|
||||
|
|
129
crates/rkgk/src/build.rs
Normal file
129
crates/rkgk/src/build.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use std::{
|
||||
ffi::OsStr,
|
||||
fs::{copy, create_dir_all, remove_dir_all, write},
|
||||
};
|
||||
|
||||
use copy_dir::copy_dir;
|
||||
use eyre::Context;
|
||||
use handlebars::Handlebars;
|
||||
use import_map::ImportMap;
|
||||
use include_static::IncludeStatic;
|
||||
use serde::Serialize;
|
||||
use static_urls::StaticUrls;
|
||||
use tracing::{info, instrument};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub mod import_map;
|
||||
mod include_static;
|
||||
mod static_urls;
|
||||
|
||||
use crate::{
|
||||
config::{BuildConfig, RenderTemplateFiles},
|
||||
Paths,
|
||||
};
|
||||
|
||||
#[instrument(skip(paths, config))]
|
||||
pub fn build(paths: &Paths<'_>, config: &BuildConfig) -> eyre::Result<()> {
|
||||
info!("building static site");
|
||||
|
||||
_ = remove_dir_all(paths.target_dir);
|
||||
create_dir_all(paths.target_dir).context("cannot create target directory")?;
|
||||
copy_dir("static", paths.target_dir.join("static")).context("cannot copy static directory")?;
|
||||
|
||||
create_dir_all(paths.target_dir.join("static/wasm"))
|
||||
.context("cannot create static/wasm directory")?;
|
||||
copy(
|
||||
paths.target_wasm_dir.join("haku_wasm.wasm"),
|
||||
paths.target_dir.join("static/wasm/haku.wasm"),
|
||||
)
|
||||
.context("cannot copy haku.wasm file")?;
|
||||
|
||||
let import_map = ImportMap::generate("".into(), &config.import_roots);
|
||||
write(
|
||||
paths.target_dir.join("static/import_map.json"),
|
||||
serde_json::to_string(&import_map)?,
|
||||
)?;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
handlebars.register_helper(
|
||||
"static",
|
||||
Box::new(StaticUrls::new(
|
||||
paths.target_dir.join("static"),
|
||||
"/static".into(),
|
||||
)),
|
||||
);
|
||||
handlebars.register_helper(
|
||||
"include_static",
|
||||
Box::new(IncludeStatic {
|
||||
base_dir: paths.target_dir.join("static"),
|
||||
}),
|
||||
);
|
||||
|
||||
for entry in WalkDir::new("template") {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if file_name
|
||||
.rsplit_once('.')
|
||||
.is_some_and(|(left, _)| left.ends_with(".hbs"))
|
||||
{
|
||||
handlebars.register_template_file(&file_name, path)?;
|
||||
info!(file_name, "registered template");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SingleFileData {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DjotData {
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
for render_template in &config.render_templates {
|
||||
info!(?render_template);
|
||||
match &render_template.files {
|
||||
RenderTemplateFiles::SingleFile { to_file } => {
|
||||
let rendered = handlebars.render(&render_template.template, &SingleFileData {})?;
|
||||
std::fs::write(paths.target_dir.join(to_file), rendered)?;
|
||||
}
|
||||
|
||||
RenderTemplateFiles::Directory { from_dir, to_dir } => {
|
||||
create_dir_all(paths.target_dir.join(to_dir))?;
|
||||
|
||||
for entry in WalkDir::new(from_dir) {
|
||||
let entry = entry?;
|
||||
let inner_path = entry.path().strip_prefix(from_dir)?;
|
||||
|
||||
if entry.path().extension() == Some(OsStr::new("dj")) {
|
||||
let djot = std::fs::read_to_string(entry.path())?;
|
||||
let events = jotdown::Parser::new(&djot);
|
||||
let content = jotdown::html::render_to_string(events);
|
||||
let title = config
|
||||
.page_titles
|
||||
.get(entry.path())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| entry.path().to_string_lossy().into_owned());
|
||||
let rendered = handlebars
|
||||
.render(&render_template.template, &DjotData { title, content })?;
|
||||
std::fs::write(
|
||||
paths
|
||||
.target_dir
|
||||
.join(to_dir)
|
||||
.join(inner_path.with_extension("html")),
|
||||
rendered,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
67
crates/rkgk/src/build/import_map.rs
Normal file
67
crates/rkgk/src/build/import_map.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use std::{ffi::OsStr, path::PathBuf};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use super::static_urls::StaticUrls;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ImportMap {
|
||||
pub imports: IndexMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ImportRoot {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl ImportMap {
|
||||
pub fn generate(base_url: String, import_roots: &[ImportRoot]) -> Self {
|
||||
let mut import_map = ImportMap {
|
||||
imports: IndexMap::new(),
|
||||
};
|
||||
|
||||
for root in import_roots {
|
||||
let static_urls = StaticUrls::new(
|
||||
PathBuf::from(&root.path),
|
||||
format!("{base_url}/{}", root.path),
|
||||
);
|
||||
for entry in WalkDir::new(&root.path) {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(error) => {
|
||||
warn!("directory walk failed: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !entry.file_type().is_dir() && entry.path().extension() == Some(OsStr::new("js"))
|
||||
{
|
||||
let normalized_path = entry
|
||||
.path()
|
||||
.strip_prefix(&root.path)
|
||||
.unwrap_or(entry.path())
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
match static_urls.get(&normalized_path) {
|
||||
Ok(url) => {
|
||||
import_map
|
||||
.imports
|
||||
.insert(format!("{}/{normalized_path}", root.name), url);
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("could not get static url for {normalized_path}: {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import_map.imports.sort_unstable_keys();
|
||||
|
||||
import_map
|
||||
}
|
||||
}
|
31
crates/rkgk/src/build/include_static.rs
Normal file
31
crates/rkgk/src/build/include_static.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, RenderErrorReason,
|
||||
ScopedJson,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
pub struct IncludeStatic {
|
||||
pub base_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl HelperDef for IncludeStatic {
|
||||
fn call_inner<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
helper: &Helper<'rc>,
|
||||
_: &'reg Handlebars<'reg>,
|
||||
_: &'rc Context,
|
||||
_: &mut RenderContext<'reg, 'rc>,
|
||||
) -> Result<ScopedJson<'rc>, RenderError> {
|
||||
if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) {
|
||||
return Ok(ScopedJson::Derived(Value::String(
|
||||
std::fs::read_to_string(self.base_dir.join(param)).map_err(|error| {
|
||||
RenderErrorReason::Other(format!("cannot read static asset {param}: {error}"))
|
||||
})?,
|
||||
)));
|
||||
}
|
||||
|
||||
Err(RenderErrorReason::Other("asset path must be provided".into()).into())
|
||||
}
|
||||
}
|
79
crates/rkgk/src/build/static_urls.rs
Normal file
79
crates/rkgk/src/build/static_urls.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{self, BufReader},
|
||||
path::PathBuf,
|
||||
sync::RwLock,
|
||||
};
|
||||
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, RenderErrorReason,
|
||||
ScopedJson,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
pub struct StaticUrls {
|
||||
base_dir: PathBuf,
|
||||
base_url: String,
|
||||
// Really annoying that we have to use an RwLock for this. We only ever generate in a
|
||||
// single-threaded environment.
|
||||
// Honestly it would be a lot more efficient if Handlebars just assumed single-threadedness
|
||||
// and required you to clone it over to different threads.
|
||||
// Stuff like this is why I really want to implement my own templating engine...
|
||||
hash_cache: RwLock<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl StaticUrls {
|
||||
pub fn new(base_dir: PathBuf, base_url: String) -> Self {
|
||||
Self {
|
||||
base_dir,
|
||||
base_url,
|
||||
hash_cache: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, filename: &str) -> Result<String, io::Error> {
|
||||
let hash_cache = self.hash_cache.read().unwrap();
|
||||
if let Some(cached) = hash_cache.get(filename) {
|
||||
return Ok(cached.to_owned());
|
||||
}
|
||||
drop(hash_cache);
|
||||
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
let file = BufReader::new(File::open(self.base_dir.join(filename))?);
|
||||
hasher.update_reader(file)?;
|
||||
// NOTE: Here the hash is truncated to 8 characters. This is fine, because we don't
|
||||
// care about security here - only detecting changes in files.
|
||||
let hash = format!(
|
||||
"{}/{}?cache=b3-{}",
|
||||
self.base_url,
|
||||
filename,
|
||||
&hasher.finalize().to_hex()[0..8]
|
||||
);
|
||||
{
|
||||
let mut hash_cache = self.hash_cache.write().unwrap();
|
||||
hash_cache.insert(filename.to_owned(), hash.clone());
|
||||
}
|
||||
Ok(hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl HelperDef for StaticUrls {
|
||||
fn call_inner<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
helper: &Helper<'rc>,
|
||||
_: &'reg Handlebars<'reg>,
|
||||
_: &'rc Context,
|
||||
_: &mut RenderContext<'reg, 'rc>,
|
||||
) -> Result<ScopedJson<'rc>, RenderError> {
|
||||
if let Some(param) = helper.param(0).and_then(|v| v.value().as_str()) {
|
||||
return Ok(ScopedJson::Derived(Value::String(
|
||||
self.get(param).map_err(|error| {
|
||||
RenderErrorReason::Other(format!("cannot get asset url for {param}: {error}"))
|
||||
})?,
|
||||
)));
|
||||
}
|
||||
|
||||
Err(RenderErrorReason::Other("asset path must be provided".into()).into())
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ use std::{collections::HashMap, path::PathBuf};
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::wall;
|
||||
use crate::{build::import_map::ImportRoot, wall};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
|
@ -15,6 +15,7 @@ pub struct Config {
|
|||
pub struct BuildConfig {
|
||||
pub render_templates: Vec<RenderTemplate>,
|
||||
pub page_titles: HashMap<PathBuf, String>,
|
||||
pub import_roots: Vec<ImportRoot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
|
@ -27,5 +28,6 @@ pub struct RenderTemplate {
|
|||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RenderTemplateFiles {
|
||||
SingleFile { to_file: PathBuf },
|
||||
Directory { from_dir: PathBuf, to_dir: PathBuf },
|
||||
}
|
||||
|
|
|
@ -1,29 +1,20 @@
|
|||
use std::{
|
||||
ffi::OsStr,
|
||||
fs::{copy, create_dir_all, remove_dir_all},
|
||||
net::Ipv4Addr,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{fs::create_dir_all, net::Ipv4Addr, path::Path, sync::Arc};
|
||||
|
||||
use api::Api;
|
||||
use axum::Router;
|
||||
use config::{BuildConfig, Config, RenderTemplateFiles};
|
||||
use copy_dir::copy_dir;
|
||||
use config::Config;
|
||||
use eyre::Context;
|
||||
use handlebars::Handlebars;
|
||||
use serde::Serialize;
|
||||
use router::router;
|
||||
use tokio::{fs, net::TcpListener};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use tracing::{info, instrument};
|
||||
use walkdir::WalkDir;
|
||||
use tracing::info;
|
||||
|
||||
mod api;
|
||||
mod auto_reload;
|
||||
mod build;
|
||||
mod config;
|
||||
mod haku;
|
||||
mod id;
|
||||
mod live_reload;
|
||||
mod login;
|
||||
mod router;
|
||||
mod schema;
|
||||
mod serialization;
|
||||
mod wall;
|
||||
|
@ -33,87 +24,11 @@ mod wall;
|
|||
static GLOBAL_ALLOCATOR: tracy_client::ProfiledAllocator<std::alloc::System> =
|
||||
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
|
||||
|
||||
struct Paths<'a> {
|
||||
target_dir: &'a Path,
|
||||
target_wasm_dir: &'a Path,
|
||||
database_dir: &'a Path,
|
||||
}
|
||||
|
||||
#[instrument(skip(paths, config))]
|
||||
fn build(paths: &Paths<'_>, config: &BuildConfig) -> eyre::Result<()> {
|
||||
info!("building static site");
|
||||
|
||||
_ = remove_dir_all(paths.target_dir);
|
||||
create_dir_all(paths.target_dir).context("cannot create target directory")?;
|
||||
copy_dir("static", paths.target_dir.join("static")).context("cannot copy static directory")?;
|
||||
|
||||
create_dir_all(paths.target_dir.join("static/wasm"))
|
||||
.context("cannot create static/wasm directory")?;
|
||||
copy(
|
||||
paths.target_wasm_dir.join("haku_wasm.wasm"),
|
||||
paths.target_dir.join("static/wasm/haku.wasm"),
|
||||
)
|
||||
.context("cannot copy haku.wasm file")?;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
for entry in WalkDir::new("template") {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if file_name
|
||||
.rsplit_once('.')
|
||||
.is_some_and(|(left, _)| left.ends_with(".hbs"))
|
||||
{
|
||||
handlebars.register_template_file(&file_name, path)?;
|
||||
info!(file_name, "registered template");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DjotData {
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
for render_template in &config.render_templates {
|
||||
info!(?render_template);
|
||||
match &render_template.files {
|
||||
RenderTemplateFiles::Directory { from_dir, to_dir } => {
|
||||
create_dir_all(paths.target_dir.join(to_dir))?;
|
||||
|
||||
for entry in WalkDir::new(from_dir) {
|
||||
let entry = entry?;
|
||||
let inner_path = entry.path().strip_prefix(from_dir)?;
|
||||
|
||||
if entry.path().extension() == Some(OsStr::new("dj")) {
|
||||
let djot = std::fs::read_to_string(entry.path())?;
|
||||
let events = jotdown::Parser::new(&djot);
|
||||
let content = jotdown::html::render_to_string(events);
|
||||
let title = config
|
||||
.page_titles
|
||||
.get(entry.path())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| entry.path().to_string_lossy().into_owned());
|
||||
let rendered = handlebars
|
||||
.render(&render_template.template, &DjotData { title, content })?;
|
||||
std::fs::write(
|
||||
paths
|
||||
.target_dir
|
||||
.join(to_dir)
|
||||
.join(inner_path.with_extension("html")),
|
||||
rendered,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Paths<'a> {
|
||||
pub target_dir: &'a Path,
|
||||
pub target_wasm_dir: &'a Path,
|
||||
pub database_dir: &'a Path,
|
||||
}
|
||||
|
||||
pub struct Databases {
|
||||
|
@ -152,20 +67,11 @@ async fn fallible_main() -> eyre::Result<()> {
|
|||
)
|
||||
.context("cannot deserialize config file")?;
|
||||
|
||||
build(&paths, &config.build)?;
|
||||
build::build(&paths, &config.build)?;
|
||||
let dbs = Arc::new(database(&config, &paths)?);
|
||||
|
||||
let api = Arc::new(Api { config, dbs });
|
||||
let app = Router::new()
|
||||
.route_service(
|
||||
"/",
|
||||
ServeFile::new(paths.target_dir.join("static/index.html")),
|
||||
)
|
||||
.nest_service("/static", ServeDir::new(paths.target_dir.join("static")))
|
||||
.nest_service("/docs", ServeDir::new(paths.target_dir.join("docs")))
|
||||
.nest("/api", api::router(api));
|
||||
|
||||
let app = app.nest("/auto-reload", live_reload::router());
|
||||
let app = router(&paths, api);
|
||||
|
||||
let port: u16 = std::env::var("RKGK_PORT")
|
||||
.unwrap_or("8080".into())
|
||||
|
|
97
crates/rkgk/src/router.rs
Normal file
97
crates/rkgk/src/router.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::{
|
||||
header::{CACHE_CONTROL, CONTENT_TYPE},
|
||||
HeaderValue,
|
||||
},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
api::{self, Api},
|
||||
auto_reload, Paths,
|
||||
};
|
||||
|
||||
struct Server {
|
||||
target_dir: PathBuf,
|
||||
|
||||
index_html: String,
|
||||
four_oh_four_html: String,
|
||||
}
|
||||
|
||||
pub fn router<S>(paths: &Paths, api: Arc<Api>) -> Router<S> {
|
||||
Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/static/*path", get(static_file))
|
||||
.route("/docs/*path", get(docs))
|
||||
.nest("/api", api::router(api))
|
||||
.nest("/auto-reload", auto_reload::router())
|
||||
.fallback(get(four_oh_four))
|
||||
.with_state(Arc::new(Server {
|
||||
target_dir: paths.target_dir.to_path_buf(),
|
||||
|
||||
index_html: std::fs::read_to_string(paths.target_dir.join("static/index.html"))
|
||||
.expect("index.html does not exist"),
|
||||
four_oh_four_html: std::fs::read_to_string(paths.target_dir.join("static/404.html"))
|
||||
.expect("404.html does not exist"),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn index(State(state): State<Arc<Server>>) -> Html<String> {
|
||||
Html(state.index_html.clone())
|
||||
}
|
||||
|
||||
async fn four_oh_four(State(state): State<Arc<Server>>) -> Html<String> {
|
||||
Html(state.four_oh_four_html.clone())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StaticFileQuery {
|
||||
cache: Option<String>,
|
||||
}
|
||||
|
||||
async fn static_file(
|
||||
Path(path): Path<String>,
|
||||
Query(query): Query<StaticFileQuery>,
|
||||
State(state): State<Arc<Server>>,
|
||||
) -> Response {
|
||||
if let Ok(file) = tokio::fs::read(state.target_dir.join("static").join(&path)).await {
|
||||
let mut response = file.into_response();
|
||||
|
||||
if let Some(content_type) = mime_guess::from_path(&path).first_raw() {
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
|
||||
} else {
|
||||
response.headers_mut().remove(CONTENT_TYPE);
|
||||
}
|
||||
|
||||
if query.cache.is_some() {
|
||||
response.headers_mut().insert(
|
||||
CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
||||
);
|
||||
}
|
||||
|
||||
response
|
||||
} else {
|
||||
four_oh_four(State(state)).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn docs(Path(mut path): Path<String>, state: State<Arc<Server>>) -> Html<String> {
|
||||
if !path.ends_with(".html") {
|
||||
path.push_str(".html")
|
||||
}
|
||||
|
||||
if let Ok(file) = tokio::fs::read_to_string(state.target_dir.join("docs").join(&path)).await {
|
||||
Html(file)
|
||||
} else {
|
||||
four_oh_four(state).await
|
||||
}
|
||||
}
|
10
rkgk.toml
10
rkgk.toml
|
@ -4,7 +4,15 @@
|
|||
|
||||
# List of Handlebars templates to render.
|
||||
render_templates = [
|
||||
{ template = "docs.hbs.html", from_dir = "docs", to_dir = "docs" }
|
||||
{ template = "fonts.hbs.css", to_file = "static/fonts.css" },
|
||||
|
||||
{ template = "index.hbs.html", to_file = "static/index.html" },
|
||||
{ template = "docs.hbs.html", from_dir = "docs", to_dir = "docs" },
|
||||
]
|
||||
|
||||
# List of JavaScript `import` root directories.
|
||||
import_roots = [
|
||||
{ name = "rkgk", path = "static" },
|
||||
]
|
||||
|
||||
[build.page_titles]
|
||||
|
|
17
static/404.html
Normal file
17
static/404.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<title>404 Not Found</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>404 Not Found</h1>
|
||||
<p>It appears the thing you're looking for doesn't exist.</p>
|
||||
<p><a href="/">Back to rakugaki</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!-- This page could really be prettier, don't you think? -->
|
|
@ -23,50 +23,6 @@ body {
|
|||
|
||||
/* Fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: "Atkinson Hyperlegible";
|
||||
src:
|
||||
local("Atkinson Hyperlegible Regular"),
|
||||
url("font/AtkinsonHyperlegible-Regular.ttf");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Atkinson Hyperlegible";
|
||||
src:
|
||||
local("Atkinson Hyperlegible Italic"),
|
||||
url("font/AtkinsonHyperlegible-Italic.ttf");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Atkinson Hyperlegible";
|
||||
src:
|
||||
local("Atkinson Hyperlegible Bold"),
|
||||
url("font/AtkinsonHyperlegible-Bold.ttf");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Atkinson Hyperlegible";
|
||||
src:
|
||||
local("Atkinson Hyperlegible Bold Italic"),
|
||||
url("font/AtkinsonHyperlegible-BoldItalic.ttf");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
/* NOTE: This is my own variant of Iosevka that more or less follows the stylistic choices
|
||||
of Atkinson Hyperlegible. */
|
||||
font-family: "Iosevka Hyperlegible";
|
||||
src:
|
||||
local("Iosevka Hyperlegible"),
|
||||
url("font/IosevkaHyperlegible-Regular.woff2");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: "Atkinson Hyperlegible", sans-serif;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { listen } from "./framework.js";
|
||||
import { Viewport } from "./viewport.js";
|
||||
import { Wall } from "./wall.js";
|
||||
import { listen } from "rkgk/framework.js";
|
||||
import { Viewport } from "rkgk/viewport.js";
|
||||
import { Wall } from "rkgk/wall.js";
|
||||
|
||||
class CanvasRenderer extends HTMLElement {
|
||||
viewport = new Viewport();
|
||||
|
|
|
@ -8,7 +8,7 @@ function makeLogFunction(level) {
|
|||
}
|
||||
|
||||
let { instance: hakuInstance, module: hakuModule } = await WebAssembly.instantiateStreaming(
|
||||
fetch(import.meta.resolve("./wasm/haku.wasm")),
|
||||
fetch(HAKU_WASM_PATH),
|
||||
{
|
||||
env: {
|
||||
panic(length, pMessage) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Wall } from "./wall.js";
|
||||
import { Wall } from "rkgk/wall.js";
|
||||
import {
|
||||
getLoginSecret,
|
||||
getUserId,
|
||||
|
@ -6,9 +6,9 @@ import {
|
|||
newSession,
|
||||
registerUser,
|
||||
waitForLogin,
|
||||
} from "./session.js";
|
||||
import { debounce } from "./framework.js";
|
||||
import { ReticleCursor } from "./reticle-renderer.js";
|
||||
} from "rkgk/session.js";
|
||||
import { debounce } from "rkgk/framework.js";
|
||||
import { ReticleCursor } from "rkgk/reticle-renderer.js";
|
||||
|
||||
const updateInterval = 1000 / 60;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Haku } from "./haku.js";
|
||||
import { Painter } from "./painter.js";
|
||||
import { Haku } from "rkgk/haku.js";
|
||||
import { Painter } from "rkgk/painter.js";
|
||||
|
||||
export class User {
|
||||
nickname = "";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { listen } from "./framework.js";
|
||||
import { listen } from "rkgk/framework.js";
|
||||
|
||||
export class ResizeHandle extends HTMLElement {
|
||||
constructor() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { listen } from "./framework.js";
|
||||
import { listen } from "rkgk/framework.js";
|
||||
|
||||
let loginStorage = JSON.parse(localStorage.getItem("rkgk.login") ?? "{}");
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Pixmap } from "./haku.js";
|
||||
import { OnlineUsers } from "./online-users.js";
|
||||
import { Pixmap } from "rkgk/haku.js";
|
||||
import { OnlineUsers } from "rkgk/online-users.js";
|
||||
|
||||
export class Chunk {
|
||||
constructor(size) {
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }} · rakugaki manual</title>
|
||||
<link rel="stylesheet" href="/static/base.css">
|
||||
<link rel="stylesheet" href="/static/docs.css">
|
||||
<link rel="stylesheet" href="{{ static 'base.css' }}">
|
||||
<link rel="stylesheet" href="{{ static 'fonts.css' }}">
|
||||
<link rel="stylesheet" href="{{ static 'docs.css' }}">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<script src="/static/live-reload.js" type="module"></script>
|
||||
<script src="{{ static 'live-reload.js' }}" type="module"></script>
|
||||
|
||||
<link rel="icon" sizes="16x16" href="/static/favicon/rkgk@1x.png">
|
||||
<link rel="icon" sizes="32x32" href="/static/favicon/rkgk@2x.png">
|
||||
|
|
44
template/fonts.hbs.css
Normal file
44
template/fonts.hbs.css
Normal file
|
@ -0,0 +1,44 @@
|
|||
@font-face {
|
||||
font-family: "Atkinson Hyperlegible";
|
||||
src:
|
||||
local("Atkinson Hyperlegible Regular"),
|
||||
url("{{ static 'font/AtkinsonHyperlegible-Regular.ttf' }}");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Atkinson Hyperlegible";
|
||||
src:
|
||||
local("Atkinson Hyperlegible Italic"),
|
||||
url("{{ static 'font/AtkinsonHyperlegible-Italic.ttf' }}");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Atkinson Hyperlegible";
|
||||
src:
|
||||
local("Atkinson Hyperlegible Bold"),
|
||||
url("{{ static 'font/AtkinsonHyperlegible-Bold.ttf' }}");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Atkinson Hyperlegible";
|
||||
src:
|
||||
local("Atkinson Hyperlegible Bold Italic"),
|
||||
url("{{ static 'font/AtkinsonHyperlegible-BoldItalic.ttf' }}");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
/* NOTE: This is my own variant of Iosevka that more or less follows the stylistic choices
|
||||
of Atkinson Hyperlegible. */
|
||||
font-family: "Iosevka Hyperlegible";
|
||||
src:
|
||||
local("Iosevka Hyperlegible"),
|
||||
url("{{ static 'font/IosevkaHyperlegible-Regular.woff2' }}");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
|
@ -6,38 +6,47 @@
|
|||
|
||||
<title>rakugaki</title>
|
||||
|
||||
<link rel="stylesheet" href="static/base.css">
|
||||
<link rel="stylesheet" href="static/index.css">
|
||||
<link rel="stylesheet" href="{{ static 'base.css' }}">
|
||||
<link rel="stylesheet" href="{{ static 'fonts.css' }}">
|
||||
<link rel="stylesheet" href="{{ static 'index.css' }}">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<script src="static/live-reload.js" type="module"></script>
|
||||
|
||||
<script src="static/brush-editor.js" type="module"></script>
|
||||
<script src="static/canvas-renderer.js" type="module"></script>
|
||||
<script src="static/connection-status.js" type="module"></script>
|
||||
<script src="static/framework.js" type="module"></script>
|
||||
<script src="static/resize-handle.js" type="module"></script>
|
||||
<script src="static/reticle-renderer.js" type="module"></script>
|
||||
<script src="static/session.js" type="module"></script>
|
||||
<script src="static/throbber.js" type="module"></script>
|
||||
<script src="static/viewport.js" type="module"></script>
|
||||
<script src="static/welcome.js" type="module"></script>
|
||||
|
||||
<script src="static/index.js" type="module" defer></script>
|
||||
<script type="importmap">{{{ include_static 'import_map.json' }}}</script>
|
||||
|
||||
<link rel="icon" sizes="16x16" href="/static/favicon/rkgk@1x.png">
|
||||
<link rel="icon" sizes="32x32" href="/static/favicon/rkgk@2x.png">
|
||||
<link rel="icon" sizes="64x64" href="/static/favicon/rkgk@4x.png">
|
||||
<link rel="icon" sizes="128x128" href="/static/favicon/rkgk@8x.png">
|
||||
<link rel="icon" sizes="256x256" href="/static/favicon/rkgk@16x.png">
|
||||
<link rel="icon" sizes="512x512" href="/static/favicon/rkgk@32x.png">
|
||||
<link rel="apple-touch-icon" sizes="16x16" href="/static/favicon/rkgk@1x.png">
|
||||
<link rel="apple-touch-icon" sizes="32x32" href="/static/favicon/rkgk@2x.png">
|
||||
<link rel="apple-touch-icon" sizes="64x64" href="/static/favicon/rkgk@4x.png">
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="/static/favicon/rkgk@8x.png">
|
||||
<link rel="apple-touch-icon" sizes="256x256" href="/static/favicon/rkgk@16x.png">
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/static/favicon/rkgk@32x.png">
|
||||
<script>
|
||||
const HAKU_WASM_PATH = "{{{ static 'wasm/haku.wasm' }}}";
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import "rkgk/live-reload.js";
|
||||
|
||||
import "rkgk/brush-editor.js";
|
||||
import "rkgk/canvas-renderer.js";
|
||||
import "rkgk/connection-status.js";
|
||||
import "rkgk/framework.js";
|
||||
import "rkgk/resize-handle.js";
|
||||
import "rkgk/reticle-renderer.js";
|
||||
import "rkgk/session.js";
|
||||
import "rkgk/throbber.js";
|
||||
import "rkgk/viewport.js";
|
||||
import "rkgk/welcome.js";
|
||||
|
||||
import "rkgk/index.js";
|
||||
</script>
|
||||
|
||||
<link rel="icon" sizes="16x16" href="{{ static 'favicon/rkgk@1x.png' }}">
|
||||
<link rel="icon" sizes="32x32" href="{{ static 'favicon/rkgk@2x.png' }}">
|
||||
<link rel="icon" sizes="64x64" href="{{ static 'favicon/rkgk@4x.png' }}">
|
||||
<link rel="icon" sizes="128x128" href="{{ static 'favicon/rkgk@8x.png' }}">
|
||||
<link rel="icon" sizes="256x256" href="{{ static 'favicon/rkgk@16x.png' }}">
|
||||
<link rel="icon" sizes="512x512" href="{{ static 'favicon/rkgk@32x.png' }}">
|
||||
<link rel="apple-touch-icon" sizes="16x16" href="{{ static 'favicon/rkgk@1x.png' }}">
|
||||
<link rel="apple-touch-icon" sizes="32x32" href="{{ static 'favicon/rkgk@2x.png' }}">
|
||||
<link rel="apple-touch-icon" sizes="64x64" href="{{ static 'favicon/rkgk@4x.png' }}">
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="{{ static 'favicon/rkgk@8x.png' }}">
|
||||
<link rel="apple-touch-icon" sizes="256x256" href="{{ static 'favicon/rkgk@16x.png' }}">
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{ static 'favicon/rkgk@32x.png' }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -133,3 +142,4 @@
|
|||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in a new issue