diff --git a/framework/backend/entry-node.ts b/framework/backend/entry-node.ts index 35292723ec4e6e68e8c92158e9980aefc6067a2e..3402f2230d789f341a20528a7ac34a9e9fd87c7b 100644 --- a/framework/backend/entry-node.ts +++ b/framework/backend/entry-node.ts @@ -1,4 +1,3 @@ -import "@paperclover/console/inject"; import "#debug"; const app = require(globalThis.CLOVER_SERVER_ENTRY ?? process.argv[2]); diff --git a/framework/backend/entry-passthru.ts b/framework/backend/entry-passthru.ts index f6e5c6eb0cf46154e80cdc9589b5ed6340800703..74468a052e8b77f669a03b149ef985f3f5da78cd 100644 --- a/framework/backend/entry-passthru.ts +++ b/framework/backend/entry-passthru.ts @@ -1,4 +1,3 @@ -import "@paperclover/console/inject"; export default app; import app from "#backend"; diff --git a/framework/debug.safe.ts b/framework/debug.safe.ts index 3b3643f685238a916ae119a27796015741572684..6807d2951d56acf3559b7aa2dc6878fef39bec23 100644 --- a/framework/debug.safe.ts +++ b/framework/debug.safe.ts @@ -1,3 +1,9 @@ +process.on("uncaughtException", (error) => { + log.error(error); + process.exit(1); +}); +import * as log from "lib/log.ts"; + globalThis.UNWRAP = (t, ...args) => { if (t == null) { throw new Error( diff --git a/framework/engine/marko-runtime.ts b/framework/engine/marko-runtime.ts index 4bd270b05d069d950089ab2e44c81ec28897fefc..95cd4dbcd4e0b0e09cba5e0db96c0678c8493fe7 100644 --- a/framework/engine/marko-runtime.ts +++ b/framework/engine/marko-runtime.ts @@ -7,11 +7,11 @@ interface BodyContentObject { content: ServerRenderer; } -export const createTemplate = ( - templateId: string, - renderer: ServerRenderer, +export const _template: typeof marko._template = ( + templateId, + renderer, ) => { - const { render: renderFn } = marko.createTemplate(templateId, renderer); + const { render: renderFn } = marko._template(templateId, renderer); function wrap(props: Record, n: number) { // Marko Custom Tags const cloverAsyncMarker = { isAsync: false }; @@ -27,21 +27,22 @@ export const createTemplate = ( return markoResult.then(render.raw); } const rr = markoResult.toString(); + console.log({ rr }); return render.raw(rr); } - wrap.render = render; + wrap.render = renderFn; wrap.unwrapped = renderer; - return wrap; + return wrap as unknown as Template; }; -export const dynamicTag = ( - scopeId: number, - accessor: Accessor, - tag: unknown | string | ServerRenderer | BodyContentObject, - inputOrArgs: unknown, - content?: (() => void) | 0, - inputIsArgs?: 1, - serializeReason?: 1 | 0, +export const _dynamic_tag: typeof marko._dynamic_tag = ( + scopeId, + accessor, + tag, + inputOrArgs, + content, + inputIsArgs, + serializeReason, ) => { if (typeof tag === "function") { clover: { @@ -73,20 +74,20 @@ export const dynamicTag = ( (r.rejections ??= []).push(...rejections); return reject(new Error("Render had errors")); }; - marko.fork( + marko._await( scopeId, accessor, promise, - (string: string) => marko.write(string), + (string: string) => marko._html(string), 0, ); } else { - marko.write(render.stringifyNode(resolved)); + marko._html(render.stringifyNode(resolved)); } return; } } - return marko.dynamicTag( + return marko._dynamic_tag( scopeId, accessor, tag, @@ -97,19 +98,19 @@ export const dynamicTag = ( ); }; -export function fork( - scopeId: number, - accessor: Accessor, - promise: Promise, - callback: (data: unknown) => void, - serializeMarker?: 0 | 1, -) { +export const _await: typeof marko._await = ( + scopeId, + accessor, + promise, + callback, + serializeMarker, +) => { const marker = marko.$global().cloverAsyncMarker as Async; marker.isAsync = true; - marko.fork(scopeId, accessor, promise, callback, serializeMarker); -} + marko._await(scopeId, accessor, promise, callback, serializeMarker); +}; -export function escapeXML(input: unknown) { +export function _escape(input: unknown) { // The rationale of this check is that the default toString method // creating `[object Object]` is universally useless to any end user. if ( @@ -126,7 +127,7 @@ export function escapeXML(input: unknown) { `To emit a literal '${input}', use \${String(value)}`, ); } - return marko.escapeXML(input); + return marko._escape(input); } interface Async { @@ -135,5 +136,5 @@ interface Async { import * as render from "#engine/render"; import type { ServerRenderer } from "marko/html/template"; -import { type Accessor } from "marko/common/types"; +import type { Template } from "marko/common/types"; import * as marko from "#marko/html"; diff --git a/framework/engine/render.ts b/framework/engine/render.ts index 727c364fa34760c1962554e320a920bb590280be..9f733cc41335045c8fbe0fd7ff23792caf52b695 100644 --- a/framework/engine/render.ts +++ b/framework/engine/render.ts @@ -157,7 +157,7 @@ export function resolveNode(r: State, node: unknown): ResolvedNode { return resolveNode(r, tag(props)); } catch (e) { const { 4: src } = node; - if (e && typeof e === "object") (e as { src?: string }).src = src; + if (e && typeof e === "object") (e as { src?: string }).src ??= src; throw e; } finally { current = null; @@ -222,19 +222,30 @@ function stringifyElement(element: ResolvedElement) { default: attr = `${prop}=${quoteIfNeeded(escapeAttribute(String(value)))}`; break; - case "className": - // Legacy React Compat - case "class": - attr = `class=${ - quoteIfNeeded(escapeAttribute(clsx(value as ClsxInput))) - }`; + case "className": // Legacy React Compat + case "class": { + const str = clsx(value as ClsxInput); + if (!str) continue; + attr = `class=${quoteIfNeeded(escapeAttribute(str))}`; break; + } + case "style": { + const str = typeof value === "string" + ? value + : Object.entries(value).map((kv) => kv.join(":")).join(";"); + if (!str) continue; + attr = `style=${quoteIfNeeded(escapeAttribute(str))}`; + break; + } case "htmlFor": throw new Error("Do not use the `htmlFor` attribute. Use `for`"); + case "dangerouslySetInnerHTML": + throw new Error( + "Do not use the `dangerouslySetInnerHTML` attribute. Use `render.raw()`", + ); // Do not process these case "children": case "ref": - case "dangerouslySetInnerHTML": case "key": continue; } diff --git a/framework/esbuild-support.ts b/framework/esbuild-support.ts index e4496eed6945580c9f01cc013a38717b2aeab9d1..dbf204daca186dde26f80d327e8ae6fedbfad4fd 100644 --- a/framework/esbuild-support.ts +++ b/framework/esbuild-support.ts @@ -112,9 +112,8 @@ export function isIgnoredSource(source: string) { } import * as esbuild from "esbuild"; -import * as string from "#sitegen/string"; +import * as string from "lib/string.ts"; import * as path from "node:path"; import * as fs from "#sitegen/fs"; -import * as incr from "./incremental.ts"; import * as hot from "./hot.ts"; import { markoCache } from "./marko.ts"; diff --git a/framework/hot.ts b/framework/hot.ts index 1eb5c78de5faccb8e734662aaf9fcbdfb075daa7..18fb3f8dfebfda9ba512c3296d918c2a93515eb0 100644 --- a/framework/hot.ts +++ b/framework/hot.ts @@ -153,6 +153,7 @@ export function loadEsbuildCode( jsxDev: true, sourcefile: filepath, sourcemap: "inline", + sourceRoot: "/", }).code; return module._compile(src, filepath, "commonjs"); } diff --git a/framework/incremental.ts b/framework/incremental.ts index e5228710c317b0aa98ff40578f91ba9ee10116ba..3b5b56c5509ca87bf29e61c6d80316759ca47692 100644 --- a/framework/incremental.ts +++ b/framework/incremental.ts @@ -63,7 +63,6 @@ export function work( const io = new Io(key, p); jobs += 1; newKeys += 1; - await timers.setTimeout(200); try { const value = await job(io, input as I); validateSerializable(value, ""); @@ -103,7 +102,7 @@ export async function compile( p: progress.Ref = progress, ) { ASSERT(!running, `Cannot run twice at the same time.`); - const node = p.start("sitegen!"); + using node = p.start("sitegen!"); try { running = true; ASSERT(jobs === 0); diff --git a/framework/lib/assets.ts b/framework/lib/assets.ts index 0d2b7393e5bc14f211c3516f7673559f9f5ea130..d093d0af90cf209c53ca504e4fd84dfaac4df773 100644 --- a/framework/lib/assets.ts +++ b/framework/lib/assets.ts @@ -2,7 +2,7 @@ // This module implements decoding and serving of the asset blobs, // but also implements patching of dynamic assets. The `Manifest` // is generated by `incremental.ts` -const debug = console.scoped("assets"); +const console = log.scoped("assets"); const root = import.meta.dirname; let current: Loaded | null = null; @@ -93,11 +93,6 @@ export async function serveAsset( ); } -/** @deprecated */ -export function hasAsset(id: string) { - return UNWRAP(current).map[id] !== undefined; -} - export function etagMatches(etag: string, ifNoneMatch: string) { return ifNoneMatch === etag || ifNoneMatch.split(/,\s*/).indexOf(etag) > -1; } @@ -171,7 +166,7 @@ function respondWithBufferAndViews( } else { body = buffer.subarray(...asset.raw); } - debug( + console.info( `${key} encoding=${headers["Content-Encoding"] ?? "raw"} status=${status}`, ); 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"; import * as crypto from "node:crypto"; import * as zlib from "node:zlib"; import * as util from "node:util"; -import * as console from "@paperclover/console"; +import * as log from "lib/log.ts"; diff --git a/framework/lib/async.ts b/framework/lib/async.ts index 379237deb78bb089044a84c2b2c7fccfced31ef1..9a8c4e4e030938251a02511add1ca449e8c51276 100644 --- a/framework/lib/async.ts +++ b/framework/lib/async.ts @@ -1,219 +1,5 @@ const five_minutes = 5 * 60 * 1000; -interface QueueOptions { - name: string; - fn: (item: T, spin: Spinner) => Promise; - getItemText?: (item: T) => string; - maxJobs?: number; - passive?: boolean; -} - -// Process multiple items in parallel, queue up as many. -/** @deprecated */ -export class Queue { - #name: string; - #fn: (item: T, spin: Spinner) => Promise; - #maxJobs: number; - #getItemText: (item: T) => string; - #passive: boolean; - - #active: Spinner[] = []; - #queue: Array<[T] | [T, (result: R) => void, (err: unknown) => void]> = []; - - #cachedProgress: Progress<{ active: Spinner[] }> | null = null; - #done: number = 0; - #total: number = 0; - #onComplete: (() => void) | null = null; - #estimate: number | null = null; - #errors: unknown[] = []; - - constructor(options: QueueOptions) { - this.#name = options.name; - this.#fn = options.fn; - this.#maxJobs = options.maxJobs ?? 5; - this.#getItemText = options.getItemText ?? defaultGetItemText; - this.#passive = options.passive ?? false; - } - - cancel() { - const bar = this.#cachedProgress; - bar?.stop(); - this.#queue = []; - } - - get bar() { - const cached = this.#cachedProgress; - if (!cached) { - const bar = (this.#cachedProgress = new Progress({ - spinner: null, - text: ({ active }) => { - const now = performance.now(); - let text = `[${this.#done}/${this.#total}] ${this.#name}`; - let n = 0; - for (const item of active) { - let itemText = "- " + item.format(now); - text += `\n` + - itemText.slice(0, Math.max(0, process.stdout.columns - 1)); - if (n > 10) { - text += `\n ... + ${active.length - n} more`; - break; - } - n++; - } - return text; - }, - props: { - active: [] as Spinner[], - }, - })); - bar.value = 0; - return bar; - } - return cached; - } - - addReturn(args: T) { - this.#total += 1; - this.updateTotal(); - if (this.#active.length >= this.#maxJobs) { - const { promise, resolve, reject } = Promise.withResolvers(); - this.#queue.push([args, resolve, reject]); - return promise; - } - return this.#run(args); - } - - add(args: T) { - return this.addReturn(args).then( - () => {}, - () => {}, - ); - } - - addMany(items: T[]) { - this.#total += items.length; - this.updateTotal(); - - const runNowCount = this.#maxJobs - this.#active.length; - const runNow = items.slice(0, runNowCount); - const runLater = items.slice(runNowCount); - this.#queue.push(...runLater.reverse().map<[T]>((x) => [x])); - runNow.map((item) => this.#run(item).catch(() => {})); - } - - async #run(args: T): Promise { - const bar = this.bar; - const itemText = this.#getItemText(args); - const spinner = new Spinner(itemText); - spinner.stop(); - (spinner as any).redraw = () => (bar as any).redraw(); - const active = this.#active; - try { - active.unshift(spinner); - bar.props = { active }; - // console.log(this.#name + ": " + itemText); - const result = await this.#fn(args, spinner); - this.#done++; - return result; - } catch (err) { - if (err && typeof err === "object") { - (err as any).job = itemText; - } - this.#errors.push(err); - console.error(util.inspect(err, false, Infinity, true)); - throw err; - } finally { - active.splice(active.indexOf(spinner), 1); - bar.props = { active }; - bar.value = this.#done; - - // Process next item - const next = this.#queue.shift(); - if (next) { - const args = next[0]; - this.#run(args) - .then((result) => next[1]?.(result)) - .catch((err) => next[2]?.(err)); - } else if (this.#active.length === 0) { - if (this.#passive) { - this.bar.stop(); - this.#cachedProgress = null; - } - this.#onComplete?.(); - } - } - } - - updateTotal() { - const bar = this.bar; - bar.total = Math.max(this.#total, this.#estimate ?? 0); - } - - set estimate(e: number) { - this.#estimate = e; - if (this.#cachedProgress) { - this.updateTotal(); - } - } - - async done(o?: { method: "success" | "stop" }) { - if (this.#active.length === 0) { - this.#end(o); - return; - } - - const { promise, resolve } = Promise.withResolvers(); - this.#onComplete = resolve; - await promise; - this.#end(o); - } - - #end({ - method = this.#passive ? "stop" : "success", - }: { - method?: "success" | "stop"; - } = {}) { - const bar = this.#cachedProgress; - if (this.#errors.length > 0) { - if (bar) bar.stop(); - throw new AggregateError( - this.#errors, - this.#errors.length + " jobs failed in '" + this.#name + "'", - ); - } - - if (bar) bar[method](); - } - - get active(): boolean { - return this.#active.length !== 0; - } - - [Symbol.dispose]() { - if (this.active) { - this.cancel(); - } - } -} - -const cwd = process.cwd(); -function defaultGetItemText(item: unknown) { - let itemText = ""; - if (typeof item === "string") { - itemText = item; - } else if (typeof item === "object" && item !== null) { - const { path, label, id } = item as any; - itemText = label ?? path ?? id ?? JSON.stringify(item); - } else { - itemText = JSON.stringify(item); - } - - if (itemText.startsWith(cwd)) { - itemText = path.relative(cwd, itemText); - } - return itemText; -} - /** @deprecated use OnceMap2 */ export class OnceMap { private ongoing = new Map>(); @@ -415,8 +201,9 @@ export class PromiseAggregator { async all() { while (this.promises.size > 0) { - await Promise.all(this.promises); + const wait = [...this.promises]; this.promises.clear(); + await Promise.all(wait); } if (this.failures.length > 0) { const agg = new AggregateError(this.failures); @@ -429,9 +216,3 @@ export class PromiseAggregator { return this.all(); } } - -import { Progress } from "@paperclover/console/Progress"; -import { Spinner } from "@paperclover/console/Spinner"; -import * as path from "node:path"; -import process from "node:process"; -import * as util from "node:util"; diff --git a/framework/lib/markdown.tsx b/framework/lib/markdown.tsx deleted file mode 100644 index 938b709c5fc763d71827c0530396d6000d4b0a24..0000000000000000000000000000000000000000 --- a/framework/lib/markdown.tsx +++ /dev/null @@ -1,200 +0,0 @@ -/* Implementation of [CommonMark] specification for markdown with support - * for custom syntax extensions via the parser options. Instead of - * returning an AST that has a second conversion pass to JSX, the - * returned value of 'parse' is 'engine.Node' which can be stringified - * via Clover's SSR engine. This way, generation optimizations, async - * components, and other features are gained for free here. - * - * [CommonMark]: https://spec.commonmark.org/0.31.2/ - */ -function parse(src: string, options: Partial = {}) {} - -/* Render markdown content. Same function as 'parse', but JSX components - * only take one argument and must start with a capital letter. */ -export function Markdown({ - src, - ...options -}: { src: string } & Partial) { - return parse(src, options); -} - -// TODO: This implementation is flawed because it is impossible to sanely handle -// emphasis and strong emphasis, and all their edge cases. Instead of making these -// using extensions interface, they should be special cased. -function parseInline(src: string, options: Partial = {}) { - const { rules = inlineRules, links = new Map() } = options; - const opts: InlineOpts = { rules, links }; - const parts: render.Node[] = []; - const ruleList = Object.values(rules); - parse: while (true) { - for (const rule of ruleList) { - if (!rule.match) continue; - const match = src.match(rule.match); - if (!match) continue; - const index = UNWRAP(match.index); - const after = src.slice(index + match[0].length); - const parse = rule.parse({ after, match: match[0], opts }); - if (!parse) continue; - // parse before - parts.push(src.slice(0, index), parse.result); - src = parse.rest ?? after; - continue parse; - } - break; - } - parts.push(src); - return parts; -} - -// -- interfaces -- -interface ParseOpts { - gfm: boolean; - blockRules: Record; - inlineRules: Record; -} -interface InlineOpts { - rules: Record; - links: Map; -} -interface InlineRule { - match: RegExp; - parse(opts: { - after: string; - match: string; - opts: InlineOpts; - }): InlineParse | null; -} -interface InlineParse { - result: engine.Node; - rest?: string; -} -interface LinkRef { - href: string; - title: string | null; -} -interface BlockRule { - match: RegExp; - parse(opts: {}): unknown; -} -export const inlineRules: Record = { - code: { - match: /`+/, - // 6.1 - code spans - parse({ after, match }) { - const end = after.indexOf(match); - if (end === -1) return null; - let inner = after.slice(0, end); - const rest = after.slice(end + match.length); - // If the resulting string both begins and ends with a space - // character, but does not consist entirely of space characters, - // a single space character is removed from the front and back. - if (inner.match(/^ [^ ]+ $/)) inner = inner.slice(1, -1); - return { result: {inner}, rest }; - }, - }, - link: { - match: /(?{parseInline(textSrc, opts)}, - rest, - }; - }, - }, - image: { - match: /!\[/, - // 6.4 - images - parse({ after, opts }) { - // Match '[' to let the inner-most link win. - const splitText = splitFirst(after, /[[\]]/); - if (!splitText) return null; - if (splitText.delim !== "]") return null; - const { first: textSrc, rest: afterText } = splitText; - }, - }, - emphasis: { - // detect left-flanking delimiter runs, but this expression does not - // consider preceding escapes. instead, those are programatically - // checked inside the parse function. - match: /(?:\*+|(? }; - }, - }, -}; - -function parseLinkTarget(src: string) { - let href: string, - title: string | null = null; - href = src; - return { href, title }; -} - -/* Find a delimiter while considering backslash escapes. */ -function splitFirst(text: string, match: RegExp) { - let first = "", - delim: string, - escaped: boolean; - do { - const find = text.match(match); - if (!find) return null; - delim = find[0]; - const index = UNWRAP(find.index); - let i = index - 1; - escaped = false; - while (i >= 0 && text[i] === "\\") (escaped = !escaped), (i -= 1); - first += text.slice(0, index - +escaped); - text = text.slice(index + find[0].length); - } while (escaped); - return { first, delim, rest: text }; -} - -console.log(render.sync(parseInline("meow `bwaa` `` ` `` `` `z``"))); - -import * as render from "#engine/render"; diff --git a/framework/lib/path.ts b/framework/lib/path.ts index 3482f6cd4f6a5c63e6e852f124fdc4e4c1fd7e27..70971262e0f8a838838096df27ada11af79d0ff2 100644 --- a/framework/lib/path.ts +++ b/framework/lib/path.ts @@ -17,6 +17,14 @@ export class Path { )); } + static createDirSync(...parts: string[]): Path; + static createDirSync(base: Path | string, ...more: string[]): Path; + static createDirSync(base: Path | string, ...more: string[]): Path { + const path = Path.resolve(base, ...more); + path.makeDirSync(); + return path; + } + /** For convenience, paths are deduplicated. */ constructor(raw: string) { ASSERT(path.isAbsolute(raw)); @@ -174,8 +182,15 @@ export class Path { return fs.readdirSync(this.raw).map((sub) => this.join(sub)); } - async readDir(): Promise> { - return (await fs.readdir(this.raw)).map((sub) => this.join(sub)); + async readDir( + { recursive }: { recursive?: boolean } = {}, + ): Promise> { + return (await fs.readdir(this.raw, { recursive })) + .map((sub) => this.join(sub)); + } + + async writeJsonFile(json: unknown) { + await fs.writeFile(this.raw, JSON.stringify(json)); } static sep = path.sep; @@ -194,3 +209,4 @@ import * as fs from "./fs.ts"; import * as util from "node:util"; import * as error from "./error.ts"; import type { RmOptions } from "node:fs"; +import type { Serialized } from "./lru.ts"; diff --git a/framework/lib/string.ts b/framework/lib/string.ts deleted file mode 100644 index a15d4120395a5e03c341416cf49c96287e1050a6..0000000000000000000000000000000000000000 --- a/framework/lib/string.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function escapeRegExp(source: string) { - return source.replace(/[\$\\]/g, "\\$&"); -} diff --git a/framework/lib/subprocess.ts b/framework/lib/subprocess.ts index 10aaecfef39b5c2d1526618ee3dae3b0456c6fd3..6d2eec5905452d043dd59e9f53ea2bdfbfafd2f8 100644 --- a/framework/lib/subprocess.ts +++ b/framework/lib/subprocess.ts @@ -1,3 +1,6 @@ +// This file is expecting a full rewrite to abstract the Node API away +// entirely, exposing web streams I/O. + const execFileRaw = util.promisify(child_process.execFile); export const exec: typeof execFileRaw = (( ...args: Parameters @@ -12,6 +15,7 @@ export const exec: typeof execFileRaw = (( })) as any; const activePids = new Set(); + export interface SpawnOptions extends child_process.SpawnOptions { cmd: string[]; } @@ -26,9 +30,28 @@ export function spawn({ cmd, ...rest }: SpawnOptions) { return process; } +export interface ForkOptions extends child_process.ForkOptions { + cmd: string[]; +} + +export function fork({ cmd, ...rest }: ForkOptions) { + rest.detached ??= true; + const process = child_process.fork(cmd[0]!, cmd.slice(1), rest); + const pid = rest.detached && process.pid ? -process.pid : process.pid; + if (pid) { + activePids.add(pid); + process.on("exit", () => void activePids.delete(pid)); + } + return process; +} + export async function spawnAndWait(opts: SpawnOptions) { opts.stdio ??= ["ignore", "inherit", "inherit"]; const process = spawn(opts); + const output = Promise.all([ + process.stdout ? Array.fromAsync(process.stdout).then(Buffer.concat) : null, + process.stderr ? Array.fromAsync(process.stderr).then(Buffer.concat) : null, + ]); let [code, signal] = await events.EventEmitter.once(process, "exit") as [ number | null, string | null, @@ -38,10 +61,7 @@ export async function spawnAndWait(opts: SpawnOptions) { const exit = signal ? `signal ${signal}` : `code ${code}`; throw new Error(`${opts.cmd[0]} failed with ${exit}`); } - const [stdout, stderr] = await Promise.all([ - process.stdout ? Array.fromAsync(process.stdout).then(Buffer.concat) : null, - process.stderr ? Array.fromAsync(process.stderr).then(Buffer.concat) : null, - ]); + const [stdout, stderr] = await output; return { stdout, stderr }; } diff --git a/framework/marko.ts b/framework/marko.ts index c62f8b855c33cebdd162610a06e2f449de391bab..bb82832a1847799649bebca76cdfc0bf2adfa285 100644 --- a/framework/marko.ts +++ b/framework/marko.ts @@ -29,10 +29,22 @@ export function loadMarko(module: NodeJS.Module, filepath: string) { ) + '\nimport { addScript as CloverScriptInclude } from "#sitegen";\n'; } - src = marko.compileSync(src, filepath).code; - src = src.replace("marko/debug/html", "#engine/marko-runtime"); + const compiled = marko.compileSync(src, filepath, { + sourceMaps: true, + }); + // workaround an esbuild bug? + (compiled.map as unknown as { sourceRoot: string }).sourceRoot = "/"; + compiled.map.sources[0] = filepath; + src = compiled.code.replace("marko/debug/html", "#engine/marko-runtime") + + "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + + Buffer.from(JSON.stringify(compiled.map), "utf-8").toString("base64"); cache = { src, scannedClientRefs: Array.from(scannedClientRefs) }; markoCache.set(filepath, cache); + fs.writeMkdirSync( + ".clover/marko/" + path.relative(".", filepath).replace(/^src\//, "") + + ".js", + src, + ); } const { src, scannedClientRefs } = cache; @@ -44,3 +56,4 @@ export function loadMarko(module: NodeJS.Module, filepath: string) { import * as marko from "@marko/compiler"; import * as hot from "./hot.ts"; import * as fs from "#sitegen/fs"; +import * as path from "node:path"; diff --git a/framework/watch.ts b/framework/watch.ts index c048d223586500ba48db2755cc6d843268b1bc4d..740b15afd6747b4ac88bcca2a14356c6383601c6 100644 --- a/framework/watch.ts +++ b/framework/watch.ts @@ -2,9 +2,9 @@ const debounceMilliseconds = 25; -let subprocess: child_process.ChildProcess | null = null; +let server: child_process.ChildProcess | null = null; process.on("beforeExit", () => { - subprocess?.removeListener("close", onSubprocessClose); + server?.removeListener("close", onSubprocessClose); }); let watch: Watch; @@ -17,21 +17,20 @@ export async function main() { } function serve() { - if (subprocess) { - subprocess.removeListener("close", onSubprocessClose); - subprocess.kill(); + if (server) { + server.removeListener("close", onSubprocessClose); + server.kill(); } - subprocess = child_process.fork(".clover/o/backend.js", [ - "--development", - ], { + server = subprocess.fork({ + cmd: [".clover/o/backend.js", "--development"], stdio: "inherit", execArgv: ["--no-warnings"], }); - subprocess.on("close", onSubprocessClose); + server.on("close", onSubprocessClose); } function onSubprocessClose(code: number | null, signal: string | null) { - subprocess = null; + server = null; const status = code != null ? `code ${code}` : `signal ${signal}`; console.error(`Backend process exited with ${status}`); } @@ -50,10 +49,10 @@ async function rebuild(files: string[]) { for (const file of removeWatch) watch.remove(file); watch.add(...watchFiles); // Restart the server if it was changed or not running. - if (!subprocess || newOutputs.includes("backend.js")) { + if (!server || newOutputs.includes("backend.js")) { serve(); - } else if (subprocess && newAssets) { - subprocess.send({ type: "clover.assets.reload" }); + } else if (server && newAssets) { + server.send({ type: "clover.assets.reload" }); } }).catch((err) => { console.error(util.inspect(err)); @@ -167,10 +166,8 @@ class Watch { } import * as fs from "node:fs"; -import { withSpinner } from "@paperclover/console/Spinner"; import * as generate from "./generate.ts"; import * as incr from "./incremental.ts"; import * as path from "node:path"; import * as util from "node:util"; -import * as hot from "./hot.ts"; -import * as child_process from "node:child_process"; +import * as subprocess from "#sitegen/subprocess"; diff --git a/lib/log.test.ts b/lib/log.test.ts index 9a80ade55bc36eddcd3f3a33245c193335bd385c..91d8ba08516dc88eec94465366fb46454cda5080 100644 --- a/lib/log.test.ts +++ b/lib/log.test.ts @@ -1,6 +1,9 @@ import { ASSERT, UNWRAP } from "lib/assert.ts"; import { test } from "node:test"; import * as headless from "lib/log/headless.ts"; +import * as log from "lib/log.ts"; + +log satisfies log.Scope; // the namespace import is a valid Scope class TestWidgetHost implements headless.WidgetHost { columns = 80; diff --git a/lib/log.ts b/lib/log.ts index 47b079870e79311c6b14077b7d1a89f3aadb8bc6..9067b4a701d7cd2c6d5136f7482b811c86b6fe9c 100644 --- a/lib/log.ts +++ b/lib/log.ts @@ -1,6 +1,59 @@ +/** + * By using `lib/log.ts`, your application gets easy scoped logging as well as + * integration with terminal widgets such as `lib/log/progress.ts`. The pattern + * for using this module is to shadow the global `console` with a local scope: + * + * import * as log from "@clo/lib/log"; + * const console = log.scoped("http"); + * console("hi!"); // debug + * + * Or to use the global socpe, import the namespace as `console`. + * + * import * as console from "@clo/lib/log"; + * + * Now, the code reads familiarly and output is organized. + * + * This module offers a Node.js integration to have logs go to the standard + * output. Custom writers can be used with `lib/log/headless.ts`. + * + * @module + */ + +/** + * Logging scopes implement part of the `Console` API + */ +export interface Scope { + /** emit an informational message */ + info(...args: unknown[]): void; + /** emit an advisory message */ + warn(...args: unknown[]): void; + /** emit a failure message */ + error(...args: unknown[]): void; + + /** + * emit a debugging message. */ + log(...args: unknown[]): void; + /** + * emit a debugging message. this method is in place for compatibility with + * the `console` API. prefer calling `.log` directly. + */ + debug(...args: unknown[]): void; + + /** create a nested sub-scope */ + scoped(name: string): Scope; +} + +/** custom scopes are colored based on the name */ +export function scoped(name: string): Scope { + return globalLog.scoped(name); +} + +// These functions implement `Scope`. If a function takes in `Scope`, the +// namespace import of this file can be passed to it. + /** informational */ export function info(...args: unknown[]) { - globalLog(...args); + globalLog.info(...args); } /** advisory */ export function warn(...args: unknown[]) { @@ -11,28 +64,12 @@ export function error(...args: unknown[]) { globalLog.error(...args); } /** debugging */ +export function log(...args: unknown[]) { + globalLog.log(...args); +} +/** debugging */ export function debug(...args: unknown[]) { - globalLog(...args); -} - -export interface Scope { - /** default call signature is informational scope */ - (...args: unknown[]): void; - /** informational */ - info(...args: unknown[]): void; - /** advisory */ - warn(...args: unknown[]): void; - /** failure */ - error(...args: unknown[]): void; - - /** prefix */ - name: string; - scoped(name: string): Scope; -} - -/** custom scopes are colored based on the name */ -export function scoped(name: string): Scope { - return globalLog.scoped(name); + globalLog.debug(...args); } /** @@ -77,9 +114,22 @@ export interface Widget { // onKey?(key: string): void; } -const globalWidgetHost = /* @__PURE__ */ (() => { +const globalWidgetHost = /* @__PURE__ */ ((): headless.WidgetHost => { + const { process } = globalThis; + if (!process || !process.stderr.isTTY) { + return { + // TODO: allow passing a level here + writeLine: (line) => console["log"](line), + getDrawLock: () => ts.defer(() => {}), + startWidget: (w) => { + const close = w.onChange?.(() => {}); + return ts.defer(close ?? (() => {})); + }, + cancel: () => {}, + }; + } const widget = headless.widgetHost({ - // TODO: consider synchronization and make these async + // TODO:allow passing a level here to put debug, warn and errors on stderr writeOutput: (string) => process.stdout.write(string), writeInteractive: (string) => process.stderr.write(string), now: () => performance.now(), @@ -100,4 +150,4 @@ const globalLog = /* @__PURE__ */ headless.logger({ import process from "node:process"; import * as headless from "lib/log/headless.ts"; -import type * as ts from "lib/ts.ts"; +import * as ts from "lib/ts.ts"; diff --git a/lib/log/headless.ts b/lib/log/headless.ts index 1020fcd93a85c313e42240bd8eb2bc2b83f38f3a..1d168453cec2eb77311c1b81febed22226b3dc51 100644 --- a/lib/log/headless.ts +++ b/lib/log/headless.ts @@ -302,7 +302,10 @@ export function logger(env: LoggerOptions): log.Scope { function scoped(name: string): log.Scope { const formatted = name ? name + colon : name; const fn = logFn.bind(null, formatted, 0) as Partial; + // TODO: repair fn.info = fn as log.Scope["info"]; + fn.debug = fn as log.Scope["info"]; + fn.log = fn as log.Scope["info"]; fn.warn = logFn.bind(null, formatted, 1); fn.error = logFn.bind(null, formatted, 2); fn.scoped = createSubScope; diff --git a/lib/log/progress.ts b/lib/log/progress.ts index ede2e98b211a812b05af6dad962e9e9511f9bec2..07a5bd43695d9837338d619dd5d7865075dbd38a 100644 --- a/lib/log/progress.ts +++ b/lib/log/progress.ts @@ -45,6 +45,10 @@ export interface Node extends Ref { * there are no children. */ passive: boolean; + /** + * reactive, default `false`. when true, this progress item is hidden + */ + hidden: boolean; /** reactive. null means decide based on nested depth + terminal height */ maxHeight: null | number; /** reactive. specify sorting for children */ @@ -66,6 +70,7 @@ export interface StartOptions { value?: number | undefined | null; estimate?: number | undefined | null; passive?: boolean | undefined | null; + hidden?: boolean | undefined | null; showEstimate?: boolean | undefined | null; maxHeight?: number | undefined | null; sortChildren?: Node["sortChildren"] | undefined | null; @@ -89,6 +94,7 @@ export type SortNode = | "value" | "estimate" | "passive" + | "hidden" | "showEstimate" | "sortChildren" > @@ -103,7 +109,7 @@ export interface Internal extends SortNode { type Signal = | "draw" - | { type: "end"; title: string; logs: string[] } + | { type: "end"; title: string; logs: string[]; state: Internal } | { type: "line"; line: string }; function newNode( @@ -116,6 +122,7 @@ function newNode( estimate: opts.estimate ?? 0, showEstimate: opts.showEstimate ?? true, passive: opts.passive ?? false, + hidden: opts.hidden ?? false, logs: [], children: [], parent: null, @@ -127,17 +134,18 @@ function newNode( let detached = false; function end() { if (detached) return; - if (!state.parent) return; - const i = state.parent.children.indexOf(state); - if (i === -1) return; let title = state.text; - let p: Internal | null = state; - while (p = p.parent) title = p.text + " / " + title; - state.signal({ type: "end", title, logs: state.logs }); + if (state.parent) { + const i = state.parent.children.indexOf(state); + ASSERT(i !== -1); + let p: Internal | null = state; + while (p = p.parent) title = p.text + " / " + title; + state.parent.children.splice(i, 1); + state.parent = null; + detached = true; + } + state.signal({ type: "end", title, logs: state.logs, state }); state.logs = []; - state.parent.children.splice(i, 1); - state.parent = null; - detached = true; } function mutate() { const { parent } = state; @@ -199,6 +207,13 @@ function newNode( state.passive = value; mutate(); }, + get hidden() { + return state.hidden; + }, + set hidden(value) { + state.hidden = value; + mutate(); + }, maxHeight: null, // get maxHeight() { // return state.maxHeight; @@ -284,7 +299,9 @@ function renderMainLine(state: Internal, now: number, depth: number) { } function hasChildren(states: Internal): boolean { - return states.children.some((x) => !x.passive || hasChildren(x)); + return states.children.some((x) => + !x.hidden && (!x.passive || hasChildren(x)) + ); } function renderChildren(state: Internal, now: number, depth: boolean[]) { @@ -292,7 +309,9 @@ function renderChildren(state: Internal, now: number, depth: boolean[]) { let truncated = 0; let out = ""; let { children } = state; - children = children.filter((x) => !x.passive || hasChildren(x)); + children = children.filter((x) => + !x.hidden && (!x.passive || hasChildren(x)) + ); if (children.length === 0) return ""; for (let i = 0, { length } = children; i < length; i += 1) { const child = UNWRAP(children[i]); @@ -376,7 +395,12 @@ export class Headless { if (msg === "draw") this.#rerender?.(); else if (msg.type === "line") this.env.writeLine(msg.line); else if (msg.type === "end") { - if (msg.logs.length === 0) return; + const indexOfActive = this.active.indexOf(msg.state); + if (indexOfActive !== -1) this.active.splice(indexOfActive, 1); + if (msg.logs.length === 0) { + this.#rerender?.(); + return; + } const { logs } = msg; const count = `${logs.length} line${logs.length === 1 ? "" : "s"}`; const header = `[${count} from ${msg.title}]`; diff --git a/lib/log/stack.ts b/lib/log/stack.ts index b16b98aca8eb8f2cefe72d67d62cbdda0d2ae0ab..ee06d44535a1da236924875a02da8efbacb12bc8 100644 --- a/lib/log/stack.ts +++ b/lib/log/stack.ts @@ -201,17 +201,20 @@ export function formatFrame(frame: Frame, colors: boolean): string { const left = text.slice(0, l); center = text.slice(l, r); const right = text.slice(r); + const irrelevant = internal || file?.includes("node_modules"); + const fg = !irrelevant ? ansi.fgRed : ""; + const bold = !irrelevant ? ansi.bold : ansi.dim + ansi.bold; out += (left - ? ansi.mergeStyles(ansi.fgRed + ansi.dim) + left + - ansi.mergeStyles(ansi.resetWeight + ansi.bold) - : ansi.mergeStyles(ansi.fgRed + ansi.bold)) + + ? ansi.mergeStyles(fg + ansi.dim) + left + + ansi.mergeStyles(ansi.resetWeight + bold) + : ansi.mergeStyles(fg + bold)) + center + (right ? ansi.mergeStyles(ansi.resetWeight + ansi.dim) + right : "") + ansi.mergeStyles(ansi.resetWeight + ansi.fgReset); } else { - out += ansi.fgRed + text + ansi.fgReset; + out += text; } } else { out += text; diff --git a/lib/readme.changes.md b/lib/readme.changes.md index 405df536dd637ea45e527f24040a71370ef01a9f..5680cecee0eef32ffc7770a934b9910621aa708f 100644 --- a/lib/readme.changes.md +++ b/lib/readme.changes.md @@ -1,5 +1,14 @@ # notable changes in clover's typescript library +## v2 + +- top level readme now explains general usage patterns. +- rework `log.Scope`, remove self call signature so that the `log` module + satisfies `log.Scope`, while adding `log` and `debug` for explicit debugging + logs. documentation prescribes doing `const console = log.scope("module");`. +- add `stack.capture()` +- add `string.escapeRegExp()` + ## v1 - new library `assert` diff --git a/lib/readme.md b/lib/readme.md index 62831430c4a2e3c65fae34359a43d7f076114dbf..66e7b24d2573bed22676e14df6520b506faf71ce 100644 --- a/lib/readme.md +++ b/lib/readme.md @@ -5,11 +5,13 @@ either browsers or node.js. originally, these were helper functions used to create my website, , but i started wanting them across projects and at work. + + +## contents + all top-level namespaces stand alone as their own projects, and generally do not depend on each other. there are no external dependencies; code is easy to audit. -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: -- `assert` -- `log` - logging, terminal i/o, interactive widgets -- `log/ansi` - cursor and color constants -- `log/headless` - use `log` and `log/progress` without a tty -- `log/progress` - progress bar, spinner, and tree -- `log/stack` - stack trace parse and formatter -- `packaging` - workflows used for `lib`'s publications -- `queue` - priority queue with rescheduling -- `string` -- `ts` typescript types and general purpose code +- `assert.ts` +- `log.ts` - logging, terminal i/o, interactive widgets +- `log/ansi.ts` - cursor and color constants +- `log/headless.ts` - use `log` and `log/progress` without a tty +- `log/progress.ts` - progress bar, spinner, and tree +- `log/stack.ts` - stack trace parse and formatter +- `packaging.ts` - workflows used for `lib`'s publications +- `queue.ts` - priority queue with rescheduling +- `string.ts` +- `ts.ts` typescript types and general purpose code + +there is currently no hosted documentation site. please refer to the source +files' comments. + +## usage + +unless otherwise noted, it is expected to import modules using a namespace +import. function names are often concise to avoid repeating redundant words in +source code. + +```ts +import * as queue from "@clo/lib/queue.ts"; + +// `run` in the context of `queue` conveys more information +const result = await queue.run({ ... }); +``` + +without "barrel" files, using multiple libraries is done by importing each +library separately: + +```ts +import * as log from "@clo/lib/log.ts"; +import * as progress from "@clo/lib/log/progress.ts"; +``` + +these patterns developed initially when using a text editor without a language +server, but i found this pattern to be nicer in general for organizing code. it +also resulted in fewer lines spent on imports, and making it much easier to +understand where code comes from. to this extent, all of the library modules are +named as single words so they can be easily imported. + +## versioning these libraries are published as is, **without versioning or maintainance guarantees**. every version is a breaking major release. please upgrade your @@ -36,6 +70,8 @@ individual modules may annotate their intended api and usage stability. keep in mind there are [known bugs and missing features] in this release. +## contributions + to contribute, please send bug reports or patches to . new features may not be accepted if they are complex and don't further my use cases. diff --git a/lib/readme.npm.md b/lib/readme.npm.md index 13d6e9046dae5815a01d019e87ecb3e5737de420..ac6951c39461f1c0220f93372ccf5d2e52b4e343 100644 --- a/lib/readme.npm.md +++ b/lib/readme.npm.md @@ -1,49 +1,9 @@ -# [clover's typescript library] - -`lib` is a collection of high-quality typescript libraries for things missing in -either browsers or node.js. originally, these were helper functions used to -create my website, , but i started wanting them across -projects and at work. - ```sh -# alias install "lib" using "@paperclover/lib" +# alias install "@paperclover/lib" as "@clo/lib" npm install lib@npm:@paperclover/lib ``` -all top-level namespaces stand alone as their own projects, and generally do not -depend on each other. there are no external dependencies; code is easy to audit. - -the public api is simply all of the code. an overview: - - - - - - - - - -- `assert` -- `log` - logging, terminal i/o, interactive widgets -- `log/ansi` - cursor and color constants -- `log/headless` - use `log` and `log/progress` without a tty -- `log/progress` - progress bar, spinner, and tree -- `log/stack` - stack trace parse and formatter -- `packaging` - workflows used for `lib`'s publications -- `queue` - priority queue with rescheduling -- `string` -- `ts` typescript types and general purpose code - -these libraries are published as is, **without versioning or maintainance -guarantees**. every version is a breaking major release. please upgrade your -libraries with intent, considering api changes in [readme.changes.md]. -individual modules may annotate their intended api and usage stability. - -keep in mind there are [known bugs and missing features] in this release. - -to contribute, please send bug reports or patches to . new -features may not be accepted if they are complex and don't further my use cases. - -[readme.changes.md]: ./readme.changes.md -[known bugs and missing features]: https://git.paperclover.net/clo/sitegen/issues?state=open&labels=4 -[clover's typescript library]: https://git.paperclover.net/clo/sitegen/src/branch/master/lib +the distribution on npm is transpiled with JSDoc annotations instead of +publishing the source code directly. this is done because +[Node.js does not plan to support TypeScript within `node_modules`](https://github.com/nodejs/node/issues/57215), +which is extremely sad to see. diff --git a/lib/string.ts b/lib/string.ts index be749150f1aad50652067d4c553f00fe7411d901..aa6543f42fb3030341e61aaf858cdfd883fe0ec9 100644 --- a/lib/string.ts +++ b/lib/string.ts @@ -1,3 +1,8 @@ +/** Escape a value for use in a regular expression. */ +export function escapeRegExp(unsafeText: string) { + return unsafeText.replace(/[\$\\]/g, "\\$&"); +} + // TODO: this function is not benchmarked export function countNewlines(str: string): number { let count = 0; diff --git a/package-lock.json b/package-lock.json index 3bb936e327dd3e8f566abcabeb9cef998bd281c0..46a8b52426e5cf656e75e0f60dfc6fff774cbffa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,45 +5,31 @@ "packages": { "": { "dependencies": { - "@hono/node-server": "^1.14.3", - "@mdx-js/mdx": "^3.1.0", - "@paperclover/console": "git+https://git.paperclover.net/clo/console.git", - "blurhash": "^2.0.5", - "codemirror": "^6.0.1", - "devalue": "^5.1.1", - "esbuild": "^0.25.5", - "hls.js": "^1.6.5", - "hono": "^4.7.11", + "@hono/node-server": "1.14.3", + "@mdx-js/mdx": "3.1.0", + "codemirror": "6.0.1", + "dashjs": "^5.0.3", + "devalue": "^5.3.2", + "esbuild": "0.25.5", + "hls.js": "1.6.5", + "hono": "4.9.9", "lib": "file:lib", - "marko": "^6.0.20", - "msgpackr": "^1.11.5", - "puppeteer": "^24.10.1", - "sharp": "^0.34.2", - "source-map-support": "^0.5.21", - "ts-to-jsdoc": "^2.4.0", - "unique-names-generator": "^4.7.1", - "vscode-oniguruma": "^2.0.1", - "vscode-textmate": "^9.2.0" + "marko": "6.0.82", + "msgpackr": "1.11.5", + "puppeteer": "24.23.0", + "sharp": "0.34.2", + "source-map-support": "0.5.21", + "ts-to-jsdoc": "2.4.0", + "unique-names-generator": "4.7.1", + "vscode-oniguruma": "2.0.1", + "vscode-textmate": "9.2.0" }, "devDependencies": { - "@types/node": "^24.0.10", - "typescript": "^5.8.3" + "@types/node": "24.0.10", + "typescript": "5.8.3" } }, "lib": {}, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -59,30 +45,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -98,13 +84,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -142,17 +128,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "engines": { @@ -198,14 +184,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -293,25 +279,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -371,9 +357,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -394,17 +380,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -412,9 +398,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -516,9 +502,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], @@ -532,9 +518,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], @@ -548,9 +534,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], @@ -564,9 +550,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], @@ -580,9 +566,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], @@ -596,9 +582,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], @@ -612,9 +598,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], @@ -628,9 +614,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], @@ -644,9 +630,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], @@ -660,9 +646,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], @@ -676,9 +662,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], @@ -692,9 +678,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], @@ -708,9 +694,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], @@ -724,9 +710,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], @@ -740,9 +726,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], @@ -756,9 +742,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], @@ -772,9 +758,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -788,9 +774,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], @@ -804,9 +790,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], @@ -820,9 +806,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], @@ -836,9 +822,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], @@ -851,26 +837,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], @@ -884,9 +854,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], @@ -900,9 +870,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], @@ -916,9 +886,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], @@ -932,9 +902,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.15.0.tgz", - "integrity": "sha512-MjmK4l5N4dQpZ9OSWN0tCj7ejuc7WvuWMzSKtc89bnknJykAeHxzRigXBTYZk85H6Awrii6RM59iUiUluApu2A==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.3.tgz", + "integrity": "sha512-KuDMwwghtFYSmIpr4WrKs1VpelTrptvJ+6x6mbUcZnFcc213cumTF5BdqfHyW93B19TNI4Vaev14vOI2a0Ie3w==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -1340,15 +1310,25 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1365,9 +1345,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1414,25 +1394,25 @@ "license": "MIT" }, "node_modules/@marko/compiler": { - "version": "5.39.28", - "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.28.tgz", - "integrity": "sha512-jO/GGNWwyHlCE/fAhiLc1+Xc3apU73wfmjqBFiQmFHyAT2KZRyKJbPz7aVs8uutrPY8Xks9ZCXiXOk0RgvUlvg==", + "version": "5.39.40", + "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.40.tgz", + "integrity": "sha512-gQSdfJ5XOy01DtP7ULiqI1onp7Cix1QvnjM0KbfR2HMrWNI14V8V/2JguFAm3BzKEMZmmgvRLk0rvBzSm9FP6Q==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/core": "^7.26.10", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/plugin-syntax-typescript": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@babel/plugin-transform-typescript": "^7.27.0", - "@babel/runtime": "^7.27.0", - "@babel/traverse": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/code-frame": "^7.27.1", + "@babel/core": "^7.28.0", + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/runtime": "^7.28.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", "@luxass/strip-json-comments": "^1.4.0", "complain": "^1.6.1", "he": "^1.2.0", - "htmljs-parser": "^5.5.4", + "htmljs-parser": "^5.7.3", "jsesc": "^3.1.0", "kleur": "^4.1.5", "lasso-package-root": "^1.0.1", @@ -1596,26 +1576,18 @@ "node": ">= 8" } }, - "node_modules/@paperclover/console": { - "resolved": "git+https://git.paperclover.net/clo/console.git#1a6ac2b79fdd8a21a1c57d25723975872bc07e3e", - "dependencies": { - "ansi-escapes": "^7.0.0", - "chalk": "^5.4.1", - "strip-ansi": "^7.1.0" - } - }, "node_modules/@puppeteer/browsers": { - "version": "2.10.5", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", - "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.10.tgz", + "integrity": "sha512-3ZG500+ZeLql8rE0hjfhkycJjDj0pI/btEh3L9IkWUYcOrgP0xCNRq3HbtbqOPbvDhFaAWD88pDFtlLv8ns8gA==", "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.1", + "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.2", - "tar-fs": "^3.0.8", + "tar-fs": "^3.1.0", "yargs": "^17.7.2" }, "bin": { @@ -1637,6 +1609,12 @@ "node": ">=10" } }, + "node_modules/@svta/common-media-library": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz", + "integrity": "sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==", + "license": "Apache-2.0" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -1710,9 +1688,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz", - "integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==", + "version": "24.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", + "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1771,31 +1749,13 @@ "node": ">= 14" } }, - "node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/ansi-styles": { @@ -1847,10 +1807,18 @@ } }, "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "license": "Apache-2.0" + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, "node_modules/bail": { "version": "2.0.2", @@ -1869,22 +1837,23 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", - "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", - "license": "Apache-2.0", - "optional": true + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", + "license": "Apache-2.0" }, "node_modules/bare-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", - "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.5.tgz", + "integrity": "sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==", "license": "Apache-2.0", "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", - "bare-stream": "^2.6.4" + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" }, "engines": { "bare": ">=1.16.0" @@ -1899,9 +1868,9 @@ } }, "node_modules/bare-os": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", - "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", "license": "Apache-2.0", "optional": true, "engines": { @@ -1919,9 +1888,9 @@ } }, "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1940,6 +1909,25 @@ } } }, + "node_modules/bare-url": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz", + "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz", + "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -1949,11 +1937,44 @@ "node": ">=10.0.0" } }, - "node_modules/blurhash": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", - "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", - "license": "MIT" + "node_modules/bcp-47": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", + "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-normalize": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", + "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", + "license": "MIT", + "dependencies": { + "bcp-47": "^2.0.0", + "bcp-47-match": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/brace-expansion": { "version": "2.0.2", @@ -1977,9 +1998,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "funding": [ { "type": "opencollective", @@ -1996,9 +2017,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -2033,9 +2055,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001747", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz", + "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==", "funding": [ { "type": "opencollective", @@ -2062,18 +2084,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -2115,9 +2125,9 @@ } }, "node_modules/chromium-bidi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", - "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz", + "integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==", "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", @@ -2141,37 +2151,22 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "license": "MIT" }, + "node_modules/codem-isoboxer": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz", + "integrity": "sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==", + "license": "MIT" + }, "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -2297,6 +2292,24 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/dashjs": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.0.3.tgz", + "integrity": "sha512-TXndNnCUjFjF2nYBxDVba+hWRpVkadkQ8flLp7kHkem+5+wZTfRShJCnVkPUosmjS0YPE9fVNLbYPJxHBeQZvA==", + "license": "BSD-3-Clause", + "dependencies": { + "@svta/common-media-library": "^0.12.4", + "bcp-47-match": "^2.0.3", + "bcp-47-normalize": "^2.3.0", + "codem-isoboxer": "0.3.10", + "fast-deep-equal": "3.1.3", + "html-entities": "^2.5.2", + "imsc": "^1.1.5", + "localforage": "^1.10.0", + "path-browserify": "^1.0.1", + "ua-parser-js": "^1.0.37" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -2307,9 +2320,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2388,15 +2401,15 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1464554", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", - "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "version": "0.0.1508733", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz", + "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==", "license": "BSD-3-Clause" }, "node_modules/electron-to-chromium": { - "version": "1.5.181", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.181.tgz", - "integrity": "sha512-+ISMj8OIQ+0qEeDj14Rt8WwcTOiqHyAB+5bnK1K7xNNLjBJ4hRCQfUkw8RWtcLbfBzDwc15ZnKH0c7SNOfwiyA==", + "version": "1.5.229", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.229.tgz", + "integrity": "sha512-cwhDcZKGcT/rEthLRJ9eBlMDkh1sorgsuk+6dpsehV0g9CABsIqBxU4rLRjG+d/U6pYU1s37A4lSKrVc5lSQYg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -2423,18 +2436,6 @@ "node": ">=6" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2486,9 +2487,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2498,32 +2499,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/escalade": { @@ -2688,6 +2688,15 @@ "node": ">=0.10.0" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2714,6 +2723,12 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -2903,9 +2918,9 @@ } }, "node_modules/hls.js": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.7.tgz", - "integrity": "sha512-QW2fnwDGKGc9DwQUGLbmMOz8G48UZK7PVNJPcOUql1b8jubKx4/eMHNP5mGqr6tYlJNDG1g10Lx2U/qPzL6zwQ==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.5.tgz", + "integrity": "sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ==", "license": "Apache-2.0" }, "node_modules/hono": { @@ -2917,10 +2932,26 @@ "node": ">=16.9.0" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/htmljs-parser": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-5.5.4.tgz", - "integrity": "sha512-qJiir3ViASVhGdNW4kwM4pQWvDtDLtjwaflEQDooiNzbOPIzoaIzmRFWweFl8GomVhJqXqX0jjrhDED3pmAdzg==", + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-5.7.4.tgz", + "integrity": "sha512-PCGI7/Z10oBkpKKqbQ9Wgd0GZjSQ/dV7QlBjOmaqD1FkBsHWk730BW9mJCbnXfRbtZCdLh8M8IA8YeKPu9tGaQ==", "license": "MIT" }, "node_modules/http-proxy-agent": { @@ -2949,6 +2980,12 @@ "node": ">= 14" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2974,6 +3011,15 @@ "node": ">=4" } }, + "node_modules/imsc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz", + "integrity": "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==", + "license": "BSD-2-Clause", + "dependencies": { + "sax": "1.2.1" + } + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -2981,14 +3027,10 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } @@ -3112,12 +3154,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT" - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3179,12 +3215,30 @@ "resolved": "lib", "link": true }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -3226,12 +3280,12 @@ } }, "node_modules/marko": { - "version": "6.0.35", - "resolved": "https://registry.npmjs.org/marko/-/marko-6.0.35.tgz", - "integrity": "sha512-1zoMBeMEfltQ8QtmW0WoYYsjC0vUJaomQPpXQSlDw9i1o7YZ6T9v8ZSPcScv4ODRM1+XQ0hfNuUT+u7qijXcsA==", + "version": "6.0.82", + "resolved": "https://registry.npmjs.org/marko/-/marko-6.0.82.tgz", + "integrity": "sha512-xnBm299wmqrK8++12dhGHBUonm2SZgQVPNhHZjfp3Wc7k4U/qPbKrc7H1DJgU7JrYv8sSdxkconisynDqBYC1g==", "license": "MIT", "dependencies": { - "@marko/compiler": "^5.39.28", + "@marko/compiler": "^5.39.40", "csstype": "^3.1.3", "magic-string": "^0.30.17" }, @@ -4125,9 +4179,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "license": "MIT" }, "node_modules/once": { @@ -4320,17 +4374,17 @@ } }, "node_modules/puppeteer": { - "version": "24.12.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.12.1.tgz", - "integrity": "sha512-+vvwl+Xo4z5uXLLHG+XW8uXnUXQ62oY6KU6bEFZJvHWLutbmv5dw9A/jcMQ0fqpQdLydHmK0Uy7/9Ilj8ufwSQ==", + "version": "24.23.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.23.0.tgz", + "integrity": "sha512-BVR1Lg8sJGKXY79JARdIssFWK2F6e1j+RyuJP66w4CUmpaXjENicmA3nNpUXA8lcTdDjAndtP+oNdni3T/qQqA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.5", - "chromium-bidi": "5.1.0", + "@puppeteer/browsers": "2.10.10", + "chromium-bidi": "9.1.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1464554", - "puppeteer-core": "24.12.1", + "devtools-protocol": "0.0.1508733", + "puppeteer-core": "24.23.0", "typed-query-selector": "^2.12.0" }, "bin": { @@ -4341,16 +4395,17 @@ } }, "node_modules/puppeteer-core": { - "version": "24.12.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.12.1.tgz", - "integrity": "sha512-8odp6d3ERKBa3BAVaYWXn95UxQv3sxvP1reD+xZamaX6ed8nCykhwlOiHSaHR9t/MtmIB+rJmNencI6Zy4Gxvg==", + "version": "24.23.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.23.0.tgz", + "integrity": "sha512-yl25C59gb14sOdIiSnJ08XiPP+O2RjuyZmEG+RjYmCXO7au0jcLf7fRiyii96dXGUBW7Zwei/mVKfxMx/POeFw==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.5", - "chromium-bidi": "5.1.0", - "debug": "^4.4.1", - "devtools-protocol": "0.0.1464554", + "@puppeteer/browsers": "2.10.10", + "chromium-bidi": "9.1.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1508733", "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.3.6", "ws": "^8.18.3" }, "engines": { @@ -4578,6 +4633,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/self-closing-tags": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/self-closing-tags/-/self-closing-tags-1.0.1.tgz", @@ -4675,12 +4736,12 @@ } }, "node_modules/socks": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", - "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -4740,12 +4801,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" - }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -4753,16 +4808,14 @@ "license": "MIT" }, "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string-width": { @@ -4779,27 +4832,6 @@ "node": ">=8" } }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -4815,18 +4847,15 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/style-mod": { @@ -4854,9 +4883,9 @@ } }, "node_modules/tar-fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", - "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { "pump": "^3.0.0", @@ -4971,6 +5000,32 @@ "node": ">=14.17" } }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", @@ -5163,6 +5218,12 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.6.tgz", + "integrity": "sha512-mlGndEOA9yK9YAbvtxaPTqdi/kaCWYYfwrZvGzcmkr/3lWM+tQj53BxtpVd6qbC6+E5OnHXgCcAhre6AkXzxjA==", + "license": "Apache-2.0" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5180,27 +5241,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 1617bac140b3b0873574b45bf1478d3f1c70ab1a..3ba0610088552a158e83917d8a7617985c1fc718 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,28 @@ { "type": "module", "dependencies": { - "@hono/node-server": "^1.14.3", - "@mdx-js/mdx": "^3.1.0", - "@paperclover/console": "git+https://git.paperclover.net/clo/console.git", - "blurhash": "^2.0.5", - "codemirror": "^6.0.1", - "devalue": "^5.1.1", - "esbuild": "^0.25.5", - "hls.js": "^1.6.5", - "hono": "^4.7.11", + "@hono/node-server": "1.14.3", + "@mdx-js/mdx": "3.1.0", + "codemirror": "6.0.1", + "dashjs": "^5.0.3", + "devalue": "^5.3.2", + "esbuild": "0.25.5", + "hls.js": "1.6.5", + "hono": "4.9.9", "lib": "file:lib", - "marko": "^6.0.20", - "msgpackr": "^1.11.5", - "puppeteer": "^24.10.1", - "sharp": "^0.34.2", - "source-map-support": "^0.5.21", - "ts-to-jsdoc": "^2.4.0", - "unique-names-generator": "^4.7.1", - "vscode-oniguruma": "^2.0.1", - "vscode-textmate": "^9.2.0" + "marko": "6.0.82", + "msgpackr": "1.11.5", + "puppeteer": "24.23.0", + "sharp": "0.34.2", + "source-map-support": "0.5.21", + "ts-to-jsdoc": "2.4.0", + "unique-names-generator": "4.7.1", + "vscode-oniguruma": "2.0.1", + "vscode-textmate": "9.2.0" }, "devDependencies": { - "@types/node": "^24.0.10", - "typescript": "^5.8.3" + "@types/node": "24.0.10", + "typescript": "5.8.3" }, "imports": { "#backend": "./src/backend.ts", diff --git a/repl.js b/repl.js index 2340dddf7c359aeb88f1e2351d2390e954371fa9..86ede140e76d8e3f7d9dff314436907e5043aeb7 100644 --- a/repl.js +++ b/repl.js @@ -3,21 +3,20 @@ // doesn't have to reload the plugin system. This is mostly // done since Clover's primary dev-env is a Windows 7 laptop; // It takes her machine 600ms to load the plugin file on it's own. +// +// TODO: as of 2025-10-03, this file is not maintained import process from "node:process"; import * as util from "node:util"; const exts = [".tsx", ".ts", ".js", ".marko"]; process.stderr.write("Loading..."); const { hot } = await import("./run.js"); // get plugins ready -const { errorAllWidgets } = hot.load("@paperclover/console/Widget"); process.stderr.write("\r" + " ".repeat("Loading...".length) + "\r"); const repl = hot.load("node:repl").start({ prompt: "% ", eval(code, _global, _id, done) { evaluate(code) .catch((err) => { - // TODO: improve @paperclover/console's ability to print AggregateError - // and errors with extra random properties console.error(util.inspect(err, false, 10, true)); }) .then((result) => done(null, result)); diff --git a/src/backend.ts b/src/backend.ts index 74236a90bd6db9940aa6e4e93ed272e509ac84ca..f2994f1c6d4d685643eb6f10807b10f865a26b28 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -1,11 +1,11 @@ // This is the main file for paperclover.net's server. const app = new Hono(); -const logHttp = console.scoped("http", { color: "magenta" }); +const console = log.scoped("http"); // Middleware app.use(trimTrailingSlash()); app.use(removeDuplicateSlashes); -app.use(logger((msg) => msg.startsWith("-->") && logHttp(msg.slice(4)))); +app.use(logger((msg) => msg.startsWith("-->") && console.info(msg.slice(4)))); app.use(admin.middleware); // Backends @@ -52,4 +52,4 @@ import { logger } from "hono/logger"; import { trimTrailingSlash } from "hono/trailing-slash"; import * as assets from "#sitegen/assets"; import * as admin from "./admin.ts"; -import * as console from "@paperclover/console"; +import * as log from "lib/log.ts"; diff --git a/src/file-viewer/backend.tsx b/src/file-viewer/backend.tsx index 42af23d8114adc839e93b0a877a1f25e99ffd88a..20a55e54907d6f1ce6bdae6f7d6fd9cec077c1db 100644 --- a/src/file-viewer/backend.tsx +++ b/src/file-viewer/backend.tsx @@ -18,8 +18,12 @@ app.get("/file/*", async (c, next) => { let rawFilePath = c.req.path.slice(5) || "/"; + let derivedKey = ""; + if (rawFilePath.includes(":/")) { + [rawFilePath, derivedKey] = rawFilePath.split(":/") as [string, string]; + } // The web file viewer sends urls suffixed with $partial to get HTML partials. - if (rawFilePath.endsWith("$partial")) { + if (!derivedKey && rawFilePath.endsWith("$partial")) { return getPartialPage(c, rawFilePath.slice(0, -"$partial".length)); } @@ -72,23 +76,25 @@ app.get("/file/*", async (c, next) => { } const download = viewMode === "download"; - const etag = file.hash; - // const filePath = file.path; - const size = file.size; + // Grab a derived asset if that was requested. + let { hash, size, date } = file; + if (derivedKey) { + const entry = derived.get(file, derivedKey); + if (entry) { + ({ hash, size, date, path: derivedKey } = entry); + } else { + return next(); + } + } + const etag = `"${hash}"`; - //let encoding = decideEncoding(c.req.header("Accept-Encoding")); - - // let sizeHeader = encoding === "raw" - // ? expectedSize - // // Size cannot be known because of compression modes - // : undefined; - // const headers = new Headers({ Vary: "Accept-Encoding, Accept", - "Content-Type": contentTypeFor(file.path), + "Content-Type": contentTypeFor(derivedKey || file.path), "Content-Length": size.toString(), - ETag: file.hash, - "Last-Modified": file.date.toUTCString(), + ETag: etag, + "Last-Modified": date.toUTCString(), + "Accept-Ranges": "bytes", }); if (download) { headers.set( @@ -110,15 +116,8 @@ app.get("/file/*", async (c, next) => { } } - // Head - if (c.req.method === "HEAD") { - c.res = new Response(null, { headers }); - return; - } - - // Prevalidate range requests + let status = 200; let rangeHeader = c.req.header("Range") ?? null; - const ifRangeHeader = c.req.header("If-Range"); if (ifRangeHeader && ifRangeOutdated(file, ifRangeHeader)) { // > If the condition is not fulfilled, the full resource is @@ -126,70 +125,56 @@ app.get("/file/*", async (c, next) => { rangeHeader = null; } - const found = await cache.read(filePath); - // foundFile = await cache.read(filePath, encoding, { - // start: rangeHeader ? rangeHeader[0] : 0, - // end: rangeHeader ? rangeHeader[1] : Infinity, - // }); - // const [streamOrBuffer, actualEncoding, src] = foundFile; - // encoding = actualEncoding; - // - // // Range requests - // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests - // // Compression is skipped because it's a confusing, but solvable problem. - // // See https://stackoverflow.com/questions/33947562/is-it-possible-to-send-http-response-using-gzip-and-byte-ranges-at-the-same-time - // if (rangeHeader) { - // const ranges = parseRange(rangeHeader, file.size); - // // TODO: multiple ranges - // if (ranges && ranges.length === 1) { - // return (c.res = handleRanges(ranges, file, streamOrBuffer, download)); - // } - // } - // - // // Respond in a streaming fashion - // c.res = new Response(streamOrBuffer, { - // headers: { - // ...fileHeaders(file, download, sizeHeader), - // ...(encoding !== "raw" && { - // "Content-Encoding": encoding, - // }), - // "X-Cache": src, - // }, - // }); -}); + let start = 0; + let end = Infinity; + + // Range requests + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests + if (rangeHeader) { + const ranges = parseRange(rangeHeader, file.size); + if (ranges?.[0] && ranges.length === 1) { + start = ranges[0][0]; + end = ranges[0][1]; -app.get("/canvas/:script", async (c, next) => { - const script = c.req.param("script"); - if (!hasAsset(`/js/canvas/${script}.js`)) { - return next(); + status = 206; + headers.set( + "Content-Range", + `bytes ${ranges[0][0]}-${ranges[0][1]}/${file.size}`, + ); + headers.delete("Content-Length"); + } else { + // multi-ranges are not supported + // it could be done by calling `cache.read` multiple times. + } } - return view.serve(c, "file-viewer/canvas", { - script, - }); -}); -function decideEncoding(encodings: string | undefined): CompressionFormat { - if (encodings?.includes("zstd")) return "zstd"; - if (encodings?.includes("gzip")) return "gzip"; - return "raw"; -} + // Head + if (c.req.method === "HEAD") { + c.res = new Response(null, { headers, status }); + return; + } -function fileHeaders( - file: MediaFile, - download: boolean, - size: number | undefined = file.size, -) { - return { - Vary: "Accept-Encoding, Accept", - "Content-Type": contentTypeFor(file.path), - "Content-Length": size.toString(), - ETag: file.hash, - "Last-Modified": file.date.toUTCString(), - ...(download && { - "Content-Disposition": `attachment; filename="${file.basename}"`, - }), - }; -} + const found = await cache.read({ + file, + hash, + derived: derivedKey, + start, + end, + }); + if (!found) { + return Response.json({ + error: "localOnly is enabled and this file is not available", + }, 500); + } + if (status === 200) { + headers.set("Content-Length", found.size.toString()); + } + headers.set("Cache-Status", found.src); + c.res = new Response(found.stream as ReadableStream, { + headers, + status, + }); +}); function ifRangeOutdated(file: MediaFile, ifRangeHeader: string) { // etag @@ -205,6 +190,7 @@ type Ranges = Array<[start: number, end: number]>; function parseRange(rangeHeader: string, fileSize: number): Ranges | null { const [unit, ranges] = rangeHeader.split("="); + if (!ranges) return null; if (unit !== "bytes") return null; const result: Array<[start: number, end: number]> = []; @@ -212,8 +198,8 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null { for (const range of rangeParts) { const split = range.split("-"); - if (split.length !== 2) return null; const [start, end] = split; + if (!start || !end) return null; if (start === "" && end === "") return null; const parsedRange: [number, number] = [ start === "" ? fileSize - +end : +start, @@ -225,7 +211,7 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null { // Validate that ranges do not intersect result.sort((a, b) => a[0] - b[0]); for (let i = 1; i < result.length; i++) { - if (result[i][0] <= result[i - 1][1]) { + if (UNWRAP(result[i])[0] <= UNWRAP(result[i - 1])[1]) { return null; } } @@ -233,94 +219,6 @@ function parseRange(rangeHeader: string, fileSize: number): Ranges | null { return result; } -function handleRanges( - ranges: Ranges, - file: MediaFile, - streamOrBuffer: ReadableStream | Buffer, - download: boolean, -): Response { - // TODO: multiple ranges - const rangeSize = ranges.reduce((a, b) => a + (b[1] - b[0] + 1), 0); - const rangeBody = streamOrBuffer instanceof ReadableStream - ? applySingleRangeToStream(streamOrBuffer, ranges) - : applyRangesToBuffer(streamOrBuffer, ranges, rangeSize); - return new Response(rangeBody, { - status: 206, - headers: { - ...fileHeaders(file, download, rangeSize), - "Content-Range": `bytes ${ranges[0][0]}-${ranges[0][1]}/${file.size}`, - }, - }); -} - -function applyRangesToBuffer( - buffer: Buffer, - ranges: Ranges, - rangeSize: number, -): Uint8Array { - const result = new Uint8Array(rangeSize); - let offset = 0; - for (const [start, end] of ranges) { - result.set(buffer.subarray(start, end + 1), offset); - offset += end - start + 1; - } - return result; -} - -function applySingleRangeToStream( - stream: ReadableStream, - ranges: Ranges, -): ReadableStream { - let reader: ReadableStreamDefaultReader; - let position = 0; - const [start, end] = ranges[0]; - return new ReadableStream({ - async start(controller) { - reader = stream.getReader(); - try { - while (position <= end) { - const { done, value } = await reader.read(); - - if (done) { - controller.close(); - return; - } - - const buffer = new Uint8Array(value); - - const bufferStart = position; - const bufferEnd = position + buffer.length - 1; - - position += buffer.length; - - if (bufferEnd < start) { - continue; - } - - if (bufferStart > end) { - break; - } - - const sendStart = Math.max(0, start - bufferStart); - const sendEnd = Math.min(buffer.length - 1, end - bufferStart); - - if (sendStart <= sendEnd) { - controller.enqueue(buffer.slice(sendStart, sendEnd + 1)); - } - } - - controller.close(); - } catch (error) { - controller.error(error); - } finally { - reader.releaseLock(); - } - }, - cancel() { - reader?.releaseLock(); - }, - }); -} function checkCotyledonCookie(c: Context) { const cookie = c.req.header("Cookie"); if (!cookie) return false; @@ -383,7 +281,7 @@ function getPartialPage(c: Context, rawFilePath: string) { import { type Context, Hono } from "hono"; import * as render from "#engine/render"; -import { etagMatches, hasAsset, serveAsset } from "#sitegen/assets"; +import { etagMatches, serveAsset } from "#sitegen/assets"; import * as view from "#sitegen/view"; import { contentTypeFor } from "#sitegen/mime"; @@ -393,3 +291,4 @@ import { FilePermissions } from "@/file-viewer/models/FilePermissions.ts"; import { MediaPanel } from "@/file-viewer/views/clofi.tsx"; import { Speedbump } from "@/file-viewer/cotyledon.tsx"; import * as cache from "@/file-viewer/cache.ts"; +import * as derived from "@/file-viewer/models/derived.ts"; diff --git a/src/file-viewer/bin/scan3.ts b/src/file-viewer/bin/scan3.ts index 4f1c0f9f5b872d38560053734218fe905a1328f8..e01e16824270a364b86fda451575ec9a1ffcc62f 100644 --- a/src/file-viewer/bin/scan3.ts +++ b/src/file-viewer/bin/scan3.ts @@ -19,6 +19,8 @@ export async function main() { `paper clover's scan3 [${((now - start) / 1000).toFixed(1)}s]`, }); + const promises = new async.PromiseAggregator(); + const walkQueue = new queue.PriorityQueue(10); const dirsNode = progress.start("Walk Tree", { estimate: 1 }); @@ -46,19 +48,15 @@ export async function main() { node.text = publicPath + " - reading"; const items = (await path.readDir()) .filter((child) => !skipBasename(child.base)) - .map((child) => (scanDirectory(child), child.base)); + .map((child) => (promises.push(scanDirectory(child)), child.base)); dirsNode.estimate += items.length; - if (mediaFile) { - const deleted = mediaFile - .getChildren() - .filter((child) => !items.includes(child.basename)) - .flatMap((child) => - child.kind === MediaFileKind.directory - ? child.getRecursiveFileChildren() - : child - ); - //deleted + for (const child of mediaFile?.getChildren() ?? []) { + if (items.includes(child.basename)) continue; + const recursive = child.kind === MediaFileKind.directory + ? [child, ...child.getRecursiveFileChildren()] + : [child]; + for (const deletion of recursive) deletion.delete(); } return; @@ -70,10 +68,10 @@ export async function main() { stat.size !== mediaFile.size || stat.mtime.getTime() !== mediaFile.date.getTime() ) { - updateMetadata({ path, publicPath, stat, mediaFile }); + promises.push(updateMetadata({ path, publicPath, stat, mediaFile })); } else { // If the scanners changed, it may mean more processes should be run. - queueProcessors({ + await queueProcessors({ path, stat, mediaFile, @@ -144,15 +142,16 @@ export async function main() { processor, index, after, - fileNode, + fileNode: innerNode, }: ProcessJob) { - using node = fileNode.start(processor.name); + using node = innerNode.start(processor.name); await processor.run({ path, stat, mediaFile, node, }); + fileNode.value += 1; mediaFile.setProcessed(mediaFile.processed | (1 << (16 + index))); for (const dependantJob of after) { ASSERT( @@ -164,7 +163,7 @@ export async function main() { queueFileProcessor(dependantJob); } } - }, (job) => ({ cores: job.processor.cores })); + }, (job) => ({ cores: job.processor.cores ?? 0 })); function decodeProcessors(input: string) { return input @@ -201,7 +200,13 @@ export async function main() { // If the hash has changed, migrate the bitfield over. // This also runs when the processor hash is in it's initial 0 state. - const order = decodeProcessors(mediaFile.processors); + let order: ReturnType; + try { + order = decodeProcessors(mediaFile.processors); + } catch { + // this function sucks and this system sucks i hate it. + order = []; + } if ((processed & 0xffff) !== hash) { const previous = order.filter( (_, i) => (processed & (1 << (16 + i))) !== 0, @@ -241,10 +246,11 @@ export async function main() { fileNode: node, }; jobs.push(job); - if (job.needs === 0) queueFileProcessor(job); + if (job.needs === 0) promises.push(queueFileProcessor(job)); } } node.estimate = jobs.length; + fileNode.estimate += jobs.length; for (const job of jobs) { for (const dependId of job.processor.depends) { const dependJob = jobs.find((j) => j.processor.id === dependId); @@ -253,7 +259,7 @@ export async function main() { } else { ASSERT(job.needs > 0, `job.needs !== 0, ${job.needs}`); job.needs -= 1; - if (job.needs === 0) queueFileProcessor(job); + if (job.needs === 0) promises.push(queueFileProcessor(job)); } } } @@ -265,16 +271,18 @@ export async function main() { if (!rootPath.ifExistsSync()) { throw new Error(`file store ${rootPath} is not mounted`); } - scanDirectory(rootPath); - // await qList.done(); - // await qMeta.done(); - // await qProcess.done(); - return; + promises.push(scanDirectory(rootPath)); + + await promises.all(); + fileNode.end(); + dirsNode.end(); // Update directory metadata + using dirMetaNode = progress.start("update directory metadata"); const dirs = MediaFile.getDirectoriesToReindex() .sort((a, b) => b.path.length - a.path.length); for (const dir of dirs) { + using _ = dirMetaNode.start(dir.path); const children = dir.getChildren(); // readme.txt @@ -327,56 +335,58 @@ export async function main() { dirsort, }); } + dirMetaNode.end(); // Sync to remote - // if ( - // ((await derived.workDir.ifExistsSync()?.readDir())?.length ?? 0) > 0 - // ) { - // await rsync.spawn({ - // args: [ - // "--links", - // "--recursive", - // "--times", - // "--partial", - // "--progress", - // "--remove-source-files", - // "--delay-updates", - // derived.workDir.toString() + "/", - // "clo@zenith:/mnt/storage1/clover/Documents/Config/clover_file/derived/", - // ], - // title: "Uploading Derived Assets", - // cwd: process.cwd(), - // }); - // - // await fs.removeEmptyDirectories(derived.workDir.toString()); - // } else { - // console.info("No new derived assets"); - // } + if ( + ((await derived.workDir.ifExistsSync()?.readDir())?.length ?? 0) > 0 + ) { + await rsync.spawn({ + args: [ + "--links", + "--recursive", + "--times", + "--partial", + "--progress", + // "--remove-source-files", + "--delay-updates", + "--exclude=tmp.*", + derived.workDir.toString() + "/", + "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/derived/", + ], + progress: progress.start("upload derived assets"), + cwd: process.cwd(), + }); + + await fs.removeEmptyDirectories(derived.workDir.toString()); + } else { + console.info("No new derived assets"); + } MediaFile.db.prepare("VACUUM").run(); MediaFile.db.reload(); - // await rsync.spawn({ - // args: [ - // MediaFile.db.file, - // "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/cache.sqlite", - // ], - // title: "Uploading Database", - // cwd: process.cwd(), - // }); - // if (sotToken) { - // const res = await fetch("https://db.paperclover.net/reload", { - // method: "post", - // headers: { - // Authorization: sotToken, - // }, - // }); - // if (!res.ok) { - // console.warn( - // `Failed to reload remote database ${res.status} ${res.statusText}`, - // ); - // } - // } else console.warn("Missing SOT token"); + await rsync.spawn({ + args: [ + MediaFile.db.file, + "clo@zenith:/mnt/storage1/clover/Documents/Config/paperclover/cache.sqlite", + ], + progress: progress.start("Uploading database"), + cwd: process.cwd(), + }); + if (sotToken) { + const res = await fetch("https://db.paperclover.net/reload", { + method: "post", + headers: { + Authorization: sotToken, + }, + }); + if (!res.ok) { + console.warn( + `Failed to reload remote database ${res.status} ${res.statusText}`, + ); + } + } else console.warn("Missing SOT token"); console.info( "Updated file viewer index in \x1b[1m" + @@ -392,21 +402,30 @@ export async function main() { from media_files `) .getNonNull(); + const { derivedSize, derivedCount } = MediaFile.db + .prepare<[], { derivedCount: number; derivedSize: number }>(` + select + count(*) as derivedCount, + sum(size) as derivedSize + from derived_files + `) + .getNonNull(); + const canonicalSize = UNWRAP(MediaFile.getByPath("/")).size; console.info(); console.info( "Global Stats:\n" + `- File Count: \x1b[1m${count}\x1b[0m\n` + - `- Canonical Size: \x1b[1m${ - formatSize(MediaFile.getByPath("/")!.size) - }\x1b[0m\n` + - `- Media Duration: \x1b[1m${formatDurationLong(duration)}\x1b[0m\n`, + `- Derived Count: \x1b[1m${derivedCount}\x1b[0m\n` + + `- Media Duration: \x1b[1m${formatDurationLong(duration)}\x1b[0m\n` + + `- Canonical Size: \x1b[1m${formatSize(canonicalSize)}\x1b[0m\n` + + `- Derived Size: \x1b[1m${formatSize(derivedSize)}\x1b[0m\n`, ); } interface Process { name: string; - cores: number; + cores?: number; enable?: boolean; include?: Set; exclude?: Set; @@ -491,6 +510,7 @@ const procLoadTextContents: Process = { name: "load text content", include: rules.extsReadContents, cores: 1, + version: 2, async run({ path, mediaFile, stat }) { if (stat.size > 1_000_000) return; const text = await path.read("utf-8"); @@ -502,6 +522,7 @@ const procHighlightCode: Process = { name: "highlight source code", include: new Set(rules.extsCode.keys()), cores: 1, + version: 2, async run({ path, mediaFile, stat }) { const language = UNWRAP( rules.extsCode.get(path.ext.toLowerCase()), @@ -531,7 +552,6 @@ const procImageSubsets: Process = { include: rules.extsImage, depends: [procDimensions.name], version: 2, - cores: 2, async run({ path, mediaFile, node }) { const { width, height } = UNWRAP(mediaFile.parseDimensions()); const targetSizes = transcodeRules.imageSizes.filter((w) => w < width); @@ -544,16 +564,22 @@ const procImageSubsets: Process = { node.text = baseStatus + ` (${w}x${h}, ${ext.slice(1).toUpperCase()})`; stack.use( - await derived.produce(mediaFile, `${size}${ext}`, async (dir) => { - await subprocess.exec(ffmpegBin!, [ - ...ffmpegOptions, - "-i", - path.toString(), - "-vf", - `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`, - ...args, - dir.join(`${size}${ext}`).toString(), - ]); + await derived.produce({ + mediaFile, + node, + cores: 2, + subkey: `${size}${ext}`, + async producer(dir) { + await subprocess.exec(ffmpegBin!, [ + ...ffmpegOptions, + "-i", + path.toString(), + "-vf", + `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`, + ...args, + dir.join(`${size}${ext}`).toString(), + ]); + }, }), ); } @@ -563,9 +589,7 @@ const procImageSubsets: Process = { }, }; -const videoInputArgsCache = new async.OnceMap2( - transcodeRules.getVideoInputArgs, -); +const videoArgsCache = new async.OnceMap2(transcodeRules.getVideoInputArgs); const qualityMap: Record = { u: "ultra-high", @@ -579,19 +603,32 @@ const procVideos = transcodeRules.videoFormats.map((preset) => ({ include: rules.extsVideo, enable: ffmpegBin != null, depends: [procDuration.name, procDimensions.name], - cores: 4, async run({ path, mediaFile, node }) { if ((mediaFile.duration ?? 0) < 5) return; - await derived.produce(mediaFile, `av1-${preset.id}`, async (dir) => { - const input = await videoInputArgsCache.getOrRun(path); - const args = transcodeRules.getAv1VideoArgs(preset, input.video, dir); + if (!mediaFile.dimensions) return; - await ffmpeg.spawn({ - ffmpeg: ffmpegBin!, - progress: node, - args, - cwd: dir, - }); + if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return; + + await derived.produce({ + mediaFile, + node, + cores: 4, + subkey: `av1-${preset.id}`, + async producer(dir) { + const input = await videoArgsCache.getOrRun(path); + const args = transcodeRules.getAv1VideoArgs( + preset, + UNWRAP(input.video, "frick on " + path + JSON.stringify(input)), + dir, + ); + + await ffmpeg.spawn({ + ffmpeg: ffmpegBin!, + progress: node, + args, + cwd: dir, + }); + }, }); }, })); @@ -600,20 +637,29 @@ const procVideoAudios = transcodeRules.audioFormats.map((preset) => ({ include: rules.extsVideo, enable: ffmpegBin != null, depends: [procDuration.name, procDimensions.name], - cores: 2, + version: 2, async run({ path, mediaFile, node }) { if ((mediaFile.duration ?? 0) < 5) return; - await derived.produce(mediaFile, `opus-${preset.id}`, async (dir) => { - const input = await videoInputArgsCache.getOrRun(path); - if (!input.audio) return; - const args = transcodeRules.getOpusAudioArgs(preset, input.audio, dir); + if (!mediaFile.dimensions) return; + if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return; + await derived.produce({ + mediaFile, + node, + cores: 4, + subkey: `opus-${preset.id}`, + async producer(dir) { + const input = await videoArgsCache.getOrRun(path); + ASSERT(input.video); + if (!input.audio) return; + const args = transcodeRules.getOpusAudioArgs(preset, input.audio, dir); - await ffmpeg.spawn({ - ffmpeg: ffmpegBin!, - progress: node, - args, - cwd: dir, - }); + await ffmpeg.spawn({ + ffmpeg: ffmpegBin!, + progress: node, + args, + cwd: dir, + }); + }, }); }, })); @@ -622,37 +668,50 @@ const procDash: Process = { include: rules.extsVideo, enable: ffmpegBin != null, depends: [...procVideos, ...procVideoAudios].map((x) => x.name), - cores: 1, + version: 4, async run({ path, mediaFile, node }) { if ((mediaFile.duration ?? 0) < 5) return; - await derived.produce(mediaFile, `dash-av1`, async (dir) => { - const input = await videoInputArgsCache.getOrRun(path); - const videos = transcodeRules.videoFormats.map( - (preset) => - UNWRAP(dir.parent).join( - `av1-${preset.id}`, - transcodeRules.av1FileName, - ), - ); - const audios = input.audio - ? transcodeRules.videoFormats.map( + if (!mediaFile.dimensions) return; + + if (mediaFile.path === "/2021/top-10000-bread/output.mp4") return; + await derived.produce({ + mediaFile, + node, + cores: 1, + subkey: `dash-av1`, + async producer(dir) { + const input = await videoArgsCache.getOrRun(path); + const videos = transcodeRules.videoFormats.map( (preset) => UNWRAP(dir.parent).join( `av1-${preset.id}`, transcodeRules.av1FileName, ), - ) - : []; - const args = transcodeRules.getMpegDashArgs(videos, audios, dir); + ); + const audios = input.audio + ? transcodeRules.audioFormats.map( + (preset) => + UNWRAP(dir.parent).join( + `opus-${preset.id}`, + transcodeRules.opusFileName, + ), + ) + : []; + const args = transcodeRules.getMpegDashArgs( + videos, + audios, + dir, + ); - await dir.join("d").makeDir(); + await dir.join("d").makeDir(); - await ffmpeg.spawn({ - ffmpeg: ffmpegBin!, - progress: node, - args, - cwd: dir, - }); + await ffmpeg.spawn({ + ffmpeg: ffmpegBin!, + progress: node, + args, + cwd: dir, + }); + }, }); }, }; @@ -661,19 +720,26 @@ const procH264Hls: Process = { include: rules.extsVideo, enable: ffmpegBin != null, depends: [procDuration.name, procDimensions.name], - cores: 8, async run({ path, mediaFile, node }) { if ((mediaFile.duration ?? 0) < 5) return; - await derived.produce(mediaFile, `hls`, async (dir) => { - const input = await videoInputArgsCache.getOrRun(path); - const args = transcodeRules.getH264HlsArgs(input, dir); + if (!mediaFile.dimensions) return; - await ffmpeg.spawn({ - ffmpeg: ffmpegBin!, - progress: node, - args, - cwd: dir, - }); + await derived.produce({ + mediaFile, + node, + cores: 8, + subkey: `hls`, + async producer(dir) { + const input = await videoArgsCache.getOrRun(path); + const args = transcodeRules.getH264HlsArgs(input, dir); + + await ffmpeg.spawn({ + ffmpeg: ffmpegBin!, + progress: node, + args, + cwd: dir, + }); + }, }); }, }; @@ -685,14 +751,19 @@ const procCompression = [ ({ name, fn }) => ({ name: `compress ${name}`, exclude: rules.extsPreCompressed, - cores: 1, - async run({ path, mediaFile }) { - await derived.produce(mediaFile, name, async (dir) => { - await stream.promises.pipeline( - fs.createReadStream(path.toString()), - fn(), - fs.createWriteStream(dir.join(name).toString()), - ); + async run({ path, mediaFile, node }) { + await derived.produce({ + mediaFile, + node, + cores: 1, + subkey: name, + async producer(dir) { + await stream.promises.pipeline( + fs.createReadStream(path.toString()), + fn(), + fs.createWriteStream(dir.join(name).toString()), + ); + }, }); }, }), diff --git a/src/file-viewer/cache.ts b/src/file-viewer/cache.ts index 9c1f5cf016aeb0c6fdf15523aa0034231adbbb51..c740539fa76b6e11019fe998631d4617b8361cd3 100644 --- a/src/file-viewer/cache.ts +++ b/src/file-viewer/cache.ts @@ -17,7 +17,7 @@ // objects given by node:fs/node:http streams -- which are re-allocated for // every read -- are weakly held when written and reused when reading. this // somewhat avoids file reads in concurrent access situations. -const log = console.scoped("cache"); +const console = log.scoped("cache"); export type Source = | "local" // local mode @@ -94,6 +94,7 @@ export interface Options { file: MediaFile; /** If set, fetches the derived asset instead. */ derived: string | null; + hash: string; start: number; /** exclusive. ex: [10, 30] skips ten bytes then grabs 20 */ end: number; @@ -106,13 +107,13 @@ export interface Options { * `null` if a derived asset does not exist */ export async function read( - { file, derived, start, end, noWrite, localOnly }: Options, + { file, derived, hash, start, end, noWrite, localOnly }: Options, ): Promise<{ stream: ReadableStream; src: Source; size: number } | null> { const rel = derived ? `${file.path}$/${derived}` : file.path; // Serve from local network share if it is mounted. const localFile = derived - ? derivedFileRoot?.join(file.hash, derived) + ? derivedFileRoot?.join(derived) : rawFileRoot?.join(file.path); if (localFile) { local: { @@ -128,7 +129,7 @@ export async function read( } const fileStream = await readStream(localFile, { start, end }); if (!fileStream) break local; - log(`from local: ${rel}`); + console.info(`from local: ${rel} ` + localFile); return { stream: stream.Readable.toWeb(fileStream), size, @@ -139,10 +140,6 @@ export async function read( if (!localOnly) return null; - const key = derived - ? crypto.createHash("sha1").update(file.hash + derived).digest("hex") - : file.hash; - // Disk cache const diskPath = diskCacheRoot.join(key); let diskFile = diskCache.get(diskPath); @@ -156,7 +153,7 @@ export async function read( }); if (diskStream) { const isPending = diskFile.written.value !== diskFile.size; - log(`from ${isPending ? "pending " : ""}disk cache: ${rel}`); + console.info(`from ${isPending ? "pending " : ""}disk cache: ${rel}`); return { stream: stream.Readable.toWeb(diskStream), src: isPending ? "pending" : "hit", @@ -173,10 +170,10 @@ export async function read( { let promise = parallelRequests.get(url); if (promise) { - log(`from other response: ${rel}`); + console.info(`from other response: ${rel}`); response = await promise; } else { - log(`cache miss: ${rel}`); + console.info(`cache miss: ${rel}`); promise = httpGet(url, { headers: token ? { Authorization: token } : {}, }); @@ -197,18 +194,18 @@ export async function read( // Do not cache large assets if (size > 1_000_000_000) { - log(`skipping disk cache due to large asset: ${rel}`); + console.info(`skipping disk cache due to large asset: ${rel}`); return { stream: stream.Readable.toWeb(response), size, src: "ignored" }; } // Bail out of the disk cache if it is contended if (diskCache.unlockedCapacity < size) { - log(`skipping disk cache due to contention: ${rel}`); + console.info(`skipping disk cache due to contention: ${rel}`); return { stream: stream.Readable.toWeb(response), size, src: "contended" }; } // IPs which request too many cache misses are temporarily // banned from writing into the disk cache to avoid abuse. if (noWrite) { - log(`skipping disk cache due to config: ${rel}`); + console.info(`skipping disk cache due to config: ${rel}`); return { stream: stream.Readable.toWeb(response), size, src: "ratelimit" }; } @@ -273,7 +270,7 @@ function openDiskCache() { const index = split.indexOf(".clover"); return index === -1 ? Path.createDirSync(clover, fileCache) - : Path.createDirSync(...split.slice(0, index + 1), fileCache); + : Path.createDirSync(split.slice(0, index + 1).join(Path.sep), fileCache); } function writeDiskCache() { @@ -284,7 +281,9 @@ function writeDiskCache() { }, }); diskCacheState.writeJsonFile(serialized) - .catch(() => {}); + .catch((err) => + console.error(`Failed to write ${diskCacheState.relative()}:`, err) + ); } async function getHandle(path: Path, flag: string, unlockOnFail?: Disposable) { @@ -404,7 +403,7 @@ async function readStream(path: Path, opts: ReadStreamOptions) { } let closed = false; - return fs.createReadStream(path.raw, { + return fs.createReadStream(path.toString(), { fd: handle.fd, start: opts?.start ?? 0, end: opts?.end ?? Infinity, @@ -433,7 +432,7 @@ async function writeStream( let closed = false; const { memory, written } = file; - const writer = fs.createWriteStream(path.raw, { + const writer = fs.createWriteStream(path.toString(), { start: 0, fd: handle.fd, fs: { @@ -480,7 +479,6 @@ async function writeStream( return writer; } -import * as console from "@paperclover/console"; import * as util from "node:util"; import * as fsCallbacks from "node:fs"; import * as fs from "#sitegen/fs"; @@ -491,6 +489,7 @@ import * as crypto from "node:crypto"; import * as http from "node:http"; import * as https from "node:https"; import * as paths from "./paths.ts"; +import * as log from "lib/log.ts"; import { MediaFile } from "./models/MediaFile.ts"; import type { ReadableStream } from "node:stream/web"; import { Lru } from "#sitegen/lru"; diff --git a/src/file-viewer/ffmpeg.ts b/src/file-viewer/ffmpeg.ts index fe639b72b4969cb0c827e3dc2f37cc07b4e44732..054b881cac21103bc7e6e146690f3b7320afa3f5 100644 --- a/src/file-viewer/ffmpeg.ts +++ b/src/file-viewer/ffmpeg.ts @@ -68,22 +68,26 @@ export async function spawn(options: SpawnOptions) { } } -// this is janky -export async function probeAudioStreams(path: Path) { +export async function probeStreams(path: Path) { const { stdout } = await subprocess.spawnAndWait({ cmd: [ "ffprobe", "-v", - "quiet", - "-select_streams", - "a", + "error", "-show_entries", - "stream=index", + "stream=codec_type", path.toString(), + "-of", + "default=noprint_wrappers=1:nokey=1", ], stdio: "pipe", }); - return stdout?.byteLength ?? 0 > 0; + + const lines = new TextDecoder().decode(stdout!).trim().split("\n"); + return { + audio: lines.includes("audio"), + video: lines.includes("video"), + }; } export class Parse { diff --git a/src/file-viewer/models/MediaFile.ts b/src/file-viewer/models/MediaFile.ts index 8acc06afe02bc728ad051586b7ce623eb5246645..e0c1b6aa07da58c77fb1005c408a59c4b1a0fa0a 100644 --- a/src/file-viewer/models/MediaFile.ts +++ b/src/file-viewer/models/MediaFile.ts @@ -117,7 +117,9 @@ export class MediaFile { .filter((file) => !file.basename.startsWith(".")); } getPublicChildren() { - const children = MediaFile.getChildren(this.id); + const children = MediaFile.getChildren(this.id).filter((x) => + !x.basename.startsWith("_unlisted") + ); if (FilePermissions.getByPrefix(this.path) == 0) { return children.filter(({ path }) => FilePermissions.getExact(path) == 0); } diff --git a/src/file-viewer/models/derived.ts b/src/file-viewer/models/derived.ts index 566fc76aafe40c9b235625ddd460adce749b7c73..6c3194a7722fb33239897a2352bb0c86113c3da3 100644 --- a/src/file-viewer/models/derived.ts +++ b/src/file-viewer/models/derived.ts @@ -21,6 +21,7 @@ db.table( file text not null, root integer not null, size integer not null, -- bytes + hash text not null, -- sha1 foreign key(root) references derived_roots(id) on delete cascade ); create table if not exists derived_refs ( @@ -44,15 +45,19 @@ let ongoing = new Map>(); /** produce a derived */ export async function produce( - file: MediaFile, - subkey: string, - producer: (root: Path) => Promise, + { mediaFile: file, node, subkey, cores, producer }: { + mediaFile: MediaFile; + node: progress.Node; + subkey: string; + cores: number; + producer: (root: Path) => Promise; + }, ) { const key = `${file.hash}/${subkey}`; const current = ongoing.get(key); let root: number | null = null; if (current) { - await current; + root = await current; } else {brk: { root = getRootQuery.get({ key })?.id ?? null; if (root) break brk; @@ -60,21 +65,57 @@ export async function produce( ongoing.set(key, promise); try { const tmp = workDir.join(key); - await tmp.makeOrEmptyDir(); - await producer(tmp); - const filesWithStats = await Promise.all( - (await tmp.readDir()) - .filter((x) => !x.base.startsWith("tmp.")) - .map(async (x) => [x, await x.stat()] as const), - ); + node.hidden = true; + const filesWithStats = await queue.run({ + cores, + async run() { + node.hidden = false; + await tmp.delete({ recursive: true, force: true }); + await tmp.makeOrEmptyDir(); + await producer(tmp); + const sha1queue = new queue.PriorityQueue(cores); + return await Promise.all( + (await tmp.readDir({ recursive: true })) + .filter((path) => !path.base.startsWith("tmp.")) + .map(async (path) => + sha1queue.run({ + async run() { + using _ = node.start("hash " + path.relative(tmp)); + const stats = await path.stat(); + if (stats.isDirectory()) { + return [path, stats.size, null] as const; + } + const hash = await new Promise( + (resolve, reject) => { + const reader = fs.createReadStream(path.toString()); + reader.on("error", reject); + + const hasher = crypto.createHash("sha1").setEncoding( + "hex", + ); + hasher.on("error", reject); + hasher.on("readable", () => resolve(hasher.read())); + + reader.pipe(hasher); + }, + ); + return [path, stats.size, hash] as const; + }, + }) + ), + ); + }, + }); db.node.exec("BEGIN"); try { root = insertRootQuery.getNonNull({ key, date: Date.now() }).id; - for (const [path, stats] of filesWithStats) { + for (const [path, size, hash] of filesWithStats) { + if (hash === null) continue; insertFileQuery.run({ root, file: path.relative(tmp), - size: stats.size, + size, + hash, }); } } catch (e) { @@ -91,7 +132,7 @@ export async function produce( ongoing.delete(key); } }} - ASSERT(root); + ASSERT(root != null); insertRefQuery.run({ root, file: file.id }); return { [Symbol.dispose]: () => { @@ -102,21 +143,23 @@ export async function produce( const getDerivedAssetQuery = db.prepare< [number, string], - { size: number; key: string } + { size: number; key: string; hash: string; date: number } >(/* SQL */ ` - select df.size, rt.key + select df.size, df.hash, rt.date, rt.key from derived_refs dr join derived_files df on df.root = dr.root join derived_roots rt on df.root = rt.id - where dr.file = ? and df.file like ? + where dr.file = ? and df.file = ? `); export function get(file: MediaFile, subPath: string) { const row = getDerivedAssetQuery.get(file.id, subPath) ?? null; if (row) { return { - path: Path.resolve(derivedFileRoot).join(row.key, subPath), + path: row.key + "/" + subPath, size: row.size, + hash: row.hash, + date: new Date(row.date), }; } return null; @@ -134,9 +177,9 @@ const getRootQuery = db.prepare<[{ key: string }], { id: number }>(/* SQL */ ` `); const insertFileQuery = db.prepare< - [{ file: string; root: number; size: number }] + [{ file: string; root: number; size: number; hash: string }] >(/* SQL */ ` - insert into derived_files (file, root, size) values ($file, $root, $size); + insert into derived_files (file, root, size, hash) values ($file, $root, $size, $hash); `); const insertRefQuery = db.prepare<[{ root: number; file: number }]>(/* SQL */ ` @@ -154,4 +197,8 @@ const deleteRefQuery = db.prepare<[{ root: number; file: number }]>(/* SQL */ ` import { getDb } from "#sitegen/sqlite"; import type { MediaFile } from "./MediaFile.ts"; import { Path } from "#sitegen/path"; +import * as fs from "node:fs"; +import * as crypto from "node:crypto"; +import * as queue from "lib/queue.ts"; +import * as progress from "lib/log/progress.ts"; import { derivedFileRoot } from "../paths.ts"; diff --git a/src/file-viewer/rsync.ts b/src/file-viewer/rsync.ts index ecfd491ea300c9c68a5614821b5ea7df82392957..a0ea9bf7ebd38e7e8e75ab5044bf967b61bf3006 100644 --- a/src/file-viewer/rsync.ts +++ b/src/file-viewer/rsync.ts @@ -23,34 +23,35 @@ export const defaultExtraOptions = [ export interface SpawnOptions { args: string[]; - title: string; rsync?: string; - progress?: Progress; + progress: progress.Node; cwd: string; } export async function spawn(options: SpawnOptions) { - const { rsync = "rsync", args, title, cwd } = options; + const { rsync = "rsync", args, cwd } = options; + using node = options.progress; const proc = child_process.spawn(rsync, [...defaultExtraOptions, ...args], { stdio: ["ignore", "pipe", "pipe"], cwd, }); const parser = new Parse(); - const bar = options.progress ?? new Progress({ text: title }); let running = true; const stdoutSplitter = readline.createInterface({ input: proc.stdout }); const stderrSplitter = readline.createInterface({ input: proc.stderr }); + const title = node.text; + const handleLine = (line: string) => { const result = parser.onLine(line); if (result.kind === "ignore") { return; } else if (result.kind === "log") { - console[result.level](result.message); + node.log[result.level](result.message); } else if (result.kind === "count") { if (!running) return; - bar.text = `${result.files} files...`; + node.text = `${title} (${result.files} files...)`; } else if (result.kind === "progress") { if (!running) return; const { @@ -61,8 +62,8 @@ export async function spawn(options: SpawnOptions) { currentFile, speed, } = result; - bar.value = transferNumber; - bar.total = totalFiles; + node.value = transferNumber; + node.estimate = totalFiles; const extras = [ formatSize(bytesTransferred), (totalFiles > filesToCheck) @@ -73,8 +74,8 @@ export async function spawn(options: SpawnOptions) { const fileName = currentFile.length > 20 ? `${currentFile.slice(0, 3)}..${currentFile.slice(-15)}` : currentFile; - bar.text = `[${transferNumber}/${totalFiles}] ${fileName} ${ - extras.length > 0 ? `(${extras})` : "" + node.text = `${title} - ${fileName}${ + extras.length > 0 ? `( ${extras})` : "" }`; } else result satisfies never; }; @@ -90,10 +91,8 @@ export async function spawn(options: SpawnOptions) { e.args = [rsync, ...args].join(" "); e.code = code; e.signal = signal; - bar.error(e.message); return e; } - bar.success(title); } export class Parse { @@ -184,5 +183,5 @@ export class Parse { import * as child_process from "node:child_process"; import * as readline from "node:readline"; import events from "node:events"; -import { Progress } from "@paperclover/console/Progress"; +import * as progress from "lib/log/progress.ts"; import { formatSize } from "@/file-viewer/format.ts"; diff --git a/src/file-viewer/transcode-rules.ts b/src/file-viewer/transcode-rules.ts index 292472b5d8959e9fc5f822c212c28dacfc17bfcc..f6108b6812d93f006cf9639b1eef58f615c4918b 100644 --- a/src/file-viewer/transcode-rules.ts +++ b/src/file-viewer/transcode-rules.ts @@ -116,24 +116,23 @@ export async function getVideoInputArgs(path: Path): Promise { ], audio: audioSrc ? ["-i", String(audioSrc)] - : (await ffmpeg.probeAudioStreams(path)) + : (await ffmpeg.probeStreams(path)).audio ? ["-i", path.toString()] : null, }; } } + const streams = await ffmpeg.probeStreams(path); return { both: ["-i", path.toString()], - video: ["-i", path.toString()], - audio: (await ffmpeg.probeAudioStreams(path)) - ? ["-i", path.toString()] - : null, + video: streams.video ? ["-i", path.toString()] : null, + audio: streams.audio ? ["-i", path.toString()] : null, }; } export interface InputArgs { - video: string[]; both: string[]; + video: string[] | null; audio: string[] | null; } @@ -181,6 +180,8 @@ export function getOpusAudioArgs( const { audioKbit } = opus; const cmd = [...audioInputArgs]; + cmd.push("-vn"); + cmd.push("-c:a", "libopus"); cmd.push("-b:a", audioKbit + "k"); @@ -191,27 +192,21 @@ export function getOpusAudioArgs( } export function getMpegDashArgs( - videos: Array, - audios: Array, + videos: Path[], + audios: Path[], outDir: Path, ) { const cmd = []; // inputs - videos.forEach((file) => cmd.push("-i", file.toString())); - audios.forEach((file) => cmd.push("-i", file.toString())); + videos.forEach((v) => cmd.push("-i", v.toString())); + audios.forEach((a) => cmd.push("-i", a.toString())); // map streams - videos.forEach((_, i) => { - cmd.push("-map", `${i}:0`); - }); - audios.forEach((_, i) => { - cmd.push("-map", `${videos.length + i}:0`); - }); + videos.forEach((_, i) => cmd.push("-map", `${i}:0`)); + audios.forEach((_, i) => cmd.push("-map", `${videos.length + i}:0`)); - // copy codecs cmd.push("-c", "copy"); - cmd.push("-f", "dash"); cmd.push("-seg_duration", "4"); cmd.push("-init_seg_name", "d/$RepresentationID$.init.$ext$"); @@ -219,11 +214,12 @@ export function getMpegDashArgs( cmd.push("-use_timeline", "1"); cmd.push("-use_template", "1"); - // build adaptation sets string - const videoAdaptationSets = videos.map((_, i) => `id=${i},streams=${i}`) + // calculate actual dimensions per video + const videoAdaptationSets = videos + .map((_, i) => `id=${i},streams=${i}`) .join(" "); + let adaptationSets = videoAdaptationSets; - if (audios.length > 0) { const audioId = videos.length; const audioIndices = audios.map((_, i) => videos.length + i).join(","); @@ -231,7 +227,6 @@ export function getMpegDashArgs( } cmd.push("-adaptation_sets", adaptationSets); - cmd.push("-y"); cmd.push(outDir.join("dash.mpd").toString()); @@ -257,53 +252,65 @@ export function getH264HlsArgs( ) { const cmd = [...inputArgs.both]; - // map streams - hlsPresets.forEach(() => { - cmd.push("-map", "0:v:0"); - if (inputArgs.audio) { - cmd.push("-map", "0:a:0"); - } - }); + if (inputArgs.audio) { + // with audio: create v+a pairs for each preset + hlsPresets.forEach(() => { + cmd.push("-map", "0:v:0", "-map", "0:a:0"); + }); - // encode each variant - hlsPresets.forEach((preset, i) => { - if (inputArgs.audio) { + hlsPresets.forEach((preset, i) => { const vIdx = i * 2; const aIdx = i * 2 + 1; - cmd.push(`-c:v:${vIdx}`, "libx264"); - cmd.push(`-b:v:${vIdx}`, preset.videoBitrate); - cmd.push(`-c:a:${aIdx}`, "aac"); - cmd.push(`-b:a:${aIdx}`, preset.audioBitrate); - cmd.push(`-filter:v:${vIdx}`, `scale=-2:min(${preset.maxHeight}\\,ih)`); - } else { - cmd.push(`-c:v:${i}`, "libx264"); - cmd.push(`-b:v:${i}`, preset.videoBitrate); - cmd.push(`-filter:v:${i}`, `scale=-2:min(${preset.maxHeight}\\,ih)`); - } - }); + // deno-fmt-ignore + cmd.push( + `-c:${vIdx}`, "libx264", + `-b:v:${vIdx}`, preset.videoBitrate, + `-filter:${vIdx}`, `scale=-2:min(${preset.maxHeight}\\,ih)`, + `-c:${aIdx}`, "aac", + `-b:a:${aIdx}`, preset.audioBitrate, + ); + }); - cmd.push("-f", "hls"); - cmd.push("-hls_time", "2"); - cmd.push("-hls_list_size", "0"); - cmd.push("-hls_segment_type", "fmp4"); - cmd.push("-hls_allow_cache", "1"); - - if (inputArgs.audio) { cmd.push( "-var_stream_map", - hlsPresets.map((_, i) => `v:${i * 2},a:${i * 2 + 1}`).join(" "), + hlsPresets.map((_, i) => `v:${i},a:${i}`).join(" "), ); } else { - cmd.push("-var_stream_map", hlsPresets.map((_, i) => `v:${i}`).join(" ")); + // video only: single stream per preset + hlsPresets.forEach(() => { + cmd.push("-map", "0:v:0"); + }); + + hlsPresets.forEach((preset, i) => { + // deno-fmt-ignore + cmd.push( + `-c:v:${i}`, "libx264", + `-b:v:${i}`, preset.videoBitrate, + `-filter:v:${i}`, `scale=-2:min(${preset.maxHeight}\\,ih)`, + ); + }); + + cmd.push( + "-var_stream_map", + hlsPresets.map((_, i) => `v:${i}`).join(" "), + ); } - cmd.push("-master_pl_name", "master.m3u8"); - cmd.push("-hls_segment_filename", "hls.%v.%03d.ts"); - - cmd.push("-y"); - cmd.push(outDir.join("hls.%v.m3u8").toString()); + // deno-fmt-ignore + cmd.push( + "-f", "hls", + "-hls_time", "2", + "-hls_list_size", "0", + "-hls_segment_type", "fmp4", + "-hls_allow_cache", "1", + "-master_pl_name", "master.m3u8", + "-hls_segment_filename", "hls.%v.%03d.ts", + "-y", + outDir.join("hls.%v.m3u8").toString(), + ); return cmd; } + import type { Path } from "#sitegen/path"; import * as ffmpeg from "./ffmpeg.ts"; diff --git a/src/file-viewer/views/clofi.tsx b/src/file-viewer/views/clofi.tsx index fb591e71b7be33e645a76e1d73e14ea60b7acc91..cffef34d7b9a13a944c7780969c9b32ae2ed16e8 100644 --- a/src/file-viewer/views/clofi.tsx +++ b/src/file-viewer/views/clofi.tsx @@ -71,7 +71,6 @@ export default function MediaList({ hasCotyledonCookie: boolean; }) { addScript("./clofi.client.ts"); - addScript("@/tags/hls-polyfill.client.ts"); const dirs: MediaFile[] = []; let dir: MediaFile | null = file; @@ -356,10 +355,8 @@ function DirView({ /> ); } - const isCotyledon = parseInt(dir.path.split("/")?.[1]) < 2025; - const unsortedFiles = dir - .getPublicChildren() - .filter((f) => !f.basenameWithoutExt.startsWith("_unlisted")); + const isCotyledon = parseInt(UNWRAP(dir.path.split("/")[1])) < 2025; + const unsortedFiles = dir.getPublicChildren(); const sorter = dir.dirsort ? sorterFromDirSort(dir.dirsort) : isNumericallyOrdered(unsortedFiles) @@ -639,31 +636,19 @@ function TextView({ siblingFiles?: MediaFile[]; }) { const contents = file.contents; - if (file.path.startsWith("/2021/phoenix-write/maps")) { const basename = file.basename; if (basename.includes("map") && basename.endsWith(".txt")) { return ( -

+        
{render.raw(highlightHashComments(contents))}
); } } return ( -
 f.kind === MediaFileKind.file),
-        ),
-      }}
-    >
+
+      {render.raw(highlightLinksInTextView( contents, siblingFiles.filter((f) => f.kind === MediaFileKind.file)))}
+    
); } @@ -673,10 +658,8 @@ function ChatView({ file }: { file: MediaFile }) {
+ {render.raw(highlightConvo(contents))}
); } @@ -690,12 +673,7 @@ function CodeView({ file }: { file: MediaFile }) { return ; } return ( -
+    
{render.raw(highlightedContents)}
); } @@ -957,3 +935,4 @@ import { import { ForEveryone } from "@/file-viewer/cotyledon.tsx"; import * as mime from "#sitegen/mime"; import * as sort from "../sort.ts"; +import * as render from "#engine/render"; diff --git a/src/pages/in-the-summer.css b/src/pages/in-the-summer.css new file mode 100644 index 0000000000000000000000000000000000000000..f85608feb2c03d9b9586d064f196189c85b17a02 --- /dev/null +++ b/src/pages/in-the-summer.css @@ -0,0 +1,11 @@ +body { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9IiM3NEM3ODQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik00My43NSAyMDcuODEyQzQzLjc1IDE5OC4zMiAzNi4wNTQ5IDE5MC42MjUgMjYuNTYyNSAxOTAuNjI1QzE3LjA3MDEgMTkwLjYyNSA5LjM3NSAxOTguMzIgOS4zNzUgMjA3LjgxMkM5LjM3NSAyMTcuMzA1IDE3LjA3MDEgMjI1IDI2LjU2MjUgMjI1VjIyOC4xMjVDMTUuMzQ0MiAyMjguMTI1IDYuMjUgMjE5LjAzMSA2LjI1IDIwNy44MTJDNi4yNSAxOTYuNTk0IDE1LjM0NDIgMTg3LjUgMjYuNTYyNSAxODcuNUMzNy43ODA4IDE4Ny41IDQ2Ljg3NSAxOTYuNTk0IDQ2Ljg3NSAyMDcuODEyQzQ2Ljg3NSAyMTkuMDMxIDM3Ljc4MDggMjI4LjEyNSAyNi41NjI1IDIyOC4xMjVWMjI1QzM2LjA1NDkgMjI1IDQzLjc1IDIxNy4zMDUgNDMuNzUgMjA3LjgxMlpNMTQzLjc1IDE4NS45MzhDMTQzLjc1IDE3Ni40NDUgMTM2LjA1NSAxNjguNzUgMTI2LjU2MiAxNjguNzVDMTE3LjA3IDE2OC43NSAxMDkuMzc1IDE3Ni40NDUgMTA5LjM3NSAxODUuOTM4QzEwOS4zNzUgMTk1LjQzIDExNy4wNyAyMDMuMTI1IDEyNi41NjIgMjAzLjEyNVYyMDYuMjVDMTE1LjM0NCAyMDYuMjUgMTA2LjI1IDE5Ny4xNTYgMTA2LjI1IDE4NS45MzhDMTA2LjI1IDE3NC43MTkgMTE1LjM0NCAxNjUuNjI1IDEyNi41NjIgMTY1LjYyNUMxMzcuNzgxIDE2NS42MjUgMTQ2Ljg3NSAxNzQuNzE5IDE0Ni44NzUgMTg1LjkzOEMxNDYuODc1IDE5Ny4xNTYgMTM3Ljc4MSAyMDYuMjUgMTI2LjU2MiAyMDYuMjVWMjAzLjEyNUMxMzYuMDU1IDIwMy4xMjUgMTQzLjc1IDE5NS40MyAxNDMuNzUgMTg1LjkzOFpNMTI4LjEyNSAxMDQuNjg4QzEyOC4xMjUgOTUuMTk1MSAxMjAuNDMgODcuNSAxMTAuOTM4IDg3LjVDMTAxLjQ0NSA4Ny41IDkzLjc1IDk1LjE5NTEgOTMuNzUgMTA0LjY4OEM5My43NSAxMTQuMTggMTAxLjQ0NSAxMjEuODc1IDExMC45MzggMTIxLjg3NVYxMjVDOTkuNzE5MiAxMjUgOTAuNjI1IDExNS45MDYgOTAuNjI1IDEwNC42ODhDOTAuNjI1IDkzLjQ2OTIgOTkuNzE5MiA4NC4zNzUgMTEwLjkzOCA4NC4zNzVDMTIyLjE1NiA4NC4zNzUgMTMxLjI1IDkzLjQ2OTIgMTMxLjI1IDEwNC42ODhDMTMxLjI1IDExNS45MDYgMTIyLjE1NiAxMjUgMTEwLjkzOCAxMjVWMTIxLjg3NUMxMjAuNDMgMTIxLjg3NSAxMjguMTI1IDExNC4xOCAxMjguMTI1IDEwNC42ODhaTTU2LjI1IDg5LjA2MjVDNTYuMjUgNzkuNTcwMSA0OC41NTQ5IDcxLjg3NSAzOS4wNjI1IDcxLjg3NUMyOS41NzAxIDcxLjg3NSAyMS44NzUgNzkuNTcwMSAyMS44NzUgODkuMDYyNUMyMS44NzUgOTguNTU0OSAyOS41NzAxIDEwNi4yNSAzOS4wNjI1IDEwNi4yNVYxMDkuMzc1QzI3Ljg0NDIgMTA5LjM3NSAxOC43NSAxMDAuMjgxIDE4Ljc1IDg5LjA2MjVDMTguNzUgNzcuODQ0MiAyNy44NDQyIDY4Ljc1IDM5LjA2MjUgNjguNzVDNTAuMjgwOCA2OC43NSA1OS4zNzUgNzcuODQ0MiA1OS4zNzUgODkuMDYyNUM1OS4zNzUgMTAwLjI4MSA1MC4yODA4IDEwOS4zNzUgMzkuMDYyNSAxMDkuMzc1VjEwNi4yNUM0OC41NTQ5IDEwNi4yNSA1Ni4yNSA5OC41NTQ5IDU2LjI1IDg5LjA2MjVaTTE5MC42MjUgMjMuNDM3NUMxOTAuNjI1IDEzLjk0NTEgMTgyLjkzIDYuMjUgMTczLjQzOCA2LjI1QzE2My45NDUgNi4yNSAxNTYuMjUgMTMuOTQ1MSAxNTYuMjUgMjMuNDM3NUMxNTYuMjUgMzIuOTI5OSAxNjMuOTQ1IDQwLjYyNSAxNzMuNDM4IDQwLjYyNVY0My43NUMxNjIuMjE5IDQzLjc1IDE1My4xMjUgMzQuNjU1OCAxNTMuMTI1IDIzLjQzNzVDMTUzLjEyNSAxMi4yMTkyIDE2Mi4yMTkgMy4xMjUgMTczLjQzOCAzLjEyNUMxODQuNjU2IDMuMTI1IDE5My43NSAxMi4yMTkyIDE5My43NSAyMy40Mzc1QzE5My43NSAzNC42NTU4IDE4NC42NTYgNDMuNzUgMTczLjQzOCA0My43NVY0MC42MjVDMTgyLjkzIDQwLjYyNSAxOTAuNjI1IDMyLjkyOTkgMTkwLjYyNSAyMy40Mzc1Wk00My43NSA3LjgxMjVDNDMuNzUgLTEuNjc5ODkgMzYuMDU0OSAtOS4zNzUgMjYuNTYyNSAtOS4zNzVDMTcuMDcwMSAtOS4zNzUgOS4zNzUgLTEuNjc5ODkgOS4zNzUgNy44MTI1QzkuMzc1IDE3LjMwNDkgMTcuMDcwMSAyNSAyNi41NjI1IDI1VjI4LjEyNUMxNS4zNDQyIDI4LjEyNSA2LjI1IDE5LjAzMDggNi4yNSA3LjgxMjVDNi4yNSAtMy40MDU3OCAxNS4zNDQyIC0xMi41IDI2LjU2MjUgLTEyLjVDMzcuNzgwOCAtMTIuNSA0Ni44NzUgLTMuNDA1NzggNDYuODc1IDcuODEyNUM0Ni44NzUgMTkuMDMwOCAzNy43ODA4IDI4LjEyNSAyNi41NjI1IDI4LjEyNVYyNUMzNi4wNTQ5IDI1IDQzLjc1IDE3LjMwNDkgNDMuNzUgNy44MTI1Wk0xNDMuNzUgLTE0LjA2MjVDMTQzLjc1IC0yMy41NTQ5IDEzNi4wNTUgLTMxLjI1IDEyNi41NjIgLTMxLjI1QzExNy4wNyAtMzEuMjUgMTA5LjM3NSAtMjMuNTU0OSAxMDkuMzc1IC0xNC4wNjI1QzEwOS4zNzUgLTQuNTcwMTEgMTE3LjA3IDMuMTI1IDEyNi41NjIgMy4xMjVWNi4yNUMxMTUuMzQ0IDYuMjUgMTA2LjI1IC0yLjg0NDIyIDEwNi4yNSAtMTQuMDYyNUMxMDYuMjUgLTI1LjI4MDggMTE1LjM0NCAtMzQuMzc1IDEyNi41NjIgLTM0LjM3NUMxMzcuNzgxIC0zNC4zNzUgMTQ2Ljg3NSAtMjUuMjgwOCAxNDYuODc1IC0xNC4wNjI1QzE0Ni44NzUgLTIuODQ0MjIgMTM3Ljc4MSA2LjI1IDEyNi41NjIgNi4yNVYzLjEyNUMxMzYuMDU1IDMuMTI1IDE0My43NSAtNC41NzAxMSAxNDMuNzUgLTE0LjA2MjVaIi8+CjxwYXRoIGQ9Ik0xMzMuOTE3IDI5LjIyOTdMNzQuNTQyMiA4OC42MDQ3TDcyLjMzMjggODYuMzk1M0wxMzEuNzA4IDI3LjAyMDNMMTMzLjkxNyAyOS4yMjk3WiIvPgo8cGF0aCBkPSJNNDEuNzI5NyAxMzguNjA1TC0xNy42NDUzIDE5Ny45OEwtMTkuODU0NyAxOTUuNzdMMzkuNTIwMyAxMzYuMzk1TDQxLjcyOTcgMTM4LjYwNVoiLz4KPHBhdGggZD0iTTE1MS4xMDUgMTEwLjQ4TDkxLjcyOTcgMTY5Ljg1NUw4OS41MjAzIDE2Ny42NDVMMTQ4Ljg5NSAxMDguMjdMMTUxLjEwNSAxMTAuNDhaIi8+CjxwYXRoIGQ9Ik0yMTAuNDggMTEwLjQ4TDE1MS4xMDUgMTY5Ljg1NUwxNDguODk1IDE2Ny42NDVMMjA4LjI3IDEwOC4yN0wyMTAuNDggMTEwLjQ4WiIvPgo8cGF0aCBkPSJNMTAuNDc5NyAxMTAuNDhMLTQ4Ljg5NTMgMTY5Ljg1NUwtNTEuMTA0NyAxNjcuNjQ1TDguMjcwMjYgMTA4LjI3TDEwLjQ3OTcgMTEwLjQ4WiIvPgo8cGF0aCBkPSJNOTcuOTc5NyAtMTEuMzk1M0wzOC42MDQ3IDQ3Ljk3OTdMMzYuMzk1MyA0NS43NzAzTDk1Ljc3MDMgLTEzLjYwNDdMOTcuOTc5NyAtMTEuMzk1M1oiLz4KPHBhdGggZD0iTTk3Ljk3OTcgMTg4LjYwNUwzOC42MDQ3IDI0Ny45OEwzNi4zOTUzIDI0NS43N0w5NS43NzAzIDE4Ni4zOTVMOTcuOTc5NyAxODguNjA1WiIvPgo8cGF0aCBkPSJNMTMuNjA0NyAzOC42MDQ3TC00NS43NzAzIDk3Ljk3OTdMLTQ3Ljk3OTcgOTUuNzcwM0wxMS4zOTUzIDM2LjM5NTNMMTMuNjA0NyAzOC42MDQ3WiIvPgo8cGF0aCBkPSJNMjQxLjczIDEzOC42MDVMMTgyLjM1NSAxOTcuOThMMTgwLjE0NSAxOTUuNzdMMjM5LjUyIDEzNi4zOTVMMjQxLjczIDEzOC42MDVaIi8+CjxwYXRoIGQ9Ik0yMTMuNjA1IDM4LjYwNDdMMTU0LjIzIDk3Ljk3OTdMMTUyLjAyIDk1Ljc3MDNMMjExLjM5NSAzNi4zOTUzTDIxMy42MDUgMzguNjA0N1oiLz4KPHBhdGggZD0iTTgxLjY5NzQgMTE0LjM0NUw1MS41NDg2IDEyMS40MDdMNzIuMTc0IDE0NC4xMDhMODEuNjk3NCAxMTQuMzQ1WiIvPgo8cGF0aCBkPSJNNjMuOTUyOCAxNTYuNzczTDQyLjEwNjggMTc4LjcxOEw3MS42MjAyIDE4Ny4wNjhMNjMuOTUyOCAxNTYuNzczWiIvPgo8cGF0aCBkPSJNODAuOTQyIDE1LjYyNUw4Ni44MzMzIDQ2LjAyNDNMMTEwLjMxNSAyNi4yOTIxTDgwLjk0MiAxNS42MjVaIi8+CjxwYXRoIGQ9Ik0xMjUuNDE0IDU5LjYzOTVMMTQ4LjM0OSA4MC40NDM1TDE1NS4zMiA1MC41NzQ1TDEyNS40MTQgNTkuNjM5NVoiLz4KPHBhdGggZD0iTS0yMC43MzI4IDEwMi42Nkw5LjU3NTM0IDk2LjMxNjRMLTEwLjUwNDkgNzMuMTMxNUwtMjAuNzMyOCAxMDIuNjZaIi8+CjxwYXRoIGQ9Ik0xNzkuMjY4IDEwMi42NkwyMDkuNTc2IDk2LjMxNjRMMTg5LjQ5NiA3My4xMzE1TDE3OS4yNjggMTAyLjY2WiIvPgo8cGF0aCBkPSJNMTcyLjE1IDE5OC40MDJMMTg4Ljc4OCAxNzIuMjg3TDE1OC4xNzEgMTcwLjQ1M0wxNzIuMTUgMTk4LjQwMloiLz4KPC9zdmc+); +} +h1 { + margin-bottom: 0; + margin-top: 1rem; +} +.contain { + max-width: 80ch; + margin: auto; +} diff --git a/src/pages/in-the-summer.marko b/src/pages/in-the-summer.marko new file mode 100644 index 0000000000000000000000000000000000000000..f0d4b450d87d85339b8204dc5b4c0f8c4c511c2a --- /dev/null +++ b/src/pages/in-the-summer.marko @@ -0,0 +1,40 @@ +export const meta = { title: 'in the summer' }; +export const theme = { + bg: '#7DCC8C', + fg: '#000', + primary: '#9b3706', +} + +import "./in-the-summer.css"; + +
+ + +
+

