From 854f4af39370542f0061888f74a85439a8549167 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 21:43:15 +0200 Subject: [PATCH] Added reader and diagnostics --- src/lib/content/load.ts | 45 +++++++++++++++++++++++++++++++++++ src/lib/content/reader.ts | 5 ++++ src/lib/content/types.ts | 18 ++++++++++++++ src/lib/diagnostics/index.ts | 22 +++++++++++++++++ src/lib/diagnostics/report.ts | 23 ++++++++++++++++++ src/lib/diagnostics/types.ts | 14 +++++++++++ 6 files changed, 127 insertions(+) create mode 100644 src/lib/content/load.ts create mode 100644 src/lib/content/reader.ts create mode 100644 src/lib/content/types.ts create mode 100644 src/lib/diagnostics/index.ts create mode 100644 src/lib/diagnostics/report.ts create mode 100644 src/lib/diagnostics/types.ts diff --git a/src/lib/content/load.ts b/src/lib/content/load.ts new file mode 100644 index 0000000..9e41e53 --- /dev/null +++ b/src/lib/content/load.ts @@ -0,0 +1,45 @@ +import { ARTICLES, PAGES, TAGS } from '../constants/collections' +import { createDiagnostics } from '../diagnostics' +import type { Diagnostics } from '../diagnostics/types' +import { createContentReader } from './reader' +import type { ArticleEntry, Content, PageEntry, TagEntry } from './types' + +interface CollectionReader { + list(): Promise + read(id: string): Promise +} + +function reason(error: unknown): string { + const [first, ...rest] = (error instanceof Error ? error.message : String(error)).split('\n') + return first.endsWith(':') && rest.length ? rest.join('; ') : first +} + +async function readAll( + reader: CollectionReader, + collection: string, + problems: Diagnostics, +): Promise> { + const entries = new Map() + await Promise.all( + (await reader.list()).map(async (id) => { + try { + const entry = await reader.read(id) + if (entry) entries.set(id, entry) + } catch (error) { + problems.error(collection, id, reason(error)) + } + }), + ) + return entries +} + +export async function loadContent(): Promise { + const { collections } = createContentReader() + const problems = createDiagnostics() + const [articles, pages, tags] = await Promise.all([ + readAll(collections.articles, ARTICLES, problems), + readAll(collections.pages, PAGES, problems), + readAll(collections.tags, TAGS, problems), + ]) + return { articles, pages, tags, diagnostics: problems.list } +} diff --git a/src/lib/content/reader.ts b/src/lib/content/reader.ts new file mode 100644 index 0000000..dc4cdd4 --- /dev/null +++ b/src/lib/content/reader.ts @@ -0,0 +1,5 @@ +import { createReader } from '@keystatic/core/reader' + +import config from '~/keystatic.config' + +export const createContentReader = () => createReader('.', config) diff --git a/src/lib/content/types.ts b/src/lib/content/types.ts new file mode 100644 index 0000000..76fb8ab --- /dev/null +++ b/src/lib/content/types.ts @@ -0,0 +1,18 @@ +import type { Entry } from '@keystatic/core/reader' + +import type config from '~/keystatic.config' + +import type { Diagnostic } from '../diagnostics/types' + +type Collections = typeof config.collections + +export type ArticleEntry = Entry +export type PageEntry = Entry +export type TagEntry = Entry + +export interface Content { + articles: Map + pages: Map + tags: Map + diagnostics: Diagnostic[] +} diff --git a/src/lib/diagnostics/index.ts b/src/lib/diagnostics/index.ts new file mode 100644 index 0000000..7a11c53 --- /dev/null +++ b/src/lib/diagnostics/index.ts @@ -0,0 +1,22 @@ +import type { Diagnostic, Diagnostics } from './types' + +export function createDiagnostics(): Diagnostics { + const list: Diagnostic[] = [] + return { + list, + error: (collection, id, message) => + list.push({ + level: 'error', + collection, + id, + message, + }), + warning: (collection, id, message) => + list.push({ + level: 'warning', + collection, + id, + message, + }), + } +} diff --git a/src/lib/diagnostics/report.ts b/src/lib/diagnostics/report.ts new file mode 100644 index 0000000..2047bdc --- /dev/null +++ b/src/lib/diagnostics/report.ts @@ -0,0 +1,23 @@ +import type { Diagnostic } from './types' + +const SYMBOL = { + error: '✖', + warning: '▲', +} as const + +const byLevelThenEntry = (a: Diagnostic, b: Diagnostic) => + (a.level === b.level ? 0 : a.level === 'error' ? -1 : 1) || + a.collection.localeCompare(b.collection) || + (a.id ?? '').localeCompare(b.id ?? '') + +export function formatDiagnostics(list: Diagnostic[]): string { + return [...list] + .sort(byLevelThenEntry) + .map((d) => `${SYMBOL[d.level]} ${d.collection}/${d.id ?? '*'} ${d.message}`) + .join('\n') +} + +export function assertNoErrors(list: Diagnostic[]): void { + const errors = list.filter((d) => d.level === 'error').length + if (errors) throw new Error(`Content has ${errors} error(s):\n${formatDiagnostics(list)}`) +} diff --git a/src/lib/diagnostics/types.ts b/src/lib/diagnostics/types.ts new file mode 100644 index 0000000..55645e0 --- /dev/null +++ b/src/lib/diagnostics/types.ts @@ -0,0 +1,14 @@ +export type Level = 'error' | 'warning' + +export interface Diagnostic { + level: Level + collection: string + id: string | null + message: string +} + +export interface Diagnostics { + list: Diagnostic[] + error(collection: string, id: string | null, message: string): void + warning(collection: string, id: string | null, message: string): void +}