feat(builder): add dynamic blocks email and dnd packages
Quality / quality (ubuntu-latest) (push) Failing after 11m26s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-03 14:00:41 +05:30
1 parent 06433e4ac4
commit 0bd2a6860d
37 files changed
+3060 -1

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
import type { BlockRegistry, ReadonlyBlockRegistry } from "./registry.ts";
import type { DocumentKindDefinition } from "./kinds.ts";
import {
DEFAULT_LIMITS,
type BlockDocument,
type BlockNode,
type Diagnostic,
type Result,
} from "./types.ts";
const byteLength = (value: string): number => new TextEncoder().encode(value).byteLength;
export function walkBlocks(
root: BlockNode,
visitor: (node: BlockNode, depth: number) => void,
): void {
const visit = (node: BlockNode, depth: number) => {
visitor(node, depth);
for (const slot of Object.keys(node.slots).sort())
for (const child of node.slots[slot] ?? []) visit(child, depth + 1);
};
visit(root, 0);
}
export function findBlock(root: BlockNode, id: string): BlockNode | undefined {
let found: BlockNode | undefined;
walkBlocks(root, (node) => {
if (node.id === id) found = node;
});
return found;
}
export function validateDocument(
document: BlockDocument,
kind: DocumentKindDefinition,
registry: ReadonlyBlockRegistry,
): Result<BlockDocument> {
const errors: Diagnostic[] = [];
const limits = kind.limits ?? DEFAULT_LIMITS;
let encoded = "";
try {
encoded = JSON.stringify(document);
} catch {
errors.push({
code: "document.json",
message: "Document must be JSON-serializable.",
severity: "error",
});
}
if (byteLength(encoded) > limits.maxBytes)
errors.push({
code: "document.size",
message: `Document exceeds ${limits.maxBytes} bytes.`,
severity: "error",
});
if (document.kind !== kind.kind)
errors.push({
code: "document.kind",
message: `Expected document kind ${kind.kind}.`,
severity: "error",
});
if (!kind.rootTypes.includes(document.root.type))
errors.push({
code: "document.root",
message: `Block ${document.root.type} cannot be the root.`,
severity: "error",
nodeId: document.root.id,
});
const meta = kind.metadata.parse(document.meta, "meta");
if (!meta.ok) errors.push(...meta.errors);
const ids = new Set<string>();
let count = 0;
walkBlocks(document.root, (node, depth) => {
count++;
if (!node.id || ids.has(node.id))
errors.push({
code: "document.id",
message: `Duplicate or empty block id ${node.id}.`,
severity: "error",
nodeId: node.id,
});
ids.add(node.id);
if (depth > limits.maxDepth)
errors.push({
code: "document.depth",
message: `Document exceeds depth ${limits.maxDepth}.`,
severity: "error",
nodeId: node.id,
});
const definition = registry.get(node.type);
if (!definition) {
errors.push({
code: "document.unknown-block",
message: `Unknown block type ${node.type}.`,
severity: "error",
nodeId: node.id,
});
return;
}
if (node.version !== definition.manifest.version)
errors.push({
code: "document.block-version",
message: `${node.type} requires migration to version ${definition.manifest.version}.`,
severity: "error",
nodeId: node.id,
});
const props = definition.manifest.props.parse(node.props, `blocks.${node.id}.props`);
if (!props.ok) errors.push(...props.errors.map((error) => ({ ...error, nodeId: node.id })));
const styles = definition.manifest.styles.parse(node.styles, `blocks.${node.id}.styles`);
if (!styles.ok) errors.push(...styles.errors.map((error) => ({ ...error, nodeId: node.id })));
for (const slotName of Object.keys(node.slots))
if (!(slotName in definition.manifest.slots))
errors.push({
code: "document.slot",
message: `Unknown slot ${slotName} on ${node.type}.`,
severity: "error",
nodeId: node.id,
});
for (const [slotName, slot] of Object.entries(definition.manifest.slots)) {
const children = node.slots[slotName] ?? [];
const maximum = Math.min(
slot.maxChildren ?? limits.maxChildrenPerSlot,
limits.maxChildrenPerSlot,
);
if (children.length < (slot.minChildren ?? 0) || children.length > maximum)
errors.push({
code: "document.cardinality",
message: `Slot ${slotName} has invalid child count.`,
severity: "error",
nodeId: node.id,
});
for (const child of children) {
const childDefinition = registry.get(child.type);
const acceptedType = !slot.accepts?.types || slot.accepts.types.includes(child.type);
const acceptedCapabilities =
!slot.accepts?.capabilities ||
slot.accepts.capabilities.every((capability) =>
childDefinition?.manifest.capabilities?.includes(capability),
);
if (!acceptedType || !acceptedCapabilities)
errors.push({
code: "document.slot-accept",
message: `${child.type} is not accepted by ${node.type}.${slotName}.`,
severity: "error",
nodeId: child.id,
});
}
}
errors.push(...(definition.validate?.(node) ?? []));
});
if (count > limits.maxNodes)
errors.push({
code: "document.nodes",
message: `Document exceeds ${limits.maxNodes} nodes.`,
severity: "error",
});
return errors.length ? { ok: false, errors } : { ok: true, value: document };
}
export function registryForKind(kind: DocumentKindDefinition): BlockRegistry {
const registry = createRegistry(kind.blocks);
return registry;
}
import { createRegistry } from "./registry.ts";