add site-wide JS caching through import maps
This commit is contained in:
parent
f3aee8f41a
commit
10ccb250c1
|
@ -1,5 +1,5 @@
|
|||
%% title = "back porch"
|
||||
scripts = ["components/chat.js"]
|
||||
scripts = ["treehouse/components/chat.js"]
|
||||
styles = ["components/chat.css"]
|
||||
|
||||
% template = true
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")?;
|
||||
|
|
64
crates/treehouse/src/import_map.rs
Normal file
64
crates/treehouse/src/import_map.rs
Normal 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
|
||||
}
|
||||
}
|
28
crates/treehouse/src/include_static.rs
Normal file
28
crates/treehouse/src/include_static.rs
Normal 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"))
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -9,24 +9,24 @@
|
|||
<link rel="stylesheet" href="{{ asset 'css/main.css' }}">
|
||||
<link rel="stylesheet" href="{{ asset 'css/tree.css' }}">
|
||||
|
||||
<script type="importmap">{
|
||||
"imports": {
|
||||
"treehouse/": "{{ config.site }}/static/js/"
|
||||
}
|
||||
}</script>
|
||||
{{!-- 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. --}}
|
||||
{{!-- <script type="importmap" src="{{ asset 'generated/import-map.json' }}"></script> --}}
|
||||
<script type="importmap">{{{ include_static 'generated/import-map.json' }}}</script>
|
||||
|
||||
<script>
|
||||
const TREEHOUSE_SITE = `{{ config.site }}`;
|
||||
const TREEHOUSE_NEWS_COUNT = {{ len feeds.news.branches }};
|
||||
</script>
|
||||
<script type="module" src="{{ config.site }}/navmap.js"></script>
|
||||
<script type="module" src="{{ config.site }}/static/js/spells.js"></script>
|
||||
<script type="module" src="{{ config.site }}/static/js/ulid.js"></script>
|
||||
<script type="module" src="{{ config.site }}/static/js/usability.js"></script>
|
||||
<script type="module" src="{{ config.site }}/static/js/settings.js"></script>
|
||||
<script type="module" src="{{ config.site }}/static/js/tree.js"></script>
|
||||
<script type="module" src="{{ config.site }}/static/js/emoji.js"></script>
|
||||
<script type="module" src="{{ config.site }}/static/js/news.js"></script>
|
||||
<script type="module">
|
||||
import "treehouse/spells.js";
|
||||
import "treehouse/ulid.js";
|
||||
import "treehouse/usability.js";
|
||||
import "treehouse/settings.js";
|
||||
import "treehouse/tree.js";
|
||||
import "treehouse/emoji.js";
|
||||
import "treehouse/news.js";
|
||||
</script>
|
||||
|
||||
<meta property="og:site_name" content="{{ config.user.title }}">
|
||||
<meta property="og:title" content="{{ page.title }}">
|
||||
|
|
|
@ -4,12 +4,15 @@
|
|||
extracting them way more painful than it needs to be. --}}
|
||||
|
||||
{{#each page.styles}}
|
||||
<link rel="stylesheet" href="{{ ../config.site }}/static/css/{{ this }}">
|
||||
<link rel="stylesheet" href="{{ asset (cat 'css/' this) }}">
|
||||
{{/each}}
|
||||
|
||||
{{#each page.scripts}}
|
||||
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}"></script>
|
||||
{{/each}}
|
||||
<script type="module">
|
||||
{{!-- Go through the import map for each script. --}}
|
||||
{{#each page.scripts}}
|
||||
import "{{ this }}";
|
||||
{{/each}}
|
||||
</script>
|
||||
|
||||
{{{ page.tree }}}
|
||||
</main>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<head>
|
||||
<title>treehouse iframe sandbox</title>
|
||||
|
||||
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
|
||||
<link rel="stylesheet" href="{{ asset 'css/main.css' }}">
|
||||
|
||||
<style>
|
||||
body {
|
||||
|
@ -21,12 +21,7 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<script type="importmap">{
|
||||
"imports": {
|
||||
"treehouse/": "{{ config.site }}/static/js/",
|
||||
"tairu/": "{{ config.site }}/static/js/components/tairu/"
|
||||
}
|
||||
}</script>
|
||||
<script type="importmap">{{{ include_static 'generated/import-map.json' }}}</script>
|
||||
|
||||
<script type="module">
|
||||
import { evaluate, domConsole, jsConsole } from "treehouse/components/literate-programming/eval.js";
|
||||
|
|
|
@ -46,3 +46,9 @@ description = "a place on the Internet I like to call home"
|
|||
[emoji]
|
||||
|
||||
[pics]
|
||||
|
||||
[javascript]
|
||||
import_roots = [
|
||||
{ name = "treehouse", path = "static/js" },
|
||||
{ name = "tairu", path = "static/js/components/tairu" },
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue