authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-06-15 10:27:30-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-06-15 12:49:48-07:00
loga25086ee5de025766806d480542ed6c10aef35f9
treecbc4e374de585ceb6d91d6d452d02596f16cb556
parent72e9a94ec20202598734c0edc99e6f5cba3fd296
signature Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ

chore: publish lib v5


12 files changed, 246 insertions(+), 568 deletions(-)

framework/generate.ts+2-2
......@@ -327,12 +327,12 @@ export async function preparePage(
327327 );
328328
329329 // -- metadata --
330 const renderedMeta = meta.toHtml({
330 const renderedMeta = meta.toHtml(metaTemplate, {
331331 canonical: item.id,
332332 ...typeof metadata === "function"
333333 ? await metadata({ ssr: true })
334334 : metadata,
335 }, metaTemplate);
335 });
336336 if (!renderedMeta.includes("<title>")) {
337337 throw new Error(
338338 "Page is missing 'meta.title'. " + "All pages need a title tag.",
framework/lib/view.ts+1-1
......@@ -62,7 +62,7 @@ export async function renderToString<K extends Key>(
6262 // -- metadata --
6363 const renderedMetaPromise = Promise.resolve(
6464 typeof metadata === "function" ? metadata(attrs) : metadata,
65 ).then((m) => meta.toHtml(m, codegen.metaTemplate));
65 ).then((m) => meta.toHtml(codegen.metaTemplate, m));
6666
6767 const cloverScripts = new Set<string>();
6868 const text = await SitegenWrap.render({
lib/arrays.ts+4-2
......@@ -6,7 +6,7 @@ export function* enumerate<T extends [unknown, ...unknown[]]>(
66 ...arrays: { [K in keyof T]: ArrayLike<T[K]> }
77): Generator<[...items: T, index: number]> {
88 const { length } = arrays[0];
9 arrays.forEach((x) => assert(x.length === length), "All arrays must be equal in length");
9 arrays.forEach((x) => ASSERT(x.length === length), "All arrays must be equal in length");
1010 for (let i = 0; i < length; i += 1) yield arrays.map((x) => x[i]!).concat(i) as [...T, number];
1111}
1212
......@@ -15,6 +15,8 @@ export function mapMany<T extends [unknown, ...unknown[]], O>(
1515 map: (...args: [...items: T, index: number]) => O,
1616): O[] {
1717 const { length } = arrays[0];
18 arrays.forEach((x) => assert(x.length === length), "All arrays must be equal in length");
18 arrays.forEach((x) => ASSERT(x.length === length), "All arrays must be equal in length");
1919 return Array.from({ length }, (_, i) => map(...(arrays.map((a) => a[i]!).concat(i) as [...T, number])));
2020}
21
22import { ASSERT } from "./assert.ts";
lib/deno.json+4-2
......@@ -1,23 +1,25 @@
11{
22 "name": "@clo/lib",
33 "description": "high quality typescript libraries",
4 "version": "4.0.0",
4 "version": "5.0.0",
55 "license": "MIT",
66 "exports": {
7 "./arrays": "./arrays.ts",
78 "./assert": "./assert.ts",
89 "./async": "./async.ts",
910 "./bytes": "./bytes.ts",
1011 "./error": "./error.ts",
1112 "./Events": "./Events.ts",
13 "./http": "./http.ts",
1214 "./log": "./log.ts",
1315 "./log/stack": "./log/stack.ts",
1416 "./Lru": "./Lru.ts",
17 "./math": "./math.ts",
1518 "./meta": "./meta.ts",
1619 "./mime": "./mime.ts",
1720 "./node": "./node.ts",
1821 "./progress": "./progress.ts",
1922 "./queue": "./queue.ts",
20 "./render/meta": "./render/meta.ts",
2123 "./stream": "./stream.ts",
2224 "./string": "./string.ts",
2325 "./string/ansi": "./string/ansi.ts",
lib/math.ts+2-2
......@@ -1,8 +1,8 @@
1export function gcd(a: number, b: number) {
1export function gcd(a: number, b: number): number {
22 while (b !== 0) [a, b] = [b, a % b];
33 return a;
44}
55
6export function clamp(value: number, min: number, max: number) {
6export function clamp(value: number, min: number, max: number): number {
77 return Math.min(max, Math.max(min, value));
88}
lib/meta.test.ts+190-109
......@@ -1,109 +1,115 @@
11const base = new URL("https://example.com/");
2const T: Template = { base };
2const T: meta.Template = { base };
33
44describe("title", () => {
55 test("is always first entry in meta", () => {
6 const { meta } = toTags({ title: "Hello" }, T);
7 assert.deepEqual(meta[0], { title: "Hello" });
6 const tags = meta.toTags(T, { title: "Hello" });
7 assert.deepEqual(tags.meta[0], { title: "Hello" });
88 });
99
1010 test("titleTemplate applied", () => {
11 const { meta } = toTags({ title: "Page" }, { base, titleTemplate: (t) => `${t} | Site` });
12 assert.deepEqual(meta[0], { title: "Page | Site" });
11 const tags = meta.toTags({ base, titleTemplate: (t) => `${t} | Site` }, { title: "Page" });
12 assert.deepEqual(tags.meta[0], { title: "Page | Site" });
1313 });
1414
1515 test("toHtml title is HTML-escaped", () => {
16 const out = toHtml({ title: "<b>Hi</b>" }, T);
16 const out = meta.toHtml(T, { title: "<b>Hi</b>" });
1717 assert.ok(out.startsWith("<title>&lt;b&gt;Hi&lt;/b&gt;</title>"));
1818 });
1919});
2020
2121describe("basic meta fields", () => {
2222 test("multiple authors emit separate tags", () => {
23 const { meta } = toTags({ title: "T", authors: ["Alice", "Bob"] }, T);
23 const tags = meta.toTags(T, { title: "T", authors: ["Alice", "Bob"] });
2424 assert.deepEqual(
25 meta.filter((m) => m.name === "author"),
25 tags.meta.filter((m) => m.name === "author"),
2626 [{ name: "author", content: "Alice" }, { name: "author", content: "Bob" }],
2727 );
2828 });
2929
3030 test("keywords joined with comma-space", () => {
31 const { meta } = toTags({ title: "T", keywords: ["cats", "dogs", "fish"] }, T);
32 assert.ok(meta.some((m) => m.name === "keywords" && m.content === "cats, dogs, fish"));
31 const tags = meta.toTags(T, { title: "T", keywords: ["cats", "dogs", "fish"] });
32 assert.ok(tags.meta.some((m) => m.name === "keywords" && m.content === "cats, dogs, fish"));
3333 });
3434
3535 test("generator: string used as-is", () => {
36 const { meta } = toTags({ title: "T", generator: "my-tool" }, T);
37 assert.ok(meta.some((m) => m.name === "generator" && m.content === "my-tool"));
36 const tags = meta.toTags(T, { title: "T", generator: "my-tool" });
37 assert.ok(tags.meta.some((m) => m.name === "generator" && m.content === "my-tool"));
3838 });
3939
4040 test("themeColor string emits single tag without media", () => {
41 const { meta } = toTags({ title: "T", themeColor: "#fff" }, T);
42 const tags = meta.filter((m) => m.name === "theme-color");
43 assert.equal(tags.length, 1);
44 assert.equal(tags[0]!.content, "#fff");
45 assert.equal(tags[0]!.media, undefined);
41 const tags = meta.toTags(T, { title: "T", themeColor: "#fff" });
42 const themeColor = tags.meta.filter((m) => m.name === "theme-color");
43 assert.equal(themeColor.length, 1);
44 assert.equal(themeColor[0]!.content, "#fff");
45 assert.equal(themeColor[0]!.media, undefined);
4646 });
4747
4848 test("themeColor object emits two tags with media queries", () => {
49 const { meta } = toTags({ title: "T", themeColor: { dark: "#111", light: "#eee" } }, T);
49 const tags = meta.toTags(T, { title: "T", themeColor: { dark: "#111", light: "#eee" } });
5050 assert.ok(
51 meta.some((m) => m.name === "theme-color" && m.media === "(prefers-color-scheme:light)" && m.content === "#eee"),
51 tags.meta.some((m) =>
52 m.name === "theme-color" && m.media === "(prefers-color-scheme:light)" && m.content === "#eee"
53 ),
5254 );
5355 assert.ok(
54 meta.some((m) => m.name === "theme-color" && m.media === "(prefers-color-scheme:dark)" && m.content === "#111"),
56 tags.meta.some((m) =>
57 m.name === "theme-color" && m.media === "(prefers-color-scheme:dark)" && m.content === "#111"
58 ),
5559 );
5660 });
5761
5862 test("denyRobots from meta", () => {
59 const { meta } = toTags({ title: "T", denyRobots: true }, T);
60 assert.ok(meta.some((m) => m.name === "robots" && m.content === "noindex,nofollow"));
63 const tags = meta.toTags(T, { title: "T", denyRobots: true });
64 assert.ok(tags.meta.some((m) => m.name === "robots" && m.content === "noindex,nofollow"));
6165 });
6266
6367 test("denyRobots from template", () => {
64 const { meta } = toTags({ title: "T" }, { ...T, denyRobots: true });
65 assert.ok(meta.some((m) => m.name === "robots" && m.content === "noindex,nofollow"));
68 const tags = meta.toTags({ ...T, denyRobots: true }, { title: "T" });
69 assert.ok(tags.meta.some((m) => m.name === "robots" && m.content === "noindex,nofollow"));
6670 });
6771
6872 test("denyRobots: false on meta does not override template true", () => {
6973 // null/undefined opts out; false is falsy so it also doesn't block
70 const { meta } = toTags({ title: "T", denyRobots: false }, { ...T, denyRobots: true });
71 assert.ok(meta.some((m) => m.name === "robots"));
74 const tags = meta.toTags({ ...T, denyRobots: true }, { title: "T", denyRobots: false });
75 assert.ok(tags.meta.some((m) => m.name === "robots"));
7276 });
7377
7478 test("viewport defaults to mobile-friendly value", () => {
75 const { meta } = toTags({ title: "T" }, T);
79 const tags = meta.toTags(T, { title: "T" });
7680 assert.ok(
77 meta.some((m) => m.name === "viewport" && m.content === "width=device-width, initial-scale=1, maximum-scale=1"),
81 tags.meta.some((m) =>
82 m.name === "viewport" && m.content === "width=device-width, initial-scale=1, maximum-scale=1"
83 ),
7884 );
7985 });
8086
8187 test("viewport can be overridden", () => {
82 const { meta } = toTags({ title: "T", viewport: "width=device-width" }, T);
83 assert.ok(meta.some((m) => m.name === "viewport" && m.content === "width=device-width"));
88 const tags = meta.toTags(T, { title: "T", viewport: "width=device-width" });
89 assert.ok(tags.meta.some((m) => m.name === "viewport" && m.content === "width=device-width"));
8490 });
8591});
8692
8793describe("links", () => {
8894 test("canonical resolved against base", () => {
89 const { links } = toTags({ title: "T", canonical: "/page" }, T);
95 const { links } = meta.toTags(T, { title: "T", canonical: "/page" });
9096 assert.ok(links.some((l) => l.rel === "canonical" && l.href === "https://example.com/page"));
9197 });
9298
9399 test("canonical absolute URL kept as-is", () => {
94 const { links } = toTags({ title: "T", canonical: "https://other.com/x" }, T);
100 const { links } = meta.toTags(T, { title: "T", canonical: "https://other.com/x" });
95101 assert.ok(links.some((l) => l.rel === "canonical" && l.href === "https://other.com/x"));
96102 });
97103
98104 test("manifest href not resolved", () => {
99 const { links } = toTags({ title: "T", manifest: "/manifest.json" }, T);
105 const { links } = meta.toTags(T, { title: "T", manifest: "/manifest.json" });
100106 assert.ok(links.some((l) => l.rel === "manifest" && l.href === "/manifest.json"));
101107 });
102108});
103109
104110describe("oEmbed", () => {
105111 test("emits alternate link with json+oembed type, URL resolved", () => {
106 const { links } = toTags({ title: "T", oEmbed: "/oembed.json" }, T);
112 const { links } = meta.toTags(T, { title: "T", oEmbed: "/oembed.json" });
107113 assert.ok(links.some((l) =>
108114 l.rel === "alternate"
109115 && l.type === "application/json+oembed"
......@@ -112,271 +118,346 @@ describe("oEmbed", () => {
112118 });
113119
114120 test("falls back to template.oEmbed", () => {
115 const { links } = toTags({ title: "T" }, { ...T, oEmbed: "/oembed.json" });
121 const { links } = meta.toTags({ ...T, oEmbed: "/oembed.json" }, { title: "T" });
116122 assert.ok(links.some((l) => l.type === "application/json+oembed"));
117123 });
118124});
119125
120126describe("alternates", () => {
121127 test("array form with lang → hreflang link", () => {
122 const { links } = toTags({ title: "T", alternates: [{ lang: "fr", url: "/fr" }] }, T);
128 const { links } = meta.toTags(T, { title: "T", alternates: [{ lang: "fr", url: "/fr" }] });
123129 assert.ok(links.some((l) => l.rel === "alternate" && l.hreflang === "fr" && l.href === "https://example.com/fr"));
124130 });
125131
126132 test("array form with type → type link", () => {
127 const { links } = toTags({ title: "T", alternates: [{ type: "application/rss+xml", url: "/feed.rss" }] }, T);
133 const { links } = meta.toTags(T, { title: "T", alternates: [{ type: "application/rss+xml", url: "/feed.rss" }] });
128134 assert.ok(links.some((l) => l.rel === "alternate" && l.type === "application/rss+xml"));
129135 });
130136
131137 test("record form: key with slash → type", () => {
132 const { links } = toTags({ title: "T", alternates: { "application/atom+xml": "/atom.xml" } }, T);
138 const { links } = meta.toTags(T, { title: "T", alternates: { "application/atom+xml": "/atom.xml" } });
133139 assert.ok(links.some((l) => l.type === "application/atom+xml" && !l.hreflang));
134140 });
135141
136142 test("record form: key without slash → hreflang", () => {
137 const { links } = toTags({ title: "T", alternates: { "ja": "/ja/" } }, T);
143 const { links } = meta.toTags(T, { title: "T", alternates: { "ja": "/ja/" } });
138144 assert.ok(links.some((l) => l.hreflang === "ja" && !l.type));
139145 });
140146
141147 test("alternate URLs resolved against base", () => {
142 const { links } = toTags({ title: "T", alternates: [{ lang: "de", url: "/de" }] }, T);
148 const { links } = meta.toTags(T, { title: "T", alternates: [{ lang: "de", url: "/de" }] });
143149 assert.ok(links.some((l) => l.href === "https://example.com/de"));
144150 });
145151
146152 test("falls back to template.alternates", () => {
147 const { links } = toTags({ title: "T" }, { ...T, alternates: [{ lang: "en", url: "/" }] });
153 const { links } = meta.toTags({ ...T, alternates: [{ lang: "en", url: "/" }] }, { title: "T" });
148154 assert.ok(links.some((l) => l.hreflang === "en"));
149155 });
150156});
151157
152158describe("embed", () => {
153159 test("no thumbnail → twitter:card = summary", () => {
154 const { meta } = toTags({ title: "T", embed: {} }, T);
155 assert.ok(meta.some((m) => m.name === "twitter:card" && m.content === "summary"));
160 const tags = meta.toTags(T, { title: "T", embed: {} });
161 assert.ok(tags.meta.some((m) => m.name === "twitter:card" && m.content === "summary"));
156162 });
157163
158164 test("thumbnail → twitter:card = summary_large_image", () => {
159 const { meta } = toTags({ title: "T", embed: { thumbnail: "/img.png" } }, T);
160 assert.ok(meta.some((m) => m.name === "twitter:card" && m.content === "summary_large_image"));
165 const tags = meta.toTags(T, { title: "T", embed: { thumbnail: "/img.png" } });
166 assert.ok(tags.meta.some((m) => m.name === "twitter:card" && m.content === "summary_large_image"));
161167 });
162168
163169 test("thumbnailSize: icon overrides thumbnail default", () => {
164 const { meta } = toTags({ title: "T", embed: { thumbnail: "/img.png", thumbnailSize: "icon" } }, T);
165 assert.ok(meta.some((m) => m.name === "twitter:card" && m.content === "summary"));
170 const tags = meta.toTags(T, { title: "T", embed: { thumbnail: "/img.png", thumbnailSize: "icon" } });
171 assert.ok(tags.meta.some((m) => m.name === "twitter:card" && m.content === "summary"));
166172 });
167173
168174 test("thumbnail resolved and applied to both og:image and twitter:image", () => {
169 const { meta } = toTags({ title: "T", embed: { thumbnail: "/img.png" } }, T);
170 assert.ok(meta.some((m) => m.name === "og:image" && m.content === "https://example.com/img.png"));
171 assert.ok(meta.some((m) => m.name === "twitter:image" && m.content === "https://example.com/img.png"));
175 const tags = meta.toTags(T, { title: "T", embed: { thumbnail: "/img.png" } });
176 assert.ok(tags.meta.some((m) => m.name === "og:image" && m.content === "https://example.com/img.png"));
177 assert.ok(tags.meta.some((m) => m.name === "twitter:image" && m.content === "https://example.com/img.png"));
172178 });
173179
174180 test("embed title falls back to page title for og", () => {
175 const { meta } = toTags({ title: "My Page", embed: {} }, T);
176 assert.ok(meta.some((m) => m.name === "og:title" && m.content === "My Page"));
181 const tags = meta.toTags(T, { title: "My Page", embed: {} });
182 assert.ok(tags.meta.some((m) => m.name === "og:title" && m.content === "My Page"));
177183 });
178184
179185 test("embed title overrides page title for og", () => {
180 const { meta } = toTags({ title: "My Page", embed: { title: "Share Title" } }, T);
181 assert.ok(meta.some((m) => m.name === "og:title" && m.content === "Share Title"));
186 const tags = meta.toTags(T, { title: "My Page", embed: { title: "Share Title" } });
187 assert.ok(tags.meta.some((m) => m.name === "og:title" && m.content === "Share Title"));
182188 });
183189
184190 test("embed description falls back to page description", () => {
185 const { meta } = toTags({ title: "T", description: "page desc", embed: {} }, T);
186 assert.ok(meta.some((m) => m.name === "og:description" && m.content === "page desc"));
191 const tags = meta.toTags(T, { title: "T", description: "page desc", embed: {} });
192 assert.ok(tags.meta.some((m) => m.name === "og:description" && m.content === "page desc"));
187193 });
188194
189195 test("siteTitle sets og:site_name", () => {
190 const { meta } = toTags({ title: "T", embed: { siteTitle: "My Site" } }, T);
191 assert.ok(meta.some((m) => m.name === "og:site_name" && m.content === "My Site"));
196 const tags = meta.toTags(T, { title: "T", embed: { siteTitle: "My Site" } });
197 assert.ok(tags.meta.some((m) => m.name === "og:site_name" && m.content === "My Site"));
192198 });
193199
194200 test("canonical sets og:url when embed active", () => {
195 const { meta } = toTags({ title: "T", canonical: "/page", embed: {} }, T);
196 assert.ok(meta.some((m) => m.name === "og:url" && m.content === "https://example.com/page"));
201 const tags = meta.toTags(T, { title: "T", canonical: "/page", embed: {} });
202 assert.ok(tags.meta.some((m) => m.name === "og:url" && m.content === "https://example.com/page"));
197203 });
198204
199205 test("no canonical → no og:url injected by embed", () => {
200 const { meta } = toTags({ title: "T", embed: {} }, T);
201 assert.ok(!meta.some((m) => m.name === "og:url"));
206 const tags = meta.toTags(T, { title: "T", embed: {} });
207 assert.ok(!tags.meta.some((m) => m.name === "og:url"));
202208 });
203209
204210 test("explicit openGraph fields override embed defaults", () => {
205 const { meta } = toTags({ title: "T", embed: { title: "Embed" }, openGraph: { title: "Explicit" } }, T);
206 assert.ok(meta.some((m) => m.name === "og:title" && m.content === "Explicit"));
207 assert.equal(meta.filter((m) => m.name === "og:title").length, 1);
211 const tags = meta.toTags(T, { title: "T", embed: { title: "Embed" }, openGraph: { title: "Explicit" } });
212 assert.ok(tags.meta.some((m) => m.name === "og:title" && m.content === "Explicit"));
213 assert.equal(tags.meta.filter((m) => m.name === "og:title").length, 1);
208214 });
209215
210216 test("embed sets og:type = website", () => {
211 const { meta } = toTags({ title: "T", embed: {} }, T);
212 assert.ok(meta.some((m) => m.name === "og:type" && m.content === "website"));
217 const tags = meta.toTags(T, { title: "T", embed: {} });
218 assert.ok(tags.meta.some((m) => m.name === "og:type" && m.content === "website"));
213219 });
214220});
215221
216222describe("openGraph nested/array fields", () => {
217223 test("nested object emits namespaced tags", () => {
218 const { meta } = toTags(
219 { title: "T", openGraph: { image: { url: "https://example.com/img.png", width: "800" } } },
224 const tags = meta.toTags(
220225 T,
226 { title: "T", openGraph: { image: { url: "https://example.com/img.png", width: "800" } } },
221227 );
222 assert.ok(meta.some((m) => m.name === "og:image:url" && m.content === "https://example.com/img.png"));
223 assert.ok(meta.some((m) => m.name === "og:image:width" && m.content === "800"));
228 assert.ok(tags.meta.some((m) => m.name === "og:image:url" && m.content === "https://example.com/img.png"));
229 assert.ok(tags.meta.some((m) => m.name === "og:image:width" && m.content === "800"));
224230 });
225231
226232 test("array value emits duplicate tags", () => {
227 const { meta } = toTags({ title: "T", openGraph: { image: ["https://a.com/1.png", "https://a.com/2.png"] } }, T);
228 assert.equal(meta.filter((m) => m.name === "og:image").length, 2);
233 const tags = meta.toTags(
234 T,
235 { title: "T", openGraph: { image: ["https://a.com/1.png", "https://a.com/2.png"] } },
236 );
237 assert.equal(tags.meta.filter((m) => m.name === "og:image").length, 2);
229238 });
230239
231240 test("null field is skipped", () => {
232 const { meta } = toTags({ title: "T", openGraph: { image: null } }, T);
233 assert.ok(!meta.some((m) => m.name === "og:image"));
241 const tags = meta.toTags(T, { title: "T", openGraph: { image: null } });
242 assert.ok(!tags.meta.some((m) => m.name === "og:image"));
234243 });
235244});
236245
237246describe("template fallbacks", () => {
238247 test("template fields used when meta omits them", () => {
239 const template: Template = { base, publisher: "Acme", keywords: ["a", "b"] };
240 const { meta } = toTags({ title: "T" }, template);
241 assert.ok(meta.some((m) => m.name === "publisher" && m.content === "Acme"));
242 assert.ok(meta.some((m) => m.name === "keywords" && m.content === "a, b"));
248 const template: meta.Template = { base, publisher: "Acme", keywords: ["a", "b"] };
249 const tags = meta.toTags(template, { title: "T" });
250 assert.ok(tags.meta.some((m) => m.name === "publisher" && m.content === "Acme"));
251 assert.ok(tags.meta.some((m) => m.name === "keywords" && m.content === "a, b"));
243252 });
244253
245254 test("meta field overrides template, no duplicate", () => {
246 const template: Template = { base, publisher: "Acme" };
247 const { meta } = toTags({ title: "T", publisher: "Override" }, template);
248 assert.ok(meta.some((m) => m.name === "publisher" && m.content === "Override"));
249 assert.equal(meta.filter((m) => m.name === "publisher").length, 1);
255 const template: meta.Template = { base, publisher: "Acme" };
256 const tags = meta.toTags(template, { title: "T", publisher: "Override" });
257 assert.ok(tags.meta.some((m) => m.name === "publisher" && m.content === "Override"));
258 assert.equal(tags.meta.filter((m) => m.name === "publisher").length, 1);
250259 });
251260});
252261
253262describe("toHtml serialization", () => {
254263 test("attribute values with spaces are quoted", () => {
255 const out = toHtml({ title: "T", description: "hello world" }, T);
264 const out = meta.toHtml(T, { title: "T", description: "hello world" });
256265 assert.ok(out.includes(`content="hello world"`));
257266 });
258267
259268 test("attribute values without special chars are unquoted", () => {
260 const out = toHtml({ title: "T", publisher: "Acme" }, T);
269 const out = meta.toHtml(T, { title: "T", publisher: "Acme" });
261270 assert.ok(out.includes(`content=Acme`));
262271 });
263272
264273 test("attribute value with slash is quoted", () => {
265 const out = toHtml({ title: "T" }, T);
274 const out = meta.toHtml(T, { title: "T" });
266275 // viewport contains / — ensure it's quoted
267276 assert.ok(out.includes(`content="width=device-width`));
268277 });
269278
270279 test("description with double quotes is escaped", () => {
271 const out = toHtml({ title: "T", description: `say "hi"` }, T);
280 const out = meta.toHtml(T, { title: "T", description: `say "hi"` });
272281 assert.ok(!out.includes(`"say "hi"`));
273282 });
274283});
275284
276285describe("scripts", () => {
277286 test("inline script emits children in scripts array", () => {
278 const { scripts } = toTags({ title: "T", scripts: [{ content: "alert(1)" }] }, T);
287 const { scripts } = meta.toTags(T, { title: "T", scripts: [{ content: "alert(1)" }] });
279288 assert.deepEqual(scripts, [{ children: "alert(1)" }]);
280289 });
281290
282291 test("external script with src and boolean attrs", () => {
283 const { scripts } = toTags({ title: "T", scripts: [{ src: "/app.js", async: true, defer: false }] }, T);
292 const { scripts } = meta.toTags(T, { title: "T", scripts: [{ src: "/app.js", async: true, defer: false }] });
284293 assert.equal(scripts.length, 1);
294 assert.ok(scripts[0]);
285295 assert.equal(scripts[0].src, "/app.js");
286296 assert.equal(scripts[0].async, true);
287297 assert.equal(scripts[0].defer, undefined);
288298 });
289299
290300 test("toHtml renders inline script", () => {
291 const out = toHtml({ title: "T", scripts: [{ content: "console.log(1)" }] }, T);
301 const out = meta.toHtml(T, { title: "T", scripts: [{ content: "console.log(1)" }] });
292302 assert.ok(out.includes("<script>console.log(1)</script>"));
293303 });
294304
295305 test("toHtml renders external script with boolean attr", () => {
296 const out = toHtml({ title: "T", scripts: [{ src: "/app.js", async: true }] }, T);
306 const out = meta.toHtml(T, { title: "T", scripts: [{ src: "/app.js", async: true }] });
297307 assert.ok(out.includes("<script src="));
298308 assert.ok(out.includes(" async></script>"));
299309 });
300310
301311 test("falls back to template scripts", () => {
302 const { scripts } = toTags({ title: "T" }, { ...T, scripts: [{ content: "init()" }] });
312 const { scripts } = meta.toTags({ ...T, scripts: [{ content: "init()" }] }, { title: "T" });
303313 assert.deepEqual(scripts, [{ children: "init()" }]);
304314 });
305315});
306316
307317describe("styles", () => {
308318 test("inline style emits children in styles array", () => {
309 const { styles } = toTags({ title: "T", styles: [{ content: "body{margin:0}" }] }, T);
319 const { styles } = meta.toTags(T, { title: "T", styles: [{ content: "body{margin:0}" }] });
310320 assert.deepEqual(styles, [{ children: "body{margin:0}" }]);
311321 });
312322
313323 test("inline style with media", () => {
314 const { styles } = toTags({ title: "T", styles: [{ content: "p{color:blue}", media: "print" }] }, T);
324 const { styles } = meta.toTags(T, { title: "T", styles: [{ content: "p{color:blue}", media: "print" }] });
315325 assert.deepEqual(styles, [{ children: "p{color:blue}", media: "print" }]);
316326 });
317327
318328 test("external stylesheet goes into links", () => {
319 const { links, styles } = toTags({ title: "T", styles: [{ href: "/style.css" }] }, T);
329 const { links, styles } = meta.toTags(T, { title: "T", styles: [{ href: "/style.css" }] });
320330 assert.ok(links.some((l) => l.rel === "stylesheet" && l.href === "/style.css"));
321331 assert.equal(styles.length, 0);
322332 });
323333
324334 test("external stylesheet with media", () => {
325 const { links } = toTags({ title: "T", styles: [{ href: "/print.css", media: "print" }] }, T);
335 const { links } = meta.toTags(T, { title: "T", styles: [{ href: "/print.css", media: "print" }] });
326336 assert.ok(links.some((l) => l.rel === "stylesheet" && l.href === "/print.css" && l.media === "print"));
327337 });
328338
329339 test("toHtml renders inline style", () => {
330 const out = toHtml({ title: "T", styles: [{ content: "body{margin:0}" }] }, T);
340 const out = meta.toHtml(T, { title: "T", styles: [{ content: "body{margin:0}" }] });
331341 assert.ok(out.includes("<style>body{margin:0}</style>"));
332342 });
333343
334344 test("toHtml renders inline style with media", () => {
335 const out = toHtml({ title: "T", styles: [{ content: "p{color:blue}", media: "print" }] }, T);
345 const out = meta.toHtml(T, { title: "T", styles: [{ content: "p{color:blue}", media: "print" }] });
336346 assert.ok(out.includes("<style media=print>p{color:blue}</style>"));
337347 });
338348
339349 test("falls back to template styles", () => {
340 const { styles } = toTags({ title: "T" }, { ...T, styles: [{ content: "h1{color:red}" }] });
350 const { styles } = meta.toTags({ ...T, styles: [{ content: "h1{color:red}" }] }, { title: "T" });
341351 assert.deepEqual(styles, [{ children: "h1{color:red}" }]);
342352 });
343353});
344354
345355describe("extra", () => {
346356 test("tag=meta goes into meta array", () => {
347 const { meta } = toTags({ title: "T", extra: [{ tag: "meta", name: "verify", content: "abc" }] }, T);
348 assert.ok(meta.some((m) => m.name === "verify" && m.content === "abc"));
357 const tags = meta.toTags(T, { title: "T", extra: [{ tag: "meta", name: "verify", content: "abc" }] });
358 assert.ok(tags.meta.some((m) => m.name === "verify" && m.content === "abc"));
349359 });
350360
351361 test("tag=link goes into links array", () => {
352 const { links } = toTags({ title: "T", extra: [{ tag: "link", rel: "icon", href: "/favicon.ico" }] }, T);
362 const { links } = meta.toTags(T, { title: "T", extra: [{ tag: "link", rel: "icon", href: "/favicon.ico" }] });
353363 assert.ok(links.some((l) => l.rel === "icon" && l.href === "/favicon.ico"));
354364 });
355365
356366 test("tag=script goes into scripts array", () => {
357 const { scripts } = toTags({ title: "T", extra: [{ tag: "script", src: "/extra.js", async: true }] }, T);
367 const { scripts } = meta.toTags(T, { title: "T", extra: [{ tag: "script", src: "/extra.js", async: true }] });
358368 assert.equal(scripts.length, 1);
369 assert.ok(scripts[0]);
359370 assert.equal(scripts[0].src, "/extra.js");
360371 assert.equal(scripts[0].async, true);
361372 });
362373
363374 test("tag=style goes into styles array", () => {
364 const { styles } = toTags({ title: "T", extra: [{ tag: "style", children: "a{color:red}" }] }, T);
375 const { styles } = meta.toTags(T, { title: "T", extra: [{ tag: "style", children: "a{color:red}" }] });
365376 assert.deepEqual(styles, [{ children: "a{color:red}" }]);
366377 });
367378
368379 test("null/false attrs are filtered out", () => {
369 const { meta } = toTags({ title: "T", extra: [{ tag: "meta", name: "x", content: "y", bogus: null }] }, T);
370 const entry = meta.find((m) => m.name === "x")!;
380 const tags = meta.toTags(T, { title: "T", extra: [{ tag: "meta", name: "x", content: "y", bogus: null }] });
381 const entry = tags.meta.find((m) => m.name === "x")!;
371382 assert.equal(entry.bogus, undefined);
372383 });
373384
374385 test("falls back to template extra", () => {
375 const { meta } = toTags({ title: "T" }, { ...T, extra: [{ tag: "meta", name: "tmpl", content: "yes" }] });
376 assert.ok(meta.some((m) => m.name === "tmpl" && m.content === "yes"));
386 const tags = meta.toTags({ ...T, extra: [{ tag: "meta", name: "tmpl", content: "yes" }] }, { title: "T" });
387 assert.ok(tags.meta.some((m) => m.name === "tmpl" && m.content === "yes"));
388 });
389});
390
391// Typescript test for TanStack router compatibility.
392interface HtmlAttributes {
393 id?: string;
394 className?: string;
395 title?: string;
396 lang?: string;
397 dir?: string;
398 hidden?: boolean;
399 tabIndex?: number;
400 children?: unknown;
401}
402interface MetaAttributes extends HtmlAttributes {
403 name?: string;
404 content?: string;
405 charSet?: string;
406 httpEquiv?: string;
407 media?: string;
408}
409interface LinkAttributes extends HtmlAttributes {
410 rel?: string;
411 href?: string;
412 hrefLang?: string;
413 media?: string;
414 type?: string;
415 sizes?: string;
416 as?: string;
417}
418interface ScriptAttributes extends HtmlAttributes {
419 src?: string;
420 async?: boolean;
421 defer?: boolean;
422 type?: string;
423 noModule?: boolean;
424 integrity?: string;
425}
426interface StyleAttributes extends HtmlAttributes {
427 media?: string;
428 type?: string;
429}
430
431/** The `head()` return contract from TanStack Start, deps-free. */
432interface TanStackHead {
433 meta?: Array<MetaAttributes | undefined>;
434 links?: Array<LinkAttributes | undefined>;
435 scripts?: Array<ScriptAttributes | undefined>;
436 styles?: Array<StyleAttributes | undefined>;
437}
438
439// Type-level assertion. A compile error on this line means `toTags`' output is
440// no longer a valid TanStack `head()` payload.
441const _asHead: (tags: meta.Tags) => TanStackHead = (tags) => tags;
442
443describe("TanStack Router compatibility", () => {
444 test("toTags output is a valid head() payload", () => {
445 const tags = meta.toTags(
446 { base: new URL("https://example.com/") },
447 {
448 title: "Home",
449 description: "hi",
450 embed: { thumbnail: "/og.png" },
451 scripts: [{ src: "/a.js", async: true }, { content: "console.log(1)" }],
452 styles: [{ href: "/a.css" }, { content: "a{color:red}" }],
453 extra: [{ tag: "link", rel: "icon", href: "/favicon.ico" }],
454 },
455 );
456 const head: TanStackHead = _asHead(tags);
457 assert.deepEqual(Object.keys(head), ["meta", "links", "scripts", "styles"]);
377458 });
378459});
379460
380461import assert from "node:assert";
381462import { describe, test } from "vitest";
382import { type Template, toHtml, toTags } from "./meta.ts";
463import * as meta from "./meta.ts";
lib/meta.ts+6-10
......@@ -80,7 +80,7 @@ export interface AutoEmbed {
8080 /* add an image to the embed card. */
8181 thumbnail?: string | null;
8282 /** @default "banner", which applies twitter:card = "summary_large_image" */
83 thumbnailSize?: "banner" | "icon";
83 thumbnailSize?: "banner" | "icon" | null;
8484 /** @default null - no site title */
8585 siteTitle?: string | null;
8686}
......@@ -153,12 +153,8 @@ export interface Tags {
153153 styles: Array<Record<string, string>>;
154154}
155155
156// TODO: before publishing clover lib v5, ensure this is actually compatibile with TanStack Router
157// this type error will prevent publishing the library
158void (NaN as string);
159
160156/** convert a metadata definition into a structured list of tags, compatible with TanStack start */
161export function toTags(meta: Meta, template: Template): Tags {
157export function toTags(template: Template, meta: Meta): Tags {
162158 const { titleTemplate, base } = template;
163159 const resolve = (str: string) => new URL(str, base).href;
164160
......@@ -252,7 +248,7 @@ export function toTags(meta: Meta, template: Template): Tags {
252248
253249 for (const entry of meta.scripts ?? template.scripts ?? []) {
254250 if ("content" in entry) {
255 scriptTags.push({ children: entry.content });
251 scriptTags.push({ children: String(entry.content) });
256252 } else {
257253 const { src, ...rest } = entry;
258254 const tag: Record<string, string | boolean> = { src };
......@@ -265,7 +261,7 @@ export function toTags(meta: Meta, template: Template): Tags {
265261
266262 for (const entry of meta.styles ?? template.styles ?? []) {
267263 if ("content" in entry) {
268 const tag: Record<string, string> = { children: entry.content };
264 const tag: Record<string, string> = { children: String(entry.content) };
269265 if ("media" in entry && entry.media) tag.media = entry.media;
270266 styleTags.push(tag);
271267 } else {
......@@ -293,8 +289,8 @@ export function toTags(meta: Meta, template: Template): Tags {
293289}
294290
295291/* convert a metadata definition into HTML text. */
296export function toHtml(meta: Meta, template: Template): string {
297 const tags = toTags(meta, template);
292export function toHtml(template: Template, meta: Meta): string {
293 const tags = toTags(template, meta);
298294 const [{ title }, ...metaTags] = tags.meta as [{ title: string }, ...Record<string, string>[]];
299295 let out = `<title>${esc(title)}</title>`;
300296 for (const attrs of metaTags) {
lib/readme.changes.md+18-5
......@@ -7,23 +7,36 @@
77- `render` has been deleted because it was not being used anymore. sad day
88 because i will never get to prove how easy it is to write a stable Suspense
99 and PPR implementations in less than 300 lines each.
10 - deprecated html escaping methods moved to `string.escapeHtmlContent` and
11 `string.escapeHtmlAttr`
1012- `render/meta` is moved to `meta`, with a new API for rendering tags
11 - `meta.render` is renamed to `meta.toHtml`
13 - `meta.render` is renamed to `meta.toHtml`, arguments re-ordered
1214 - `meta.toTags` creates an object compatible with TanStack Router
1315 - the `extra` object now takes an array of React-like elements among some
1416 other supported forms.
15- `log` has recieved so many bug-fix changes that it is worth ensuring your
16 application edge cases still work as expected.
17- `log` and `progress` have recieved so many bug-fix changes that it is worth
18 ensuring your application edge cases still work as expected.
19- `string.formatByteSize` formats differently.
1720
1821### features
1922
20- `mime`'s database contains `.eot` for embedded opentype fonts.
21- `async.deferred`
23- new library `http`.
24- new library `math`.
25- new library `arrays`.
2226- `log`
2327 - many many many many rendering bug-fixes
2428 - `writeError` writes to stderr with proper logic.
2529 - buffering auto-flushes synchronously when it grows past 64k.
2630 - custom scopes
31- `string`
32 - `formatDurationLetters` spans years to seconds, plus infinity and negatives.
33- `mime`'s database contains `.eot` for embedded opentype fonts.
34- `async.deferred`.
35- `queue.setConcurrency`.
36- `subprocess.lookupProcessOnPort`.
37- `subprocess/ffmpeg` progress handles fractional framerates (progress bug).
38- `string/ansi` gains `clearToEndOfScreen`.
39- `progress` estimation, maxHeight, and timer bug fixes.
2740
2841## v4
2942
lib/subprocess.ts+10-5
......@@ -90,7 +90,7 @@ export async function spawnAndWait(
9090// }
9191// });
9292
93async function findInodeForPort(port) {
93async function findInodeForPort(port: number) {
9494 const hexPort = port.toString(16).toUpperCase().padStart(4, "0");
9595 for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
9696 try {
......@@ -104,7 +104,7 @@ async function findInodeForPort(port) {
104104 return null;
105105}
106106
107async function findPidForInode(inode) {
107async function findPidForInode(inode: string) {
108108 const target = `socket:[${inode}]`;
109109 for (const pid of await fs.readdir("/proc")) {
110110 if (!/^\d+$/.test(pid)) continue;
......@@ -154,7 +154,8 @@ async function commandFromPortWindows(port: number) {
154154 return { pid: Number(pid), command: cmdline };
155155}
156156
157export async function lookupProcessOnPort(port: number) {
157/** Racily searches for what process is taking up a given port. */
158export async function lookupProcessOnPort(port: number): Promise<ProcessLookup> {
158159 switch (os.platform()) {
159160 case "linux":
160161 return commandFromPortLinux(port);
......@@ -167,10 +168,14 @@ export async function lookupProcessOnPort(port: number) {
167168 }
168169}
169170
170// import * as process from "node:process";
171interface ProcessLookup {
172 pid: number;
173 command: string;
174}
175
171176import { Buffer } from "node:buffer";
172177import * as child_process from "node:child_process";
173178import * as events from "node:events";
174179import * as fs from "node:fs/promises";
180import * as os from "node:os";
175181import * as util from "node:util";
176import os from "os";
lib/testing.ts+8-6
......@@ -111,13 +111,15 @@ export class SyncPromise<T> implements Promise<T> {
111111 }
112112}
113113
114export interface FakeTimerEntry {
115 deadline: number;
116 resolve: () => void;
117 src: stack.Frame[];
118}
119
114120export class FakeTimers {
115121 time = 0;
116 entries: Array<{
117 deadline: number;
118 resolve: () => void;
119 src: stack.Frame[];
120 }> = [];
122 entries: Array<FakeTimerEntry> = [];
121123
122124 now: () => number = () => {
123125 return this.time;
......@@ -260,7 +262,7 @@ export class MockScreen implements Disposable, log.WidgetHost {
260262 ASSERT(this.timers.entries.length === 0);
261263 }
262264
263 expectWithoutConsume(ms: number) {
265 expectWithoutConsume(ms: number): FakeTimerEntry {
264266 const wait = UNWRAP(
265267 this.timers.next(),
266268 () => this.out.length > 0 ? "terminal i/o did not wait" : "no terminal i/o",
package-lock.json-420
......@@ -573,22 +573,6 @@
573573 "node": ">=18"
574574 }
575575 },
576 "node_modules/@esbuild/openharmony-arm64": {
577 "version": "0.27.7",
578 "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
579 "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
580 "cpu": [
581 "arm64"
582 ],
583 "license": "MIT",
584 "optional": true,
585 "os": [
586 "openharmony"
587 ],
588 "engines": {
589 "node": ">=18"
590 }
591 },
592576 "node_modules/@esbuild/sunos-x64": {
593577 "version": "0.25.5",
594578 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
......@@ -673,7 +657,6 @@
673657 "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.39.62.tgz",
674658 "integrity": "sha512-aBmWraqvVMLzi3oZkm5cEAVTv4qtLIVl9tkci6M+duSaEbKq4+n9FuNjYlOA0/4slRmOeAOuYyi2BenzgKmQNA==",
675659 "license": "MIT",
676 "peer": true,
677660 "dependencies": {
678661 "@luxass/strip-json-comments": "^1.4.0",
679662 "complain": "^1.6.1",
......@@ -1443,7 +1426,6 @@
14431426 "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
14441427 "hasInstallScript": true,
14451428 "license": "MIT",
1446 "peer": true,
14471429 "bin": {
14481430 "esbuild": "bin/esbuild"
14491431 },
......@@ -2114,7 +2096,6 @@
21142096 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
21152097 "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
21162098 "license": "MIT",
2117 "peer": true,
21182099 "engines": {
21192100 "node": ">=12"
21202101 },
......@@ -2528,406 +2509,6 @@
25282509 }
25292510 }
25302511 },
2531 "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
2532 "version": "0.27.7",
2533 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
2534 "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
2535 "cpu": [
2536 "ppc64"
2537 ],
2538 "license": "MIT",
2539 "optional": true,
2540 "os": [
2541 "aix"
2542 ],
2543 "engines": {
2544 "node": ">=18"
2545 }
2546 },
2547 "node_modules/vitest/node_modules/@esbuild/android-arm": {
2548 "version": "0.27.7",
2549 "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
2550 "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
2551 "cpu": [
2552 "arm"
2553 ],
2554 "license": "MIT",
2555 "optional": true,
2556 "os": [
2557 "android"
2558 ],
2559 "engines": {
2560 "node": ">=18"
2561 }
2562 },
2563 "node_modules/vitest/node_modules/@esbuild/android-arm64": {
2564 "version": "0.27.7",
2565 "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
2566 "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
2567 "cpu": [
2568 "arm64"
2569 ],
2570 "license": "MIT",
2571 "optional": true,
2572 "os": [
2573 "android"
2574 ],
2575 "engines": {
2576 "node": ">=18"
2577 }
2578 },
2579 "node_modules/vitest/node_modules/@esbuild/android-x64": {
2580 "version": "0.27.7",
2581 "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
2582 "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
2583 "cpu": [
2584 "x64"
2585 ],
2586 "license": "MIT",
2587 "optional": true,
2588 "os": [
2589 "android"
2590 ],
2591 "engines": {
2592 "node": ">=18"
2593 }
2594 },
2595 "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
2596 "version": "0.27.7",
2597 "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
2598 "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
2599 "cpu": [
2600 "arm64"
2601 ],
2602 "license": "MIT",
2603 "optional": true,
2604 "os": [
2605 "darwin"
2606 ],
2607 "engines": {
2608 "node": ">=18"
2609 }
2610 },
2611 "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
2612 "version": "0.27.7",
2613 "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
2614 "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
2615 "cpu": [
2616 "x64"
2617 ],
2618 "license": "MIT",
2619 "optional": true,
2620 "os": [
2621 "darwin"
2622 ],
2623 "engines": {
2624 "node": ">=18"
2625 }
2626 },
2627 "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
2628 "version": "0.27.7",
2629 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
2630 "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
2631 "cpu": [
2632 "arm64"
2633 ],
2634 "license": "MIT",
2635 "optional": true,
2636 "os": [
2637 "freebsd"
2638 ],
2639 "engines": {
2640 "node": ">=18"
2641 }
2642 },
2643 "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
2644 "version": "0.27.7",
2645 "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
2646 "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
2647 "cpu": [
2648 "x64"
2649 ],
2650 "license": "MIT",
2651 "optional": true,
2652 "os": [
2653 "freebsd"
2654 ],
2655 "engines": {
2656 "node": ">=18"
2657 }
2658 },
2659 "node_modules/vitest/node_modules/@esbuild/linux-arm": {
2660 "version": "0.27.7",
2661 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
2662 "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
2663 "cpu": [
2664 "arm"
2665 ],
2666 "license": "MIT",
2667 "optional": true,
2668 "os": [
2669 "linux"
2670 ],
2671 "engines": {
2672 "node": ">=18"
2673 }
2674 },
2675 "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
2676 "version": "0.27.7",
2677 "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
2678 "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
2679 "cpu": [
2680 "arm64"
2681 ],
2682 "license": "MIT",
2683 "optional": true,
2684 "os": [
2685 "linux"
2686 ],
2687 "engines": {
2688 "node": ">=18"
2689 }
2690 },
2691 "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
2692 "version": "0.27.7",
2693 "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
2694 "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
2695 "cpu": [
2696 "ia32"
2697 ],
2698 "license": "MIT",
2699 "optional": true,
2700 "os": [
2701 "linux"
2702 ],
2703 "engines": {
2704 "node": ">=18"
2705 }
2706 },
2707 "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
2708 "version": "0.27.7",
2709 "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
2710 "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
2711 "cpu": [
2712 "loong64"
2713 ],
2714 "license": "MIT",
2715 "optional": true,
2716 "os": [
2717 "linux"
2718 ],
2719 "engines": {
2720 "node": ">=18"
2721 }
2722 },
2723 "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
2724 "version": "0.27.7",
2725 "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
2726 "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
2727 "cpu": [
2728 "mips64el"
2729 ],
2730 "license": "MIT",
2731 "optional": true,
2732 "os": [
2733 "linux"
2734 ],
2735 "engines": {
2736 "node": ">=18"
2737 }
2738 },
2739 "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
2740 "version": "0.27.7",
2741 "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
2742 "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
2743 "cpu": [
2744 "ppc64"
2745 ],
2746 "license": "MIT",
2747 "optional": true,
2748 "os": [
2749 "linux"
2750 ],
2751 "engines": {
2752 "node": ">=18"
2753 }
2754 },
2755 "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
2756 "version": "0.27.7",
2757 "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
2758 "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
2759 "cpu": [
2760 "riscv64"
2761 ],
2762 "license": "MIT",
2763 "optional": true,
2764 "os": [
2765 "linux"
2766 ],
2767 "engines": {
2768 "node": ">=18"
2769 }
2770 },
2771 "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
2772 "version": "0.27.7",
2773 "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
2774 "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
2775 "cpu": [
2776 "s390x"
2777 ],
2778 "license": "MIT",
2779 "optional": true,
2780 "os": [
2781 "linux"
2782 ],
2783 "engines": {
2784 "node": ">=18"
2785 }
2786 },
2787 "node_modules/vitest/node_modules/@esbuild/linux-x64": {
2788 "version": "0.27.7",
2789 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
2790 "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
2791 "cpu": [
2792 "x64"
2793 ],
2794 "license": "MIT",
2795 "optional": true,
2796 "os": [
2797 "linux"
2798 ],
2799 "engines": {
2800 "node": ">=18"
2801 }
2802 },
2803 "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": {
2804 "version": "0.27.7",
2805 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
2806 "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
2807 "cpu": [
2808 "arm64"
2809 ],
2810 "license": "MIT",
2811 "optional": true,
2812 "os": [
2813 "netbsd"
2814 ],
2815 "engines": {
2816 "node": ">=18"
2817 }
2818 },
2819 "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
2820 "version": "0.27.7",
2821 "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
2822 "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
2823 "cpu": [
2824 "x64"
2825 ],
2826 "license": "MIT",
2827 "optional": true,
2828 "os": [
2829 "netbsd"
2830 ],
2831 "engines": {
2832 "node": ">=18"
2833 }
2834 },
2835 "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": {
2836 "version": "0.27.7",
2837 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
2838 "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
2839 "cpu": [
2840 "arm64"
2841 ],
2842 "license": "MIT",
2843 "optional": true,
2844 "os": [
2845 "openbsd"
2846 ],
2847 "engines": {
2848 "node": ">=18"
2849 }
2850 },
2851 "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
2852 "version": "0.27.7",
2853 "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
2854 "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
2855 "cpu": [
2856 "x64"
2857 ],
2858 "license": "MIT",
2859 "optional": true,
2860 "os": [
2861 "openbsd"
2862 ],
2863 "engines": {
2864 "node": ">=18"
2865 }
2866 },
2867 "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
2868 "version": "0.27.7",
2869 "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
2870 "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
2871 "cpu": [
2872 "x64"
2873 ],
2874 "license": "MIT",
2875 "optional": true,
2876 "os": [
2877 "sunos"
2878 ],
2879 "engines": {
2880 "node": ">=18"
2881 }
2882 },
2883 "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
2884 "version": "0.27.7",
2885 "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
2886 "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
2887 "cpu": [
2888 "arm64"
2889 ],
2890 "license": "MIT",
2891 "optional": true,
2892 "os": [
2893 "win32"
2894 ],
2895 "engines": {
2896 "node": ">=18"
2897 }
2898 },
2899 "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
2900 "version": "0.27.7",
2901 "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
2902 "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
2903 "cpu": [
2904 "ia32"
2905 ],
2906 "license": "MIT",
2907 "optional": true,
2908 "os": [
2909 "win32"
2910 ],
2911 "engines": {
2912 "node": ">=18"
2913 }
2914 },
2915 "node_modules/vitest/node_modules/@esbuild/win32-x64": {
2916 "version": "0.27.7",
2917 "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
2918 "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
2919 "cpu": [
2920 "x64"
2921 ],
2922 "license": "MIT",
2923 "optional": true,
2924 "os": [
2925 "win32"
2926 ],
2927 "engines": {
2928 "node": ">=18"
2929 }
2930 },
29312512 "node_modules/vitest/node_modules/@vitest/mocker": {
29322513 "version": "4.1.2",
29332514 "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz",
......@@ -2973,7 +2554,6 @@
29732554 "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
29742555 "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
29752556 "license": "MIT",
2976 "peer": true,
29772557 "dependencies": {
29782558 "lightningcss": "^1.32.0",
29792559 "picomatch": "^4.0.4",
src/pages/index.marko+1-4
......@@ -22,10 +22,7 @@ export const meta = {
2222 </p>
2323 <h2>art</h2>
2424 <ul class="section-list">
25 <li>
26 ongoing:
27 <a href="/hotewig-reanimated">hotewig reanimated</a>
28 </li>
25 <li>ongoing: <a href="/hotewig-reanimated">hotewig reanimated</a></li>
2926 <li>
3027 video collab:
3128 <a href="/history-of-japan-reanimated">history of japan reanimated</a>