Use rxjs properly
This commit is contained in:
parent
6dc157e55a
commit
500361c023
|
@ -1,3 +1,3 @@
|
||||||
import { articles } from "../lib/tasks/articles.js";
|
import { articles } from "../lib/tasks/articles.js";
|
||||||
|
|
||||||
articles().then(() => console.log("Done."));
|
(async () => await articles())();
|
||||||
|
|
|
@ -1,22 +1,10 @@
|
||||||
import { Observable, from, map } from "rxjs";
|
import { Observable, Subscriber } from "rxjs";
|
||||||
import File from "vinyl";
|
import File from "vinyl";
|
||||||
import { Glob } from "glob";
|
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { join, dirname } from "node:path";
|
import { join, dirname } from "node:path";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { writeFile, mkdir } from "node:fs/promises";
|
import { writeFile, mkdir } from "node:fs/promises";
|
||||||
|
|
||||||
export function src(glob: string | string[]): Observable<Promise<File>> {
|
|
||||||
return from(new Glob(glob, {})).pipe(
|
|
||||||
map(async (path) => new File({ path, contents: await readFile(path) })),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function then<T, U>(f: (v: T) => Promise<U>) {
|
|
||||||
return (observable: Observable<Promise<T>>): Observable<Promise<U>> =>
|
|
||||||
observable.pipe(map((v) => v.then(f)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dest(prefix: string) {
|
export function dest(prefix: string) {
|
||||||
return async (file: File) => {
|
return async (file: File) => {
|
||||||
const actualPath = join(prefix, file.path);
|
const actualPath = join(prefix, file.path);
|
||||||
|
@ -30,30 +18,23 @@ export function dest(prefix: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function synchronise<T>() {
|
export const loadFile = async (path: string): Promise<File> =>
|
||||||
return (observable: Observable<Promise<T>>): Observable<T> =>
|
new File({ path, contents: await readFile(path) });
|
||||||
new Observable<T>((subscriber) => {
|
|
||||||
const runningPromises = new Set<Promise<void>>();
|
|
||||||
let done = false;
|
|
||||||
|
|
||||||
|
export function onComplete<T>(f: (sink: Subscriber<T>) => Promise<void>) {
|
||||||
|
return (observable: Observable<T>) =>
|
||||||
|
new Observable<T>((subscriber) =>
|
||||||
observable.subscribe({
|
observable.subscribe({
|
||||||
next(value) {
|
next(value) {
|
||||||
const promise = value.then((v) => {
|
subscriber.next(value);
|
||||||
runningPromises.delete(promise);
|
},
|
||||||
|
error(err) {
|
||||||
subscriber.next(v);
|
subscriber.error(err);
|
||||||
if (runningPromises.size === 0 && done) {
|
|
||||||
subscriber.complete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
runningPromises.add(promise);
|
|
||||||
},
|
},
|
||||||
complete() {
|
complete() {
|
||||||
done = true;
|
f(subscriber);
|
||||||
if (runningPromises.size === 0) {
|
subscriber.complete();
|
||||||
subscriber.complete();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { join, basename, dirname } from "node:path";
|
import { join, basename, dirname } from "node:path";
|
||||||
import asciidoctor from "asciidoctor";
|
import asciidoctor from "asciidoctor";
|
||||||
import File from "vinyl";
|
import File from "vinyl";
|
||||||
|
@ -18,8 +18,9 @@ import { renderToStaticMarkup } from "preact-render-to-string";
|
||||||
import { SITE_TITLE, SITE_DESCRIPTION } from "../constants.js";
|
import { SITE_TITLE, SITE_DESCRIPTION } from "../constants.js";
|
||||||
import { JSX } from "preact/jsx-runtime";
|
import { JSX } from "preact/jsx-runtime";
|
||||||
import { reloadAssets } from "../assets.js";
|
import { reloadAssets } from "../assets.js";
|
||||||
import { lastValueFrom } from "rxjs";
|
import { from, mergeMap, Observable, Subscriber } from "rxjs";
|
||||||
import { src, synchronise, dest, then } from "../rx-utils.js";
|
import { dest, loadFile, onComplete } from "../rx-utils.js";
|
||||||
|
import { Glob } from "glob";
|
||||||
|
|
||||||
const Asciidoctor = asciidoctor();
|
const Asciidoctor = asciidoctor();
|
||||||
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
|
const EXTENSION_REGISTRY = Asciidoctor.Extensions.create();
|
||||||
|
@ -33,7 +34,7 @@ EXTENSION_REGISTRY.inlineMacro("abbr", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function extractImages(array: File[], articlePath: string) {
|
function extractImages(sink: (image: File) => void, articlePath: string) {
|
||||||
return function () {
|
return function () {
|
||||||
return function (tree) {
|
return function (tree) {
|
||||||
visit(tree, "element", function (node: any) {
|
visit(tree, "element", function (node: any) {
|
||||||
|
@ -47,7 +48,7 @@ function extractImages(array: File[], articlePath: string) {
|
||||||
contents: readFileSync(imagePath),
|
contents: readFileSync(imagePath),
|
||||||
});
|
});
|
||||||
|
|
||||||
array.push(image);
|
sink(image);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -57,75 +58,50 @@ function renderDocument(root: JSX.Element): Buffer {
|
||||||
return Buffer.concat([Buffer.from("<!doctype html>"), Buffer.from(renderToStaticMarkup(root))]);
|
return Buffer.concat([Buffer.from("<!doctype html>"), Buffer.from(renderToStaticMarkup(root))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = (prefix: string) => (file: File) => {
|
const transformArticle =
|
||||||
const realPath = join(prefix, file.path);
|
(sink: (file: File) => void, articles: Article[]) => async (file: File) => {
|
||||||
if (!existsSync(dirname(realPath))) {
|
const slug = basename(file.path, ".asciidoc");
|
||||||
mkdirSync(dirname(realPath), { recursive: true });
|
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 };
|
||||||
|
|
||||||
console.log("[-] Outputting", join(prefix, file.path));
|
if (PRODUCTION && date.equals(DEFAULT_DATE)) {
|
||||||
writeFileSync(realPath, file.contents as Buffer);
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function articles(): Promise<void> {
|
const vfile = await unified()
|
||||||
reloadAssets();
|
.use(rehypeParse)
|
||||||
const _output = output("dist");
|
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
|
||||||
const articles: Article[] = [];
|
.use(extractImages(sink, file.path))
|
||||||
const images: File[] = [];
|
.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() }),
|
||||||
|
});
|
||||||
|
|
||||||
await lastValueFrom(
|
file.path = join(slug, "index.html");
|
||||||
src("articles/**/*.asciidoc").pipe(
|
file.contents = renderDocument(content);
|
||||||
then(async (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)) {
|
articles.push(article);
|
||||||
return null;
|
sink(file);
|
||||||
}
|
};
|
||||||
|
|
||||||
const vfile = await unified()
|
|
||||||
.use(rehypeParse)
|
|
||||||
.use(rehypeExternalLinks, { rel: "noopener", target: "_blank" })
|
|
||||||
.use(extractImages(images, 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(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
file.path = join(slug, "index.html");
|
|
||||||
file.contents = renderDocument(content);
|
|
||||||
|
|
||||||
articles.push(article);
|
|
||||||
return file;
|
|
||||||
}),
|
|
||||||
then(dest("dist")),
|
|
||||||
synchronise(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
images.forEach(_output);
|
|
||||||
|
|
||||||
|
const finalizeArticles = (articles: Article[]) => async (sink: Subscriber<File>) => {
|
||||||
articles.sort(({ date: a }, { date: b }) => b.diff(a).toMillis());
|
articles.sort(({ date: a }, { date: b }) => b.diff(a).toMillis());
|
||||||
const contents = renderLayout({
|
const contents = renderLayout({
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -137,10 +113,26 @@ export async function articles(): Promise<void> {
|
||||||
Content: () => renderIndex({ articles }),
|
Content: () => renderIndex({ articles }),
|
||||||
});
|
});
|
||||||
|
|
||||||
_output(
|
sink.next(new File({ path: "index.html", contents: renderDocument(contents) }));
|
||||||
new File({
|
};
|
||||||
path: "index.html",
|
|
||||||
contents: renderDocument(contents),
|
export const articles = () => {
|
||||||
}),
|
reloadAssets();
|
||||||
);
|
const articles = [];
|
||||||
}
|
|
||||||
|
return from(new Glob("articles/**/*.asciidoc", {}))
|
||||||
|
.pipe(
|
||||||
|
mergeMap(loadFile),
|
||||||
|
mergeMap(
|
||||||
|
(file) =>
|
||||||
|
new Observable<File>((subscriber) => {
|
||||||
|
transformArticle(
|
||||||
|
(f) => subscriber.next(f),
|
||||||
|
articles,
|
||||||
|
)(file).then(() => subscriber.complete());
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
onComplete(finalizeArticles(articles)),
|
||||||
|
)
|
||||||
|
.forEach(dest("dist"));
|
||||||
|
};
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import postcss, { Result } from "postcss";
|
import postcss, { Result } from "postcss";
|
||||||
import config from "../../postcss.config.js";
|
import config from "../../postcss.config.js";
|
||||||
import { src, then, synchronise, dest } from "../rx-utils.js";
|
import { dest, loadFile } from "../rx-utils.js";
|
||||||
import { map, lastValueFrom } from "rxjs";
|
import { from, lastValueFrom, mergeMap } from "rxjs";
|
||||||
import hashPaths from "../hash.js";
|
import hashPaths from "../hash.js";
|
||||||
|
import { Glob } from "glob";
|
||||||
|
|
||||||
export const css = () =>
|
export const css = () =>
|
||||||
lastValueFrom(
|
lastValueFrom(
|
||||||
src("src/index.css").pipe(
|
from(new Glob("src/index.css", {})).pipe(
|
||||||
then(
|
mergeMap(loadFile),
|
||||||
|
mergeMap(
|
||||||
(file) =>
|
(file) =>
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
postcss(config.plugins)
|
postcss(config.plugins)
|
||||||
|
@ -20,8 +22,7 @@ export const css = () =>
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
synchronise(),
|
|
||||||
hashPaths("css.manifest"),
|
hashPaths("css.manifest"),
|
||||||
map(dest("dist/_assets")),
|
mergeMap(dest("dist/_assets")),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,8 +3,9 @@ import tmp from "tmp";
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import { readFileSync, unlinkSync } from "node:fs";
|
import { readFileSync, unlinkSync } from "node:fs";
|
||||||
import hashPaths from "../hash.js";
|
import hashPaths from "../hash.js";
|
||||||
import { src, then, synchronise, dest } from "../rx-utils.js";
|
import { dest, loadFile } from "../rx-utils.js";
|
||||||
import { map } from "rxjs";
|
import { from, mergeMap } from "rxjs";
|
||||||
|
import { Glob } from "glob";
|
||||||
|
|
||||||
const FONT_PRESETS = {
|
const FONT_PRESETS = {
|
||||||
mono: { ranges: ["20-7F", "2205", "2E22-2E25", "2713", "2717"] },
|
mono: { ranges: ["20-7F", "2205", "2E22-2E25", "2713", "2717"] },
|
||||||
|
@ -34,9 +35,6 @@ function compileFont(font: File): Promise<File> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fonts = () =>
|
export const fonts = () =>
|
||||||
src("vendor/*.ttf").pipe(
|
from(new Glob("vendor/*.ttf", {}))
|
||||||
then(compileFont),
|
.pipe(mergeMap(loadFile), mergeMap(compileFont), hashPaths("fonts.manifest"))
|
||||||
synchronise(),
|
.forEach(dest("dist/_assets"));
|
||||||
hashPaths("fonts.manifest"),
|
|
||||||
map(dest("dist/_assets")),
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
import { Glob } from "glob";
|
||||||
import hashPaths from "../hash.js";
|
import hashPaths from "../hash.js";
|
||||||
import { src, then, synchronise, dest } from "../rx-utils.js";
|
import { dest, loadFile } from "../rx-utils.js";
|
||||||
import { map } from "rxjs";
|
import { from, mergeMap, tap } from "rxjs";
|
||||||
|
|
||||||
export const images = () =>
|
export const images = () =>
|
||||||
src("src/*.avif").pipe(
|
from(new Glob("src/*.avif", {}))
|
||||||
then(async (file) => {
|
.pipe(
|
||||||
file.path = file.path.substring(4);
|
mergeMap(loadFile),
|
||||||
return file;
|
tap((f) => (f.path = f.path.substring(4))),
|
||||||
}),
|
hashPaths("images.manifest"),
|
||||||
synchronise(),
|
)
|
||||||
hashPaths("images.manifest"),
|
.forEach(dest("dist/_assets"));
|
||||||
map(dest("dist/_assets")),
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { src, dest } from "gulp";
|
import { Glob } from "glob";
|
||||||
|
import { from, mergeMap, tap } from "rxjs";
|
||||||
|
import { dest, loadFile } from "../rx-utils.js";
|
||||||
|
|
||||||
// SVG `use` has no way of allowing cross-origin, so we need to keep them with the HTML files.
|
// SVG `use` has no way of allowing cross-origin, so we need to keep them with the HTML files.
|
||||||
export const svg = () => src("src/*.svg").pipe(dest("dist/"));
|
export const svg = () =>
|
||||||
|
from(new Glob("src/*.svg", {}))
|
||||||
|
.pipe(
|
||||||
|
mergeMap(loadFile),
|
||||||
|
tap((f) => (f.path = f.path.substring(4))),
|
||||||
|
)
|
||||||
|
.forEach(dest("dist"));
|
||||||
|
|
Loading…
Reference in New Issue
Block a user