in the summer

+ +
+ +

+ this is a song about the summer, and related feelings. written in 2022, + produced in 2025. i had a lot of fun playing with a much larger instrument + selection, and new 3d workflows. enjoy! +

+ + + + <@header>music video: in the summer + + + +

downloads

+ +

mentions on the q&a

+ // + +
+ diff --git a/src/pages/waterfalls.marko b/src/pages/waterfalls.marko new file mode 100644 index 0000000000000000000000000000000000000000..b1f226670b55e9918d679db8f4237355a0012536 --- /dev/null +++ b/src/pages/waterfalls.marko @@ -0,0 +1,47 @@ +import "./waterfalls.css"; + +export const meta = { title: "waterfalls by paper clover" }; +export const theme = { + fg: "#fff", + bg: "#000", +}; + +
+
+
+
+

waterfalls and gender identity update

+

+ this is a song about identity, jealousness, and the seasons. more + specifically, it's a personal account of how it felt realizing i was a + transgender woman. enjoy. +

+
+ + <@header>music video: waterfalls + +
+

+ it's nice to be free. +

+

like paper airplanes in the wind.

+

the stars shine in the sky. it's beautiful.

+

+ love +
+ –clover caruso +

+
+ +

+ more:
+ fragments +
+ q&a page +

+
+
+
diff --git a/src/pages/waterfalls.tsx b/src/pages/waterfalls.tsx deleted file mode 100644 index e3f3aa87b3bd2cc22a2994a252c686e70fa39eb8..0000000000000000000000000000000000000000 --- a/src/pages/waterfalls.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Video } from "@/tags/Video.tsx"; - -export const meta = { title: "waterfalls by paper clover" }; - -export default () => ( -
-
-
-
-

waterfalls and gender identity update

-

- this is a song about identity, jealousness, and the seasons. more - specifically, it's a personal account of how it felt realizing i was a - transgender woman. enjoy. -

-
-
-
-); - -export const theme = { - fg: "#fff", - bg: "#000", -}; diff --git a/src/tags/Video.tsx b/src/tags/Video.tsx deleted file mode 100644 index 9f57e9a98cbc9a79f3e6feb885f1fd21112a0b35..0000000000000000000000000000000000000000 --- a/src/tags/Video.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import "./video.css"; -import * as path from "node:path"; -import { addScript } from "#sitegen"; -import { PrecomputedBlurhash } from "./blurhash.tsx"; - -export namespace Video { - export interface Props { - title: string; - width: number; - height: number; - sources: string[]; - downloads: string[]; - poster?: string; - posterHash?: string; - borderless?: boolean; - } -} - -export function Video( - { title, sources, height, poster, posterHash, width, borderless }: - Video.Props, -) { - addScript("./hls-polyfill.client.ts"); - return ( -
-
{title}
- {posterHash && } - {poster && waterfalls} - -
- ); -} -export function contentTypeFromExt(src: string) { - if (src.endsWith(".m3u8")) return "application/x-mpegURL"; - if (src.endsWith(".webm")) return "video/webm"; - if (src.endsWith(".mp4")) return "video/mp4"; - if (src.endsWith(".ogg")) return "video/ogg"; - throw new Error("Unknown video extension: " + path.extname(src)); -} -const gcd = (a: number, b: number): number => b ? gcd(b, a % b) : a; -function simplifyFraction(n: number, d: number) { - const divisor = gcd(n, d); - return `${n / divisor}/${d / divisor}`; -} diff --git a/src/tags/blurhash.tsx b/src/tags/blurhash.tsx deleted file mode 100644 index f0eb840e0569b5cbe84caa127611025f628b9eb0..0000000000000000000000000000000000000000 --- a/src/tags/blurhash.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as blurhash from "blurhash"; -import sharp from "sharp"; -function decode83(str: string) { - return [...str].reduce( - (value, char) => - value * 83 + - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" // - .indexOf(char), - 0, - ); -} -function getSizeFromHash(hash: string) { - const sizeFlag = decode83(hash[0]); - const height = ((sizeFlag / 9) | 0) + 1; - const width = (sizeFlag % 9) + 1; - return { width, height }; -} -export async function encodeBlurhashPng(hash: string) { - const size = getSizeFromHash(hash); - const data = blurhash.decode(hash, size.width, size.height); - return (await sharp(data, { - raw: { width: size.width, height: size.height, channels: 4 }, - }).png({ colors: size.width * size.height }).toBuffer()); -} -export async function PrecomputedBlurhash({ hash }: { hash: string }) { - const data = await encodeBlurhashPng(hash); - return ( - - ); -} diff --git a/src/tags/clover-video.client.ts b/src/tags/clover-video.client.ts new file mode 100644 index 0000000000000000000000000000000000000000..dab6c8c48f5b371feb1b32809cbced7866563e4d --- /dev/null +++ b/src/tags/clover-video.client.ts @@ -0,0 +1,155 @@ +// Theoretical minimum browser support: Safari v6, Chrome v23, Firefox v42 + +// Polyfill for old browsers. +type PlaybackMode = "av1" | "hls-polyfill" | "hls-native" | "none"; +let hls: Promise | null = null; + +Promise.allSettled ??= (promises: Iterable>) => + Promise.all(Array.from(promises, (p) => + Promise.resolve(p) + .then( + (value) => ({ status: "fulfilled" as const, value }), + (reason) => ({ status: "rejected" as const, reason }), + ))); + +decidePlaybackMode().then((mode) => + Array.from( + document.querySelectorAll(".clover-video"), + (container) => hydrateVideo(container, mode), + ) +); + +function decidePlaybackMode(): Promise { + return shouldPlayAv1().catch(() => ({ hardware: false, supported: false })) + .then((av1): Awaitable => { + // Native AV1 with DASH Polyfill + // (Chrome v70, Safari v17 on M3/A17 chip, Firefox v67) + if (av1.hardware) return "av1"; + if (av1.supported) { + // Software decoding is too slow to use, unfortunately. + // Consider a device like a 2014 two core laptop. + console.warn(`implements non-accelerated av1+opus`); + } else { + console.warn(`does not implement av1+opus`); + } + + // Native HLS support (Safari v6) + const video = document.querySelector("video")!; + if (video.canPlayType("application/x-mpegURL")) { + return "hls-native"; + } + + return (hls ??= import("hls.js")) + .then((hls): PlaybackMode => { + // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8) + // TODO: ES modules have a minimum of Chrome 63 or Firefox 60 + if (hls.default.isSupported()) return "hls-polyfill"; + // Other browsers + return "none"; + }); + }) + .catch((): PlaybackMode => "none") + .then((mode) => { + console.info(`selected playback mode: ${mode}`); + return mode; + }); +} + +function hydrateVideo(container: HTMLElement, mode: PlaybackMode) { + const video = container.querySelector("video")!; + let src = video.getAttribute("data-src"); + if (!src) return; + src = `/file${src}`; + + const dashFile = `${src}:/dash.mpd`; + const hlsFile = `${src}:/master.m3u8`; + + if (mode === "av1") { + console.info(`Playback: av1, opus, mpeg-dash`); + import("dashjs").then((dashjs) => { + const player = dashjs.MediaPlayer().create(); + // This proxying is for ManagedMediaSource, exposing the HLS stream for AirPlay + let mainSource: HTMLSourceElement | null = null; + Object.defineProperty(video, "src", { + get: () => false, + set: (v) => { + if (!mainSource) { + mainSource = document.createElement("source"); + mainSource.src = v; + video.appendChild(mainSource); + } else { + mainSource.src = v; + } + }, + }); + Object.defineProperty(video, "disableRemotePlayback", { + get: () => false, + set: () => { + const source = document.createElement("source"); + source.src = hlsFile; + video.appendChild(source); + }, + }); + player.initialize( + video, + dashFile, + false, // autoplay + 0, // start + ); + video.controls = true; + }); + } else if (mode === "hls-native") { + const hlsSource = document.createElement("source"); + hlsSource.src = `${src}:/hls.m3u8`; + video.appendChild(hlsSource); + video.controls = true; + } else if (mode === "hls-polyfill") { + hls!.then((hls) => { + const player = new hls.default(); + player.loadSource(hlsFile); + player.attachMedia(video); + video.controls = true; + }); + } else { + video.src = src; + video.controls = true; + } +} + +declare global { + var ManagedMediaSource: typeof MediaSource | null; +} + +function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> { + const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource; + if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) { + return Promise.resolve({ + hardware: false, + supported: false, + }); + } + + if (navigator.vendor?.includes("Apple")) { + const supported = !!MediaSource?.isTypeSupported( + 'video/mp4;codecs="av01.0.01M.08"', + ); + return Promise.resolve({ + hardware: supported, + supported, + }); + } else { + return navigator.mediaCapabilities.decodingInfo({ + type: "media-source", + video: { + contentType: 'video/mp4;codecs="av01.0.01M.08"', + bitrate: 128_000, + width: 1920, + height: 1080, + framerate: 30, + }, + }).then((info) => ({ + hardware: !!info.powerEfficient, + supported: !!info.supported, + })); + } +} diff --git a/src/tags/clover-video.css b/src/tags/clover-video.css new file mode 100644 index 0000000000000000000000000000000000000000..a0867a391a0d5d5f47b3577d6df93a675c98cc34 --- /dev/null +++ b/src/tags/clover-video.css @@ -0,0 +1,40 @@ +.clover-video { + border: 4px solid var(--fg); + display: flex; + flex-direction: column; +} +.clover-video video { + background-color: transparent; +} +.clover-video .stack { + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; +} +.clover-video video { + width: 100%; +} +.clover-video h2 { + margin-top: 0; +} +.clover-video .fallback { + background-color: #222b; + color: white; + padding: 2rem; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; +} +.clover-video video:has(~ .fallback > *) { + filter: blur(20px); +} +.clover-video figcaption { + background-color: var(--fg); + color: white; + width: 100%; + margin-top: -1px; + padding-bottom: 2px; +} diff --git a/src/tags/clover-video.marko b/src/tags/clover-video.marko new file mode 100644 index 0000000000000000000000000000000000000000..1f18035982711a5b8874f8e86e30370b8611b877 --- /dev/null +++ b/src/tags/clover-video.marko @@ -0,0 +1,57 @@ +import "./clover-video.css"; + +export interface Input { + file: MediaFile | string; + poster?: MediaFile | string; + header?: Marko.AttrTag<{ content: Marko.Body }>; +} + + + + + + + + +
+ <${header.content}/> + ${file.basename} + +
+ +