authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-06-15 11:35:28-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:45-07:00
logdad822b38eaec73a2696f012cd4e8a4117dd2e87
treec68c53bc6e4f35204ad662ebc970f8fc2db7dffe
parent76133659a0900c85d5f2442f79ab61732f3cb22b
signature Commit is signed but in an unrecognized format.

work on porting paperclover.net and also some tests


42 files changed, 2450 insertions(+), 503 deletions(-)

framework/backend/entry-node.ts+1
......@@ -1,4 +1,5 @@
11import "@paperclover/console/inject";
2import "#debug";
23
34const protocol = "http";
45
framework/bundle.ts+19-1
......@@ -39,6 +39,9 @@ export async function bundleClientJavaScript(
3939 write: false,
4040 metafile: true,
4141 external: ["node_modules/"],
42 jsx: "automatic",
43 jsxImportSource: "#ssr",
44 jsxDev: dev,
4245 });
4346 if (bundle.errors.length || bundle.warnings.length) {
4447 throw new AggregateError(
......@@ -135,11 +138,22 @@ export async function bundleServerJavaScript(
135138 return ({
136139 loader: "ts",
137140 contents: cacheEntry.src,
141 resolveDir: path.dirname(file),
138142 });
139143 },
140144 );
141145 },
142146 },
147 {
148 name: "replace client references",
149 setup(b) {
150 b.onLoad({ filter: /\.tsx?$/ }, async ({ path: file }) => ({
151 contents:
152 hot.resolveClientRefs(await fs.readFile(file, "utf-8"), file).code,
153 loader: path.extname(file).slice(1) as esbuild.Loader,
154 }));
155 },
156 },
143157 {
144158 name: "mark css external",
145159 setup(b) {
......@@ -154,7 +168,7 @@ export async function bundleServerJavaScript(
154168 },
155169 },
156170 ];
157 const { metafile, outputFiles, warnings } = await esbuild.build({
171 const { metafile, outputFiles } = await esbuild.build({
158172 bundle: true,
159173 chunkNames: "c.[hash]",
160174 entryNames: "server",
......@@ -169,6 +183,9 @@ export async function bundleServerJavaScript(
169183 splitting: true,
170184 write: false,
171185 metafile: true,
186 jsx: "automatic",
187 jsxImportSource: "#ssr",
188 jsxDev: false,
172189 });
173190
174191 const files: Record<string, Buffer> = {};
......@@ -273,3 +290,4 @@ import {
273290} from "./esbuild-support.ts";
274291import { Incremental } from "./incremental.ts";
275292import * as css from "./css.ts";
293import * as fs from "#sitegen/fs";
framework/debug.safe.ts created+17
......@@ -0,0 +1,17 @@
1globalThis.UNWRAP = (t, ...args) => {
2 if (t == null) {
3 throw new Error(
4 args.length > 0 ? util.format(...args) : "UNWRAP(" + t + ")",
5 );
6 }
7 return t;
8};
9globalThis.ASSERT = (t, ...args) => {
10 if (!t) {
11 throw new Error(
12 args.length > 0 ? util.format(...args) : "Assertion Failed",
13 );
14 }
15};
16
17import * as util from "node:util";
framework/engine/ssr.test.tsx+21
......@@ -18,3 +18,24 @@ test("simple tree", (t) =>
1818 ).text,
1919 '<main class="a b"><h1 style=background-color:red>hello world</h1><p>haha</p>1|0|||||</main>',
2020 ));
21test("unescaped/escaped html", (t) =>
22 t.assert.equal(
23 engine.ssrSync(<div>{engine.html("<fuck>")}{"\"&'`<>"}</div>).text,
24 "<div><fuck>&quot;&amp;&#x27;&#x60;&lt;&gt;</div>",
25 ));
26test("clsx built-in", (t) =>
27 t.assert.equal(
28 engine.ssrSync(
29 <>
30 <a class="a" />
31 <b class={null} />
32 <c class={undefined} />
33 <d class={["a", "b", null]} />
34 <e class={{ a: true, b: false }} />
35 <e
36 class={[null, "x", { z: true }, [{ m: true }, null, { v: false }]]}
37 />
38 </>,
39 ).text,
40 '<a class=a></a><b></b><c></c><d class="a b"></d><e class=a></e><e class="x z m"></e>',
41 ));
framework/esbuild-support.ts+6
......@@ -64,6 +64,11 @@ export function projectRelativeResolution(root = process.cwd() + "/src") {
6464 path: path.resolve(root, id.slice(2)),
6565 };
6666 });
67 b.onResolve({ filter: /^#/ }, ({ path: id, importer }) => {
68 return {
69 path: hot.resolveFrom(importer, id),
70 };
71 });
6772 },
6873 } satisfies esbuild.Plugin;
6974}
......@@ -71,3 +76,4 @@ export function projectRelativeResolution(root = process.cwd() + "/src") {
7176import * as esbuild from "esbuild";
7277import * as string from "#sitegen/string";
7378import * as path from "node:path";
79import * as hot from "./hot.ts";
framework/generate.ts+37-21
......@@ -74,26 +74,40 @@ export async function sitegen(
7474 dir: sectionPath("pages"),
7575 list: pages,
7676 prefix: "/",
77 exclude: [".css", ".client.ts", ".client.tsx"],
77 include: [".tsx", ".mdx", ".marko"],
78 exclude: [".client.ts", ".client.tsx"],
79 },
80 {
81 dir: sectionPath("static"),
82 list: staticFiles,
83 prefix: "/",
84 ext: true,
85 },
86 {
87 dir: sectionPath("scripts"),
88 list: scripts,
89 prefix: rootPrefix,
90 include: [".client.ts", ".client.tsx"],
7891 },
79 { dir: sectionPath("static"), list: staticFiles, prefix: "/", ext: true },
80 { dir: sectionPath("scripts"), list: scripts, prefix: rootPrefix },
8192 {
8293 dir: sectionPath("views"),
8394 list: views,
8495 prefix: rootPrefix,
85 exclude: [".css", ".client.ts", ".client.tsx"],
96 include: [".tsx", ".mdx", ".marko"],
97 exclude: [".client.ts", ".client.tsx"],
8698 },
8799 ];
88 for (const { dir, list, prefix, exclude = [], ext = false } of kinds) {
100 for (
101 const { dir, list, prefix, include = [""], exclude = [], ext = false }
102 of kinds
103 ) {
89104 const items = fs.readDirRecOptionalSync(dir);
90 item: for (const subPath of items) {
105 for (const subPath of items) {
91106 const file = path.join(dir, subPath);
92107 const stat = fs.statSync(file);
93108 if (stat.isDirectory()) continue;
94 for (const e of exclude) {
95 if (subPath.endsWith(e)) continue item;
96 }
109 if (!include.some((e) => subPath.endsWith(e))) continue;
110 if (exclude.some((e) => subPath.endsWith(e))) continue;
97111 const trim = ext
98112 ? subPath
99113 : subPath.slice(0, -path.extname(subPath).length).replaceAll(
......@@ -216,31 +230,33 @@ export async function sitegen(
216230 },
217231 });
218232 }
219 async function prepareView(view: FileItem) {
220 const module = require(view.file);
233 async function prepareView(item: FileItem) {
234 const module = require(item.file);
221235 if (!module.meta) {
222 throw new Error(`${view.file} is missing 'export const meta'`);
236 throw new Error(`${item.file} is missing 'export const meta'`);
223237 }
224238 if (!module.default) {
225 throw new Error(`${view.file} is missing a default export.`);
239 throw new Error(`${item.file} is missing a default export.`);
226240 }
227241 const pageTheme = module.layout?.theme ?? module.theme;
228242 const theme: css.Theme = {
229243 ...css.defaultTheme,
230244 ...pageTheme,
231245 };
232 const cssImports = hot.getCssImports(view.file)
233 .concat("src/global.css")
234 .map((file) => path.relative(hot.projectSrc, path.resolve(file)));
246 const cssImports = Array.from(
247 new Set([globalCssPath, ...hot.getCssImports(item.file)]),
248 (file) => path.relative(hot.projectSrc, file),
249 );
250 ensureCssGetsBuilt(cssImports, theme, item.id);
235251 incr.put({
236252 kind: "viewMetadata",
237 key: view.id,
238 sources: [view.file],
253 key: item.id,
254 sources: [item.file],
239255 value: {
240 file: path.relative(hot.projectRoot, view.file),
256 file: path.relative(hot.projectRoot, item.file),
241257 cssImports,
242258 theme,
243 clientRefs: hot.getClientScriptRefs(view.file),
259 clientRefs: hot.getClientScriptRefs(item.file),
244260 hasLayout: !!module.layout?.default,
245261 },
246262 });
......@@ -422,7 +438,7 @@ function getItemText({ file }: FileItem) {
422438 return path.relative(hot.projectSrc, file).replaceAll("\\", "/");
423439}
424440
425import { OnceMap, Queue } from "./queue.ts";
441import { OnceMap, Queue } from "#sitegen/async";
426442import { Incremental } from "./incremental.ts";
427443import * as bundle from "./bundle.ts";
428444import * as css from "./css.ts";
framework/incremental.ts+2-2
......@@ -148,7 +148,7 @@ export class Incremental {
148148 for (const key of map.keys()) {
149149 if (!this.round.referenced.has(`${kind}\0${key}`)) {
150150 unreferenced.push({ kind: kind as ArtifactKind, key });
151 console.warn("unreferened " + kind + " : " + key);
151 this.out[kind as ArtifactKind].delete(key);
152152 }
153153 }
154154 }
......@@ -652,7 +652,7 @@ const zstd = util.promisify(zlib.zstdCompress);
652652import * as fs from "#sitegen/fs";
653653import * as zlib from "node:zlib";
654654import * as util from "node:util";
655import { Queue } from "./queue.ts";
655import { Queue } from "#sitegen/async";
656656import * as hot from "./hot.ts";
657657import * as mime from "#sitegen/mime";
658658import * as path from "node:path";
framework/lib/async.ts created+279
......@@ -0,0 +1,279 @@
1const five_minutes = 5 * 60 * 1000;
2
3interface QueueOptions<T, R> {
4 name: string;
5 fn: (item: T, spin: Spinner) => Promise<R>;
6 getItemText?: (item: T) => string;
7 maxJobs?: number;
8 passive?: boolean;
9}
10
11// Process multiple items in parallel, queue up as many.
12export class Queue<T, R> {
13 #name: string;
14 #fn: (item: T, spin: Spinner) => Promise<R>;
15 #maxJobs: number;
16 #getItemText: (item: T) => string;
17 #passive: boolean;
18
19 #active: Spinner[] = [];
20 #queue: Array<[T] | [T, (result: R) => void, (err: unknown) => void]> = [];
21
22 #cachedProgress: Progress<{ active: Spinner[] }> | null = null;
23 #done: number = 0;
24 #total: number = 0;
25 #onComplete: (() => void) | null = null;
26 #estimate: number | null = null;
27 #errors: unknown[] = [];
28
29 constructor(options: QueueOptions<T, R>) {
30 this.#name = options.name;
31 this.#fn = options.fn;
32 this.#maxJobs = options.maxJobs ?? 5;
33 this.#getItemText = options.getItemText ?? defaultGetItemText;
34 this.#passive = options.passive ?? false;
35 }
36
37 get bar() {
38 const cached = this.#cachedProgress;
39 if (!cached) {
40 const bar = this.#cachedProgress = new Progress({
41 spinner: null,
42 text: ({ active }) => {
43 const now = performance.now();
44 let text = `[${this.#done}/${this.#total}] ${this.#name}`;
45 let n = 0;
46 for (const item of active) {
47 let itemText = "- " + item.format(now);
48 text += `\n` +
49 itemText.slice(0, Math.max(0, process.stdout.columns - 1));
50 if (n > 10) {
51 text += `\n ... + ${active.length - n} more`;
52 break;
53 }
54 n++;
55 }
56 return text;
57 },
58 props: {
59 active: [] as Spinner[],
60 },
61 });
62 bar.value = 0;
63 return bar;
64 }
65 return cached;
66 }
67
68 add(args: T) {
69 this.#total += 1;
70 this.updateTotal();
71 if (this.#active.length > this.#maxJobs) {
72 const { promise, resolve, reject } = Promise.withResolvers<R>();
73 this.#queue.push([args, resolve, reject]);
74 return promise;
75 }
76 return this.#run(args);
77 }
78
79 addMany(items: T[]) {
80 this.#total += items.length;
81 this.updateTotal();
82
83 const runNowCount = this.#maxJobs - this.#active.length;
84 const runNow = items.slice(0, runNowCount);
85 const runLater = items.slice(runNowCount);
86 this.#queue.push(...runLater.reverse().map<[T]>((x) => [x]));
87 runNow.map((item) => this.#run(item).catch(() => {}));
88 }
89
90 async #run(args: T): Promise<R> {
91 const bar = this.bar;
92 const itemText = this.#getItemText(args);
93 const spinner = new Spinner(itemText);
94 spinner.stop();
95 const active = this.#active;
96 try {
97 active.unshift(spinner);
98 bar.props = { active };
99 const result = await this.#fn(args, spinner);
100 this.#done++;
101 return result;
102 } catch (err) {
103 if (err && typeof err === "object") {
104 (err as any).job = itemText;
105 }
106 this.#errors.push(err);
107 throw err;
108 } finally {
109 active.splice(active.indexOf(spinner), 1);
110 bar.props = { active };
111 bar.value = this.#done;
112
113 // Process next item
114 const next = this.#queue.shift();
115 if (next) {
116 const args = next[0];
117 this.#run(args)
118 .then((result) => next[1]?.(result))
119 .catch((err) => next[2]?.(err));
120 } else if (this.#active.length === 0) {
121 if (this.#passive) {
122 this.bar.stop();
123 this.#cachedProgress = null;
124 }
125 this.#onComplete?.();
126 }
127 }
128 }
129
130 updateTotal() {
131 const bar = this.bar;
132 bar.total = Math.max(this.#total, this.#estimate ?? 0);
133 }
134
135 set estimate(e: number) {
136 this.#estimate = e;
137 if (this.#cachedProgress) {
138 this.updateTotal();
139 }
140 }
141
142 async done(o: { method: "success" | "stop" }) {
143 if (this.#active.length === 0) {
144 this.#end(o);
145 return;
146 }
147
148 const { promise, resolve } = Promise.withResolvers<void>();
149 this.#onComplete = resolve;
150 await promise;
151 this.#end(o);
152 }
153
154 #end(
155 { method = this.#passive ? "stop" : "success" }: {
156 method: "success" | "stop";
157 },
158 ) {
159 const bar = this.#cachedProgress;
160 if (this.#errors.length > 0) {
161 if (bar) bar.stop();
162 throw new AggregateError(
163 this.#errors,
164 this.#errors.length + " jobs failed in '" + this.#name + "'",
165 );
166 }
167
168 if (bar) bar[method]();
169 }
170
171 get active(): boolean {
172 return this.#active.length !== 0;
173 }
174}
175
176const cwd = process.cwd();
177function defaultGetItemText(item: unknown) {
178 let itemText = "";
179 if (typeof item === "string") {
180 itemText = item;
181 } else if (typeof item === "object" && item !== null) {
182 const { path, label, id } = item as any;
183 itemText = label ?? path ?? id ?? JSON.stringify(item);
184 } else {
185 itemText = JSON.stringify(item);
186 }
187
188 if (itemText.startsWith(cwd)) {
189 itemText = path.relative(cwd, itemText);
190 }
191 return itemText;
192}
193
194export class OnceMap<T> {
195 private ongoing = new Map<string, Promise<T>>();
196
197 get(key: string, compute: () => Promise<T>) {
198 if (this.ongoing.has(key)) {
199 return this.ongoing.get(key)!;
200 }
201
202 const result = compute();
203 this.ongoing.set(key, result);
204 return result;
205 }
206}
207
208interface ARCEValue<T> {
209 value: T;
210 [Symbol.dispose]: () => void;
211}
212
213export function RefCountedExpirable<T>(
214 init: () => Promise<T>,
215 deinit: (value: T) => void,
216 expire: number = five_minutes,
217): () => Promise<ARCEValue<T>> {
218 let refs = 0;
219 let item: ARCEValue<T> | null = null;
220 let loading: Promise<ARCEValue<T>> | null = null;
221 let timer: ReturnType<typeof setTimeout> | null = null;
222
223 function deref() {
224 ASSERT(item !== null);
225 if (--refs !== 0) return;
226 ASSERT(timer === null);
227 timer = setTimeout(() => {
228 ASSERT(refs === 0);
229 ASSERT(loading === null);
230 ASSERT(item !== null);
231 deinit(item.value);
232 item = null;
233 timer = null;
234 }, expire);
235 }
236
237 return async function () {
238 if (timer !== null) {
239 clearTimeout(timer);
240 timer = null;
241 }
242 if (item !== null) {
243 refs++;
244 return item;
245 }
246 if (loading !== null) {
247 refs++;
248 return loading;
249 }
250 const p = Promise.withResolvers<ARCEValue<T>>();
251 loading = p.promise;
252 try {
253 const value = await init();
254 item = { value, [Symbol.dispose]: deref };
255 refs++;
256 p.resolve(item);
257 return item;
258 } catch (e) {
259 p.reject(e);
260 throw e;
261 } finally {
262 loading = null;
263 }
264 };
265}
266
267export function once<T>(fn: () => Promise<T>): () => Promise<T> {
268 let result: T | Promise<T> | null = null;
269 return async () => {
270 if (result) return result;
271 result = await fn();
272 return result;
273 };
274}
275
276import { Progress } from "@paperclover/console/Progress";
277import { Spinner } from "@paperclover/console/Spinner";
278import * as path from "node:path";
279import process from "node:process";
framework/lib/view.ts+16-7
......@@ -16,7 +16,14 @@ let scripts: Record<string, string> = null!;
1616// boundaries, but those were never used. Pages will wait until they
1717// are fully rendered before sending.
1818export async function renderView(
19 c: hono.Context,
19 context: hono.Context,
20 id: string,
21 props: Record<string, unknown>,
22) {
23 return context.html(await renderViewToString(id, { context, ...props }));
24}
25
26export async function renderViewToString(
2027 id: string,
2128 props: Record<string, unknown>,
2229) {
......@@ -29,11 +36,11 @@ export async function renderView(
2936 inlineCss,
3037 layout,
3138 meta: metadata,
32 }: View = views[id];
39 }: View = UNWRAP(views[id], `Missing view ${id}`);
3340
3441 // -- metadata --
3542 const renderedMetaPromise = Promise.resolve(
36 typeof metadata === "function" ? metadata({ context: c }) : metadata,
43 typeof metadata === "function" ? metadata(props) : metadata,
3744 ).then((m) => meta.renderMeta(m));
3845
3946 // -- html --
......@@ -44,14 +51,17 @@ export async function renderView(
4451 });
4552
4653 // -- join document and send --
47 return c.html(wrapDocument({
54 return wrapDocument({
4855 body,
4956 head: await renderedMetaPromise,
5057 inlineCss,
5158 scripts: joinScripts(
52 Array.from(sitegen.scripts, (script) => scripts[script]),
59 Array.from(
60 sitegen.scripts,
61 (id) => UNWRAP(scripts[id], `Missing script ${id}`),
62 ),
5363 ),
54 }));
64 });
5565}
5666
5767export function provideViewData(v: typeof views, s: typeof scripts) {
......@@ -87,5 +97,4 @@ export function wrapDocument({
8797import * as meta from "./meta.ts";
8898import type * as hono from "#hono";
8999import * as engine from "../engine/ssr.ts";
90import type * as css from "../css.ts";
91100import * as sg from "./sitegen.ts";
framework/queue.ts deleted-210
......@@ -1,210 +0,0 @@
1interface QueueOptions<T, R> {
2 name: string;
3 fn: (item: T, spin: Spinner) => Promise<R>;
4 getItemText?: (item: T) => string;
5 maxJobs?: number;
6 passive?: boolean;
7}
8
9// Process multiple items in parallel, queue up as many.
10export class Queue<T, R> {
11 #name: string;
12 #fn: (item: T, spin: Spinner) => Promise<R>;
13 #maxJobs: number;
14 #getItemText: (item: T) => string;
15 #passive: boolean;
16
17 #active: Spinner[] = [];
18 #queue: Array<[T] | [T, (result: R) => void, (err: unknown) => void]> = [];
19
20 #cachedProgress: Progress<{ active: Spinner[] }> | null = null;
21 #done: number = 0;
22 #total: number = 0;
23 #onComplete: (() => void) | null = null;
24 #estimate: number | null = null;
25 #errors: unknown[] = [];
26
27 constructor(options: QueueOptions<T, R>) {
28 this.#name = options.name;
29 this.#fn = options.fn;
30 this.#maxJobs = options.maxJobs ?? 5;
31 this.#getItemText = options.getItemText ?? defaultGetItemText;
32 this.#passive = options.passive ?? false;
33 }
34
35 get bar() {
36 const cached = this.#cachedProgress;
37 if (!cached) {
38 const bar = this.#cachedProgress = new Progress({
39 spinner: null,
40 text: ({ active }) => {
41 const now = performance.now();
42 let text = `[${this.#done}/${this.#total}] ${this.#name}`;
43 let n = 0;
44 for (const item of active) {
45 let itemText = "- " + item.format(now);
46 text += `\n` +
47 itemText.slice(0, Math.max(0, process.stdout.columns - 1));
48 if (n > 10) {
49 text += `\n ... + ${active.length - n} more`;
50 break;
51 }
52 n++;
53 }
54 return text;
55 },
56 props: {
57 active: [] as Spinner[],
58 },
59 });
60 bar.value = 0;
61 return bar;
62 }
63 return cached;
64 }
65
66 add(args: T) {
67 this.#total += 1;
68 this.updateTotal();
69 if (this.#active.length > this.#maxJobs) {
70 const { promise, resolve, reject } = Promise.withResolvers<R>();
71 this.#queue.push([args, resolve, reject]);
72 return promise;
73 }
74 return this.#run(args);
75 }
76
77 addMany(items: T[]) {
78 this.#total += items.length;
79 this.updateTotal();
80
81 const runNowCount = this.#maxJobs - this.#active.length;
82 const runNow = items.slice(0, runNowCount);
83 const runLater = items.slice(runNowCount);
84 this.#queue.push(...runLater.reverse().map<[T]>((x) => [x]));
85 runNow.map((item) => this.#run(item).catch(() => {}));
86 }
87
88 async #run(args: T): Promise<R> {
89 const bar = this.bar;
90 const itemText = this.#getItemText(args);
91 const spinner = new Spinner(itemText);
92 spinner.stop();
93 const active = this.#active;
94 try {
95 active.unshift(spinner);
96 bar.props = { active };
97 const result = await this.#fn(args, spinner);
98 this.#done++;
99 return result;
100 } catch (err) {
101 if (err && typeof err === "object") {
102 (err as any).job = itemText;
103 }
104 this.#errors.push(err);
105 throw err;
106 } finally {
107 active.splice(active.indexOf(spinner), 1);
108 bar.props = { active };
109 bar.value = this.#done;
110
111 // Process next item
112 const next = this.#queue.shift();
113 if (next) {
114 const args = next[0];
115 this.#run(args)
116 .then((result) => next[1]?.(result))
117 .catch((err) => next[2]?.(err));
118 } else if (this.#active.length === 0) {
119 if (this.#passive) {
120 this.bar.stop();
121 this.#cachedProgress = null;
122 }
123 this.#onComplete?.();
124 }
125 }
126 }
127
128 updateTotal() {
129 const bar = this.bar;
130 bar.total = Math.max(this.#total, this.#estimate ?? 0);
131 }
132
133 set estimate(e: number) {
134 this.#estimate = e;
135 if (this.#cachedProgress) {
136 this.updateTotal();
137 }
138 }
139
140 async done(o: { method: "success" | "stop" }) {
141 if (this.#active.length === 0) {
142 this.#end(o);
143 return;
144 }
145
146 const { promise, resolve } = Promise.withResolvers<void>();
147 this.#onComplete = resolve;
148 await promise;
149 this.#end(o);
150 }
151
152 #end(
153 { method = this.#passive ? "stop" : "success" }: {
154 method: "success" | "stop";
155 },
156 ) {
157 const bar = this.#cachedProgress;
158 if (this.#errors.length > 0) {
159 if (bar) bar.stop();
160 throw new AggregateError(
161 this.#errors,
162 this.#errors.length + " jobs failed in '" + this.#name + "'",
163 );
164 }
165
166 if (bar) bar[method]();
167 }
168
169 get active(): boolean {
170 return this.#active.length !== 0;
171 }
172}
173
174const cwd = process.cwd();
175function defaultGetItemText(item: unknown) {
176 let itemText = "";
177 if (typeof item === "string") {
178 itemText = item;
179 } else if (typeof item === "object" && item !== null) {
180 const { path, label, id } = item as any;
181 itemText = label ?? path ?? id ?? JSON.stringify(item);
182 } else {
183 itemText = JSON.stringify(item);
184 }
185
186 if (itemText.startsWith(cwd)) {
187 itemText = path.relative(cwd, itemText);
188 }
189 return itemText;
190}
191
192export class OnceMap<T> {
193 private ongoing = new Map<string, Promise<T>>();
194
195 get(key: string, compute: () => Promise<T>) {
196 if (this.ongoing.has(key)) {
197 return this.ongoing.get(key)!;
198 }
199
200 const result = compute();
201 this.ongoing.set(key, result);
202 result.finally(() => this.ongoing.delete(key));
203 return result;
204 }
205}
206
207import { Progress } from "@paperclover/console/Progress";
208import { Spinner } from "@paperclover/console/Spinner";
209import * as path from "node:path";
210import process from "node:process";
framework/test-fixture/Component.marko created+4
......@@ -0,0 +1,4 @@
1<div meow=null />
2<div>
3 wait(${null})
4</div>
framework/test-fixture/UseComponent.marko created+6
......@@ -0,0 +1,6 @@
1import Component from './Component.marko';
2
3<h1>web page</h1>
4<if=!false>
5 <Component=null/>
6</>
framework/watch.ts+8-2
......@@ -11,7 +11,7 @@ export async function main() {
1111 const watch = new Watch(rebuild);
1212 watch.add(...incr.invals.keys());
1313 statusLine();
14 // ... an
14 // ... and then serve it!
1515 serve();
1616
1717 function serve() {
......@@ -41,7 +41,13 @@ export async function main() {
4141 files = files.map((file) => path.relative(hot.projectRoot, file));
4242 const changed: string[] = [];
4343 for (const file of files) {
44 if (incr.updateStat(file, fs.statSync(file).mtimeMs)) changed.push(file);
44 let mtimeMs: number | null = null;
45 try {
46 mtimeMs = fs.statSync(file).mtimeMs;
47 } catch (err: any) {
48 if (err?.code !== "ENOENT") throw err;
49 }
50 if (incr.updateStat(file, mtimeMs)) changed.push(file);
4551 }
4652 if (changed.length === 0) {
4753 console.warn("Files were modified but the 'modify' time did not change.");
package-lock.json+1055-10
......@@ -12,6 +12,7 @@
1212 "hls.js": "^1.6.5",
1313 "hono": "^4.7.11",
1414 "marko": "^6.0.20",
15 "puppeteer": "^24.10.1",
1516 "unique-names-generator": "^4.7.1"
1617 },
1718 "devDependencies": {
......@@ -950,6 +951,45 @@
950951 "strip-ansi": "^7.1.0"
951952 }
952953 },
954 "node_modules/@puppeteer/browsers": {
955 "version": "2.10.5",
956 "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
957 "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
958 "license": "Apache-2.0",
959 "dependencies": {
960 "debug": "^4.4.1",
961 "extract-zip": "^2.0.1",
962 "progress": "^2.0.3",
963 "proxy-agent": "^6.5.0",
964 "semver": "^7.7.2",
965 "tar-fs": "^3.0.8",
966 "yargs": "^17.7.2"
967 },
968 "bin": {
969 "browsers": "lib/cjs/main-cli.js"
970 },
971 "engines": {
972 "node": ">=18"
973 }
974 },
975 "node_modules/@puppeteer/browsers/node_modules/semver": {
976 "version": "7.7.2",
977 "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
978 "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
979 "license": "ISC",
980 "bin": {
981 "semver": "bin/semver.js"
982 },
983 "engines": {
984 "node": ">=10"
985 }
986 },
987 "node_modules/@tootallnate/quickjs-emscripten": {
988 "version": "0.23.0",
989 "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
990 "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
991 "license": "MIT"
992 },
953993 "node_modules/@types/debug": {
954994 "version": "4.1.12",
955995 "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
......@@ -1008,7 +1048,7 @@
10081048 "version": "22.15.30",
10091049 "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
10101050 "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
1011 "dev": true,
1051 "devOptional": true,
10121052 "license": "MIT",
10131053 "dependencies": {
10141054 "undici-types": "~6.21.0"
......@@ -1020,6 +1060,16 @@
10201060 "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
10211061 "license": "MIT"
10221062 },
1063 "node_modules/@types/yauzl": {
1064 "version": "2.10.3",
1065 "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
1066 "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
1067 "license": "MIT",
1068 "optional": true,
1069 "dependencies": {
1070 "@types/node": "*"
1071 }
1072 },
10231073 "node_modules/@ungap/structured-clone": {
10241074 "version": "1.3.0",
10251075 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
......@@ -1047,6 +1097,15 @@
10471097 "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
10481098 }
10491099 },
1100 "node_modules/agent-base": {
1101 "version": "7.1.3",
1102 "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
1103 "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
1104 "license": "MIT",
1105 "engines": {
1106 "node": ">= 14"
1107 }
1108 },
10501109 "node_modules/ansi-escapes": {
10511110 "version": "7.0.0",
10521111 "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
......@@ -1074,6 +1133,39 @@
10741133 "url": "https://github.com/chalk/ansi-regex?sponsor=1"
10751134 }
10761135 },
1136 "node_modules/ansi-styles": {
1137 "version": "4.3.0",
1138 "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
1139 "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
1140 "license": "MIT",
1141 "dependencies": {
1142 "color-convert": "^2.0.1"
1143 },
1144 "engines": {
1145 "node": ">=8"
1146 },
1147 "funding": {
1148 "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1149 }
1150 },
1151 "node_modules/argparse": {
1152 "version": "2.0.1",
1153 "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
1154 "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
1155 "license": "Python-2.0"
1156 },
1157 "node_modules/ast-types": {
1158 "version": "0.13.4",
1159 "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
1160 "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
1161 "license": "MIT",
1162 "dependencies": {
1163 "tslib": "^2.0.1"
1164 },
1165 "engines": {
1166 "node": ">=4"
1167 }
1168 },
10771169 "node_modules/astring": {
10781170 "version": "1.9.0",
10791171 "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
......@@ -1083,6 +1175,12 @@
10831175 "astring": "bin/astring"
10841176 }
10851177 },
1178 "node_modules/b4a": {
1179 "version": "1.6.7",
1180 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
1181 "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
1182 "license": "Apache-2.0"
1183 },
10861184 "node_modules/bail": {
10871185 "version": "2.0.2",
10881186 "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
......@@ -1093,6 +1191,87 @@
10931191 "url": "https://github.com/sponsors/wooorm"
10941192 }
10951193 },
1194 "node_modules/bare-events": {
1195 "version": "2.5.4",
1196 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
1197 "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
1198 "license": "Apache-2.0",
1199 "optional": true
1200 },
1201 "node_modules/bare-fs": {
1202 "version": "4.1.5",
1203 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz",
1204 "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==",
1205 "license": "Apache-2.0",
1206 "optional": true,
1207 "dependencies": {
1208 "bare-events": "^2.5.4",
1209 "bare-path": "^3.0.0",
1210 "bare-stream": "^2.6.4"
1211 },
1212 "engines": {
1213 "bare": ">=1.16.0"
1214 },
1215 "peerDependencies": {
1216 "bare-buffer": "*"
1217 },
1218 "peerDependenciesMeta": {
1219 "bare-buffer": {
1220 "optional": true
1221 }
1222 }
1223 },
1224 "node_modules/bare-os": {
1225 "version": "3.6.1",
1226 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
1227 "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
1228 "license": "Apache-2.0",
1229 "optional": true,
1230 "engines": {
1231 "bare": ">=1.14.0"
1232 }
1233 },
1234 "node_modules/bare-path": {
1235 "version": "3.0.0",
1236 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
1237 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
1238 "license": "Apache-2.0",
1239 "optional": true,
1240 "dependencies": {
1241 "bare-os": "^3.0.1"
1242 }
1243 },
1244 "node_modules/bare-stream": {
1245 "version": "2.6.5",
1246 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
1247 "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
1248 "license": "Apache-2.0",
1249 "optional": true,
1250 "dependencies": {
1251 "streamx": "^2.21.0"
1252 },
1253 "peerDependencies": {
1254 "bare-buffer": "*",
1255 "bare-events": "*"
1256 },
1257 "peerDependenciesMeta": {
1258 "bare-buffer": {
1259 "optional": true
1260 },
1261 "bare-events": {
1262 "optional": true
1263 }
1264 }
1265 },
1266 "node_modules/basic-ftp": {
1267 "version": "5.0.5",
1268 "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
1269 "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
1270 "license": "MIT",
1271 "engines": {
1272 "node": ">=10.0.0"
1273 }
1274 },
10961275 "node_modules/browserslist": {
10971276 "version": "4.25.0",
10981277 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
......@@ -1125,12 +1304,30 @@
11251304 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
11261305 }
11271306 },
1307 "node_modules/buffer-crc32": {
1308 "version": "0.2.13",
1309 "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
1310 "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
1311 "license": "MIT",
1312 "engines": {
1313 "node": "*"
1314 }
1315 },
11281316 "node_modules/buffer-from": {
11291317 "version": "1.1.2",
11301318 "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
11311319 "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
11321320 "license": "MIT"
11331321 },
1322 "node_modules/callsites": {
1323 "version": "3.1.0",
1324 "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
1325 "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
1326 "license": "MIT",
1327 "engines": {
1328 "node": ">=6"
1329 }
1330 },
11341331 "node_modules/caniuse-lite": {
11351332 "version": "1.0.30001721",
11361333 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
......@@ -1213,6 +1410,54 @@
12131410 "url": "https://github.com/sponsors/wooorm"
12141411 }
12151412 },
1413 "node_modules/chromium-bidi": {
1414 "version": "5.1.0",
1415 "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
1416 "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",
1417 "license": "Apache-2.0",
1418 "dependencies": {
1419 "mitt": "^3.0.1",
1420 "zod": "^3.24.1"
1421 },
1422 "peerDependencies": {
1423 "devtools-protocol": "*"
1424 }
1425 },
1426 "node_modules/cliui": {
1427 "version": "8.0.1",
1428 "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
1429 "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
1430 "license": "ISC",
1431 "dependencies": {
1432 "string-width": "^4.2.0",
1433 "strip-ansi": "^6.0.1",
1434 "wrap-ansi": "^7.0.0"
1435 },
1436 "engines": {
1437 "node": ">=12"
1438 }
1439 },
1440 "node_modules/cliui/node_modules/ansi-regex": {
1441 "version": "5.0.1",
1442 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1443 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1444 "license": "MIT",
1445 "engines": {
1446 "node": ">=8"
1447 }
1448 },
1449 "node_modules/cliui/node_modules/strip-ansi": {
1450 "version": "6.0.1",
1451 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
1452 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1453 "license": "MIT",
1454 "dependencies": {
1455 "ansi-regex": "^5.0.1"
1456 },
1457 "engines": {
1458 "node": ">=8"
1459 }
1460 },
12161461 "node_modules/collapse-white-space": {
12171462 "version": "2.1.0",
12181463 "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
......@@ -1223,6 +1468,24 @@
12231468 "url": "https://github.com/sponsors/wooorm"
12241469 }
12251470 },
1471 "node_modules/color-convert": {
1472 "version": "2.0.1",
1473 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1474 "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1475 "license": "MIT",
1476 "dependencies": {
1477 "color-name": "~1.1.4"
1478 },
1479 "engines": {
1480 "node": ">=7.0.0"
1481 }
1482 },
1483 "node_modules/color-name": {
1484 "version": "1.1.4",
1485 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1486 "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1487 "license": "MIT"
1488 },
12261489 "node_modules/comma-separated-tokens": {
12271490 "version": "2.0.3",
12281491 "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
......@@ -1248,12 +1511,47 @@
12481511 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
12491512 "license": "MIT"
12501513 },
1514 "node_modules/cosmiconfig": {
1515 "version": "9.0.0",
1516 "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
1517 "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
1518 "license": "MIT",
1519 "dependencies": {
1520 "env-paths": "^2.2.1",
1521 "import-fresh": "^3.3.0",
1522 "js-yaml": "^4.1.0",
1523 "parse-json": "^5.2.0"
1524 },
1525 "engines": {
1526 "node": ">=14"
1527 },
1528 "funding": {
1529 "url": "https://github.com/sponsors/d-fischer"
1530 },
1531 "peerDependencies": {
1532 "typescript": ">=4.9.5"
1533 },
1534 "peerDependenciesMeta": {
1535 "typescript": {
1536 "optional": true
1537 }
1538 }
1539 },
12511540 "node_modules/csstype": {
12521541 "version": "3.1.3",
12531542 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
12541543 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
12551544 "license": "MIT"
12561545 },
1546 "node_modules/data-uri-to-buffer": {
1547 "version": "6.0.2",
1548 "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
1549 "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
1550 "license": "MIT",
1551 "engines": {
1552 "node": ">= 14"
1553 }
1554 },
12571555 "node_modules/debug": {
12581556 "version": "4.4.1",
12591557 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
......@@ -1284,6 +1582,20 @@
12841582 "url": "https://github.com/sponsors/wooorm"
12851583 }
12861584 },
1585 "node_modules/degenerator": {
1586 "version": "5.0.1",
1587 "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
1588 "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
1589 "license": "MIT",
1590 "dependencies": {
1591 "ast-types": "^0.13.4",
1592 "escodegen": "^2.1.0",
1593 "esprima": "^4.0.1"
1594 },
1595 "engines": {
1596 "node": ">= 14"
1597 }
1598 },
12871599 "node_modules/dequal": {
12881600 "version": "2.0.3",
12891601 "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
......@@ -1306,12 +1618,42 @@
13061618 "url": "https://github.com/sponsors/wooorm"
13071619 }
13081620 },
1621 "node_modules/devtools-protocol": {
1622 "version": "0.0.1452169",
1623 "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
1624 "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
1625 "license": "BSD-3-Clause"
1626 },
13091627 "node_modules/electron-to-chromium": {
13101628 "version": "1.5.165",
13111629 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
13121630 "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==",
13131631 "license": "ISC"
13141632 },
1633 "node_modules/emoji-regex": {
1634 "version": "8.0.0",
1635 "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
1636 "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1637 "license": "MIT"
1638 },
1639 "node_modules/end-of-stream": {
1640 "version": "1.4.4",
1641 "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
1642 "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
1643 "license": "MIT",
1644 "dependencies": {
1645 "once": "^1.4.0"
1646 }
1647 },
1648 "node_modules/env-paths": {
1649 "version": "2.2.1",
1650 "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
1651 "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
1652 "license": "MIT",
1653 "engines": {
1654 "node": ">=6"
1655 }
1656 },
13151657 "node_modules/environment": {
13161658 "version": "1.1.0",
13171659 "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
......@@ -1324,6 +1666,15 @@
13241666 "url": "https://github.com/sponsors/sindresorhus"
13251667 }
13261668 },
1669 "node_modules/error-ex": {
1670 "version": "1.3.2",
1671 "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
1672 "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
1673 "license": "MIT",
1674 "dependencies": {
1675 "is-arrayish": "^0.2.1"
1676 }
1677 },
13271678 "node_modules/error-stack-parser": {
13281679 "version": "2.1.4",
13291680 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
......@@ -1414,6 +1765,59 @@
14141765 "node": ">=6"
14151766 }
14161767 },
1768 "node_modules/escodegen": {
1769 "version": "2.1.0",
1770 "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
1771 "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
1772 "license": "BSD-2-Clause",
1773 "dependencies": {
1774 "esprima": "^4.0.1",
1775 "estraverse": "^5.2.0",
1776 "esutils": "^2.0.2"
1777 },
1778 "bin": {
1779 "escodegen": "bin/escodegen.js",
1780 "esgenerate": "bin/esgenerate.js"
1781 },
1782 "engines": {
1783 "node": ">=6.0"
1784 },
1785 "optionalDependencies": {
1786 "source-map": "~0.6.1"
1787 }
1788 },
1789 "node_modules/escodegen/node_modules/source-map": {
1790 "version": "0.6.1",
1791 "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
1792 "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
1793 "license": "BSD-3-Clause",
1794 "optional": true,
1795 "engines": {
1796 "node": ">=0.10.0"
1797 }
1798 },
1799 "node_modules/esprima": {
1800 "version": "4.0.1",
1801 "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
1802 "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
1803 "license": "BSD-2-Clause",
1804 "bin": {
1805 "esparse": "bin/esparse.js",
1806 "esvalidate": "bin/esvalidate.js"
1807 },
1808 "engines": {
1809 "node": ">=4"
1810 }
1811 },
1812 "node_modules/estraverse": {
1813 "version": "5.3.0",
1814 "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
1815 "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
1816 "license": "BSD-2-Clause",
1817 "engines": {
1818 "node": ">=4.0"
1819 }
1820 },
14171821 "node_modules/estree-util-attach-comments": {
14181822 "version": "3.0.0",
14191823 "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz",
......@@ -1505,12 +1909,56 @@
15051909 "@types/estree": "^1.0.0"
15061910 }
15071911 },
1912 "node_modules/esutils": {
1913 "version": "2.0.3",
1914 "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
1915 "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
1916 "license": "BSD-2-Clause",
1917 "engines": {
1918 "node": ">=0.10.0"
1919 }
1920 },
15081921 "node_modules/extend": {
15091922 "version": "3.0.2",
15101923 "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
15111924 "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
15121925 "license": "MIT"
15131926 },
1927 "node_modules/extract-zip": {
1928 "version": "2.0.1",
1929 "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
1930 "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
1931 "license": "BSD-2-Clause",
1932 "dependencies": {
1933 "debug": "^4.1.1",
1934 "get-stream": "^5.1.0",
1935 "yauzl": "^2.10.0"
1936 },
1937 "bin": {
1938 "extract-zip": "cli.js"
1939 },
1940 "engines": {
1941 "node": ">= 10.17.0"
1942 },
1943 "optionalDependencies": {
1944 "@types/yauzl": "^2.9.1"
1945 }
1946 },
1947 "node_modules/fast-fifo": {
1948 "version": "1.3.2",
1949 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
1950 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
1951 "license": "MIT"
1952 },
1953 "node_modules/fd-slicer": {
1954 "version": "1.1.0",
1955 "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
1956 "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
1957 "license": "MIT",
1958 "dependencies": {
1959 "pend": "~1.2.0"
1960 }
1961 },
15141962 "node_modules/gensync": {
15151963 "version": "1.0.0-beta.2",
15161964 "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
......@@ -1520,6 +1968,44 @@
15201968 "node": ">=6.9.0"
15211969 }
15221970 },
1971 "node_modules/get-caller-file": {
1972 "version": "2.0.5",
1973 "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
1974 "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
1975 "license": "ISC",
1976 "engines": {
1977 "node": "6.* || 8.* || >= 10.*"
1978 }
1979 },
1980 "node_modules/get-stream": {
1981 "version": "5.2.0",
1982 "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
1983 "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
1984 "license": "MIT",
1985 "dependencies": {
1986 "pump": "^3.0.0"
1987 },
1988 "engines": {
1989 "node": ">=8"
1990 },
1991 "funding": {
1992 "url": "https://github.com/sponsors/sindresorhus"
1993 }
1994 },
1995 "node_modules/get-uri": {
1996 "version": "6.0.4",
1997 "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
1998 "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
1999 "license": "MIT",
2000 "dependencies": {
2001 "basic-ftp": "^5.0.2",
2002 "data-uri-to-buffer": "^6.0.2",
2003 "debug": "^4.3.4"
2004 },
2005 "engines": {
2006 "node": ">= 14"
2007 }
2008 },
15232009 "node_modules/globals": {
15242010 "version": "11.12.0",
15252011 "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
......@@ -1618,21 +2104,85 @@
16182104 "integrity": "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ==",
16192105 "license": "MIT",
16202106 "engines": {
1621 "node": ">=16.9.0"
2107 "node": ">=16.9.0"
2108 }
2109 },
2110 "node_modules/htmljs-parser": {
2111 "version": "5.5.4",
2112 "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-5.5.4.tgz",
2113 "integrity": "sha512-qJiir3ViASVhGdNW4kwM4pQWvDtDLtjwaflEQDooiNzbOPIzoaIzmRFWweFl8GomVhJqXqX0jjrhDED3pmAdzg==",
2114 "license": "MIT"
2115 },
2116 "node_modules/http-proxy-agent": {
2117 "version": "7.0.2",
2118 "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
2119 "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
2120 "license": "MIT",
2121 "dependencies": {
2122 "agent-base": "^7.1.0",
2123 "debug": "^4.3.4"
2124 },
2125 "engines": {
2126 "node": ">= 14"
2127 }
2128 },
2129 "node_modules/https-proxy-agent": {
2130 "version": "7.0.6",
2131 "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
2132 "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
2133 "license": "MIT",
2134 "dependencies": {
2135 "agent-base": "^7.1.2",
2136 "debug": "4"
2137 },
2138 "engines": {
2139 "node": ">= 14"
2140 }
2141 },
2142 "node_modules/import-fresh": {
2143 "version": "3.3.1",
2144 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
2145 "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
2146 "license": "MIT",
2147 "dependencies": {
2148 "parent-module": "^1.0.0",
2149 "resolve-from": "^4.0.0"
2150 },
2151 "engines": {
2152 "node": ">=6"
2153 },
2154 "funding": {
2155 "url": "https://github.com/sponsors/sindresorhus"
2156 }
2157 },
2158 "node_modules/import-fresh/node_modules/resolve-from": {
2159 "version": "4.0.0",
2160 "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
2161 "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
2162 "license": "MIT",
2163 "engines": {
2164 "node": ">=4"
16222165 }
16232166 },
1624 "node_modules/htmljs-parser": {
1625 "version": "5.5.4",
1626 "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-5.5.4.tgz",
1627 "integrity": "sha512-qJiir3ViASVhGdNW4kwM4pQWvDtDLtjwaflEQDooiNzbOPIzoaIzmRFWweFl8GomVhJqXqX0jjrhDED3pmAdzg==",
1628 "license": "MIT"
1629 },
16302167 "node_modules/inline-style-parser": {
16312168 "version": "0.2.4",
16322169 "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
16332170 "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
16342171 "license": "MIT"
16352172 },
2173 "node_modules/ip-address": {
2174 "version": "9.0.5",
2175 "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
2176 "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
2177 "license": "MIT",
2178 "dependencies": {
2179 "jsbn": "1.1.0",
2180 "sprintf-js": "^1.1.3"
2181 },
2182 "engines": {
2183 "node": ">= 12"
2184 }
2185 },
16362186 "node_modules/is-alphabetical": {
16372187 "version": "2.0.1",
16382188 "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
......@@ -1657,6 +2207,12 @@
16572207 "url": "https://github.com/sponsors/wooorm"
16582208 }
16592209 },
2210 "node_modules/is-arrayish": {
2211 "version": "0.2.1",
2212 "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
2213 "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
2214 "license": "MIT"
2215 },
16602216 "node_modules/is-decimal": {
16612217 "version": "2.0.1",
16622218 "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
......@@ -1667,6 +2223,15 @@
16672223 "url": "https://github.com/sponsors/wooorm"
16682224 }
16692225 },
2226 "node_modules/is-fullwidth-code-point": {
2227 "version": "3.0.0",
2228 "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
2229 "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
2230 "license": "MIT",
2231 "engines": {
2232 "node": ">=8"
2233 }
2234 },
16702235 "node_modules/is-hexadecimal": {
16712236 "version": "2.0.1",
16722237 "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
......@@ -1695,6 +2260,24 @@
16952260 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
16962261 "license": "MIT"
16972262 },
2263 "node_modules/js-yaml": {
2264 "version": "4.1.0",
2265 "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
2266 "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
2267 "license": "MIT",
2268 "dependencies": {
2269 "argparse": "^2.0.1"
2270 },
2271 "bin": {
2272 "js-yaml": "bin/js-yaml.js"
2273 }
2274 },
2275 "node_modules/jsbn": {
2276 "version": "1.1.0",
2277 "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
2278 "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
2279 "license": "MIT"
2280 },
16982281 "node_modules/jsesc": {
16992282 "version": "3.1.0",
17002283 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
......@@ -1707,6 +2290,12 @@
17072290 "node": ">=6"
17082291 }
17092292 },
2293 "node_modules/json-parse-even-better-errors": {
2294 "version": "2.3.1",
2295 "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
2296 "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
2297 "license": "MIT"
2298 },
17102299 "node_modules/json5": {
17112300 "version": "2.2.3",
17122301 "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
......@@ -1746,6 +2335,12 @@
17462335 "lasso-caching-fs": "^1.0.0"
17472336 }
17482337 },
2338 "node_modules/lines-and-columns": {
2339 "version": "1.2.4",
2340 "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
2341 "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
2342 "license": "MIT"
2343 },
17492344 "node_modules/longest-streak": {
17502345 "version": "3.1.0",
17512346 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
......@@ -2566,18 +3161,86 @@
25663161 ],
25673162 "license": "MIT"
25683163 },
3164 "node_modules/mitt": {
3165 "version": "3.0.1",
3166 "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
3167 "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
3168 "license": "MIT"
3169 },
25693170 "node_modules/ms": {
25703171 "version": "2.1.3",
25713172 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
25723173 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
25733174 "license": "MIT"
25743175 },
3176 "node_modules/netmask": {
3177 "version": "2.0.2",
3178 "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
3179 "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
3180 "license": "MIT",
3181 "engines": {
3182 "node": ">= 0.4.0"
3183 }
3184 },
25753185 "node_modules/node-releases": {
25763186 "version": "2.0.19",
25773187 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
25783188 "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
25793189 "license": "MIT"
25803190 },
3191 "node_modules/once": {
3192 "version": "1.4.0",
3193 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
3194 "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
3195 "license": "ISC",
3196 "dependencies": {
3197 "wrappy": "1"
3198 }
3199 },
3200 "node_modules/pac-proxy-agent": {
3201 "version": "7.2.0",
3202 "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
3203 "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
3204 "license": "MIT",
3205 "dependencies": {
3206 "@tootallnate/quickjs-emscripten": "^0.23.0",
3207 "agent-base": "^7.1.2",
3208 "debug": "^4.3.4",
3209 "get-uri": "^6.0.1",
3210 "http-proxy-agent": "^7.0.0",
3211 "https-proxy-agent": "^7.0.6",
3212 "pac-resolver": "^7.0.1",
3213 "socks-proxy-agent": "^8.0.5"
3214 },
3215 "engines": {
3216 "node": ">= 14"
3217 }
3218 },
3219 "node_modules/pac-resolver": {
3220 "version": "7.0.1",
3221 "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
3222 "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
3223 "license": "MIT",
3224 "dependencies": {
3225 "degenerator": "^5.0.0",
3226 "netmask": "^2.0.2"
3227 },
3228 "engines": {
3229 "node": ">= 14"
3230 }
3231 },
3232 "node_modules/parent-module": {
3233 "version": "1.0.1",
3234 "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
3235 "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
3236 "license": "MIT",
3237 "dependencies": {
3238 "callsites": "^3.0.0"
3239 },
3240 "engines": {
3241 "node": ">=6"
3242 }
3243 },
25813244 "node_modules/parse-entities": {
25823245 "version": "4.0.2",
25833246 "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
......@@ -2603,12 +3266,45 @@
26033266 "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
26043267 "license": "MIT"
26053268 },
3269 "node_modules/parse-json": {
3270 "version": "5.2.0",
3271 "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
3272 "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
3273 "license": "MIT",
3274 "dependencies": {
3275 "@babel/code-frame": "^7.0.0",
3276 "error-ex": "^1.3.1",
3277 "json-parse-even-better-errors": "^2.3.0",
3278 "lines-and-columns": "^1.1.6"
3279 },
3280 "engines": {
3281 "node": ">=8"
3282 },
3283 "funding": {
3284 "url": "https://github.com/sponsors/sindresorhus"
3285 }
3286 },
3287 "node_modules/pend": {
3288 "version": "1.2.0",
3289 "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
3290 "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
3291 "license": "MIT"
3292 },
26063293 "node_modules/picocolors": {
26073294 "version": "1.1.1",
26083295 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
26093296 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
26103297 "license": "ISC"
26113298 },
3299 "node_modules/progress": {
3300 "version": "2.0.3",
3301 "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
3302 "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
3303 "license": "MIT",
3304 "engines": {
3305 "node": ">=0.4.0"
3306 }
3307 },
26123308 "node_modules/property-information": {
26133309 "version": "7.1.0",
26143310 "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
......@@ -2619,6 +3315,88 @@
26193315 "url": "https://github.com/sponsors/wooorm"
26203316 }
26213317 },
3318 "node_modules/proxy-agent": {
3319 "version": "6.5.0",
3320 "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
3321 "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
3322 "license": "MIT",
3323 "dependencies": {
3324 "agent-base": "^7.1.2",
3325 "debug": "^4.3.4",
3326 "http-proxy-agent": "^7.0.1",
3327 "https-proxy-agent": "^7.0.6",
3328 "lru-cache": "^7.14.1",
3329 "pac-proxy-agent": "^7.1.0",
3330 "proxy-from-env": "^1.1.0",
3331 "socks-proxy-agent": "^8.0.5"
3332 },
3333 "engines": {
3334 "node": ">= 14"
3335 }
3336 },
3337 "node_modules/proxy-agent/node_modules/lru-cache": {
3338 "version": "7.18.3",
3339 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
3340 "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
3341 "license": "ISC",
3342 "engines": {
3343 "node": ">=12"
3344 }
3345 },
3346 "node_modules/proxy-from-env": {
3347 "version": "1.1.0",
3348 "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
3349 "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
3350 "license": "MIT"
3351 },
3352 "node_modules/pump": {
3353 "version": "3.0.2",
3354 "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
3355 "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
3356 "license": "MIT",
3357 "dependencies": {
3358 "end-of-stream": "^1.1.0",
3359 "once": "^1.3.1"
3360 }
3361 },
3362 "node_modules/puppeteer": {
3363 "version": "24.10.1",
3364 "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.10.1.tgz",
3365 "integrity": "sha512-7T3rfSaaPt5A31VITV5YKQ4wPCCv4aPn8byDaV+9lhDU9v7BWYY4Ncwerw3ZR5mIolrh/PvzGdIDK7yiBth75g==",
3366 "hasInstallScript": true,
3367 "license": "Apache-2.0",
3368 "dependencies": {
3369 "@puppeteer/browsers": "2.10.5",
3370 "chromium-bidi": "5.1.0",
3371 "cosmiconfig": "^9.0.0",
3372 "devtools-protocol": "0.0.1452169",
3373 "puppeteer-core": "24.10.1",
3374 "typed-query-selector": "^2.12.0"
3375 },
3376 "bin": {
3377 "puppeteer": "lib/cjs/puppeteer/node/cli.js"
3378 },
3379 "engines": {
3380 "node": ">=18"
3381 }
3382 },
3383 "node_modules/puppeteer-core": {
3384 "version": "24.10.1",
3385 "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.1.tgz",
3386 "integrity": "sha512-AE6doA9znmEEps/pC5lc9p0zejCdNLR6UBp3EZ49/15Nbvh+uklXxGox7Qh8/lFGqGVwxInl0TXmsOmIuIMwiQ==",
3387 "license": "Apache-2.0",
3388 "dependencies": {
3389 "@puppeteer/browsers": "2.10.5",
3390 "chromium-bidi": "5.1.0",
3391 "debug": "^4.4.1",
3392 "devtools-protocol": "0.0.1452169",
3393 "typed-query-selector": "^2.12.0",
3394 "ws": "^8.18.2"
3395 },
3396 "engines": {
3397 "node": ">=18"
3398 }
3399 },
26223400 "node_modules/raptor-async": {
26233401 "version": "1.1.3",
26243402 "resolved": "https://registry.npmjs.org/raptor-async/-/raptor-async-1.1.3.tgz",
......@@ -2769,6 +3547,15 @@
27693547 "url": "https://opencollective.com/unified"
27703548 }
27713549 },
3550 "node_modules/require-directory": {
3551 "version": "2.1.1",
3552 "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
3553 "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
3554 "license": "MIT",
3555 "engines": {
3556 "node": ">=0.10.0"
3557 }
3558 },
27723559 "node_modules/resolve-from": {
27733560 "version": "5.0.0",
27743561 "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
......@@ -2796,6 +3583,44 @@
27963583 "semver": "bin/semver.js"
27973584 }
27983585 },
3586 "node_modules/smart-buffer": {
3587 "version": "4.2.0",
3588 "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
3589 "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
3590 "license": "MIT",
3591 "engines": {
3592 "node": ">= 6.0.0",
3593 "npm": ">= 3.0.0"
3594 }
3595 },
3596 "node_modules/socks": {
3597 "version": "2.8.5",
3598 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
3599 "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
3600 "license": "MIT",
3601 "dependencies": {
3602 "ip-address": "^9.0.5",
3603 "smart-buffer": "^4.2.0"
3604 },
3605 "engines": {
3606 "node": ">= 10.0.0",
3607 "npm": ">= 3.0.0"
3608 }
3609 },
3610 "node_modules/socks-proxy-agent": {
3611 "version": "8.0.5",
3612 "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
3613 "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
3614 "license": "MIT",
3615 "dependencies": {
3616 "agent-base": "^7.1.2",
3617 "debug": "^4.3.4",
3618 "socks": "^2.8.3"
3619 },
3620 "engines": {
3621 "node": ">= 14"
3622 }
3623 },
27993624 "node_modules/source-map": {
28003625 "version": "0.7.4",
28013626 "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
......@@ -2834,12 +3659,66 @@
28343659 "url": "https://github.com/sponsors/wooorm"
28353660 }
28363661 },
3662 "node_modules/sprintf-js": {
3663 "version": "1.1.3",
3664 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
3665 "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
3666 "license": "BSD-3-Clause"
3667 },
28373668 "node_modules/stackframe": {
28383669 "version": "1.3.4",
28393670 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
28403671 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
28413672 "license": "MIT"
28423673 },
3674 "node_modules/streamx": {
3675 "version": "2.22.1",
3676 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz",
3677 "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==",
3678 "license": "MIT",
3679 "dependencies": {
3680 "fast-fifo": "^1.3.2",
3681 "text-decoder": "^1.1.0"
3682 },
3683 "optionalDependencies": {
3684 "bare-events": "^2.2.0"
3685 }
3686 },
3687 "node_modules/string-width": {
3688 "version": "4.2.3",
3689 "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
3690 "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
3691 "license": "MIT",
3692 "dependencies": {
3693 "emoji-regex": "^8.0.0",
3694 "is-fullwidth-code-point": "^3.0.0",
3695 "strip-ansi": "^6.0.1"
3696 },
3697 "engines": {
3698 "node": ">=8"
3699 }
3700 },
3701 "node_modules/string-width/node_modules/ansi-regex": {
3702 "version": "5.0.1",
3703 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
3704 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
3705 "license": "MIT",
3706 "engines": {
3707 "node": ">=8"
3708 }
3709 },
3710 "node_modules/string-width/node_modules/strip-ansi": {
3711 "version": "6.0.1",
3712 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
3713 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
3714 "license": "MIT",
3715 "dependencies": {
3716 "ansi-regex": "^5.0.1"
3717 },
3718 "engines": {
3719 "node": ">=8"
3720 }
3721 },
28433722 "node_modules/stringify-entities": {
28443723 "version": "4.0.4",
28453724 "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
......@@ -2887,6 +3766,40 @@
28873766 "inline-style-parser": "0.2.4"
28883767 }
28893768 },
3769 "node_modules/tar-fs": {
3770 "version": "3.0.9",
3771 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
3772 "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
3773 "license": "MIT",
3774 "dependencies": {
3775 "pump": "^3.0.0",
3776 "tar-stream": "^3.1.5"
3777 },
3778 "optionalDependencies": {
3779 "bare-fs": "^4.0.1",
3780 "bare-path": "^3.0.0"
3781 }
3782 },
3783 "node_modules/tar-stream": {
3784 "version": "3.1.7",
3785 "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
3786 "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
3787 "license": "MIT",
3788 "dependencies": {
3789 "b4a": "^1.6.4",
3790 "fast-fifo": "^1.2.0",
3791 "streamx": "^2.15.0"
3792 }
3793 },
3794 "node_modules/text-decoder": {
3795 "version": "1.2.3",
3796 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
3797 "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
3798 "license": "Apache-2.0",
3799 "dependencies": {
3800 "b4a": "^1.6.4"
3801 }
3802 },
28903803 "node_modules/trim-lines": {
28913804 "version": "3.0.1",
28923805 "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
......@@ -2907,11 +3820,23 @@
29073820 "url": "https://github.com/sponsors/wooorm"
29083821 }
29093822 },
3823 "node_modules/tslib": {
3824 "version": "2.8.1",
3825 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
3826 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
3827 "license": "0BSD"
3828 },
3829 "node_modules/typed-query-selector": {
3830 "version": "2.12.0",
3831 "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
3832 "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
3833 "license": "MIT"
3834 },
29103835 "node_modules/typescript": {
29113836 "version": "5.8.3",
29123837 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
29133838 "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
2914 "dev": true,
3839 "devOptional": true,
29153840 "license": "Apache-2.0",
29163841 "bin": {
29173842 "tsc": "bin/tsc",
......@@ -2925,7 +3850,7 @@
29253850 "version": "6.21.0",
29263851 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
29273852 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2928 "dev": true,
3853 "devOptional": true,
29293854 "license": "MIT"
29303855 },
29313856 "node_modules/unified": {
......@@ -3095,12 +4020,132 @@
30954020 "url": "https://opencollective.com/unified"
30964021 }
30974022 },
4023 "node_modules/wrap-ansi": {
4024 "version": "7.0.0",
4025 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
4026 "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
4027 "license": "MIT",
4028 "dependencies": {
4029 "ansi-styles": "^4.0.0",
4030 "string-width": "^4.1.0",
4031 "strip-ansi": "^6.0.0"
4032 },
4033 "engines": {
4034 "node": ">=10"
4035 },
4036 "funding": {
4037 "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
4038 }
4039 },
4040 "node_modules/wrap-ansi/node_modules/ansi-regex": {
4041 "version": "5.0.1",
4042 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
4043 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
4044 "license": "MIT",
4045 "engines": {
4046 "node": ">=8"
4047 }
4048 },
4049 "node_modules/wrap-ansi/node_modules/strip-ansi": {
4050 "version": "6.0.1",
4051 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
4052 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
4053 "license": "MIT",
4054 "dependencies": {
4055 "ansi-regex": "^5.0.1"
4056 },
4057 "engines": {
4058 "node": ">=8"
4059 }
4060 },
4061 "node_modules/wrappy": {
4062 "version": "1.0.2",
4063 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
4064 "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
4065 "license": "ISC"
4066 },
4067 "node_modules/ws": {
4068 "version": "8.18.2",
4069 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
4070 "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
4071 "license": "MIT",
4072 "engines": {
4073 "node": ">=10.0.0"
4074 },
4075 "peerDependencies": {
4076 "bufferutil": "^4.0.1",
4077 "utf-8-validate": ">=5.0.2"
4078 },
4079 "peerDependenciesMeta": {
4080 "bufferutil": {
4081 "optional": true
4082 },
4083 "utf-8-validate": {
4084 "optional": true
4085 }
4086 }
4087 },
4088 "node_modules/y18n": {
4089 "version": "5.0.8",
4090 "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
4091 "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
4092 "license": "ISC",
4093 "engines": {
4094 "node": ">=10"
4095 }
4096 },
30984097 "node_modules/yallist": {
30994098 "version": "3.1.1",
31004099 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
31014100 "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
31024101 "license": "ISC"
31034102 },
4103 "node_modules/yargs": {
4104 "version": "17.7.2",
4105 "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
4106 "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
4107 "license": "MIT",
4108 "dependencies": {
4109 "cliui": "^8.0.1",
4110 "escalade": "^3.1.1",
4111 "get-caller-file": "^2.0.5",
4112 "require-directory": "^2.1.1",
4113 "string-width": "^4.2.3",
4114 "y18n": "^5.0.5",
4115 "yargs-parser": "^21.1.1"
4116 },
4117 "engines": {
4118 "node": ">=12"
4119 }
4120 },
4121 "node_modules/yargs-parser": {
4122 "version": "21.1.1",
4123 "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
4124 "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
4125 "license": "ISC",
4126 "engines": {
4127 "node": ">=12"
4128 }
4129 },
4130 "node_modules/yauzl": {
4131 "version": "2.10.0",
4132 "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
4133 "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
4134 "license": "MIT",
4135 "dependencies": {
4136 "buffer-crc32": "~0.2.3",
4137 "fd-slicer": "~1.1.0"
4138 }
4139 },
4140 "node_modules/zod": {
4141 "version": "3.25.64",
4142 "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz",
4143 "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==",
4144 "license": "MIT",
4145 "funding": {
4146 "url": "https://github.com/sponsors/colinhacks"
4147 }
4148 },
31044149 "node_modules/zwitch": {
31054150 "version": "2.0.4",
31064151 "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
package.json+2
......@@ -8,6 +8,7 @@
88 "hls.js": "^1.6.5",
99 "hono": "^4.7.11",
1010 "marko": "^6.0.20",
11 "puppeteer": "^24.10.1",
1112 "unique-names-generator": "^4.7.1"
1213 },
1314 "devDependencies": {
......@@ -16,6 +17,7 @@
1617 },
1718 "imports": {
1819 "#backend": "./src/backend.ts",
20 "#debug": "./framework/debug.safe.ts",
1921 "#sitegen": "./framework/lib/sitegen.ts",
2022 "#sitegen/*": "./framework/lib/*.ts",
2123 "#ssr": "./framework/engine/ssr.ts",
readme.md+13-10
......@@ -3,26 +3,29 @@
33this repository contains clover's "sitegen" framework, which is a set of tools
44that assist building websites. these tools power https://paperclover.net.
55
6- HTML "Server Side Rendering") engine written from scratch.
7 - A more practical JSX runtime (`class` instead of `className`, etc).
8 - Transparent integration with [Marko][1] to mix component types.
9 - MDX support for text-heavy content pages.
10- Incremental static site generator and build system
6- **HTML "Server Side Rendering") engine written from scratch.** (~500 lines)
7 - A more practical JSX runtime (`class` instead of `className`, built-in
8 `clsx`, `html()` helper over `dangerouslySetInnerHTML` prop, etc).
9 - Integration with [Marko][1] for concisely written components.
10 - TODO: MDX-like compiler for content-heavy pages like blogs.
11 - Different languages can be used at the same time. Supports
12 `async function` components, `<Suspense />`, and custom extensions.
13- **Incremental static site generator and build system.**
1114 - Build entire production site at start, incremental updates when pages
1215 change; Build system state survives coding sessions.
1316 - The only difference in development and production mode is hidden
14 source-maps and stripped assertions and `console.debug` calls. The site
15 you see locally is the site you see deployed.
17 source-maps and stripped `console.debug` calls. The site you
18 see locally is the same site you see deployed.
1619 - (TODO) Tests, Lints, and Type-checking is run alongside, and only re-runs
1720 checks when the files change. For example, changing a component re-tests
1821 only pages that use that component and re-lints only the changed file.
19- Integrated libraries for building complex, content heavy web sites.
22- **Integrated libraries for building complex, content heavy web sites.**
2023 - Static asset serving with ETag and build-time compression.
2124 - Dynamicly rendered pages with static client. (`#import "#sitegen/view"`)
2225 - Databases with a typed SQLite wrapper. (`import "#sitegen/sqlite"`)
2326 - TODO: Meta and Open Graph generation. (`export const meta`)
24 - TODO: Font subsetting tools to reduce
25- Built on the battle-tested Node.js runtime. Partial support for Deno and Bun.
27 - TODO: Font subsetting tools to reduce bytes downloaded by fonts.
28- **Built on the battle-tested Node.js runtime.** Partial support for Deno and Bun.
2629
2730[1]: https://next.markojs.com
2831
run.js+13-26
......@@ -4,18 +4,17 @@ import * as util from "node:util";
44import process from "node:process";
55
66// Disable experimental warnings (Type Stripping, etc)
7{
8 const { emit: originalEmit } = process;
9 const warnings = ["ExperimentalWarning"];
10 process.emit = function (event, error) {
11 return event === "warning" && warnings.includes(error.name)
12 ? false
13 : originalEmit.apply(process, arguments);
14 };
15}
7const { emit: originalEmit } = process;
8const warnings = ["ExperimentalWarning"];
9process.emit = function (event, error) {
10 return event === "warning" && warnings.includes(error.name)
11 ? false
12 : originalEmit.apply(process, arguments);
13};
1614
1715// Init hooks
1816const hot = await import("./framework/hot.ts");
17await import("#debug");
1918
2019const console = hot.load("@paperclover/console");
2120globalThis.console["log"] = console.info;
......@@ -24,22 +23,6 @@ globalThis.console.warn = console.warn;
2423globalThis.console.error = console.error;
2524globalThis.console.debug = console.scoped("dbg");
2625
27globalThis.UNWRAP = (t, ...args) => {
28 if (t == null) {
29 throw new Error(
30 args.length > 0 ? util.format(...args) : "UNWRAP(" + t + ")",
31 );
32 }
33 return t;
34};
35globalThis.ASSERT = (t, ...args) => {
36 if (!t) {
37 throw new Error(
38 args.length > 0 ? util.format(...args) : "Assertion Failed",
39 );
40 }
41};
42
4326// Load with hooks
4427if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) {
4528 if (process.argv.length == 2) {
......@@ -60,7 +43,11 @@ if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) {
6043 process.exit(1);
6144 }
6245 process.argv = [process.argv[0], ...process.argv.slice(2)];
63 hot.load(found).main?.();
46 try {
47 await hot.load(found).main?.();
48 } catch (e) {
49 console.error(util.inspect(e));
50 }
6451}
6552
6653export { hot };
src/admin.ts+12-1
......@@ -59,6 +59,17 @@ export function hasAdminToken(c: Context) {
5959 return token && compareToken(token);
6060}
6161
62import * as fs from "node:fs";
62export async function main() {
63 const key = crypto.randomUUID();
64 await fs.writeMkdir(".clover/admin-token.txt", key);
65 const start = ({
66 win32: "start",
67 darwin: "open",
68 } as Record<string, string>)[process.platform] ?? "xdg-open";
69 child_process.exec(`${start} http://[::1]:3000/admin/login?key=${key}`);
70}
71
72import * as fs from "#sitegen/fs";
6373import type { Context, Next } from "hono";
6474import { serveAsset } from "#sitegen/assets";
75import * as child_process from "node:child_process";
src/backend.ts+13
......@@ -15,6 +15,17 @@ app.route("", require("./q+a/backend.ts").app);
1515
1616app.use(assets.middleware);
1717
18if (process.argv.includes("--development")) {
19 app.onError((err, c) => {
20 if (err instanceof HTTPException) {
21 // Get the custom response
22 return err.getResponse();
23 }
24
25 return c.text(util.inspect(err), 500);
26 });
27}
28
1829export default app;
1930
2031async function removeDuplicateSlashes(c: Context, next: Next) {
......@@ -31,8 +42,10 @@ async function removeDuplicateSlashes(c: Context, next: Next) {
3142}
3243
3344import { type Context, Hono, type Next } from "#hono";
45import { HTTPException } from "hono/http-exception";
3446import { logger } from "hono/logger";
3547import { trimTrailingSlash } from "hono/trailing-slash";
3648import * as assets from "#sitegen/assets";
3749import * as admin from "./admin.ts";
3850import { scoped } from "@paperclover/console";
51import * as util from "node:util";
src/blog/pages/25/marko-intro.markodown created+283
......@@ -0,0 +1,283 @@
1export const blog: BlogMeta = {
2 title: "Marko is the coziest HTML templating language",
3 desc: "...todo...",
4 date: "2025-06-13",
5 draft: true,
6};
7export const meta = formatBlogMeta(blob);
8
9I've been recently playing around [Marko][1], and after adding limited support
10for it in my website generator, [sitegen][2], I instantly fell in love with how
11minimalistic it is in comparison to JSX, Astro components, and Svelte.
12
13## Introduction
14
15If JSX was taking HTML and shoving its syntax into JavaScript, Marko is shoving
16JavaScript into HTML. Attributes are JavaScript expressions.
17
18```marko
19<div>
20 // `input` is like props, but in global scope
21 <time datetime=input.date.toISOString()>
22 // Interpolation with JS template string syntax
23 ${formatTimeNicely(input.date)}
24 </time>
25 <div>
26 <a href=`/users/${input.user.id}`>${input.user.name}</a>
27 </div>
28
29 // Capital letter variables for imported components
30 <MarkdownContent message=input.message />
31</div>
32
33// ESM `import` / `export` just work as expected.
34// I prefer my imports at the end, to highlight the markup.
35import MarkdownContent from "./MarkdownContent.marko";
36import { formatTimeNicely } from "../date-helpers.ts";
37```
38
39Tags with the `value` attribute have a shorthand, which is used by the built-in
40`<if>` for conditional rendering.
41
42```marko
43// Sugar for <input value="string" />
44<input="string" />
45
46// and it composes amazingly to the 'if' built-in
47<if=input.user>
48 <UserProfile=input.user />
49</if>
50```
51
52Tags can also return values into the scope for use in the template using `/`, such as `<id>` for unique ID generation. This is available to components that `<return=output/>`.
53
54```
55<id/uniqueId />
56
57<input id=uniqueId type="checkbox" name="allow_trans_rights" />
58<label for=uniqueId>click me!</>
59// ^ oh, you can also omit the
60// closing tag name if you want.
61```
62
63It's important that I started with the two forms of "Tag I/O": `=` for input
64and `/` for output. With those building blocks, we introduce local variables
65with `const`
66
67```
68<const/rendered = markdownToHtml(input.value) />
69
70// This is how you insert raw HTML to the document
71<inline-html=rendered />
72
73// It supports all of the cozy destructuring syntax JS has
74<const/{ id, name } = user />
75```
76
77Unlike JSX, when you pass content within a tag (`input.content` instead of
78JSX's `children`), instead of it being a JSX element, it is actually a
79function. This means that the `for` tag can render the content multiple times.
80
81```
82<ul>
83 <for from=1 to=10>
84 // Renders a new random number for each iteration.
85 <li>${Math.random()}</li>
86 </>
87</ul>
88```
89
90Since `content` is a function, it can take arguments. This is done with `|`
91
92```
93<h1>my friends</h1>
94<ul>
95 // I tend to omit the closing tag names for the built-in control
96 // flow tags, but I keep them for HTML tags. It's kinda like how
97 // in JavaScript you just write `}` to close your `if`s and loops.
98 //
99 // Anyways <for> also has 'of'
100 <for|item| of=user.friends>
101 <li class="friend">${item.name}</li>
102 </>
103
104 // They support the same syntax JavaScript function params allows,
105 // so you can have destructuring here too, and multiple params.
106 <for|{ name }, index| of=user.friends>
107 // By the way you can also use emmet-style class and ID shorthands.
108 <li.friend>My #${index + 1} friend is ${name}</li>
109 </>
110</ul>
111```
112
113Instead of named slots, Marko has attribute tags. These are more powerful than
114slots since they are functions, and can also act as sugar for more complicated
115attributes.
116
117```
118<Layout title="Welcome">
119 <@header variant="big">
120 <h1>the next big thing</h1>
121 </@header>
122
123 <p>body text...</p>
124</Layout>
125
126// The `input` variable inside of <Layout /> is:
127//
128// {
129// title: "Welcome",
130// header: {
131// content: /* function rendering "<h1>the next big thing</h1>" */,
132// variant: "big",
133// },
134// content: /* function rendering "<p>body text</p>" */
135// }
136```
137
138This layout could be implemented as such:
139
140```marko
141<main>
142 <if=input.header />
143 <const/{ ...headerProps, content }=input.header />
144 <header ...headerProps>
145 // Instead of assigning to a variable with a capital letter,
146 // template interpolation works on tag names. This can also
147 // be a string to render the native HTML tag of that kind.
148 <${content} />
149 </header>
150 <hr />
151 </>
152
153 <${input.content} />
154</main>
155```
156
157The last syntax feature missing is calling a tag with parameters. That is done
158just like a regular function call, with '('.
159
160```
161<Something(item, index) />
162```
163
164In fact, attributes can just be sugar over this syntax; _this technically isn't
165true but it's close enough for the example_
166
167```
168<SpecialButton type="submit" class="red" />
169
170// is equal to
171
172<SpecialButton({ type: "submit", class: "red" }) />
173```
174
175All of the above is about how Marko's syntax works, and how it performs HTML
176generation with components. Marko also allows interactive components, but an
177explaination of that is beyond the scope of this page, mostly since I have not
178used it. A brief example of it, modified from their documentation.
179
180```marko
181// Reactive variables with <let/> just work...
182<let/basicCounter=0 />
183<button onClick() { basicCounter += 1 }>${basicCounter}</button>
184// ...but a counter is boring.
185
186<let/todos=[
187 { id: 0, text: "Learn Marko" },
188 { id: 1, text: "Make a Website" },
189]/>
190
191// 'by' is like React JSX's "key" property, but it's optional.
192<ul><for|todo, i| of=todos by=(todo => todo.id)>
193 <li.todo>
194 // this variable remains stable even if the list
195 // re-orders, because 'by' was specified.
196 <let/done=false/>
197 <label>
198 <span>${todo.text}</span>
199 // ':=' creates a two-way reactive binding,
200 // (it passes a callback for `checkedChanged`)
201 <input type="checkbox" checked:=done />
202 </label>
203 <button
204 title="delete"
205 disabled=!done
206 onClick() {
207 todos = todos.toSpliced(i, 1);
208 }
209 > &times; </button>
210 </li>
211</></ul>
212
213// Form example
214<let/nextId=2/>
215<form onSubmit(e) {
216 e.preventDefault();
217 todos = todos.concat({
218 id: nextId++,
219 // HTMLFormElement exposes all its named input
220 // elements as extra properties on the object.
221 text: e.target.text.value,
222 });
223 // And you can clear it with 'reset()'
224 e.target.reset();
225}>
226 // We don't 'onChange' like a React loser. The form
227 // value can be read in the submit event like normal.
228 <input name="text" placeholder="Another Item">
229 <button type="submit">Add</button>
230</form>
231```
232
233## Usage on `paperclover.net`
234
235TODO: document a lot of feedback, how i embedded Marko
236
237My website uses statically generated HTML. That is why I have not needed to use
238reactive variables. My generator doesn't even try compiling components
239client-side.
240
241Here is the actual component used to render [questions on the clover q+a][/q+a].
242
243```marko
244// Renders a `Question` entry including its markdown body.
245export interface Input {
246 question: Question;
247 admin?: boolean;
248}
249
250// 2024-12-31 05:00:00 EST
251export const transitionDate = 1735639200000;
252
253<const/{ question, admin } = input />
254<const/{ id, date, text } = question/>
255
256<${"e-"}
257 f=(date > transitionDate ? true : undefined)
258 id=admin ? `q${id}` : undefined
259>
260 <if=admin>
261 <a
262 style="margin-right: 0.5rem"
263 href=`/admin/q+a/${id}`
264 >[EDIT]</a>
265 </>
266 <a>
267 <time
268 datetime=formatQuestionISOTimestamp(date)
269 >${formatQuestionTimestamp(date)}</time>
270 </a>
271
272 <CloverMarkdown ...{ text } />
273</>
274
275// this singleton script will make all the '<time>' tags clickable.
276client import "./clickable-links.client.ts";
277
278import type { Question } from "@/q+a/models/Question.ts";
279import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
280import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
281```
282
283import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts';
src/components/Video.tsx deleted-57
......@@ -1,57 +0,0 @@
1import * as path from "node:path";
2import { addScript } from "#sitegen";
3import { PrecomputedBlurhash } from "./blurhash.tsx";
4import "./Video.css";
5export namespace Video {
6 export interface Props {
7 title: string;
8 width: number;
9 height: number;
10 sources: string[];
11 downloads: string[];
12 poster?: string;
13 posterHash?: string;
14 borderless?: boolean;
15 }
16}
17 function PrecomputedBlurhash({ hash }: { hash: string }) {
18export function Video(
19 { title, sources, height, poster, posterHash, width, borderless }:
20 Video.Props,
21) {
22 addScript("./video.client.ts");
23 return (
24 <figure class={`video ${borderless ? "borderless" : ""}`}>
25 <figcaption>{title}</figcaption>
26 {/* posterHash && <PrecomputedBlurhash hash={posterHash} /> */}
27 {poster && <img src={poster} alt="waterfalls" />}
28 <video
29 controls
30 preload="none"
31 style={`width:100%;background:transparent;aspect-ratio:${
32 simplifyFraction(width, height)
33 }`}
34 poster="data:null"
35 >
36 {sources.map((src) => (
37 <source
38 src={src}
39 type={contentTypeFromExt(src)}
40 />
41 ))}
42 </video>
43 </figure>
44 );
45}
46export function contentTypeFromExt(src: string) {
47 if (src.endsWith(".m3u8")) return "application/x-mpegURL";
48 if (src.endsWith(".webm")) return "video/webm";
49 if (src.endsWith(".mp4")) return "video/mp4";
50 if (src.endsWith(".ogg")) return "video/ogg";
51 throw new Error("Unknown video extension: " + path.extname(src));
52}
53const gcd = (a: number, b: number): number => b ? gcd(b, a % b) : a;
54function simplifyFraction(n: number, d: number) {
55 const divisor = gcd(n, d);
56 return `${n / divisor}/${d / divisor}`;
57}
src/components/video.client.ts deleted-29
......@@ -1,29 +0,0 @@
1import type Hls from "hls.js";
2
3let cachedHls: null | Promise<typeof Hls | false> | typeof Hls | false = null;
4
5const loadHls = () =>
6 cachedHls ?? (cachedHls = (
7 // @ts-ignore
8 +"HLS POLYFILL", import("hls.js")
9 ).then((m, y = m.default) => cachedHls = y.isSupported() && y));
10
11const createVideoPlayer = async (videoElement: HTMLVideoElement) => {
12 const sources = videoElement.querySelectorAll("source");
13 for (let i = 0; i < sources.length; i++) {
14 const { src, type } = sources[i];
15 if (videoElement.canPlayType(type)) {
16 videoElement.src = src;
17 return;
18 }
19
20 if (type === "application/x-mpegURL" && await loadHls()) {
21 const hls = new (cachedHls as typeof Hls)();
22 hls.loadSource(src);
23 hls.attachMedia(videoElement);
24 return;
25 }
26 }
27};
28
29document.querySelectorAll("video").forEach(createVideoPlayer);
src/pages/404.mdx deleted-6
......@@ -1,6 +0,0 @@
1export const meta = { title: 'oh no,,,' };
2
3# oh dear
4
5sound the alarms
6
src/q+a/backend.ts+3-3
......@@ -104,12 +104,12 @@ async function sendSuccess(c: Context, date: Date) {
104104 id: formatQuestionId(date),
105105 }, { status: 200 });
106106 }
107 c.res = await renderView(c, "qa_success", {
107 c.res = await renderView(c, "q+a/success", {
108108 permalink: `https://paperclover.net/q+a/${formatQuestionId(date)}`,
109109 });
110110}
111111// Question Permalink
112app.get("/q+a/:id", (c, next) => {
112app.get("/q+a/:id", async (c, next) => {
113113 // from deadname era, the seconds used to be in the url.
114114 // this was removed so that the url can be crafted by hand.
115115 let id = c.req.param("id");
......@@ -138,7 +138,7 @@ app.get("/admin/q+a", async (c) => {
138138 return serveAsset(c, "/admin/q+a", 200);
139139});
140140app.get("/admin/q+a/inbox", async (c) => {
141 return renderView(c, "qa_backend_inbox", {});
141 return renderView(c, "q+a/backend-inbox", {});
142142});
143143app.delete("/admin/q+a/:id", async (c, next) => {
144144 const id = c.req.param("id");
src/q+a/components/Question.marko deleted-37
......@@ -1,37 +0,0 @@
1// Renders a `Question` entry including its markdown body.
2export interface Input {
3 question: Question;
4 admin?: boolean;
5}
6
7// 2024-12-31 05:00:00 EST
8static const transitionDate = 1735639200000;
9
10<const/{ question, admin } = input />
11<const/{ id, date } = question/>
12
13<${"e-"}
14 f=(date > transitionDate ? true : undefined)
15 id=admin ? `q${id}` : undefined
16>
17 <if=admin>
18 <a
19 style="margin-right: 0.5rem"
20 href=`/admin/q+a/${id}`
21 >[EDIT]</a>
22 </>
23 <a>
24 <time
25 datetime=formatQuestionISOTimestamp(date)
26 >${formatQuestionTimestamp(date)}</time>
27 </a>
28
29 <CloverMarkdown text=question.text />
30</>
31
32// this singleton script will make all the '<time>' tags clickable.
33client import "./clickable-links.client.ts";
34
35import type { Question } from "@/q+a/models/Question.ts";
36import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
37import { CloverMarkdown } from "@/q+a/clover-markdown";
src/q+a/components/QuestionForm.marko deleted-13
......@@ -1,13 +0,0 @@
1<form action="/q+a" method="POST">
2 <textarea
3 name="text"
4 placeholder="ask clover a question..."
5 required
6 minlength="1"
7 maxlength="10000"
8 />
9 <div aria-hidden class="title">ask a question</div>
10 <button type="submit">send</button>
11 <div class="disabled-button">send</div>
12</form>
13
src/q+a/components/clickable-links.client.ts deleted-57
......@@ -1,57 +0,0 @@
1async function qaDate(el: HTMLAnchorElement) {
2 if (el.hasAttribute("data-copy-state")) return;
3 const time = el.children[0] as HTMLTimeElement;
4 const prevText = time.textContent;
5 const url = `${window.location.origin}/q+a/${
6 time.dateTime.replace(/[^\d]/g, "").slice(2, 12)
7 }`;
8 el.setAttribute("data-copy-state", "1");
9 try {
10 await navigator.clipboard.writeText(url);
11 time.textContent = "copied permalink to question";
12 time.classList.add("winner");
13 } catch (error) {
14 time.textContent = "failed to copy :(";
15 }
16 setTimeout(() => {
17 el.removeAttribute("data-copy-state");
18 time.textContent = prevText;
19 time.classList.remove("winner");
20 }, 1500);
21}
22requestAnimationFrame(() => {
23 // Make every link clickable
24 document.querySelectorAll("e-").forEach((questionRoot) =>
25 (questionRoot.querySelector("a:not([href])") as HTMLAnchorElement).href =
26 "#"
27 );
28 // Avoid attaching 1500+ event listeners
29 document.querySelector("main")!.addEventListener(
30 "click",
31 (event, element = event.target as HTMLAnchorElement) => {
32 if (
33 !(event.button ||
34 event.which != 1 ||
35 event.metaKey ||
36 event.ctrlKey ||
37 event.shiftKey ||
38 event.altKey ||
39 event.defaultPrevented)
40 ) {
41 while (element && element !== document.body) {
42 if (
43 element.nodeName.toUpperCase() === "A" &&
44 element.getAttribute("href") === "#" &&
45 element.parentElement?.tagName === "E-"
46 ) {
47 event.preventDefault();
48 qaDate(element);
49 return;
50 }
51 element =
52 (element.assignedSlot ?? element.parentNode) as HTMLAnchorElement;
53 }
54 }
55 },
56 );
57});
src/q+a/image.tsx created+82
......@@ -0,0 +1,82 @@
1const width = 768;
2const cacheImageDir = path.resolve(".clover/question_images");
3
4// Cached browser session
5const getBrowser = RefCountedExpirable(
6 () =>
7 puppeteer.launch({
8 // headless: false,
9 args: ["--no-sandbox", "--disable-setuid-sandbox"],
10 }),
11 (b) => b.close(),
12);
13
14export async function renderQuestionImage(question: Question) {
15 const html = await renderViewToString("q+a/embed-image", { question });
16
17 // this browser session will be reused if multiple images are generated
18 // either at the same time or within a 5-minute time span. the dispose
19 // symbol
20 using sharedBrowser = await getBrowser();
21 const b = sharedBrowser.value;
22
23 const p = await b.newPage();
24 await p.setViewport({ width, height: 400 });
25 await p.setContent(html);
26 try {
27 await p.waitForNetworkIdle({ idleTime: 100, timeout: 500 });
28 } catch (e) {}
29 const height = await p.evaluate(() => {
30 const e = document.querySelector("main")!;
31 return e.getBoundingClientRect().height;
32 });
33 const buf = await p.screenshot({
34 path: "screenshot.png",
35 type: "png",
36 captureBeyondViewport: true,
37 clip: { x: 0, width, y: 0, height: height, scale: 1.5 },
38 });
39 await p.close();
40
41 return Buffer.from(buf);
42}
43
44export async function getQuestionImage(
45 question: Question,
46 headOnly: boolean,
47): Promise<Response> {
48 const hash = crypto.createHash("sha1")
49 .update(question.qmid + question.type + question.text)
50 .digest("hex");
51
52 const headers = {
53 "Content-Type": "image/png",
54 "Cache-Control": "public, max-age=31536000",
55 "ETag": `"${hash}"`,
56 "Last-Modified": question.date.toUTCString(),
57 };
58
59 if (headOnly) {
60 return new Response(null, { headers });
61 }
62
63 const cachedFilePath = path.join(cacheImageDir, `/${hash}.png`);
64 let buf: Buffer;
65 try {
66 buf = await fs.readFile(cachedFilePath);
67 } catch (e: any) {
68 if (e.code !== "ENOENT") throw e;
69 buf = await renderQuestionImage(question);
70 fs.writeMkdir(cachedFilePath, buf).catch(() => {});
71 }
72
73 return new Response(buf, { headers });
74}
75
76import * as crypto from "node:crypto";
77import * as fs from "#sitegen/fs";
78import * as path from "node:path";
79import * as puppeteer from "puppeteer";
80import { Question } from "@/q+a/models/Question.ts";
81import { RefCountedExpirable } from "#sitegen/async";
82import { renderViewToString } from "#sitegen/view";
src/q+a/pages/q+a.marko+3-5
......@@ -12,15 +12,13 @@ export const meta: Metadata = {
1212<const/questions = [...Question.getAll()] />
1313
1414<if=!admin>
15 <QuestionForm />
15 <question-form />
1616</>
17<for|q| of=questions>
18 <QuestionRender question=q admin=admin />
17<for|question| of=questions>
18 <question ...{ question, admin } />
1919</>
2020<footer>
2121 fun fact: clover has answered ${questions.length} questions
2222</footer>
2323
2424import { Question } from "@/q+a/models/Question.ts";
25import { QuestionForm } from "@/q+a/components/QuestionForm.marko";
26import QuestionRender from '@/q+a/components/Question.marko';
src/q+a/scripts/editor.client.tsx created+115
......@@ -0,0 +1,115 @@
1import { basicSetup, EditorState, EditorView } from "codemirror";
2import { ssrSync } from "#ssr";
3// @ts-ignore
4import type { ScriptPayload } from "../views/editor.marko";
5// @ts-ignore
6import QuestionRender from "@/q+a/components/Question.marko";
7
8declare const payload: ScriptPayload;
9const date = new Date(payload.date);
10
11const main = document.getElementById("edit-grid")! as HTMLDivElement;
12const preview = document.getElementById("preview")! as HTMLDivElement;
13
14function updatePreview(text: string) {
15 preview.innerHTML = ssrSync(
16 <QuestionRender
17 question={{
18 id: payload.id,
19 qmid: payload.qmid,
20 text: text,
21 type: payload.type,
22 date,
23 }}
24 />,
25 ).text;
26}
27
28const startState = EditorState.create({
29 doc: payload.text,
30 extensions: [
31 basicSetup,
32 EditorView.darkTheme.of(true),
33 EditorView.updateListener.of((update) => {
34 if (update.docChanged) {
35 updatePreview(update.state.doc.toString());
36 }
37 }),
38 EditorView.lineWrapping,
39 ],
40 // selection: EditorSelection.create([
41 // EditorSelection.cursor(0),
42 // ], 0),
43});
44
45const view = new EditorView({
46 state: startState,
47 parent: document.getElementById("editor")!,
48});
49view.focus();
50
51(globalThis as any).onCommitQuestion = wrapAction(async () => {
52 const text = view.state.doc.toString();
53 const res = await fetch(`/admin/q+a/${payload.id}`, {
54 method: "PATCH",
55 headers: {
56 Accept: "application/json",
57 "Content-Type": "application/json",
58 },
59 body: JSON.stringify({
60 text,
61 type: payload.type,
62 }),
63 });
64 if (!res.ok) {
65 throw new Error("Failed to update question, status: " + res.status);
66 }
67 if (location.search.includes("return=inbox")) {
68 location.href = "/admin/q+a/inbox";
69 } else {
70 location.href = "/q+a#q" + payload.id;
71 }
72});
73(globalThis as any).onDelete = wrapAction(async () => {
74 if (confirm("Are you sure you want to delete this question?")) {
75 const res = await fetch(`/admin/q+a/${payload.id}`, {
76 method: "DELETE",
77 headers: {
78 Accept: "application/json",
79 },
80 });
81 if (!res.ok) {
82 throw new Error("Failed to delete question, status: " + res.status);
83 }
84 location.href = document.referrer || "/admin/q+a";
85 }
86});
87(globalThis as any).onTypeChange = () => {
88 payload.type = parseInt(
89 (document.getElementById("type") as HTMLSelectElement).value,
90 );
91 updatePreview(view.state.doc.toString());
92};
93
94function wrapAction(cb: () => Promise<void>) {
95 return async () => {
96 main.style.opacity = "0.5";
97 main.style.pointerEvents = "none";
98 const inputs = main.querySelectorAll("button,select,input") as NodeListOf<
99 HTMLButtonElement
100 >;
101 inputs.forEach((b) => {
102 b.disabled = true;
103 });
104 try {
105 await cb();
106 } catch (e: any) {
107 main.style.opacity = "1";
108 main.style.pointerEvents = "auto";
109 inputs.forEach((b) => {
110 b.disabled = false;
111 });
112 alert(e.message);
113 }
114 };
115}
src/q+a/tags/clickable-links.client.ts created+57
......@@ -0,0 +1,57 @@
1async function qaDate(el: HTMLAnchorElement) {
2 if (el.hasAttribute("data-copy-state")) return;
3 const time = el.children[0] as HTMLTimeElement;
4 const prevText = time.textContent;
5 const url = `${window.location.origin}/q+a/${
6 time.dateTime.replace(/[^\d]/g, "").slice(2, 12)
7 }`;
8 el.setAttribute("data-copy-state", "1");
9 try {
10 await navigator.clipboard.writeText(url);
11 time.textContent = "copied permalink to question";
12 time.classList.add("winner");
13 } catch (error) {
14 time.textContent = "failed to copy :(";
15 }
16 setTimeout(() => {
17 el.removeAttribute("data-copy-state");
18 time.textContent = prevText;
19 time.classList.remove("winner");
20 }, 1500);
21}
22requestAnimationFrame(() => {
23 // Make every link clickable
24 document.querySelectorAll("e-").forEach((questionRoot) =>
25 (questionRoot.querySelector("a:not([href])") as HTMLAnchorElement).href =
26 "#"
27 );
28 // Avoid attaching 1500+ event listeners
29 document.querySelector("main")!.addEventListener(
30 "click",
31 (event, element = event.target as HTMLAnchorElement) => {
32 if (
33 !(event.button ||
34 event.which != 1 ||
35 event.metaKey ||
36 event.ctrlKey ||
37 event.shiftKey ||
38 event.altKey ||
39 event.defaultPrevented)
40 ) {
41 while (element && element !== document.body) {
42 if (
43 element.nodeName.toUpperCase() === "A" &&
44 element.getAttribute("href") === "#" &&
45 element.parentElement?.tagName === "E-"
46 ) {
47 event.preventDefault();
48 qaDate(element);
49 return;
50 }
51 element =
52 (element.assignedSlot ?? element.parentNode) as HTMLAnchorElement;
53 }
54 }
55 },
56 );
57});
src/q+a/tags/question-form.marko created+13
......@@ -0,0 +1,13 @@
1<form action="/q+a" method="POST">
2 <textarea
3 name="text"
4 placeholder="ask clover a question..."
5 required
6 minlength="1"
7 maxlength="10000"
8 />
9 <div aria-hidden class="title">ask a question</div>
10 <button type="submit">send</button>
11 <div class="disabled-button">send</div>
12</form>
13
src/q+a/tags/question.marko created+37
......@@ -0,0 +1,37 @@
1// Renders a `Question` entry including its markdown body.
2export interface Input {
3 question: Question;
4 admin?: boolean;
5}
6
7// 2024-12-31 05:00:00 EST
8export const transitionDate = 1735639200000;
9
10<const/{ question, admin } = input />
11<const/{ id, date } = question/>
12
13<${"e-"}
14 f=(date > transitionDate ? true : undefined)
15 id=admin ? `q${id}` : undefined
16>
17 <if=admin>
18 <a
19 style="margin-right: 0.5rem"
20 href=`/admin/q+a/${id}`
21 >[EDIT]</a>
22 </>
23 <a>
24 <time
25 datetime=formatQuestionISOTimestamp(date)
26 >${formatQuestionTimestamp(date)}</time>
27 </a>
28
29 <CloverMarkdown text=question.text />
30</>
31
32// this singleton script will make all the '<time>' tags clickable.
33client import "./clickable-links.client.ts";
34
35import type { Question } from "@/q+a/models/Question.ts";
36import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
37import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
src/q+a/views/backend-inbox.client.ts+74
......@@ -0,0 +1,74 @@
1// @ts-ignore
2globalThis.onReply = (id: string) => {
3 location.href = `/admin/q+a/${id}?return=inbox`;
4};
5// @ts-ignore
6globalThis.onDelete = async (id: string) => {
7 const div = document.querySelector(`[data-q="${id}"]`) as HTMLDivElement;
8 if (!div) return alert("Question not found");
9
10 // Pending State
11 div.style.opacity = "0.5";
12 div.style.pointerEvents = "none";
13 div?.querySelectorAll("button").forEach((b) => {
14 b.disabled = true;
15 });
16
17 try {
18 const resp = await fetch(`/admin/q+a/${id}`, {
19 method: "DELETE",
20 headers: {
21 Accept: "application/json",
22 },
23 });
24 if (resp.status !== 200) {
25 throw new Error("Failed to delete question, status: " + resp.status);
26 }
27 } catch (e: any) {
28 div.style.opacity = "1";
29 div.style.pointerEvents = "auto";
30 div?.querySelectorAll("button").forEach((b) => {
31 b.disabled = false;
32 });
33 return alert(e.message);
34 }
35
36 div.remove();
37};
38// @ts-ignore
39globalThis.onDeleteFull = async (id: string) => {
40 const div = document.querySelector(`[data-q="${id}"]`) as HTMLDivElement;
41 if (!div) return alert("Question not found");
42
43 // Confirmation
44 if (!confirm("Are you sure you want to delete this question?")) return;
45
46 // Pending State
47 div.style.opacity = "0.5";
48 div.style.pointerEvents = "none";
49 div?.querySelectorAll("button").forEach((b) => {
50 b.disabled = true;
51 });
52
53 try {
54 const resp = await fetch(`/admin/q+a/${id}`, {
55 method: "DELETE",
56 headers: {
57 Accept: "application/json",
58 "X-Delete-Full": "true",
59 },
60 });
61 if (resp.status !== 200) {
62 throw new Error("Failed to delete question, status: " + resp.status);
63 }
64 } catch (e: any) {
65 div.style.opacity = "1";
66 div.style.pointerEvents = "auto";
67 div?.querySelectorAll("button").forEach((b) => {
68 b.disabled = false;
69 });
70 return alert(e.message);
71 }
72
73 div.remove();
74};
src/q+a/views/editor.css created+39
......@@ -0,0 +1,39 @@
1#edit-grid {
2 display: grid;
3 grid-template-columns: 1fr 80ch;
4 grid-template-rows: 3rem 1fr;
5 grid-gap: 1em;
6 height: 100vh;
7
8 button {
9 margin-right: 1rem;
10 }
11 main {
12 padding: 0;
13 margin: 0;
14 }
15}
16#topleft, #topright {
17 padding: 1rem;
18}
19#topleft {
20 padding-right: 0rem;
21}
22#topright {
23 padding-left: 0rem;
24}
25#preview {
26 overflow-y: auto;
27 e- {
28 margin-top: 0;
29 }
30}
31#editor {
32 background-color: #303030;
33 overflow-y: scroll;
34 height: 100%;
35}
36.cm-scroller {
37 overflow-y: auto;
38 height: 100%;
39}
src/q+a/views/editor.marko created+65
......@@ -0,0 +1,65 @@
1import "./editor.css";
2export { theme } from "@/q+a/layout.tsx";
3
4export const meta = { title: "question editor" };
5
6export interface Input {
7 question: Question;
8 pendingInfo: PendingQuestion.JSON | null;
9}
10
11export interface ScriptPayload {
12 id: string;
13 qmid: string;
14 text: string;
15 type: Question.Type;
16 date: string;
17}
18
19<const/{ question } = input />
20
21<const/payload= {
22 id: question.id,
23 qmid: question.qmid,
24 text: question.text,
25 type: question.type,
26 date: question.date.toISOString(),
27} satisfies ScriptPayload/>
28
29<div#edit-grid.qa>
30 <div#topleft>
31 <button
32 onclick="location.href='/admin/q+a'"
33 style={ color: "magenta" }
34 >
35 all
36 </button>
37 <button
38 onclick="location.href='/admin/q+a/inbox'"
39 style={ color: "dodgerblue" }
40 >
41 inbox
42 </button>
43 </div>
44 <div#topright>
45 <button onclick="onCommitQuestion()" style="color:lime">
46 update
47 </button>
48 <button onclick="onDelete()" style={ color: "red" }>
49 reject
50 </button>
51 <select id="type" onchange="onTypeChange()" value=question.type>
52 <option value=QuestionType.normal>normal</>
53 <option value=QuestionType.annotation>annotation</>
54 <option value=QuestionType.reject>reject</>
55 </select>
56 </div>
57 <div#editor />
58 <main#preview>
59 <question ...{question} />
60 </main>
61</div>
62<html-script src="/js/edit_frontend.js" type="module" />
63
64import { type PendingQuestion } from "@/q+a/models/PendingQuestion.ts";
65import { Question, QuestionType } from "@/q+a/models/Question.ts";
src/q+a/views/image-embed.marko created+16
......@@ -0,0 +1,16 @@
1export const meta = { title: "embed image" };
2export interface Input {
3 question: Question;
4}
5
6<html-style>
7 main { padding-top: 11px; }
8 e- { margin: 0!important }
9 e- > :first-child { margin-top: 0!important }
10 e- > :last-child { margin-bottom: 0!important }
11</html-style>
12<main.qa>
13 <question question=input.question />
14</main>
15
16import { Question } from '@/q+a/models/Question';
src/q+a/views/permalink.marko created+47
......@@ -0,0 +1,47 @@
1export interface Input {
2 question: Question;
3}
4
5server export function meta({ context: { req }, question }) {
6 const isDiscord = req.get("user-agent")
7 ?.toLowerCase()
8 .includes("discordbot");
9 if (question.type === QuestionType.normal) {
10 return {
11 title: "question permalink",
12 openGraph: {
13 images: [{ url: `https://paperclover.net/q+a/${q.id}.png` }],
14 },
15 twitter: { card: "summary_large_image" },
16 themeColor: isDiscord
17 ? q.date.getTime() > transitionDate ? "#8c78ff" : "#58ff71"
18 : undefined,
19 };
20 }
21}
22
23<const/{ question }=input/>
24<const/{ type }=question/>
25<if=type==QuestionType.normal>
26 <p>this page is a permalink to the following question:</p>
27 <question ...{question} />
28</><else if=type==QuestionType.pending>
29 <p>
30 this page is a permalink to a question that
31 has not yet been answered.
32 </p>
33 <p><a href="/q+a">read questions with existing responses</a>.</p>
34</><else if=type==QuestionType.reject>
35 <p>
36 this page is a permalink to a question, but the question
37 was deleted instead of answered. maybe it was sent multiple
38 times, or maybe the question was not a question. who knows.
39 </p>
40 <p>sorry, sister</p>
41 <p><a href="/q+a">all questions</a></p>
42</><else>
43 <p>oh dear, this question is in an invalid state</p>
44 <pre>${JSON.stringify(question, null, 2)}</pre>
45</>
46
47import { Question, QuestionType } from '@/q+a/models/Question.ts';
src/site.ts+4-5
......@@ -3,17 +3,16 @@
33// sub-projects like the file viewer in 'file', or the question answer system
44// in 'q+a'. Each section can define configuration, pages, backend routes, and
55// contain other files.
6interface Section {
7 root: string;
8}
9
106const join = (...paths: string[]) => path.join(import.meta.dirname, ...paths);
117
128export const siteSections: Section[] = [
13 { root: join("./") },
9 { root: join(".") },
1410 { root: join("q+a/") },
1511 { root: join("file-viewer/") },
1612 { root: join("friends/") },
13 // { root: join("blog/"), pageBase: "/blog" },
14 // { root: join("fiction/"), pageBase: "/fiction" },
1715];
1816
1917import * as path from "node:path";
18import { Section } from "#sitegen";
src/tags/Video.tsx created+57
......@@ -0,0 +1,57 @@
1import * as path from "node:path";
2import { addScript } from "#sitegen";
3import { PrecomputedBlurhash } from "./blurhash.tsx";
4import "./Video.css";
5export namespace Video {
6 export interface Props {
7 title: string;
8 width: number;
9 height: number;
10 sources: string[];
11 downloads: string[];
12 poster?: string;
13 posterHash?: string;
14 borderless?: boolean;
15 }
16}
17 function PrecomputedBlurhash({ hash }: { hash: string }) {
18export function Video(
19 { title, sources, height, poster, posterHash, width, borderless }:
20 Video.Props,
21) {
22 addScript("./video.client.ts");
23 return (
24 <figure class={`video ${borderless ? "borderless" : ""}`}>
25 <figcaption>{title}</figcaption>
26 {/* posterHash && <PrecomputedBlurhash hash={posterHash} /> */}
27 {poster && <img src={poster} alt="waterfalls" />}
28 <video
29 controls
30 preload="none"
31 style={`width:100%;background:transparent;aspect-ratio:${
32 simplifyFraction(width, height)
33 }`}
34 poster="data:null"
35 >
36 {sources.map((src) => (
37 <source
38 src={src}
39 type={contentTypeFromExt(src)}
40 />
41 ))}
42 </video>
43 </figure>
44 );
45}
46export function contentTypeFromExt(src: string) {
47 if (src.endsWith(".m3u8")) return "application/x-mpegURL";
48 if (src.endsWith(".webm")) return "video/webm";
49 if (src.endsWith(".mp4")) return "video/mp4";
50 if (src.endsWith(".ogg")) return "video/ogg";
51 throw new Error("Unknown video extension: " + path.extname(src));
52}
53const gcd = (a: number, b: number): number => b ? gcd(b, a % b) : a;
54function simplifyFraction(n: number, d: number) {
55 const divisor = gcd(n, d);
56 return `${n / divisor}/${d / divisor}`;
57}
src/tags/video.client.ts created+29
......@@ -0,0 +1,29 @@
1import type Hls from "hls.js";
2
3let cachedHls: null | Promise<typeof Hls | false> | typeof Hls | false = null;
4
5const loadHls = () =>
6 cachedHls ?? (cachedHls = (
7 // @ts-ignore
8 +"HLS POLYFILL", import("hls.js")
9 ).then((m, y = m.default) => cachedHls = y.isSupported() && y));
10
11const createVideoPlayer = async (videoElement: HTMLVideoElement) => {
12 const sources = videoElement.querySelectorAll("source");
13 for (let i = 0; i < sources.length; i++) {
14 const { src, type } = sources[i];
15 if (videoElement.canPlayType(type)) {
16 videoElement.src = src;
17 return;
18 }
19
20 if (type === "application/x-mpegURL" && await loadHls()) {
21 const hls = new (cachedHls as typeof Hls)();
22 hls.loadSource(src);
23 hls.attachMedia(videoElement);
24 return;
25 }
26 }
27};
28
29document.querySelectorAll("video").forEach(createVideoPlayer);
tsconfig.json+2-1
......@@ -15,5 +15,6 @@
1515 "strict": true,
1616 "verbaitimModuleSyntax": true,
1717 "target": "es2022"
18 }
18 },
19 "include": ["framework/**/*", "src/**/*"]
1920}