Switch to deno

This commit is contained in:
Thibault “Adædra” Hamel 2024-06-27 06:22:58 +02:00
parent 4b4aa3ff79
commit 40723b32cf
25 changed files with 1996 additions and 5969 deletions

View File

@ -2,4 +2,3 @@ root = true
[*]
indent_size = 4
max_line_length = 100

View File

@ -1,8 +1,8 @@
import { articles } from "../lib/tasks/articles.js";
import { css } from "../lib/tasks/css.js";
import { fonts } from "../lib/tasks/fonts.js";
import { images } from "../lib/tasks/images.js";
import { svg } from "../lib/tasks/svg.js";
import { articles } from "../lib/tasks/articles.ts";
import { css } from "../lib/tasks/css.ts";
import { fonts } from "../lib/tasks/fonts.ts";
import { images } from "../lib/tasks/images.ts";
import { svg } from "../lib/tasks/svg.ts";
import { argv, exit } from "node:process";
@ -12,7 +12,7 @@ const wrapTask = (name: string, task: () => Promise<void>) => async () => {
console.log("[end]", name);
};
const TASKS = {
const TASKS: { [task: string]: () => Promise<void> } = {
articles: wrapTask("articles", articles),
css: wrapTask("css", css),
fonts: wrapTask("fonts", fonts),
@ -23,8 +23,9 @@ const TASKS = {
const ALL_TASKS = ["fonts", "images", "svg", "css", "articles"];
const args = argv.slice(2);
await (args.length ? args : ALL_TASKS)
.map((task) => {
console.log("args", args);
(args.length ? args : ALL_TASKS)
.map((task: string) => {
if (!TASKS.hasOwnProperty(task)) {
console.error("Unknown task", task);
exit(1);
@ -32,4 +33,4 @@ await (args.length ? args : ALL_TASKS)
return TASKS[task];
})
.reduce((prev, cur) => prev.then(cur), Promise.resolve());
.reduce((prev: Promise<void>, cur: () => Promise<void>) => prev.then(cur), Promise.resolve());

View File

@ -1,12 +1,15 @@
import { Worker } from "node:worker_threads";
import chokidar from "chokidar";
const watch = (glob: string | string[], task: string) =>
chokidar.watch(glob).on("change", (path: string) => {
console.log("[change]", path, "->", task);
return new Promise<void>((resolve) => {
const worker = new Worker(new URL(import.meta.resolve("./build.ts")), { argv: [task] });
worker.on("exit", () => resolve());
const worker = new Worker(import.meta.resolve("../lib/watch-worker.ts"), { type: "module" });
worker.addEventListener("message", () => {
worker.terminate();
resolve();
});
worker.postMessage({ task });
});
});

37
deno.jsonc Normal file
View File

@ -0,0 +1,37 @@
{
"imports": {
"@std/io": "jsr:@std/io@^0.224.2",
"asciidoctor": "https://esm.sh/asciidoctor@^3.0.4",
"chokidar": "npm:chokidar@^3.6.0",
"cssnano": "npm:cssnano@^7.0.2",
"glob": "npm:glob@^10.4.2",
"luxon": "npm:luxon@^3.4.4",
"postcss": "npm:postcss@^8.4.38",
"postcss-import": "npm:postcss-import@^16.1.0",
"postcss-nesting": "npm:postcss-nesting@^12.1.5",
"preact": "npm:preact@^10.22.0",
"preact-render-to-string": "npm:preact-render-to-string@^6.5.5",
"rehype": "npm:rehype@^13.0.1",
"rehype-external-links": "npm:rehype-external-links@^3.0.0",
"rehype-parse": "npm:rehype-parse@^9.0.0",
"rehype-preset-minify": "npm:rehype-preset-minify@^7.0.0",
"rehype-prism": "npm:@mapbox/rehype-prism@^0.9.0",
"rehype-stringify": "npm:rehype-stringify@^10.0.0",
"rxjs": "npm:rxjs@^7.8.1",
"tmp": "npm:tmp@^0.2.3",
"unified": "npm:unified@^11.0.4",
"unist-util-visit": "npm:unist-util-visit@^5.0.0",
"vinyl": "npm:vinyl@^3.0.0"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"fmt": {
"lineWidth": 120
},
"tasks": {
"build": "deno run -A bin/build.ts",
"watch": "deno run -A bin/watch.ts"
}
}

1824
deno.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1717646450,
"narHash": "sha256-KE+UmfSVk5PG8jdKdclPVcMrUB8yVZHbsjo7ZT1Bm3c=",
"lastModified": 1719379843,
"narHash": "sha256-u+D+IOAMMl70+CJ9NKB+RMrASjInuIWMHzjLWQjPZ6c=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "818dbe2f96df233d2041739d6079bb616d3e5597",
"rev": "b3f3c1b13fb08f3828442ee86630362e81136bbc",
"type": "github"
},
"original": {

View File

@ -6,7 +6,7 @@
pkgs = import nixpkgs { inherit system; };
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ nodejs_20 python312Packages.fonttools python312Packages.brotli libavif pandoc imagemagick ];
buildInputs = with pkgs; [ deno python312Packages.fonttools python312Packages.brotli libavif pandoc imagemagick ];
};
});
}

View File

@ -10,18 +10,13 @@ export function reloadAssets() {
globSync("dist/_assets/*.manifest")
.map(
(manifest) =>
JSON.parse(readFileSync(manifest).toString()) as { [orig: string]: string },
(manifest) => JSON.parse(readFileSync(manifest).toString()) as { [orig: string]: string },
)
.forEach((mapping) =>
Object.entries(mapping).forEach(([orig, hashed]) => assetMap.set(orig, hashed)),
);
.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;
const realPath = assetMap.has(path) ? assetMap.get(path)! : path;
return env.ASSETS_HOSTS
? new URL(realPath, env.ASSETS_HOSTS).toString()
: join("/_assets/", realPath);
return env.ASSETS_HOSTS ? new URL(realPath, env.ASSETS_HOSTS).toString() : join("/_assets/", realPath);
}

View File

@ -1,7 +1,8 @@
import { createHash } from "node:crypto";
import File from "vinyl";
import { PRODUCTION } from "./environment.js";
import { PRODUCTION } from "./environment.ts";
import { Observable } from "rxjs";
import { Buffer } from "node:buffer";
function fileHash(buffer: Buffer) {
const hash = createHash("sha256");
@ -9,21 +10,19 @@ function fileHash(buffer: Buffer) {
return hash.digest("hex");
}
const hashPath =
(mappings: Map<string, string>) =>
(file: File): File => {
const hash = PRODUCTION ? fileHash(file.contents as Buffer) : "00000000";
const newName = [
file.basename.substring(0, file.basename.length - file.extname.length),
hash.substring(0, 8),
file.extname.substring(1),
].join(".");
const hashPath = (mappings: Map<string, string>) => (file: File): File => {
const hash = PRODUCTION ? fileHash(file.contents as Buffer) : "00000000";
const newName = [
file.basename.substring(0, file.basename.length - file.extname.length),
hash.substring(0, 8),
file.extname.substring(1),
].join(".");
mappings.set(file.basename, newName);
file.basename = newName;
mappings.set(file.basename, newName);
file.basename = newName;
return file;
};
return file;
};
export default function (manifestName: string) {
return (observable: Observable<File>): Observable<File> =>

View File

@ -1,5 +1,5 @@
import Postcss from "postcss";
import { reloadAssets, assetMap } from "../assets.js";
import { assetMap, reloadAssets } from "../assets.ts";
export default (): Postcss.Plugin => {
return {
@ -8,9 +8,9 @@ export default (): Postcss.Plugin => {
reloadAssets();
},
Declaration(decl: Postcss.Declaration) {
decl.value = decl.value.replace(/url\("([^"]+)"\)/, (v, url) => {
decl.value = decl.value.replace(/url\("([^"]+)"\)/, (v: string, url: string) => {
if (assetMap.has(url)) {
return v.replace(url, assetMap.get(url));
return v.replace(url, assetMap.get(url)!);
}
return v;

View File

@ -1,9 +1,9 @@
import { Observable, Subscriber, from, mergeMap } from "rxjs";
import { from, mergeMap, Observable, Subscriber } from "rxjs";
import File from "vinyl";
import { readFile } from "node:fs/promises";
import { join, dirname } from "node:path";
import { dirname, join } from "node:path";
import { existsSync } from "node:fs";
import { writeFile, mkdir } from "node:fs/promises";
import { mkdir, writeFile } from "node:fs/promises";
import { Glob } from "glob";
export function dest(prefix: string) {
@ -33,12 +33,11 @@ export function onComplete<T>(f: (sink: Subscriber<T>) => Promise<void>) {
f(subscriber);
subscriber.complete();
},
}),
})
);
}
const loadFile = async (path: string): Promise<File> =>
new File({ path, contents: await readFile(path) });
const loadFile = async (path: string): Promise<File> => new File({ path, contents: await readFile(path) });
export const fromGlob = (paths: string | string[]): Observable<File> =>
from(new Glob(paths, {})).pipe(mergeMap(loadFile));

View File

@ -1,5 +1,5 @@
import { readFileSync } from "node:fs";
import { join, basename, dirname } from "node:path";
import { basename, dirname, join } from "node:path";
import asciidoctor from "asciidoctor";
import File from "vinyl";
import { DateTime } from "luxon";
@ -7,21 +7,22 @@ import { unified } from "unified";
import rehypeParse from "rehype-parse";
import rehypeStringify from "rehype-stringify";
import rehypePresetMinify from "rehype-preset-minify";
import rehypePrism from "@mapbox/rehype-prism";
import { PRODUCTION, DEFAULT_DATE } from "../environment.js";
import rehypePrism from "rehype-prism";
import { DEFAULT_DATE, PRODUCTION } from "../environment.ts";
import rehypeExternalLinks from "rehype-external-links";
import visit from "unist-util-visit";
import renderLayout from "../views/layout.js";
import renderArticleLayout, { Article } from "../views/article.js";
import renderIndex from "../views/index.js";
import { visit } from "unist-util-visit";
import renderLayout from "../views/layout.tsx";
import renderArticleLayout, { Article } from "../views/article.tsx";
import renderIndex from "../views/index.tsx";
import { renderToStaticMarkup } from "preact-render-to-string";
import { SITE_TITLE, SITE_DESCRIPTION } from "../constants.js";
import { SITE_DESCRIPTION, SITE_TITLE } from "../constants.ts";
import { JSX } from "preact/jsx-runtime";
import { reloadAssets } from "../assets.js";
import { reloadAssets } from "../assets.ts";
import { lastValueFrom, mergeMap, Observable, Subscriber } from "rxjs";
import { dest, fromGlob, onComplete } from "../rx-utils.js";
import { dest, fromGlob, onComplete } from "../rx-utils.ts";
import { Buffer } from "node:buffer";
const Asciidoctor = (asciidoctor as unknown as () => asciidoctor.Asciidoctor)();
const Asciidoctor = asciidoctor();
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
EXTENSION_REGISTRY.inlineMacro("abbr", function () {
this.process(function (parent, target, attributes) {
@ -35,7 +36,7 @@ EXTENSION_REGISTRY.inlineMacro("abbr", function () {
function extractImages(sink: (image: File) => void, articlePath: string) {
return function () {
return function (tree) {
return function (tree: any) {
visit(tree, "element", function (node: any) {
if (node.tagName !== "img") {
return;
@ -57,48 +58,47 @@ function renderDocument(root: JSX.Element): Buffer {
return Buffer.concat([Buffer.from("<!doctype html>"), Buffer.from(renderToStaticMarkup(root))]);
}
const transformArticle =
(sink: (file: File) => void, articles: Article[]) => async (file: File) => {
const slug = basename(file.path, ".asciidoc");
const document = Asciidoctor.load(file.contents.toString(), {
extension_registry: EXTENSION_REGISTRY,
});
const date = DateTime.fromISO(document.getAttribute("docdate", { zone: "UTC" }));
const article = { path: file.path, slug, date, document };
const transformArticle = (sink: (file: File) => void, articles: Article[]) => async (file: File) => {
const slug = basename(file.path, ".asciidoc");
const document = Asciidoctor.load(file.contents!.toString(), {
extension_registry: EXTENSION_REGISTRY,
});
const date = DateTime.fromISO(document.getAttribute("docdate", { zone: "UTC" }));
const article = { path: file.path, slug, date, document };
if (PRODUCTION && date.equals(DEFAULT_DATE)) {
return;
}
if (PRODUCTION && date.equals(DEFAULT_DATE)) {
return;
}
const vfile = await unified()
.use(rehypeParse)
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
.use(extractImages(sink, file.path))
.use(rehypePrism)
.use(rehypePresetMinify)
.use(rehypeStringify)
.process(document.convert());
const content = renderLayout({
title: document.getDoctitle({}) as string,
meta: {
description: document.getAttribute("description"),
keywords: document.getAttribute("keywords"),
"og:title": document.getDoctitle({}) as string,
"og:type": "article",
"og:article:published_time": document.getAttribute("docdate"),
"og:url": `https://adaedra.eu/${slug}/`,
"og:description": document.getAttribute("description"),
"og:site_name": SITE_TITLE,
},
Content: () => renderArticleLayout({ article, body: vfile.toString() }),
});
const vfile = await unified()
.use(rehypeParse as any)
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
.use(extractImages(sink, file.path))
.use(rehypePrism)
.use(rehypePresetMinify as any)
.use(rehypeStringify as any)
.process(document.convert());
const content = renderLayout({
title: document.getDoctitle({}) as string,
meta: {
description: document.getAttribute("description"),
keywords: document.getAttribute("keywords"),
"og:title": document.getDoctitle({}) as string,
"og:type": "article",
"og:article:published_time": document.getAttribute("docdate"),
"og:url": `https://adaedra.eu/${slug}/`,
"og:description": document.getAttribute("description"),
"og:site_name": SITE_TITLE,
},
Content: () => renderArticleLayout({ article, body: vfile.toString() }),
});
file.path = join(slug, "index.html");
file.contents = renderDocument(content);
file.path = join(slug, "index.html");
file.contents = renderDocument(content);
articles.push(article);
sink(file);
};
articles.push(article);
sink(file);
};
const finalizeArticles = (articles: Article[]) => async (sink: Subscriber<File>) => {
articles.sort(({ date: a }, { date: b }) => b.diff(a).toMillis());
@ -117,7 +117,7 @@ const finalizeArticles = (articles: Article[]) => async (sink: Subscriber<File>)
export const articles = () => {
reloadAssets();
const articles = [];
const articles: Article[] = [];
return lastValueFrom(
fromGlob("articles/**/*.asciidoc").pipe(

View File

@ -1,15 +1,17 @@
import postcss, { Result } from "postcss";
import config from "../../postcss.config.js";
import { dest, fromGlob } from "../rx-utils.js";
import config from "../../postcss.config.ts";
import { dest, fromGlob } from "../rx-utils.ts";
import { lastValueFrom, mergeMap } from "rxjs";
import hashPaths from "../hash.js";
import hashPaths from "../hash.ts";
import File from "vinyl";
import { Buffer } from "node:buffer";
export const css = () =>
lastValueFrom(
fromGlob("src/index.css").pipe(
mergeMap(
(file) =>
new Promise((resolve) =>
new Promise<File>((resolve) =>
postcss(config.plugins)
.process(file.contents, { from: file.path })
.then((result: Result) => {
@ -17,7 +19,7 @@ export const css = () =>
file.path = "index.css";
resolve(file);
}),
})
),
),
hashPaths("css.manifest"),

View File

@ -2,17 +2,17 @@ import File from "vinyl";
import tmp from "tmp";
import { execFile } from "node:child_process";
import { readFileSync, unlinkSync } from "node:fs";
import hashPaths from "../hash.js";
import { dest, fromGlob } from "../rx-utils.js";
import hashPaths from "../hash.ts";
import { dest, fromGlob } from "../rx-utils.ts";
import { mergeMap } from "rxjs";
const FONT_PRESETS = {
const FONT_PRESETS: { [variant: string]: { ranges: string[] } } = {
mono: { ranges: ["20-7F", "2205", "2E22-2E25", "2713", "2717"] },
text: { ranges: ["20-7F", "A0-FF", "2000-206F", "20AC"] },
};
function compileFont(font: File): Promise<File> {
const [, variant, weight] = /([A-Z][a-z]+)-(\w+)\.ttf$/.exec(font.basename);
const [, variant, weight] = /([A-Z][a-z]+)-(\w+)\.ttf$/.exec(font.basename) as string[];
const tmpOutput = tmp.fileSync({ discardDescriptor: true });
const unicodes = FONT_PRESETS[variant.toLowerCase()].ranges;
@ -25,7 +25,7 @@ function compileFont(font: File): Promise<File> {
]).once("exit", () => {
font.path = `iosevka-adaedra-${variant.toLowerCase()}-${weight.toLowerCase()}.woff2`;
font.contents = readFileSync(tmpOutput.name);
font.base = null;
(font as any).base = null;
unlinkSync(tmpOutput.name);
resolve(font);

View File

@ -1,6 +1,6 @@
import hashPaths from "../hash.js";
import { dest, fromGlob } from "../rx-utils.js";
import { mergeMap, tap } from "rxjs";
import hashPaths from "../hash.ts";
import { dest, fromGlob } from "../rx-utils.ts";
import { tap } from "rxjs";
export const images = () =>
fromGlob("src/*.avif")

View File

@ -1,5 +1,5 @@
import { tap } from "rxjs";
import { dest, fromGlob } from "../rx-utils.js";
import { dest, fromGlob } from "../rx-utils.ts";
// SVG `use` has no way of allowing cross-origin, so we need to keep them with the HTML files.
export const svg = () =>

View File

@ -1,3 +0,0 @@
import { register } from "node:module";
register("ts-node/esm", import.meta.url);

View File

@ -1,6 +1,6 @@
import { Document } from "asciidoctor";
import { DateTime } from "luxon";
import { asset } from "../assets.js";
import { asset } from "../assets.ts";
type Props = {
article: Article;

View File

@ -1,5 +1,5 @@
import { asset } from "../assets.js";
import { Article } from "./article.js";
import { asset } from "../assets.ts";
import { Article } from "./article.tsx";
const Header = () => (
<header class="index-header">

View File

@ -1,6 +1,6 @@
import { SITE_DEFAULT_META, SITE_TITLE } from "../constants.js";
import { SITE_DEFAULT_META, SITE_TITLE } from "../constants.ts";
import { JSX } from "preact/jsx-runtime";
import { asset } from "../assets.js";
import { asset } from "../assets.ts";
type Props = {
title?: string;

17
lib/watch-worker.ts Normal file
View File

@ -0,0 +1,17 @@
import { articles } from "./tasks/articles.ts";
import { css } from "./tasks/css.ts";
import { fonts } from "./tasks/fonts.ts";
import { images } from "./tasks/images.ts";
import { svg } from "./tasks/svg.ts";
const TASKS: { [task: string]: () => Promise<void> } = { articles, css, fonts, images, svg };
self.addEventListener("message", async (message) => {
const { task } = message.data;
console.log("[start]", task);
await TASKS[task]();
console.log("[done]", task);
self.postMessage({ done: true });
});

5797
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +0,0 @@
{
"name": "@adaedra/blog",
"version": "0.1.0",
"private": true,
"type": "module",
"dependencies": {
"@mapbox/rehype-prism": "^0.9.0",
"asciidoctor": "^3.0.4",
"chokidar": "^3.6.0",
"cssnano": "^7.0.2",
"glob": "^10.4.2",
"luxon": "^3.4.4",
"postcss": "^8.4.38",
"postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.5",
"preact": "^10.22.0",
"preact-render-to-string": "^6.5.5",
"rehype": "^13.0.1",
"rehype-external-links": "^3.0.0",
"rehype-preset-minify": "^7.0.0",
"rxjs": "^7.8.1",
"tmp": "^0.2.3",
"ts-node": "^10.9.2",
"unified": "^11.0.4",
"vinyl": "^3.0.0"
},
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/luxon": "^3.4.2",
"@types/tmp": "^0.2.6",
"@types/vinyl": "^2.0.12",
"prettier": "^3.3.1"
},
"scripts": {
"build": "node --import ./lib/ts-loader.js ./bin/build.ts",
"watch": "node --import ./lib/ts-loader.js ./bin/watch.ts",
"dev": "python -m http.server -d dist"
}
}

View File

@ -1,9 +1,8 @@
import importPlugin from "postcss-import";
import nestingPlugin from "postcss-nesting";
import cssnanoPlugin from "cssnano";
import hashesPlugin from "./lib/postcss/hashes.js";
import hashesPlugin from "./lib/postcss/hashes.ts";
/** @type {import('postcss-load-config').Config} */
export default {
plugins: [
importPlugin(),

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"module": "NodeNext",
"moduleResolution": "NodeNext",
}
}