Added reader and diagnostics

This commit is contained in:
2026-09-21 21:43:15 +02:00
parent f82d631ba6
commit 854f4af393
6 changed files with 127 additions and 0 deletions

45
src/lib/content/load.ts Normal file
View File

@@ -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<T> {
list(): Promise<string[]>
read(id: string): Promise<T | null>
}
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<T>(
reader: CollectionReader<T>,
collection: string,
problems: Diagnostics,
): Promise<Map<string, T>> {
const entries = new Map<string, T>()
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<Content> {
const { collections } = createContentReader()
const problems = createDiagnostics()
const [articles, pages, tags] = await Promise.all([
readAll<ArticleEntry>(collections.articles, ARTICLES, problems),
readAll<PageEntry>(collections.pages, PAGES, problems),
readAll<TagEntry>(collections.tags, TAGS, problems),
])
return { articles, pages, tags, diagnostics: problems.list }
}

View File

@@ -0,0 +1,5 @@
import { createReader } from '@keystatic/core/reader'
import config from '~/keystatic.config'
export const createContentReader = () => createReader('.', config)

18
src/lib/content/types.ts Normal file
View File

@@ -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<Collections['articles']>
export type PageEntry = Entry<Collections['pages']>
export type TagEntry = Entry<Collections['tags']>
export interface Content {
articles: Map<string, ArticleEntry>
pages: Map<string, PageEntry>
tags: Map<string, TagEntry>
diagnostics: Diagnostic[]
}

View File

@@ -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,
}),
}
}

View File

@@ -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)}`)
}

View File

@@ -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
}