diff --git a/framework/generate.ts b/framework/generate.ts index d473e55b2df43164ec5341cef5f27c141c93a1e4..5d209a5bdc833674665b288334078a84caf81650 100644 --- a/framework/generate.ts +++ b/framework/generate.ts @@ -327,12 +327,12 @@ export async function preparePage( ); // -- metadata -- - const renderedMeta = meta.toHtml({ + const renderedMeta = meta.toHtml(metaTemplate, { canonical: item.id, ...typeof metadata === "function" ? await metadata({ ssr: true }) : metadata, - }, metaTemplate); + }); if (!renderedMeta.includes("")) { throw new Error( "Page is missing 'meta.title'. " + "All pages need a title tag.", diff --git a/framework/lib/view.ts b/framework/lib/view.ts index c8ad37589db256ce302156b8685215fcb33d5bcf..54c8f7c6254c6f96688d9f7897d829fe8f4c6aa9 100644 --- a/framework/lib/view.ts +++ b/framework/lib/view.ts @@ -62,7 +62,7 @@ export async function renderToString<K extends Key>( // -- metadata -- const renderedMetaPromise = Promise.resolve( typeof metadata === "function" ? metadata(attrs) : metadata, - ).then((m) => meta.toHtml(m, codegen.metaTemplate)); + ).then((m) => meta.toHtml(codegen.metaTemplate, m)); const cloverScripts = new Set<string>(); const text = await SitegenWrap.render({ diff --git a/lib/arrays.ts b/lib/arrays.ts index d5d0220baea98e01750ff6c77f5e105d3b7c7a49..f1c9540c28b8d3d1c56baddb957d97de291e63e7 100644 --- a/lib/arrays.ts +++ b/lib/arrays.ts @@ -6,7 +6,7 @@ export function* enumerate<T extends [unknown, ...unknown[]]>( ...arrays: { [K in keyof T]: ArrayLike<T[K]> } ): Generator<[...items: T, index: number]> { const { length } = arrays[0]; - arrays.forEach((x) => assert(x.length === length), "All arrays must be equal in length"); + arrays.forEach((x) => ASSERT(x.length === length), "All arrays must be equal in length"); for (let i = 0; i < length; i += 1) yield arrays.map((x) => x[i]!).concat(i) as [...T, number]; } @@ -15,6 +15,8 @@ export function mapMany<T extends [unknown, ...unknown[]], O>( map: (...args: [...items: T, index: number]) => O, ): O[] { const { length } = arrays[0]; - arrays.forEach((x) => assert(x.length === length), "All arrays must be equal in length"); + arrays.forEach((x) => ASSERT(x.length === length), "All arrays must be equal in length"); return Array.from({ length }, (_, i) => map(...(arrays.map((a) => a[i]!).concat(i) as [...T, number]))); } + +import { ASSERT } from "./assert.ts"; diff --git a/lib/deno.json b/lib/deno.json index 9095319340e35ab28b1a627f7dd5328e713ecdfa..4279c56053e6fc0b149e21827b0d27d0282382ba 100644 --- a/lib/deno.json +++ b/lib/deno.json @@ -1,23 +1,25 @@ { "name": "@clo/lib", "description": "high quality typescript libraries", - "version": "4.0.0", + "version": "5.0.0", "license": "MIT", "exports": { + "./arrays": "./arrays.ts", "./assert": "./assert.ts", "./async": "./async.ts", "./bytes": "./bytes.ts", "./error": "./error.ts", "./Events": "./Events.ts", + "./http": "./http.ts", "./log": "./log.ts", "./log/stack": "./log/stack.ts", "./Lru": "./Lru.ts", + "./math": "./math.ts", "./meta": "./meta.ts", "./mime": "./mime.ts", "./node": "./node.ts", "./progress": "./progress.ts", "./queue": "./queue.ts", - "./render/meta": "./render/meta.ts", "./stream": "./stream.ts", "./string": "./string.ts", "./string/ansi": "./string/ansi.ts", diff --git a/lib/math.ts b/lib/math.ts index 11509965e9433eb10c9da0c4546d89567be9f852..20a6d7868cd22473e3962f5fb124e3b272037aeb 100644 --- a/lib/math.ts +++ b/lib/math.ts @@ -1,8 +1,8 @@ -export function gcd(a: number, b: number) { +export function gcd(a: number, b: number): number { while (b !== 0) [a, b] = [b, a % b]; return a; } -export function clamp(value: number, min: number, max: number) { +export function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } diff --git a/lib/meta.test.ts b/lib/meta.test.ts index d6737d169a2dc00fbea6b3d933dffb1cf9efc9fc..75f753c7fe1c88c067cd9a6fe9212cdb45c4c90f 100644 --- a/lib/meta.test.ts +++ b/lib/meta.test.ts @@ -1,109 +1,115 @@ const base = new URL("https://example.com/"); -const T: Template = { base }; +const T: meta.Template = { base }; describe("title", () => { test("is always first entry in meta", () => { - const { meta } = toTags({ title: "Hello" }, T); - assert.deepEqual(meta[0], { title: "Hello" }); + const tags = meta.toTags(T, { title: "Hello" }); + assert.deepEqual(tags.meta[0], { title: "Hello" }); }); test("titleTemplate applied", () => { - const { meta } = toTags({ title: "Page" }, { base, titleTemplate: (t) => `${t} | Site` }); - assert.deepEqual(meta[0], { title: "Page | Site" }); + const tags = meta.toTags({ base, titleTemplate: (t) => `${t} | Site` }, { title: "Page" }); + assert.deepEqual(tags.meta[0], { title: "Page | Site" }); }); test("toHtml title is HTML-escaped", () => { - const out = toHtml({ title: "<b>Hi</b>" }, T); + const out = meta.toHtml(T, { title: "<b>Hi</b>" }); assert.ok(out.startsWith("<title><b>Hi</b>")); }); }); describe("basic meta fields", () => { test("multiple authors emit separate tags", () => { - const { meta } = toTags({ title: "T", authors: ["Alice", "Bob"] }, T); + const tags = meta.toTags(T, { title: "T", authors: ["Alice", "Bob"] }); assert.deepEqual( - meta.filter((m) => m.name === "author"), + tags.meta.filter((m) => m.name === "author"), [{ name: "author", content: "Alice" }, { name: "author", content: "Bob" }], ); }); test("keywords joined with comma-space", () => { - const { meta } = toTags({ title: "T", keywords: ["cats", "dogs", "fish"] }, T); - assert.ok(meta.some((m) => m.name === "keywords" && m.content === "cats, dogs, fish")); + const tags = meta.toTags(T, { title: "T", keywords: ["cats", "dogs", "fish"] }); + assert.ok(tags.meta.some((m) => m.name === "keywords" && m.content === "cats, dogs, fish")); }); test("generator: string used as-is", () => { - const { meta } = toTags({ title: "T", generator: "my-tool" }, T); - assert.ok(meta.some((m) => m.name === "generator" && m.content === "my-tool")); + const tags = meta.toTags(T, { title: "T", generator: "my-tool" }); + assert.ok(tags.meta.some((m) => m.name === "generator" && m.content === "my-tool")); }); test("themeColor string emits single tag without media", () => { - const { meta } = toTags({ title: "T", themeColor: "#fff" }, T); - const tags = meta.filter((m) => m.name === "theme-color"); - assert.equal(tags.length, 1); - assert.equal(tags[0]!.content, "#fff"); - assert.equal(tags[0]!.media, undefined); + const tags = meta.toTags(T, { title: "T", themeColor: "#fff" }); + const themeColor = tags.meta.filter((m) => m.name === "theme-color"); + assert.equal(themeColor.length, 1); + assert.equal(themeColor[0]!.content, "#fff"); + assert.equal(themeColor[0]!.media, undefined); }); test("themeColor object emits two tags with media queries", () => { - const { meta } = toTags({ title: "T", themeColor: { dark: "#111", light: "#eee" } }, T); + const tags = meta.toTags(T, { title: "T", themeColor: { dark: "#111", light: "#eee" } }); assert.ok( - meta.some((m) => m.name === "theme-color" && m.media === "(prefers-color-scheme:light)" && m.content === "#eee"), + tags.meta.some((m) => + m.name === "theme-color" && m.media === "(prefers-color-scheme:light)" && m.content === "#eee" + ), ); assert.ok( - meta.some((m) => m.name === "theme-color" && m.media === "(prefers-color-scheme:dark)" && m.content === "#111"), + tags.meta.some((m) => + m.name === "theme-color" && m.media === "(prefers-color-scheme:dark)" && m.content === "#111" + ), ); }); test("denyRobots from meta", () => { - const { meta } = toTags({ title: "T", denyRobots: true }, T); - assert.ok(meta.some((m) => m.name === "robots" && m.content === "noindex,nofollow")); + const tags = meta.toTags(T, { title: "T", denyRobots: true }); + assert.ok(tags.meta.some((m) => m.name === "robots" && m.content === "noindex,nofollow")); }); test("denyRobots from template", () => { - const { meta } = toTags({ title: "T" }, { ...T, denyRobots: true }); - assert.ok(meta.some((m) => m.name === "robots" && m.content === "noindex,nofollow")); + const tags = meta.toTags({ ...T, denyRobots: true }, { title: "T" }); + assert.ok(tags.meta.some((m) => m.name === "robots" && m.content === "noindex,nofollow")); }); test("denyRobots: false on meta does not override template true", () => { // null/undefined opts out; false is falsy so it also doesn't block - const { meta } = toTags({ title: "T", denyRobots: false }, { ...T, denyRobots: true }); - assert.ok(meta.some((m) => m.name === "robots")); + const tags = meta.toTags({ ...T, denyRobots: true }, { title: "T", denyRobots: false }); + assert.ok(tags.meta.some((m) => m.name === "robots")); }); test("viewport defaults to mobile-friendly value", () => { - const { meta } = toTags({ title: "T" }, T); + const tags = meta.toTags(T, { title: "T" }); assert.ok( - meta.some((m) => m.name === "viewport" && m.content === "width=device-width, initial-scale=1, maximum-scale=1"), + tags.meta.some((m) => + m.name === "viewport" && m.content === "width=device-width, initial-scale=1, maximum-scale=1" + ), ); }); test("viewport can be overridden", () => { - const { meta } = toTags({ title: "T", viewport: "width=device-width" }, T); - assert.ok(meta.some((m) => m.name === "viewport" && m.content === "width=device-width")); + const tags = meta.toTags(T, { title: "T", viewport: "width=device-width" }); + assert.ok(tags.meta.some((m) => m.name === "viewport" && m.content === "width=device-width")); }); }); describe("links", () => { test("canonical resolved against base", () => { - const { links } = toTags({ title: "T", canonical: "/page" }, T); + const { links } = meta.toTags(T, { title: "T", canonical: "/page" }); assert.ok(links.some((l) => l.rel === "canonical" && l.href === "https://example.com/page")); }); test("canonical absolute URL kept as-is", () => { - const { links } = toTags({ title: "T", canonical: "https://other.com/x" }, T); + const { links } = meta.toTags(T, { title: "T", canonical: "https://other.com/x" }); assert.ok(links.some((l) => l.rel === "canonical" && l.href === "https://other.com/x")); }); test("manifest href not resolved", () => { - const { links } = toTags({ title: "T", manifest: "/manifest.json" }, T); + const { links } = meta.toTags(T, { title: "T", manifest: "/manifest.json" }); assert.ok(links.some((l) => l.rel === "manifest" && l.href === "/manifest.json")); }); }); describe("oEmbed", () => { test("emits alternate link with json+oembed type, URL resolved", () => { - const { links } = toTags({ title: "T", oEmbed: "/oembed.json" }, T); + const { links } = meta.toTags(T, { title: "T", oEmbed: "/oembed.json" }); assert.ok(links.some((l) => l.rel === "alternate" && l.type === "application/json+oembed" @@ -112,271 +118,346 @@ describe("oEmbed", () => { }); test("falls back to template.oEmbed", () => { - const { links } = toTags({ title: "T" }, { ...T, oEmbed: "/oembed.json" }); + const { links } = meta.toTags({ ...T, oEmbed: "/oembed.json" }, { title: "T" }); assert.ok(links.some((l) => l.type === "application/json+oembed")); }); }); describe("alternates", () => { test("array form with lang → hreflang link", () => { - const { links } = toTags({ title: "T", alternates: [{ lang: "fr", url: "/fr" }] }, T); + const { links } = meta.toTags(T, { title: "T", alternates: [{ lang: "fr", url: "/fr" }] }); assert.ok(links.some((l) => l.rel === "alternate" && l.hreflang === "fr" && l.href === "https://example.com/fr")); }); test("array form with type → type link", () => { - const { links } = toTags({ title: "T", alternates: [{ type: "application/rss+xml", url: "/feed.rss" }] }, T); + const { links } = meta.toTags(T, { title: "T", alternates: [{ type: "application/rss+xml", url: "/feed.rss" }] }); assert.ok(links.some((l) => l.rel === "alternate" && l.type === "application/rss+xml")); }); test("record form: key with slash → type", () => { - const { links } = toTags({ title: "T", alternates: { "application/atom+xml": "/atom.xml" } }, T); + const { links } = meta.toTags(T, { title: "T", alternates: { "application/atom+xml": "/atom.xml" } }); assert.ok(links.some((l) => l.type === "application/atom+xml" && !l.hreflang)); }); test("record form: key without slash → hreflang", () => { - const { links } = toTags({ title: "T", alternates: { "ja": "/ja/" } }, T); + const { links } = meta.toTags(T, { title: "T", alternates: { "ja": "/ja/" } }); assert.ok(links.some((l) => l.hreflang === "ja" && !l.type)); }); test("alternate URLs resolved against base", () => { - const { links } = toTags({ title: "T", alternates: [{ lang: "de", url: "/de" }] }, T); + const { links } = meta.toTags(T, { title: "T", alternates: [{ lang: "de", url: "/de" }] }); assert.ok(links.some((l) => l.href === "https://example.com/de")); }); test("falls back to template.alternates", () => { - const { links } = toTags({ title: "T" }, { ...T, alternates: [{ lang: "en", url: "/" }] }); + const { links } = meta.toTags({ ...T, alternates: [{ lang: "en", url: "/" }] }, { title: "T" }); assert.ok(links.some((l) => l.hreflang === "en")); }); }); describe("embed", () => { test("no thumbnail → twitter:card = summary", () => { - const { meta } = toTags({ title: "T", embed: {} }, T); - assert.ok(meta.some((m) => m.name === "twitter:card" && m.content === "summary")); + const tags = meta.toTags(T, { title: "T", embed: {} }); + assert.ok(tags.meta.some((m) => m.name === "twitter:card" && m.content === "summary")); }); test("thumbnail → twitter:card = summary_large_image", () => { - const { meta } = toTags({ title: "T", embed: { thumbnail: "/img.png" } }, T); - assert.ok(meta.some((m) => m.name === "twitter:card" && m.content === "summary_large_image")); + const tags = meta.toTags(T, { title: "T", embed: { thumbnail: "/img.png" } }); + assert.ok(tags.meta.some((m) => m.name === "twitter:card" && m.content === "summary_large_image")); }); test("thumbnailSize: icon overrides thumbnail default", () => { - const { meta } = toTags({ title: "T", embed: { thumbnail: "/img.png", thumbnailSize: "icon" } }, T); - assert.ok(meta.some((m) => m.name === "twitter:card" && m.content === "summary")); + const tags = meta.toTags(T, { title: "T", embed: { thumbnail: "/img.png", thumbnailSize: "icon" } }); + assert.ok(tags.meta.some((m) => m.name === "twitter:card" && m.content === "summary")); }); test("thumbnail resolved and applied to both og:image and twitter:image", () => { - const { meta } = toTags({ title: "T", embed: { thumbnail: "/img.png" } }, T); - assert.ok(meta.some((m) => m.name === "og:image" && m.content === "https://example.com/img.png")); - assert.ok(meta.some((m) => m.name === "twitter:image" && m.content === "https://example.com/img.png")); + const tags = meta.toTags(T, { title: "T", embed: { thumbnail: "/img.png" } }); + assert.ok(tags.meta.some((m) => m.name === "og:image" && m.content === "https://example.com/img.png")); + assert.ok(tags.meta.some((m) => m.name === "twitter:image" && m.content === "https://example.com/img.png")); }); test("embed title falls back to page title for og", () => { - const { meta } = toTags({ title: "My Page", embed: {} }, T); - assert.ok(meta.some((m) => m.name === "og:title" && m.content === "My Page")); + const tags = meta.toTags(T, { title: "My Page", embed: {} }); + assert.ok(tags.meta.some((m) => m.name === "og:title" && m.content === "My Page")); }); test("embed title overrides page title for og", () => { - const { meta } = toTags({ title: "My Page", embed: { title: "Share Title" } }, T); - assert.ok(meta.some((m) => m.name === "og:title" && m.content === "Share Title")); + const tags = meta.toTags(T, { title: "My Page", embed: { title: "Share Title" } }); + assert.ok(tags.meta.some((m) => m.name === "og:title" && m.content === "Share Title")); }); test("embed description falls back to page description", () => { - const { meta } = toTags({ title: "T", description: "page desc", embed: {} }, T); - assert.ok(meta.some((m) => m.name === "og:description" && m.content === "page desc")); + const tags = meta.toTags(T, { title: "T", description: "page desc", embed: {} }); + assert.ok(tags.meta.some((m) => m.name === "og:description" && m.content === "page desc")); }); test("siteTitle sets og:site_name", () => { - const { meta } = toTags({ title: "T", embed: { siteTitle: "My Site" } }, T); - assert.ok(meta.some((m) => m.name === "og:site_name" && m.content === "My Site")); + const tags = meta.toTags(T, { title: "T", embed: { siteTitle: "My Site" } }); + assert.ok(tags.meta.some((m) => m.name === "og:site_name" && m.content === "My Site")); }); test("canonical sets og:url when embed active", () => { - const { meta } = toTags({ title: "T", canonical: "/page", embed: {} }, T); - assert.ok(meta.some((m) => m.name === "og:url" && m.content === "https://example.com/page")); + const tags = meta.toTags(T, { title: "T", canonical: "/page", embed: {} }); + assert.ok(tags.meta.some((m) => m.name === "og:url" && m.content === "https://example.com/page")); }); test("no canonical → no og:url injected by embed", () => { - const { meta } = toTags({ title: "T", embed: {} }, T); - assert.ok(!meta.some((m) => m.name === "og:url")); + const tags = meta.toTags(T, { title: "T", embed: {} }); + assert.ok(!tags.meta.some((m) => m.name === "og:url")); }); test("explicit openGraph fields override embed defaults", () => { - const { meta } = toTags({ title: "T", embed: { title: "Embed" }, openGraph: { title: "Explicit" } }, T); - assert.ok(meta.some((m) => m.name === "og:title" && m.content === "Explicit")); - assert.equal(meta.filter((m) => m.name === "og:title").length, 1); + const tags = meta.toTags(T, { title: "T", embed: { title: "Embed" }, openGraph: { title: "Explicit" } }); + assert.ok(tags.meta.some((m) => m.name === "og:title" && m.content === "Explicit")); + assert.equal(tags.meta.filter((m) => m.name === "og:title").length, 1); }); test("embed sets og:type = website", () => { - const { meta } = toTags({ title: "T", embed: {} }, T); - assert.ok(meta.some((m) => m.name === "og:type" && m.content === "website")); + const tags = meta.toTags(T, { title: "T", embed: {} }); + assert.ok(tags.meta.some((m) => m.name === "og:type" && m.content === "website")); }); }); describe("openGraph nested/array fields", () => { test("nested object emits namespaced tags", () => { - const { meta } = toTags( + const tags = meta.toTags( + T, { title: "T", openGraph: { image: { url: "https://example.com/img.png", width: "800" } } }, - T, ); - assert.ok(meta.some((m) => m.name === "og:image:url" && m.content === "https://example.com/img.png")); - assert.ok(meta.some((m) => m.name === "og:image:width" && m.content === "800")); + assert.ok(tags.meta.some((m) => m.name === "og:image:url" && m.content === "https://example.com/img.png")); + assert.ok(tags.meta.some((m) => m.name === "og:image:width" && m.content === "800")); }); test("array value emits duplicate tags", () => { - const { meta } = toTags({ title: "T", openGraph: { image: ["https://a.com/1.png", "https://a.com/2.png"] } }, T); - assert.equal(meta.filter((m) => m.name === "og:image").length, 2); + const tags = meta.toTags( + T, + { title: "T", openGraph: { image: ["https://a.com/1.png", "https://a.com/2.png"] } }, + ); + assert.equal(tags.meta.filter((m) => m.name === "og:image").length, 2); }); test("null field is skipped", () => { - const { meta } = toTags({ title: "T", openGraph: { image: null } }, T); - assert.ok(!meta.some((m) => m.name === "og:image")); + const tags = meta.toTags(T, { title: "T", openGraph: { image: null } }); + assert.ok(!tags.meta.some((m) => m.name === "og:image")); }); }); describe("template fallbacks", () => { test("template fields used when meta omits them", () => { - const template: Template = { base, publisher: "Acme", keywords: ["a", "b"] }; - const { meta } = toTags({ title: "T" }, template); - assert.ok(meta.some((m) => m.name === "publisher" && m.content === "Acme")); - assert.ok(meta.some((m) => m.name === "keywords" && m.content === "a, b")); + const template: meta.Template = { base, publisher: "Acme", keywords: ["a", "b"] }; + const tags = meta.toTags(template, { title: "T" }); + assert.ok(tags.meta.some((m) => m.name === "publisher" && m.content === "Acme")); + assert.ok(tags.meta.some((m) => m.name === "keywords" && m.content === "a, b")); }); test("meta field overrides template, no duplicate", () => { - const template: Template = { base, publisher: "Acme" }; - const { meta } = toTags({ title: "T", publisher: "Override" }, template); - assert.ok(meta.some((m) => m.name === "publisher" && m.content === "Override")); - assert.equal(meta.filter((m) => m.name === "publisher").length, 1); + const template: meta.Template = { base, publisher: "Acme" }; + const tags = meta.toTags(template, { title: "T", publisher: "Override" }); + assert.ok(tags.meta.some((m) => m.name === "publisher" && m.content === "Override")); + assert.equal(tags.meta.filter((m) => m.name === "publisher").length, 1); }); }); describe("toHtml serialization", () => { test("attribute values with spaces are quoted", () => { - const out = toHtml({ title: "T", description: "hello world" }, T); + const out = meta.toHtml(T, { title: "T", description: "hello world" }); assert.ok(out.includes(`content="hello world"`)); }); test("attribute values without special chars are unquoted", () => { - const out = toHtml({ title: "T", publisher: "Acme" }, T); + const out = meta.toHtml(T, { title: "T", publisher: "Acme" }); assert.ok(out.includes(`content=Acme`)); }); test("attribute value with slash is quoted", () => { - const out = toHtml({ title: "T" }, T); + const out = meta.toHtml(T, { title: "T" }); // viewport contains / — ensure it's quoted assert.ok(out.includes(`content="width=device-width`)); }); test("description with double quotes is escaped", () => { - const out = toHtml({ title: "T", description: `say "hi"` }, T); + const out = meta.toHtml(T, { title: "T", description: `say "hi"` }); assert.ok(!out.includes(`"say "hi"`)); }); }); describe("scripts", () => { test("inline script emits children in scripts array", () => { - const { scripts } = toTags({ title: "T", scripts: [{ content: "alert(1)" }] }, T); + const { scripts } = meta.toTags(T, { title: "T", scripts: [{ content: "alert(1)" }] }); assert.deepEqual(scripts, [{ children: "alert(1)" }]); }); test("external script with src and boolean attrs", () => { - const { scripts } = toTags({ title: "T", scripts: [{ src: "/app.js", async: true, defer: false }] }, T); + const { scripts } = meta.toTags(T, { title: "T", scripts: [{ src: "/app.js", async: true, defer: false }] }); assert.equal(scripts.length, 1); + assert.ok(scripts[0]); assert.equal(scripts[0].src, "/app.js"); assert.equal(scripts[0].async, true); assert.equal(scripts[0].defer, undefined); }); test("toHtml renders inline script", () => { - const out = toHtml({ title: "T", scripts: [{ content: "console.log(1)" }] }, T); + const out = meta.toHtml(T, { title: "T", scripts: [{ content: "console.log(1)" }] }); assert.ok(out.includes("")); }); test("toHtml renders external script with boolean attr", () => { - const out = toHtml({ title: "T", scripts: [{ src: "/app.js", async: true }] }, T); + const out = meta.toHtml(T, { title: "T", scripts: [{ src: "/app.js", async: true }] }); assert.ok(out.includes("")); }); test("falls back to template scripts", () => { - const { scripts } = toTags({ title: "T" }, { ...T, scripts: [{ content: "init()" }] }); + const { scripts } = meta.toTags({ ...T, scripts: [{ content: "init()" }] }, { title: "T" }); assert.deepEqual(scripts, [{ children: "init()" }]); }); }); describe("styles", () => { test("inline style emits children in styles array", () => { - const { styles } = toTags({ title: "T", styles: [{ content: "body{margin:0}" }] }, T); + const { styles } = meta.toTags(T, { title: "T", styles: [{ content: "body{margin:0}" }] }); assert.deepEqual(styles, [{ children: "body{margin:0}" }]); }); test("inline style with media", () => { - const { styles } = toTags({ title: "T", styles: [{ content: "p{color:blue}", media: "print" }] }, T); + const { styles } = meta.toTags(T, { title: "T", styles: [{ content: "p{color:blue}", media: "print" }] }); assert.deepEqual(styles, [{ children: "p{color:blue}", media: "print" }]); }); test("external stylesheet goes into links", () => { - const { links, styles } = toTags({ title: "T", styles: [{ href: "/style.css" }] }, T); + const { links, styles } = meta.toTags(T, { title: "T", styles: [{ href: "/style.css" }] }); assert.ok(links.some((l) => l.rel === "stylesheet" && l.href === "/style.css")); assert.equal(styles.length, 0); }); test("external stylesheet with media", () => { - const { links } = toTags({ title: "T", styles: [{ href: "/print.css", media: "print" }] }, T); + const { links } = meta.toTags(T, { title: "T", styles: [{ href: "/print.css", media: "print" }] }); assert.ok(links.some((l) => l.rel === "stylesheet" && l.href === "/print.css" && l.media === "print")); }); test("toHtml renders inline style", () => { - const out = toHtml({ title: "T", styles: [{ content: "body{margin:0}" }] }, T); + const out = meta.toHtml(T, { title: "T", styles: [{ content: "body{margin:0}" }] }); assert.ok(out.includes("")); }); test("toHtml renders inline style with media", () => { - const out = toHtml({ title: "T", styles: [{ content: "p{color:blue}", media: "print" }] }, T); + const out = meta.toHtml(T, { title: "T", styles: [{ content: "p{color:blue}", media: "print" }] }); assert.ok(out.includes("")); }); test("falls back to template styles", () => { - const { styles } = toTags({ title: "T" }, { ...T, styles: [{ content: "h1{color:red}" }] }); + const { styles } = meta.toTags({ ...T, styles: [{ content: "h1{color:red}" }] }, { title: "T" }); assert.deepEqual(styles, [{ children: "h1{color:red}" }]); }); }); describe("extra", () => { test("tag=meta goes into meta array", () => { - const { meta } = toTags({ title: "T", extra: [{ tag: "meta", name: "verify", content: "abc" }] }, T); - assert.ok(meta.some((m) => m.name === "verify" && m.content === "abc")); + const tags = meta.toTags(T, { title: "T", extra: [{ tag: "meta", name: "verify", content: "abc" }] }); + assert.ok(tags.meta.some((m) => m.name === "verify" && m.content === "abc")); }); test("tag=link goes into links array", () => { - const { links } = toTags({ title: "T", extra: [{ tag: "link", rel: "icon", href: "/favicon.ico" }] }, T); + const { links } = meta.toTags(T, { title: "T", extra: [{ tag: "link", rel: "icon", href: "/favicon.ico" }] }); assert.ok(links.some((l) => l.rel === "icon" && l.href === "/favicon.ico")); }); test("tag=script goes into scripts array", () => { - const { scripts } = toTags({ title: "T", extra: [{ tag: "script", src: "/extra.js", async: true }] }, T); + const { scripts } = meta.toTags(T, { title: "T", extra: [{ tag: "script", src: "/extra.js", async: true }] }); assert.equal(scripts.length, 1); + assert.ok(scripts[0]); assert.equal(scripts[0].src, "/extra.js"); assert.equal(scripts[0].async, true); }); test("tag=style goes into styles array", () => { - const { styles } = toTags({ title: "T", extra: [{ tag: "style", children: "a{color:red}" }] }, T); + const { styles } = meta.toTags(T, { title: "T", extra: [{ tag: "style", children: "a{color:red}" }] }); assert.deepEqual(styles, [{ children: "a{color:red}" }]); }); test("null/false attrs are filtered out", () => { - const { meta } = toTags({ title: "T", extra: [{ tag: "meta", name: "x", content: "y", bogus: null }] }, T); - const entry = meta.find((m) => m.name === "x")!; + const tags = meta.toTags(T, { title: "T", extra: [{ tag: "meta", name: "x", content: "y", bogus: null }] }); + const entry = tags.meta.find((m) => m.name === "x")!; assert.equal(entry.bogus, undefined); }); test("falls back to template extra", () => { - const { meta } = toTags({ title: "T" }, { ...T, extra: [{ tag: "meta", name: "tmpl", content: "yes" }] }); - assert.ok(meta.some((m) => m.name === "tmpl" && m.content === "yes")); + const tags = meta.toTags({ ...T, extra: [{ tag: "meta", name: "tmpl", content: "yes" }] }, { title: "T" }); + assert.ok(tags.meta.some((m) => m.name === "tmpl" && m.content === "yes")); + }); +}); + +// Typescript test for TanStack router compatibility. +interface HtmlAttributes { + id?: string; + className?: string; + title?: string; + lang?: string; + dir?: string; + hidden?: boolean; + tabIndex?: number; + children?: unknown; +} +interface MetaAttributes extends HtmlAttributes { + name?: string; + content?: string; + charSet?: string; + httpEquiv?: string; + media?: string; +} +interface LinkAttributes extends HtmlAttributes { + rel?: string; + href?: string; + hrefLang?: string; + media?: string; + type?: string; + sizes?: string; + as?: string; +} +interface ScriptAttributes extends HtmlAttributes { + src?: string; + async?: boolean; + defer?: boolean; + type?: string; + noModule?: boolean; + integrity?: string; +} +interface StyleAttributes extends HtmlAttributes { + media?: string; + type?: string; +} + +/** The `head()` return contract from TanStack Start, deps-free. */ +interface TanStackHead { + meta?: Array; + links?: Array; + scripts?: Array; + styles?: Array; +} + +// Type-level assertion. A compile error on this line means `toTags`' output is +// no longer a valid TanStack `head()` payload. +const _asHead: (tags: meta.Tags) => TanStackHead = (tags) => tags; + +describe("TanStack Router compatibility", () => { + test("toTags output is a valid head() payload", () => { + const tags = meta.toTags( + { base: new URL("https://example.com/") }, + { + title: "Home", + description: "hi", + embed: { thumbnail: "/og.png" }, + scripts: [{ src: "/a.js", async: true }, { content: "console.log(1)" }], + styles: [{ href: "/a.css" }, { content: "a{color:red}" }], + extra: [{ tag: "link", rel: "icon", href: "/favicon.ico" }], + }, + ); + const head: TanStackHead = _asHead(tags); + assert.deepEqual(Object.keys(head), ["meta", "links", "scripts", "styles"]); }); }); import assert from "node:assert"; import { describe, test } from "vitest"; -import { type Template, toHtml, toTags } from "./meta.ts"; +import * as meta from "./meta.ts"; diff --git a/lib/meta.ts b/lib/meta.ts index 019a79c1c869d4b8d9155dd6d01607c19e487f56..4b76007da717699e9e4beb06c927cb520d054e63 100644 --- a/lib/meta.ts +++ b/lib/meta.ts @@ -80,7 +80,7 @@ export interface AutoEmbed { /* add an image to the embed card. */ thumbnail?: string | null; /** @default "banner", which applies twitter:card = "summary_large_image" */ - thumbnailSize?: "banner" | "icon"; + thumbnailSize?: "banner" | "icon" | null; /** @default null - no site title */ siteTitle?: string | null; } @@ -153,12 +153,8 @@ export interface Tags { styles: Array>; } -// TODO: before publishing clover lib v5, ensure this is actually compatibile with TanStack Router -// this type error will prevent publishing the library -void (NaN as string); - /** convert a metadata definition into a structured list of tags, compatible with TanStack start */ -export function toTags(meta: Meta, template: Template): Tags { +export function toTags(template: Template, meta: Meta): Tags { const { titleTemplate, base } = template; const resolve = (str: string) => new URL(str, base).href; @@ -252,7 +248,7 @@ export function toTags(meta: Meta, template: Template): Tags { for (const entry of meta.scripts ?? template.scripts ?? []) { if ("content" in entry) { - scriptTags.push({ children: entry.content }); + scriptTags.push({ children: String(entry.content) }); } else { const { src, ...rest } = entry; const tag: Record = { src }; @@ -265,7 +261,7 @@ export function toTags(meta: Meta, template: Template): Tags { for (const entry of meta.styles ?? template.styles ?? []) { if ("content" in entry) { - const tag: Record = { children: entry.content }; + const tag: Record = { children: String(entry.content) }; if ("media" in entry && entry.media) tag.media = entry.media; styleTags.push(tag); } else { @@ -293,8 +289,8 @@ export function toTags(meta: Meta, template: Template): Tags { } /* convert a metadata definition into HTML text. */ -export function toHtml(meta: Meta, template: Template): string { - const tags = toTags(meta, template); +export function toHtml(template: Template, meta: Meta): string { + const tags = toTags(template, meta); const [{ title }, ...metaTags] = tags.meta as [{ title: string }, ...Record[]]; let out = `${esc(title)}`; for (const attrs of metaTags) { diff --git a/lib/readme.changes.md b/lib/readme.changes.md index 1e5bea473919187b0feed50602f839b289c41ea1..48067405e35137742cfad5cff9c206e3df479d1b 100644 --- a/lib/readme.changes.md +++ b/lib/readme.changes.md @@ -7,23 +7,36 @@ - `render` has been deleted because it was not being used anymore. sad day because i will never get to prove how easy it is to write a stable Suspense and PPR implementations in less than 300 lines each. + - deprecated html escaping methods moved to `string.escapeHtmlContent` and + `string.escapeHtmlAttr` - `render/meta` is moved to `meta`, with a new API for rendering tags - - `meta.render` is renamed to `meta.toHtml` + - `meta.render` is renamed to `meta.toHtml`, arguments re-ordered - `meta.toTags` creates an object compatible with TanStack Router - the `extra` object now takes an array of React-like elements among some other supported forms. -- `log` has recieved so many bug-fix changes that it is worth ensuring your - application edge cases still work as expected. +- `log` and `progress` have recieved so many bug-fix changes that it is worth + ensuring your application edge cases still work as expected. +- `string.formatByteSize` formats differently. ### features -- `mime`'s database contains `.eot` for embedded opentype fonts. -- `async.deferred` +- new library `http`. +- new library `math`. +- new library `arrays`. - `log` - many many many many rendering bug-fixes - `writeError` writes to stderr with proper logic. - buffering auto-flushes synchronously when it grows past 64k. - custom scopes +- `string` + - `formatDurationLetters` spans years to seconds, plus infinity and negatives. +- `mime`'s database contains `.eot` for embedded opentype fonts. +- `async.deferred`. +- `queue.setConcurrency`. +- `subprocess.lookupProcessOnPort`. +- `subprocess/ffmpeg` progress handles fractional framerates (progress bug). +- `string/ansi` gains `clearToEndOfScreen`. +- `progress` estimation, maxHeight, and timer bug fixes. ## v4 diff --git a/lib/subprocess.ts b/lib/subprocess.ts index 157485b77b5ac1d2b97aa11d58dfbacb853b4a1c..851a9733a294d82bd56b1b80e02cf30eec3794be 100644 --- a/lib/subprocess.ts +++ b/lib/subprocess.ts @@ -90,7 +90,7 @@ export async function spawnAndWait( // } // }); -async function findInodeForPort(port) { +async function findInodeForPort(port: number) { const hexPort = port.toString(16).toUpperCase().padStart(4, "0"); for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) { try { @@ -104,7 +104,7 @@ async function findInodeForPort(port) { return null; } -async function findPidForInode(inode) { +async function findPidForInode(inode: string) { const target = `socket:[${inode}]`; for (const pid of await fs.readdir("/proc")) { if (!/^\d+$/.test(pid)) continue; @@ -154,7 +154,8 @@ async function commandFromPortWindows(port: number) { return { pid: Number(pid), command: cmdline }; } -export async function lookupProcessOnPort(port: number) { +/** Racily searches for what process is taking up a given port. */ +export async function lookupProcessOnPort(port: number): Promise { switch (os.platform()) { case "linux": return commandFromPortLinux(port); @@ -167,10 +168,14 @@ export async function lookupProcessOnPort(port: number) { } } -// import * as process from "node:process"; +interface ProcessLookup { + pid: number; + command: string; +} + import { Buffer } from "node:buffer"; import * as child_process from "node:child_process"; import * as events from "node:events"; import * as fs from "node:fs/promises"; +import * as os from "node:os"; import * as util from "node:util"; -import os from "os"; diff --git a/lib/testing.ts b/lib/testing.ts index 29307f1efbc584dcf7b082e00ed8d249965627a5..82c623f2e6009aa6f5b1d5911af0c155bf2e4906 100644 --- a/lib/testing.ts +++ b/lib/testing.ts @@ -111,13 +111,15 @@ export class SyncPromise implements Promise { } } +export interface FakeTimerEntry { + deadline: number; + resolve: () => void; + src: stack.Frame[]; +} + export class FakeTimers { time = 0; - entries: Array<{ - deadline: number; - resolve: () => void; - src: stack.Frame[]; - }> = []; + entries: Array = []; now: () => number = () => { return this.time; @@ -260,7 +262,7 @@ export class MockScreen implements Disposable, log.WidgetHost { ASSERT(this.timers.entries.length === 0); } - expectWithoutConsume(ms: number) { + expectWithoutConsume(ms: number): FakeTimerEntry { const wait = UNWRAP( this.timers.next(), () => this.out.length > 0 ? "terminal i/o did not wait" : "no terminal i/o", diff --git a/package-lock.json b/package-lock.json index 6f0ca7c483219628ac9e1249a12400bf826a0b8f..fcfe0424b6a333f7d1114a25515c1853eb013f60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -573,22 +573,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", @@ -673,7 +657,6 @@ "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.62.tgz", "integrity": "sha512-aBmWraqvVMLzi3oZkm5cEAVTv4qtLIVl9tkci6M+duSaEbKq4+n9FuNjYlOA0/4slRmOeAOuYyi2BenzgKmQNA==", "license": "MIT", - "peer": true, "dependencies": { "@luxass/strip-json-comments": "^1.4.0", "complain": "^1.6.1", @@ -1443,7 +1426,6 @@ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2114,7 +2096,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2528,406 +2509,6 @@ } } }, - "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", @@ -2973,7 +2554,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/src/pages/index.marko b/src/pages/index.marko index 0667a798df73193be579f09ecdcf163fe1962413..c3c4e5b141a94f6c5291633f8a4f3e9b7acc9fdd 100644 --- a/src/pages/index.marko +++ b/src/pages/index.marko @@ -22,10 +22,7 @@ export const meta = {

art