Global asset management

This commit is contained in:
Thibault “Adædra” Hamel 2024-06-24 19:31:11 +02:00
parent 4b7c669ad0
commit 73849b7f04
7 changed files with 46 additions and 49 deletions

27
lib/assets.ts Normal file
View File

@ -0,0 +1,27 @@
import { globSync } from "glob";
import { readFileSync } from "node:fs";
import { env } from "node:process";
import { join } from "node:path";
export const assetMap: Map<string, string> = new Map();
export function reloadAssets() {
assetMap.clear();
globSync("dist/_assets/*.manifest")
.map(
(manifest) =>
JSON.parse(readFileSync(manifest).toString()) as { [orig: string]: string },
)
.forEach((mapping) =>
Object.entries(mapping).forEach(([orig, hashed]) => assetMap.set(orig, hashed)),
);
}
export function asset(path: string): string {
const realPath = assetMap.has(path) ? assetMap.get(path) : path;
return env.ASSETS_HOSTS
? new URL(realPath, env.ASSETS_HOSTS).toString()
: join("/_assets/", realPath);
}

View File

@ -1,19 +1,8 @@
import { Transform } from "node:stream"; import { Transform } from "node:stream";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import File from "vinyl"; import File from "vinyl";
import { globSync } from "glob";
import { readFileSync } from "node:fs";
import { PRODUCTION } from "./environment.js"; import { PRODUCTION } from "./environment.js";
export function readAssetManifest(): { [entry: string]: string } {
return Object.assign(
{},
...globSync("dist/_assets/*.manifest").map((path) =>
JSON.parse(readFileSync(path).toString()),
),
);
}
function fileHash(buffer: Buffer) { function fileHash(buffer: Buffer) {
const hash = createHash("sha256"); const hash = createHash("sha256");
hash.update(buffer.toString()); hash.update(buffer.toString());

View File

@ -1,16 +1,16 @@
import Postcss from "postcss"; import Postcss from "postcss";
import { readAssetManifest } from "../hash.js"; import { reloadAssets, assetMap } from "../assets.js";
export default (opts: any = {}): Postcss.Plugin => { export default (): Postcss.Plugin => {
return { return {
postcssPlugin: "postcss-hashes", postcssPlugin: "postcss-hashes",
Once() { Once() {
opts._mappings = readAssetManifest(); reloadAssets();
}, },
Declaration(decl: Postcss.Declaration) { Declaration(decl: Postcss.Declaration) {
decl.value = decl.value.replace(/url\("([^"]+)"\)/, (v, url) => { decl.value = decl.value.replace(/url\("([^"]+)"\)/, (v, url) => {
if (opts._mappings.hasOwnProperty(url)) { if (assetMap.has(url)) {
return v.replace(url, opts._mappings[url]); return v.replace(url, assetMap.get(url));
} }
return v; return v;

View File

@ -1,5 +1,4 @@
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { env } from "node:process";
import { join, basename, dirname } from "node:path"; import { join, basename, dirname } from "node:path";
import { Transform } from "node:stream"; import { Transform } from "node:stream";
import asciidoctor, { Document } from "asciidoctor"; import asciidoctor, { Document } from "asciidoctor";
@ -14,13 +13,13 @@ import { PRODUCTION, DEFAULT_DATE } from "../environment.js";
import rehypeExternalLinks from "rehype-external-links"; import rehypeExternalLinks from "rehype-external-links";
import visit from "unist-util-visit"; import visit from "unist-util-visit";
import { src, dest } from "gulp"; import { src, dest } from "gulp";
import { readAssetManifest } from "../hash.js";
import renderLayout from "../../src/layout.js"; import renderLayout from "../../src/layout.js";
import renderArticleLayout from "../../src/article.js"; import renderArticleLayout from "../../src/article.js";
import renderIndex from "../../src/index.js"; import renderIndex from "../../src/index.js";
import { renderToStaticMarkup } from "preact-render-to-string"; import { renderToStaticMarkup } from "preact-render-to-string";
import { SITE_TITLE, SITE_DESCRIPTION } from "../../src/constants.js"; import { SITE_TITLE, SITE_DESCRIPTION } from "../../src/constants.js";
import { JSX } from "preact/jsx-runtime"; import { JSX } from "preact/jsx-runtime";
import { reloadAssets } from "../assets.js";
const Asciidoctor = asciidoctor(); const Asciidoctor = asciidoctor();
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create(); const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
@ -60,17 +59,7 @@ function renderDocument(root: JSX.Element): Buffer {
function renderArticle() { function renderArticle() {
const allArticles: [string, DateTime, Document][] = []; const allArticles: [string, DateTime, Document][] = [];
const assetManifest = readAssetManifest(); reloadAssets();
const asset = (path: string) => {
if (assetManifest.hasOwnProperty(path)) {
path = assetManifest[path];
}
return env.ASSETS_HOSTS
? new URL(path, env.ASSETS_HOSTS).toString()
: join("/_assets/", path);
};
return new Transform({ return new Transform({
readableObjectMode: true, readableObjectMode: true,
@ -98,7 +87,6 @@ function renderArticle() {
.use(rehypeStringify) .use(rehypeStringify)
.process(article.convert()); .process(article.convert());
const content = renderLayout({ const content = renderLayout({
asset,
title: article.getDoctitle({}) as string, title: article.getDoctitle({}) as string,
meta: { meta: {
description: article.getAttribute("description"), description: article.getAttribute("description"),
@ -112,7 +100,6 @@ function renderArticle() {
}, },
Content: () => Content: () =>
renderArticleLayout({ renderArticleLayout({
asset,
article, article,
date, date,
body: vfile.toString(), body: vfile.toString(),
@ -133,14 +120,13 @@ function renderArticle() {
try { try {
allArticles.sort(([, a], [, b]) => b.diff(a).toMillis()); allArticles.sort(([, a], [, b]) => b.diff(a).toMillis());
const contents = renderLayout({ const contents = renderLayout({
asset,
meta: { meta: {
description: SITE_DESCRIPTION, description: SITE_DESCRIPTION,
"og:title": SITE_TITLE, "og:title": SITE_TITLE,
"og:type": "website", "og:type": "website",
"og:url": `https://adaedra.eu`, "og:url": `https://adaedra.eu`,
}, },
Content: () => renderIndex({ articles: allArticles, asset }), Content: () => renderIndex({ articles: allArticles }),
}); });
this.push( this.push(

View File

@ -1,14 +1,14 @@
import { Document } from "asciidoctor"; import { Document } from "asciidoctor";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { asset } from "../lib/assets.js";
type Props = { type Props = {
article: Document; article: Document;
date: DateTime; date: DateTime;
body: string; body: string;
asset(path: string): string;
}; };
export default ({ asset, article, date, body }: Props) => ( export default ({ article, date, body }: Props) => (
<> <>
<nav> <nav>
<a href="/" title="Back to home page"> <a href="/" title="Back to home page">

View File

@ -1,11 +1,8 @@
import { Document } from "asciidoctor"; import { Document } from "asciidoctor";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { asset } from "../lib/assets.js";
type HeaderProps = { const Header = () => (
asset(path: string): string;
};
const Header = ({ asset }: HeaderProps) => (
<header class="index-header"> <header class="index-header">
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<h1>Adædra</h1> <h1>Adædra</h1>
@ -19,11 +16,11 @@ const Header = ({ asset }: HeaderProps) => (
</header> </header>
); );
type ArticleProps = { type Props = {
articles: [string, DateTime, Document][]; articles: [string, DateTime, Document][];
}; };
const Articles = ({ articles }: ArticleProps) => ( const Articles = ({ articles }: Props) => (
<> <>
<h2>Latest articles</h2> <h2>Latest articles</h2>
<ul class="index-list"> <ul class="index-list">
@ -42,8 +39,6 @@ const Articles = ({ articles }: ArticleProps) => (
</> </>
); );
type Props = HeaderProps & ArticleProps;
const SOCIALS: [string, string, string][] = [ const SOCIALS: [string, string, string][] = [
["Gitea", "https://gitea.adaedra.eu", "git"], ["Gitea", "https://gitea.adaedra.eu", "git"],
["Mastodon", "https://nerdculture.de/@adaedra", "mastodon"], ["Mastodon", "https://nerdculture.de/@adaedra", "mastodon"],
@ -74,10 +69,10 @@ const Socials = () => (
const Footer = () => <footer class="index-footer"></footer>; const Footer = () => <footer class="index-footer"></footer>;
export default ({ asset, articles }: Props) => ( export default (props: Props) => (
<> <>
<Header asset={asset} /> <Header />
<Articles articles={articles} /> <Articles {...props} />
<Socials /> <Socials />
<Footer /> <Footer />
</> </>

View File

@ -1,10 +1,10 @@
import { SITE_DEFAULT_META, SITE_TITLE } from "./constants.js"; import { SITE_DEFAULT_META, SITE_TITLE } from "./constants.js";
import { JSX } from "preact/jsx-runtime"; import { JSX } from "preact/jsx-runtime";
import { asset } from "../lib/assets.js";
type Props = { type Props = {
title?: string; title?: string;
meta?: { [name: string]: string }; meta?: { [name: string]: string };
asset(path: string): string;
} & ({ content: string } | { Content(): JSX.Element }); } & ({ content: string } | { Content(): JSX.Element });
export default (props: Props) => { export default (props: Props) => {
@ -22,7 +22,7 @@ export default (props: Props) => {
}} }}
/> />
{metaTags} {metaTags}
<link rel="stylesheet" type="text/css" href={props.asset("index.css")} /> <link rel="stylesheet" type="text/css" href={asset("index.css")} />
</head> </head>
<body> <body>
{"Content" in props ? ( {"Content" in props ? (