Files
buzz-sheet/lib/blocks/registry.ts
T

166 lines
4.5 KiB
TypeScript

import type { z } from "zod";
import {
blockSchemas,
responsiveLayoutSchema,
type KnownBlockType,
} from "./schemas";
export interface SerializedBlock {
id: string;
type: string;
schemaVersion: number;
config: unknown;
responsiveLayout?: unknown;
sortOrder: number;
}
export interface ValidBlock extends SerializedBlock {
type: KnownBlockType;
schemaVersion: 1;
config: Record<string, unknown>;
responsiveLayout: Record<string, unknown>;
}
export interface UnsupportedBlock {
kind: "unsupported";
block: SerializedBlock;
reason: "unknown-type" | "future-version";
}
export interface InvalidBlock {
kind: "invalid";
block: SerializedBlock;
issues: z.core.$ZodIssue[];
}
export interface ReadyBlock {
kind: "ready";
block: ValidBlock;
}
export type BlockResolution = ReadyBlock | InvalidBlock | UnsupportedBlock;
type Migration = (legacyConfig: unknown) => unknown;
interface BlockDefinition {
currentVersion: 1;
schema: z.ZodType;
migrations: Record<number, Migration>;
}
const migrateLegacyConfig: Migration = (legacyConfig) => legacyConfig;
function defineBlock(schema: z.ZodType): BlockDefinition {
return {
currentVersion: 1,
schema,
// Version zero was the pre-release shape. Every registered block gets an
// explicit step so future migrations remain sequential and testable.
migrations: { 0: migrateLegacyConfig },
};
}
export const blockRegistry = {
profile: defineBlock(blockSchemas.profile),
"rich-text": defineBlock(blockSchemas["rich-text"]),
heading: defineBlock(blockSchemas.heading),
callout: defineBlock(blockSchemas.callout),
"stat-cards": defineBlock(blockSchemas["stat-cards"]),
"key-value": defineBlock(blockSchemas["key-value"]),
ranking: defineBlock(blockSchemas.ranking),
"team-composition": defineBlock(blockSchemas["team-composition"]),
rotation: defineBlock(blockSchemas.rotation),
table: defineBlock(blockSchemas.table),
"comparison-chart": defineBlock(blockSchemas["comparison-chart"]),
image: defineBlock(blockSchemas.image),
gallery: defineBlock(blockSchemas.gallery),
tabs: defineBlock(blockSchemas.tabs),
accordion: defineBlock(blockSchemas.accordion),
section: defineBlock(blockSchemas.section),
columns: defineBlock(blockSchemas.columns),
links: defineBlock(blockSchemas.links),
separator: defineBlock(blockSchemas.separator),
} satisfies Record<KnownBlockType, BlockDefinition>;
export function isKnownBlockType(type: string): type is KnownBlockType {
return Object.hasOwn(blockRegistry, type);
}
export function resolveBlock(block: SerializedBlock): BlockResolution {
if (!isKnownBlockType(block.type)) {
return { kind: "unsupported", block, reason: "unknown-type" };
}
const definition = blockRegistry[block.type];
if (block.schemaVersion > definition.currentVersion) {
return { kind: "unsupported", block, reason: "future-version" };
}
let config = block.config;
for (
let version = Math.max(0, block.schemaVersion);
version < definition.currentVersion;
version += 1
) {
const migration = definition.migrations[version];
if (!migration) {
return { kind: "unsupported", block, reason: "future-version" };
}
config = migration(config);
}
const configResult = definition.schema.safeParse(config);
const layoutResult = responsiveLayoutSchema.safeParse(
block.responsiveLayout ?? {},
);
if (!configResult.success || !layoutResult.success) {
return {
kind: "invalid",
block,
issues: [
...(configResult.success ? [] : configResult.error.issues),
...(layoutResult.success ? [] : layoutResult.error.issues),
],
};
}
return {
kind: "ready",
block: {
...block,
type: block.type,
schemaVersion: 1,
config: configResult.data as Record<string, unknown>,
responsiveLayout: layoutResult.data,
},
};
}
export function validatePageBlocks(
blocks: readonly SerializedBlock[],
): BlockResolution[] {
const ids = new Set<string>();
return [...blocks]
.sort((left, right) => left.sortOrder - right.sortOrder)
.map((block) => {
if (ids.has(block.id)) {
return {
kind: "invalid",
block,
issues: [
{
code: "custom",
path: ["id"],
message: `Duplicate stable block id: ${block.id}`,
input: block.id,
},
],
} satisfies InvalidBlock;
}
ids.add(block.id);
return resolveBlock(block);
});
}