diff --git a/bin/build.ts b/bin/build.ts index c61cd1a..a0ff63f 100644 --- a/bin/build.ts +++ b/bin/build.ts @@ -3,17 +3,11 @@ 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 { error } from "../lib/log.ts"; +import { Task, wrapTask } from "../lib/task.ts"; import { argv, exit } from "node:process"; -type Task = () => Promise; - -const wrapTask = (name: string, task: Task) => async () => { - console.log("[start]", name); - await task(); - console.log("[end]", name); -}; - const TASKS = new Map(Object.entries({ articles: wrapTask("articles", articles), css: wrapTask("css", css), @@ -28,7 +22,7 @@ const args = argv.slice(2); await (args.length ? args : ALL_TASKS) .map((task: string) => { if (!TASKS.has(task)) { - console.error("Unknown task", task); + error("Unknown task", { task }); exit(1); } diff --git a/bin/watch.ts b/bin/watch.ts index d49c0fb..a620a17 100644 --- a/bin/watch.ts +++ b/bin/watch.ts @@ -1,8 +1,9 @@ import chokidar from "chokidar"; +import { debug, info } from "../lib/log.ts"; const watch = (glob: string | string[], task: string) => chokidar.watch(glob).on("change", (path: string) => { - console.log("[change]", path, "->", task); + debug("File changed", { path, task }); return new Promise((resolve) => { const worker = new Worker(import.meta.resolve("../lib/watch-worker.ts"), { type: "module" }); worker.addEventListener("message", () => { @@ -20,3 +21,5 @@ watch("src/*.svg", "svg"); watch("dist/_assets/fonts.manifest", "css"); watch("dist/_assets/css.manifest", "articles"); + +info("Started"); diff --git a/deno.jsonc b/deno.jsonc index 65c82e5..841e4df 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,8 @@ { "imports": { + "@std/fmt": "jsr:@std/fmt@^0.225.4", "@std/io": "jsr:@std/io@^0.224.2", + "@std/log": "jsr:@std/log@^0.224.3", "asciidoctor": "https://esm.sh/asciidoctor@^3.0.4", "chokidar": "npm:chokidar@^3.6.0", "cssnano": "npm:cssnano@^7.0.2", diff --git a/deno.lock b/deno.lock index 64cd81a..2a15fc0 100644 --- a/deno.lock +++ b/deno.lock @@ -2,6 +2,10 @@ "version": "3", "packages": { "specifiers": { + "jsr:@std/fmt@^0.225.4": "jsr:@std/fmt@0.225.4", + "jsr:@std/fs@^1.0.0-rc.1": "jsr:@std/fs@1.0.0-rc.1", + "jsr:@std/io@^0.224.2": "jsr:@std/io@0.224.2", + "jsr:@std/log@^0.224.3": "jsr:@std/log@0.224.3", "npm:@mapbox/rehype-prism@^0.9.0": "npm:@mapbox/rehype-prism@0.9.0", "npm:@types/node": "npm:@types/node@18.16.19", "npm:chokidar@^3.6.0": "npm:chokidar@3.6.0", @@ -22,6 +26,25 @@ "npm:unified@^11.0.4": "npm:unified@11.0.5", "npm:unist-util-visit@^5.0.0": "npm:unist-util-visit@5.0.0" }, + "jsr": { + "@std/fmt@0.225.4": { + "integrity": "584c681cf422b70e28959b57e59012823609c087384cbf12d05f67814797fda3" + }, + "@std/fs@1.0.0-rc.1": { + "integrity": "10a78e43d89507deda6e2ce9c7aac24ba2ee468bf74da1101d95b0fbeb463aca" + }, + "@std/io@0.224.2": { + "integrity": "25ecd4f674527d660ab09e571e15eb541e8eb8a4575f2d20bdbf029374a609b5" + }, + "@std/log@0.224.3": { + "integrity": "601af539ff0c80d117fcb6cab7d9339242872d7f7f5fe4862aaf32152d86b9bf", + "dependencies": [ + "jsr:@std/fmt@^0.225.4", + "jsr:@std/fs@^1.0.0-rc.1", + "jsr:@std/io@^0.224.2" + ] + } + }, "npm": { "@csstools/selector-resolve-nested@1.1.0_postcss-selector-parser@6.1.0": { "integrity": "sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==", @@ -1745,7 +1768,9 @@ }, "workspace": { "dependencies": [ + "jsr:@std/fmt@^0.225.4", "jsr:@std/io@^0.224.2", + "jsr:@std/log@^0.224.3", "npm:@mapbox/rehype-prism@^0.9.0", "npm:chokidar@^3.6.0", "npm:cssnano@^7.0.2", diff --git a/lib/log.ts b/lib/log.ts new file mode 100644 index 0000000..d039eac --- /dev/null +++ b/lib/log.ts @@ -0,0 +1,23 @@ +import { ConsoleHandler, LogRecord, setup } from "@std/log"; +import { bold, dim, magenta } from "@std/fmt/colors"; +import { PRODUCTION } from "./environment.ts"; + +const formatter = (record: LogRecord): string => { + const params = Object.entries(record.args[0] || {}).map(([key, value]) => `${magenta(key)}: ${value.toString()}`) + .join(", "); + return `${dim(record.datetime.toISOString())} ${record.levelName.substring(0, 1)}, ${bold(record.msg)} ${params}`; +}; + +setup({ + handlers: { + default: new ConsoleHandler("DEBUG", { formatter, useColors: false }), + }, + loggers: { + default: { + level: PRODUCTION ? "INFO" : "DEBUG", + handlers: ["default"], + }, + }, +}); + +export { debug, error, info } from "@std/log"; diff --git a/lib/rx-utils.ts b/lib/rx-utils.ts index 858cf02..13fa333 100644 --- a/lib/rx-utils.ts +++ b/lib/rx-utils.ts @@ -5,6 +5,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { Glob } from "glob"; import { VFile } from "vfile"; import { read } from "to-vfile"; +import { info } from "./log.ts"; export function dest(prefix: string) { return async (file: VFile) => { @@ -13,7 +14,7 @@ export function dest(prefix: string) { await mkdir(dirname(actualPath), { recursive: true }); } await writeFile(actualPath, file.value); - console.log("[-] Written", actualPath); + info("Written", { path: actualPath }); return file; }; diff --git a/lib/task.ts b/lib/task.ts new file mode 100644 index 0000000..bd279d9 --- /dev/null +++ b/lib/task.ts @@ -0,0 +1,11 @@ +import { debug, info } from "./log.ts"; +import { DateTime } from "luxon"; + +export type Task = () => Promise; + +export const wrapTask = (name: string, task: Task) => async () => { + debug("Running task", { name }); + const start = DateTime.now(); + await task(); + info("Task finished", { name, time: `${-start.diffNow("milliseconds")} ms` }); +}; diff --git a/lib/tasks/images.ts b/lib/tasks/images.ts index 808f682..7c21905 100644 --- a/lib/tasks/images.ts +++ b/lib/tasks/images.ts @@ -1,11 +1,16 @@ +import { mergeMap } from "rxjs"; import hashPaths from "../hash.ts"; import { dest, fromGlob } from "../rx-utils.ts"; import { tap } from "rxjs"; +import { lastValueFrom } from "rxjs"; -export const images = () => - fromGlob("src/*.avif") - .pipe( - tap((f) => (f.path = f.basename!)), - hashPaths("images.manifest"), - ) - .forEach(dest("dist/_assets")); +export const images = async () => { + await lastValueFrom( + fromGlob("src/*.avif") + .pipe( + tap((f) => (f.path = f.basename!)), + hashPaths("images.manifest"), + mergeMap(dest("dist/_assets")), + ), + ); +}; diff --git a/lib/tasks/svg.ts b/lib/tasks/svg.ts index a3eb190..3f11991 100644 --- a/lib/tasks/svg.ts +++ b/lib/tasks/svg.ts @@ -1,8 +1,11 @@ -import { tap } from "rxjs"; +import { lastValueFrom, tap } from "rxjs"; import { dest, fromGlob } from "../rx-utils.ts"; +import { mergeMap } from "rxjs"; // SVG `use` has no way of allowing cross-origin, so we need to keep them with the HTML files. -export const svg = () => - fromGlob("src/*.svg") - .pipe(tap((f) => (f.path = f.basename!))) - .forEach(dest("dist")); +export const svg = async () => { + await lastValueFrom( + fromGlob("src/*.svg") + .pipe(tap((f) => (f.path = f.basename!)), mergeMap(dest("dist"))), + ); +}; diff --git a/lib/watch-worker.ts b/lib/watch-worker.ts index 6b893eb..8e79b90 100644 --- a/lib/watch-worker.ts +++ b/lib/watch-worker.ts @@ -3,15 +3,12 @@ import { css } from "./tasks/css.ts"; import { fonts } from "./tasks/fonts.ts"; import { images } from "./tasks/images.ts"; import { svg } from "./tasks/svg.ts"; +import { wrapTask } from "./task.ts"; const TASKS: { [task: string]: () => Promise } = { 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); - + await wrapTask(task, TASKS[task])(); self.postMessage({ done: true }); });