collection
A namespace of files as a collection of typed entries: each file's data (a Markdown file's frontmatter, or a JSON or
YAML file's content, as format says) checked against schema, any Standard Schema
(Zod, Valibot, ArkType). Entries are read when asked for, and in sort's order (by name otherwise).
Its files are a namespace imported whole (import * as files from "#codebase/blog/posts"), or any record of texts.
A namespace of MDX pages (.mdx) is a collection too: each entry's Content is its page, a component
(<post.Content />), and its data is its frontmatter.
import { z } from "zod";
const posts = collection(
{ hello: "---\ntitle: Hello\ndate: 2026-11-07\n---\nHi there.\n" },
{
schema: z.object({ title: z.string(), date: z.iso.date() }),
sort: (a, b) => b.data.date.localeCompare(a.data.date),
},
);
posts.all()[0]!.data.title; // "Hello"
posts.check("bad", "---\ntitle: 3\n---\n"); // ["title: Invalid input: expected string, received number", "date: Invalid input: expected string, received undefined"]
Hello
⧩ [ 'title: Invalid input: expected string, received number', 'date: Invalid input: expected string, received undefined' ]
A namespace's files are the namespace's as it is: one added, changed or removed changes the collection. Saving a file that
doesn't fit a collection of its namespace is refused, saying why. A schema that checks asynchronously (Zod's async
refinements, say) is waited for as files are saved; await posts.load() checks every entry once before they're read.
import { frontmatter } from "#codebase/tcmts/content"; import type { Collection, ContentOf, Entry, Output, Page, StandardResult, StandardSchema, } from "#codebase/tcmts/content"; import { parse as parseYaml } from "yaml"; export function collection<S extends StandardSchema, F extends string | Page = string>( files: Readonly<Record<string, F>>, options: { schema: S; format?: "markdown" | "json" | "yaml"; sort?: (a: Entry<Output<S>, ContentOf<F>>, b: Entry<Output<S>, ContentOf<F>>) => number; }, ): Collection<Output<S>, ContentOf<F>> { const { schema, format = "markdown", sort } = options; type Read = | { entry: Entry<Output<S>, ContentOf<F>>; problems?: undefined } | { entry?: undefined; problems: string[] }; // an entry as its schema makes it, or what's wrong with it: at once, or, a schema that checks asynchronously, later const read = (name: string, file: string | Page): Read | Promise<Read> => { let data: unknown; let body = ""; const text = typeof file === "string" ? file : file.text; try { if (format === "json") data = JSON.parse(text); else if (format === "yaml") data = parseYaml(text); else ({ data, body } = frontmatter(text)); } catch (e) { return { problems: [(e as Error).message.split("\n")[0]!] }; } const made = (r: StandardResult<unknown>): Read => { if (r.issues) return { problems: r.issues.map((i) => { const at = (i.path ?? []) .map((p) => String(typeof p === "object" ? p.key : p)) .join("."); return at ? `${at}: ${i.message}` : i.message; }), }; const Content = (typeof file === "string" ? undefined : file) as ContentOf<F>; return { entry: { name, data: r.value as Output<S>, body, Content } }; }; const r = schema["~standard"].validate(data); return r instanceof Promise ? r.then(made) : made(r); }; const seen = new Map<string, Read>(); const entry = (name: string): Read => { if (!seen.has(name)) { const r = read(name, files[name]!); if (r instanceof Promise) { r.catch(() => {}); throw new Error( "this collection's schema checks asynchronously: `await` its `load()` once before reading its entries", ); } seen.set(name, r); } return seen.get(name)!; }; const wrong = (name: string, problems: string[]) => `${name}: ${problems.join("; ")}`; const it: Collection<Output<S>, ContentOf<F>> = { all() { const names = Object.keys(files).sort(); const read = names.map(entry); const problems = names.flatMap((n, i) => read[i]!.problems ? [wrong(n, read[i]!.problems!)] : [], ); if (problems.length) throw new Error( `entries that don't fit the collection's schema:\n ${problems.join("\n ")}`, ); const all = read.map((r) => r.entry!); return sort ? all.sort(sort) : all; }, get(name) { if (!Object.hasOwn(files, name)) return undefined; const r = entry(name); if (r.problems) throw new Error( `an entry that doesn't fit the collection's schema: ${wrong(name, r.problems)}`, ); return r.entry; }, check(name, text) { const r = read(name, text); if (r instanceof Promise) { r.catch(() => {}); throw new Error( "this collection's schema checks asynchronously: tcm checks each file saved into its namespace", ); } return r.problems ?? []; }, async load() { for (const name of Object.keys(files)) if (!seen.has(name)) seen.set(name, await read(name, files[name]!)); }, }; // (how tcm knows it, to check a file saved into its namespace: a promise of it, for a schema that checks // asynchronously) Object.defineProperty(it, Symbol.for("tcm.collection"), { value: (name: string, text: string) => { const r = read(name, text); return r instanceof Promise ? r.then((x) => x.problems ?? []) : (r.problems ?? []); }, }); return it; }
uses
- tcmts.content.Collection
- tcmts.content.ContentOf
- tcmts.content.Entry
- tcmts.content.Output
- tcmts.content.Page
- tcmts.content.StandardResult
- tcmts.content.StandardSchema
- tcmts.content.frontmatter