add site-wide JS caching through import maps

This commit is contained in:
りき萌 2024-07-19 20:05:17 +02:00
parent f3aee8f41a
commit 10ccb250c1
15 changed files with 169 additions and 48 deletions

View file

@ -25,6 +25,8 @@ use crate::{
navmap::{build_navigation_map, NavigationMap},
tree::branches_to_html,
},
import_map::ImportMap,
include_static::IncludeStatic,
state::Source,
static_urls::StaticUrls,
tree::SemaRoots,
@ -216,7 +218,8 @@ impl Generator {
let mut config_derived_data = ConfigDerivedData {
image_sizes: Default::default(),
static_urls: StaticUrls::new(
paths.static_dir.to_owned(),
// NOTE: Allow referring to generated static assets here.
paths.target_dir.join("static"),
format!("{}/static", config.site),
),
};
@ -227,10 +230,19 @@ impl Generator {
handlebars.register_helper(
"asset",
Box::new(StaticUrls::new(
paths.static_dir.to_owned(),
paths.target_dir.join("static"),
format!("{}/static", config.site),
)),
);
handlebars.register_helper(
"include_static",
Box::new(IncludeStatic {
// NOTE: Again, allow referring to generated static assets.
// This is necessary for import maps, for whom the <src> attribute is not
// currently supported.
base_dir: paths.target_dir.join("static"),
}),
);
let mut template_file_ids = HashMap::new();
for entry in WalkDir::new(paths.template_dir) {
@ -389,11 +401,18 @@ pub fn generate(paths: &Paths<'_>) -> anyhow::Result<(Config, Treehouse)> {
info!("copying static directory to target directory");
copy_dir(paths.static_dir, paths.target_dir.join("static"))?;
info!("creating static/generated directory");
std::fs::create_dir_all(paths.target_dir.join("static/generated"))?;
info!("parsing tree");
let mut generator = Generator::default();
generator.add_directory_rec(paths.content_dir)?;
let (mut treehouse, parsed_trees) = generator.parse_trees(&config, paths)?;
// NOTE: The navigation map is a legacy feature that is lazy-loaded when fragment-based
// navigation is used.
// I couldn't be bothered with adding it to the import map since fragment-based navigation is
// only used on very old links. Adding caching to the navigation map is probably not worth it.
info!("generating navigation map");
let navigation_map = build_navigation_map(&treehouse, "index");
std::fs::write(
@ -401,6 +420,13 @@ pub fn generate(paths: &Paths<'_>) -> anyhow::Result<(Config, Treehouse)> {
navigation_map.to_javascript(),
)?;
info!("generating import map");
let import_map = ImportMap::generate(config.site.clone(), &config.javascript.import_roots);
std::fs::write(
paths.target_dir.join("static/generated/import-map.json"),
serde_json::to_string_pretty(&import_map).context("could not serialize import map")?,
)?;
info!("generating standalone pages");
generator.generate_all_files(
&mut treehouse,

View file

@ -11,6 +11,7 @@ use crate::{
compiled::{compile_syntax, CompiledSyntax},
Syntax,
},
import_map::ImportRoot,
static_urls::StaticUrls,
};
@ -47,6 +48,9 @@ pub struct Config {
/// ```
pub redirects: Redirects,
/// JavaScript configuration.
pub javascript: JavaScript,
/// Overrides for emoji filenames. Useful for setting up aliases.
///
/// On top of this, emojis are autodiscovered by walking the `static/emoji` directory.
@ -74,6 +78,12 @@ pub struct Redirects {
pub page: HashMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JavaScript {
/// Import roots to generate in the project's import map.
pub import_roots: Vec<ImportRoot>,
}
impl Config {
pub fn load(path: &Path) -> anyhow::Result<Self> {
let string = std::fs::read_to_string(path).context("cannot read config file")?;

View file

@ -0,0 +1,64 @@
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use log::warn;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use crate::static_urls::StaticUrls;
#[derive(Debug, Clone, Serialize)]
pub struct ImportMap {
pub imports: HashMap<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: HashMap::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
}
}

View file

@ -0,0 +1,28 @@
use std::path::PathBuf;
use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, 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<'reg, 'rc>,
_: &'reg Handlebars<'reg>,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<ScopedJson<'reg, '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| {
RenderError::new(format!("cannot read static asset {param}: {error}"))
})?,
)));
}
Err(RenderError::new("asset path must be provided"))
}
}

View file

@ -14,6 +14,8 @@ mod cli;
mod config;
mod fun;
mod html;
mod import_map;
mod include_static;
mod paths;
mod state;
mod static_urls;