authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-09-27 18:13:11-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:47-07:00
log27b17df57b1d672f6fee971407b0e4d9916bbf11
treec7c0531a642031b5bcd5bec15db73ca59af17c8a
parent7773a878828806850398f80dfa514df399b2deed
signature Commit is signed but in an unrecognized format.

feat(framework): use progress in sitegen / revive sitegen

fixes #30 fixes #32

15 files changed, 213 insertions(+), 106 deletions(-)

framework/bundle.ts+17-5
......@@ -41,6 +41,8 @@ export async function bundleClientJavaScript(
4141 markoViaBuildCache(),
4242 ];
4343
44 using _ = io.start(`bundle ${entryPoints.length} files`);
45
4446 const bundle = await esbuild
4547 .build({
4648 assetNames: "/asset/[hash]",
......@@ -108,6 +110,7 @@ export async function bundleClientJavaScript(
108110
109111export type ServerPlatform = "node" | "passthru";
110112export interface ServerSideOptions {
113 progress: progress.Ref;
111114 entries: string[];
112115 viewItems: sg.FileItem[];
113116 viewRefs: incr.Ref<PageOrView>[];
......@@ -117,6 +120,7 @@ export interface ServerSideOptions {
117120 metaTemplate: meta.Template;
118121}
119122export async function bundleServerJavaScript({
123 progress,
120124 viewItems,
121125 viewRefs,
122126 styleMap,
......@@ -143,6 +147,7 @@ export async function bundleServerJavaScript({
143147 }
144148
145149 const wViewSource = incr.work(
150 progress,
146151 async (
147152 _,
148153 { viewItems, regenKeys, regenTtls }: {
......@@ -188,6 +193,7 @@ export async function bundleServerJavaScript({
188193 );
189194
190195 await incr.work(
196 progress,
191197 async (io, { regenKeys, viewItems }) => {
192198 io.writeFile(
193199 "../ts/view.d.ts",
......@@ -212,8 +218,11 @@ export async function bundleServerJavaScript({
212218 { regenKeys: Object.keys(regenKeys), viewItems },
213219 );
214220
215 const wBundles = entries.map((entry) =>
216 incr.work(async (io, entry) => {
221 const wBundles = entries.map((entry) => {
222 const node = progress.start(incr.toRel(entry), { passive: true });
223 return incr.work(node, async (io, entry) => {
224 using _ = io.start("esbuild");
225
217226 const pkg = await io.readJson<{
218227 dependencies: Record<string, string>;
219228 }>("package.json");
......@@ -313,12 +322,14 @@ export async function bundleServerJavaScript({
313322 }
314323 }
315324 return fileWithMagicWord;
316 }, entry)
317 );
325 }, entry);
326 });
318327
319328 const wProcessed = wBundles.map(async (wBundle) => {
320329 if (!(await wBundle)) return;
321 await incr.work(async (io) => {
330 await incr.work(wBundle.progress, async (io) => {
331 using _ = io.start("postprocess");
332
322333 // Only the reachable resources need to be read and inserted into the bundle.
323334 // This is what Map<string, incr.Ref> is for
324335 const { basename, bytes, magicWord } = UNWRAP(await io.readWork(wBundle));
......@@ -380,3 +391,4 @@ import * as sg from "#sitegen";
380391import type { PageOrView } from "./generate.ts";
381392import type * as view from "#sitegen/view";
382393import * as meta from "#sitegen/meta";
394import * as progress from "lib/log/progress.ts";
framework/css.ts+1
......@@ -62,6 +62,7 @@ export async function bundleCssFiles(
6262 dev: boolean;
6363 },
6464) {
65 using _ = io.start(styleKey(cssImports, theme));
6566 cssImports = await Promise.all(
6667 cssImports.map((file) => io.trackFile("src/" + file)),
6768 );
framework/font.ts+36-19
......@@ -1,14 +1,17 @@
1export async function buildFonts(fonts: sg.Font[]) {
1export async function buildFonts(p: progress.Ref, fonts: sg.Font[]) {
22 if (fonts.length === 0) return;
3 using node = p.start("build fonts");
34
45 const temp = path.resolve(".clover/font");
56 const venv = path.join(temp, "venv");
67 const bin = path.join(venv, "bin");
78
89 if (!fs.existsSync(venv)) {
10 using _ = node.start("setup python env");
911 await subprocess.exec("python", ["-m", "venv", venv]);
1012 }
1113 if (!fs.existsSync(path.join(bin, "pyftsubset"))) {
14 using _ = node.start("install pyftsubset");
1215 await subprocess.exec(path.join(bin, "pip"), [
1316 "install",
1417 "fonttools==4.38.0",
......@@ -17,19 +20,27 @@ export async function buildFonts(fonts: sg.Font[]) {
1720 }
1821
1922 const instances = new async.OnceMap<void>();
20 async function makeInstance(input: string, vars: sg.FontVars) {
21 const args = Object.entries(vars).map((x) => {
23 function getArgs(vars: sg.FontVars) {
24 return Object.entries(vars).map((x) => {
2225 const lower = x[0].toLowerCase();
2326 const k = lower === "wght" || lower === "slnt" ? lower : x[0];
2427 const v = Array.isArray(x[1]) ? x[1].join(":") : x[1];
2528 return `${k}=${v}`;
2629 }).sort();
27 const hash = crypto
30 }
31
32 function getHash(input: string, args: string[]) {
33 return crypto
2834 .createHash("sha256")
2935 .update(`${input}${args.join(" ")}`)
3036 .digest("hex")
3137 .slice(0, 16)
3238 .toLowerCase();
39 }
40
41 async function makeInstance(input: string, vars: sg.FontVars) {
42 const args = getArgs(vars);
43 const hash = getHash(input, args);
3344 const outfile = path.join(temp, `${hash}${path.extname(input)}`);
3445 await instances.get(outfile, async () => {
3546 await fs.rm(outfile, { force: true });
......@@ -45,32 +56,37 @@ export async function buildFonts(fonts: sg.Font[]) {
4556 }
4657
4758 await Promise.all(fonts.map((font) =>
48 incr.work(async (io, font) => {
59 incr.work(node, async (io, font) => {
60 using node = io.start(font.name);
4961 const baseFile = await fetchFont(font.name, font.sources);
5062 await Promise.all(font.subsets.map(async (subset) => {
51 let file = baseFile;
52 if (subset.vars) {
53 file = await makeInstance(baseFile, subset.vars);
54 }
63 const args = getArgs(subset.vars ?? {});
5564 const unicodes = fontRangesToString(
5665 Array.isArray(subset.unicodes) ? subset.unicodes : [subset.unicodes],
5766 );
5867 const hash = crypto
5968 .createHash("sha256")
60 .update(`${file}${unicodes}`)
69 .update(`${baseFile}${args.join()}${unicodes}`)
6170 .digest("hex")
6271 .slice(0, 16)
6372 .toLowerCase();
6473 const woff = path.join(temp, hash + ".woff2");
65 await subprocess.exec(path.join(bin, "pyftsubset"), [
66 file,
67 "--flavor=woff2",
68 `--output-file=${woff}`,
69 ...subset.layoutFeatures
70 ? [`--layout-features=${subset.layoutFeatures.join(",")}`]
71 : [],
72 `--unicodes=${unicodes}`,
73 ]);
74 if (!fs.existsSync(woff)) {
75 using _ = node.start(hash);
76 let file = baseFile;
77 if (subset.vars) {
78 file = await makeInstance(baseFile, subset.vars);
79 }
80 await subprocess.exec(path.join(bin, "pyftsubset"), [
81 file,
82 "--flavor=woff2",
83 `--output-file=${woff}`,
84 ...subset.layoutFeatures
85 ? [`--layout-features=${subset.layoutFeatures.join(",")}`]
86 : [],
87 `--unicodes=${unicodes}`,
88 ]);
89 }
7490 await io.writeAsset({
7591 pathname: subset.asset.replace("[ext]", "woff2"),
7692 buffer: await fs.readFile(woff),
......@@ -195,3 +211,4 @@ import * as incr from "./incremental.ts";
195211import * as crypto from "node:crypto";
196212import * as async from "#sitegen/async";
197213import * as error from "#sitegen/error";
214import * as progress from "../lib/log/progress.ts";
framework/generate.ts+68-26
......@@ -11,44 +11,70 @@ export async function main() {
1111 await incr.compile(generate);
1212}
1313
14export async function generate() {
14export async function generate(p: progress.Ref) {
1515 // -- read config and discover files --
16 const siteConfig = await incr.work(readManifest);
16 const siteConfig = await incr.work(p, readManifest);
1717 const metaTemplate: meta.Template = {
1818 ...siteConfig.meta,
1919 base: new URL(siteConfig.meta.base),
2020 };
21 // -- start font work --
2122 const {
2223 staticFiles,
2324 scripts,
2425 views,
2526 pages,
26 } = await discoverAllFiles(siteConfig);
27 } = await discoverAllFiles(p, siteConfig);
28
29 const aliasingInputs = Array.from(
30 Map.groupBy([...staticFiles, ...pages], (x) => x.id),
31 ).filter((x) => x[1].length > 1);
32 if (
33 aliasingInputs.length > 0
34 ) {
35 throw new Error(
36 `source files alias:\n` +
37 aliasingInputs.map(([id, files]) =>
38 ` - ${id}\n` + files.map((x) => " - " + x.file).join("\n")
39 ).join("\n"),
40 );
41 }
2742
28 // TODO: make sure that `static` and `pages` does not overlap
43 const builtFonts = fonts.buildFonts(p, siteConfig.fonts);
2944
3045 // TODO: loadMarkoCache
3146
32 // -- start font work --
33 const builtFonts = fonts.buildFonts(siteConfig.fonts);
47 const pageNode = p.start("generate static pages", {});
48 const viewNode = p.start("compile dynamic views", {});
49 const staticNode = p.start("compress static asset", {});
3450
3551 // -- perform build-time rendering --
3652 const builtPages = pages.map((item) =>
37 incr.work(preparePage, { item, metaTemplate })
53 incr.work(
54 pageNode.start(item.file, { passive: true }),
55 preparePage,
56 { item, metaTemplate },
57 )
3858 );
39 const builtViews = views.map((item) => incr.work(prepareView, item));
59 const builtViews = views.map((item) =>
60 incr.work(
61 viewNode.start(item.file, { passive: true }),
62 prepareView,
63 item,
64 )
65 );
66 Promise.allSettled(builtViews).then(() => viewNode.end());
4067 const builtStaticFiles = Promise.all(
4168 staticFiles.map((item) =>
42 incr.work(
43 async (io, { id, file }) =>
44 void (await io.writeAsset({
45 pathname: id,
46 buffer: await io.readFile(file),
47 })),
48 item,
49 )
69 incr.work(staticNode, async (io, { id, file }) => {
70 using _ = io.start(file);
71 return void (await io.writeAsset({
72 pathname: id,
73 buffer: await io.readFile(file),
74 }));
75 }, item)
5076 ),
51 );
77 ).then(() => staticNode.end());
5278 const routes = await Promise.all([...builtViews, ...builtPages]);
5379
5480 const viewsAndDynPages: incr.Ref<PageOrView>[] = [
......@@ -57,15 +83,16 @@ export async function generate() {
5783 ];
5884
5985 // -- page resources --
60 const scriptMap = incr.work(bundle.bundleClientJavaScript, {
86 const scriptMap = incr.work(p, bundle.bundleClientJavaScript, {
6187 clientRefs: routes.flatMap((x) => x.clientRefs),
6288 extraPublicScripts: scripts.map((entry) => entry.file),
6389 dev: false,
6490 });
65 const styleMap = prepareInlineCss(routes);
91 const styleMap = prepareInlineCss(p, routes);
6692
6793 // -- backend --
6894 const builtBackend = bundle.bundleServerJavaScript({
95 progress: p,
6996 entries: siteConfig.backends,
7097 platform: "node",
7198 styleMap,
......@@ -79,19 +106,20 @@ export async function generate() {
79106 });
80107
81108 // -- assemble page assets --
82 const pAssemblePages = builtPages.map((page) =>
83 assembleAndWritePage(page, styleMap, scriptMap)
84 );
109 const pAssemblePages = Promise.all(
110 builtPages.map((page) => assembleAndWritePage(page, styleMap, scriptMap)),
111 ).then(() => pageNode.end());
85112
86113 await Promise.all([
87114 builtBackend,
88115 builtStaticFiles,
89 ...pAssemblePages,
116 pAssemblePages,
90117 builtFonts,
91118 ]);
92119}
93120
94121export async function readManifest(io: Io) {
122 using _ = io.start("read site manifest");
95123 const cfg = await io.import<typeof import("../src/site.ts")>("src/site.ts");
96124 return {
97125 siteSections: cfg.siteSections.map((section) => ({
......@@ -107,12 +135,14 @@ export async function readManifest(io: Io) {
107135}
108136
109137export async function discoverAllFiles(
138 p: progress.Ref,
110139 siteConfig: Awaited<ReturnType<typeof readManifest>>,
111140) {
141 using node = p.start("discover files");
112142 return (
113143 await Promise.all(
114144 siteConfig.siteSections.map(({ root: sectionRoot }) =>
115 incr.work(scanSiteSection, toAbs(sectionRoot))
145 incr.work(node, scanSiteSection, toAbs(sectionRoot))
116146 ),
117147 )
118148 ).reduce((acc, next) => ({
......@@ -124,6 +154,7 @@ export async function discoverAllFiles(
124154}
125155
126156export async function scanSiteSection(io: Io, sectionRoot: string) {
157 using _ = io.start(toRel(sectionRoot));
127158 // Static files are compressed and served as-is.
128159 // - "{section}/static/*.png"
129160 let staticFiles: FileItem[] = [];
......@@ -274,7 +305,7 @@ export async function preparePage(
274305 cssImports,
275306 theme: theme ?? null,
276307 styleKey,
277 clientRefs: Array.from(addon[sg.userData.key].scripts),
308 clientRefs: Array.from(UNWRAP(addon[sg.userData.key]).scripts),
278309 } as const;
279310}
280311
......@@ -303,16 +334,19 @@ export async function prepareView(io: Io, item: sg.FileItem) {
303334export type PreparedView = Awaited<ReturnType<typeof prepareView>>;
304335
305336export function prepareInlineCss(
337 p: progress.Ref,
306338 items: Array<{ styleKey: string; cssImports: string[]; theme: css.Theme }>,
307339) {
340 const node = p.start("bundle css");
308341 const map = new Map<string, incr.Ref<string>>();
309342 for (const { styleKey, cssImports, theme } of items) {
310343 if (map.has(styleKey)) continue;
311344 map.set(
312345 styleKey,
313 incr.work(css.bundleCssFiles, { cssImports, theme, dev: false }),
346 incr.work(node, css.bundleCssFiles, { cssImports, theme, dev: false }),
314347 );
315348 }
349 Promise.allSettled([...map.values()]).then(() => node.end());
316350 return map;
317351}
318352
......@@ -323,12 +357,18 @@ export async function assembleAndWritePage(
323357 scriptWork: incr.Ref<Record<string, string>>,
324358) {
325359 const page = await pageWork;
360 ASSERT("passive" in pageWork.progress);
361 const pageNode = pageWork.progress;
326362 return incr.work(
363 pageWork.progress,
327364 async (io, { id, html, meta, styleKey, clientRefs, regenerate }) => {
328365 const inlineCss = await io.readWork(UNWRAP(styleMap.get(styleKey)));
329366
330367 const scriptIds = clientRefs.map(hot.getScriptId);
331368 const scriptMap = await io.readWork(scriptWork);
369
370 pageNode.passive = false;
371
332372 const scripts = scriptIds
333373 .map((ref) => UNWRAP(scriptMap[ref], `Missing script ${ref}`))
334374 .map((x) => `{${x}}`)
......@@ -348,6 +388,7 @@ export async function assembleAndWritePage(
348388 },
349389 regenerative: !!regenerate,
350390 });
391 pageNode.passive = true;
351392 },
352393 page,
353394 );
......@@ -362,6 +403,7 @@ import * as meta from "#sitegen/meta";
362403import * as render from "#engine/render";
363404import * as sg from "#sitegen";
364405import type { FileItem } from "#sitegen";
406import * as progress from "../lib/log/progress.ts";
365407
366408import * as bundle from "./bundle.ts";
367409import * as css from "./css.ts";
framework/incremental.ts+40-26
......@@ -18,6 +18,8 @@ export interface Ref<T> {
1818 /** This method is compatible with `await` syntax */
1919 then(resolve: (value: T) => void, reject: (error: unknown) => void): void;
2020 get value(): T | null;
21 /** The progress node passed into the input */
22 progress: progress.Node | progress.Ref;
2123}
2224type Job<I = any, O = any> = (io: Io, input: I) => Promise<O>;
2325
......@@ -29,9 +31,13 @@ type Job<I = any, O = any> = (io: Io, input: I) => Promise<O>;
2931 *
3032 * Work items are lazy, only started when `Ref` is awaited or `io.readWork`ed.
3133 */
32export function work<O>(job: Job<void, O>): Ref<O>;
33export function work<I, O>(job: Job<I, O>, input: I): Ref<O>;
34export function work<I, O>(job: Job<I, O>, input: I = null as I): Ref<O> {
34export function work<O>(p: progress.Ref, job: Job<void, O>): Ref<O>;
35export function work<I, O>(p: progress.Ref, job: Job<I, O>, input: I): Ref<O>;
36export function work<I, O>(
37 p: progress.Ref,
38 job: Job<I, O>,
39 input?: I,
40): Ref<O> {
3541 const source = JSON.stringify(UNWRAP(util.getCallSites(2)[1]));
3642 const keySource = [source, util.inspect(input)].join(":");
3743 const key = crypto.createHash("sha1").update(keySource).digest("base64url");
......@@ -45,15 +51,21 @@ export function work<I, O>(job: Job<I, O>, input: I = null as I): Ref<O> {
4551
4652 const prev = works.get(key) as Work<O> | null;
4753 if (prev) {
48 return { key, then: (done) => done(prev.value), value: prev.value };
54 return {
55 key,
56 then: (done) => done(prev.value),
57 value: prev.value,
58 progress: p,
59 };
4960 }
5061
5162 async function perform() {
52 const io = new Io(key);
63 const io = new Io(key, p);
5364 jobs += 1;
5465 newKeys += 1;
66 await timers.setTimeout(200);
5567 try {
56 const value = await job(io, input);
68 const value = await job(io, input as I);
5769 validateSerializable(value, "");
5870 const { reads, writes } = io;
5971 works.set(key, { value, affects: [], reads, writes });
......@@ -82,27 +94,24 @@ export function work<I, O>(job: Job<I, O>, input: I = null as I): Ref<O> {
8294 get value() {
8395 return (works.get(this.key)?.value as O) ?? null;
8496 },
97 progress: p,
8598 };
8699}
87100
88export async function compile<T>(compiler: () => Promise<T>) {
101export async function compile<T>(
102 compiler: (node: progress.Ref) => Promise<T>,
103 p: progress.Ref = progress,
104) {
89105 ASSERT(!running, `Cannot run twice at the same time.`);
106 const node = p.start("sitegen!");
90107 try {
91108 running = true;
92109 ASSERT(jobs === 0);
93110 const start = performance.now();
94 const timerSpinner = new Spinner({
95 text: () =>
96 `sitegen! [${((performance.now() - start) / 1000).toFixed(1)}s]`,
97 fps: 10,
98 });
99 using _endTimerSpinner = { [Symbol.dispose]: () => timerSpinner.stop() };
100111
101 const value = await compiler();
112 const value = await compiler(node);
102113 ASSERT(jobs === 0);
103 timerSpinner.text = "incremental flush";
104 await flush(start);
105 timerSpinner.stop();
114 await flush(node, start);
106115 return {
107116 value,
108117 watchFiles: new Set(files.keys()),
......@@ -112,6 +121,7 @@ export async function compile<T>(compiler: () => Promise<T>) {
112121 newAssets: !Array.from(seenWrites).some((x) => x.startsWith("a:")),
113122 };
114123 } finally {
124 node.end();
115125 running = false;
116126 newKeys = 0;
117127 seenWrites.clear();
......@@ -119,7 +129,8 @@ export async function compile<T>(compiler: () => Promise<T>) {
119129 }
120130}
121131
122export async function flush(start: number) {
132export async function flush(progress: progress.Node, start: number) {
133 using _ = progress.start("incremental flush");
123134 // Trim
124135 const detachedFiles = new Set<string>();
125136 const referencedAssets = new Set<string>();
......@@ -163,11 +174,11 @@ export async function flush(start: number) {
163174 const serialized = msgpackr.pack(serialize());
164175 await fs.writeMkdir(".clover/incr.state", serialized);
165176 const time = (performance.now() - start).toFixed(0);
166 console.success(`sitegen! in ${time} ms`);
167 console.writeLine(` - ${works.size} keys (${works.size - newKeys} cached)`);
168 console.writeLine(` - ${assets.size} static assets`);
169 console.writeLine(
170 ` - dist: ${formatSize(dist)},` +
177 progress.log.info(
178 `sitegen! in ${time} ms\n` +
179 ` - ${works.size} keys (${works.size - newKeys} cached)\n` +
180 ` - ${assets.size} static assets\n` +
181 ` - dist: ${formatSize(dist)},` +
171182 ` incremental: ${formatSize(serialized.byteLength)}`,
172183 );
173184}
......@@ -453,10 +464,14 @@ export function getAssetManifest() {
453464 * - Outputs written with Io are deleted when abandoned.
454465 */
455466export class Io {
456 constructor(public key: string) {}
467 constructor(public key: string, private node: progress.Ref) {}
457468 reads: Reads = { files: new Set(), works: new Set() };
458469 writes: Writes = { files: new Set(), assets: new Map() };
459470
471 start(text: string, options?: progress.StartOptions) {
472 return this.node.start(text, options);
473 }
474
460475 #trackFs(file: string) {
461476 const resolved = toAbs(file);
462477 const key = toRel(resolved);
......@@ -720,8 +735,7 @@ import * as util from "node:util";
720735import * as crypto from "node:crypto";
721736import * as zlib from "node:zlib";
722737
723import * as console from "@paperclover/console";
724import { Spinner } from "@paperclover/console/Spinner";
738import * as progress from "../lib/log/progress.ts";
725739import { formatSize } from "@/file-viewer/format.ts";
726740
727741import * as msgpackr from "msgpackr";
lib/log.ts+1-4
......@@ -66,10 +66,7 @@ export interface Widget {
6666 * may get called more often than the specified `fps`.
6767 * supports color codes but not ansi cursor movements.
6868 */
69 format(
70 now: ReturnType<typeof performance.now>,
71 focus: boolean,
72 ): string | null;
69 format(now: ReturnType<typeof performance.now>): string | null;
7370 /** 'null' to never update (use 'onChange'). defaults to 12 fps */
7471 fps?: number | null;
7572 /** Subscribe to manual widget updates. Call `rerender` when needed. */
lib/log/progress.ts+8-4
......@@ -233,7 +233,7 @@ function newNode(
233233function formatProgress(now: number, list: Internal[]): string {
234234 let out = "";
235235 for (const top of list) {
236 if (top.passive && top.children.length === 0) continue;
236 if (top.passive && !hasChildren(top)) continue;
237237 out += renderMainLine(top, now, 0) + "\n";
238238 out += renderChildren(top, now, []);
239239 }
......@@ -283,15 +283,19 @@ function renderMainLine(state: Internal, now: number, depth: number) {
283283 (state.value > 0 ? `[${state.value}] ` : "") + state.text;
284284}
285285
286function hasChildren(states: Internal): boolean {
287 return states.children.some((x) => !x.passive || hasChildren(x));
288}
289
286290function renderChildren(state: Internal, now: number, depth: boolean[]) {
287 let maxHeight = 25; // TODO:
291 let maxHeight = 50; // TODO:
288292 let truncated = 0;
289293 let out = "";
290 const { children } = state;
294 let { children } = state;
295 children = children.filter((x) => !x.passive || hasChildren(x));
291296 if (children.length === 0) return "";
292297 for (let i = 0, { length } = children; i < length; i += 1) {
293298 const child = UNWRAP(children[i]);
294 if (child.passive && child.children.length === 0) continue;
295299 let item = "";
296300 const left = depth.map((x) => x ? box.line : " ").join("");
297301 item += left + (i === length - 1 && !truncated ? box.langle : box.tee);
lib/packaging.ts+3-4
......@@ -33,12 +33,11 @@ export async function transpileForNpm(
3333 packageJson: Record<string, unknown>,
3434 tsToJsdoc?: TsToJsdoc,
3535) {
36 const libName = await Promise.resolve("ts-to-jsdoc");
3637 try {
37 // @ts-expect-error optional dependency
38 tsToJsdoc ??= await import("ts-to-jsdoc") as TsToJsdoc;
38 tsToJsdoc ??= await import(libName) as TsToJsdoc;
3939 } catch {
40 // @ts-expect-error unsupported import specifier
41 tsToJsdoc ??= await import("npm:ts-to-jsdoc@2.4.0") as TsToJsdoc;
40 tsToJsdoc ??= await import("npm:" + libName + "@2.4.0") as TsToJsdoc;
4241 }
4342 const { transpileFile } = tsToJsdoc;
4443
lib/string.ts+8
......@@ -155,4 +155,12 @@ function segmentWidth(segment: string) {
155155 );
156156}
157157
158export function escapeShellArgument(s: string): string {
159 if (s.length === 0) return "''";
160 if (/^[a-zA-Z0-9_\-.\/@,+=%:]+$/.test(s) && s[0] !== ":") return s;
161 if (!s.includes("'")) return `'${s}'`;
162 if (!/["$`\\]/.test(s)) return `"${s}"`;
163 return `'${s.replace(/'/g, "'\\''")}'`;
164}
165
158166import { UNWRAP } from "lib/assert.ts";
readme.md+10-6
......@@ -1,7 +1,9 @@
1# clover sitegen framework
1# clover sitegen framework and typescript library
22
33this repository contains clover's "sitegen" framework, which is a set of tools
4that assist building websites. these tools power <https://paperclover.net>.
4that assist building websites. additionally, this is the home of her typescript
5library `lib`, containing many high quality standalone sub-projects. all these
6tools power <https://paperclover.net>.
57
68- **HTML ("Server Side Rendering") engine written from scratch.** (~500 lines)
79 - A more practical JSX runtime (`class` instead of `className`, built-in
......@@ -18,12 +20,14 @@ that assist building websites. these tools power <https://paperclover.net>.
1820 - (TODO) Tests, Lints, and Type-checking is run alongside, and only re-runs
1921 checks when the files change. For example, changing a component re-tests
2022 only pages that use that component and re-lints only the changed file.
21- **Integrated libraries for building complex, content heavy web sites.**
2223 - Static asset serving with automatic ETag and build-time compression.
2324 - Dynamic pages. Static pages can be regenerated (and cached) at runtime.
24 - Databases with a minimal SQLite wrapper.
25 - Meta Tags and Open Graph generation.
2625 - Font subsetting to reduce page bandwidth.
26- **General purpose typescript libraries.** (See [`./lib`](./lib/readme.md))
27 - [Logging](./lib/log.ts) and [progress](./lib/log/progress.ts) indicators.
28 - [Priority Queue](./lib/queue.ts) with automatic reschedulability.
29 - [Meta Tags](./framework/lib/meta.ts) and Open Graph generation.
30 - Databases with a minimal SQLite wrapper.
2731- **Built on the stable and battle-tested Node.js runtime.**
2832
2933none of these tools are complex or revolutionary. rather, this project is the
......@@ -32,7 +36,7 @@ example on how other over-complicate other frameworks.
3236
3337[Marko]: https://next.markojs.com
3438
35Included is `src`, which contains `paperclover.net`. Website highlights:
39Included is `src`, which contains `paperclover.net`'s source code. Highlights:
3640
3741- TODO: flashy homepage.
3842- [Question/Answer board, custom markdown parser and components][q+a].
run.js-2
......@@ -53,8 +53,6 @@ process.on("uncaughtException", (error) => {
5353 process.exit(1);
5454});
5555
56hot.load("node:fs").readFileSync(undefined);
57
5856// Load with hooks
5957if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) {
6058 if (process.argv.length == 2) {
src/file-viewer/backend.tsx+1-1
......@@ -126,7 +126,7 @@ app.get("/file/*", async (c, next) => {
126126 rangeHeader = null;
127127 }
128128
129 const found = await cache.read(filePath
129 const found = await cache.read(filePath);
130130 // foundFile = await cache.read(filePath, encoding, {
131131 // start: rangeHeader ? rangeHeader[0] : 0,
132132 // end: rangeHeader ? rangeHeader[1] : Infinity,
src/file-viewer/bin/scan3.ts+6-1
......@@ -22,6 +22,7 @@ export async function main() {
2222 const walkQueue = new queue.PriorityQueue(10);
2323
2424 const dirsNode = progress.start("Walk Tree", { estimate: 1 });
25 dirsNode.passive = true;
2526 const fileNode = progress.start("Process File", { estimate: 0 });
2627 fileNode.sortChildren = (a, b) => {
2728 const ac = a.children.length > 0 ? 1 : 0;
......@@ -260,7 +261,11 @@ export async function main() {
260261 }
261262
262263 // Add the root & recursively iterate!
263 scanDirectory(Path.resolve(root));
264 const rootPath = Path.resolve(root);
265 if (!rootPath.ifExistsSync()) {
266 throw new Error(`file store ${rootPath} is not mounted`);
267 }
268 scanDirectory(rootPath);
264269 // await qList.done();
265270 // await qMeta.done();
266271 // await qProcess.done();
src/file-viewer/ffmpeg.ts+3-2
......@@ -61,7 +61,7 @@ export async function spawn(options: SpawnOptions) {
6161 if (code !== 0) {
6262 const fmt = code ? `code ${code}` : `signal ${signal}`;
6363 const e: any = new Error(`ffmpeg failed with ${fmt}`);
64 e.args = [ffmpeg, ...args].join(" ");
64 e.args = [ffmpeg, ...args].map(string.escapeShellArgument).join(" ");
6565 e.code = code;
6666 e.signal = signal;
6767 throw e;
......@@ -155,7 +155,7 @@ export class Parse {
155155 }
156156 }
157157
158 const hlsMatch = line.match(/^\[hls @ .*Opening '(.+)' for writing/);
158 const hlsMatch = line.match(/.*Opening '(.+)' for writing/);
159159 if (hlsMatch) {
160160 if (!hlsMatch[1].endsWith(".tmp")) {
161161 this.hlsFile = path.basename(hlsMatch[1]);
......@@ -179,3 +179,4 @@ import events from "node:events";
179179import * as path from "node:path";
180180import * as progress from "../../lib/log/progress.ts";
181181import type { Path } from "#sitegen/path";
182import * as string from "lib/string.ts";
src/file-viewer/transcode-rules.ts+11-6
......@@ -220,12 +220,17 @@ export function getMpegDashArgs(
220220 cmd.push("-use_template", "1");
221221
222222 // build adaptation sets string
223 const videoIndices = videos.map((_, i) => i).join(",");
224 const audioIndices = audios.map((_, i) => videos.length + i).join(",");
225 cmd.push(
226 "-adaptation_sets",
227 `id=0,streams=${videoIndices} id=1,streams=${audioIndices}`,
228 );
223 const videoAdaptationSets = videos.map((_, i) => `id=${i},streams=${i}`)
224 .join(" ");
225 let adaptationSets = videoAdaptationSets;
226
227 if (audios.length > 0) {
228 const audioId = videos.length;
229 const audioIndices = audios.map((_, i) => videos.length + i).join(",");
230 adaptationSets += ` id=${audioId},streams=${audioIndices}`;
231 }
232
233 cmd.push("-adaptation_sets", adaptationSets);
229234
230235 cmd.push("-y");
231236 cmd.push(outDir.join("dash.mpd").toString());