add site-wide JS caching through import maps
This commit is contained in:
parent
f3aee8f41a
commit
10ccb250c1
|
@ -1,5 +1,5 @@
|
||||||
%% title = "back porch"
|
%% title = "back porch"
|
||||||
scripts = ["components/chat.js"]
|
scripts = ["treehouse/components/chat.js"]
|
||||||
styles = ["components/chat.css"]
|
styles = ["components/chat.css"]
|
||||||
|
|
||||||
% template = true
|
% template = true
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
%% title = "tairu - an interactive exploration of 2D autotiling techniques"
|
%% title = "tairu - an interactive exploration of 2D autotiling techniques"
|
||||||
scripts = [
|
scripts = [
|
||||||
"components/literate-programming.js",
|
"treehouse/components/literate-programming.js",
|
||||||
"vendor/codejar.js",
|
"treehouse/vendor/codejar.js",
|
||||||
]
|
]
|
||||||
styles = ["page/tairu.css"]
|
styles = ["page/tairu.css"]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
%% title = "testing ground for (chit)chats"
|
%% title = "testing ground for (chit)chats"
|
||||||
scripts = ["components/chat.js"]
|
scripts = ["treehouse/components/chat.js"]
|
||||||
styles = ["components/chat.css"]
|
styles = ["components/chat.css"]
|
||||||
|
|
||||||
% id = "01HSR695VNDXRGPC3XCHS3A61V"
|
% id = "01HSR695VNDXRGPC3XCHS3A61V"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
%% title = "the treehouse sandbox"
|
%% title = "the treehouse sandbox"
|
||||||
scripts = ["components/literate-programming.js"]
|
scripts = ["treehouse/components/literate-programming.js"]
|
||||||
|
|
||||||
% id = "01HPWJB4Y5ST6AEK9VDYNS865P"
|
% id = "01HPWJB4Y5ST6AEK9VDYNS865P"
|
||||||
- the sandbox is a framework for playing around with code
|
- the sandbox is a framework for playing around with code
|
||||||
|
|
|
@ -25,6 +25,8 @@ use crate::{
|
||||||
navmap::{build_navigation_map, NavigationMap},
|
navmap::{build_navigation_map, NavigationMap},
|
||||||
tree::branches_to_html,
|
tree::branches_to_html,
|
||||||
},
|
},
|
||||||
|
import_map::ImportMap,
|
||||||
|
include_static::IncludeStatic,
|
||||||
state::Source,
|
state::Source,
|
||||||
static_urls::StaticUrls,
|
static_urls::StaticUrls,
|
||||||
tree::SemaRoots,
|
tree::SemaRoots,
|
||||||
|
@ -216,7 +218,8 @@ impl Generator {
|
||||||
let mut config_derived_data = ConfigDerivedData {
|
let mut config_derived_data = ConfigDerivedData {
|
||||||
image_sizes: Default::default(),
|
image_sizes: Default::default(),
|
||||||
static_urls: StaticUrls::new(
|
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),
|
format!("{}/static", config.site),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -227,10 +230,19 @@ impl Generator {
|
||||||
handlebars.register_helper(
|
handlebars.register_helper(
|
||||||
"asset",
|
"asset",
|
||||||
Box::new(StaticUrls::new(
|
Box::new(StaticUrls::new(
|
||||||
paths.static_dir.to_owned(),
|
paths.target_dir.join("static"),
|
||||||
format!("{}/static", config.site),
|
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();
|
let mut template_file_ids = HashMap::new();
|
||||||
for entry in WalkDir::new(paths.template_dir) {
|
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");
|
info!("copying static directory to target directory");
|
||||||
copy_dir(paths.static_dir, paths.target_dir.join("static"))?;
|
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");
|
info!("parsing tree");
|
||||||
let mut generator = Generator::default();
|
let mut generator = Generator::default();
|
||||||
generator.add_directory_rec(paths.content_dir)?;
|
generator.add_directory_rec(paths.content_dir)?;
|
||||||
let (mut treehouse, parsed_trees) = generator.parse_trees(&config, paths)?;
|
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");
|
info!("generating navigation map");
|
||||||
let navigation_map = build_navigation_map(&treehouse, "index");
|
let navigation_map = build_navigation_map(&treehouse, "index");
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
|
@ -401,6 +420,13 @@ pub fn generate(paths: &Paths<'_>) -> anyhow::Result<(Config, Treehouse)> {
|
||||||
navigation_map.to_javascript(),
|
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");
|
info!("generating standalone pages");
|
||||||
generator.generate_all_files(
|
generator.generate_all_files(
|
||||||
&mut treehouse,
|
&mut treehouse,
|
||||||
|
|
|
@ -11,6 +11,7 @@ use crate::{
|
||||||
compiled::{compile_syntax, CompiledSyntax},
|
compiled::{compile_syntax, CompiledSyntax},
|
||||||
Syntax,
|
Syntax,
|
||||||
},
|
},
|
||||||
|
import_map::ImportRoot,
|
||||||
static_urls::StaticUrls,
|
static_urls::StaticUrls,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,6 +48,9 @@ pub struct Config {
|
||||||
/// ```
|
/// ```
|
||||||
pub redirects: Redirects,
|
pub redirects: Redirects,
|
||||||
|
|
||||||
|
/// JavaScript configuration.
|
||||||
|
pub javascript: JavaScript,
|
||||||
|
|
||||||
/// Overrides for emoji filenames. Useful for setting up aliases.
|
/// Overrides for emoji filenames. Useful for setting up aliases.
|
||||||
///
|
///
|
||||||
/// On top of this, emojis are autodiscovered by walking the `static/emoji` directory.
|
/// 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>,
|
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 {
|
impl Config {
|
||||||
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
||||||
let string = std::fs::read_to_string(path).context("cannot read config file")?;
|
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 config;
|
||||||
mod fun;
|
mod fun;
|
||||||
mod html;
|
mod html;
|
||||||
|
mod import_map;
|
||||||
|
mod include_static;
|
||||||
mod paths;
|
mod paths;
|
||||||
mod state;
|
mod state;
|
||||||
mod static_urls;
|
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;
|
let evaluationComplete = null;
|
||||||
|
|
||||||
export async function evaluate(commands, { error, newOutput }) {
|
export async function evaluate(commands, { error, newOutput }) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// This is definitely not a three.js ripoff.
|
// This is definitely not a three.js ripoff.
|
||||||
|
|
||||||
import { addSpell } from "treehouse/spells.js";
|
import { addSpell } from "treehouse/spells.js";
|
||||||
import { navigationMap } from "/navmap.js";
|
|
||||||
import * as ulid from "treehouse/ulid.js";
|
import * as ulid from "treehouse/ulid.js";
|
||||||
|
|
||||||
/* Branch persistence */
|
/* 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.
|
// No need to await for the import because we don't use the resulting module.
|
||||||
// Just fire and forger 💀
|
// Just fire and forger 💀
|
||||||
// and let them run in parallel.
|
// and let them run in parallel.
|
||||||
import(script.src);
|
let url = URL.createObjectURL(new Blob([script.textContent], { type: "text/javascript" }))
|
||||||
|
import(url);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.loadingText.innerText = error.toString();
|
this.loadingText.innerText = error.toString();
|
||||||
|
@ -177,6 +177,8 @@ function navigateToPage(page) {
|
||||||
async function navigateToBranch(fragment) {
|
async function navigateToBranch(fragment) {
|
||||||
if (fragment.length == 0) return;
|
if (fragment.length == 0) return;
|
||||||
|
|
||||||
|
let { navigationMap } = await import("/navmap.js");
|
||||||
|
|
||||||
let element = document.getElementById(fragment);
|
let element = document.getElementById(fragment);
|
||||||
if (element !== null) {
|
if (element !== null) {
|
||||||
// If the element is already loaded on the page, we're good.
|
// 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/main.css' }}">
|
||||||
<link rel="stylesheet" href="{{ asset 'css/tree.css' }}">
|
<link rel="stylesheet" href="{{ asset 'css/tree.css' }}">
|
||||||
|
|
||||||
<script type="importmap">{
|
{{!-- Import maps currently don't support the src="" attribute. Unless we come up with something
|
||||||
"imports": {
|
clever to do while browser vendors figure that out, we'll just have to do a cache-busting include_static. --}}
|
||||||
"treehouse/": "{{ config.site }}/static/js/"
|
{{!-- <script type="importmap" src="{{ asset 'generated/import-map.json' }}"></script> --}}
|
||||||
}
|
<script type="importmap">{{{ include_static 'generated/import-map.json' }}}</script>
|
||||||
}</script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const TREEHOUSE_SITE = `{{ config.site }}`;
|
const TREEHOUSE_SITE = `{{ config.site }}`;
|
||||||
const TREEHOUSE_NEWS_COUNT = {{ len feeds.news.branches }};
|
const TREEHOUSE_NEWS_COUNT = {{ len feeds.news.branches }};
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="{{ config.site }}/navmap.js"></script>
|
<script type="module">
|
||||||
<script type="module" src="{{ config.site }}/static/js/spells.js"></script>
|
import "treehouse/spells.js";
|
||||||
<script type="module" src="{{ config.site }}/static/js/ulid.js"></script>
|
import "treehouse/ulid.js";
|
||||||
<script type="module" src="{{ config.site }}/static/js/usability.js"></script>
|
import "treehouse/usability.js";
|
||||||
<script type="module" src="{{ config.site }}/static/js/settings.js"></script>
|
import "treehouse/settings.js";
|
||||||
<script type="module" src="{{ config.site }}/static/js/tree.js"></script>
|
import "treehouse/tree.js";
|
||||||
<script type="module" src="{{ config.site }}/static/js/emoji.js"></script>
|
import "treehouse/emoji.js";
|
||||||
<script type="module" src="{{ config.site }}/static/js/news.js"></script>
|
import "treehouse/news.js";
|
||||||
|
</script>
|
||||||
|
|
||||||
<meta property="og:site_name" content="{{ config.user.title }}">
|
<meta property="og:site_name" content="{{ config.user.title }}">
|
||||||
<meta property="og:title" content="{{ page.title }}">
|
<meta property="og:title" content="{{ page.title }}">
|
||||||
|
|
|
@ -4,12 +4,15 @@
|
||||||
extracting them way more painful than it needs to be. --}}
|
extracting them way more painful than it needs to be. --}}
|
||||||
|
|
||||||
{{#each page.styles}}
|
{{#each page.styles}}
|
||||||
<link rel="stylesheet" href="{{ ../config.site }}/static/css/{{ this }}">
|
<link rel="stylesheet" href="{{ asset (cat 'css/' this) }}">
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
{{#each page.scripts}}
|
<script type="module">
|
||||||
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}"></script>
|
{{!-- Go through the import map for each script. --}}
|
||||||
{{/each}}
|
{{#each page.scripts}}
|
||||||
|
import "{{ this }}";
|
||||||
|
{{/each}}
|
||||||
|
</script>
|
||||||
|
|
||||||
{{{ page.tree }}}
|
{{{ page.tree }}}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<head>
|
<head>
|
||||||
<title>treehouse iframe sandbox</title>
|
<title>treehouse iframe sandbox</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
|
<link rel="stylesheet" href="{{ asset 'css/main.css' }}">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
|
@ -21,12 +21,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="importmap">{
|
<script type="importmap">{{{ include_static 'generated/import-map.json' }}}</script>
|
||||||
"imports": {
|
|
||||||
"treehouse/": "{{ config.site }}/static/js/",
|
|
||||||
"tairu/": "{{ config.site }}/static/js/components/tairu/"
|
|
||||||
}
|
|
||||||
}</script>
|
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { evaluate, domConsole, jsConsole } from "treehouse/components/literate-programming/eval.js";
|
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]
|
[emoji]
|
||||||
|
|
||||||
[pics]
|
[pics]
|
||||||
|
|
||||||
|
[javascript]
|
||||||
|
import_roots = [
|
||||||
|
{ name = "treehouse", path = "static/js" },
|
||||||
|
{ name = "tairu", path = "static/js/components/tairu" },
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in a new issue