authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-09-29 23:21:26-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:47-07:00
log9365f5f867b8b7f4c55d03ee1dec7151f3b987ce
treea2547a777c3f2b8251f39c7d8ee622762cfff51f
parent27b17df57b1d672f6fee971407b0e4d9916bbf11
signature Commit is signed but in an unrecognized format.

feat(file-viewer): finish scan3, cache layer, and other

feat(paperclover.net): in the summer and waterfalls work feat(lib/log): rework Scope feat(lib/log/stack): do not highlight node_modules in red fix(lib/subprocess): fix spawnAndWait fix(lib/render): lower style object to string chore(lib/render/marko-runtime): upgrade chore: remove `@clo/console` chore: pin all dependencies chore: delete markdown renderer experiment chore: move framework/lib/string to lib/

49 files changed, 1684 insertions(+), 1773 deletions(-)

framework/backend/entry-node.ts-1
...@@ -1,4 +1,3 @@...@@ -1,4 +1,3 @@
1import "@paperclover/console/inject";
2import "#debug";1import "#debug";
32
4const app = require(globalThis.CLOVER_SERVER_ENTRY ?? process.argv[2]);3const app = require(globalThis.CLOVER_SERVER_ENTRY ?? process.argv[2]);
framework/backend/entry-passthru.ts-1
...@@ -1,4 +1,3 @@...@@ -1,4 +1,3 @@
1import "@paperclover/console/inject";
2export default app;1export default app;
32
4import app from "#backend";3import app from "#backend";
framework/debug.safe.ts+6
...@@ -1,3 +1,9 @@...@@ -1,3 +1,9 @@
1process.on("uncaughtException", (error) => {
2 log.error(error);
3 process.exit(1);
4});
5import * as log from "lib/log.ts";
6
1globalThis.UNWRAP = (t, ...args) => {7globalThis.UNWRAP = (t, ...args) => {
2 if (t == null) {8 if (t == null) {
3 throw new Error(9 throw new Error(
framework/engine/marko-runtime.ts+31-30
...@@ -7,11 +7,11 @@ interface BodyContentObject {...@@ -7,11 +7,11 @@ interface BodyContentObject {
7 content: ServerRenderer;7 content: ServerRenderer;
8}8}
99
10export const createTemplate = (10export const _template: typeof marko._template = (
11 templateId: string,11 templateId,
12 renderer: ServerRenderer,12 renderer,
13) => {13) => {
14 const { render: renderFn } = marko.createTemplate(templateId, renderer);14 const { render: renderFn } = marko._template(templateId, renderer);
15 function wrap(props: Record<string, unknown>, n: number) {15 function wrap(props: Record<string, unknown>, n: number) {
16 // Marko Custom Tags16 // Marko Custom Tags
17 const cloverAsyncMarker = { isAsync: false };17 const cloverAsyncMarker = { isAsync: false };
...@@ -27,21 +27,22 @@ export const createTemplate = (...@@ -27,21 +27,22 @@ export const createTemplate = (
27 return markoResult.then(render.raw);27 return markoResult.then(render.raw);
28 }28 }
29 const rr = markoResult.toString();29 const rr = markoResult.toString();
30 console.log({ rr });
30 return render.raw(rr);31 return render.raw(rr);
31 }32 }
32 wrap.render = render;33 wrap.render = renderFn;
33 wrap.unwrapped = renderer;34 wrap.unwrapped = renderer;
34 return wrap;35 return wrap as unknown as Template;
35};36};
3637
37export const dynamicTag = (38export const _dynamic_tag: typeof marko._dynamic_tag = (
38 scopeId: number,39 scopeId,
39 accessor: Accessor,40 accessor,
40 tag: unknown | string | ServerRenderer | BodyContentObject,41 tag,
41 inputOrArgs: unknown,42 inputOrArgs,
42 content?: (() => void) | 0,43 content,
43 inputIsArgs?: 1,44 inputIsArgs,
44 serializeReason?: 1 | 0,45 serializeReason,
45) => {46) => {
46 if (typeof tag === "function") {47 if (typeof tag === "function") {
47 clover: {48 clover: {
...@@ -73,20 +74,20 @@ export const dynamicTag = (...@@ -73,20 +74,20 @@ export const dynamicTag = (
73 (r.rejections ??= []).push(...rejections);74 (r.rejections ??= []).push(...rejections);
74 return reject(new Error("Render had errors"));75 return reject(new Error("Render had errors"));
75 };76 };
76 marko.fork(77 marko._await(
77 scopeId,78 scopeId,
78 accessor,79 accessor,
79 promise,80 promise,
80 (string: string) => marko.write(string),81 (string: string) => marko._html(string),
81 0,82 0,
82 );83 );
83 } else {84 } else {
84 marko.write(render.stringifyNode(resolved));85 marko._html(render.stringifyNode(resolved));
85 }86 }
86 return;87 return;
87 }88 }
88 }89 }
89 return marko.dynamicTag(90 return marko._dynamic_tag(
90 scopeId,91 scopeId,
91 accessor,92 accessor,
92 tag,93 tag,
...@@ -97,19 +98,19 @@ export const dynamicTag = (...@@ -97,19 +98,19 @@ export const dynamicTag = (
97 );98 );
98};99};
99100
100export function fork(101export const _await: typeof marko._await = (
101 scopeId: number,102 scopeId,
102 accessor: Accessor,103 accessor,
103 promise: Promise<unknown>,104 promise,
104 callback: (data: unknown) => void,105 callback,
105 serializeMarker?: 0 | 1,106 serializeMarker,
106) {107) => {
107 const marker = marko.$global().cloverAsyncMarker as Async;108 const marker = marko.$global().cloverAsyncMarker as Async;
108 marker.isAsync = true;109 marker.isAsync = true;
109 marko.fork(scopeId, accessor, promise, callback, serializeMarker);110 marko._await(scopeId, accessor, promise, callback, serializeMarker);
110}111};
111112
112export function escapeXML(input: unknown) {113export function _escape(input: unknown) {
113 // The rationale of this check is that the default toString method114 // The rationale of this check is that the default toString method
114 // creating `[object Object]` is universally useless to any end user.115 // creating `[object Object]` is universally useless to any end user.
115 if (116 if (
...@@ -126,7 +127,7 @@ export function escapeXML(input: unknown) {...@@ -126,7 +127,7 @@ export function escapeXML(input: unknown) {
126 `To emit a literal '${input}', use \${String(value)}`,127 `To emit a literal '${input}', use \${String(value)}`,
127 );128 );
128 }129 }
129 return marko.escapeXML(input);130 return marko._escape(input);
130}131}
131132
132interface Async {133interface Async {
...@@ -135,5 +136,5 @@ interface Async {...@@ -135,5 +136,5 @@ interface Async {
135136
136import * as render from "#engine/render";137import * as render from "#engine/render";
137import type { ServerRenderer } from "marko/html/template";138import type { ServerRenderer } from "marko/html/template";
138import { type Accessor } from "marko/common/types";139import type { Template } from "marko/common/types";
139import * as marko from "#marko/html";140import * as marko from "#marko/html";
framework/engine/render.ts+19-8
...@@ -157,7 +157,7 @@ export function resolveNode(r: State, node: unknown): ResolvedNode {...@@ -157,7 +157,7 @@ export function resolveNode(r: State, node: unknown): ResolvedNode {
157 return resolveNode(r, tag(props));157 return resolveNode(r, tag(props));
158 } catch (e) {158 } catch (e) {
159 const { 4: src } = node;159 const { 4: src } = node;
160 if (e && typeof e === "object") (e as { src?: string }).src = src;160 if (e && typeof e === "object") (e as { src?: string }).src ??= src;
161 throw e;161 throw e;
162 } finally {162 } finally {
163 current = null;163 current = null;
...@@ -222,19 +222,30 @@ function stringifyElement(element: ResolvedElement) {...@@ -222,19 +222,30 @@ function stringifyElement(element: ResolvedElement) {
222 default:222 default:
223 attr = `${prop}=${quoteIfNeeded(escapeAttribute(String(value)))}`;223 attr = `${prop}=${quoteIfNeeded(escapeAttribute(String(value)))}`;
224 break;224 break;
225 case "className":225 case "className": // Legacy React Compat
226 // Legacy React Compat226 case "class": {
227 case "class":227 const str = clsx(value as ClsxInput);
228 attr = `class=${228 if (!str) continue;
229 quoteIfNeeded(escapeAttribute(clsx(value as ClsxInput)))229 attr = `class=${quoteIfNeeded(escapeAttribute(str))}`;
230 }`;
231 break;230 break;
231 }
232 case "style": {
233 const str = typeof value === "string"
234 ? value
235 : Object.entries(value).map((kv) => kv.join(":")).join(";");
236 if (!str) continue;
237 attr = `style=${quoteIfNeeded(escapeAttribute(str))}`;
238 break;
239 }
232 case "htmlFor":240 case "htmlFor":
233 throw new Error("Do not use the `htmlFor` attribute. Use `for`");241 throw new Error("Do not use the `htmlFor` attribute. Use `for`");
242 case "dangerouslySetInnerHTML":
243 throw new Error(
244 "Do not use the `dangerouslySetInnerHTML` attribute. Use `render.raw()`",
245 );
234 // Do not process these246 // Do not process these
235 case "children":247 case "children":
236 case "ref":248 case "ref":
237 case "dangerouslySetInnerHTML":
238 case "key":249 case "key":
239 continue;250 continue;
240 }251 }
framework/esbuild-support.ts+1-2
...@@ -112,9 +112,8 @@ export function isIgnoredSource(source: string) {...@@ -112,9 +112,8 @@ export function isIgnoredSource(source: string) {
112}112}
113113
114import * as esbuild from "esbuild";114import * as esbuild from "esbuild";
115import * as string from "#sitegen/string";115import * as string from "lib/string.ts";
116import * as path from "node:path";116import * as path from "node:path";
117import * as fs from "#sitegen/fs";117import * as fs from "#sitegen/fs";
118import * as incr from "./incremental.ts";
119import * as hot from "./hot.ts";118import * as hot from "./hot.ts";
120import { markoCache } from "./marko.ts";119import { markoCache } from "./marko.ts";
framework/hot.ts+1
...@@ -153,6 +153,7 @@ export function loadEsbuildCode(...@@ -153,6 +153,7 @@ export function loadEsbuildCode(
153 jsxDev: true,153 jsxDev: true,
154 sourcefile: filepath,154 sourcefile: filepath,
155 sourcemap: "inline",155 sourcemap: "inline",
156 sourceRoot: "/",
156 }).code;157 }).code;
157 return module._compile(src, filepath, "commonjs");158 return module._compile(src, filepath, "commonjs");
158}159}
framework/incremental.ts+1-2
...@@ -63,7 +63,6 @@ export function work<I, O>(...@@ -63,7 +63,6 @@ export function work<I, O>(
63 const io = new Io(key, p);63 const io = new Io(key, p);
64 jobs += 1;64 jobs += 1;
65 newKeys += 1;65 newKeys += 1;
66 await timers.setTimeout(200);
67 try {66 try {
68 const value = await job(io, input as I);67 const value = await job(io, input as I);
69 validateSerializable(value, "");68 validateSerializable(value, "");
...@@ -103,7 +102,7 @@ export async function compile<T>(...@@ -103,7 +102,7 @@ export async function compile<T>(
103 p: progress.Ref = progress,102 p: progress.Ref = progress,
104) {103) {
105 ASSERT(!running, `Cannot run twice at the same time.`);104 ASSERT(!running, `Cannot run twice at the same time.`);
106 const node = p.start("sitegen!");105 using node = p.start("sitegen!");
107 try {106 try {
108 running = true;107 running = true;
109 ASSERT(jobs === 0);108 ASSERT(jobs === 0);
framework/lib/assets.ts+3-8
...@@ -2,7 +2,7 @@...@@ -2,7 +2,7 @@
2// This module implements decoding and serving of the asset blobs,2// This module implements decoding and serving of the asset blobs,
3// but also implements patching of dynamic assets. The `Manifest`3// but also implements patching of dynamic assets. The `Manifest`
4// is generated by `incremental.ts`4// is generated by `incremental.ts`
5const debug = console.scoped("assets");5const console = log.scoped("assets");
6const root = import.meta.dirname;6const root = import.meta.dirname;
7let current: Loaded | null = null;7let current: Loaded | null = null;
88
...@@ -93,11 +93,6 @@ export async function serveAsset(...@@ -93,11 +93,6 @@ export async function serveAsset(
93 );93 );
94}94}
9595
96/** @deprecated */
97export function hasAsset(id: string) {
98 return UNWRAP(current).map[id] !== undefined;
99}
100
101export function etagMatches(etag: string, ifNoneMatch: string) {96export function etagMatches(etag: string, ifNoneMatch: string) {
102 return ifNoneMatch === etag || ifNoneMatch.split(/,\s*/).indexOf(etag) > -1;97 return ifNoneMatch === etag || ifNoneMatch.split(/,\s*/).indexOf(etag) > -1;
103}98}
...@@ -171,7 +166,7 @@ function respondWithBufferAndViews(...@@ -171,7 +166,7 @@ function respondWithBufferAndViews(
171 } else {166 } else {
172 body = buffer.subarray(...asset.raw);167 body = buffer.subarray(...asset.raw);
173 }168 }
174 debug(169 console.info(
175 `${key} encoding=${headers["Content-Encoding"] ?? "raw"} status=${status}`,170 `${key} encoding=${headers["Content-Encoding"] ?? "raw"} status=${status}`,
176 );171 );
177 if (!Object.keys(headers).some((x) => x.toLowerCase() === "cache-control")) {172 if (!Object.keys(headers).some((x) => x.toLowerCase() === "cache-control")) {
...@@ -290,4 +285,4 @@ import type { AssetKey as Key } from "../../.clover/ts/asset.d.ts";...@@ -290,4 +285,4 @@ import type { AssetKey as Key } from "../../.clover/ts/asset.d.ts";
290import * as crypto from "node:crypto";285import * as crypto from "node:crypto";
291import * as zlib from "node:zlib";286import * as zlib from "node:zlib";
292import * as util from "node:util";287import * as util from "node:util";
293import * as console from "@paperclover/console";288import * as log from "lib/log.ts";
framework/lib/async.ts+2-221
...@@ -1,219 +1,5 @@...@@ -1,219 +1,5 @@
1const five_minutes = 5 * 60 * 1000;1const five_minutes = 5 * 60 * 1000;
22
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.
12/** @deprecated */
13export class Queue<T, R> {
14 #name: string;
15 #fn: (item: T, spin: Spinner) => Promise<R>;
16 #maxJobs: number;
17 #getItemText: (item: T) => string;
18 #passive: boolean;
19
20 #active: Spinner[] = [];
21 #queue: Array<[T] | [T, (result: R) => void, (err: unknown) => void]> = [];
22
23 #cachedProgress: Progress<{ active: Spinner[] }> | null = null;
24 #done: number = 0;
25 #total: number = 0;
26 #onComplete: (() => void) | null = null;
27 #estimate: number | null = null;
28 #errors: unknown[] = [];
29
30 constructor(options: QueueOptions<T, R>) {
31 this.#name = options.name;
32 this.#fn = options.fn;
33 this.#maxJobs = options.maxJobs ?? 5;
34 this.#getItemText = options.getItemText ?? defaultGetItemText;
35 this.#passive = options.passive ?? false;
36 }
37
38 cancel() {
39 const bar = this.#cachedProgress;
40 bar?.stop();
41 this.#queue = [];
42 }
43
44 get bar() {
45 const cached = this.#cachedProgress;
46 if (!cached) {
47 const bar = (this.#cachedProgress = new Progress({
48 spinner: null,
49 text: ({ active }) => {
50 const now = performance.now();
51 let text = `[${this.#done}/${this.#total}] ${this.#name}`;
52 let n = 0;
53 for (const item of active) {
54 let itemText = "- " + item.format(now);
55 text += `\n` +
56 itemText.slice(0, Math.max(0, process.stdout.columns - 1));
57 if (n > 10) {
58 text += `\n ... + ${active.length - n} more`;
59 break;
60 }
61 n++;
62 }
63 return text;
64 },
65 props: {
66 active: [] as Spinner[],
67 },
68 }));
69 bar.value = 0;
70 return bar;
71 }
72 return cached;
73 }
74
75 addReturn(args: T) {
76 this.#total += 1;
77 this.updateTotal();
78 if (this.#active.length >= this.#maxJobs) {
79 const { promise, resolve, reject } = Promise.withResolvers<R>();
80 this.#queue.push([args, resolve, reject]);
81 return promise;
82 }
83 return this.#run(args);
84 }
85
86 add(args: T) {
87 return this.addReturn(args).then(
88 () => {},
89 () => {},
90 );
91 }
92
93 addMany(items: T[]) {
94 this.#total += items.length;
95 this.updateTotal();
96
97 const runNowCount = this.#maxJobs - this.#active.length;
98 const runNow = items.slice(0, runNowCount);
99 const runLater = items.slice(runNowCount);
100 this.#queue.push(...runLater.reverse().map<[T]>((x) => [x]));
101 runNow.map((item) => this.#run(item).catch(() => {}));
102 }
103
104 async #run(args: T): Promise<R> {
105 const bar = this.bar;
106 const itemText = this.#getItemText(args);
107 const spinner = new Spinner(itemText);
108 spinner.stop();
109 (spinner as any).redraw = () => (bar as any).redraw();
110 const active = this.#active;
111 try {
112 active.unshift(spinner);
113 bar.props = { active };
114 // console.log(this.#name + ": " + itemText);
115 const result = await this.#fn(args, spinner);
116 this.#done++;
117 return result;
118 } catch (err) {
119 if (err && typeof err === "object") {
120 (err as any).job = itemText;
121 }
122 this.#errors.push(err);
123 console.error(util.inspect(err, false, Infinity, true));
124 throw err;
125 } finally {
126 active.splice(active.indexOf(spinner), 1);
127 bar.props = { active };
128 bar.value = this.#done;
129
130 // Process next item
131 const next = this.#queue.shift();
132 if (next) {
133 const args = next[0];
134 this.#run(args)
135 .then((result) => next[1]?.(result))
136 .catch((err) => next[2]?.(err));
137 } else if (this.#active.length === 0) {
138 if (this.#passive) {
139 this.bar.stop();
140 this.#cachedProgress = null;
141 }
142 this.#onComplete?.();
143 }
144 }
145 }
146
147 updateTotal() {
148 const bar = this.bar;
149 bar.total = Math.max(this.#total, this.#estimate ?? 0);
150 }
151
152 set estimate(e: number) {
153 this.#estimate = e;
154 if (this.#cachedProgress) {
155 this.updateTotal();
156 }
157 }
158
159 async done(o?: { method: "success" | "stop" }) {
160 if (this.#active.length === 0) {
161 this.#end(o);
162 return;
163 }
164
165 const { promise, resolve } = Promise.withResolvers<void>();
166 this.#onComplete = resolve;
167 await promise;
168 this.#end(o);
169 }
170
171 #end({
172 method = this.#passive ? "stop" : "success",
173 }: {
174 method?: "success" | "stop";
175 } = {}) {
176 const bar = this.#cachedProgress;
177 if (this.#errors.length > 0) {
178 if (bar) bar.stop();
179 throw new AggregateError(
180 this.#errors,
181 this.#errors.length + " jobs failed in '" + this.#name + "'",
182 );
183 }
184
185 if (bar) bar[method]();
186 }
187
188 get active(): boolean {
189 return this.#active.length !== 0;
190 }
191
192 [Symbol.dispose]() {
193 if (this.active) {
194 this.cancel();
195 }
196 }
197}
198
199const cwd = process.cwd();
200function defaultGetItemText(item: unknown) {
201 let itemText = "";
202 if (typeof item === "string") {
203 itemText = item;
204 } else if (typeof item === "object" && item !== null) {
205 const { path, label, id } = item as any;
206 itemText = label ?? path ?? id ?? JSON.stringify(item);
207 } else {
208 itemText = JSON.stringify(item);
209 }
210
211 if (itemText.startsWith(cwd)) {
212 itemText = path.relative(cwd, itemText);
213 }
214 return itemText;
215}
216
217/** @deprecated use OnceMap2 */3/** @deprecated use OnceMap2 */
218export class OnceMap<T> {4export class OnceMap<T> {
219 private ongoing = new Map<string, Promise<T>>();5 private ongoing = new Map<string, Promise<T>>();
...@@ -415,8 +201,9 @@ export class PromiseAggregator {...@@ -415,8 +201,9 @@ export class PromiseAggregator {
415201
416 async all() {202 async all() {
417 while (this.promises.size > 0) {203 while (this.promises.size > 0) {
418 await Promise.all(this.promises);204 const wait = [...this.promises];
419 this.promises.clear();205 this.promises.clear();
206 await Promise.all(wait);
420 }207 }
421 if (this.failures.length > 0) {208 if (this.failures.length > 0) {
422 const agg = new AggregateError(this.failures);209 const agg = new AggregateError(this.failures);
...@@ -429,9 +216,3 @@ export class PromiseAggregator {...@@ -429,9 +216,3 @@ export class PromiseAggregator {
429 return this.all();216 return this.all();
430 }217 }
431}218}
432
433import { Progress } from "@paperclover/console/Progress";
434import { Spinner } from "@paperclover/console/Spinner";
435import * as path from "node:path";
436import process from "node:process";
437import * as util from "node:util";
framework/lib/markdown.tsx deleted-200
...@@ -1,200 +0,0 @@
1/* Implementation of [CommonMark] specification for markdown with support
2 * for custom syntax extensions via the parser options. Instead of
3 * returning an AST that has a second conversion pass to JSX, the
4 * returned value of 'parse' is 'engine.Node' which can be stringified
5 * via Clover's SSR engine. This way, generation optimizations, async
6 * components, and other features are gained for free here.
7 *
8 * [CommonMark]: https://spec.commonmark.org/0.31.2/
9 */
10function parse(src: string, options: Partial<ParseOpts> = {}) {}
11
12/* Render markdown content. Same function as 'parse', but JSX components
13 * only take one argument and must start with a capital letter. */
14export function Markdown({
15 src,
16 ...options
17}: { src: string } & Partial<ParseOpts>) {
18 return parse(src, options);
19}
20
21// TODO: This implementation is flawed because it is impossible to sanely handle
22// emphasis and strong emphasis, and all their edge cases. Instead of making these
23// using extensions interface, they should be special cased.
24function parseInline(src: string, options: Partial<InlineOpts> = {}) {
25 const { rules = inlineRules, links = new Map() } = options;
26 const opts: InlineOpts = { rules, links };
27 const parts: render.Node[] = [];
28 const ruleList = Object.values(rules);
29 parse: while (true) {
30 for (const rule of ruleList) {
31 if (!rule.match) continue;
32 const match = src.match(rule.match);
33 if (!match) continue;
34 const index = UNWRAP(match.index);
35 const after = src.slice(index + match[0].length);
36 const parse = rule.parse({ after, match: match[0], opts });
37 if (!parse) continue;
38 // parse before
39 parts.push(src.slice(0, index), parse.result);
40 src = parse.rest ?? after;
41 continue parse;
42 }
43 break;
44 }
45 parts.push(src);
46 return parts;
47}
48
49// -- interfaces --
50interface ParseOpts {
51 gfm: boolean;
52 blockRules: Record<string, BlockRule>;
53 inlineRules: Record<string, InlineRule>;
54}
55interface InlineOpts {
56 rules: Record<string, InlineRule>;
57 links: Map<string, LinkRef>;
58}
59interface InlineRule {
60 match: RegExp;
61 parse(opts: {
62 after: string;
63 match: string;
64 opts: InlineOpts;
65 }): InlineParse | null;
66}
67interface InlineParse {
68 result: engine.Node;
69 rest?: string;
70}
71interface LinkRef {
72 href: string;
73 title: string | null;
74}
75interface BlockRule {
76 match: RegExp;
77 parse(opts: {}): unknown;
78}
79export const inlineRules: Record<string, InlineRule> = {
80 code: {
81 match: /`+/,
82 // 6.1 - code spans
83 parse({ after, match }) {
84 const end = after.indexOf(match);
85 if (end === -1) return null;
86 let inner = after.slice(0, end);
87 const rest = after.slice(end + match.length);
88 // If the resulting string both begins and ends with a space
89 // character, but does not consist entirely of space characters,
90 // a single space character is removed from the front and back.
91 if (inner.match(/^ [^ ]+ $/)) inner = inner.slice(1, -1);
92 return { result: <code>{inner}</code>, rest };
93 },
94 },
95 link: {
96 match: /(?<!!)\[/,
97 // 6.3 - links
98 parse({ after, opts }) {
99 // Match '[' to let the inner-most link win.
100 const splitText = splitFirst(after, /[[\]]/);
101 if (!splitText) return null;
102 if (splitText.delim !== "]") return null;
103 const { first: textSrc, rest: afterText } = splitText;
104 let href: string,
105 title: string | null = null,
106 rest: string;
107 if (afterText[0] === "(") {
108 // Inline link
109 const splitTarget = splitFirst(afterText.slice(1), /\)/);
110 if (!splitTarget) return null;
111 ({ rest } = splitTarget);
112 const target = parseLinkTarget(splitTarget.first);
113 if (!target) return null;
114 ({ href, title } = target);
115 } else if (afterText[0] === "[") {
116 const splitTarget = splitFirst(afterText.slice(1), /]/);
117 if (!splitTarget) return null;
118 const name = splitTarget.first.trim().length === 0
119 // Collapsed reference link
120 ? textSrc.trim()
121 // Full Reference Link
122 : splitTarget.first.trim();
123 const target = opts.links.get(name);
124 if (!target) return null;
125 ({ href, title } = target);
126 ({ rest } = splitTarget);
127 } else {
128 // Shortcut reference link
129 const target = opts.links.get(textSrc);
130 if (!target) return null;
131 ({ href, title } = target);
132 rest = afterText;
133 }
134 return {
135 result: <a {...{ href, title }}>{parseInline(textSrc, opts)}</a>,
136 rest,
137 };
138 },
139 },
140 image: {
141 match: /!\[/,
142 // 6.4 - images
143 parse({ after, opts }) {
144 // Match '[' to let the inner-most link win.
145 const splitText = splitFirst(after, /[[\]]/);
146 if (!splitText) return null;
147 if (splitText.delim !== "]") return null;
148 const { first: textSrc, rest: afterText } = splitText;
149 },
150 },
151 emphasis: {
152 // detect left-flanking delimiter runs, but this expression does not
153 // consider preceding escapes. instead, those are programatically
154 // checked inside the parse function.
155 match: /(?:\*+|(?<!\p{P})_+)(?!\s|\p{P}|$)/u,
156 // 6.2 - emphasis and strong emphasis
157 parse({ before, match, after, opts }) {
158 // find out how long the delim sequence is
159 // look for 'ends'
160 },
161 },
162 autolink: {},
163 html: {},
164 br: {
165 match: / +\n|\\\n/,
166 parse() {
167 return { result: <br /> };
168 },
169 },
170};
171
172function parseLinkTarget(src: string) {
173 let href: string,
174 title: string | null = null;
175 href = src;
176 return { href, title };
177}
178
179/* Find a delimiter while considering backslash escapes. */
180function splitFirst(text: string, match: RegExp) {
181 let first = "",
182 delim: string,
183 escaped: boolean;
184 do {
185 const find = text.match(match);
186 if (!find) return null;
187 delim = find[0];
188 const index = UNWRAP(find.index);
189 let i = index - 1;
190 escaped = false;
191 while (i >= 0 && text[i] === "\\") (escaped = !escaped), (i -= 1);
192 first += text.slice(0, index - +escaped);
193 text = text.slice(index + find[0].length);
194 } while (escaped);
195 return { first, delim, rest: text };
196}
197
198console.log(render.sync(parseInline("meow `bwaa` `` ` `` `` `z``")));
199
200import * as render from "#engine/render";
framework/lib/path.ts+18-2
...@@ -17,6 +17,14 @@ export class Path {...@@ -17,6 +17,14 @@ export class Path {
17 ));17 ));
18 }18 }
1919
20 static createDirSync(...parts: string[]): Path;
21 static createDirSync(base: Path | string, ...more: string[]): Path;
22 static createDirSync(base: Path | string, ...more: string[]): Path {
23 const path = Path.resolve(base, ...more);
24 path.makeDirSync();
25 return path;
26 }
27
20 /** For convenience, paths are deduplicated. */28 /** For convenience, paths are deduplicated. */
21 constructor(raw: string) {29 constructor(raw: string) {
22 ASSERT(path.isAbsolute(raw));30 ASSERT(path.isAbsolute(raw));
...@@ -174,8 +182,15 @@ export class Path {...@@ -174,8 +182,15 @@ export class Path {
174 return fs.readdirSync(this.raw).map((sub) => this.join(sub));182 return fs.readdirSync(this.raw).map((sub) => this.join(sub));
175 }183 }
176184
177 async readDir(): Promise<Array<Path>> {185 async readDir(
178 return (await fs.readdir(this.raw)).map((sub) => this.join(sub));186 { recursive }: { recursive?: boolean } = {},
187 ): Promise<Array<Path>> {
188 return (await fs.readdir(this.raw, { recursive }))
189 .map((sub) => this.join(sub));
190 }
191
192 async writeJsonFile(json: unknown) {
193 await fs.writeFile(this.raw, JSON.stringify(json));
179 }194 }
180195
181 static sep = path.sep;196 static sep = path.sep;
...@@ -194,3 +209,4 @@ import * as fs from "./fs.ts";...@@ -194,3 +209,4 @@ import * as fs from "./fs.ts";
194import * as util from "node:util";209import * as util from "node:util";
195import * as error from "./error.ts";210import * as error from "./error.ts";
196import type { RmOptions } from "node:fs";211import type { RmOptions } from "node:fs";
212import type { Serialized } from "./lru.ts";
framework/lib/string.ts deleted-3
...@@ -1,3 +0,0 @@
1export function escapeRegExp(source: string) {
2 return source.replace(/[\$\\]/g, "\\$&");
3}
framework/lib/subprocess.ts+24-4
...@@ -1,3 +1,6 @@...@@ -1,3 +1,6 @@
1// This file is expecting a full rewrite to abstract the Node API away
2// entirely, exposing web streams I/O.
3
1const execFileRaw = util.promisify(child_process.execFile);4const execFileRaw = util.promisify(child_process.execFile);
2export const exec: typeof execFileRaw = ((5export const exec: typeof execFileRaw = ((
3 ...args: Parameters<typeof execFileRaw>6 ...args: Parameters<typeof execFileRaw>
...@@ -12,6 +15,7 @@ export const exec: typeof execFileRaw = ((...@@ -12,6 +15,7 @@ export const exec: typeof execFileRaw = ((
12 })) as any;15 })) as any;
1316
14const activePids = new Set<number>();17const activePids = new Set<number>();
18
15export interface SpawnOptions extends child_process.SpawnOptions {19export interface SpawnOptions extends child_process.SpawnOptions {
16 cmd: string[];20 cmd: string[];
17}21}
...@@ -26,9 +30,28 @@ export function spawn({ cmd, ...rest }: SpawnOptions) {...@@ -26,9 +30,28 @@ export function spawn({ cmd, ...rest }: SpawnOptions) {
26 return process;30 return process;
27}31}
2832
33export interface ForkOptions extends child_process.ForkOptions {
34 cmd: string[];
35}
36
37export function fork({ cmd, ...rest }: ForkOptions) {
38 rest.detached ??= true;
39 const process = child_process.fork(cmd[0]!, cmd.slice(1), rest);
40 const pid = rest.detached && process.pid ? -process.pid : process.pid;
41 if (pid) {
42 activePids.add(pid);
43 process.on("exit", () => void activePids.delete(pid));
44 }
45 return process;
46}
47
29export async function spawnAndWait(opts: SpawnOptions) {48export async function spawnAndWait(opts: SpawnOptions) {
30 opts.stdio ??= ["ignore", "inherit", "inherit"];49 opts.stdio ??= ["ignore", "inherit", "inherit"];
31 const process = spawn(opts);50 const process = spawn(opts);
51 const output = Promise.all([
52 process.stdout ? Array.fromAsync(process.stdout).then(Buffer.concat) : null,
53 process.stderr ? Array.fromAsync(process.stderr).then(Buffer.concat) : null,
54 ]);
32 let [code, signal] = await events.EventEmitter.once(process, "exit") as [55 let [code, signal] = await events.EventEmitter.once(process, "exit") as [
33 number | null,56 number | null,
34 string | null,57 string | null,
...@@ -38,10 +61,7 @@ export async function spawnAndWait(opts: SpawnOptions) {...@@ -38,10 +61,7 @@ export async function spawnAndWait(opts: SpawnOptions) {
38 const exit = signal ? `signal ${signal}` : `code ${code}`;61 const exit = signal ? `signal ${signal}` : `code ${code}`;
39 throw new Error(`${opts.cmd[0]} failed with ${exit}`);62 throw new Error(`${opts.cmd[0]} failed with ${exit}`);
40 }63 }
41 const [stdout, stderr] = await Promise.all([64 const [stdout, stderr] = await output;
42 process.stdout ? Array.fromAsync(process.stdout).then(Buffer.concat) : null,
43 process.stderr ? Array.fromAsync(process.stderr).then(Buffer.concat) : null,
44 ]);
45 return { stdout, stderr };65 return { stdout, stderr };
46}66}
4767
framework/marko.ts+15-2
...@@ -29,10 +29,22 @@ export function loadMarko(module: NodeJS.Module, filepath: string) {...@@ -29,10 +29,22 @@ export function loadMarko(module: NodeJS.Module, filepath: string) {
29 ) + '\nimport { addScript as CloverScriptInclude } from "#sitegen";\n';29 ) + '\nimport { addScript as CloverScriptInclude } from "#sitegen";\n';
30 }30 }
3131
32 src = marko.compileSync(src, filepath).code;32 const compiled = marko.compileSync(src, filepath, {
33 src = src.replace("marko/debug/html", "#engine/marko-runtime");33 sourceMaps: true,
34 });
35 // workaround an esbuild bug?
36 (compiled.map as unknown as { sourceRoot: string }).sourceRoot = "/";
37 compiled.map.sources[0] = filepath;
38 src = compiled.code.replace("marko/debug/html", "#engine/marko-runtime") +
39 "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," +
40 Buffer.from(JSON.stringify(compiled.map), "utf-8").toString("base64");
34 cache = { src, scannedClientRefs: Array.from(scannedClientRefs) };41 cache = { src, scannedClientRefs: Array.from(scannedClientRefs) };
35 markoCache.set(filepath, cache);42 markoCache.set(filepath, cache);
43 fs.writeMkdirSync(
44 ".clover/marko/" + path.relative(".", filepath).replace(/^src\//, "") +
45 ".js",
46 src,
47 );
36 }48 }
3749
38 const { src, scannedClientRefs } = cache;50 const { src, scannedClientRefs } = cache;
...@@ -44,3 +56,4 @@ export function loadMarko(module: NodeJS.Module, filepath: string) {...@@ -44,3 +56,4 @@ export function loadMarko(module: NodeJS.Module, filepath: string) {
44import * as marko from "@marko/compiler";56import * as marko from "@marko/compiler";
45import * as hot from "./hot.ts";57import * as hot from "./hot.ts";
46import * as fs from "#sitegen/fs";58import * as fs from "#sitegen/fs";
59import * as path from "node:path";
framework/watch.ts+13-16
...@@ -2,9 +2,9 @@...@@ -2,9 +2,9 @@
22
3const debounceMilliseconds = 25;3const debounceMilliseconds = 25;
44
5let subprocess: child_process.ChildProcess | null = null;5let server: child_process.ChildProcess | null = null;
6process.on("beforeExit", () => {6process.on("beforeExit", () => {
7 subprocess?.removeListener("close", onSubprocessClose);7 server?.removeListener("close", onSubprocessClose);
8});8});
99
10let watch: Watch;10let watch: Watch;
...@@ -17,21 +17,20 @@ export async function main() {...@@ -17,21 +17,20 @@ export async function main() {
17}17}
1818
19function serve() {19function serve() {
20 if (subprocess) {20 if (server) {
21 subprocess.removeListener("close", onSubprocessClose);21 server.removeListener("close", onSubprocessClose);
22 subprocess.kill();22 server.kill();
23 }23 }
24 subprocess = child_process.fork(".clover/o/backend.js", [24 server = subprocess.fork({
25 "--development",25 cmd: [".clover/o/backend.js", "--development"],
26 ], {
27 stdio: "inherit",26 stdio: "inherit",
28 execArgv: ["--no-warnings"],27 execArgv: ["--no-warnings"],
29 });28 });
30 subprocess.on("close", onSubprocessClose);29 server.on("close", onSubprocessClose);
31}30}
3231
33function onSubprocessClose(code: number | null, signal: string | null) {32function onSubprocessClose(code: number | null, signal: string | null) {
34 subprocess = null;33 server = null;
35 const status = code != null ? `code ${code}` : `signal ${signal}`;34 const status = code != null ? `code ${code}` : `signal ${signal}`;
36 console.error(`Backend process exited with ${status}`);35 console.error(`Backend process exited with ${status}`);
37}36}
...@@ -50,10 +49,10 @@ async function rebuild(files: string[]) {...@@ -50,10 +49,10 @@ async function rebuild(files: string[]) {
50 for (const file of removeWatch) watch.remove(file);49 for (const file of removeWatch) watch.remove(file);
51 watch.add(...watchFiles);50 watch.add(...watchFiles);
52 // Restart the server if it was changed or not running.51 // Restart the server if it was changed or not running.
53 if (!subprocess || newOutputs.includes("backend.js")) {52 if (!server || newOutputs.includes("backend.js")) {
54 serve();53 serve();
55 } else if (subprocess && newAssets) {54 } else if (server && newAssets) {
56 subprocess.send({ type: "clover.assets.reload" });55 server.send({ type: "clover.assets.reload" });
57 }56 }
58 }).catch((err) => {57 }).catch((err) => {
59 console.error(util.inspect(err));58 console.error(util.inspect(err));
...@@ -167,10 +166,8 @@ class Watch {...@@ -167,10 +166,8 @@ class Watch {
167}166}
168167
169import * as fs from "node:fs";168import * as fs from "node:fs";
170import { withSpinner } from "@paperclover/console/Spinner";
171import * as generate from "./generate.ts";169import * as generate from "./generate.ts";
172import * as incr from "./incremental.ts";170import * as incr from "./incremental.ts";
173import * as path from "node:path";171import * as path from "node:path";
174import * as util from "node:util";172import * as util from "node:util";
175import * as hot from "./hot.ts";173import * as subprocess from "#sitegen/subprocess";
176import * as child_process from "node:child_process";
lib/log.test.ts+3
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import { ASSERT, UNWRAP } from "lib/assert.ts";1import { ASSERT, UNWRAP } from "lib/assert.ts";
2import { test } from "node:test";2import { test } from "node:test";
3import * as headless from "lib/log/headless.ts";3import * as headless from "lib/log/headless.ts";
4import * as log from "lib/log.ts";
5
6log satisfies log.Scope; // the namespace import is a valid Scope
47
5class TestWidgetHost implements headless.WidgetHost {8class TestWidgetHost implements headless.WidgetHost {
6 columns = 80;9 columns = 80;
lib/log.ts+75-25
...@@ -1,6 +1,59 @@...@@ -1,6 +1,59 @@
1/**
2 * By using `lib/log.ts`, your application gets easy scoped logging as well as
3 * integration with terminal widgets such as `lib/log/progress.ts`. The pattern
4 * for using this module is to shadow the global `console` with a local scope:
5 *
6 * import * as log from "@clo/lib/log";
7 * const console = log.scoped("http");
8 * console("hi!"); // debug
9 *
10 * Or to use the global socpe, import the namespace as `console`.
11 *
12 * import * as console from "@clo/lib/log";
13 *
14 * Now, the code reads familiarly and output is organized.
15 *
16 * This module offers a Node.js integration to have logs go to the standard
17 * output. Custom writers can be used with `lib/log/headless.ts`.
18 *
19 * @module
20 */
21
22/**
23 * Logging scopes implement part of the `Console` API
24 */
25export interface Scope {
26 /** emit an informational message */
27 info(...args: unknown[]): void;
28 /** emit an advisory message */
29 warn(...args: unknown[]): void;
30 /** emit a failure message */
31 error(...args: unknown[]): void;
32
33 /**
34 * emit a debugging message. */
35 log(...args: unknown[]): void;
36 /**
37 * emit a debugging message. this method is in place for compatibility with
38 * the `console` API. prefer calling `.log` directly.
39 */
40 debug(...args: unknown[]): void;
41
42 /** create a nested sub-scope */
43 scoped(name: string): Scope;
44}
45
46/** custom scopes are colored based on the name */
47export function scoped(name: string): Scope {
48 return globalLog.scoped(name);
49}
50
51// These functions implement `Scope`. If a function takes in `Scope`, the
52// namespace import of this file can be passed to it.
53
1/** informational */54/** informational */
2export function info(...args: unknown[]) {55export function info(...args: unknown[]) {
3 globalLog(...args);56 globalLog.info(...args);
4}57}
5/** advisory */58/** advisory */
6export function warn(...args: unknown[]) {59export function warn(...args: unknown[]) {
...@@ -11,28 +64,12 @@ export function error(...args: unknown[]) {...@@ -11,28 +64,12 @@ export function error(...args: unknown[]) {
11 globalLog.error(...args);64 globalLog.error(...args);
12}65}
13/** debugging */66/** debugging */
14export function debug(...args: unknown[]) {67export function log(...args: unknown[]) {
15 globalLog(...args);68 globalLog.log(...args);
16}69}
1770/** debugging */
18export interface Scope {71export function debug(...args: unknown[]) {
19 /** default call signature is informational scope */72 globalLog.debug(...args);
20 (...args: unknown[]): void;
21 /** informational */
22 info(...args: unknown[]): void;
23 /** advisory */
24 warn(...args: unknown[]): void;
25 /** failure */
26 error(...args: unknown[]): void;
27
28 /** prefix */
29 name: string;
30 scoped(name: string): Scope;
31}
32
33/** custom scopes are colored based on the name */
34export function scoped(name: string): Scope {
35 return globalLog.scoped(name);
36}73}
3774
38/**75/**
...@@ -77,9 +114,22 @@ export interface Widget {...@@ -77,9 +114,22 @@ export interface Widget {
77 // onKey?(key: string): void;114 // onKey?(key: string): void;
78}115}
79116
80const globalWidgetHost = /* @__PURE__ */ (() => {117const globalWidgetHost = /* @__PURE__ */ ((): headless.WidgetHost => {
118 const { process } = globalThis;
119 if (!process || !process.stderr.isTTY) {
120 return {
121 // TODO: allow passing a level here
122 writeLine: (line) => console["log"](line),
123 getDrawLock: () => ts.defer(() => {}),
124 startWidget: (w) => {
125 const close = w.onChange?.(() => {});
126 return ts.defer(close ?? (() => {}));
127 },
128 cancel: () => {},
129 };
130 }
81 const widget = headless.widgetHost({131 const widget = headless.widgetHost({
82 // TODO: consider synchronization and make these async132 // TODO:allow passing a level here to put debug, warn and errors on stderr
83 writeOutput: (string) => process.stdout.write(string),133 writeOutput: (string) => process.stdout.write(string),
84 writeInteractive: (string) => process.stderr.write(string),134 writeInteractive: (string) => process.stderr.write(string),
85 now: () => performance.now(),135 now: () => performance.now(),
...@@ -100,4 +150,4 @@ const globalLog = /* @__PURE__ */ headless.logger({...@@ -100,4 +150,4 @@ const globalLog = /* @__PURE__ */ headless.logger({
100150
101import process from "node:process";151import process from "node:process";
102import * as headless from "lib/log/headless.ts";152import * as headless from "lib/log/headless.ts";
103import type * as ts from "lib/ts.ts";153import * as ts from "lib/ts.ts";
lib/log/headless.ts+3
...@@ -302,7 +302,10 @@ export function logger(env: LoggerOptions): log.Scope {...@@ -302,7 +302,10 @@ export function logger(env: LoggerOptions): log.Scope {
302 function scoped(name: string): log.Scope {302 function scoped(name: string): log.Scope {
303 const formatted = name ? name + colon : name;303 const formatted = name ? name + colon : name;
304 const fn = logFn.bind(null, formatted, 0) as Partial<log.Scope>;304 const fn = logFn.bind(null, formatted, 0) as Partial<log.Scope>;
305 // TODO: repair
305 fn.info = fn as log.Scope["info"];306 fn.info = fn as log.Scope["info"];
307 fn.debug = fn as log.Scope["info"];
308 fn.log = fn as log.Scope["info"];
306 fn.warn = logFn.bind(null, formatted, 1);309 fn.warn = logFn.bind(null, formatted, 1);
307 fn.error = logFn.bind(null, formatted, 2);310 fn.error = logFn.bind(null, formatted, 2);
308 fn.scoped = createSubScope;311 fn.scoped = createSubScope;
lib/log/progress.ts+37-13
...@@ -45,6 +45,10 @@ export interface Node extends Ref {...@@ -45,6 +45,10 @@ export interface Node extends Ref {
45 * there are no children.45 * there are no children.
46 */46 */
47 passive: boolean;47 passive: boolean;
48 /**
49 * reactive, default `false`. when true, this progress item is hidden
50 */
51 hidden: boolean;
48 /** reactive. null means decide based on nested depth + terminal height */52 /** reactive. null means decide based on nested depth + terminal height */
49 maxHeight: null | number;53 maxHeight: null | number;
50 /** reactive. specify sorting for children */54 /** reactive. specify sorting for children */
...@@ -66,6 +70,7 @@ export interface StartOptions {...@@ -66,6 +70,7 @@ export interface StartOptions {
66 value?: number | undefined | null;70 value?: number | undefined | null;
67 estimate?: number | undefined | null;71 estimate?: number | undefined | null;
68 passive?: boolean | undefined | null;72 passive?: boolean | undefined | null;
73 hidden?: boolean | undefined | null;
69 showEstimate?: boolean | undefined | null;74 showEstimate?: boolean | undefined | null;
70 maxHeight?: number | undefined | null;75 maxHeight?: number | undefined | null;
71 sortChildren?: Node["sortChildren"] | undefined | null;76 sortChildren?: Node["sortChildren"] | undefined | null;
...@@ -89,6 +94,7 @@ export type SortNode =...@@ -89,6 +94,7 @@ export type SortNode =
89 | "value"94 | "value"
90 | "estimate"95 | "estimate"
91 | "passive"96 | "passive"
97 | "hidden"
92 | "showEstimate"98 | "showEstimate"
93 | "sortChildren"99 | "sortChildren"
94 >100 >
...@@ -103,7 +109,7 @@ export interface Internal extends SortNode {...@@ -103,7 +109,7 @@ export interface Internal extends SortNode {
103109
104type Signal =110type Signal =
105 | "draw"111 | "draw"
106 | { type: "end"; title: string; logs: string[] }112 | { type: "end"; title: string; logs: string[]; state: Internal }
107 | { type: "line"; line: string };113 | { type: "line"; line: string };
108114
109function newNode(115function newNode(
...@@ -116,6 +122,7 @@ function newNode(...@@ -116,6 +122,7 @@ function newNode(
116 estimate: opts.estimate ?? 0,122 estimate: opts.estimate ?? 0,
117 showEstimate: opts.showEstimate ?? true,123 showEstimate: opts.showEstimate ?? true,
118 passive: opts.passive ?? false,124 passive: opts.passive ?? false,
125 hidden: opts.hidden ?? false,
119 logs: [],126 logs: [],
120 children: [],127 children: [],
121 parent: null,128 parent: null,
...@@ -127,17 +134,18 @@ function newNode(...@@ -127,17 +134,18 @@ function newNode(
127 let detached = false;134 let detached = false;
128 function end() {135 function end() {
129 if (detached) return;136 if (detached) return;
130 if (!state.parent) return;
131 const i = state.parent.children.indexOf(state);
132 if (i === -1) return;
133 let title = state.text;137 let title = state.text;
134 let p: Internal | null = state;138 if (state.parent) {
135 while (p = p.parent) title = p.text + " / " + title;139 const i = state.parent.children.indexOf(state);
136 state.signal({ type: "end", title, logs: state.logs });140 ASSERT(i !== -1);
141 let p: Internal | null = state;
142 while (p = p.parent) title = p.text + " / " + title;
143 state.parent.children.splice(i, 1);
144 state.parent = null;
145 detached = true;
146 }
147 state.signal({ type: "end", title, logs: state.logs, state });
137 state.logs = [];148 state.logs = [];
138 state.parent.children.splice(i, 1);
139 state.parent = null;
140 detached = true;
141 }149 }
142 function mutate() {150 function mutate() {
143 const { parent } = state;151 const { parent } = state;
...@@ -199,6 +207,13 @@ function newNode(...@@ -199,6 +207,13 @@ function newNode(
199 state.passive = value;207 state.passive = value;
200 mutate();208 mutate();
201 },209 },
210 get hidden() {
211 return state.hidden;
212 },
213 set hidden(value) {
214 state.hidden = value;
215 mutate();
216 },
202 maxHeight: null,217 maxHeight: null,
203 // get maxHeight() {218 // get maxHeight() {
204 // return state.maxHeight;219 // return state.maxHeight;
...@@ -284,7 +299,9 @@ function renderMainLine(state: Internal, now: number, depth: number) {...@@ -284,7 +299,9 @@ function renderMainLine(state: Internal, now: number, depth: number) {
284}299}
285300
286function hasChildren(states: Internal): boolean {301function hasChildren(states: Internal): boolean {
287 return states.children.some((x) => !x.passive || hasChildren(x));302 return states.children.some((x) =>
303 !x.hidden && (!x.passive || hasChildren(x))
304 );
288}305}
289306
290function renderChildren(state: Internal, now: number, depth: boolean[]) {307function renderChildren(state: Internal, now: number, depth: boolean[]) {
...@@ -292,7 +309,9 @@ function renderChildren(state: Internal, now: number, depth: boolean[]) {...@@ -292,7 +309,9 @@ function renderChildren(state: Internal, now: number, depth: boolean[]) {
292 let truncated = 0;309 let truncated = 0;
293 let out = "";310 let out = "";
294 let { children } = state;311 let { children } = state;
295 children = children.filter((x) => !x.passive || hasChildren(x));312 children = children.filter((x) =>
313 !x.hidden && (!x.passive || hasChildren(x))
314 );
296 if (children.length === 0) return "";315 if (children.length === 0) return "";
297 for (let i = 0, { length } = children; i < length; i += 1) {316 for (let i = 0, { length } = children; i < length; i += 1) {
298 const child = UNWRAP(children[i]);317 const child = UNWRAP(children[i]);
...@@ -376,7 +395,12 @@ export class Headless {...@@ -376,7 +395,12 @@ export class Headless {
376 if (msg === "draw") this.#rerender?.();395 if (msg === "draw") this.#rerender?.();
377 else if (msg.type === "line") this.env.writeLine(msg.line);396 else if (msg.type === "line") this.env.writeLine(msg.line);
378 else if (msg.type === "end") {397 else if (msg.type === "end") {
379 if (msg.logs.length === 0) return;398 const indexOfActive = this.active.indexOf(msg.state);
399 if (indexOfActive !== -1) this.active.splice(indexOfActive, 1);
400 if (msg.logs.length === 0) {
401 this.#rerender?.();
402 return;
403 }
380 const { logs } = msg;404 const { logs } = msg;
381 const count = `${logs.length} line${logs.length === 1 ? "" : "s"}`;405 const count = `${logs.length} line${logs.length === 1 ? "" : "s"}`;
382 const header = `[${count} from ${msg.title}]`;406 const header = `[${count} from ${msg.title}]`;
lib/log/stack.ts+7-4
...@@ -201,17 +201,20 @@ export function formatFrame(frame: Frame, colors: boolean): string {...@@ -201,17 +201,20 @@ export function formatFrame(frame: Frame, colors: boolean): string {
201 const left = text.slice(0, l);201 const left = text.slice(0, l);
202 center = text.slice(l, r);202 center = text.slice(l, r);
203 const right = text.slice(r);203 const right = text.slice(r);
204 const irrelevant = internal || file?.includes("node_modules");
205 const fg = !irrelevant ? ansi.fgRed : "";
206 const bold = !irrelevant ? ansi.bold : ansi.dim + ansi.bold;
204 out += (left207 out += (left
205 ? ansi.mergeStyles(ansi.fgRed + ansi.dim) + left +208 ? ansi.mergeStyles(fg + ansi.dim) + left +
206 ansi.mergeStyles(ansi.resetWeight + ansi.bold)209 ansi.mergeStyles(ansi.resetWeight + bold)
207 : ansi.mergeStyles(ansi.fgRed + ansi.bold)) +210 : ansi.mergeStyles(fg + bold)) +
208 center + (right211 center + (right
209 ? ansi.mergeStyles(ansi.resetWeight + ansi.dim) +212 ? ansi.mergeStyles(ansi.resetWeight + ansi.dim) +
210 right213 right
211 : "") +214 : "") +
212 ansi.mergeStyles(ansi.resetWeight + ansi.fgReset);215 ansi.mergeStyles(ansi.resetWeight + ansi.fgReset);
213 } else {216 } else {
214 out += ansi.fgRed + text + ansi.fgReset;217 out += text;
215 }218 }
216 } else {219 } else {
217 out += text;220 out += text;
lib/readme.changes.md+9
...@@ -1,5 +1,14 @@...@@ -1,5 +1,14 @@
1# notable changes in clover's typescript library1# notable changes in clover's typescript library
22
3## v2
4
5- top level readme now explains general usage patterns.
6- rework `log.Scope`, remove self call signature so that the `log` module
7 satisfies `log.Scope`, while adding `log` and `debug` for explicit debugging
8 logs. documentation prescribes doing `const console = log.scope("module");`.
9- add `stack.capture()`
10- add `string.escapeRegExp()`
11
3## v112## v1
413
5- new library `assert`14- new library `assert`
lib/readme.md+48-12
...@@ -5,11 +5,13 @@ either browsers or node.js. originally, these were helper functions used to...@@ -5,11 +5,13 @@ either browsers or node.js. originally, these were helper functions used to
5create my website, <https://paperclover.net>, but i started wanting them across5create my website, <https://paperclover.net>, but i started wanting them across
6projects and at work.6projects and at work.
77
8<!--%npm%-->
9
10## contents
11
8all top-level namespaces stand alone as their own projects, and generally do not12all top-level namespaces stand alone as their own projects, and generally do not
9depend on each other. there are no external dependencies; code is easy to audit.13depend on each other. there are no external dependencies; code is easy to audit.
1014
11the public api is simply all of the code. an overview:
12
13<!-- - `async` -->15<!-- - `async` -->
14<!-- - `error` - interop with the `unknown` type in error handlers -->16<!-- - `error` - interop with the `unknown` type in error handlers -->
15<!-- - `fs` - rebuilt api for `node:fs` -->17<!-- - `fs` - rebuilt api for `node:fs` -->
...@@ -18,16 +20,48 @@ the public api is simply all of the code. an overview:...@@ -18,16 +20,48 @@ the public api is simply all of the code. an overview:
18<!-- - `subprocess` - rebuilt api for `node:child_process` -->20<!-- - `subprocess` - rebuilt api for `node:child_process` -->
19<!-- - `testing` -->21<!-- - `testing` -->
2022
21- `assert`23- `assert.ts`
22- `log` - logging, terminal i/o, interactive widgets24- `log.ts` - logging, terminal i/o, interactive widgets
23- `log/ansi` - cursor and color constants25- `log/ansi.ts` - cursor and color constants
24- `log/headless` - use `log` and `log/progress` without a tty26- `log/headless.ts` - use `log` and `log/progress` without a tty
25- `log/progress` - progress bar, spinner, and tree27- `log/progress.ts` - progress bar, spinner, and tree
26- `log/stack` - stack trace parse and formatter28- `log/stack.ts` - stack trace parse and formatter
27- `packaging` - workflows used for `lib`'s publications29- `packaging.ts` - workflows used for `lib`'s publications
28- `queue` - priority queue with rescheduling30- `queue.ts` - priority queue with rescheduling
29- `string`31- `string.ts`
30- `ts` typescript types and general purpose code32- `ts.ts` typescript types and general purpose code
33
34there is currently no hosted documentation site. please refer to the source
35files' comments.
36
37## usage
38
39unless otherwise noted, it is expected to import modules using a namespace
40import. function names are often concise to avoid repeating redundant words in
41source code.
42
43```ts
44import * as queue from "@clo/lib/queue.ts";
45
46// `run` in the context of `queue` conveys more information
47const result = await queue.run({ ... });
48```
49
50without "barrel" files, using multiple libraries is done by importing each
51library separately:
52
53```ts
54import * as log from "@clo/lib/log.ts";
55import * as progress from "@clo/lib/log/progress.ts";
56```
57
58these patterns developed initially when using a text editor without a language
59server, but i found this pattern to be nicer in general for organizing code. it
60also resulted in fewer lines spent on imports, and making it much easier to
61understand where code comes from. to this extent, all of the library modules are
62named as single words so they can be easily imported.
63
64## versioning
3165
32these libraries are published as is, **without versioning or maintainance66these libraries are published as is, **without versioning or maintainance
33guarantees**. every version is a breaking major release. please upgrade your67guarantees**. every version is a breaking major release. please upgrade your
...@@ -36,6 +70,8 @@ individual modules may annotate their intended api and usage stability....@@ -36,6 +70,8 @@ individual modules may annotate their intended api and usage stability.
3670
37keep in mind there are [known bugs and missing features] in this release.71keep in mind there are [known bugs and missing features] in this release.
3872
73## contributions
74
39to contribute, please send bug reports or patches to <dev@paperclover.net>. new75to contribute, please send bug reports or patches to <dev@paperclover.net>. new
40features may not be accepted if they are complex and don't further my use cases.76features may not be accepted if they are complex and don't further my use cases.
4177
lib/readme.npm.md+5-45
...@@ -1,49 +1,9 @@...@@ -1,49 +1,9 @@
1# [clover's typescript library]
2
3`lib` is a collection of high-quality typescript libraries for things missing in
4either browsers or node.js. originally, these were helper functions used to
5create my website, <https://paperclover.net>, but i started wanting them across
6projects and at work.
7
8```sh1```sh
9# alias install "lib" using "@paperclover/lib"2# alias install "@paperclover/lib" as "@clo/lib"
10npm install lib@npm:@paperclover/lib3npm install lib@npm:@paperclover/lib
11```4```
125
13all top-level namespaces stand alone as their own projects, and generally do not6the distribution on npm is transpiled with JSDoc annotations instead of
14depend on each other. there are no external dependencies; code is easy to audit.7publishing the source code directly. this is done because
158[Node.js does not plan to support TypeScript within `node_modules`](https://github.com/nodejs/node/issues/57215),
16the public api is simply all of the code. an overview:9which is extremely sad to see.
17
18<!-- - `async` -->
19<!-- - `error` - interop with the `unknown` type in error handlers -->
20<!-- - `fs` - rebuilt api for `node:fs` -->
21<!-- - `lru` - least recently used cache -->
22<!-- - `mime` - trimmed database of mime types -->
23<!-- - `subprocess` - rebuilt api for `node:child_process` -->
24<!-- - `testing` -->
25
26- `assert`
27- `log` - logging, terminal i/o, interactive widgets
28- `log/ansi` - cursor and color constants
29- `log/headless` - use `log` and `log/progress` without a tty
30- `log/progress` - progress bar, spinner, and tree
31- `log/stack` - stack trace parse and formatter
32- `packaging` - workflows used for `lib`'s publications
33- `queue` - priority queue with rescheduling
34- `string`
35- `ts` typescript types and general purpose code
36
37these libraries are published as is, **without versioning or maintainance
38guarantees**. every version is a breaking major release. please upgrade your
39libraries with intent, considering api changes in [readme.changes.md].
40individual modules may annotate their intended api and usage stability.
41
42keep in mind there are [known bugs and missing features] in this release.
43
44to contribute, please send bug reports or patches to <dev@paperclover.net>. new
45features may not be accepted if they are complex and don't further my use cases.
46
47[readme.changes.md]: ./readme.changes.md
48[known bugs and missing features]: https://git.paperclover.net/clo/sitegen/issues?state=open&labels=4
49[clover's typescript library]: https://git.paperclover.net/clo/sitegen/src/branch/master/lib
lib/string.ts+5
...@@ -1,3 +1,8 @@...@@ -1,3 +1,8 @@
1/** Escape a value for use in a regular expression. */
2export function escapeRegExp(unsafeText: string) {
3 return unsafeText.replace(/[\$\\]/g, "\\$&");
4}
5
1// TODO: this function is not benchmarked6// TODO: this function is not benchmarked
2export function countNewlines(str: string): number {7export function countNewlines(str: string): number {
3 let count = 0;8 let count = 0;
package-lock.json+505-465
...@@ -5,45 +5,31 @@...@@ -5,45 +5,31 @@
5 "packages": {5 "packages": {
6 "": {6 "": {
7 "dependencies": {7 "dependencies": {
8 "@hono/node-server": "^1.14.3",8 "@hono/node-server": "1.14.3",
9 "@mdx-js/mdx": "^3.1.0",9 "@mdx-js/mdx": "3.1.0",
10 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",10 "codemirror": "6.0.1",
11 "blurhash": "^2.0.5",11 "dashjs": "^5.0.3",
12 "codemirror": "^6.0.1",12 "devalue": "^5.3.2",
13 "devalue": "^5.1.1",13 "esbuild": "0.25.5",
14 "esbuild": "^0.25.5",14 "hls.js": "1.6.5",
15 "hls.js": "^1.6.5",15 "hono": "4.9.9",
16 "hono": "^4.7.11",
17 "lib": "file:lib",16 "lib": "file:lib",
18 "marko": "^6.0.20",17 "marko": "6.0.82",
19 "msgpackr": "^1.11.5",18 "msgpackr": "1.11.5",
20 "puppeteer": "^24.10.1",19 "puppeteer": "24.23.0",
21 "sharp": "^0.34.2",20 "sharp": "0.34.2",
22 "source-map-support": "^0.5.21",21 "source-map-support": "0.5.21",
23 "ts-to-jsdoc": "^2.4.0",22 "ts-to-jsdoc": "2.4.0",
24 "unique-names-generator": "^4.7.1",23 "unique-names-generator": "4.7.1",
25 "vscode-oniguruma": "^2.0.1",24 "vscode-oniguruma": "2.0.1",
26 "vscode-textmate": "^9.2.0"25 "vscode-textmate": "9.2.0"
27 },26 },
28 "devDependencies": {27 "devDependencies": {
29 "@types/node": "^24.0.10",28 "@types/node": "24.0.10",
30 "typescript": "^5.8.3"29 "typescript": "5.8.3"
31 }30 }
32 },31 },
33 "lib": {},32 "lib": {},
34 "node_modules/@ampproject/remapping": {
35 "version": "2.3.0",
36 "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
37 "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
38 "license": "Apache-2.0",
39 "dependencies": {
40 "@jridgewell/gen-mapping": "^0.3.5",
41 "@jridgewell/trace-mapping": "^0.3.24"
42 },
43 "engines": {
44 "node": ">=6.0.0"
45 }
46 },
47 "node_modules/@babel/code-frame": {33 "node_modules/@babel/code-frame": {
48 "version": "7.27.1",34 "version": "7.27.1",
49 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",35 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
...@@ -59,30 +45,30 @@...@@ -59,30 +45,30 @@
59 }45 }
60 },46 },
61 "node_modules/@babel/compat-data": {47 "node_modules/@babel/compat-data": {
62 "version": "7.28.0",48 "version": "7.28.4",
63 "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",49 "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
64 "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",50 "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
65 "license": "MIT",51 "license": "MIT",
66 "engines": {52 "engines": {
67 "node": ">=6.9.0"53 "node": ">=6.9.0"
68 }54 }
69 },55 },
70 "node_modules/@babel/core": {56 "node_modules/@babel/core": {
71 "version": "7.28.0",57 "version": "7.28.4",
72 "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",58 "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
73 "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",59 "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
74 "license": "MIT",60 "license": "MIT",
75 "dependencies": {61 "dependencies": {
76 "@ampproject/remapping": "^2.2.0",
77 "@babel/code-frame": "^7.27.1",62 "@babel/code-frame": "^7.27.1",
78 "@babel/generator": "^7.28.0",63 "@babel/generator": "^7.28.3",
79 "@babel/helper-compilation-targets": "^7.27.2",64 "@babel/helper-compilation-targets": "^7.27.2",
80 "@babel/helper-module-transforms": "^7.27.3",65 "@babel/helper-module-transforms": "^7.28.3",
81 "@babel/helpers": "^7.27.6",66 "@babel/helpers": "^7.28.4",
82 "@babel/parser": "^7.28.0",67 "@babel/parser": "^7.28.4",
83 "@babel/template": "^7.27.2",68 "@babel/template": "^7.27.2",
84 "@babel/traverse": "^7.28.0",69 "@babel/traverse": "^7.28.4",
85 "@babel/types": "^7.28.0",70 "@babel/types": "^7.28.4",
71 "@jridgewell/remapping": "^2.3.5",
86 "convert-source-map": "^2.0.0",72 "convert-source-map": "^2.0.0",
87 "debug": "^4.1.0",73 "debug": "^4.1.0",
88 "gensync": "^1.0.0-beta.2",74 "gensync": "^1.0.0-beta.2",
...@@ -98,13 +84,13 @@...@@ -98,13 +84,13 @@
98 }84 }
99 },85 },
100 "node_modules/@babel/generator": {86 "node_modules/@babel/generator": {
101 "version": "7.28.0",87 "version": "7.28.3",
102 "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",88 "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
103 "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",89 "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
104 "license": "MIT",90 "license": "MIT",
105 "dependencies": {91 "dependencies": {
106 "@babel/parser": "^7.28.0",92 "@babel/parser": "^7.28.3",
107 "@babel/types": "^7.28.0",93 "@babel/types": "^7.28.2",
108 "@jridgewell/gen-mapping": "^0.3.12",94 "@jridgewell/gen-mapping": "^0.3.12",
109 "@jridgewell/trace-mapping": "^0.3.28",95 "@jridgewell/trace-mapping": "^0.3.28",
110 "jsesc": "^3.0.2"96 "jsesc": "^3.0.2"
...@@ -142,17 +128,17 @@...@@ -142,17 +128,17 @@
142 }128 }
143 },129 },
144 "node_modules/@babel/helper-create-class-features-plugin": {130 "node_modules/@babel/helper-create-class-features-plugin": {
145 "version": "7.27.1",131 "version": "7.28.3",
146 "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",132 "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz",
147 "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",133 "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==",
148 "license": "MIT",134 "license": "MIT",
149 "dependencies": {135 "dependencies": {
150 "@babel/helper-annotate-as-pure": "^7.27.1",136 "@babel/helper-annotate-as-pure": "^7.27.3",
151 "@babel/helper-member-expression-to-functions": "^7.27.1",137 "@babel/helper-member-expression-to-functions": "^7.27.1",
152 "@babel/helper-optimise-call-expression": "^7.27.1",138 "@babel/helper-optimise-call-expression": "^7.27.1",
153 "@babel/helper-replace-supers": "^7.27.1",139 "@babel/helper-replace-supers": "^7.27.1",
154 "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",140 "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
155 "@babel/traverse": "^7.27.1",141 "@babel/traverse": "^7.28.3",
156 "semver": "^6.3.1"142 "semver": "^6.3.1"
157 },143 },
158 "engines": {144 "engines": {
...@@ -198,14 +184,14 @@...@@ -198,14 +184,14 @@
198 }184 }
199 },185 },
200 "node_modules/@babel/helper-module-transforms": {186 "node_modules/@babel/helper-module-transforms": {
201 "version": "7.27.3",187 "version": "7.28.3",
202 "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",188 "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
203 "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",189 "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
204 "license": "MIT",190 "license": "MIT",
205 "dependencies": {191 "dependencies": {
206 "@babel/helper-module-imports": "^7.27.1",192 "@babel/helper-module-imports": "^7.27.1",
207 "@babel/helper-validator-identifier": "^7.27.1",193 "@babel/helper-validator-identifier": "^7.27.1",
208 "@babel/traverse": "^7.27.3"194 "@babel/traverse": "^7.28.3"
209 },195 },
210 "engines": {196 "engines": {
211 "node": ">=6.9.0"197 "node": ">=6.9.0"
...@@ -293,25 +279,25 @@...@@ -293,25 +279,25 @@
293 }279 }
294 },280 },
295 "node_modules/@babel/helpers": {281 "node_modules/@babel/helpers": {
296 "version": "7.27.6",282 "version": "7.28.4",
297 "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",283 "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
298 "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",284 "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
299 "license": "MIT",285 "license": "MIT",
300 "dependencies": {286 "dependencies": {
301 "@babel/template": "^7.27.2",287 "@babel/template": "^7.27.2",
302 "@babel/types": "^7.27.6"288 "@babel/types": "^7.28.4"
303 },289 },
304 "engines": {290 "engines": {
305 "node": ">=6.9.0"291 "node": ">=6.9.0"
306 }292 }
307 },293 },
308 "node_modules/@babel/parser": {294 "node_modules/@babel/parser": {
309 "version": "7.28.0",295 "version": "7.28.4",
310 "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",296 "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
311 "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",297 "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
312 "license": "MIT",298 "license": "MIT",
313 "dependencies": {299 "dependencies": {
314 "@babel/types": "^7.28.0"300 "@babel/types": "^7.28.4"
315 },301 },
316 "bin": {302 "bin": {
317 "parser": "bin/babel-parser.js"303 "parser": "bin/babel-parser.js"
...@@ -371,9 +357,9 @@...@@ -371,9 +357,9 @@
371 }357 }
372 },358 },
373 "node_modules/@babel/runtime": {359 "node_modules/@babel/runtime": {
374 "version": "7.27.6",360 "version": "7.28.4",
375 "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",361 "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
376 "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",362 "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
377 "license": "MIT",363 "license": "MIT",
378 "engines": {364 "engines": {
379 "node": ">=6.9.0"365 "node": ">=6.9.0"
...@@ -394,17 +380,17 @@...@@ -394,17 +380,17 @@
394 }380 }
395 },381 },
396 "node_modules/@babel/traverse": {382 "node_modules/@babel/traverse": {
397 "version": "7.28.0",383 "version": "7.28.4",
398 "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",384 "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
399 "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",385 "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
400 "license": "MIT",386 "license": "MIT",
401 "dependencies": {387 "dependencies": {
402 "@babel/code-frame": "^7.27.1",388 "@babel/code-frame": "^7.27.1",
403 "@babel/generator": "^7.28.0",389 "@babel/generator": "^7.28.3",
404 "@babel/helper-globals": "^7.28.0",390 "@babel/helper-globals": "^7.28.0",
405 "@babel/parser": "^7.28.0",391 "@babel/parser": "^7.28.4",
406 "@babel/template": "^7.27.2",392 "@babel/template": "^7.27.2",
407 "@babel/types": "^7.28.0",393 "@babel/types": "^7.28.4",
408 "debug": "^4.3.1"394 "debug": "^4.3.1"
409 },395 },
410 "engines": {396 "engines": {
...@@ -412,9 +398,9 @@...@@ -412,9 +398,9 @@
412 }398 }
413 },399 },
414 "node_modules/@babel/types": {400 "node_modules/@babel/types": {
415 "version": "7.28.0",401 "version": "7.28.4",
416 "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",402 "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
417 "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",403 "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
418 "license": "MIT",404 "license": "MIT",
419 "dependencies": {405 "dependencies": {
420 "@babel/helper-string-parser": "^7.27.1",406 "@babel/helper-string-parser": "^7.27.1",
...@@ -516,9 +502,9 @@...@@ -516,9 +502,9 @@
516 }502 }
517 },503 },
518 "node_modules/@esbuild/aix-ppc64": {504 "node_modules/@esbuild/aix-ppc64": {
519 "version": "0.25.6",505 "version": "0.25.5",
520 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",506 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
521 "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",507 "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
522 "cpu": [508 "cpu": [
523 "ppc64"509 "ppc64"
524 ],510 ],
...@@ -532,9 +518,9 @@...@@ -532,9 +518,9 @@
532 }518 }
533 },519 },
534 "node_modules/@esbuild/android-arm": {520 "node_modules/@esbuild/android-arm": {
535 "version": "0.25.6",521 "version": "0.25.5",
536 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",522 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
537 "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",523 "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
538 "cpu": [524 "cpu": [
539 "arm"525 "arm"
540 ],526 ],
...@@ -548,9 +534,9 @@...@@ -548,9 +534,9 @@
548 }534 }
549 },535 },
550 "node_modules/@esbuild/android-arm64": {536 "node_modules/@esbuild/android-arm64": {
551 "version": "0.25.6",537 "version": "0.25.5",
552 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",538 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
553 "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",539 "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
554 "cpu": [540 "cpu": [
555 "arm64"541 "arm64"
556 ],542 ],
...@@ -564,9 +550,9 @@...@@ -564,9 +550,9 @@
564 }550 }
565 },551 },
566 "node_modules/@esbuild/android-x64": {552 "node_modules/@esbuild/android-x64": {
567 "version": "0.25.6",553 "version": "0.25.5",
568 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",554 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
569 "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",555 "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
570 "cpu": [556 "cpu": [
571 "x64"557 "x64"
572 ],558 ],
...@@ -580,9 +566,9 @@...@@ -580,9 +566,9 @@
580 }566 }
581 },567 },
582 "node_modules/@esbuild/darwin-arm64": {568 "node_modules/@esbuild/darwin-arm64": {
583 "version": "0.25.6",569 "version": "0.25.5",
584 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",570 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
585 "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",571 "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
586 "cpu": [572 "cpu": [
587 "arm64"573 "arm64"
588 ],574 ],
...@@ -596,9 +582,9 @@...@@ -596,9 +582,9 @@
596 }582 }
597 },583 },
598 "node_modules/@esbuild/darwin-x64": {584 "node_modules/@esbuild/darwin-x64": {
599 "version": "0.25.6",585 "version": "0.25.5",
600 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",586 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
601 "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",587 "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
602 "cpu": [588 "cpu": [
603 "x64"589 "x64"
604 ],590 ],
...@@ -612,9 +598,9 @@...@@ -612,9 +598,9 @@
612 }598 }
613 },599 },
614 "node_modules/@esbuild/freebsd-arm64": {600 "node_modules/@esbuild/freebsd-arm64": {
615 "version": "0.25.6",601 "version": "0.25.5",
616 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",602 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
617 "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",603 "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
618 "cpu": [604 "cpu": [
619 "arm64"605 "arm64"
620 ],606 ],
...@@ -628,9 +614,9 @@...@@ -628,9 +614,9 @@
628 }614 }
629 },615 },
630 "node_modules/@esbuild/freebsd-x64": {616 "node_modules/@esbuild/freebsd-x64": {
631 "version": "0.25.6",617 "version": "0.25.5",
632 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",618 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
633 "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",619 "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
634 "cpu": [620 "cpu": [
635 "x64"621 "x64"
636 ],622 ],
...@@ -644,9 +630,9 @@...@@ -644,9 +630,9 @@
644 }630 }
645 },631 },
646 "node_modules/@esbuild/linux-arm": {632 "node_modules/@esbuild/linux-arm": {
647 "version": "0.25.6",633 "version": "0.25.5",
648 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",634 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
649 "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",635 "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
650 "cpu": [636 "cpu": [
651 "arm"637 "arm"
652 ],638 ],
...@@ -660,9 +646,9 @@...@@ -660,9 +646,9 @@
660 }646 }
661 },647 },
662 "node_modules/@esbuild/linux-arm64": {648 "node_modules/@esbuild/linux-arm64": {
663 "version": "0.25.6",649 "version": "0.25.5",
664 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",650 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
665 "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",651 "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
666 "cpu": [652 "cpu": [
667 "arm64"653 "arm64"
668 ],654 ],
...@@ -676,9 +662,9 @@...@@ -676,9 +662,9 @@
676 }662 }
677 },663 },
678 "node_modules/@esbuild/linux-ia32": {664 "node_modules/@esbuild/linux-ia32": {
679 "version": "0.25.6",665 "version": "0.25.5",
680 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",666 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
681 "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",667 "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
682 "cpu": [668 "cpu": [
683 "ia32"669 "ia32"
684 ],670 ],
...@@ -692,9 +678,9 @@...@@ -692,9 +678,9 @@
692 }678 }
693 },679 },
694 "node_modules/@esbuild/linux-loong64": {680 "node_modules/@esbuild/linux-loong64": {
695 "version": "0.25.6",681 "version": "0.25.5",
696 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",682 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
697 "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",683 "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
698 "cpu": [684 "cpu": [
699 "loong64"685 "loong64"
700 ],686 ],
...@@ -708,9 +694,9 @@...@@ -708,9 +694,9 @@
708 }694 }
709 },695 },
710 "node_modules/@esbuild/linux-mips64el": {696 "node_modules/@esbuild/linux-mips64el": {
711 "version": "0.25.6",697 "version": "0.25.5",
712 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",698 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
713 "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",699 "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
714 "cpu": [700 "cpu": [
715 "mips64el"701 "mips64el"
716 ],702 ],
...@@ -724,9 +710,9 @@...@@ -724,9 +710,9 @@
724 }710 }
725 },711 },
726 "node_modules/@esbuild/linux-ppc64": {712 "node_modules/@esbuild/linux-ppc64": {
727 "version": "0.25.6",713 "version": "0.25.5",
728 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",714 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
729 "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",715 "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
730 "cpu": [716 "cpu": [
731 "ppc64"717 "ppc64"
732 ],718 ],
...@@ -740,9 +726,9 @@...@@ -740,9 +726,9 @@
740 }726 }
741 },727 },
742 "node_modules/@esbuild/linux-riscv64": {728 "node_modules/@esbuild/linux-riscv64": {
743 "version": "0.25.6",729 "version": "0.25.5",
744 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",730 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
745 "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",731 "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
746 "cpu": [732 "cpu": [
747 "riscv64"733 "riscv64"
748 ],734 ],
...@@ -756,9 +742,9 @@...@@ -756,9 +742,9 @@
756 }742 }
757 },743 },
758 "node_modules/@esbuild/linux-s390x": {744 "node_modules/@esbuild/linux-s390x": {
759 "version": "0.25.6",745 "version": "0.25.5",
760 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",746 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
761 "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",747 "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
762 "cpu": [748 "cpu": [
763 "s390x"749 "s390x"
764 ],750 ],
...@@ -772,9 +758,9 @@...@@ -772,9 +758,9 @@
772 }758 }
773 },759 },
774 "node_modules/@esbuild/linux-x64": {760 "node_modules/@esbuild/linux-x64": {
775 "version": "0.25.6",761 "version": "0.25.5",
776 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",762 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
777 "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",763 "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
778 "cpu": [764 "cpu": [
779 "x64"765 "x64"
780 ],766 ],
...@@ -788,9 +774,9 @@...@@ -788,9 +774,9 @@
788 }774 }
789 },775 },
790 "node_modules/@esbuild/netbsd-arm64": {776 "node_modules/@esbuild/netbsd-arm64": {
791 "version": "0.25.6",777 "version": "0.25.5",
792 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",778 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
793 "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",779 "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
794 "cpu": [780 "cpu": [
795 "arm64"781 "arm64"
796 ],782 ],
...@@ -804,9 +790,9 @@...@@ -804,9 +790,9 @@
804 }790 }
805 },791 },
806 "node_modules/@esbuild/netbsd-x64": {792 "node_modules/@esbuild/netbsd-x64": {
807 "version": "0.25.6",793 "version": "0.25.5",
808 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",794 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
809 "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",795 "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
810 "cpu": [796 "cpu": [
811 "x64"797 "x64"
812 ],798 ],
...@@ -820,9 +806,9 @@...@@ -820,9 +806,9 @@
820 }806 }
821 },807 },
822 "node_modules/@esbuild/openbsd-arm64": {808 "node_modules/@esbuild/openbsd-arm64": {
823 "version": "0.25.6",809 "version": "0.25.5",
824 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",810 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
825 "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",811 "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
826 "cpu": [812 "cpu": [
827 "arm64"813 "arm64"
828 ],814 ],
...@@ -836,9 +822,9 @@...@@ -836,9 +822,9 @@
836 }822 }
837 },823 },
838 "node_modules/@esbuild/openbsd-x64": {824 "node_modules/@esbuild/openbsd-x64": {
839 "version": "0.25.6",825 "version": "0.25.5",
840 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",826 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
841 "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",827 "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
842 "cpu": [828 "cpu": [
843 "x64"829 "x64"
844 ],830 ],
...@@ -851,26 +837,10 @@...@@ -851,26 +837,10 @@
851 "node": ">=18"837 "node": ">=18"
852 }838 }
853 },839 },
854 "node_modules/@esbuild/openharmony-arm64": {
855 "version": "0.25.6",
856 "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz",
857 "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==",
858 "cpu": [
859 "arm64"
860 ],
861 "license": "MIT",
862 "optional": true,
863 "os": [
864 "openharmony"
865 ],
866 "engines": {
867 "node": ">=18"
868 }
869 },
870 "node_modules/@esbuild/sunos-x64": {840 "node_modules/@esbuild/sunos-x64": {
871 "version": "0.25.6",841 "version": "0.25.5",
872 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",842 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
873 "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",843 "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
874 "cpu": [844 "cpu": [
875 "x64"845 "x64"
876 ],846 ],
...@@ -884,9 +854,9 @@...@@ -884,9 +854,9 @@
884 }854 }
885 },855 },
886 "node_modules/@esbuild/win32-arm64": {856 "node_modules/@esbuild/win32-arm64": {
887 "version": "0.25.6",857 "version": "0.25.5",
888 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",858 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
889 "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",859 "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
890 "cpu": [860 "cpu": [
891 "arm64"861 "arm64"
892 ],862 ],
...@@ -900,9 +870,9 @@...@@ -900,9 +870,9 @@
900 }870 }
901 },871 },
902 "node_modules/@esbuild/win32-ia32": {872 "node_modules/@esbuild/win32-ia32": {
903 "version": "0.25.6",873 "version": "0.25.5",
904 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",874 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
905 "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",875 "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
906 "cpu": [876 "cpu": [
907 "ia32"877 "ia32"
908 ],878 ],
...@@ -916,9 +886,9 @@...@@ -916,9 +886,9 @@
916 }886 }
917 },887 },
918 "node_modules/@esbuild/win32-x64": {888 "node_modules/@esbuild/win32-x64": {
919 "version": "0.25.6",889 "version": "0.25.5",
920 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",890 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
921 "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",891 "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
922 "cpu": [892 "cpu": [
923 "x64"893 "x64"
924 ],894 ],
...@@ -932,9 +902,9 @@...@@ -932,9 +902,9 @@
932 }902 }
933 },903 },
934 "node_modules/@hono/node-server": {904 "node_modules/@hono/node-server": {
935 "version": "1.15.0",905 "version": "1.14.3",
936 "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.15.0.tgz",906 "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.3.tgz",
937 "integrity": "sha512-MjmK4l5N4dQpZ9OSWN0tCj7ejuc7WvuWMzSKtc89bnknJykAeHxzRigXBTYZk85H6Awrii6RM59iUiUluApu2A==",907 "integrity": "sha512-KuDMwwghtFYSmIpr4WrKs1VpelTrptvJ+6x6mbUcZnFcc213cumTF5BdqfHyW93B19TNI4Vaev14vOI2a0Ie3w==",
938 "license": "MIT",908 "license": "MIT",
939 "engines": {909 "engines": {
940 "node": ">=18.14.1"910 "node": ">=18.14.1"
...@@ -1340,15 +1310,25 @@...@@ -1340,15 +1310,25 @@
1340 }1310 }
1341 },1311 },
1342 "node_modules/@jridgewell/gen-mapping": {1312 "node_modules/@jridgewell/gen-mapping": {
1343 "version": "0.3.12",1313 "version": "0.3.13",
1344 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",1314 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
1345 "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",1315 "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
1346 "license": "MIT",1316 "license": "MIT",
1347 "dependencies": {1317 "dependencies": {
1348 "@jridgewell/sourcemap-codec": "^1.5.0",1318 "@jridgewell/sourcemap-codec": "^1.5.0",
1349 "@jridgewell/trace-mapping": "^0.3.24"1319 "@jridgewell/trace-mapping": "^0.3.24"
1350 }1320 }
1351 },1321 },
1322 "node_modules/@jridgewell/remapping": {
1323 "version": "2.3.5",
1324 "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
1325 "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
1326 "license": "MIT",
1327 "dependencies": {
1328 "@jridgewell/gen-mapping": "^0.3.5",
1329 "@jridgewell/trace-mapping": "^0.3.24"
1330 }
1331 },
1352 "node_modules/@jridgewell/resolve-uri": {1332 "node_modules/@jridgewell/resolve-uri": {
1353 "version": "3.1.2",1333 "version": "3.1.2",
1354 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",1334 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
...@@ -1365,9 +1345,9 @@...@@ -1365,9 +1345,9 @@
1365 "license": "MIT"1345 "license": "MIT"
1366 },1346 },
1367 "node_modules/@jridgewell/trace-mapping": {1347 "node_modules/@jridgewell/trace-mapping": {
1368 "version": "0.3.29",1348 "version": "0.3.31",
1369 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",1349 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
1370 "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",1350 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
1371 "license": "MIT",1351 "license": "MIT",
1372 "dependencies": {1352 "dependencies": {
1373 "@jridgewell/resolve-uri": "^3.1.0",1353 "@jridgewell/resolve-uri": "^3.1.0",
...@@ -1414,25 +1394,25 @@...@@ -1414,25 +1394,25 @@
1414 "license": "MIT"1394 "license": "MIT"
1415 },1395 },
1416 "node_modules/@marko/compiler": {1396 "node_modules/@marko/compiler": {
1417 "version": "5.39.28",1397 "version": "5.39.40",
1418 "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.28.tgz",1398 "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.40.tgz",
1419 "integrity": "sha512-jO/GGNWwyHlCE/fAhiLc1+Xc3apU73wfmjqBFiQmFHyAT2KZRyKJbPz7aVs8uutrPY8Xks9ZCXiXOk0RgvUlvg==",1399 "integrity": "sha512-gQSdfJ5XOy01DtP7ULiqI1onp7Cix1QvnjM0KbfR2HMrWNI14V8V/2JguFAm3BzKEMZmmgvRLk0rvBzSm9FP6Q==",
1420 "license": "MIT",1400 "license": "MIT",
1421 "dependencies": {1401 "dependencies": {
1422 "@babel/code-frame": "^7.26.2",1402 "@babel/code-frame": "^7.27.1",
1423 "@babel/core": "^7.26.10",1403 "@babel/core": "^7.28.0",
1424 "@babel/generator": "^7.27.0",1404 "@babel/generator": "^7.28.0",
1425 "@babel/parser": "^7.27.0",1405 "@babel/parser": "^7.28.0",
1426 "@babel/plugin-syntax-typescript": "^7.25.9",1406 "@babel/plugin-syntax-typescript": "^7.27.1",
1427 "@babel/plugin-transform-modules-commonjs": "^7.26.3",1407 "@babel/plugin-transform-modules-commonjs": "^7.27.1",
1428 "@babel/plugin-transform-typescript": "^7.27.0",1408 "@babel/plugin-transform-typescript": "^7.28.0",
1429 "@babel/runtime": "^7.27.0",1409 "@babel/runtime": "^7.28.2",
1430 "@babel/traverse": "^7.27.0",1410 "@babel/traverse": "^7.28.0",
1431 "@babel/types": "^7.27.0",1411 "@babel/types": "^7.28.2",
1432 "@luxass/strip-json-comments": "^1.4.0",1412 "@luxass/strip-json-comments": "^1.4.0",
1433 "complain": "^1.6.1",1413 "complain": "^1.6.1",
1434 "he": "^1.2.0",1414 "he": "^1.2.0",
1435 "htmljs-parser": "^5.5.4",1415 "htmljs-parser": "^5.7.3",
1436 "jsesc": "^3.1.0",1416 "jsesc": "^3.1.0",
1437 "kleur": "^4.1.5",1417 "kleur": "^4.1.5",
1438 "lasso-package-root": "^1.0.1",1418 "lasso-package-root": "^1.0.1",
...@@ -1596,26 +1576,18 @@...@@ -1596,26 +1576,18 @@
1596 "node": ">= 8"1576 "node": ">= 8"
1597 }1577 }
1598 },1578 },
1599 "node_modules/@paperclover/console": {
1600 "resolved": "git+https://git.paperclover.net/clo/console.git#1a6ac2b79fdd8a21a1c57d25723975872bc07e3e",
1601 "dependencies": {
1602 "ansi-escapes": "^7.0.0",
1603 "chalk": "^5.4.1",
1604 "strip-ansi": "^7.1.0"
1605 }
1606 },
1607 "node_modules/@puppeteer/browsers": {1579 "node_modules/@puppeteer/browsers": {
1608 "version": "2.10.5",1580 "version": "2.10.10",
1609 "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",1581 "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.10.tgz",
1610 "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",1582 "integrity": "sha512-3ZG500+ZeLql8rE0hjfhkycJjDj0pI/btEh3L9IkWUYcOrgP0xCNRq3HbtbqOPbvDhFaAWD88pDFtlLv8ns8gA==",
1611 "license": "Apache-2.0",1583 "license": "Apache-2.0",
1612 "dependencies": {1584 "dependencies": {
1613 "debug": "^4.4.1",1585 "debug": "^4.4.3",
1614 "extract-zip": "^2.0.1",1586 "extract-zip": "^2.0.1",
1615 "progress": "^2.0.3",1587 "progress": "^2.0.3",
1616 "proxy-agent": "^6.5.0",1588 "proxy-agent": "^6.5.0",
1617 "semver": "^7.7.2",1589 "semver": "^7.7.2",
1618 "tar-fs": "^3.0.8",1590 "tar-fs": "^3.1.0",
1619 "yargs": "^17.7.2"1591 "yargs": "^17.7.2"
1620 },1592 },
1621 "bin": {1593 "bin": {
...@@ -1637,6 +1609,12 @@...@@ -1637,6 +1609,12 @@
1637 "node": ">=10"1609 "node": ">=10"
1638 }1610 }
1639 },1611 },
1612 "node_modules/@svta/common-media-library": {
1613 "version": "0.12.4",
1614 "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz",
1615 "integrity": "sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==",
1616 "license": "Apache-2.0"
1617 },
1640 "node_modules/@tootallnate/quickjs-emscripten": {1618 "node_modules/@tootallnate/quickjs-emscripten": {
1641 "version": "0.23.0",1619 "version": "0.23.0",
1642 "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",1620 "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
...@@ -1710,9 +1688,9 @@...@@ -1710,9 +1688,9 @@
1710 "license": "MIT"1688 "license": "MIT"
1711 },1689 },
1712 "node_modules/@types/node": {1690 "node_modules/@types/node": {
1713 "version": "24.0.12",1691 "version": "24.0.10",
1714 "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",1692 "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
1715 "integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",1693 "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
1716 "devOptional": true,1694 "devOptional": true,
1717 "license": "MIT",1695 "license": "MIT",
1718 "dependencies": {1696 "dependencies": {
...@@ -1771,31 +1749,13 @@...@@ -1771,31 +1749,13 @@
1771 "node": ">= 14"1749 "node": ">= 14"
1772 }1750 }
1773 },1751 },
1774 "node_modules/ansi-escapes": {
1775 "version": "7.0.0",
1776 "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
1777 "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
1778 "license": "MIT",
1779 "dependencies": {
1780 "environment": "^1.0.0"
1781 },
1782 "engines": {
1783 "node": ">=18"
1784 },
1785 "funding": {
1786 "url": "https://github.com/sponsors/sindresorhus"
1787 }
1788 },
1789 "node_modules/ansi-regex": {1752 "node_modules/ansi-regex": {
1790 "version": "6.1.0",1753 "version": "5.0.1",
1791 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",1754 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1792 "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",1755 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1793 "license": "MIT",1756 "license": "MIT",
1794 "engines": {1757 "engines": {
1795 "node": ">=12"1758 "node": ">=8"
1796 },
1797 "funding": {
1798 "url": "https://github.com/chalk/ansi-regex?sponsor=1"
1799 }1759 }
1800 },1760 },
1801 "node_modules/ansi-styles": {1761 "node_modules/ansi-styles": {
...@@ -1847,10 +1807,18 @@...@@ -1847,10 +1807,18 @@
1847 }1807 }
1848 },1808 },
1849 "node_modules/b4a": {1809 "node_modules/b4a": {
1850 "version": "1.6.7",1810 "version": "1.7.3",
1851 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",1811 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
1852 "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",1812 "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
1853 "license": "Apache-2.0"1813 "license": "Apache-2.0",
1814 "peerDependencies": {
1815 "react-native-b4a": "*"
1816 },
1817 "peerDependenciesMeta": {
1818 "react-native-b4a": {
1819 "optional": true
1820 }
1821 }
1854 },1822 },
1855 "node_modules/bail": {1823 "node_modules/bail": {
1856 "version": "2.0.2",1824 "version": "2.0.2",
...@@ -1869,22 +1837,23 @@...@@ -1869,22 +1837,23 @@
1869 "license": "MIT"1837 "license": "MIT"
1870 },1838 },
1871 "node_modules/bare-events": {1839 "node_modules/bare-events": {
1872 "version": "2.6.0",1840 "version": "2.7.0",
1873 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz",1841 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz",
1874 "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==",1842 "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==",
1875 "license": "Apache-2.0",1843 "license": "Apache-2.0"
1876 "optional": true
1877 },1844 },
1878 "node_modules/bare-fs": {1845 "node_modules/bare-fs": {
1879 "version": "4.1.6",1846 "version": "4.4.5",
1880 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz",1847 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.5.tgz",
1881 "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==",1848 "integrity": "sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==",
1882 "license": "Apache-2.0",1849 "license": "Apache-2.0",
1883 "optional": true,1850 "optional": true,
1884 "dependencies": {1851 "dependencies": {
1885 "bare-events": "^2.5.4",1852 "bare-events": "^2.5.4",
1886 "bare-path": "^3.0.0",1853 "bare-path": "^3.0.0",
1887 "bare-stream": "^2.6.4"1854 "bare-stream": "^2.6.4",
1855 "bare-url": "^2.2.2",
1856 "fast-fifo": "^1.3.2"
1888 },1857 },
1889 "engines": {1858 "engines": {
1890 "bare": ">=1.16.0"1859 "bare": ">=1.16.0"
...@@ -1899,9 +1868,9 @@...@@ -1899,9 +1868,9 @@
1899 }1868 }
1900 },1869 },
1901 "node_modules/bare-os": {1870 "node_modules/bare-os": {
1902 "version": "3.6.1",1871 "version": "3.6.2",
1903 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",1872 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
1904 "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",1873 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
1905 "license": "Apache-2.0",1874 "license": "Apache-2.0",
1906 "optional": true,1875 "optional": true,
1907 "engines": {1876 "engines": {
...@@ -1919,9 +1888,9 @@...@@ -1919,9 +1888,9 @@
1919 }1888 }
1920 },1889 },
1921 "node_modules/bare-stream": {1890 "node_modules/bare-stream": {
1922 "version": "2.6.5",1891 "version": "2.7.0",
1923 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",1892 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
1924 "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",1893 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
1925 "license": "Apache-2.0",1894 "license": "Apache-2.0",
1926 "optional": true,1895 "optional": true,
1927 "dependencies": {1896 "dependencies": {
...@@ -1940,6 +1909,25 @@...@@ -1940,6 +1909,25 @@
1940 }1909 }
1941 }1910 }
1942 },1911 },
1912 "node_modules/bare-url": {
1913 "version": "2.2.2",
1914 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz",
1915 "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==",
1916 "license": "Apache-2.0",
1917 "optional": true,
1918 "dependencies": {
1919 "bare-path": "^3.0.0"
1920 }
1921 },
1922 "node_modules/baseline-browser-mapping": {
1923 "version": "2.8.10",
1924 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz",
1925 "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==",
1926 "license": "Apache-2.0",
1927 "bin": {
1928 "baseline-browser-mapping": "dist/cli.js"
1929 }
1930 },
1943 "node_modules/basic-ftp": {1931 "node_modules/basic-ftp": {
1944 "version": "5.0.5",1932 "version": "5.0.5",
1945 "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",1933 "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
...@@ -1949,11 +1937,44 @@...@@ -1949,11 +1937,44 @@
1949 "node": ">=10.0.0"1937 "node": ">=10.0.0"
1950 }1938 }
1951 },1939 },
1952 "node_modules/blurhash": {1940 "node_modules/bcp-47": {
1953 "version": "2.0.5",1941 "version": "2.1.0",
1954 "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",1942 "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz",
1955 "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",1943 "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==",
1956 "license": "MIT"1944 "license": "MIT",
1945 "dependencies": {
1946 "is-alphabetical": "^2.0.0",
1947 "is-alphanumerical": "^2.0.0",
1948 "is-decimal": "^2.0.0"
1949 },
1950 "funding": {
1951 "type": "github",
1952 "url": "https://github.com/sponsors/wooorm"
1953 }
1954 },
1955 "node_modules/bcp-47-match": {
1956 "version": "2.0.3",
1957 "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
1958 "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==",
1959 "license": "MIT",
1960 "funding": {
1961 "type": "github",
1962 "url": "https://github.com/sponsors/wooorm"
1963 }
1964 },
1965 "node_modules/bcp-47-normalize": {
1966 "version": "2.3.0",
1967 "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz",
1968 "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==",
1969 "license": "MIT",
1970 "dependencies": {
1971 "bcp-47": "^2.0.0",
1972 "bcp-47-match": "^2.0.0"
1973 },
1974 "funding": {
1975 "type": "github",
1976 "url": "https://github.com/sponsors/wooorm"
1977 }
1957 },1978 },
1958 "node_modules/brace-expansion": {1979 "node_modules/brace-expansion": {
1959 "version": "2.0.2",1980 "version": "2.0.2",
...@@ -1977,9 +1998,9 @@...@@ -1977,9 +1998,9 @@
1977 }1998 }
1978 },1999 },
1979 "node_modules/browserslist": {2000 "node_modules/browserslist": {
1980 "version": "4.25.1",2001 "version": "4.26.3",
1981 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",2002 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
1982 "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",2003 "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
1983 "funding": [2004 "funding": [
1984 {2005 {
1985 "type": "opencollective",2006 "type": "opencollective",
...@@ -1996,9 +2017,10 @@...@@ -1996,9 +2017,10 @@
1996 ],2017 ],
1997 "license": "MIT",2018 "license": "MIT",
1998 "dependencies": {2019 "dependencies": {
1999 "caniuse-lite": "^1.0.30001726",2020 "baseline-browser-mapping": "^2.8.9",
2000 "electron-to-chromium": "^1.5.173",2021 "caniuse-lite": "^1.0.30001746",
2001 "node-releases": "^2.0.19",2022 "electron-to-chromium": "^1.5.227",
2023 "node-releases": "^2.0.21",
2002 "update-browserslist-db": "^1.1.3"2024 "update-browserslist-db": "^1.1.3"
2003 },2025 },
2004 "bin": {2026 "bin": {
...@@ -2033,9 +2055,9 @@...@@ -2033,9 +2055,9 @@
2033 }2055 }
2034 },2056 },
2035 "node_modules/caniuse-lite": {2057 "node_modules/caniuse-lite": {
2036 "version": "1.0.30001727",2058 "version": "1.0.30001747",
2037 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",2059 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz",
2038 "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",2060 "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==",
2039 "funding": [2061 "funding": [
2040 {2062 {
2041 "type": "opencollective",2063 "type": "opencollective",
...@@ -2062,18 +2084,6 @@...@@ -2062,18 +2084,6 @@
2062 "url": "https://github.com/sponsors/wooorm"2084 "url": "https://github.com/sponsors/wooorm"
2063 }2085 }
2064 },2086 },
2065 "node_modules/chalk": {
2066 "version": "5.4.1",
2067 "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
2068 "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
2069 "license": "MIT",
2070 "engines": {
2071 "node": "^12.17.0 || ^14.13 || >=16.0.0"
2072 },
2073 "funding": {
2074 "url": "https://github.com/chalk/chalk?sponsor=1"
2075 }
2076 },
2077 "node_modules/character-entities": {2087 "node_modules/character-entities": {
2078 "version": "2.0.2",2088 "version": "2.0.2",
2079 "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",2089 "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
...@@ -2115,9 +2125,9 @@...@@ -2115,9 +2125,9 @@
2115 }2125 }
2116 },2126 },
2117 "node_modules/chromium-bidi": {2127 "node_modules/chromium-bidi": {
2118 "version": "5.1.0",2128 "version": "9.1.0",
2119 "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",2129 "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz",
2120 "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",2130 "integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==",
2121 "license": "Apache-2.0",2131 "license": "Apache-2.0",
2122 "dependencies": {2132 "dependencies": {
2123 "mitt": "^3.0.1",2133 "mitt": "^3.0.1",
...@@ -2141,37 +2151,22 @@...@@ -2141,37 +2151,22 @@
2141 "node": ">=12"2151 "node": ">=12"
2142 }2152 }
2143 },2153 },
2144 "node_modules/cliui/node_modules/ansi-regex": {
2145 "version": "5.0.1",
2146 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2147 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2148 "license": "MIT",
2149 "engines": {
2150 "node": ">=8"
2151 }
2152 },
2153 "node_modules/cliui/node_modules/strip-ansi": {
2154 "version": "6.0.1",
2155 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2156 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2157 "license": "MIT",
2158 "dependencies": {
2159 "ansi-regex": "^5.0.1"
2160 },
2161 "engines": {
2162 "node": ">=8"
2163 }
2164 },
2165 "node_modules/code-block-writer": {2154 "node_modules/code-block-writer": {
2166 "version": "13.0.3",2155 "version": "13.0.3",
2167 "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",2156 "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
2168 "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",2157 "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
2169 "license": "MIT"2158 "license": "MIT"
2170 },2159 },
2160 "node_modules/codem-isoboxer": {
2161 "version": "0.3.10",
2162 "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz",
2163 "integrity": "sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==",
2164 "license": "MIT"
2165 },
2171 "node_modules/codemirror": {2166 "node_modules/codemirror": {
2172 "version": "6.0.2",2167 "version": "6.0.1",
2173 "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",2168 "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
2174 "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",2169 "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
2175 "license": "MIT",2170 "license": "MIT",
2176 "dependencies": {2171 "dependencies": {
2177 "@codemirror/autocomplete": "^6.0.0",2172 "@codemirror/autocomplete": "^6.0.0",
...@@ -2297,6 +2292,24 @@...@@ -2297,6 +2292,24 @@
2297 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",2292 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
2298 "license": "MIT"2293 "license": "MIT"
2299 },2294 },
2295 "node_modules/dashjs": {
2296 "version": "5.0.3",
2297 "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.0.3.tgz",
2298 "integrity": "sha512-TXndNnCUjFjF2nYBxDVba+hWRpVkadkQ8flLp7kHkem+5+wZTfRShJCnVkPUosmjS0YPE9fVNLbYPJxHBeQZvA==",
2299 "license": "BSD-3-Clause",
2300 "dependencies": {
2301 "@svta/common-media-library": "^0.12.4",
2302 "bcp-47-match": "^2.0.3",
2303 "bcp-47-normalize": "^2.3.0",
2304 "codem-isoboxer": "0.3.10",
2305 "fast-deep-equal": "3.1.3",
2306 "html-entities": "^2.5.2",
2307 "imsc": "^1.1.5",
2308 "localforage": "^1.10.0",
2309 "path-browserify": "^1.0.1",
2310 "ua-parser-js": "^1.0.37"
2311 }
2312 },
2300 "node_modules/data-uri-to-buffer": {2313 "node_modules/data-uri-to-buffer": {
2301 "version": "6.0.2",2314 "version": "6.0.2",
2302 "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",2315 "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
...@@ -2307,9 +2320,9 @@...@@ -2307,9 +2320,9 @@
2307 }2320 }
2308 },2321 },
2309 "node_modules/debug": {2322 "node_modules/debug": {
2310 "version": "4.4.1",2323 "version": "4.4.3",
2311 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",2324 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
2312 "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",2325 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
2313 "license": "MIT",2326 "license": "MIT",
2314 "dependencies": {2327 "dependencies": {
2315 "ms": "^2.1.3"2328 "ms": "^2.1.3"
...@@ -2388,15 +2401,15 @@...@@ -2388,15 +2401,15 @@
2388 }2401 }
2389 },2402 },
2390 "node_modules/devtools-protocol": {2403 "node_modules/devtools-protocol": {
2391 "version": "0.0.1464554",2404 "version": "0.0.1508733",
2392 "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz",2405 "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
2393 "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==",2406 "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
2394 "license": "BSD-3-Clause"2407 "license": "BSD-3-Clause"
2395 },2408 },
2396 "node_modules/electron-to-chromium": {2409 "node_modules/electron-to-chromium": {
2397 "version": "1.5.181",2410 "version": "1.5.229",
2398 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.181.tgz",2411 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.229.tgz",
2399 "integrity": "sha512-+ISMj8OIQ+0qEeDj14Rt8WwcTOiqHyAB+5bnK1K7xNNLjBJ4hRCQfUkw8RWtcLbfBzDwc15ZnKH0c7SNOfwiyA==",2412 "integrity": "sha512-cwhDcZKGcT/rEthLRJ9eBlMDkh1sorgsuk+6dpsehV0g9CABsIqBxU4rLRjG+d/U6pYU1s37A4lSKrVc5lSQYg==",
2400 "license": "ISC"2413 "license": "ISC"
2401 },2414 },
2402 "node_modules/emoji-regex": {2415 "node_modules/emoji-regex": {
...@@ -2423,18 +2436,6 @@...@@ -2423,18 +2436,6 @@
2423 "node": ">=6"2436 "node": ">=6"
2424 }2437 }
2425 },2438 },
2426 "node_modules/environment": {
2427 "version": "1.1.0",
2428 "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
2429 "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
2430 "license": "MIT",
2431 "engines": {
2432 "node": ">=18"
2433 },
2434 "funding": {
2435 "url": "https://github.com/sponsors/sindresorhus"
2436 }
2437 },
2438 "node_modules/error-ex": {2439 "node_modules/error-ex": {
2439 "version": "1.3.2",2440 "version": "1.3.2",
2440 "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",2441 "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
...@@ -2486,9 +2487,9 @@...@@ -2486,9 +2487,9 @@
2486 }2487 }
2487 },2488 },
2488 "node_modules/esbuild": {2489 "node_modules/esbuild": {
2489 "version": "0.25.6",2490 "version": "0.25.5",
2490 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",2491 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
2491 "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",2492 "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
2492 "hasInstallScript": true,2493 "hasInstallScript": true,
2493 "license": "MIT",2494 "license": "MIT",
2494 "bin": {2495 "bin": {
...@@ -2498,32 +2499,31 @@...@@ -2498,32 +2499,31 @@
2498 "node": ">=18"2499 "node": ">=18"
2499 },2500 },
2500 "optionalDependencies": {2501 "optionalDependencies": {
2501 "@esbuild/aix-ppc64": "0.25.6",2502 "@esbuild/aix-ppc64": "0.25.5",
2502 "@esbuild/android-arm": "0.25.6",2503 "@esbuild/android-arm": "0.25.5",
2503 "@esbuild/android-arm64": "0.25.6",2504 "@esbuild/android-arm64": "0.25.5",
2504 "@esbuild/android-x64": "0.25.6",2505 "@esbuild/android-x64": "0.25.5",
2505 "@esbuild/darwin-arm64": "0.25.6",2506 "@esbuild/darwin-arm64": "0.25.5",
2506 "@esbuild/darwin-x64": "0.25.6",2507 "@esbuild/darwin-x64": "0.25.5",
2507 "@esbuild/freebsd-arm64": "0.25.6",2508 "@esbuild/freebsd-arm64": "0.25.5",
2508 "@esbuild/freebsd-x64": "0.25.6",2509 "@esbuild/freebsd-x64": "0.25.5",
2509 "@esbuild/linux-arm": "0.25.6",2510 "@esbuild/linux-arm": "0.25.5",
2510 "@esbuild/linux-arm64": "0.25.6",2511 "@esbuild/linux-arm64": "0.25.5",
2511 "@esbuild/linux-ia32": "0.25.6",2512 "@esbuild/linux-ia32": "0.25.5",
2512 "@esbuild/linux-loong64": "0.25.6",2513 "@esbuild/linux-loong64": "0.25.5",
2513 "@esbuild/linux-mips64el": "0.25.6",2514 "@esbuild/linux-mips64el": "0.25.5",
2514 "@esbuild/linux-ppc64": "0.25.6",2515 "@esbuild/linux-ppc64": "0.25.5",
2515 "@esbuild/linux-riscv64": "0.25.6",2516 "@esbuild/linux-riscv64": "0.25.5",
2516 "@esbuild/linux-s390x": "0.25.6",2517 "@esbuild/linux-s390x": "0.25.5",
2517 "@esbuild/linux-x64": "0.25.6",2518 "@esbuild/linux-x64": "0.25.5",
2518 "@esbuild/netbsd-arm64": "0.25.6",2519 "@esbuild/netbsd-arm64": "0.25.5",
2519 "@esbuild/netbsd-x64": "0.25.6",2520 "@esbuild/netbsd-x64": "0.25.5",
2520 "@esbuild/openbsd-arm64": "0.25.6",2521 "@esbuild/openbsd-arm64": "0.25.5",
2521 "@esbuild/openbsd-x64": "0.25.6",2522 "@esbuild/openbsd-x64": "0.25.5",
2522 "@esbuild/openharmony-arm64": "0.25.6",2523 "@esbuild/sunos-x64": "0.25.5",
2523 "@esbuild/sunos-x64": "0.25.6",2524 "@esbuild/win32-arm64": "0.25.5",
2524 "@esbuild/win32-arm64": "0.25.6",2525 "@esbuild/win32-ia32": "0.25.5",
2525 "@esbuild/win32-ia32": "0.25.6",2526 "@esbuild/win32-x64": "0.25.5"
2526 "@esbuild/win32-x64": "0.25.6"
2527 }2527 }
2528 },2528 },
2529 "node_modules/escalade": {2529 "node_modules/escalade": {
...@@ -2688,6 +2688,15 @@...@@ -2688,6 +2688,15 @@
2688 "node": ">=0.10.0"2688 "node": ">=0.10.0"
2689 }2689 }
2690 },2690 },
2691 "node_modules/events-universal": {
2692 "version": "1.0.1",
2693 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
2694 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
2695 "license": "Apache-2.0",
2696 "dependencies": {
2697 "bare-events": "^2.7.0"
2698 }
2699 },
2691 "node_modules/extend": {2700 "node_modules/extend": {
2692 "version": "3.0.2",2701 "version": "3.0.2",
2693 "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",2702 "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
...@@ -2714,6 +2723,12 @@...@@ -2714,6 +2723,12 @@
2714 "@types/yauzl": "^2.9.1"2723 "@types/yauzl": "^2.9.1"
2715 }2724 }
2716 },2725 },
2726 "node_modules/fast-deep-equal": {
2727 "version": "3.1.3",
2728 "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
2729 "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
2730 "license": "MIT"
2731 },
2717 "node_modules/fast-fifo": {2732 "node_modules/fast-fifo": {
2718 "version": "1.3.2",2733 "version": "1.3.2",
2719 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",2734 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
...@@ -2903,9 +2918,9 @@...@@ -2903,9 +2918,9 @@
2903 }2918 }
2904 },2919 },
2905 "node_modules/hls.js": {2920 "node_modules/hls.js": {
2906 "version": "1.6.7",2921 "version": "1.6.5",
2907 "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.7.tgz",2922 "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.5.tgz",
2908 "integrity": "sha512-QW2fnwDGKGc9DwQUGLbmMOz8G48UZK7PVNJPcOUql1b8jubKx4/eMHNP5mGqr6tYlJNDG1g10Lx2U/qPzL6zwQ==",2923 "integrity": "sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ==",
2909 "license": "Apache-2.0"2924 "license": "Apache-2.0"
2910 },2925 },
2911 "node_modules/hono": {2926 "node_modules/hono": {
...@@ -2917,10 +2932,26 @@...@@ -2917,10 +2932,26 @@
2917 "node": ">=16.9.0"2932 "node": ">=16.9.0"
2918 }2933 }
2919 },2934 },
2935 "node_modules/html-entities": {
2936 "version": "2.6.0",
2937 "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
2938 "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
2939 "funding": [
2940 {
2941 "type": "github",
2942 "url": "https://github.com/sponsors/mdevils"
2943 },
2944 {
2945 "type": "patreon",
2946 "url": "https://patreon.com/mdevils"
2947 }
2948 ],
2949 "license": "MIT"
2950 },
2920 "node_modules/htmljs-parser": {2951 "node_modules/htmljs-parser": {
2921 "version": "5.5.4",2952 "version": "5.7.4",
2922 "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-5.5.4.tgz",2953 "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-5.7.4.tgz",
2923 "integrity": "sha512-qJiir3ViASVhGdNW4kwM4pQWvDtDLtjwaflEQDooiNzbOPIzoaIzmRFWweFl8GomVhJqXqX0jjrhDED3pmAdzg==",2954 "integrity": "sha512-PCGI7/Z10oBkpKKqbQ9Wgd0GZjSQ/dV7QlBjOmaqD1FkBsHWk730BW9mJCbnXfRbtZCdLh8M8IA8YeKPu9tGaQ==",
2924 "license": "MIT"2955 "license": "MIT"
2925 },2956 },
2926 "node_modules/http-proxy-agent": {2957 "node_modules/http-proxy-agent": {
...@@ -2949,6 +2980,12 @@...@@ -2949,6 +2980,12 @@
2949 "node": ">= 14"2980 "node": ">= 14"
2950 }2981 }
2951 },2982 },
2983 "node_modules/immediate": {
2984 "version": "3.0.6",
2985 "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
2986 "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
2987 "license": "MIT"
2988 },
2952 "node_modules/import-fresh": {2989 "node_modules/import-fresh": {
2953 "version": "3.3.1",2990 "version": "3.3.1",
2954 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",2991 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
...@@ -2974,6 +3011,15 @@...@@ -2974,6 +3011,15 @@
2974 "node": ">=4"3011 "node": ">=4"
2975 }3012 }
2976 },3013 },
3014 "node_modules/imsc": {
3015 "version": "1.1.5",
3016 "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz",
3017 "integrity": "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==",
3018 "license": "BSD-2-Clause",
3019 "dependencies": {
3020 "sax": "1.2.1"
3021 }
3022 },
2977 "node_modules/inline-style-parser": {3023 "node_modules/inline-style-parser": {
2978 "version": "0.2.4",3024 "version": "0.2.4",
2979 "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",3025 "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
...@@ -2981,14 +3027,10 @@...@@ -2981,14 +3027,10 @@
2981 "license": "MIT"3027 "license": "MIT"
2982 },3028 },
2983 "node_modules/ip-address": {3029 "node_modules/ip-address": {
2984 "version": "9.0.5",3030 "version": "10.0.1",
2985 "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",3031 "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
2986 "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",3032 "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
2987 "license": "MIT",3033 "license": "MIT",
2988 "dependencies": {
2989 "jsbn": "1.1.0",
2990 "sprintf-js": "^1.1.3"
2991 },
2992 "engines": {3034 "engines": {
2993 "node": ">= 12"3035 "node": ">= 12"
2994 }3036 }
...@@ -3112,12 +3154,6 @@...@@ -3112,12 +3154,6 @@
3112 "js-yaml": "bin/js-yaml.js"3154 "js-yaml": "bin/js-yaml.js"
3113 }3155 }
3114 },3156 },
3115 "node_modules/jsbn": {
3116 "version": "1.1.0",
3117 "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
3118 "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
3119 "license": "MIT"
3120 },
3121 "node_modules/jsesc": {3157 "node_modules/jsesc": {
3122 "version": "3.1.0",3158 "version": "3.1.0",
3123 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",3159 "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
...@@ -3179,12 +3215,30 @@...@@ -3179,12 +3215,30 @@
3179 "resolved": "lib",3215 "resolved": "lib",
3180 "link": true3216 "link": true
3181 },3217 },
3218 "node_modules/lie": {
3219 "version": "3.1.1",
3220 "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
3221 "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
3222 "license": "MIT",
3223 "dependencies": {
3224 "immediate": "~3.0.5"
3225 }
3226 },
3182 "node_modules/lines-and-columns": {3227 "node_modules/lines-and-columns": {
3183 "version": "1.2.4",3228 "version": "1.2.4",
3184 "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",3229 "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
3185 "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",3230 "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
3186 "license": "MIT"3231 "license": "MIT"
3187 },3232 },
3233 "node_modules/localforage": {
3234 "version": "1.10.0",
3235 "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
3236 "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
3237 "license": "Apache-2.0",
3238 "dependencies": {
3239 "lie": "3.1.1"
3240 }
3241 },
3188 "node_modules/longest-streak": {3242 "node_modules/longest-streak": {
3189 "version": "3.1.0",3243 "version": "3.1.0",
3190 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",3244 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
...@@ -3226,12 +3280,12 @@...@@ -3226,12 +3280,12 @@
3226 }3280 }
3227 },3281 },
3228 "node_modules/marko": {3282 "node_modules/marko": {
3229 "version": "6.0.35",3283 "version": "6.0.82",
3230 "resolved": "https://registry.npmjs.org/marko/-/marko-6.0.35.tgz",3284 "resolved": "https://registry.npmjs.org/marko/-/marko-6.0.82.tgz",
3231 "integrity": "sha512-1zoMBeMEfltQ8QtmW0WoYYsjC0vUJaomQPpXQSlDw9i1o7YZ6T9v8ZSPcScv4ODRM1+XQ0hfNuUT+u7qijXcsA==",3285 "integrity": "sha512-xnBm299wmqrK8++12dhGHBUonm2SZgQVPNhHZjfp3Wc7k4U/qPbKrc7H1DJgU7JrYv8sSdxkconisynDqBYC1g==",
3232 "license": "MIT",3286 "license": "MIT",
3233 "dependencies": {3287 "dependencies": {
3234 "@marko/compiler": "^5.39.28",3288 "@marko/compiler": "^5.39.40",
3235 "csstype": "^3.1.3",3289 "csstype": "^3.1.3",
3236 "magic-string": "^0.30.17"3290 "magic-string": "^0.30.17"
3237 },3291 },
...@@ -4125,9 +4179,9 @@...@@ -4125,9 +4179,9 @@
4125 }4179 }
4126 },4180 },
4127 "node_modules/node-releases": {4181 "node_modules/node-releases": {
4128 "version": "2.0.19",4182 "version": "2.0.21",
4129 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",4183 "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
4130 "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",4184 "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
4131 "license": "MIT"4185 "license": "MIT"
4132 },4186 },
4133 "node_modules/once": {4187 "node_modules/once": {
...@@ -4320,17 +4374,17 @@...@@ -4320,17 +4374,17 @@
4320 }4374 }
4321 },4375 },
4322 "node_modules/puppeteer": {4376 "node_modules/puppeteer": {
4323 "version": "24.12.1",4377 "version": "24.23.0",
4324 "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.12.1.tgz",4378 "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.23.0.tgz",
4325 "integrity": "sha512-+vvwl+Xo4z5uXLLHG+XW8uXnUXQ62oY6KU6bEFZJvHWLutbmv5dw9A/jcMQ0fqpQdLydHmK0Uy7/9Ilj8ufwSQ==",4379 "integrity": "sha512-BVR1Lg8sJGKXY79JARdIssFWK2F6e1j+RyuJP66w4CUmpaXjENicmA3nNpUXA8lcTdDjAndtP+oNdni3T/qQqA==",
4326 "hasInstallScript": true,4380 "hasInstallScript": true,
4327 "license": "Apache-2.0",4381 "license": "Apache-2.0",
4328 "dependencies": {4382 "dependencies": {
4329 "@puppeteer/browsers": "2.10.5",4383 "@puppeteer/browsers": "2.10.10",
4330 "chromium-bidi": "5.1.0",4384 "chromium-bidi": "9.1.0",
4331 "cosmiconfig": "^9.0.0",4385 "cosmiconfig": "^9.0.0",
4332 "devtools-protocol": "0.0.1464554",4386 "devtools-protocol": "0.0.1508733",
4333 "puppeteer-core": "24.12.1",4387 "puppeteer-core": "24.23.0",
4334 "typed-query-selector": "^2.12.0"4388 "typed-query-selector": "^2.12.0"
4335 },4389 },
4336 "bin": {4390 "bin": {
...@@ -4341,16 +4395,17 @@...@@ -4341,16 +4395,17 @@
4341 }4395 }
4342 },4396 },
4343 "node_modules/puppeteer-core": {4397 "node_modules/puppeteer-core": {
4344 "version": "24.12.1",4398 "version": "24.23.0",
4345 "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.12.1.tgz",4399 "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.23.0.tgz",
4346 "integrity": "sha512-8odp6d3ERKBa3BAVaYWXn95UxQv3sxvP1reD+xZamaX6ed8nCykhwlOiHSaHR9t/MtmIB+rJmNencI6Zy4Gxvg==",4400 "integrity": "sha512-yl25C59gb14sOdIiSnJ08XiPP+O2RjuyZmEG+RjYmCXO7au0jcLf7fRiyii96dXGUBW7Zwei/mVKfxMx/POeFw==",
4347 "license": "Apache-2.0",4401 "license": "Apache-2.0",
4348 "dependencies": {4402 "dependencies": {
4349 "@puppeteer/browsers": "2.10.5",4403 "@puppeteer/browsers": "2.10.10",
4350 "chromium-bidi": "5.1.0",4404 "chromium-bidi": "9.1.0",
4351 "debug": "^4.4.1",4405 "debug": "^4.4.3",
4352 "devtools-protocol": "0.0.1464554",4406 "devtools-protocol": "0.0.1508733",
4353 "typed-query-selector": "^2.12.0",4407 "typed-query-selector": "^2.12.0",
4408 "webdriver-bidi-protocol": "0.3.6",
4354 "ws": "^8.18.3"4409 "ws": "^8.18.3"
4355 },4410 },
4356 "engines": {4411 "engines": {
...@@ -4578,6 +4633,12 @@...@@ -4578,6 +4633,12 @@
4578 "queue-microtask": "^1.2.2"4633 "queue-microtask": "^1.2.2"
4579 }4634 }
4580 },4635 },
4636 "node_modules/sax": {
4637 "version": "1.2.1",
4638 "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
4639 "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==",
4640 "license": "ISC"
4641 },
4581 "node_modules/self-closing-tags": {4642 "node_modules/self-closing-tags": {
4582 "version": "1.0.1",4643 "version": "1.0.1",
4583 "resolved": "https://registry.npmjs.org/self-closing-tags/-/self-closing-tags-1.0.1.tgz",4644 "resolved": "https://registry.npmjs.org/self-closing-tags/-/self-closing-tags-1.0.1.tgz",
...@@ -4675,12 +4736,12 @@...@@ -4675,12 +4736,12 @@
4675 }4736 }
4676 },4737 },
4677 "node_modules/socks": {4738 "node_modules/socks": {
4678 "version": "2.8.5",4739 "version": "2.8.7",
4679 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",4740 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
4680 "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",4741 "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
4681 "license": "MIT",4742 "license": "MIT",
4682 "dependencies": {4743 "dependencies": {
4683 "ip-address": "^9.0.5",4744 "ip-address": "^10.0.1",
4684 "smart-buffer": "^4.2.0"4745 "smart-buffer": "^4.2.0"
4685 },4746 },
4686 "engines": {4747 "engines": {
...@@ -4740,12 +4801,6 @@...@@ -4740,12 +4801,6 @@
4740 "url": "https://github.com/sponsors/wooorm"4801 "url": "https://github.com/sponsors/wooorm"
4741 }4802 }
4742 },4803 },
4743 "node_modules/sprintf-js": {
4744 "version": "1.1.3",
4745 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
4746 "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
4747 "license": "BSD-3-Clause"
4748 },
4749 "node_modules/stackframe": {4804 "node_modules/stackframe": {
4750 "version": "1.3.4",4805 "version": "1.3.4",
4751 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",4806 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
...@@ -4753,16 +4808,14 @@...@@ -4753,16 +4808,14 @@
4753 "license": "MIT"4808 "license": "MIT"
4754 },4809 },
4755 "node_modules/streamx": {4810 "node_modules/streamx": {
4756 "version": "2.22.1",4811 "version": "2.23.0",
4757 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz",4812 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
4758 "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==",4813 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
4759 "license": "MIT",4814 "license": "MIT",
4760 "dependencies": {4815 "dependencies": {
4816 "events-universal": "^1.0.0",
4761 "fast-fifo": "^1.3.2",4817 "fast-fifo": "^1.3.2",
4762 "text-decoder": "^1.1.0"4818 "text-decoder": "^1.1.0"
4763 },
4764 "optionalDependencies": {
4765 "bare-events": "^2.2.0"
4766 }4819 }
4767 },4820 },
4768 "node_modules/string-width": {4821 "node_modules/string-width": {
...@@ -4779,27 +4832,6 @@...@@ -4779,27 +4832,6 @@
4779 "node": ">=8"4832 "node": ">=8"
4780 }4833 }
4781 },4834 },
4782 "node_modules/string-width/node_modules/ansi-regex": {
4783 "version": "5.0.1",
4784 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
4785 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
4786 "license": "MIT",
4787 "engines": {
4788 "node": ">=8"
4789 }
4790 },
4791 "node_modules/string-width/node_modules/strip-ansi": {
4792 "version": "6.0.1",
4793 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
4794 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
4795 "license": "MIT",
4796 "dependencies": {
4797 "ansi-regex": "^5.0.1"
4798 },
4799 "engines": {
4800 "node": ">=8"
4801 }
4802 },
4803 "node_modules/stringify-entities": {4835 "node_modules/stringify-entities": {
4804 "version": "4.0.4",4836 "version": "4.0.4",
4805 "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",4837 "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
...@@ -4815,18 +4847,15 @@...@@ -4815,18 +4847,15 @@
4815 }4847 }
4816 },4848 },
4817 "node_modules/strip-ansi": {4849 "node_modules/strip-ansi": {
4818 "version": "7.1.0",4850 "version": "6.0.1",
4819 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",4851 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
4820 "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",4852 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
4821 "license": "MIT",4853 "license": "MIT",
4822 "dependencies": {4854 "dependencies": {
4823 "ansi-regex": "^6.0.1"4855 "ansi-regex": "^5.0.1"
4824 },4856 },
4825 "engines": {4857 "engines": {
4826 "node": ">=12"4858 "node": ">=8"
4827 },
4828 "funding": {
4829 "url": "https://github.com/chalk/strip-ansi?sponsor=1"
4830 }4859 }
4831 },4860 },
4832 "node_modules/style-mod": {4861 "node_modules/style-mod": {
...@@ -4854,9 +4883,9 @@...@@ -4854,9 +4883,9 @@
4854 }4883 }
4855 },4884 },
4856 "node_modules/tar-fs": {4885 "node_modules/tar-fs": {
4857 "version": "3.1.0",4886 "version": "3.1.1",
4858 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",4887 "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
4859 "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",4888 "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
4860 "license": "MIT",4889 "license": "MIT",
4861 "dependencies": {4890 "dependencies": {
4862 "pump": "^3.0.0",4891 "pump": "^3.0.0",
...@@ -4971,6 +5000,32 @@...@@ -4971,6 +5000,32 @@
4971 "node": ">=14.17"5000 "node": ">=14.17"
4972 }5001 }
4973 },5002 },
5003 "node_modules/ua-parser-js": {
5004 "version": "1.0.41",
5005 "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
5006 "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
5007 "funding": [
5008 {
5009 "type": "opencollective",
5010 "url": "https://opencollective.com/ua-parser-js"
5011 },
5012 {
5013 "type": "paypal",
5014 "url": "https://paypal.me/faisalman"
5015 },
5016 {
5017 "type": "github",
5018 "url": "https://github.com/sponsors/faisalman"
5019 }
5020 ],
5021 "license": "MIT",
5022 "bin": {
5023 "ua-parser-js": "script/cli.js"
5024 },
5025 "engines": {
5026 "node": "*"
5027 }
5028 },
4974 "node_modules/undici-types": {5029 "node_modules/undici-types": {
4975 "version": "7.8.0",5030 "version": "7.8.0",
4976 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",5031 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
...@@ -5163,6 +5218,12 @@...@@ -5163,6 +5218,12 @@
5163 "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",5218 "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
5164 "license": "MIT"5219 "license": "MIT"
5165 },5220 },
5221 "node_modules/webdriver-bidi-protocol": {
5222 "version": "0.3.6",
5223 "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.6.tgz",
5224 "integrity": "sha512-mlGndEOA9yK9YAbvtxaPTqdi/kaCWYYfwrZvGzcmkr/3lWM+tQj53BxtpVd6qbC6+E5OnHXgCcAhre6AkXzxjA==",
5225 "license": "Apache-2.0"
5226 },
5166 "node_modules/wrap-ansi": {5227 "node_modules/wrap-ansi": {
5167 "version": "7.0.0",5228 "version": "7.0.0",
5168 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",5229 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
...@@ -5180,27 +5241,6 @@...@@ -5180,27 +5241,6 @@
5180 "url": "https://github.com/chalk/wrap-ansi?sponsor=1"5241 "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
5181 }5242 }
5182 },5243 },
5183 "node_modules/wrap-ansi/node_modules/ansi-regex": {
5184 "version": "5.0.1",
5185 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
5186 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
5187 "license": "MIT",
5188 "engines": {
5189 "node": ">=8"
5190 }
5191 },
5192 "node_modules/wrap-ansi/node_modules/strip-ansi": {
5193 "version": "6.0.1",
5194 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
5195 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
5196 "license": "MIT",
5197 "dependencies": {
5198 "ansi-regex": "^5.0.1"
5199 },
5200 "engines": {
5201 "node": ">=8"
5202 }
5203 },
5204 "node_modules/wrappy": {5244 "node_modules/wrappy": {
5205 "version": "1.0.2",5245 "version": "1.0.2",
5206 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",5246 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
package.json+19-20
...@@ -1,29 +1,28 @@...@@ -1,29 +1,28 @@
1{1{
2 "type": "module",2 "type": "module",
3 "dependencies": {3 "dependencies": {
4 "@hono/node-server": "^1.14.3",4 "@hono/node-server": "1.14.3",
5 "@mdx-js/mdx": "^3.1.0",5 "@mdx-js/mdx": "3.1.0",
6 "@paperclover/console": "git+https://git.paperclover.net/clo/console.git",6 "codemirror": "6.0.1",
7 "blurhash": "^2.0.5",7 "dashjs": "^5.0.3",
8 "codemirror": "^6.0.1",8 "devalue": "^5.3.2",
9 "devalue": "^5.1.1",9 "esbuild": "0.25.5",
10 "esbuild": "^0.25.5",10 "hls.js": "1.6.5",
11 "hls.js": "^1.6.5",11 "hono": "4.9.9",
12 "hono": "^4.7.11",
13 "lib": "file:lib",12 "lib": "file:lib",
14 "marko": "^6.0.20",13 "marko": "6.0.82",
15 "msgpackr": "^1.11.5",14 "msgpackr": "1.11.5",
16 "puppeteer": "^24.10.1",15 "puppeteer": "24.23.0",
17 "sharp": "^0.34.2",16 "sharp": "0.34.2",
18 "source-map-support": "^0.5.21",17 "source-map-support": "0.5.21",
19 "ts-to-jsdoc": "^2.4.0",18 "ts-to-jsdoc": "2.4.0",
20 "unique-names-generator": "^4.7.1",19 "unique-names-generator": "4.7.1",
21 "vscode-oniguruma": "^2.0.1",20 "vscode-oniguruma": "2.0.1",
22 "vscode-textmate": "^9.2.0"21 "vscode-textmate": "9.2.0"
23 },22 },
24 "devDependencies": {23 "devDependencies": {
25 "@types/node": "^24.0.10",24 "@types/node": "24.0.10",
26 "typescript": "^5.8.3"25 "typescript": "5.8.3"
27 },26 },
28 "imports": {27 "imports": {
29 "#backend": "./src/backend.ts",28 "#backend": "./src/backend.ts",
repl.js+2-3
...@@ -3,21 +3,20 @@...@@ -3,21 +3,20 @@
3// doesn't have to reload the plugin system. This is mostly3// doesn't have to reload the plugin system. This is mostly
4// done since Clover's primary dev-env is a Windows 7 laptop;4// done since Clover's primary dev-env is a Windows 7 laptop;
5// It takes her machine 600ms to load the plugin file on it's own.5// It takes her machine 600ms to load the plugin file on it's own.
6//
7// TODO: as of 2025-10-03, this file is not maintained
6import process from "node:process";8import process from "node:process";
7import * as util from "node:util";9import * as util from "node:util";
8const exts = [".tsx", ".ts", ".js", ".marko"];10const exts = [".tsx", ".ts", ".js", ".marko"];
911
10process.stderr.write("Loading...");12process.stderr.write("Loading...");
11const { hot } = await import("./run.js"); // get plugins ready13const { hot } = await import("./run.js"); // get plugins ready
12const { errorAllWidgets } = hot.load("@paperclover/console/Widget");
13process.stderr.write("\r" + " ".repeat("Loading...".length) + "\r");14process.stderr.write("\r" + " ".repeat("Loading...".length) + "\r");
14const repl = hot.load("node:repl").start({15const repl = hot.load("node:repl").start({
15 prompt: "% ",16 prompt: "% ",
16 eval(code, _global, _id, done) {17 eval(code, _global, _id, done) {
17 evaluate(code)18 evaluate(code)
18 .catch((err) => {19 .catch((err) => {
19 // TODO: improve @paperclover/console's ability to print AggregateError
20 // and errors with extra random properties
21 console.error(util.inspect(err, false, 10, true));20 console.error(util.inspect(err, false, 10, true));
22 })21 })
23 .then((result) => done(null, result));22 .then((result) => done(null, result));
src/backend.ts+3-3
...@@ -1,11 +1,11 @@...@@ -1,11 +1,11 @@
1// This is the main file for paperclover.net's server.1// This is the main file for paperclover.net's server.
2const app = new Hono();2const app = new Hono();
3const logHttp = console.scoped("http", { color: "magenta" });3const console = log.scoped("http");
44
5// Middleware5// Middleware
6app.use(trimTrailingSlash());6app.use(trimTrailingSlash());
7app.use(removeDuplicateSlashes);7app.use(removeDuplicateSlashes);
8app.use(logger((msg) => msg.startsWith("-->") && logHttp(msg.slice(4))));8app.use(logger((msg) => msg.startsWith("-->") && console.info(msg.slice(4))));
9app.use(admin.middleware);9app.use(admin.middleware);
1010
11// Backends11// Backends
...@@ -52,4 +52,4 @@ import { logger } from "hono/logger";...@@ -52,4 +52,4 @@ import { logger } from "hono/logger";
52import { trimTrailingSlash } from "hono/trailing-slash";52import { trimTrailingSlash } from "hono/trailing-slash";
53import * as assets from "#sitegen/assets";53import * as assets from "#sitegen/assets";
54import * as admin from "./admin.ts";54import * as admin from "./admin.ts";
55import * as console from "@paperclover/console";55import * as log from "lib/log.ts";
src/file-viewer/backend.tsx+72-173
...@@ -18,8 +18,12 @@ app.get("/file/*", async (c, next) => {...@@ -18,8 +18,12 @@ app.get("/file/*", async (c, next) => {
1818
19 let rawFilePath = c.req.path.slice(5) || "/";19 let rawFilePath = c.req.path.slice(5) || "/";
2020
21 let derivedKey = "";
22 if (rawFilePath.includes(":/")) {
23 [rawFilePath, derivedKey] = rawFilePath.split(":/") as [string, string];
24 }
21 // The web file viewer sends urls suffixed with $partial to get HTML partials.25 // The web file viewer sends urls suffixed with $partial to get HTML partials.
22 if (rawFilePath.endsWith("$partial")) {26 if (!derivedKey && rawFilePath.endsWith("$partial")) {
23 return getPartialPage(c, rawFilePath.slice(0, -"$partial".length));27 return getPartialPage(c, rawFilePath.slice(0, -"$partial".length));
24 }28 }
2529
...@@ -72,23 +76,25 @@ app.get("/file/*", async (c, next) => {...@@ -72,23 +76,25 @@ app.get("/file/*", async (c, next) => {
72 }76 }
73 const download = viewMode === "download";77 const download = viewMode === "download";
7478
75 const etag = file.hash;79 // Grab a derived asset if that was requested.
76 // const filePath = file.path;80 let { hash, size, date } = file;
77 const size = file.size;81 if (derivedKey) {
7882 const entry = derived.get(file, derivedKey);
79 //let encoding = decideEncoding(c.req.header("Accept-Encoding"));83 if (entry) {
84 ({ hash, size, date, path: derivedKey } = entry);
85 } else {
86 return next();
87 }
88 }
89 const etag = `"${hash}"`;
8090
81 // let sizeHeader = encoding === "raw"
82 // ? expectedSize
83 // // Size cannot be known because of compression modes
84 // : undefined;
85 //
86 const headers = new Headers({91 const headers = new Headers({
87 Vary: "Accept-Encoding, Accept",92 Vary: "Accept-Encoding, Accept",
88 "Content-Type": contentTypeFor(file.path),93 "Content-Type": contentTypeFor(derivedKey || file.path),
89 "Content-Length": size.toString(),94 "Content-Length": size.toString(),
90 ETag: file.hash,95 ETag: etag,
91 "Last-Modified": file.date.toUTCString(),96 "Last-Modified": date.toUTCString(),
97 "Accept-Ranges": "bytes",
92 });98 });
93 if (download) {99 if (download) {
94 headers.set(100 headers.set(
...@@ -110,15 +116,8 @@ app.get("/file/*", async (c, next) => {...@@ -110,15 +116,8 @@ app.get("/file/*", async (c, next) => {
110 }116 }
111 }117 }
112118
113 // Head119 let status = 200;
114 if (c.req.method === "HEAD") {
115 c.res = new Response(null, { headers });
116 return;
117 }
118
119 // Prevalidate range requests
120 let rangeHeader = c.req.header("Range") ?? null;120 let rangeHeader = c.req.header("Range") ?? null;
121
122 const ifRangeHeader = c.req.header("If-Range");121 const ifRangeHeader = c.req.header("If-Range");
123 if (ifRangeHeader && ifRangeOutdated(file, ifRangeHeader)) {122 if (ifRangeHeader && ifRangeOutdated(file, ifRangeHeader)) {
124 // > If the condition is not fulfilled, the full resource is123 // > If the condition is not fulfilled, the full resource is
...@@ -126,71 +125,57 @@ app.get("/file/*", async (c, next) => {...@@ -126,71 +125,57 @@ app.get("/file/*", async (c, next) => {
126 rangeHeader = null;125 rangeHeader = null;
127 }126 }
128127
129 const found = await cache.read(filePath);128 let start = 0;
130 // foundFile = await cache.read(filePath, encoding, {129 let end = Infinity;
131 // start: rangeHeader ? rangeHeader[0] : 0,130
132 // end: rangeHeader ? rangeHeader[1] : Infinity,131 // Range requests
133 // });132 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
134 // const [streamOrBuffer, actualEncoding, src] = foundFile;133 if (rangeHeader) {
135 // encoding = actualEncoding;134 const ranges = parseRange(rangeHeader, file.size);
136 //135 if (ranges?.[0] && ranges.length === 1) {
137 // // Range requests136 start = ranges[0][0];
138 // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests137 end = ranges[0][1];
139 // // Compression is skipped because it's a confusing, but solvable problem.138
140 // // See https://stackoverflow.com/questions/33947562/is-it-possible-to-send-http-response-using-gzip-and-byte-ranges-at-the-same-time139 status = 206;
141 // if (rangeHeader) {140 headers.set(
142 // const ranges = parseRange(rangeHeader, file.size);141 "Content-Range",
143 // // TODO: multiple ranges142 `bytes ${ranges[0][0]}-${ranges[0][1]}/${file.size}`,
144 // if (ranges && ranges.length === 1) {143 );
145 // return (c.res = handleRanges(ranges, file, streamOrBuffer, download));144 headers.delete("Content-Length");
146 // }145 } else {
147 // }146 // multi-ranges are not supported
148 //147 // it could be done by calling `cache.read` multiple times.
149 // // Respond in a streaming fashion148 }
150 // c.res = new Response(streamOrBuffer, {149 }
151 // headers: {150
152 // ...fileHeaders(file, download, sizeHeader),151 // Head
153 // ...(encoding !== "raw" && {152 if (c.req.method === "HEAD") {
154 // "Content-Encoding": encoding,153 c.res = new Response(null, { headers, status });
155 // }),154 return;
156 // "X-Cache": src,155 }
157 // },
158 // });
159});
160156
161app.get("/canvas/:script", async (c, next) => {157 const found = await cache.read({
162 const script = c.req.param("script");158 file,
163 if (!hasAsset(`/js/canvas/${script}.js`)) {159 hash,
164 return next();160 derived: derivedKey,
161 start,
162 end,
163 });
164 if (!found) {
165 return Response.json({
166 error: "localOnly is enabled and this file is not available",
167 }, 500);
165 }168 }
166 return view.serve(c, "file-viewer/canvas", {169 if (status === 200) {
167 script,170 headers.set("Content-Length", found.size.toString());
171 }
172 headers.set("Cache-Status", found.src);
173 c.res = new Response(found.stream as ReadableStream, {
174 headers,
175 status,
168 });176 });
169});177});
170178
171function decideEncoding(encodings: string | undefined): CompressionFormat {
172 if (encodings?.includes("zstd")) return "zstd";
173 if (encodings?.includes("gzip")) return "gzip";
174 return "raw";
175}
176
177function fileHeaders(
178 file: MediaFile,
179 download: boolean,
180 size: number | undefined = file.size,
181) {
182 return {
183 Vary: "Accept-Encoding, Accept",
184 "Content-Type": contentTypeFor(file.path),
185 "Content-Length": size.toString(),
186 ETag: file.hash,
187 "Last-Modified": file.date.toUTCString(),
188 ...(download && {
189 "Content-Disposition": `attachment; filename="${file.basename}"`,
190 }),
191 };
192}
193
194function ifRangeOutdated(file: MediaFile, ifRangeHeader: string) {179function ifRangeOutdated(file: MediaFile, ifRangeHeader: string) {
195 // etag180 // etag
196 if (ifRangeHeader[0] === '"') {181 if (ifRangeHeader[0] === '"') {
...@@ -205,6 +190,7 @@ type Ranges = Array<[start: number, end: number]>;...@@ -205,6 +190,7 @@ type Ranges = Array<[start: number, end: number]>;
205190
206function parseRange(rangeHeader: string, fileSize: number): Ranges | null {191function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
207 const [unit, ranges] = rangeHeader.split("=");192 const [unit, ranges] = rangeHeader.split("=");
193 if (!ranges) return null;
208 if (unit !== "bytes") return null;194 if (unit !== "bytes") return null;
209195
210 const result: Array<[start: number, end: number]> = [];196 const result: Array<[start: number, end: number]> = [];
...@@ -212,8 +198,8 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {...@@ -212,8 +198,8 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
212198
213 for (const range of rangeParts) {199 for (const range of rangeParts) {
214 const split = range.split("-");200 const split = range.split("-");
215 if (split.length !== 2) return null;
216 const [start, end] = split;201 const [start, end] = split;
202 if (!start || !end) return null;
217 if (start === "" && end === "") return null;203 if (start === "" && end === "") return null;
218 const parsedRange: [number, number] = [204 const parsedRange: [number, number] = [
219 start === "" ? fileSize - +end : +start,205 start === "" ? fileSize - +end : +start,
...@@ -225,7 +211,7 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {...@@ -225,7 +211,7 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
225 // Validate that ranges do not intersect211 // Validate that ranges do not intersect
226 result.sort((a, b) => a[0] - b[0]);212 result.sort((a, b) => a[0] - b[0]);
227 for (let i = 1; i < result.length; i++) {213 for (let i = 1; i < result.length; i++) {
228 if (result[i][0] <= result[i - 1][1]) {214 if (UNWRAP(result[i])[0] <= UNWRAP(result[i - 1])[1]) {
229 return null;215 return null;
230 }216 }
231 }217 }
...@@ -233,94 +219,6 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {...@@ -233,94 +219,6 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null {
233 return result;219 return result;
234}220}
235221
236function handleRanges(
237 ranges: Ranges,
238 file: MediaFile,
239 streamOrBuffer: ReadableStream | Buffer,
240 download: boolean,
241): Response {
242 // TODO: multiple ranges
243 const rangeSize = ranges.reduce((a, b) => a + (b[1] - b[0] + 1), 0);
244 const rangeBody = streamOrBuffer instanceof ReadableStream
245 ? applySingleRangeToStream(streamOrBuffer, ranges)
246 : applyRangesToBuffer(streamOrBuffer, ranges, rangeSize);
247 return new Response(rangeBody, {
248 status: 206,
249 headers: {
250 ...fileHeaders(file, download, rangeSize),
251 "Content-Range": `bytes ${ranges[0][0]}-${ranges[0][1]}/${file.size}`,
252 },
253 });
254}
255
256function applyRangesToBuffer(
257 buffer: Buffer,
258 ranges: Ranges,
259 rangeSize: number,
260): Uint8Array {
261 const result = new Uint8Array(rangeSize);
262 let offset = 0;
263 for (const [start, end] of ranges) {
264 result.set(buffer.subarray(start, end + 1), offset);
265 offset += end - start + 1;
266 }
267 return result;
268}
269
270function applySingleRangeToStream(
271 stream: ReadableStream,
272 ranges: Ranges,
273): ReadableStream {
274 let reader: ReadableStreamDefaultReader<Uint8Array>;
275 let position = 0;
276 const [start, end] = ranges[0];
277 return new ReadableStream({
278 async start(controller) {
279 reader = stream.getReader();
280 try {
281 while (position <= end) {
282 const { done, value } = await reader.read();
283
284 if (done) {
285 controller.close();
286 return;
287 }
288
289 const buffer = new Uint8Array(value);
290
291 const bufferStart = position;
292 const bufferEnd = position + buffer.length - 1;
293
294 position += buffer.length;
295
296 if (bufferEnd < start) {
297 continue;
298 }
299
300 if (bufferStart > end) {
301 break;
302 }
303
304 const sendStart = Math.max(0, start - bufferStart);
305 const sendEnd = Math.min(buffer.length - 1, end - bufferStart);
306
307 if (sendStart <= sendEnd) {
308 controller.enqueue(buffer.slice(sendStart, sendEnd + 1));
309 }
310 }
311
312 controller.close();
313 } catch (error) {
314 controller.error(error);
315 } finally {
316 reader.releaseLock();
317 }
318 },
319 cancel() {
320 reader?.releaseLock();
321 },
322 });
323}
324function checkCotyledonCookie(c: Context) {222function checkCotyledonCookie(c: Context) {
325 const cookie = c.req.header("Cookie");223 const cookie = c.req.header("Cookie");
326 if (!cookie) return false;224 if (!cookie) return false;
...@@ -383,7 +281,7 @@ function getPartialPage(c: Context, rawFilePath: string) {...@@ -383,7 +281,7 @@ function getPartialPage(c: Context, rawFilePath: string) {
383import { type Context, Hono } from "hono";281import { type Context, Hono } from "hono";
384282
385import * as render from "#engine/render";283import * as render from "#engine/render";
386import { etagMatches, hasAsset, serveAsset } from "#sitegen/assets";284import { etagMatches, serveAsset } from "#sitegen/assets";
387import * as view from "#sitegen/view";285import * as view from "#sitegen/view";
388import { contentTypeFor } from "#sitegen/mime";286import { contentTypeFor } from "#sitegen/mime";
389287
...@@ -393,3 +291,4 @@ import { FilePermissions } from "@/file-viewer/models/FilePermissions.ts";...@@ -393,3 +291,4 @@ import { FilePermissions } from "@/file-viewer/models/FilePermissions.ts";
393import { MediaPanel } from "@/file-viewer/views/clofi.tsx";291import { MediaPanel } from "@/file-viewer/views/clofi.tsx";
394import { Speedbump } from "@/file-viewer/cotyledon.tsx";292import { Speedbump } from "@/file-viewer/cotyledon.tsx";
395import * as cache from "@/file-viewer/cache.ts";293import * as cache from "@/file-viewer/cache.ts";
294import * as derived from "@/file-viewer/models/derived.ts";
src/file-viewer/bin/scan3.ts+222-151
...@@ -19,6 +19,8 @@ export async function main() {...@@ -19,6 +19,8 @@ export async function main() {
19 `paper clover's scan3 [${((now - start) / 1000).toFixed(1)}s]`,19 `paper clover's scan3 [${((now - start) / 1000).toFixed(1)}s]`,
20 });20 });
2121
22 const promises = new async.PromiseAggregator();
23
22 const walkQueue = new queue.PriorityQueue(10);24 const walkQueue = new queue.PriorityQueue(10);
2325
24 const dirsNode = progress.start("Walk Tree", { estimate: 1 });26 const dirsNode = progress.start("Walk Tree", { estimate: 1 });
...@@ -46,19 +48,15 @@ export async function main() {...@@ -46,19 +48,15 @@ export async function main() {
46 node.text = publicPath + " - reading";48 node.text = publicPath + " - reading";
47 const items = (await path.readDir())49 const items = (await path.readDir())
48 .filter((child) => !skipBasename(child.base))50 .filter((child) => !skipBasename(child.base))
49 .map((child) => (scanDirectory(child), child.base));51 .map((child) => (promises.push(scanDirectory(child)), child.base));
50 dirsNode.estimate += items.length;52 dirsNode.estimate += items.length;
5153
52 if (mediaFile) {54 for (const child of mediaFile?.getChildren() ?? []) {
53 const deleted = mediaFile55 if (items.includes(child.basename)) continue;
54 .getChildren()56 const recursive = child.kind === MediaFileKind.directory
55 .filter((child) => !items.includes(child.basename))57 ? [child, ...child.getRecursiveFileChildren()]
56 .flatMap((child) =>58 : [child];
57 child.kind === MediaFileKind.directory59 for (const deletion of recursive) deletion.delete();
58 ? child.getRecursiveFileChildren()
59 : child
60 );
61 //deleted
62 }60 }
6361
64 return;62 return;
...@@ -70,10 +68,10 @@ export async function main() {...@@ -70,10 +68,10 @@ export async function main() {
70 stat.size !== mediaFile.size ||68 stat.size !== mediaFile.size ||
71 stat.mtime.getTime() !== mediaFile.date.getTime()69 stat.mtime.getTime() !== mediaFile.date.getTime()
72 ) {70 ) {
73 updateMetadata({ path, publicPath, stat, mediaFile });71 promises.push(updateMetadata({ path, publicPath, stat, mediaFile }));
74 } else {72 } else {
75 // If the scanners changed, it may mean more processes should be run.73 // If the scanners changed, it may mean more processes should be run.
76 queueProcessors({74 await queueProcessors({
77 path,75 path,
78 stat,76 stat,
79 mediaFile,77 mediaFile,
...@@ -144,15 +142,16 @@ export async function main() {...@@ -144,15 +142,16 @@ export async function main() {
144 processor,142 processor,
145 index,143 index,
146 after,144 after,
147 fileNode,145 fileNode: innerNode,
148 }: ProcessJob) {146 }: ProcessJob) {
149 using node = fileNode.start(processor.name);147 using node = innerNode.start(processor.name);
150 await processor.run({148 await processor.run({
151 path,149 path,
152 stat,150 stat,
153 mediaFile,151 mediaFile,
154 node,152 node,
155 });153 });
154 fileNode.value += 1;
156 mediaFile.setProcessed(mediaFile.processed | (1 << (16 + index)));155 mediaFile.setProcessed(mediaFile.processed | (1 << (16 + index)));
157 for (const dependantJob of after) {156 for (const dependantJob of after) {
158 ASSERT(157 ASSERT(
...@@ -164,7 +163,7 @@ export async function main() {...@@ -164,7 +163,7 @@ export async function main() {
164 queueFileProcessor(dependantJob);163 queueFileProcessor(dependantJob);
165 }164 }
166 }165 }
167 }, (job) => ({ cores: job.processor.cores }));166 }, (job) => ({ cores: job.processor.cores ?? 0 }));
168167
169 function decodeProcessors(input: string) {168 function decodeProcessors(input: string) {
170 return input169 return input
...@@ -201,7 +200,13 @@ export async function main() {...@@ -201,7 +200,13 @@ export async function main() {
201200
202 // If the hash has changed, migrate the bitfield over.201 // If the hash has changed, migrate the bitfield over.
203 // This also runs when the processor hash is in it's initial 0 state.202 // This also runs when the processor hash is in it's initial 0 state.
204 const order = decodeProcessors(mediaFile.processors);203 let order: ReturnType<typeof decodeProcessors>;
204 try {
205 order = decodeProcessors(mediaFile.processors);
206 } catch {
207 // this function sucks and this system sucks i hate it.
208 order = [];
209 }
205 if ((processed & 0xffff) !== hash) {210 if ((processed & 0xffff) !== hash) {
206 const previous = order.filter(211 const previous = order.filter(
207 (_, i) => (processed & (1 << (16 + i))) !== 0,212 (_, i) => (processed & (1 << (16 + i))) !== 0,
...@@ -241,10 +246,11 @@ export async function main() {...@@ -241,10 +246,11 @@ export async function main() {
241 fileNode: node,246 fileNode: node,
242 };247 };
243 jobs.push(job);248 jobs.push(job);
244 if (job.needs === 0) queueFileProcessor(job);249 if (job.needs === 0) promises.push(queueFileProcessor(job));
245 }250 }
246 }251 }
247 node.estimate = jobs.length;252 node.estimate = jobs.length;
253 fileNode.estimate += jobs.length;
248 for (const job of jobs) {254 for (const job of jobs) {
249 for (const dependId of job.processor.depends) {255 for (const dependId of job.processor.depends) {
250 const dependJob = jobs.find((j) => j.processor.id === dependId);256 const dependJob = jobs.find((j) => j.processor.id === dependId);
...@@ -253,7 +259,7 @@ export async function main() {...@@ -253,7 +259,7 @@ export async function main() {
253 } else {259 } else {
254 ASSERT(job.needs > 0, `job.needs !== 0, ${job.needs}`);260 ASSERT(job.needs > 0, `job.needs !== 0, ${job.needs}`);
255 job.needs -= 1;261 job.needs -= 1;
256 if (job.needs === 0) queueFileProcessor(job);262 if (job.needs === 0) promises.push(queueFileProcessor(job));
257 }263 }
258 }264 }
259 }265 }
...@@ -265,16 +271,18 @@ export async function main() {...@@ -265,16 +271,18 @@ export async function main() {
265 if (!rootPath.ifExistsSync()) {271 if (!rootPath.ifExistsSync()) {
266 throw new Error(`file store ${rootPath} is not mounted`);272 throw new Error(`file store ${rootPath} is not mounted`);
267 }273 }
268 scanDirectory(rootPath);274 promises.push(scanDirectory(rootPath));
269 // await qList.done();275
270 // await qMeta.done();276 await promises.all();
271 // await qProcess.done();277 fileNode.end();
272 return;278 dirsNode.end();
273279
274 // Update directory metadata280 // Update directory metadata
281 using dirMetaNode = progress.start("update directory metadata");
275 const dirs = MediaFile.getDirectoriesToReindex()282 const dirs = MediaFile.getDirectoriesToReindex()
276 .sort((a, b) => b.path.length - a.path.length);283 .sort((a, b) => b.path.length - a.path.length);
277 for (const dir of dirs) {284 for (const dir of dirs) {
285 using _ = dirMetaNode.start(dir.path);
278 const children = dir.getChildren();286 const children = dir.getChildren();
279287
280 // readme.txt288 // readme.txt
...@@ -327,56 +335,58 @@ export async function main() {...@@ -327,56 +335,58 @@ export async function main() {
327 dirsort,335 dirsort,
328 });336 });
329 }337 }
338 dirMetaNode.end();
330339
331 // Sync to remote340 // Sync to remote
332 // if (341 if (
333 // ((await derived.workDir.ifExistsSync()?.readDir())?.length ?? 0) > 0342 ((await derived.workDir.ifExistsSync()?.readDir())?.length ?? 0) > 0
334 // ) {343 ) {
335 // await rsync.spawn({344 await rsync.spawn({
336 // args: [345 args: [
337 // "--links",346 "--links",
338 // "--recursive",347 "--recursive",
339 // "--times",348 "--times",
340 // "--partial",349 "--partial",
341 // "--progress",350 "--progress",
342 // "--remove-source-files",351 // "--remove-source-files",
343 // "--delay-updates",352 "--delay-updates",
344 // derived.workDir.toString() + "/",353 "--exclude=tmp.*",
345 // "clo@zenith:/mnt/storage1/clover/Documents/Config/clover_file/derived/",354 derived.workDir.toString() + "/",
346 // ],355 "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/derived/",
347 // title: "Uploading Derived Assets",356 ],
348 // cwd: process.cwd(),357 progress: progress.start("upload derived assets"),
349 // });358 cwd: process.cwd(),
350 //359 });
351 // await fs.removeEmptyDirectories(derived.workDir.toString());360
352 // } else {361 await fs.removeEmptyDirectories(derived.workDir.toString());
353 // console.info("No new derived assets");362 } else {
354 // }363 console.info("No new derived assets");
364 }
355365
356 MediaFile.db.prepare("VACUUM").run();366 MediaFile.db.prepare("VACUUM").run();
357 MediaFile.db.reload();367 MediaFile.db.reload();
358368
359 // await rsync.spawn({369 await rsync.spawn({
360 // args: [370 args: [
361 // MediaFile.db.file,371 MediaFile.db.file,
362 // "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/cache.sqlite",372 "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/cache.sqlite",
363 // ],373 ],
364 // title: "Uploading Database",374 progress: progress.start("Uploading database"),
365 // cwd: process.cwd(),375 cwd: process.cwd(),
366 // });376 });
367 // if (sotToken) {377 if (sotToken) {
368 // const res = await fetch("https://db.paperclover.net/reload", {378 const res = await fetch("https://db.paperclover.net/reload", {
369 // method: "post",379 method: "post",
370 // headers: {380 headers: {
371 // Authorization: sotToken,381 Authorization: sotToken,
372 // },382 },
373 // });383 });
374 // if (!res.ok) {384 if (!res.ok) {
375 // console.warn(385 console.warn(
376 // `Failed to reload remote database ${res.status} ${res.statusText}`,386 `Failed to reload remote database ${res.status} ${res.statusText}`,
377 // );387 );
378 // }388 }
379 // } else console.warn("Missing SOT token");389 } else console.warn("Missing SOT token");
380390
381 console.info(391 console.info(
382 "Updated file viewer index in \x1b[1m" +392 "Updated file viewer index in \x1b[1m" +
...@@ -392,21 +402,30 @@ export async function main() {...@@ -392,21 +402,30 @@ export async function main() {
392 from media_files402 from media_files
393 `)403 `)
394 .getNonNull();404 .getNonNull();
405 const { derivedSize, derivedCount } = MediaFile.db
406 .prepare<[], { derivedCount: number; derivedSize: number }>(`
407 select
408 count(*) as derivedCount,
409 sum(size) as derivedSize
410 from derived_files
411 `)
412 .getNonNull();
413 const canonicalSize = UNWRAP(MediaFile.getByPath("/")).size;
395414
396 console.info();415 console.info();
397 console.info(416 console.info(
398 "Global Stats:\n" +417 "Global Stats:\n" +
399 `- File Count: \x1b[1m${count}\x1b[0m\n` +418 `- File Count: \x1b[1m${count}\x1b[0m\n` +
400 `- Canonical Size: \x1b[1m${419 `- Derived Count: \x1b[1m${derivedCount}\x1b[0m\n` +
401 formatSize(MediaFile.getByPath("/")!.size)420 `- Media Duration: \x1b[1m${formatDurationLong(duration)}\x1b[0m\n` +
402 }\x1b[0m\n` +421 `- Canonical Size: \x1b[1m${formatSize(canonicalSize)}\x1b[0m\n` +
403 `- Media Duration: \x1b[1m${formatDurationLong(duration)}\x1b[0m\n`,422 `- Derived Size: \x1b[1m${formatSize(derivedSize)}\x1b[0m\n`,
404 );423 );
405}424}
406425
407interface Process {426interface Process {
408 name: string;427 name: string;
409 cores: number;428 cores?: number;
410 enable?: boolean;429 enable?: boolean;
411 include?: Set<string>;430 include?: Set<string>;
412 exclude?: Set<string>;431 exclude?: Set<string>;
...@@ -491,6 +510,7 @@ const procLoadTextContents: Process = {...@@ -491,6 +510,7 @@ const procLoadTextContents: Process = {
491 name: "load text content",510 name: "load text content",
492 include: rules.extsReadContents,511 include: rules.extsReadContents,
493 cores: 1,512 cores: 1,
513 version: 2,
494 async run({ path, mediaFile, stat }) {514 async run({ path, mediaFile, stat }) {
495 if (stat.size > 1_000_000) return;515 if (stat.size > 1_000_000) return;
496 const text = await path.read("utf-8");516 const text = await path.read("utf-8");
...@@ -502,6 +522,7 @@ const procHighlightCode: Process = {...@@ -502,6 +522,7 @@ const procHighlightCode: Process = {
502 name: "highlight source code",522 name: "highlight source code",
503 include: new Set(rules.extsCode.keys()),523 include: new Set(rules.extsCode.keys()),
504 cores: 1,524 cores: 1,
525 version: 2,
505 async run({ path, mediaFile, stat }) {526 async run({ path, mediaFile, stat }) {
506 const language = UNWRAP(527 const language = UNWRAP(
507 rules.extsCode.get(path.ext.toLowerCase()),528 rules.extsCode.get(path.ext.toLowerCase()),
...@@ -531,7 +552,6 @@ const procImageSubsets: Process = {...@@ -531,7 +552,6 @@ const procImageSubsets: Process = {
531 include: rules.extsImage,552 include: rules.extsImage,
532 depends: [procDimensions.name],553 depends: [procDimensions.name],
533 version: 2,554 version: 2,
534 cores: 2,
535 async run({ path, mediaFile, node }) {555 async run({ path, mediaFile, node }) {
536 const { width, height } = UNWRAP(mediaFile.parseDimensions());556 const { width, height } = UNWRAP(mediaFile.parseDimensions());
537 const targetSizes = transcodeRules.imageSizes.filter((w) => w < width);557 const targetSizes = transcodeRules.imageSizes.filter((w) => w < width);
...@@ -544,16 +564,22 @@ const procImageSubsets: Process = {...@@ -544,16 +564,22 @@ const procImageSubsets: Process = {
544 node.text = baseStatus + ` (${w}x${h}, ${ext.slice(1).toUpperCase()})`;564 node.text = baseStatus + ` (${w}x${h}, ${ext.slice(1).toUpperCase()})`;
545565
546 stack.use(566 stack.use(
547 await derived.produce(mediaFile, `${size}${ext}`, async (dir) => {567 await derived.produce({
548 await subprocess.exec(ffmpegBin!, [568 mediaFile,
549 ...ffmpegOptions,569 node,
550 "-i",570 cores: 2,
551 path.toString(),571 subkey: `${size}${ext}`,
552 "-vf",572 async producer(dir) {
553 `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`,573 await subprocess.exec(ffmpegBin!, [
554 ...args,574 ...ffmpegOptions,
555 dir.join(`${size}${ext}`).toString(),575 "-i",
556 ]);576 path.toString(),
577 "-vf",
578 `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`,
579 ...args,
580 dir.join(`${size}${ext}`).toString(),
581 ]);
582 },
557 }),583 }),
558 );584 );
559 }585 }
...@@ -563,9 +589,7 @@ const procImageSubsets: Process = {...@@ -563,9 +589,7 @@ const procImageSubsets: Process = {
563 },589 },
564};590};
565591
566const videoInputArgsCache = new async.OnceMap2(592const videoArgsCache = new async.OnceMap2(transcodeRules.getVideoInputArgs);
567 transcodeRules.getVideoInputArgs,
568);
569593
570const qualityMap: Record<string, string> = {594const qualityMap: Record<string, string> = {
571 u: "ultra-high",595 u: "ultra-high",
...@@ -579,19 +603,32 @@ const procVideos = transcodeRules.videoFormats.map<Process>((preset) => ({...@@ -579,19 +603,32 @@ const procVideos = transcodeRules.videoFormats.map<Process>((preset) => ({
579 include: rules.extsVideo,603 include: rules.extsVideo,
580 enable: ffmpegBin != null,604 enable: ffmpegBin != null,
581 depends: [procDuration.name, procDimensions.name],605 depends: [procDuration.name, procDimensions.name],
582 cores: 4,
583 async run({ path, mediaFile, node }) {606 async run({ path, mediaFile, node }) {
584 if ((mediaFile.duration ?? 0) < 5) return;607 if ((mediaFile.duration ?? 0) < 5) return;
585 await derived.produce(mediaFile, `av1-${preset.id}`, async (dir) => {608 if (!mediaFile.dimensions) return;
586 const input = await videoInputArgsCache.getOrRun(path);609
587 const args = transcodeRules.getAv1VideoArgs(preset, input.video, dir);610 if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return;
588611
589 await ffmpeg.spawn({612 await derived.produce({
590 ffmpeg: ffmpegBin!,613 mediaFile,
591 progress: node,614 node,
592 args,615 cores: 4,
593 cwd: dir,616 subkey: `av1-${preset.id}`,
594 });617 async producer(dir) {
618 const input = await videoArgsCache.getOrRun(path);
619 const args = transcodeRules.getAv1VideoArgs(
620 preset,
621 UNWRAP(input.video, "frick on " + path + JSON.stringify(input)),
622 dir,
623 );
624
625 await ffmpeg.spawn({
626 ffmpeg: ffmpegBin!,
627 progress: node,
628 args,
629 cwd: dir,
630 });
631 },
595 });632 });
596 },633 },
597}));634}));
...@@ -600,20 +637,29 @@ const procVideoAudios = transcodeRules.audioFormats.map<Process>((preset) => ({...@@ -600,20 +637,29 @@ const procVideoAudios = transcodeRules.audioFormats.map<Process>((preset) => ({
600 include: rules.extsVideo,637 include: rules.extsVideo,
601 enable: ffmpegBin != null,638 enable: ffmpegBin != null,
602 depends: [procDuration.name, procDimensions.name],639 depends: [procDuration.name, procDimensions.name],
603 cores: 2,640 version: 2,
604 async run({ path, mediaFile, node }) {641 async run({ path, mediaFile, node }) {
605 if ((mediaFile.duration ?? 0) < 5) return;642 if ((mediaFile.duration ?? 0) < 5) return;
606 await derived.produce(mediaFile, `opus-${preset.id}`, async (dir) => {643 if (!mediaFile.dimensions) return;
607 const input = await videoInputArgsCache.getOrRun(path);644 if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return;
608 if (!input.audio) return;645 await derived.produce({
609 const args = transcodeRules.getOpusAudioArgs(preset, input.audio, dir);646 mediaFile,
610647 node,
611 await ffmpeg.spawn({648 cores: 4,
612 ffmpeg: ffmpegBin!,649 subkey: `opus-${preset.id}`,
613 progress: node,650 async producer(dir) {
614 args,651 const input = await videoArgsCache.getOrRun(path);
615 cwd: dir,652 ASSERT(input.video);
616 });653 if (!input.audio) return;
654 const args = transcodeRules.getOpusAudioArgs(preset, input.audio, dir);
655
656 await ffmpeg.spawn({
657 ffmpeg: ffmpegBin!,
658 progress: node,
659 args,
660 cwd: dir,
661 });
662 },
617 });663 });
618 },664 },
619}));665}));
...@@ -622,37 +668,50 @@ const procDash: Process = {...@@ -622,37 +668,50 @@ const procDash: Process = {
622 include: rules.extsVideo,668 include: rules.extsVideo,
623 enable: ffmpegBin != null,669 enable: ffmpegBin != null,
624 depends: [...procVideos, ...procVideoAudios].map((x) => x.name),670 depends: [...procVideos, ...procVideoAudios].map((x) => x.name),
625 cores: 1,671 version: 4,
626 async run({ path, mediaFile, node }) {672 async run({ path, mediaFile, node }) {
627 if ((mediaFile.duration ?? 0) < 5) return;673 if ((mediaFile.duration ?? 0) < 5) return;
628 await derived.produce(mediaFile, `dash-av1`, async (dir) => {674 if (!mediaFile.dimensions) return;
629 const input = await videoInputArgsCache.getOrRun(path);675
630 const videos = transcodeRules.videoFormats.map(676 if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return;
631 (preset) =>677 await derived.produce({
632 UNWRAP(dir.parent).join(678 mediaFile,
633 `av1-${preset.id}`,679 node,
634 transcodeRules.av1FileName,680 cores: 1,
635 ),681 subkey: `dash-av1`,
636 );682 async producer(dir) {
637 const audios = input.audio683 const input = await videoArgsCache.getOrRun(path);
638 ? transcodeRules.videoFormats.map(684 const videos = transcodeRules.videoFormats.map(
639 (preset) =>685 (preset) =>
640 UNWRAP(dir.parent).join(686 UNWRAP(dir.parent).join(
641 `av1-${preset.id}`,687 `av1-${preset.id}`,
642 transcodeRules.av1FileName,688 transcodeRules.av1FileName,
643 ),689 ),
644 )690 );
645 : [];691 const audios = input.audio
646 const args = transcodeRules.getMpegDashArgs(videos, audios, dir);692 ? transcodeRules.audioFormats.map(
693 (preset) =>
694 UNWRAP(dir.parent).join(
695 `opus-${preset.id}`,
696 transcodeRules.opusFileName,
697 ),
698 )
699 : [];
700 const args = transcodeRules.getMpegDashArgs(
701 videos,
702 audios,
703 dir,
704 );
647705
648 await dir.join("d").makeDir();706 await dir.join("d").makeDir();
649707
650 await ffmpeg.spawn({708 await ffmpeg.spawn({
651 ffmpeg: ffmpegBin!,709 ffmpeg: ffmpegBin!,
652 progress: node,710 progress: node,
653 args,711 args,
654 cwd: dir,712 cwd: dir,
655 });713 });
714 },
656 });715 });
657 },716 },
658};717};
...@@ -661,19 +720,26 @@ const procH264Hls: Process = {...@@ -661,19 +720,26 @@ const procH264Hls: Process = {
661 include: rules.extsVideo,720 include: rules.extsVideo,
662 enable: ffmpegBin != null,721 enable: ffmpegBin != null,
663 depends: [procDuration.name, procDimensions.name],722 depends: [procDuration.name, procDimensions.name],
664 cores: 8,
665 async run({ path, mediaFile, node }) {723 async run({ path, mediaFile, node }) {
666 if ((mediaFile.duration ?? 0) < 5) return;724 if ((mediaFile.duration ?? 0) < 5) return;
667 await derived.produce(mediaFile, `hls`, async (dir) => {725 if (!mediaFile.dimensions) return;
668 const input = await videoInputArgsCache.getOrRun(path);726
669 const args = transcodeRules.getH264HlsArgs(input, dir);727 await derived.produce({
670728 mediaFile,
671 await ffmpeg.spawn({729 node,
672 ffmpeg: ffmpegBin!,730 cores: 8,
673 progress: node,731 subkey: `hls`,
674 args,732 async producer(dir) {
675 cwd: dir,733 const input = await videoArgsCache.getOrRun(path);
676 });734 const args = transcodeRules.getH264HlsArgs(input, dir);
735
736 await ffmpeg.spawn({
737 ffmpeg: ffmpegBin!,
738 progress: node,
739 args,
740 cwd: dir,
741 });
742 },
677 });743 });
678 },744 },
679};745};
...@@ -685,14 +751,19 @@ const procCompression = [...@@ -685,14 +751,19 @@ const procCompression = [
685 ({ name, fn }) => ({751 ({ name, fn }) => ({
686 name: `compress ${name}`,752 name: `compress ${name}`,
687 exclude: rules.extsPreCompressed,753 exclude: rules.extsPreCompressed,
688 cores: 1,754 async run({ path, mediaFile, node }) {
689 async run({ path, mediaFile }) {755 await derived.produce({
690 await derived.produce(mediaFile, name, async (dir) => {756 mediaFile,
691 await stream.promises.pipeline(757 node,
692 fs.createReadStream(path.toString()),758 cores: 1,
693 fn(),759 subkey: name,
694 fs.createWriteStream(dir.join(name).toString()),760 async producer(dir) {
695 );761 await stream.promises.pipeline(
762 fs.createReadStream(path.toString()),
763 fn(),
764 fs.createWriteStream(dir.join(name).toString()),
765 );
766 },
696 });767 });
697 },768 },
698 }),769 }),
src/file-viewer/cache.ts+18-19
...@@ -17,7 +17,7 @@...@@ -17,7 +17,7 @@
17// objects given by node:fs/node:http streams -- which are re-allocated for17// objects given by node:fs/node:http streams -- which are re-allocated for
18// every read -- are weakly held when written and reused when reading. this18// every read -- are weakly held when written and reused when reading. this
19// somewhat avoids file reads in concurrent access situations.19// somewhat avoids file reads in concurrent access situations.
20const log = console.scoped("cache");20const console = log.scoped("cache");
2121
22export type Source =22export type Source =
23 | "local" // local mode23 | "local" // local mode
...@@ -94,6 +94,7 @@ export interface Options {...@@ -94,6 +94,7 @@ export interface Options {
94 file: MediaFile;94 file: MediaFile;
95 /** If set, fetches the derived asset instead. */95 /** If set, fetches the derived asset instead. */
96 derived: string | null;96 derived: string | null;
97 hash: string;
97 start: number;98 start: number;
98 /** exclusive. ex: [10, 30] skips ten bytes then grabs 20 */99 /** exclusive. ex: [10, 30] skips ten bytes then grabs 20 */
99 end: number;100 end: number;
...@@ -106,13 +107,13 @@ export interface Options {...@@ -106,13 +107,13 @@ export interface Options {
106 * `null` if a derived asset does not exist107 * `null` if a derived asset does not exist
107 */108 */
108export async function read(109export async function read(
109 { file, derived, start, end, noWrite, localOnly }: Options,110 { file, derived, hash, start, end, noWrite, localOnly }: Options,
110): Promise<{ stream: ReadableStream; src: Source; size: number } | null> {111): Promise<{ stream: ReadableStream; src: Source; size: number } | null> {
111 const rel = derived ? `${file.path}$/${derived}` : file.path;112 const rel = derived ? `${file.path}$/${derived}` : file.path;
112113
113 // Serve from local network share if it is mounted.114 // Serve from local network share if it is mounted.
114 const localFile = derived115 const localFile = derived
115 ? derivedFileRoot?.join(file.hash, derived)116 ? derivedFileRoot?.join(derived)
116 : rawFileRoot?.join(file.path);117 : rawFileRoot?.join(file.path);
117 if (localFile) {118 if (localFile) {
118 local: {119 local: {
...@@ -128,7 +129,7 @@ export async function read(...@@ -128,7 +129,7 @@ export async function read(
128 }129 }
129 const fileStream = await readStream(localFile, { start, end });130 const fileStream = await readStream(localFile, { start, end });
130 if (!fileStream) break local;131 if (!fileStream) break local;
131 log(`from local: ${rel}`);132 console.info(`from local: ${rel} ` + localFile);
132 return {133 return {
133 stream: stream.Readable.toWeb(fileStream),134 stream: stream.Readable.toWeb(fileStream),
134 size,135 size,
...@@ -139,10 +140,6 @@ export async function read(...@@ -139,10 +140,6 @@ export async function read(
139140
140 if (!localOnly) return null;141 if (!localOnly) return null;
141142
142 const key = derived
143 ? crypto.createHash("sha1").update(file.hash + derived).digest("hex")
144 : file.hash;
145
146 // Disk cache143 // Disk cache
147 const diskPath = diskCacheRoot.join(key);144 const diskPath = diskCacheRoot.join(key);
148 let diskFile = diskCache.get(diskPath);145 let diskFile = diskCache.get(diskPath);
...@@ -156,7 +153,7 @@ export async function read(...@@ -156,7 +153,7 @@ export async function read(
156 });153 });
157 if (diskStream) {154 if (diskStream) {
158 const isPending = diskFile.written.value !== diskFile.size;155 const isPending = diskFile.written.value !== diskFile.size;
159 log(`from ${isPending ? "pending " : ""}disk cache: ${rel}`);156 console.info(`from ${isPending ? "pending " : ""}disk cache: ${rel}`);
160 return {157 return {
161 stream: stream.Readable.toWeb(diskStream),158 stream: stream.Readable.toWeb(diskStream),
162 src: isPending ? "pending" : "hit",159 src: isPending ? "pending" : "hit",
...@@ -173,10 +170,10 @@ export async function read(...@@ -173,10 +170,10 @@ export async function read(
173 {170 {
174 let promise = parallelRequests.get(url);171 let promise = parallelRequests.get(url);
175 if (promise) {172 if (promise) {
176 log(`from other response: ${rel}`);173 console.info(`from other response: ${rel}`);
177 response = await promise;174 response = await promise;
178 } else {175 } else {
179 log(`cache miss: ${rel}`);176 console.info(`cache miss: ${rel}`);
180 promise = httpGet(url, {177 promise = httpGet(url, {
181 headers: token ? { Authorization: token } : {},178 headers: token ? { Authorization: token } : {},
182 });179 });
...@@ -197,18 +194,18 @@ export async function read(...@@ -197,18 +194,18 @@ export async function read(
197194
198 // Do not cache large assets195 // Do not cache large assets
199 if (size > 1_000_000_000) {196 if (size > 1_000_000_000) {
200 log(`skipping disk cache due to large asset: ${rel}`);197 console.info(`skipping disk cache due to large asset: ${rel}`);
201 return { stream: stream.Readable.toWeb(response), size, src: "ignored" };198 return { stream: stream.Readable.toWeb(response), size, src: "ignored" };
202 }199 }
203 // Bail out of the disk cache if it is contended200 // Bail out of the disk cache if it is contended
204 if (diskCache.unlockedCapacity < size) {201 if (diskCache.unlockedCapacity < size) {
205 log(`skipping disk cache due to contention: ${rel}`);202 console.info(`skipping disk cache due to contention: ${rel}`);
206 return { stream: stream.Readable.toWeb(response), size, src: "contended" };203 return { stream: stream.Readable.toWeb(response), size, src: "contended" };
207 }204 }
208 // IPs which request too many cache misses are temporarily205 // IPs which request too many cache misses are temporarily
209 // banned from writing into the disk cache to avoid abuse.206 // banned from writing into the disk cache to avoid abuse.
210 if (noWrite) {207 if (noWrite) {
211 log(`skipping disk cache due to config: ${rel}`);208 console.info(`skipping disk cache due to config: ${rel}`);
212 return { stream: stream.Readable.toWeb(response), size, src: "ratelimit" };209 return { stream: stream.Readable.toWeb(response), size, src: "ratelimit" };
213 }210 }
214211
...@@ -273,7 +270,7 @@ function openDiskCache() {...@@ -273,7 +270,7 @@ function openDiskCache() {
273 const index = split.indexOf(".clover");270 const index = split.indexOf(".clover");
274 return index === -1271 return index === -1
275 ? Path.createDirSync(clover, fileCache)272 ? Path.createDirSync(clover, fileCache)
276 : Path.createDirSync(...split.slice(0, index + 1), fileCache);273 : Path.createDirSync(split.slice(0, index + 1).join(Path.sep), fileCache);
277}274}
278275
279function writeDiskCache() {276function writeDiskCache() {
...@@ -284,7 +281,9 @@ function writeDiskCache() {...@@ -284,7 +281,9 @@ function writeDiskCache() {
284 },281 },
285 });282 });
286 diskCacheState.writeJsonFile(serialized)283 diskCacheState.writeJsonFile(serialized)
287 .catch(() => {});284 .catch((err) =>
285 console.error(`Failed to write ${diskCacheState.relative()}:`, err)
286 );
288}287}
289288
290async function getHandle(path: Path, flag: string, unlockOnFail?: Disposable) {289async function getHandle(path: Path, flag: string, unlockOnFail?: Disposable) {
...@@ -404,7 +403,7 @@ async function readStream(path: Path, opts: ReadStreamOptions) {...@@ -404,7 +403,7 @@ async function readStream(path: Path, opts: ReadStreamOptions) {
404 }403 }
405404
406 let closed = false;405 let closed = false;
407 return fs.createReadStream(path.raw, {406 return fs.createReadStream(path.toString(), {
408 fd: handle.fd,407 fd: handle.fd,
409 start: opts?.start ?? 0,408 start: opts?.start ?? 0,
410 end: opts?.end ?? Infinity,409 end: opts?.end ?? Infinity,
...@@ -433,7 +432,7 @@ async function writeStream(...@@ -433,7 +432,7 @@ async function writeStream(
433 let closed = false;432 let closed = false;
434433
435 const { memory, written } = file;434 const { memory, written } = file;
436 const writer = fs.createWriteStream(path.raw, {435 const writer = fs.createWriteStream(path.toString(), {
437 start: 0,436 start: 0,
438 fd: handle.fd,437 fd: handle.fd,
439 fs: {438 fs: {
...@@ -480,7 +479,6 @@ async function writeStream(...@@ -480,7 +479,6 @@ async function writeStream(
480 return writer;479 return writer;
481}480}
482481
483import * as console from "@paperclover/console";
484import * as util from "node:util";482import * as util from "node:util";
485import * as fsCallbacks from "node:fs";483import * as fsCallbacks from "node:fs";
486import * as fs from "#sitegen/fs";484import * as fs from "#sitegen/fs";
...@@ -491,6 +489,7 @@ import * as crypto from "node:crypto";...@@ -491,6 +489,7 @@ import * as crypto from "node:crypto";
491import * as http from "node:http";489import * as http from "node:http";
492import * as https from "node:https";490import * as https from "node:https";
493import * as paths from "./paths.ts";491import * as paths from "./paths.ts";
492import * as log from "lib/log.ts";
494import { MediaFile } from "./models/MediaFile.ts";493import { MediaFile } from "./models/MediaFile.ts";
495import type { ReadableStream } from "node:stream/web";494import type { ReadableStream } from "node:stream/web";
496import { Lru } from "#sitegen/lru";495import { Lru } from "#sitegen/lru";
src/file-viewer/ffmpeg.ts+11-7
...@@ -68,22 +68,26 @@ export async function spawn(options: SpawnOptions) {...@@ -68,22 +68,26 @@ export async function spawn(options: SpawnOptions) {
68 }68 }
69}69}
7070
71// this is janky71export async function probeStreams(path: Path) {
72export async function probeAudioStreams(path: Path) {
73 const { stdout } = await subprocess.spawnAndWait({72 const { stdout } = await subprocess.spawnAndWait({
74 cmd: [73 cmd: [
75 "ffprobe",74 "ffprobe",
76 "-v",75 "-v",
77 "quiet",76 "error",
78 "-select_streams",
79 "a",
80 "-show_entries",77 "-show_entries",
81 "stream=index",78 "stream=codec_type",
82 path.toString(),79 path.toString(),
80 "-of",
81 "default=noprint_wrappers=1:nokey=1",
83 ],82 ],
84 stdio: "pipe",83 stdio: "pipe",
85 });84 });
86 return stdout?.byteLength ?? 0 > 0;85
86 const lines = new TextDecoder().decode(stdout!).trim().split("\n");
87 return {
88 audio: lines.includes("audio"),
89 video: lines.includes("video"),
90 };
87}91}
8892
89export class Parse {93export class Parse {
src/file-viewer/models/MediaFile.ts+3-1
...@@ -117,7 +117,9 @@ export class MediaFile {...@@ -117,7 +117,9 @@ export class MediaFile {
117 .filter((file) => !file.basename.startsWith("."));117 .filter((file) => !file.basename.startsWith("."));
118 }118 }
119 getPublicChildren() {119 getPublicChildren() {
120 const children = MediaFile.getChildren(this.id);120 const children = MediaFile.getChildren(this.id).filter((x) =>
121 !x.basename.startsWith("_unlisted")
122 );
121 if (FilePermissions.getByPrefix(this.path) == 0) {123 if (FilePermissions.getByPrefix(this.path) == 0) {
122 return children.filter(({ path }) => FilePermissions.getExact(path) == 0);124 return children.filter(({ path }) => FilePermissions.getExact(path) == 0);
123 }125 }
src/file-viewer/models/derived.ts+67-20
...@@ -21,6 +21,7 @@ db.table(...@@ -21,6 +21,7 @@ db.table(
21 file text not null,21 file text not null,
22 root integer not null,22 root integer not null,
23 size integer not null, -- bytes23 size integer not null, -- bytes
24 hash text not null, -- sha1
24 foreign key(root) references derived_roots(id) on delete cascade25 foreign key(root) references derived_roots(id) on delete cascade
25 );26 );
26 create table if not exists derived_refs (27 create table if not exists derived_refs (
...@@ -44,15 +45,19 @@ let ongoing = new Map<string, Promise<number>>();...@@ -44,15 +45,19 @@ let ongoing = new Map<string, Promise<number>>();
4445
45/** produce a derived */46/** produce a derived */
46export async function produce(47export async function produce(
47 file: MediaFile,48 { mediaFile: file, node, subkey, cores, producer }: {
48 subkey: string,49 mediaFile: MediaFile;
49 producer: (root: Path) => Promise<void>,50 node: progress.Node;
51 subkey: string;
52 cores: number;
53 producer: (root: Path) => Promise<void>;
54 },
50) {55) {
51 const key = `${file.hash}/${subkey}`;56 const key = `${file.hash}/${subkey}`;
52 const current = ongoing.get(key);57 const current = ongoing.get(key);
53 let root: number | null = null;58 let root: number | null = null;
54 if (current) {59 if (current) {
55 await current;60 root = await current;
56 } else {brk: {61 } else {brk: {
57 root = getRootQuery.get({ key })?.id ?? null;62 root = getRootQuery.get({ key })?.id ?? null;
58 if (root) break brk;63 if (root) break brk;
...@@ -60,21 +65,57 @@ export async function produce(...@@ -60,21 +65,57 @@ export async function produce(
60 ongoing.set(key, promise);65 ongoing.set(key, promise);
61 try {66 try {
62 const tmp = workDir.join(key);67 const tmp = workDir.join(key);
63 await tmp.makeOrEmptyDir();68 node.hidden = true;
64 await producer(tmp);69 const filesWithStats = await queue.run({
65 const filesWithStats = await Promise.all(70 cores,
66 (await tmp.readDir())71 async run() {
67 .filter((x) => !x.base.startsWith("tmp."))72 node.hidden = false;
68 .map(async (x) => [x, await x.stat()] as const),73 await tmp.delete({ recursive: true, force: true });
69 );74 await tmp.makeOrEmptyDir();
75 await producer(tmp);
76 const sha1queue = new queue.PriorityQueue(cores);
77 return await Promise.all(
78 (await tmp.readDir({ recursive: true }))
79 .filter((path) => !path.base.startsWith("tmp."))
80 .map(async (path) =>
81 sha1queue.run({
82 async run() {
83 using _ = node.start("hash " + path.relative(tmp));
84 const stats = await path.stat();
85 if (stats.isDirectory()) {
86 return [path, stats.size, null] as const;
87 }
88 const hash = await new Promise<string>(
89 (resolve, reject) => {
90 const reader = fs.createReadStream(path.toString());
91 reader.on("error", reject);
92
93 const hasher = crypto.createHash("sha1").setEncoding(
94 "hex",
95 );
96 hasher.on("error", reject);
97 hasher.on("readable", () => resolve(hasher.read()));
98
99 reader.pipe(hasher);
100 },
101 );
102 return [path, stats.size, hash] as const;
103 },
104 })
105 ),
106 );
107 },
108 });
70 db.node.exec("BEGIN");109 db.node.exec("BEGIN");
71 try {110 try {
72 root = insertRootQuery.getNonNull({ key, date: Date.now() }).id;111 root = insertRootQuery.getNonNull({ key, date: Date.now() }).id;
73 for (const [path, stats] of filesWithStats) {112 for (const [path, size, hash] of filesWithStats) {
113 if (hash === null) continue;
74 insertFileQuery.run({114 insertFileQuery.run({
75 root,115 root,
76 file: path.relative(tmp),116 file: path.relative(tmp),
77 size: stats.size,117 size,
118 hash,
78 });119 });
79 }120 }
80 } catch (e) {121 } catch (e) {
...@@ -91,7 +132,7 @@ export async function produce(...@@ -91,7 +132,7 @@ export async function produce(
91 ongoing.delete(key);132 ongoing.delete(key);
92 }133 }
93 }}134 }}
94 ASSERT(root);135 ASSERT(root != null);
95 insertRefQuery.run({ root, file: file.id });136 insertRefQuery.run({ root, file: file.id });
96 return {137 return {
97 [Symbol.dispose]: () => {138 [Symbol.dispose]: () => {
...@@ -102,21 +143,23 @@ export async function produce(...@@ -102,21 +143,23 @@ export async function produce(
102143
103const getDerivedAssetQuery = db.prepare<144const getDerivedAssetQuery = db.prepare<
104 [number, string],145 [number, string],
105 { size: number; key: string }146 { size: number; key: string; hash: string; date: number }
106>(/* SQL */ `147>(/* SQL */ `
107 select df.size, rt.key148 select df.size, df.hash, rt.date, rt.key
108 from derived_refs dr149 from derived_refs dr
109 join derived_files df on df.root = dr.root150 join derived_files df on df.root = dr.root
110 join derived_roots rt on df.root = rt.id151 join derived_roots rt on df.root = rt.id
111 where dr.file = ? and df.file like ?152 where dr.file = ? and df.file = ?
112`);153`);
113154
114export function get(file: MediaFile, subPath: string) {155export function get(file: MediaFile, subPath: string) {
115 const row = getDerivedAssetQuery.get(file.id, subPath) ?? null;156 const row = getDerivedAssetQuery.get(file.id, subPath) ?? null;
116 if (row) {157 if (row) {
117 return {158 return {
118 path: Path.resolve(derivedFileRoot).join(row.key, subPath),159 path: row.key + "/" + subPath,
119 size: row.size,160 size: row.size,
161 hash: row.hash,
162 date: new Date(row.date),
120 };163 };
121 }164 }
122 return null;165 return null;
...@@ -134,9 +177,9 @@ const getRootQuery = db.prepare<[{ key: string }], { id: number }>(/* SQL */ `...@@ -134,9 +177,9 @@ const getRootQuery = db.prepare<[{ key: string }], { id: number }>(/* SQL */ `
134`);177`);
135178
136const insertFileQuery = db.prepare<179const insertFileQuery = db.prepare<
137 [{ file: string; root: number; size: number }]180 [{ file: string; root: number; size: number; hash: string }]
138>(/* SQL */ `181>(/* SQL */ `
139 insert into derived_files (file, root, size) values ($file, $root, $size);182 insert into derived_files (file, root, size, hash) values ($file, $root, $size, $hash);
140`);183`);
141184
142const insertRefQuery = db.prepare<[{ root: number; file: number }]>(/* SQL */ `185const insertRefQuery = db.prepare<[{ root: number; file: number }]>(/* SQL */ `
...@@ -154,4 +197,8 @@ const deleteRefQuery = db.prepare<[{ root: number; file: number }]>(/* SQL */ `...@@ -154,4 +197,8 @@ const deleteRefQuery = db.prepare<[{ root: number; file: number }]>(/* SQL */ `
154import { getDb } from "#sitegen/sqlite";197import { getDb } from "#sitegen/sqlite";
155import type { MediaFile } from "./MediaFile.ts";198import type { MediaFile } from "./MediaFile.ts";
156import { Path } from "#sitegen/path";199import { Path } from "#sitegen/path";
200import * as fs from "node:fs";
201import * as crypto from "node:crypto";
202import * as queue from "lib/queue.ts";
203import * as progress from "lib/log/progress.ts";
157import { derivedFileRoot } from "../paths.ts";204import { derivedFileRoot } from "../paths.ts";
src/file-viewer/rsync.ts+12-13
...@@ -23,34 +23,35 @@ export const defaultExtraOptions = [...@@ -23,34 +23,35 @@ export const defaultExtraOptions = [
2323
24export interface SpawnOptions {24export interface SpawnOptions {
25 args: string[];25 args: string[];
26 title: string;
27 rsync?: string;26 rsync?: string;
28 progress?: Progress;27 progress: progress.Node;
29 cwd: string;28 cwd: string;
30}29}
3130
32export async function spawn(options: SpawnOptions) {31export async function spawn(options: SpawnOptions) {
33 const { rsync = "rsync", args, title, cwd } = options;32 const { rsync = "rsync", args, cwd } = options;
33 using node = options.progress;
34 const proc = child_process.spawn(rsync, [...defaultExtraOptions, ...args], {34 const proc = child_process.spawn(rsync, [...defaultExtraOptions, ...args], {
35 stdio: ["ignore", "pipe", "pipe"],35 stdio: ["ignore", "pipe", "pipe"],
36 cwd,36 cwd,
37 });37 });
38 const parser = new Parse();38 const parser = new Parse();
39 const bar = options.progress ?? new Progress({ text: title });
40 let running = true;39 let running = true;
4140
42 const stdoutSplitter = readline.createInterface({ input: proc.stdout });41 const stdoutSplitter = readline.createInterface({ input: proc.stdout });
43 const stderrSplitter = readline.createInterface({ input: proc.stderr });42 const stderrSplitter = readline.createInterface({ input: proc.stderr });
4443
44 const title = node.text;
45
45 const handleLine = (line: string) => {46 const handleLine = (line: string) => {
46 const result = parser.onLine(line);47 const result = parser.onLine(line);
47 if (result.kind === "ignore") {48 if (result.kind === "ignore") {
48 return;49 return;
49 } else if (result.kind === "log") {50 } else if (result.kind === "log") {
50 console[result.level](result.message);51 node.log[result.level](result.message);
51 } else if (result.kind === "count") {52 } else if (result.kind === "count") {
52 if (!running) return;53 if (!running) return;
53 bar.text = `${result.files} files...`;54 node.text = `${title} (${result.files} files...)`;
54 } else if (result.kind === "progress") {55 } else if (result.kind === "progress") {
55 if (!running) return;56 if (!running) return;
56 const {57 const {
...@@ -61,8 +62,8 @@ export async function spawn(options: SpawnOptions) {...@@ -61,8 +62,8 @@ export async function spawn(options: SpawnOptions) {
61 currentFile,62 currentFile,
62 speed,63 speed,
63 } = result;64 } = result;
64 bar.value = transferNumber;65 node.value = transferNumber;
65 bar.total = totalFiles;66 node.estimate = totalFiles;
66 const extras = [67 const extras = [
67 formatSize(bytesTransferred),68 formatSize(bytesTransferred),
68 (totalFiles > filesToCheck)69 (totalFiles > filesToCheck)
...@@ -73,8 +74,8 @@ export async function spawn(options: SpawnOptions) {...@@ -73,8 +74,8 @@ export async function spawn(options: SpawnOptions) {
73 const fileName = currentFile.length > 2074 const fileName = currentFile.length > 20
74 ? `${currentFile.slice(0, 3)}..${currentFile.slice(-15)}`75 ? `${currentFile.slice(0, 3)}..${currentFile.slice(-15)}`
75 : currentFile;76 : currentFile;
76 bar.text = `[${transferNumber}/${totalFiles}] ${fileName} ${77 node.text = `${title} - ${fileName}${
77 extras.length > 0 ? `(${extras})` : ""78 extras.length > 0 ? `( ${extras})` : ""
78 }`;79 }`;
79 } else result satisfies never;80 } else result satisfies never;
80 };81 };
...@@ -90,10 +91,8 @@ export async function spawn(options: SpawnOptions) {...@@ -90,10 +91,8 @@ export async function spawn(options: SpawnOptions) {
90 e.args = [rsync, ...args].join(" ");91 e.args = [rsync, ...args].join(" ");
91 e.code = code;92 e.code = code;
92 e.signal = signal;93 e.signal = signal;
93 bar.error(e.message);
94 return e;94 return e;
95 }95 }
96 bar.success(title);
97}96}
9897
99export class Parse {98export class Parse {
...@@ -184,5 +183,5 @@ export class Parse {...@@ -184,5 +183,5 @@ export class Parse {
184import * as child_process from "node:child_process";183import * as child_process from "node:child_process";
185import * as readline from "node:readline";184import * as readline from "node:readline";
186import events from "node:events";185import events from "node:events";
187import { Progress } from "@paperclover/console/Progress";186import * as progress from "lib/log/progress.ts";
188import { formatSize } from "@/file-viewer/format.ts";187import { formatSize } from "@/file-viewer/format.ts";
src/file-viewer/transcode-rules.ts+65-58
...@@ -116,24 +116,23 @@ export async function getVideoInputArgs(path: Path): Promise<InputArgs> {...@@ -116,24 +116,23 @@ export async function getVideoInputArgs(path: Path): Promise<InputArgs> {
116 ],116 ],
117 audio: audioSrc117 audio: audioSrc
118 ? ["-i", String(audioSrc)]118 ? ["-i", String(audioSrc)]
119 : (await ffmpeg.probeAudioStreams(path))119 : (await ffmpeg.probeStreams(path)).audio
120 ? ["-i", path.toString()]120 ? ["-i", path.toString()]
121 : null,121 : null,
122 };122 };
123 }123 }
124 }124 }
125 const streams = await ffmpeg.probeStreams(path);
125 return {126 return {
126 both: ["-i", path.toString()],127 both: ["-i", path.toString()],
127 video: ["-i", path.toString()],128 video: streams.video ? ["-i", path.toString()] : null,
128 audio: (await ffmpeg.probeAudioStreams(path))129 audio: streams.audio ? ["-i", path.toString()] : null,
129 ? ["-i", path.toString()]
130 : null,
131 };130 };
132}131}
133132
134export interface InputArgs {133export interface InputArgs {
135 video: string[];
136 both: string[];134 both: string[];
135 video: string[] | null;
137 audio: string[] | null;136 audio: string[] | null;
138}137}
139138
...@@ -181,6 +180,8 @@ export function getOpusAudioArgs(...@@ -181,6 +180,8 @@ export function getOpusAudioArgs(
181 const { audioKbit } = opus;180 const { audioKbit } = opus;
182 const cmd = [...audioInputArgs];181 const cmd = [...audioInputArgs];
183182
183 cmd.push("-vn");
184
184 cmd.push("-c:a", "libopus");185 cmd.push("-c:a", "libopus");
185 cmd.push("-b:a", audioKbit + "k");186 cmd.push("-b:a", audioKbit + "k");
186187
...@@ -191,27 +192,21 @@ export function getOpusAudioArgs(...@@ -191,27 +192,21 @@ export function getOpusAudioArgs(
191}192}
192193
193export function getMpegDashArgs(194export function getMpegDashArgs(
194 videos: Array<Path>,195 videos: Path[],
195 audios: Array<Path>,196 audios: Path[],
196 outDir: Path,197 outDir: Path,
197) {198) {
198 const cmd = [];199 const cmd = [];
199200
200 // inputs201 // inputs
201 videos.forEach((file) => cmd.push("-i", file.toString()));202 videos.forEach((v) => cmd.push("-i", v.toString()));
202 audios.forEach((file) => cmd.push("-i", file.toString()));203 audios.forEach((a) => cmd.push("-i", a.toString()));
203204
204 // map streams205 // map streams
205 videos.forEach((_, i) => {206 videos.forEach((_, i) => cmd.push("-map", `${i}:0`));
206 cmd.push("-map", `${i}:0`);207 audios.forEach((_, i) => cmd.push("-map", `${videos.length + i}:0`));
207 });
208 audios.forEach((_, i) => {
209 cmd.push("-map", `${videos.length + i}:0`);
210 });
211
212 // copy codecs
213 cmd.push("-c", "copy");
214208
209 cmd.push("-c", "copy");
215 cmd.push("-f", "dash");210 cmd.push("-f", "dash");
216 cmd.push("-seg_duration", "4");211 cmd.push("-seg_duration", "4");
217 cmd.push("-init_seg_name", "d/$RepresentationID$.init.$ext$");212 cmd.push("-init_seg_name", "d/$RepresentationID$.init.$ext$");
...@@ -219,11 +214,12 @@ export function getMpegDashArgs(...@@ -219,11 +214,12 @@ export function getMpegDashArgs(
219 cmd.push("-use_timeline", "1");214 cmd.push("-use_timeline", "1");
220 cmd.push("-use_template", "1");215 cmd.push("-use_template", "1");
221216
222 // build adaptation sets string217 // calculate actual dimensions per video
223 const videoAdaptationSets = videos.map((_, i) => `id=${i},streams=${i}`)218 const videoAdaptationSets = videos
219 .map((_, i) => `id=${i},streams=${i}`)
224 .join(" ");220 .join(" ");
225 let adaptationSets = videoAdaptationSets;
226221
222 let adaptationSets = videoAdaptationSets;
227 if (audios.length > 0) {223 if (audios.length > 0) {
228 const audioId = videos.length;224 const audioId = videos.length;
229 const audioIndices = audios.map((_, i) => videos.length + i).join(",");225 const audioIndices = audios.map((_, i) => videos.length + i).join(",");
...@@ -231,7 +227,6 @@ export function getMpegDashArgs(...@@ -231,7 +227,6 @@ export function getMpegDashArgs(
231 }227 }
232228
233 cmd.push("-adaptation_sets", adaptationSets);229 cmd.push("-adaptation_sets", adaptationSets);
234
235 cmd.push("-y");230 cmd.push("-y");
236 cmd.push(outDir.join("dash.mpd").toString());231 cmd.push(outDir.join("dash.mpd").toString());
237232
...@@ -257,53 +252,65 @@ export function getH264HlsArgs(...@@ -257,53 +252,65 @@ export function getH264HlsArgs(
257) {252) {
258 const cmd = [...inputArgs.both];253 const cmd = [...inputArgs.both];
259254
260 // map streams255 if (inputArgs.audio) {
261 hlsPresets.forEach(() => {256 // with audio: create v+a pairs for each preset
262 cmd.push("-map", "0:v:0");257 hlsPresets.forEach(() => {
263 if (inputArgs.audio) {258 cmd.push("-map", "0:v:0", "-map", "0:a:0");
264 cmd.push("-map", "0:a:0");259 });
265 }
266 });
267260
268 // encode each variant261 hlsPresets.forEach((preset, i) => {
269 hlsPresets.forEach((preset, i) => {
270 if (inputArgs.audio) {
271 const vIdx = i * 2;262 const vIdx = i * 2;
272 const aIdx = i * 2 + 1;263 const aIdx = i * 2 + 1;
273 cmd.push(`-c:v:${vIdx}`, "libx264");264 // deno-fmt-ignore
274 cmd.push(`-b:v:${vIdx}`, preset.videoBitrate);265 cmd.push(
275 cmd.push(`-c:a:${aIdx}`, "aac");266 `-c:${vIdx}`, "libx264",
276 cmd.push(`-b:a:${aIdx}`, preset.audioBitrate);267 `-b:v:${vIdx}`, preset.videoBitrate,
277 cmd.push(`-filter:v:${vIdx}`, `scale=-2:min(${preset.maxHeight}\\,ih)`);268 `-filter:${vIdx}`, `scale=-2:min(${preset.maxHeight}\\,ih)`,
278 } else {269 `-c:${aIdx}`, "aac",
279 cmd.push(`-c:v:${i}`, "libx264");270 `-b:a:${aIdx}`, preset.audioBitrate,
280 cmd.push(`-b:v:${i}`, preset.videoBitrate);271 );
281 cmd.push(`-filter:v:${i}`, `scale=-2:min(${preset.maxHeight}\\,ih)`);272 });
282 }
283 });
284
285 cmd.push("-f", "hls");
286 cmd.push("-hls_time", "2");
287 cmd.push("-hls_list_size", "0");
288 cmd.push("-hls_segment_type", "fmp4");
289 cmd.push("-hls_allow_cache", "1");
290273
291 if (inputArgs.audio) {
292 cmd.push(274 cmd.push(
293 "-var_stream_map",275 "-var_stream_map",
294 hlsPresets.map((_, i) => `v:${i * 2},a:${i * 2 + 1}`).join(" "),276 hlsPresets.map((_, i) => `v:${i},a:${i}`).join(" "),
295 );277 );
296 } else {278 } else {
297 cmd.push("-var_stream_map", hlsPresets.map((_, i) => `v:${i}`).join(" "));279 // video only: single stream per preset
298 }280 hlsPresets.forEach(() => {
281 cmd.push("-map", "0:v:0");
282 });
283
284 hlsPresets.forEach((preset, i) => {
285 // deno-fmt-ignore
286 cmd.push(
287 `-c:v:${i}`, "libx264",
288 `-b:v:${i}`, preset.videoBitrate,
289 `-filter:v:${i}`, `scale=-2:min(${preset.maxHeight}\\,ih)`,
290 );
291 });
299292
300 cmd.push("-master_pl_name", "master.m3u8");293 cmd.push(
301 cmd.push("-hls_segment_filename", "hls.%v.%03d.ts");294 "-var_stream_map",
295 hlsPresets.map((_, i) => `v:${i}`).join(" "),
296 );
297 }
302298
303 cmd.push("-y");299 // deno-fmt-ignore
304 cmd.push(outDir.join("hls.%v.m3u8").toString());300 cmd.push(
301 "-f", "hls",
302 "-hls_time", "2",
303 "-hls_list_size", "0",
304 "-hls_segment_type", "fmp4",
305 "-hls_allow_cache", "1",
306 "-master_pl_name", "master.m3u8",
307 "-hls_segment_filename", "hls.%v.%03d.ts",
308 "-y",
309 outDir.join("hls.%v.m3u8").toString(),
310 );
305311
306 return cmd;312 return cmd;
307}313}
314
308import type { Path } from "#sitegen/path";315import type { Path } from "#sitegen/path";
309import * as ffmpeg from "./ffmpeg.ts";316import * as ffmpeg from "./ffmpeg.ts";
src/file-viewer/views/clofi.tsx+9-30
...@@ -71,7 +71,6 @@ export default function MediaList({...@@ -71,7 +71,6 @@ export default function MediaList({
71 hasCotyledonCookie: boolean;71 hasCotyledonCookie: boolean;
72}) {72}) {
73 addScript("./clofi.client.ts");73 addScript("./clofi.client.ts");
74 addScript("@/tags/hls-polyfill.client.ts");
7574
76 const dirs: MediaFile[] = [];75 const dirs: MediaFile[] = [];
77 let dir: MediaFile | null = file;76 let dir: MediaFile | null = file;
...@@ -356,10 +355,8 @@ function DirView({...@@ -356,10 +355,8 @@ function DirView({
356 />355 />
357 );356 );
358 }357 }
359 const isCotyledon = parseInt(dir.path.split("/")?.[1]) < 2025;358 const isCotyledon = parseInt(UNWRAP(dir.path.split("/")[1])) < 2025;
360 const unsortedFiles = dir359 const unsortedFiles = dir.getPublicChildren();
361 .getPublicChildren()
362 .filter((f) => !f.basenameWithoutExt.startsWith("_unlisted"));
363 const sorter = dir.dirsort360 const sorter = dir.dirsort
364 ? sorterFromDirSort(dir.dirsort)361 ? sorterFromDirSort(dir.dirsort)
365 : isNumericallyOrdered(unsortedFiles)362 : isNumericallyOrdered(unsortedFiles)
...@@ -639,31 +636,19 @@ function TextView({...@@ -639,31 +636,19 @@ function TextView({
639 siblingFiles?: MediaFile[];636 siblingFiles?: MediaFile[];
640}) {637}) {
641 const contents = file.contents;638 const contents = file.contents;
642
643 if (file.path.startsWith("/2021/phoenix-write/maps")) {639 if (file.path.startsWith("/2021/phoenix-write/maps")) {
644 const basename = file.basename;640 const basename = file.basename;
645 if (basename.includes("map") && basename.endsWith(".txt")) {641 if (basename.includes("map") && basename.endsWith(".txt")) {
646 return (642 return (
647 <pre643 <pre style="white-space:normal;max-width:73ch">{render.raw(highlightHashComments(contents))}</pre>
648 style="white-space:normal;max-width:73ch"
649 dangerouslySetInnerHTML={{
650 __html: highlightHashComments(contents),
651 }}
652 ></pre>
653 );644 );
654 }645 }
655 }646 }
656647
657 return (648 return (
658 <pre649 <pre style={{ "white-space": "pre-wrap", "max-width": "70ch" }}>
659 style="white-space:pre-wrap;max-width:70ch"650 {render.raw(highlightLinksInTextView( contents, siblingFiles.filter((f) => f.kind === MediaFileKind.file)))}
660 dangerouslySetInnerHTML={{651 </pre>
661 __html: highlightLinksInTextView(
662 contents,
663 siblingFiles.filter((f) => f.kind === MediaFileKind.file),
664 ),
665 }}
666 ></pre>
667 );652 );
668}653}
669654
...@@ -673,10 +658,8 @@ function ChatView({ file }: { file: MediaFile }) {...@@ -673,10 +658,8 @@ function ChatView({ file }: { file: MediaFile }) {
673 <div658 <div
674 style="max-width:70ch"659 style="max-width:70ch"
675 class="convo"660 class="convo"
676 dangerouslySetInnerHTML={{
677 __html: highlightConvo(contents),
678 }}
679 >661 >
662 {render.raw(highlightConvo(contents))}
680 </div>663 </div>
681 );664 );
682}665}
...@@ -690,12 +673,7 @@ function CodeView({ file }: { file: MediaFile }) {...@@ -690,12 +673,7 @@ function CodeView({ file }: { file: MediaFile }) {
690 return <DownloadViewCodeNotComputed file={file} />;673 return <DownloadViewCodeNotComputed file={file} />;
691 }674 }
692 return (675 return (
693 <pre676 <pre style="white-space:pre-wrap">{render.raw(highlightedContents)}</pre>
694 style="white-space:pre-wrap"
695 dangerouslySetInnerHTML={{
696 __html: highlightedContents,
697 }}
698 />
699 );677 );
700}678}
701679
...@@ -957,3 +935,4 @@ import {...@@ -957,3 +935,4 @@ import {
957import { ForEveryone } from "@/file-viewer/cotyledon.tsx";935import { ForEveryone } from "@/file-viewer/cotyledon.tsx";
958import * as mime from "#sitegen/mime";936import * as mime from "#sitegen/mime";
959import * as sort from "../sort.ts";937import * as sort from "../sort.ts";
938import * as render from "#engine/render";
src/pages/in-the-summer.css created+11
...@@ -0,0 +1,11 @@
1body {
2 background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9IiM3NEM3ODQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik00My43NSAyMDcuODEyQzQzLjc1IDE5OC4zMiAzNi4wNTQ5IDE5MC42MjUgMjYuNTYyNSAxOTAuNjI1QzE3LjA3MDEgMTkwLjYyNSA5LjM3NSAxOTguMzIgOS4zNzUgMjA3LjgxMkM5LjM3NSAyMTcuMzA1IDE3LjA3MDEgMjI1IDI2LjU2MjUgMjI1VjIyOC4xMjVDMTUuMzQ0MiAyMjguMTI1IDYuMjUgMjE5LjAzMSA2LjI1IDIwNy44MTJDNi4yNSAxOTYuNTk0IDE1LjM0NDIgMTg3LjUgMjYuNTYyNSAxODcuNUMzNy43ODA4IDE4Ny41IDQ2Ljg3NSAxOTYuNTk0IDQ2Ljg3NSAyMDcuODEyQzQ2Ljg3NSAyMTkuMDMxIDM3Ljc4MDggMjI4LjEyNSAyNi41NjI1IDIyOC4xMjVWMjI1QzM2LjA1NDkgMjI1IDQzLjc1IDIxNy4zMDUgNDMuNzUgMjA3LjgxMlpNMTQzLjc1IDE4NS45MzhDMTQzLjc1IDE3Ni40NDUgMTM2LjA1NSAxNjguNzUgMTI2LjU2MiAxNjguNzVDMTE3LjA3IDE2OC43NSAxMDkuMzc1IDE3Ni40NDUgMTA5LjM3NSAxODUuOTM4QzEwOS4zNzUgMTk1LjQzIDExNy4wNyAyMDMuMTI1IDEyNi41NjIgMjAzLjEyNVYyMDYuMjVDMTE1LjM0NCAyMDYuMjUgMTA2LjI1IDE5Ny4xNTYgMTA2LjI1IDE4NS45MzhDMTA2LjI1IDE3NC43MTkgMTE1LjM0NCAxNjUuNjI1IDEyNi41NjIgMTY1LjYyNUMxMzcuNzgxIDE2NS42MjUgMTQ2Ljg3NSAxNzQuNzE5IDE0Ni44NzUgMTg1LjkzOEMxNDYuODc1IDE5Ny4xNTYgMTM3Ljc4MSAyMDYuMjUgMTI2LjU2MiAyMDYuMjVWMjAzLjEyNUMxMzYuMDU1IDIwMy4xMjUgMTQzLjc1IDE5NS40MyAxNDMuNzUgMTg1LjkzOFpNMTI4LjEyNSAxMDQuNjg4QzEyOC4xMjUgOTUuMTk1MSAxMjAuNDMgODcuNSAxMTAuOTM4IDg3LjVDMTAxLjQ0NSA4Ny41IDkzLjc1IDk1LjE5NTEgOTMuNzUgMTA0LjY4OEM5My43NSAxMTQuMTggMTAxLjQ0NSAxMjEuODc1IDExMC45MzggMTIxLjg3NVYxMjVDOTkuNzE5MiAxMjUgOTAuNjI1IDExNS45MDYgOTAuNjI1IDEwNC42ODhDOTAuNjI1IDkzLjQ2OTIgOTkuNzE5MiA4NC4zNzUgMTEwLjkzOCA4NC4zNzVDMTIyLjE1NiA4NC4zNzUgMTMxLjI1IDkzLjQ2OTIgMTMxLjI1IDEwNC42ODhDMTMxLjI1IDExNS45MDYgMTIyLjE1NiAxMjUgMTEwLjkzOCAxMjVWMTIxLjg3NUMxMjAuNDMgMTIxLjg3NSAxMjguMTI1IDExNC4xOCAxMjguMTI1IDEwNC42ODhaTTU2LjI1IDg5LjA2MjVDNTYuMjUgNzkuNTcwMSA0OC41NTQ5IDcxLjg3NSAzOS4wNjI1IDcxLjg3NUMyOS41NzAxIDcxLjg3NSAyMS44NzUgNzkuNTcwMSAyMS44NzUgODkuMDYyNUMyMS44NzUgOTguNTU0OSAyOS41NzAxIDEwNi4yNSAzOS4wNjI1IDEwNi4yNVYxMDkuMzc1QzI3Ljg0NDIgMTA5LjM3NSAxOC43NSAxMDAuMjgxIDE4Ljc1IDg5LjA2MjVDMTguNzUgNzcuODQ0MiAyNy44NDQyIDY4Ljc1IDM5LjA2MjUgNjguNzVDNTAuMjgwOCA2OC43NSA1OS4zNzUgNzcuODQ0MiA1OS4zNzUgODkuMDYyNUM1OS4zNzUgMTAwLjI4MSA1MC4yODA4IDEwOS4zNzUgMzkuMDYyNSAxMDkuMzc1VjEwNi4yNUM0OC41NTQ5IDEwNi4yNSA1Ni4yNSA5OC41NTQ5IDU2LjI1IDg5LjA2MjVaTTE5MC42MjUgMjMuNDM3NUMxOTAuNjI1IDEzLjk0NTEgMTgyLjkzIDYuMjUgMTczLjQzOCA2LjI1QzE2My45NDUgNi4yNSAxNTYuMjUgMTMuOTQ1MSAxNTYuMjUgMjMuNDM3NUMxNTYuMjUgMzIuOTI5OSAxNjMuOTQ1IDQwLjYyNSAxNzMuNDM4IDQwLjYyNVY0My43NUMxNjIuMjE5IDQzLjc1IDE1My4xMjUgMzQuNjU1OCAxNTMuMTI1IDIzLjQzNzVDMTUzLjEyNSAxMi4yMTkyIDE2Mi4yMTkgMy4xMjUgMTczLjQzOCAzLjEyNUMxODQuNjU2IDMuMTI1IDE5My43NSAxMi4yMTkyIDE5My43NSAyMy40Mzc1QzE5My43NSAzNC42NTU4IDE4NC42NTYgNDMuNzUgMTczLjQzOCA0My43NVY0MC42MjVDMTgyLjkzIDQwLjYyNSAxOTAuNjI1IDMyLjkyOTkgMTkwLjYyNSAyMy40Mzc1Wk00My43NSA3LjgxMjVDNDMuNzUgLTEuNjc5ODkgMzYuMDU0OSAtOS4zNzUgMjYuNTYyNSAtOS4zNzVDMTcuMDcwMSAtOS4zNzUgOS4zNzUgLTEuNjc5ODkgOS4zNzUgNy44MTI1QzkuMzc1IDE3LjMwNDkgMTcuMDcwMSAyNSAyNi41NjI1IDI1VjI4LjEyNUMxNS4zNDQyIDI4LjEyNSA2LjI1IDE5LjAzMDggNi4yNSA3LjgxMjVDNi4yNSAtMy40MDU3OCAxNS4zNDQyIC0xMi41IDI2LjU2MjUgLTEyLjVDMzcuNzgwOCAtMTIuNSA0Ni44NzUgLTMuNDA1NzggNDYuODc1IDcuODEyNUM0Ni44NzUgMTkuMDMwOCAzNy43ODA4IDI4LjEyNSAyNi41NjI1IDI4LjEyNVYyNUMzNi4wNTQ5IDI1IDQzLjc1IDE3LjMwNDkgNDMuNzUgNy44MTI1Wk0xNDMuNzUgLTE0LjA2MjVDMTQzLjc1IC0yMy41NTQ5IDEzNi4wNTUgLTMxLjI1IDEyNi41NjIgLTMxLjI1QzExNy4wNyAtMzEuMjUgMTA5LjM3NSAtMjMuNTU0OSAxMDkuMzc1IC0xNC4wNjI1QzEwOS4zNzUgLTQuNTcwMTEgMTE3LjA3IDMuMTI1IDEyNi41NjIgMy4xMjVWNi4yNUMxMTUuMzQ0IDYuMjUgMTA2LjI1IC0yLjg0NDIyIDEwNi4yNSAtMTQuMDYyNUMxMDYuMjUgLTI1LjI4MDggMTE1LjM0NCAtMzQuMzc1IDEyNi41NjIgLTM0LjM3NUMxMzcuNzgxIC0zNC4zNzUgMTQ2Ljg3NSAtMjUuMjgwOCAxNDYuODc1IC0xNC4wNjI1QzE0Ni44NzUgLTIuODQ0MjIgMTM3Ljc4MSA2LjI1IDEyNi41NjIgNi4yNVYzLjEyNUMxMzYuMDU1IDMuMTI1IDE0My43NSAtNC41NzAxMSAxNDMuNzUgLTE0LjA2MjVaIi8+CjxwYXRoIGQ9Ik0xMzMuOTE3IDI5LjIyOTdMNzQuNTQyMiA4OC42MDQ3TDcyLjMzMjggODYuMzk1M0wxMzEuNzA4IDI3LjAyMDNMMTMzLjkxNyAyOS4yMjk3WiIvPgo8cGF0aCBkPSJNNDEuNzI5NyAxMzguNjA1TC0xNy42NDUzIDE5Ny45OEwtMTkuODU0NyAxOTUuNzdMMzkuNTIwMyAxMzYuMzk1TDQxLjcyOTcgMTM4LjYwNVoiLz4KPHBhdGggZD0iTTE1MS4xMDUgMTEwLjQ4TDkxLjcyOTcgMTY5Ljg1NUw4OS41MjAzIDE2Ny42NDVMMTQ4Ljg5NSAxMDguMjdMMTUxLjEwNSAxMTAuNDhaIi8+CjxwYXRoIGQ9Ik0yMTAuNDggMTEwLjQ4TDE1MS4xMDUgMTY5Ljg1NUwxNDguODk1IDE2Ny42NDVMMjA4LjI3IDEwOC4yN0wyMTAuNDggMTEwLjQ4WiIvPgo8cGF0aCBkPSJNMTAuNDc5NyAxMTAuNDhMLTQ4Ljg5NTMgMTY5Ljg1NUwtNTEuMTA0NyAxNjcuNjQ1TDguMjcwMjYgMTA4LjI3TDEwLjQ3OTcgMTEwLjQ4WiIvPgo8cGF0aCBkPSJNOTcuOTc5NyAtMTEuMzk1M0wzOC42MDQ3IDQ3Ljk3OTdMMzYuMzk1MyA0NS43NzAzTDk1Ljc3MDMgLTEzLjYwNDdMOTcuOTc5NyAtMTEuMzk1M1oiLz4KPHBhdGggZD0iTTk3Ljk3OTcgMTg4LjYwNUwzOC42MDQ3IDI0Ny45OEwzNi4zOTUzIDI0NS43N0w5NS43NzAzIDE4Ni4zOTVMOTcuOTc5NyAxODguNjA1WiIvPgo8cGF0aCBkPSJNMTMuNjA0NyAzOC42MDQ3TC00NS43NzAzIDk3Ljk3OTdMLTQ3Ljk3OTcgOTUuNzcwM0wxMS4zOTUzIDM2LjM5NTNMMTMuNjA0NyAzOC42MDQ3WiIvPgo8cGF0aCBkPSJNMjQxLjczIDEzOC42MDVMMTgyLjM1NSAxOTcuOThMMTgwLjE0NSAxOTUuNzdMMjM5LjUyIDEzNi4zOTVMMjQxLjczIDEzOC42MDVaIi8+CjxwYXRoIGQ9Ik0yMTMuNjA1IDM4LjYwNDdMMTU0LjIzIDk3Ljk3OTdMMTUyLjAyIDk1Ljc3MDNMMjExLjM5NSAzNi4zOTUzTDIxMy42MDUgMzguNjA0N1oiLz4KPHBhdGggZD0iTTgxLjY5NzQgMTE0LjM0NUw1MS41NDg2IDEyMS40MDdMNzIuMTc0IDE0NC4xMDhMODEuNjk3NCAxMTQuMzQ1WiIvPgo8cGF0aCBkPSJNNjMuOTUyOCAxNTYuNzczTDQyLjEwNjggMTc4LjcxOEw3MS42MjAyIDE4Ny4wNjhMNjMuOTUyOCAxNTYuNzczWiIvPgo8cGF0aCBkPSJNODAuOTQyIDE1LjYyNUw4Ni44MzMzIDQ2LjAyNDNMMTEwLjMxNSAyNi4yOTIxTDgwLjk0MiAxNS42MjVaIi8+CjxwYXRoIGQ9Ik0xMjUuNDE0IDU5LjYzOTVMMTQ4LjM0OSA4MC40NDM1TDE1NS4zMiA1MC41NzQ1TDEyNS40MTQgNTkuNjM5NVoiLz4KPHBhdGggZD0iTS0yMC43MzI4IDEwMi42Nkw5LjU3NTM0IDk2LjMxNjRMLTEwLjUwNDkgNzMuMTMxNUwtMjAuNzMyOCAxMDIuNjZaIi8+CjxwYXRoIGQ9Ik0xNzkuMjY4IDEwMi42NkwyMDkuNTc2IDk2LjMxNjRMMTg5LjQ5NiA3My4xMzE1TDE3OS4yNjggMTAyLjY2WiIvPgo8cGF0aCBkPSJNMTcyLjE1IDE5OC40MDJMMTg4Ljc4OCAxNzIuMjg3TDE1OC4xNzEgMTcwLjQ1M0wxNzIuMTUgMTk4LjQwMloiLz4KPC9zdmc+);
3}
4h1 {
5 margin-bottom: 0;
6 margin-top: 1rem;
7}
8.contain {
9 max-width: 80ch;
10 margin: auto;
11}
src/pages/in-the-summer.marko created+40
...@@ -0,0 +1,40 @@
1export const meta = { title: 'in the summer' };
2export const theme = {
3 bg: '#7DCC8C',
4 fg: '#000',
5 primary: '#9b3706',
6}
7
8import "./in-the-summer.css";
9
10<main style={ 'max-width': '110ch' }>
11<nav><a href="/">back to home</a></nav>
12<div.contain>
13 <header>
14 <h1>in the summer</h1>
15 <time style="margin-left:.5rem;opacity:.5">(2025-08-01)</time>
16 </header>
17
18 <p>
19 this is a song about the summer, and related feelings. written in 2022,
20 produced in 2025. i had a lot of fun playing with a much larger instrument
21 selection, and new 3d workflows. enjoy!
22 </p>
23</div>
24
25<clover-video file="/2025/in the summer/in the summer.mp4">
26 <@header><strong>music video</>: in the summer</>
27</>
28
29<div.contain>
30 <h2>downloads</h2>
31 <ul>
32 <li><a href="/file/2025/in%20the%20summer/in%20the%20summer.mp4?view=dl">in the summer.mp4</a> (AV1, requires modern player)</li>
33 <li><a href="/file/2025/in%20the%20summer/in%20the%20summer.mp3?view=dl">in the summer.mp3</a> (Music)</li>
34 <li><a href="/file/2025/in%20the%20summer/fragments">fragments/</a> (behind the scenes)</li>
35 </ul>
36 <h2>mentions on the q&a</h2>
37 // <QuestionsFor artifact='in-the-summer'/>
38</div>
39</main>
40
src/pages/waterfalls.marko created+47
...@@ -0,0 +1,47 @@
1import "./waterfalls.css";
2
3export const meta = { title: "waterfalls by paper clover" };
4export const theme = {
5 fg: "#fff",
6 bg: "#000",
7};
8
9<main class="waterfalls">
10 <div class="waterfalls_bg1" />
11 <main class="waterfalls">
12 <div class="contain">
13 <h1>waterfalls and gender identity update</h1>
14 <p>
15 this is a song about identity, jealousness, and the seasons. more
16 specifically, it's a personal account of how it felt realizing i was a
17 transgender woman. enjoy.
18 </p>
19 </div>
20 <clover-video
21 file="/2025/waterfalls/waterfalls.mp4"
22 poster="/2025/waterfalls/thumbnail.jpeg"
23 >
24 <@header><strong>music video</strong>: waterfalls</>
25 </>
26 <div class="contain">
27 <p>
28 it's nice to be <em>free</em>.
29 </p>
30 <p>like paper airplanes in the wind.</p>
31 <p>the stars shine in the sky. it's beautiful.</p>
32 <p>
33 love
34 <br />
35 –clover caruso
36 </p>
37 <br />
38
39 <p>
40 <strong>more</strong>: <br />
41 <a href="/file/2025/waterfalls/fragments">fragments</a>
42 <br />
43 <a href="/q+a">q&a page</a>
44 </p>
45 </div>
46 </main>
47</main>
src/pages/waterfalls.tsx deleted-66
...@@ -1,66 +0,0 @@
1import { Video } from "@/tags/Video.tsx";
2
3export const meta = { title: "waterfalls by paper clover" };
4
5export default () => (
6 <main class="waterfalls">
7 <div class="waterfalls_bg1" />
8 <main class="waterfalls">
9 <div class="contain">
10 <h1>waterfalls and gender identity update</h1>
11 <p>
12 this is a song about identity, jealousness, and the seasons. more
13 specifically, it's a personal account of how it felt realizing i was a
14 transgender woman. enjoy.
15 </p>
16 </div>
17 <Video
18 title={
19 <>
20 <strong>music video</strong>: waterfalls
21 </>
22 }
23 width={1920}
24 height={1080}
25 poster="/file/2025/waterfalls/thumbnail.jpeg"
26 posterHash="hMM%+pl^]eOvxCXU$$Ew4TB~LceBsAacSiOq3*F;MevmthFf+]$*WM]%E2KybarEt7NIJ3K$J8r=igxbTERP"
27 sources={[
28 "/file/2025/waterfalls/stream/hls.m3u8",
29 "/file/2025/waterfalls/waterfalls.webm",
30 ]}
31 downloads={[
32 "/file/2025/waterfalls/waterfalls.mp3",
33 "/file/2025/waterfalls/waterfalls.mp4",
34 "/file/2025/waterfalls/waterfalls.webm",
35 "/file/2025/waterfalls/lyrics.txt",
36 "/file/2025/waterfalls/",
37 ]}
38 />
39 <div class="contain">
40 <p>
41 it's nice to be <em>free</em>.
42 </p>
43 <p>like paper airplanes in the wind.</p>
44 <p>the stars shine in the sky. it's beautiful.</p>
45 <p>
46 love
47 <br />
48 –clover caruso
49 </p>
50 <br />
51
52 <p>
53 <strong>more</strong>: <br />
54 <a href="/file/2025/waterfalls/fragments">fragments</a>
55 <br />
56 <a href="/q+a">q&a page</a>
57 </p>
58 </div>
59 </main>
60 </main>
61);
62
63export const theme = {
64 fg: "#fff",
65 bg: "#000",
66};
src/tags/Video.tsx deleted-58
...@@ -1,58 +0,0 @@
1import "./video.css";
2import * as path from "node:path";
3import { addScript } from "#sitegen";
4import { PrecomputedBlurhash } from "./blurhash.tsx";
5
6export namespace Video {
7 export interface Props {
8 title: string;
9 width: number;
10 height: number;
11 sources: string[];
12 downloads: string[];
13 poster?: string;
14 posterHash?: string;
15 borderless?: boolean;
16 }
17}
18
19export function Video(
20 { title, sources, height, poster, posterHash, width, borderless }:
21 Video.Props,
22) {
23 addScript("./hls-polyfill.client.ts");
24 return (
25 <figure class={`video ${borderless ? "borderless" : ""}`}>
26 <figcaption>{title}</figcaption>
27 {posterHash && <PrecomputedBlurhash hash={posterHash} />}
28 {poster && <img src={poster} alt="waterfalls" />}
29 <video
30 controls
31 preload="none"
32 style={`width:100%;background:transparent;aspect-ratio:${
33 simplifyFraction(width, height)
34 }`}
35 poster="data:null"
36 >
37 {sources.map((src) => (
38 <source
39 src={src}
40 type={contentTypeFromExt(src)}
41 />
42 ))}
43 </video>
44 </figure>
45 );
46}
47export function contentTypeFromExt(src: string) {
48 if (src.endsWith(".m3u8")) return "application/x-mpegURL";
49 if (src.endsWith(".webm")) return "video/webm";
50 if (src.endsWith(".mp4")) return "video/mp4";
51 if (src.endsWith(".ogg")) return "video/ogg";
52 throw new Error("Unknown video extension: " + path.extname(src));
53}
54const gcd = (a: number, b: number): number => b ? gcd(b, a % b) : a;
55function simplifyFraction(n: number, d: number) {
56 const divisor = gcd(n, d);
57 return `${n / divisor}/${d / divisor}`;
58}
src/tags/blurhash.tsx deleted-34
...@@ -1,34 +0,0 @@
1import * as blurhash from "blurhash";
2import sharp from "sharp";
3function decode83(str: string) {
4 return [...str].reduce(
5 (value, char) =>
6 value * 83 +
7 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" //
8 .indexOf(char),
9 0,
10 );
11}
12function getSizeFromHash(hash: string) {
13 const sizeFlag = decode83(hash[0]);
14 const height = ((sizeFlag / 9) | 0) + 1;
15 const width = (sizeFlag % 9) + 1;
16 return { width, height };
17}
18export async function encodeBlurhashPng(hash: string) {
19 const size = getSizeFromHash(hash);
20 const data = blurhash.decode(hash, size.width, size.height);
21 return (await sharp(data, {
22 raw: { width: size.width, height: size.height, channels: 4 },
23 }).png({ colors: size.width * size.height }).toBuffer());
24}
25export async function PrecomputedBlurhash({ hash }: { hash: string }) {
26 const data = await encodeBlurhashPng(hash);
27 return (
28 <span
29 style={`background:url("data:image/png;base64,${
30 data.toString("base64url")
31 }")0px 0px/100% auto;`}
32 />
33 );
34}
src/tags/clover-video.client.ts created+155
...@@ -0,0 +1,155 @@
1// Theoretical minimum browser support: Safari v6, Chrome v23, Firefox v42
2
3// Polyfill for old browsers.
4type PlaybackMode = "av1" | "hls-polyfill" | "hls-native" | "none";
5let hls: Promise<typeof import("hls.js")> | null = null;
6
7Promise.allSettled ??= <T>(promises: Iterable<T | PromiseLike<T>>) =>
8 Promise.all(Array.from(promises, (p) =>
9 Promise.resolve(p)
10 .then(
11 (value) => ({ status: "fulfilled" as const, value }),
12 (reason) => ({ status: "rejected" as const, reason }),
13 )));
14
15decidePlaybackMode().then((mode) =>
16 Array.from(
17 document.querySelectorAll<HTMLElement>(".clover-video"),
18 (container) => hydrateVideo(container, mode),
19 )
20);
21
22function decidePlaybackMode(): Promise<PlaybackMode> {
23 return shouldPlayAv1().catch(() => ({ hardware: false, supported: false }))
24 .then((av1): Awaitable<PlaybackMode> => {
25 // Native AV1 with DASH Polyfill
26 // (Chrome v70, Safari v17 on M3/A17 chip, Firefox v67)
27 if (av1.hardware) return "av1";
28 if (av1.supported) {
29 // Software decoding is too slow to use, unfortunately.
30 // Consider a device like a 2014 two core laptop.
31 console.warn(`implements non-accelerated av1+opus`);
32 } else {
33 console.warn(`does not implement av1+opus`);
34 }
35
36 // Native HLS support (Safari v6)
37 const video = document.querySelector("video")!;
38 if (video.canPlayType("application/x-mpegURL")) {
39 return "hls-native";
40 }
41
42 return (hls ??= import("hls.js"))
43 .then((hls): PlaybackMode => {
44 // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8)
45 // TODO: ES modules have a minimum of Chrome 63 or Firefox 60
46 if (hls.default.isSupported()) return "hls-polyfill";
47 // Other browsers
48 return "none";
49 });
50 })
51 .catch((): PlaybackMode => "none")
52 .then((mode) => {
53 console.info(`selected playback mode: ${mode}`);
54 return mode;
55 });
56}
57
58function hydrateVideo(container: HTMLElement, mode: PlaybackMode) {
59 const video = container.querySelector("video")!;
60 let src = video.getAttribute("data-src");
61 if (!src) return;
62 src = `/file${src}`;
63
64 const dashFile = `${src}:/dash.mpd`;
65 const hlsFile = `${src}:/master.m3u8`;
66
67 if (mode === "av1") {
68 console.info(`Playback: av1, opus, mpeg-dash`);
69 import("dashjs").then((dashjs) => {
70 const player = dashjs.MediaPlayer().create();
71 // This proxying is for ManagedMediaSource, exposing the HLS stream for AirPlay
72 let mainSource: HTMLSourceElement | null = null;
73 Object.defineProperty(video, "src", {
74 get: () => false,
75 set: (v) => {
76 if (!mainSource) {
77 mainSource = document.createElement("source");
78 mainSource.src = v;
79 video.appendChild(mainSource);
80 } else {
81 mainSource.src = v;
82 }
83 },
84 });
85 Object.defineProperty(video, "disableRemotePlayback", {
86 get: () => false,
87 set: () => {
88 const source = document.createElement("source");
89 source.src = hlsFile;
90 video.appendChild(source);
91 },
92 });
93 player.initialize(
94 video,
95 dashFile,
96 false, // autoplay
97 0, // start
98 );
99 video.controls = true;
100 });
101 } else if (mode === "hls-native") {
102 const hlsSource = document.createElement("source");
103 hlsSource.src = `${src}:/hls.m3u8`;
104 video.appendChild(hlsSource);
105 video.controls = true;
106 } else if (mode === "hls-polyfill") {
107 hls!.then((hls) => {
108 const player = new hls.default();
109 player.loadSource(hlsFile);
110 player.attachMedia(video);
111 video.controls = true;
112 });
113 } else {
114 video.src = src;
115 video.controls = true;
116 }
117}
118
119declare global {
120 var ManagedMediaSource: typeof MediaSource | null;
121}
122
123function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> {
124 const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource;
125 if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) {
126 return Promise.resolve({
127 hardware: false,
128 supported: false,
129 });
130 }
131
132 if (navigator.vendor?.includes("Apple")) {
133 const supported = !!MediaSource?.isTypeSupported(
134 'video/mp4;codecs="av01.0.01M.08"',
135 );
136 return Promise.resolve({
137 hardware: supported,
138 supported,
139 });
140 } else {
141 return navigator.mediaCapabilities.decodingInfo({
142 type: "media-source",
143 video: {
144 contentType: 'video/mp4;codecs="av01.0.01M.08"',
145 bitrate: 128_000,
146 width: 1920,
147 height: 1080,
148 framerate: 30,
149 },
150 }).then((info) => ({
151 hardware: !!info.powerEfficient,
152 supported: !!info.supported,
153 }));
154 }
155}
src/tags/clover-video.css created+40
...@@ -0,0 +1,40 @@
1.clover-video {
2 border: 4px solid var(--fg);
3 display: flex;
4 flex-direction: column;
5}
6.clover-video video {
7 background-color: transparent;
8}
9.clover-video .stack {
10 position: relative;
11 overflow: hidden;
12 display: flex;
13 flex-direction: column;
14}
15.clover-video video {
16 width: 100%;
17}
18.clover-video h2 {
19 margin-top: 0;
20}
21.clover-video .fallback {
22 background-color: #222b;
23 color: white;
24 padding: 2rem;
25 position: absolute;
26 left: 0;
27 top: 0;
28 width: 100%;
29 height: 100%;
30}
31.clover-video video:has(~ .fallback > *) {
32 filter: blur(20px);
33}
34.clover-video figcaption {
35 background-color: var(--fg);
36 color: white;
37 width: 100%;
38 margin-top: -1px;
39 padding-bottom: 2px;
40}
src/tags/clover-video.marko created+57
...@@ -0,0 +1,57 @@
1import "./clover-video.css";
2
3export interface Input {
4 file: MediaFile | string;
5 poster?: MediaFile | string;
6 header?: Marko.AttrTag<{ content: Marko.Body }>;
7}
8
9<const/{ header } = input />
10<const/file = typeof UNWRAP(input.file) === 'object'
11 ? input.file
12 : UNWRAP(MediaFile.getByPath(input.file), `No file ${JSON.stringify(input.file)}`) />
13<const/poster = input.poster
14 ? typeof input.poster === 'object'
15 ? input.poster
16 : UNWRAP(MediaFile.getByPath(input.poster), `No file ${JSON.stringify(input.poster)}`)
17 : null />
18
19<const/{ width, height } = file.parseDimensions() />
20
21<figure.clover-video>
22 <figcaption>
23 <if=header><${header.content}/></><else>
24 ${file.basename}
25 </else>
26 </figcaption>
27 <div.stack style=`aspect-ratio:${width}/${height}`>
28 <video
29 data-src=file.path
30 width=1920
31 height=1080
32 poster=poster ? `/file${poster.path}` : null
33 />
34 <noscript.fallback>
35 <h2>scripting disabled!</h2>
36 <p>
37 to play this video, JavaScript must be enabled in your
38 browser. the default video playback behavior is not enabled here
39 as it results in a worse experience on slower internet
40 connections, and <a href="http://caniuse.com/mpeg-dash">MPEG-DASH
41 streaming is not natively supported in any browser</a> without JavaScript.
42 </p>
43 <p>
44 since disabling JavaScript is not the default in any browser,
45 you likely wanted to download the raw video file anyways.
46 those links are available below, but note that you will need
47 an AV1-compatible video player, such as VLC Media Player or
48 QuickTime Player (M3 chip or newer)
49 </p>
50 </noscript>
51 </div>
52</figure>
53
54client import "./clover-video.client.ts";
55
56import { MediaFile } from "@/file-viewer/models/MediaFile.ts";
57
src/tags/hls-polyfill.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/tags/video.css deleted-24
...@@ -1,24 +0,0 @@
1.video {
2 border: 4px solid var(--fg);
3 display: flex;
4 flex-direction: column;
5 position: relative;
6}
7
8.video > img,
9.video > span {
10 position: absolute;
11 left: 0;
12 top: 0;
13 width: 100%;
14 height: 100%;
15 z-index: -1;
16}
17
18.video figcaption {
19 background-color: var(--fg);
20 color: var(--bg);
21 width: 100%;
22 margin-top: -1px;
23 padding-bottom: 2px;
24}