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 @@...@@ -1,4 +1,5 @@
1import "@paperclover/console/inject"; 1import "@paperclover/console/inject";
2import "#debug";
2 3
3const protocol = "http"; 4const protocol = "http";
4 5
framework/bundle.ts+19-1
...@@ -39,6 +39,9 @@ export async function bundleClientJavaScript(...@@ -39,6 +39,9 @@ export async function bundleClientJavaScript(
39 write: false,39 write: false,
40 metafile: true,40 metafile: true,
41 external: ["node_modules/"],41 external: ["node_modules/"],
42 jsx: "automatic",
43 jsxImportSource: "#ssr",
44 jsxDev: dev,
42 });45 });
43 if (bundle.errors.length || bundle.warnings.length) {46 if (bundle.errors.length || bundle.warnings.length) {
44 throw new AggregateError(47 throw new AggregateError(
...@@ -135,11 +138,22 @@ export async function bundleServerJavaScript(...@@ -135,11 +138,22 @@ export async function bundleServerJavaScript(
135 return ({138 return ({
136 loader: "ts",139 loader: "ts",
137 contents: cacheEntry.src,140 contents: cacheEntry.src,
141 resolveDir: path.dirname(file),
138 });142 });
139 },143 },
140 );144 );
141 },145 },
142 },146 },
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 },
143 {157 {
144 name: "mark css external",158 name: "mark css external",
145 setup(b) {159 setup(b) {
...@@ -154,7 +168,7 @@ export async function bundleServerJavaScript(...@@ -154,7 +168,7 @@ export async function bundleServerJavaScript(
154 },168 },
155 },169 },
156 ];170 ];
157 const { metafile, outputFiles, warnings } = await esbuild.build({171 const { metafile, outputFiles } = await esbuild.build({
158 bundle: true,172 bundle: true,
159 chunkNames: "c.[hash]",173 chunkNames: "c.[hash]",
160 entryNames: "server",174 entryNames: "server",
...@@ -169,6 +183,9 @@ export async function bundleServerJavaScript(...@@ -169,6 +183,9 @@ export async function bundleServerJavaScript(
169 splitting: true,183 splitting: true,
170 write: false,184 write: false,
171 metafile: true,185 metafile: true,
186 jsx: "automatic",
187 jsxImportSource: "#ssr",
188 jsxDev: false,
172 });189 });
173190
174 const files: Record<string, Buffer> = {};191 const files: Record<string, Buffer> = {};
...@@ -273,3 +290,4 @@ import {...@@ -273,3 +290,4 @@ import {
273} from "./esbuild-support.ts";290} from "./esbuild-support.ts";
274import { Incremental } from "./incremental.ts";291import { Incremental } from "./incremental.ts";
275import * as css from "./css.ts";292import * 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) =>...@@ -18,3 +18,24 @@ test("simple tree", (t) =>
18 ).text, 18 ).text,
19 '<main class="a b"><h1 style=background-color:red>hello world</h1><p>haha</p>1|0|||||</main>', 19 '<main class="a b"><h1 style=background-color:red>hello world</h1><p>haha</p>1|0|||||</main>',
20 )); 20 ));
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") {...@@ -64,6 +64,11 @@ export function projectRelativeResolution(root = process.cwd() + "/src") {
64 path: path.resolve(root, id.slice(2)), 64 path: path.resolve(root, id.slice(2)),
65 }; 65 };
66 }); 66 });
67 b.onResolve({ filter: /^#/ }, ({ path: id, importer }) => {
68 return {
69 path: hot.resolveFrom(importer, id),
70 };
71 });
67 }, 72 },
68 } satisfies esbuild.Plugin; 73 } satisfies esbuild.Plugin;
69} 74}
...@@ -71,3 +76,4 @@ export function projectRelativeResolution(root = process.cwd() + "/src") {...@@ -71,3 +76,4 @@ export function projectRelativeResolution(root = process.cwd() + "/src") {
71import * as esbuild from "esbuild"; 76import * as esbuild from "esbuild";
72import * as string from "#sitegen/string"; 77import * as string from "#sitegen/string";
73import * as path from "node:path"; 78import * as path from "node:path";
79import * as hot from "./hot.ts";
framework/generate.ts+37-21
...@@ -74,26 +74,40 @@ export async function sitegen(...@@ -74,26 +74,40 @@ export async function sitegen(
74 dir: sectionPath("pages"),74 dir: sectionPath("pages"),
75 list: pages,75 list: pages,
76 prefix: "/",76 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"],
78 },91 },
79 { dir: sectionPath("static"), list: staticFiles, prefix: "/", ext: true },
80 { dir: sectionPath("scripts"), list: scripts, prefix: rootPrefix },
81 {92 {
82 dir: sectionPath("views"),93 dir: sectionPath("views"),
83 list: views,94 list: views,
84 prefix: rootPrefix,95 prefix: rootPrefix,
85 exclude: [".css", ".client.ts", ".client.tsx"],96 include: [".tsx", ".mdx", ".marko"],
97 exclude: [".client.ts", ".client.tsx"],
86 },98 },
87 ];99 ];
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 ) {
89 const items = fs.readDirRecOptionalSync(dir);104 const items = fs.readDirRecOptionalSync(dir);
90 item: for (const subPath of items) {105 for (const subPath of items) {
91 const file = path.join(dir, subPath);106 const file = path.join(dir, subPath);
92 const stat = fs.statSync(file);107 const stat = fs.statSync(file);
93 if (stat.isDirectory()) continue;108 if (stat.isDirectory()) continue;
94 for (const e of exclude) {109 if (!include.some((e) => subPath.endsWith(e))) continue;
95 if (subPath.endsWith(e)) continue item;110 if (exclude.some((e) => subPath.endsWith(e))) continue;
96 }
97 const trim = ext111 const trim = ext
98 ? subPath112 ? subPath
99 : subPath.slice(0, -path.extname(subPath).length).replaceAll(113 : subPath.slice(0, -path.extname(subPath).length).replaceAll(
...@@ -216,31 +230,33 @@ export async function sitegen(...@@ -216,31 +230,33 @@ export async function sitegen(
216 },230 },
217 });231 });
218 }232 }
219 async function prepareView(view: FileItem) {233 async function prepareView(item: FileItem) {
220 const module = require(view.file);234 const module = require(item.file);
221 if (!module.meta) {235 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'`);
223 }237 }
224 if (!module.default) {238 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.`);
226 }240 }
227 const pageTheme = module.layout?.theme ?? module.theme;241 const pageTheme = module.layout?.theme ?? module.theme;
228 const theme: css.Theme = {242 const theme: css.Theme = {
229 ...css.defaultTheme,243 ...css.defaultTheme,
230 ...pageTheme,244 ...pageTheme,
231 };245 };
232 const cssImports = hot.getCssImports(view.file)246 const cssImports = Array.from(
233 .concat("src/global.css")247 new Set([globalCssPath, ...hot.getCssImports(item.file)]),
234 .map((file) => path.relative(hot.projectSrc, path.resolve(file)));248 (file) => path.relative(hot.projectSrc, file),
249 );
250 ensureCssGetsBuilt(cssImports, theme, item.id);
235 incr.put({251 incr.put({
236 kind: "viewMetadata",252 kind: "viewMetadata",
237 key: view.id,253 key: item.id,
238 sources: [view.file],254 sources: [item.file],
239 value: {255 value: {
240 file: path.relative(hot.projectRoot, view.file),256 file: path.relative(hot.projectRoot, item.file),
241 cssImports,257 cssImports,
242 theme,258 theme,
243 clientRefs: hot.getClientScriptRefs(view.file),259 clientRefs: hot.getClientScriptRefs(item.file),
244 hasLayout: !!module.layout?.default,260 hasLayout: !!module.layout?.default,
245 },261 },
246 });262 });
...@@ -422,7 +438,7 @@ function getItemText({ file }: FileItem) {...@@ -422,7 +438,7 @@ function getItemText({ file }: FileItem) {
422 return path.relative(hot.projectSrc, file).replaceAll("\\", "/");438 return path.relative(hot.projectSrc, file).replaceAll("\\", "/");
423}439}
424440
425import { OnceMap, Queue } from "./queue.ts";441import { OnceMap, Queue } from "#sitegen/async";
426import { Incremental } from "./incremental.ts";442import { Incremental } from "./incremental.ts";
427import * as bundle from "./bundle.ts";443import * as bundle from "./bundle.ts";
428import * as css from "./css.ts";444import * as css from "./css.ts";
framework/incremental.ts+2-2
...@@ -148,7 +148,7 @@ export class Incremental {...@@ -148,7 +148,7 @@ export class Incremental {
148 for (const key of map.keys()) {148 for (const key of map.keys()) {
149 if (!this.round.referenced.has(`${kind}\0${key}`)) {149 if (!this.round.referenced.has(`${kind}\0${key}`)) {
150 unreferenced.push({ kind: kind as ArtifactKind, key });150 unreferenced.push({ kind: kind as ArtifactKind, key });
151 console.warn("unreferened " + kind + " : " + key);151 this.out[kind as ArtifactKind].delete(key);
152 }152 }
153 }153 }
154 }154 }
...@@ -652,7 +652,7 @@ const zstd = util.promisify(zlib.zstdCompress);...@@ -652,7 +652,7 @@ const zstd = util.promisify(zlib.zstdCompress);
652import * as fs from "#sitegen/fs";652import * as fs from "#sitegen/fs";
653import * as zlib from "node:zlib";653import * as zlib from "node:zlib";
654import * as util from "node:util";654import * as util from "node:util";
655import { Queue } from "./queue.ts";655import { Queue } from "#sitegen/async";
656import * as hot from "./hot.ts";656import * as hot from "./hot.ts";
657import * as mime from "#sitegen/mime";657import * as mime from "#sitegen/mime";
658import * as path from "node:path";658import * 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!;...@@ -16,7 +16,14 @@ let scripts: Record<string, string> = null!;
16// boundaries, but those were never used. Pages will wait until they 16// boundaries, but those were never used. Pages will wait until they
17// are fully rendered before sending. 17// are fully rendered before sending.
18export async function renderView( 18export 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(
20 id: string, 27 id: string,
21 props: Record<string, unknown>, 28 props: Record<string, unknown>,
22) { 29) {
...@@ -29,11 +36,11 @@ export async function renderView(...@@ -29,11 +36,11 @@ export async function renderView(
29 inlineCss, 36 inlineCss,
30 layout, 37 layout,
31 meta: metadata, 38 meta: metadata,
32 }: View = views[id]; 39 }: View = UNWRAP(views[id], `Missing view ${id}`);
33 40
34 // -- metadata -- 41 // -- metadata --
35 const renderedMetaPromise = Promise.resolve( 42 const renderedMetaPromise = Promise.resolve(
36 typeof metadata === "function" ? metadata({ context: c }) : metadata, 43 typeof metadata === "function" ? metadata(props) : metadata,
37 ).then((m) => meta.renderMeta(m)); 44 ).then((m) => meta.renderMeta(m));
38 45
39 // -- html -- 46 // -- html --
...@@ -44,14 +51,17 @@ export async function renderView(...@@ -44,14 +51,17 @@ export async function renderView(
44 }); 51 });
45 52
46 // -- join document and send -- 53 // -- join document and send --
47 return c.html(wrapDocument({ 54 return wrapDocument({
48 body, 55 body,
49 head: await renderedMetaPromise, 56 head: await renderedMetaPromise,
50 inlineCss, 57 inlineCss,
51 scripts: joinScripts( 58 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 ),
53 ), 63 ),
54 })); 64 });
55} 65}
56 66
57export function provideViewData(v: typeof views, s: typeof scripts) { 67export function provideViewData(v: typeof views, s: typeof scripts) {
...@@ -87,5 +97,4 @@ export function wrapDocument({...@@ -87,5 +97,4 @@ export function wrapDocument({
87import * as meta from "./meta.ts"; 97import * as meta from "./meta.ts";
88import type * as hono from "#hono"; 98import type * as hono from "#hono";
89import * as engine from "../engine/ssr.ts"; 99import * as engine from "../engine/ssr.ts";
90import type * as css from "../css.ts";
91import * as sg from "./sitegen.ts"; 100import * 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() {...@@ -11,7 +11,7 @@ export async function main() {
11 const watch = new Watch(rebuild); 11 const watch = new Watch(rebuild);
12 watch.add(...incr.invals.keys()); 12 watch.add(...incr.invals.keys());
13 statusLine(); 13 statusLine();
14 // ... an 14 // ... and then serve it!
15 serve(); 15 serve();
16 16
17 function serve() { 17 function serve() {
...@@ -41,7 +41,13 @@ export async function main() {...@@ -41,7 +41,13 @@ export async function main() {
41 files = files.map((file) => path.relative(hot.projectRoot, file)); 41 files = files.map((file) => path.relative(hot.projectRoot, file));
42 const changed: string[] = []; 42 const changed: string[] = [];
43 for (const file of files) { 43 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);
45 } 51 }
46 if (changed.length === 0) { 52 if (changed.length === 0) {
47 console.warn("Files were modified but the 'modify' time did not change."); 53 console.warn("Files were modified but the 'modify' time did not change.");
package-lock.json+1055-10
...@@ -12,6 +12,7 @@...@@ -12,6 +12,7 @@
12 "hls.js": "^1.6.5",12 "hls.js": "^1.6.5",
13 "hono": "^4.7.11",13 "hono": "^4.7.11",
14 "marko": "^6.0.20",14 "marko": "^6.0.20",
15 "puppeteer": "^24.10.1",
15 "unique-names-generator": "^4.7.1"16 "unique-names-generator": "^4.7.1"
16 },17 },
17 "devDependencies": {18 "devDependencies": {
...@@ -950,6 +951,45 @@...@@ -950,6 +951,45 @@
950 "strip-ansi": "^7.1.0"951 "strip-ansi": "^7.1.0"
951 }952 }
952 },953 },
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 },
953 "node_modules/@types/debug": {993 "node_modules/@types/debug": {
954 "version": "4.1.12",994 "version": "4.1.12",
955 "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",995 "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
...@@ -1008,7 +1048,7 @@...@@ -1008,7 +1048,7 @@
1008 "version": "22.15.30",1048 "version": "22.15.30",
1009 "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",1049 "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
1010 "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",1050 "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
1011 "dev": true,1051 "devOptional": true,
1012 "license": "MIT",1052 "license": "MIT",
1013 "dependencies": {1053 "dependencies": {
1014 "undici-types": "~6.21.0"1054 "undici-types": "~6.21.0"
...@@ -1020,6 +1060,16 @@...@@ -1020,6 +1060,16 @@
1020 "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",1060 "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1021 "license": "MIT"1061 "license": "MIT"
1022 },1062 },
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 },
1023 "node_modules/@ungap/structured-clone": {1073 "node_modules/@ungap/structured-clone": {
1024 "version": "1.3.0",1074 "version": "1.3.0",
1025 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",1075 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
...@@ -1047,6 +1097,15 @@...@@ -1047,6 +1097,15 @@
1047 "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"1097 "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1048 }1098 }
1049 },1099 },
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 },
1050 "node_modules/ansi-escapes": {1109 "node_modules/ansi-escapes": {
1051 "version": "7.0.0",1110 "version": "7.0.0",
1052 "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",1111 "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
...@@ -1074,6 +1133,39 @@...@@ -1074,6 +1133,39 @@
1074 "url": "https://github.com/chalk/ansi-regex?sponsor=1"1133 "url": "https://github.com/chalk/ansi-regex?sponsor=1"
1075 }1134 }
1076 },1135 },
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 },
1077 "node_modules/astring": {1169 "node_modules/astring": {
1078 "version": "1.9.0",1170 "version": "1.9.0",
1079 "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",1171 "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
...@@ -1083,6 +1175,12 @@...@@ -1083,6 +1175,12 @@
1083 "astring": "bin/astring"1175 "astring": "bin/astring"
1084 }1176 }
1085 },1177 },
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 },
1086 "node_modules/bail": {1184 "node_modules/bail": {
1087 "version": "2.0.2",1185 "version": "2.0.2",
1088 "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",1186 "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
...@@ -1093,6 +1191,87 @@...@@ -1093,6 +1191,87 @@
1093 "url": "https://github.com/sponsors/wooorm"1191 "url": "https://github.com/sponsors/wooorm"
1094 }1192 }
1095 },1193 },
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 },
1096 "node_modules/browserslist": {1275 "node_modules/browserslist": {
1097 "version": "4.25.0",1276 "version": "4.25.0",
1098 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",1277 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
...@@ -1125,12 +1304,30 @@...@@ -1125,12 +1304,30 @@
1125 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"1304 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1126 }1305 }
1127 },1306 },
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 },
1128 "node_modules/buffer-from": {1316 "node_modules/buffer-from": {
1129 "version": "1.1.2",1317 "version": "1.1.2",
1130 "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",1318 "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
1131 "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",1319 "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
1132 "license": "MIT"1320 "license": "MIT"
1133 },1321 },
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 },
1134 "node_modules/caniuse-lite": {1331 "node_modules/caniuse-lite": {
1135 "version": "1.0.30001721",1332 "version": "1.0.30001721",
1136 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",1333 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
...@@ -1213,6 +1410,54 @@...@@ -1213,6 +1410,54 @@
1213 "url": "https://github.com/sponsors/wooorm"1410 "url": "https://github.com/sponsors/wooorm"
1214 }1411 }
1215 },1412 },
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 },
1216 "node_modules/collapse-white-space": {1461 "node_modules/collapse-white-space": {
1217 "version": "2.1.0",1462 "version": "2.1.0",
1218 "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",1463 "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
...@@ -1223,6 +1468,24 @@...@@ -1223,6 +1468,24 @@
1223 "url": "https://github.com/sponsors/wooorm"1468 "url": "https://github.com/sponsors/wooorm"
1224 }1469 }
1225 },1470 },
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 },
1226 "node_modules/comma-separated-tokens": {1489 "node_modules/comma-separated-tokens": {
1227 "version": "2.0.3",1490 "version": "2.0.3",
1228 "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",1491 "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
...@@ -1248,12 +1511,47 @@...@@ -1248,12 +1511,47 @@
1248 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",1511 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1249 "license": "MIT"1512 "license": "MIT"
1250 },1513 },
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 },
1251 "node_modules/csstype": {1540 "node_modules/csstype": {
1252 "version": "3.1.3",1541 "version": "3.1.3",
1253 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",1542 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
1254 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",1543 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
1255 "license": "MIT"1544 "license": "MIT"
1256 },1545 },
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 },
1257 "node_modules/debug": {1555 "node_modules/debug": {
1258 "version": "4.4.1",1556 "version": "4.4.1",
1259 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",1557 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
...@@ -1284,6 +1582,20 @@...@@ -1284,6 +1582,20 @@
1284 "url": "https://github.com/sponsors/wooorm"1582 "url": "https://github.com/sponsors/wooorm"
1285 }1583 }
1286 },1584 },
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 },
1287 "node_modules/dequal": {1599 "node_modules/dequal": {
1288 "version": "2.0.3",1600 "version": "2.0.3",
1289 "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",1601 "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
...@@ -1306,12 +1618,42 @@...@@ -1306,12 +1618,42 @@
1306 "url": "https://github.com/sponsors/wooorm"1618 "url": "https://github.com/sponsors/wooorm"
1307 }1619 }
1308 },1620 },
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 },
1309 "node_modules/electron-to-chromium": {1627 "node_modules/electron-to-chromium": {
1310 "version": "1.5.165",1628 "version": "1.5.165",
1311 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",1629 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
1312 "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==",1630 "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==",
1313 "license": "ISC"1631 "license": "ISC"
1314 },1632 },
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 },
1315 "node_modules/environment": {1657 "node_modules/environment": {
1316 "version": "1.1.0",1658 "version": "1.1.0",
1317 "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",1659 "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
...@@ -1324,6 +1666,15 @@...@@ -1324,6 +1666,15 @@
1324 "url": "https://github.com/sponsors/sindresorhus"1666 "url": "https://github.com/sponsors/sindresorhus"
1325 }1667 }
1326 },1668 },
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 },
1327 "node_modules/error-stack-parser": {1678 "node_modules/error-stack-parser": {
1328 "version": "2.1.4",1679 "version": "2.1.4",
1329 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",1680 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
...@@ -1414,6 +1765,59 @@...@@ -1414,6 +1765,59 @@
1414 "node": ">=6"1765 "node": ">=6"
1415 }1766 }
1416 },1767 },
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 },
1417 "node_modules/estree-util-attach-comments": {1821 "node_modules/estree-util-attach-comments": {
1418 "version": "3.0.0",1822 "version": "3.0.0",
1419 "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz",1823 "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz",
...@@ -1505,12 +1909,56 @@...@@ -1505,12 +1909,56 @@
1505 "@types/estree": "^1.0.0"1909 "@types/estree": "^1.0.0"
1506 }1910 }
1507 },1911 },
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 },
1508 "node_modules/extend": {1921 "node_modules/extend": {
1509 "version": "3.0.2",1922 "version": "3.0.2",
1510 "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",1923 "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
1511 "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",1924 "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
1512 "license": "MIT"1925 "license": "MIT"
1513 },1926 },
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 },
1514 "node_modules/gensync": {1962 "node_modules/gensync": {
1515 "version": "1.0.0-beta.2",1963 "version": "1.0.0-beta.2",
1516 "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",1964 "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
...@@ -1520,6 +1968,44 @@...@@ -1520,6 +1968,44 @@
1520 "node": ">=6.9.0"1968 "node": ">=6.9.0"
1521 }1969 }
1522 },1970 },
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 },
1523 "node_modules/globals": {2009 "node_modules/globals": {
1524 "version": "11.12.0",2010 "version": "11.12.0",
1525 "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",2011 "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
...@@ -1618,21 +2104,85 @@...@@ -1618,21 +2104,85 @@
1618 "integrity": "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ==",2104 "integrity": "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ==",
1619 "license": "MIT",2105 "license": "MIT",
1620 "engines": {2106 "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"
1622 }2165 }
1623 },2166 },
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 },
1630 "node_modules/inline-style-parser": {2167 "node_modules/inline-style-parser": {
1631 "version": "0.2.4",2168 "version": "0.2.4",
1632 "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",2169 "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
1633 "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",2170 "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
1634 "license": "MIT"2171 "license": "MIT"
1635 },2172 },
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 },
1636 "node_modules/is-alphabetical": {2186 "node_modules/is-alphabetical": {
1637 "version": "2.0.1",2187 "version": "2.0.1",
1638 "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",2188 "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
...@@ -1657,6 +2207,12 @@...@@ -1657,6 +2207,12 @@
1657 "url": "https://github.com/sponsors/wooorm"2207 "url": "https://github.com/sponsors/wooorm"
1658 }2208 }
1659 },2209 },
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 },
1660 "node_modules/is-decimal": {2216 "node_modules/is-decimal": {
1661 "version": "2.0.1",2217 "version": "2.0.1",
1662 "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",2218 "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
...@@ -1667,6 +2223,15 @@...@@ -1667,6 +2223,15 @@
1667 "url": "https://github.com/sponsors/wooorm"2223 "url": "https://github.com/sponsors/wooorm"
1668 }2224 }
1669 },2225 },
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 },
1670 "node_modules/is-hexadecimal": {2235 "node_modules/is-hexadecimal": {
1671 "version": "2.0.1",2236 "version": "2.0.1",
1672 "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",2237 "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
...@@ -1695,6 +2260,24 @@...@@ -1695,6 +2260,24 @@
1695 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",2260 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1696 "license": "MIT"2261 "license": "MIT"
1697 },2262 },
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 },
1698 "node_modules/jsesc": {2281 "node_modules/jsesc": {
1699 "version": "3.1.0",2282 "version": "3.1.0",
1700 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",2283 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
...@@ -1707,6 +2290,12 @@...@@ -1707,6 +2290,12 @@
1707 "node": ">=6"2290 "node": ">=6"
1708 }2291 }
1709 },2292 },
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 },
1710 "node_modules/json5": {2299 "node_modules/json5": {
1711 "version": "2.2.3",2300 "version": "2.2.3",
1712 "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",2301 "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
...@@ -1746,6 +2335,12 @@...@@ -1746,6 +2335,12 @@
1746 "lasso-caching-fs": "^1.0.0"2335 "lasso-caching-fs": "^1.0.0"
1747 }2336 }
1748 },2337 },
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 },
1749 "node_modules/longest-streak": {2344 "node_modules/longest-streak": {
1750 "version": "3.1.0",2345 "version": "3.1.0",
1751 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",2346 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
...@@ -2566,18 +3161,86 @@...@@ -2566,18 +3161,86 @@
2566 ],3161 ],
2567 "license": "MIT"3162 "license": "MIT"
2568 },3163 },
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 },
2569 "node_modules/ms": {3170 "node_modules/ms": {
2570 "version": "2.1.3",3171 "version": "2.1.3",
2571 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",3172 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2572 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",3173 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2573 "license": "MIT"3174 "license": "MIT"
2574 },3175 },
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 },
2575 "node_modules/node-releases": {3185 "node_modules/node-releases": {
2576 "version": "2.0.19",3186 "version": "2.0.19",
2577 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",3187 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
2578 "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",3188 "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
2579 "license": "MIT"3189 "license": "MIT"
2580 },3190 },
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 },
2581 "node_modules/parse-entities": {3244 "node_modules/parse-entities": {
2582 "version": "4.0.2",3245 "version": "4.0.2",
2583 "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",3246 "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
...@@ -2603,12 +3266,45 @@...@@ -2603,12 +3266,45 @@
2603 "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",3266 "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
2604 "license": "MIT"3267 "license": "MIT"
2605 },3268 },
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 },
2606 "node_modules/picocolors": {3293 "node_modules/picocolors": {
2607 "version": "1.1.1",3294 "version": "1.1.1",
2608 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",3295 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2609 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",3296 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2610 "license": "ISC"3297 "license": "ISC"
2611 },3298 },
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 },
2612 "node_modules/property-information": {3308 "node_modules/property-information": {
2613 "version": "7.1.0",3309 "version": "7.1.0",
2614 "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",3310 "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
...@@ -2619,6 +3315,88 @@...@@ -2619,6 +3315,88 @@
2619 "url": "https://github.com/sponsors/wooorm"3315 "url": "https://github.com/sponsors/wooorm"
2620 }3316 }
2621 },3317 },
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 },
2622 "node_modules/raptor-async": {3400 "node_modules/raptor-async": {
2623 "version": "1.1.3",3401 "version": "1.1.3",
2624 "resolved": "https://registry.npmjs.org/raptor-async/-/raptor-async-1.1.3.tgz",3402 "resolved": "https://registry.npmjs.org/raptor-async/-/raptor-async-1.1.3.tgz",
...@@ -2769,6 +3547,15 @@...@@ -2769,6 +3547,15 @@
2769 "url": "https://opencollective.com/unified"3547 "url": "https://opencollective.com/unified"
2770 }3548 }
2771 },3549 },
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 },
2772 "node_modules/resolve-from": {3559 "node_modules/resolve-from": {
2773 "version": "5.0.0",3560 "version": "5.0.0",
2774 "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",3561 "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
...@@ -2796,6 +3583,44 @@...@@ -2796,6 +3583,44 @@
2796 "semver": "bin/semver.js"3583 "semver": "bin/semver.js"
2797 }3584 }
2798 },3585 },
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 },
2799 "node_modules/source-map": {3624 "node_modules/source-map": {
2800 "version": "0.7.4",3625 "version": "0.7.4",
2801 "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",3626 "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
...@@ -2834,12 +3659,66 @@...@@ -2834,12 +3659,66 @@
2834 "url": "https://github.com/sponsors/wooorm"3659 "url": "https://github.com/sponsors/wooorm"
2835 }3660 }
2836 },3661 },
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 },
2837 "node_modules/stackframe": {3668 "node_modules/stackframe": {
2838 "version": "1.3.4",3669 "version": "1.3.4",
2839 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",3670 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
2840 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",3671 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
2841 "license": "MIT"3672 "license": "MIT"
2842 },3673 },
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 },
2843 "node_modules/stringify-entities": {3722 "node_modules/stringify-entities": {
2844 "version": "4.0.4",3723 "version": "4.0.4",
2845 "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",3724 "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
...@@ -2887,6 +3766,40 @@...@@ -2887,6 +3766,40 @@
2887 "inline-style-parser": "0.2.4"3766 "inline-style-parser": "0.2.4"
2888 }3767 }
2889 },3768 },
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 },
2890 "node_modules/trim-lines": {3803 "node_modules/trim-lines": {
2891 "version": "3.0.1",3804 "version": "3.0.1",
2892 "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",3805 "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
...@@ -2907,11 +3820,23 @@...@@ -2907,11 +3820,23 @@
2907 "url": "https://github.com/sponsors/wooorm"3820 "url": "https://github.com/sponsors/wooorm"
2908 }3821 }
2909 },3822 },
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 },
2910 "node_modules/typescript": {3835 "node_modules/typescript": {
2911 "version": "5.8.3",3836 "version": "5.8.3",
2912 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",3837 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
2913 "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",3838 "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
2914 "dev": true,3839 "devOptional": true,
2915 "license": "Apache-2.0",3840 "license": "Apache-2.0",
2916 "bin": {3841 "bin": {
2917 "tsc": "bin/tsc",3842 "tsc": "bin/tsc",
...@@ -2925,7 +3850,7 @@...@@ -2925,7 +3850,7 @@
2925 "version": "6.21.0",3850 "version": "6.21.0",
2926 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",3851 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2927 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",3852 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2928 "dev": true,3853 "devOptional": true,
2929 "license": "MIT"3854 "license": "MIT"
2930 },3855 },
2931 "node_modules/unified": {3856 "node_modules/unified": {
...@@ -3095,12 +4020,132 @@...@@ -3095,12 +4020,132 @@
3095 "url": "https://opencollective.com/unified"4020 "url": "https://opencollective.com/unified"
3096 }4021 }
3097 },4022 },
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 },
3098 "node_modules/yallist": {4097 "node_modules/yallist": {
3099 "version": "3.1.1",4098 "version": "3.1.1",
3100 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",4099 "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
3101 "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",4100 "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
3102 "license": "ISC"4101 "license": "ISC"
3103 },4102 },
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 },
3104 "node_modules/zwitch": {4149 "node_modules/zwitch": {
3105 "version": "2.0.4",4150 "version": "2.0.4",
3106 "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",4151 "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
package.json+2
...@@ -8,6 +8,7 @@...@@ -8,6 +8,7 @@
8 "hls.js": "^1.6.5",8 "hls.js": "^1.6.5",
9 "hono": "^4.7.11",9 "hono": "^4.7.11",
10 "marko": "^6.0.20",10 "marko": "^6.0.20",
11 "puppeteer": "^24.10.1",
11 "unique-names-generator": "^4.7.1"12 "unique-names-generator": "^4.7.1"
12 },13 },
13 "devDependencies": {14 "devDependencies": {
...@@ -16,6 +17,7 @@...@@ -16,6 +17,7 @@
16 },17 },
17 "imports": {18 "imports": {
18 "#backend": "./src/backend.ts",19 "#backend": "./src/backend.ts",
20 "#debug": "./framework/debug.safe.ts",
19 "#sitegen": "./framework/lib/sitegen.ts",21 "#sitegen": "./framework/lib/sitegen.ts",
20 "#sitegen/*": "./framework/lib/*.ts",22 "#sitegen/*": "./framework/lib/*.ts",
21 "#ssr": "./framework/engine/ssr.ts",23 "#ssr": "./framework/engine/ssr.ts",
readme.md+13-10
...@@ -3,26 +3,29 @@...@@ -3,26 +3,29 @@
3this repository contains clover's "sitegen" framework, which is a set of tools3this repository contains clover's "sitegen" framework, which is a set of tools
4that assist building websites. these tools power https://paperclover.net.4that assist building websites. these tools power https://paperclover.net.
55
6- HTML "Server Side Rendering") engine written from scratch.6- **HTML "Server Side Rendering") engine written from scratch.** (~500 lines)
7 - A more practical JSX runtime (`class` instead of `className`, etc).7 - A more practical JSX runtime (`class` instead of `className`, built-in
8 - Transparent integration with [Marko][1] to mix component types.8 `clsx`, `html()` helper over `dangerouslySetInnerHTML` prop, etc).
9 - MDX support for text-heavy content pages.9 - Integration with [Marko][1] for concisely written components.
10- Incremental static site generator and build system10 - 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.**
11 - Build entire production site at start, incremental updates when pages14 - Build entire production site at start, incremental updates when pages
12 change; Build system state survives coding sessions.15 change; Build system state survives coding sessions.
13 - The only difference in development and production mode is hidden16 - The only difference in development and production mode is hidden
14 source-maps and stripped assertions and `console.debug` calls. The site17 source-maps and stripped `console.debug` calls. The site you
15 you see locally is the site you see deployed.18 see locally is the same site you see deployed.
16 - (TODO) Tests, Lints, and Type-checking is run alongside, and only re-runs19 - (TODO) Tests, Lints, and Type-checking is run alongside, and only re-runs
17 checks when the files change. For example, changing a component re-tests20 checks when the files change. For example, changing a component re-tests
18 only pages that use that component and re-lints only the changed file.21 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.**
20 - Static asset serving with ETag and build-time compression.23 - Static asset serving with ETag and build-time compression.
21 - Dynamicly rendered pages with static client. (`#import "#sitegen/view"`)24 - Dynamicly rendered pages with static client. (`#import "#sitegen/view"`)
22 - Databases with a typed SQLite wrapper. (`import "#sitegen/sqlite"`)25 - Databases with a typed SQLite wrapper. (`import "#sitegen/sqlite"`)
23 - TODO: Meta and Open Graph generation. (`export const meta`)26 - TODO: Meta and Open Graph generation. (`export const meta`)
24 - TODO: Font subsetting tools to reduce 27 - TODO: Font subsetting tools to reduce bytes downloaded by fonts.
25- Built on the battle-tested Node.js runtime. Partial support for Deno and Bun.28- **Built on the battle-tested Node.js runtime.** Partial support for Deno and Bun.
2629
27[1]: https://next.markojs.com30[1]: https://next.markojs.com
2831
run.js+13-26
...@@ -4,18 +4,17 @@ import * as util from "node:util";...@@ -4,18 +4,17 @@ import * as util from "node:util";
4import process from "node:process";4import process from "node:process";
55
6// Disable experimental warnings (Type Stripping, etc)6// Disable experimental warnings (Type Stripping, etc)
7{7const { emit: originalEmit } = process;
8 const { emit: originalEmit } = process;8const warnings = ["ExperimentalWarning"];
9 const warnings = ["ExperimentalWarning"];9process.emit = function (event, error) {
10 process.emit = function (event, error) {10 return event === "warning" && warnings.includes(error.name)
11 return event === "warning" && warnings.includes(error.name)11 ? false
12 ? false12 : originalEmit.apply(process, arguments);
13 : originalEmit.apply(process, arguments);13};
14 };
15}
1614
17// Init hooks15// Init hooks
18const hot = await import("./framework/hot.ts");16const hot = await import("./framework/hot.ts");
17await import("#debug");
1918
20const console = hot.load("@paperclover/console");19const console = hot.load("@paperclover/console");
21globalThis.console["log"] = console.info;20globalThis.console["log"] = console.info;
...@@ -24,22 +23,6 @@ globalThis.console.warn = console.warn;...@@ -24,22 +23,6 @@ globalThis.console.warn = console.warn;
24globalThis.console.error = console.error;23globalThis.console.error = console.error;
25globalThis.console.debug = console.scoped("dbg");24globalThis.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
43// Load with hooks26// Load with hooks
44if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) {27if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) {
45 if (process.argv.length == 2) {28 if (process.argv.length == 2) {
...@@ -60,7 +43,11 @@ if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) {...@@ -60,7 +43,11 @@ if (process.argv[1].startsWith(import.meta.filename.slice(0, -".js".length))) {
60 process.exit(1);43 process.exit(1);
61 }44 }
62 process.argv = [process.argv[0], ...process.argv.slice(2)];45 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 }
64}51}
6552
66export { hot };53export { hot };
src/admin.ts+12-1
...@@ -59,6 +59,17 @@ export function hasAdminToken(c: Context) {...@@ -59,6 +59,17 @@ export function hasAdminToken(c: Context) {
59 return token && compareToken(token); 59 return token && compareToken(token);
60} 60}
61 61
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";
63import type { Context, Next } from "hono"; 73import type { Context, Next } from "hono";
64import { serveAsset } from "#sitegen/assets"; 74import { 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);...@@ -15,6 +15,17 @@ app.route("", require("./q+a/backend.ts").app);
15 15
16app.use(assets.middleware); 16app.use(assets.middleware);
17 17
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
18export default app; 29export default app;
19 30
20async function removeDuplicateSlashes(c: Context, next: Next) { 31async function removeDuplicateSlashes(c: Context, next: Next) {
...@@ -31,8 +42,10 @@ async function removeDuplicateSlashes(c: Context, next: Next) {...@@ -31,8 +42,10 @@ async function removeDuplicateSlashes(c: Context, next: Next) {
31} 42}
32 43
33import { type Context, Hono, type Next } from "#hono"; 44import { type Context, Hono, type Next } from "#hono";
45import { HTTPException } from "hono/http-exception";
34import { logger } from "hono/logger"; 46import { logger } from "hono/logger";
35import { trimTrailingSlash } from "hono/trailing-slash"; 47import { trimTrailingSlash } from "hono/trailing-slash";
36import * as assets from "#sitegen/assets"; 48import * as assets from "#sitegen/assets";
37import * as admin from "./admin.ts"; 49import * as admin from "./admin.ts";
38import { scoped } from "@paperclover/console"; 50import { 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) {...@@ -104,12 +104,12 @@ async function sendSuccess(c: Context, date: Date) {
104 id: formatQuestionId(date), 104 id: formatQuestionId(date),
105 }, { status: 200 }); 105 }, { status: 200 });
106 } 106 }
107 c.res = await renderView(c, "qa_success", { 107 c.res = await renderView(c, "q+a/success", {
108 permalink: `https://paperclover.net/q+a/${formatQuestionId(date)}`, 108 permalink: `https://paperclover.net/q+a/${formatQuestionId(date)}`,
109 }); 109 });
110} 110}
111// Question Permalink 111// Question Permalink
112app.get("/q+a/:id", (c, next) => { 112app.get("/q+a/:id", async (c, next) => {
113 // from deadname era, the seconds used to be in the url. 113 // from deadname era, the seconds used to be in the url.
114 // this was removed so that the url can be crafted by hand. 114 // this was removed so that the url can be crafted by hand.
115 let id = c.req.param("id"); 115 let id = c.req.param("id");
...@@ -138,7 +138,7 @@ app.get("/admin/q+a", async (c) => {...@@ -138,7 +138,7 @@ app.get("/admin/q+a", async (c) => {
138 return serveAsset(c, "/admin/q+a", 200); 138 return serveAsset(c, "/admin/q+a", 200);
139}); 139});
140app.get("/admin/q+a/inbox", async (c) => { 140app.get("/admin/q+a/inbox", async (c) => {
141 return renderView(c, "qa_backend_inbox", {}); 141 return renderView(c, "q+a/backend-inbox", {});
142}); 142});
143app.delete("/admin/q+a/:id", async (c, next) => { 143app.delete("/admin/q+a/:id", async (c, next) => {
144 const id = c.req.param("id"); 144 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 = {...@@ -12,15 +12,13 @@ export const meta: Metadata = {
12<const/questions = [...Question.getAll()] />12<const/questions = [...Question.getAll()] />
1313
14<if=!admin>14<if=!admin>
15 <QuestionForm />15 <question-form />
16</>16</>
17<for|q| of=questions>17<for|question| of=questions>
18 <QuestionRender question=q admin=admin />18 <question ...{ question, admin } />
19</>19</>
20<footer>20<footer>
21 fun fact: clover has answered ${questions.length} questions21 fun fact: clover has answered ${questions.length} questions
22</footer>22</footer>
2323
24import { Question } from "@/q+a/models/Question.ts";24import { 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 @@...@@ -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 @@...@@ -3,17 +3,16 @@
3// sub-projects like the file viewer in 'file', or the question answer system3// sub-projects like the file viewer in 'file', or the question answer system
4// in 'q+a'. Each section can define configuration, pages, backend routes, and4// in 'q+a'. Each section can define configuration, pages, backend routes, and
5// contain other files.5// contain other files.
6interface Section {
7 root: string;
8}
9
10const join = (...paths: string[]) => path.join(import.meta.dirname, ...paths);6const join = (...paths: string[]) => path.join(import.meta.dirname, ...paths);
117
12export const siteSections: Section[] = [8export const siteSections: Section[] = [
13 { root: join("./") },9 { root: join(".") },
14 { root: join("q+a/") },10 { root: join("q+a/") },
15 { root: join("file-viewer/") },11 { root: join("file-viewer/") },
16 { root: join("friends/") },12 { root: join("friends/") },
13 // { root: join("blog/"), pageBase: "/blog" },
14 // { root: join("fiction/"), pageBase: "/fiction" },
17];15];
1816
19import * as path from "node:path";17import * 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 @@...@@ -15,5 +15,6 @@
15 "strict": true,15 "strict": true,
16 "verbaitimModuleSyntax": true,16 "verbaitimModuleSyntax": true,
17 "target": "es2022"17 "target": "es2022"
18 }18 },
19 "include": ["framework/**/*", "src/**/*"]
19}20}