From 10ccb250c1b205dfc13e7ef49473dd416d61bd4e Mon Sep 17 00:00:00 2001 From: liquidev Date: Fri, 19 Jul 2024 20:05:17 +0200 Subject: [PATCH] add site-wide JS caching through import maps --- content/kuroneko.tree | 2 +- content/programming/blog/tairu.tree | 4 +- content/treehouse/dev/chats.tree | 2 +- content/treehouse/sandbox.tree | 2 +- crates/treehouse/src/cli/generate.rs | 30 ++++++++- crates/treehouse/src/config.rs | 10 +++ crates/treehouse/src/import_map.rs | 64 +++++++++++++++++++ crates/treehouse/src/include_static.rs | 28 ++++++++ crates/treehouse/src/main.rs | 2 + .../components/literate-programming/eval.js | 15 ----- static/js/tree.js | 6 +- template/components/_head.hbs | 26 ++++---- template/components/_tree.hbs | 11 ++-- template/sandbox.hbs | 9 +-- treehouse.toml | 6 ++ 15 files changed, 169 insertions(+), 48 deletions(-) create mode 100644 crates/treehouse/src/import_map.rs create mode 100644 crates/treehouse/src/include_static.rs diff --git a/content/kuroneko.tree b/content/kuroneko.tree index 90c0f17..8c11a89 100644 --- a/content/kuroneko.tree +++ b/content/kuroneko.tree @@ -1,5 +1,5 @@ %% title = "back porch" - scripts = ["components/chat.js"] + scripts = ["treehouse/components/chat.js"] styles = ["components/chat.css"] % template = true diff --git a/content/programming/blog/tairu.tree b/content/programming/blog/tairu.tree index 372696a..4f18eca 100644 --- a/content/programming/blog/tairu.tree +++ b/content/programming/blog/tairu.tree @@ -1,7 +1,7 @@ %% title = "tairu - an interactive exploration of 2D autotiling techniques" scripts = [ - "components/literate-programming.js", - "vendor/codejar.js", + "treehouse/components/literate-programming.js", + "treehouse/vendor/codejar.js", ] styles = ["page/tairu.css"] diff --git a/content/treehouse/dev/chats.tree b/content/treehouse/dev/chats.tree index 30507ab..28bff41 100644 --- a/content/treehouse/dev/chats.tree +++ b/content/treehouse/dev/chats.tree @@ -1,5 +1,5 @@ %% title = "testing ground for (chit)chats" - scripts = ["components/chat.js"] + scripts = ["treehouse/components/chat.js"] styles = ["components/chat.css"] % id = "01HSR695VNDXRGPC3XCHS3A61V" diff --git a/content/treehouse/sandbox.tree b/content/treehouse/sandbox.tree index 6ad68e8..b83aa76 100644 --- a/content/treehouse/sandbox.tree +++ b/content/treehouse/sandbox.tree @@ -1,5 +1,5 @@ %% title = "the treehouse sandbox" -scripts = ["components/literate-programming.js"] +scripts = ["treehouse/components/literate-programming.js"] % id = "01HPWJB4Y5ST6AEK9VDYNS865P" - the sandbox is a framework for playing around with code diff --git a/crates/treehouse/src/cli/generate.rs b/crates/treehouse/src/cli/generate.rs index 3b61f95..4ad56cf 100644 --- a/crates/treehouse/src/cli/generate.rs +++ b/crates/treehouse/src/cli/generate.rs @@ -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 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, diff --git a/crates/treehouse/src/config.rs b/crates/treehouse/src/config.rs index 78b1d9a..2b54211 100644 --- a/crates/treehouse/src/config.rs +++ b/crates/treehouse/src/config.rs @@ -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, } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct JavaScript { + /// Import roots to generate in the project's import map. + pub import_roots: Vec, +} + impl Config { pub fn load(path: &Path) -> anyhow::Result { let string = std::fs::read_to_string(path).context("cannot read config file")?; diff --git a/crates/treehouse/src/import_map.rs b/crates/treehouse/src/import_map.rs new file mode 100644 index 0000000..6fd38e6 --- /dev/null +++ b/crates/treehouse/src/import_map.rs @@ -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, +} + +#[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 + } +} diff --git a/crates/treehouse/src/include_static.rs b/crates/treehouse/src/include_static.rs new file mode 100644 index 0000000..63ca871 --- /dev/null +++ b/crates/treehouse/src/include_static.rs @@ -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, 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")) + } +} diff --git a/crates/treehouse/src/main.rs b/crates/treehouse/src/main.rs index 70324c2..7d057b4 100644 --- a/crates/treehouse/src/main.rs +++ b/crates/treehouse/src/main.rs @@ -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; diff --git a/static/js/components/literate-programming/eval.js b/static/js/components/literate-programming/eval.js index 19db75c..3815b2b 100644 --- a/static/js/components/literate-programming/eval.js +++ b/static/js/components/literate-programming/eval.js @@ -17,21 +17,6 @@ export const domConsole = { } }; -async function withTemporaryGlobalScope(callback) { - let state = { - oldValues: {}, - set(key, value) { - this.oldValues[key] = globalThis[key]; - globalThis[key] = value; - } - }; - await callback(state); - jsConsole.trace(state.oldValues, "bringing back old state"); - for (let key in state.oldValues) { - globalThis[key] = state.oldValues[key]; - } -} - let evaluationComplete = null; export async function evaluate(commands, { error, newOutput }) { diff --git a/static/js/tree.js b/static/js/tree.js index ae9a92a..a8428f1 100644 --- a/static/js/tree.js +++ b/static/js/tree.js @@ -1,7 +1,6 @@ // This is definitely not a three.js ripoff. import { addSpell } from "treehouse/spells.js"; -import { navigationMap } from "/navmap.js"; import * as ulid from "treehouse/ulid.js"; /* Branch persistence */ @@ -128,7 +127,8 @@ class LinkedBranch extends Branch { // No need to await for the import because we don't use the resulting module. // Just fire and forger 💀 // and let them run in parallel. - import(script.src); + let url = URL.createObjectURL(new Blob([script.textContent], { type: "text/javascript" })) + import(url); } } catch (error) { this.loadingText.innerText = error.toString(); @@ -177,6 +177,8 @@ function navigateToPage(page) { async function navigateToBranch(fragment) { if (fragment.length == 0) return; + let { navigationMap } = await import("/navmap.js"); + let element = document.getElementById(fragment); if (element !== null) { // If the element is already loaded on the page, we're good. diff --git a/template/components/_head.hbs b/template/components/_head.hbs index 310db9c..b3922ae 100644 --- a/template/components/_head.hbs +++ b/template/components/_head.hbs @@ -9,24 +9,24 @@ - +{{!-- Import maps currently don't support the src="" attribute. Unless we come up with something +clever to do while browser vendors figure that out, we'll just have to do a cache-busting include_static. --}} +{{!-- --}} + - - - - - - - - + diff --git a/template/components/_tree.hbs b/template/components/_tree.hbs index 0b2bde8..a4c4725 100644 --- a/template/components/_tree.hbs +++ b/template/components/_tree.hbs @@ -4,12 +4,15 @@ extracting them way more painful than it needs to be. --}} {{#each page.styles}} - + {{/each}} - {{#each page.scripts}} - - {{/each}} + {{{ page.tree }}} diff --git a/template/sandbox.hbs b/template/sandbox.hbs index 2e45265..d7b08a0 100644 --- a/template/sandbox.hbs +++ b/template/sandbox.hbs @@ -5,7 +5,7 @@ treehouse iframe sandbox - + - +