Skip to content

feat: extract json-renderer core - #30

Merged
pyramation merged 1 commit into
mainfrom
feat/json-renderer
Aug 22, 2026
Merged

feat: extract json-renderer core#30
pyramation merged 1 commit into
mainfrom
feat/json-renderer

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

Extracts the framework-agnostic JSON UI document core out of blocks-schema/blocks-renderer into a new package packages/json-renderer (json-renderer@0.0.1, publishable but not published here — Lerna bump publishes it, and it must publish before blocks-schema). Per https://github.com/constructive-io/constructive-planning/issues/1822, the core owns the document format; Constructive's vocabulary and React stay in the layers above.

json-renderer depends only on zod. No React, no shadcn, no @constructive-io/*, and no Constructive block list.

What moved (implementation moved, not copied — the old bodies are deleted):

core module previously
node.tsDocumentNode<TType, TProps>, walkNodes, findNodeByKey, collectNodes, mapNodes, collectNodeTypes, createNode blocks-schema/node
envelope.tsDocumentEnvelope<TNode, TKind, TVersion>, createEnvelope, isDocumentEnvelope, EnvelopeKind blocks-schema/envelope
constraints.tsNodeConstraints, validateValue blocks-schema/validation
fields.tscollectFieldNames/DefaultValues/FieldConstraints(node, predicate) blocks-schema/node (hardcoded isWidgetNode)
compose.tscomposeEnvelope, composeNodeTree, mergeNodeTrees, mergeEnvelopes blocks-schema/compose
bindings.tsreadPath, resolveBinding, resolveNodeProps, composeScope blocks-renderer/bindings
registry.tscomposeRegistry, resolveHandler, registeredTypes, missingTypes blocks-renderer/registry
zod.ts / json-schema.tscreateNodeSchema, createDocumentSchema, parseEnvelope, toJsonSchema blocks-schema/zod, blocks-schema/json-schema
adapter.ts — the adapter contract (new)

Genericity is by parameter, not by fixed list: the vocabulary is TType extends string, field-ness is a predicate you pass in, and even the composition keywords are configurable, so a host that spells indirection include/from composes the same way:

composeNodeTree(tree, {
  fragments: { body: leaf },
  vocabulary: { fragmentNodeType: 'include', fragmentRefProp: 'from' },
});
// default vocabulary is Constructive's: Fragment/ref, Slot/name

The adapter contract (json-renderer/adapter) states what a renderer must provide, so blocks-renderer is one adapter rather than the renderer:

interface RendererAdapter<THandler, TOutput, TDocument, TContext> {
  readonly name: string;
  resolve(type: string, ctx: TContext): NodeResolution<THandler>;   // registry: type -> handler
  resolveProps(node, ctx: TContext): NodeProps;                     // binding scope access
  renderNode(node, ctx: TContext): TOutput;
  renderUnknown(node, ctx: TContext): TOutput;                      // unknown-node handling
  renderDocument(doc: TDocument, ctx: TContext): TOutput;
}

resolveNode() returns { status: 'resolved', handler } | { status: 'unknown' } and UnknownNodePolicy = 'fallback' | 'omit' | 'throw' names the choice instead of burying it in a component. blocks-renderer now exports reactAdapter (THandler = BlockComponent, TOutput = ReactNode, unknown → UnknownBlock); the React components remain the ergonomic entry point, the adapter object states the contract they satisfy. A test in json-renderer implements a string-output adapter with no framework at all, proving the contract isn't React-shaped.

Backwards compatibility. No renames, no removals: every blocks-schema and blocks-renderer export keeps its name and signature and now delegates instead of duplicating, e.g.

// blocks-schema
export interface UINode extends DocumentNode<UINodeType, UINodeProps> { children: UINode[]; ... }
export interface UIDocument extends DocumentEnvelope<UINode, 'UISchema', '1.0'> { ... }
export const collectFieldNames = (node: UINode) => collectNodeFieldNames(node, isWidgetNode);
export const validateField = (v, c, r) => validateValue(v, c, r);

WIDGET_NODE_TYPES / CONTAINER_NODE_TYPES / BLOCK_NODE_TYPES, the node predicates, uiNodeSchema & friends, composeDocument, parseDocument, toDocumentJsonSchema, BlockRenderer, DocumentRenderer, composeRegistry, resolveBlock, useBlockField — all unchanged for json-schema-to-blocks, meta-to-blocks, and flow-to-blocks (their tests pass untouched). Additions only: blocks-schema gains blocks-schema/core re-exporting the generic types plus mergeDocuments/mergeNodes; blocks-renderer gains reactAdapter, composeScope, missingTypes.

API-shape decisions worth reviewing:

  • createEnvelope(kind, page, options) takes the kind as a value ({ documentType, formatVersion }) so a specialization pins its discriminator once — blocks-schema exports it as UI_DOCUMENT_KIND and createDocument is a thin wrapper.
  • RenderContext is split: RenderContextBase holds what the core needs (document, registry, scope, unknownNodePolicy), and RenderContext<THandler, TDocument, TAction> adds onAction. That's what lets RendererContextValue extend it with UIAction and the form's values/errors without the core knowing about either.
  • Field collection takes a predicate rather than a props.name convention, because "which nodes are fields" is vocabulary, not format.
  • blocks-schema's build script changed from makage build to makage clean && makage build-ts && makage assets (same form blocks-renderer already uses) because makage build's check-publish step rejects the workspace:^ dependency on json-renderer in the source manifest. pnpm pack:check still proves the packed manifests contain no workspace: protocol.

Wiring: root build:packages (before blocks-schema), scripts/pack-local.ts, scripts/check-packed-packages.ts (packed tarball installs into the isolated consumer, asserts dist-relative entry points, no surviving workspace: ranges, a LICENSE, and resolves json-renderer + json-renderer/compose + json-renderer/adapter while exercising createEnvelope/composeEnvelope), root README.md, docs/RELEASING.md. No exports map, no generated registry output touched, no automated publishing.

Verified locally: pnpm --filter json-renderer build, pnpm build, pnpm lint:types, pnpm test (33 new core tests), pnpm check, pnpm pack:check.

Link to Devin session: https://app.devin.ai/sessions/027937d092794c92a31c6ee49c513f59
Requested by: @pyramation

Extract the framework-agnostic JSON UI document core into the new
json-renderer package: document envelope + node tree generic over the node
vocabulary, validation primitives and JSON Schema export, composition
(fragments, slots, per-node overrides, merge), React-free binding/scope
resolution, and an explicit renderer adapter contract.

blocks-schema now specializes that core with the Constructive vocabulary and
blocks-renderer is its first adapter (reactAdapter). Public APIs of both
packages are unchanged.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review complete. No blocking issues — approved ✅; 1 nitpick below.

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 Caret-pin json-renderer on its 0.0.1 version (package.json:33) — blocks-schema and blocks-renderer declare "json-renderer": "workspace:^" (packages/blocks-schema/package.json:33), which pnpm rewrites on publish to ^0.0.1 against json-renderer's 0.0.1 version (packages/json-renderer/package.json:3).

The change introduces a packages/json-renderer package holding the document-format, composition, binding, zod-schema, and JSON-Schema logic, then migrates blocks-schema and blocks-renderer to consume it, with updated adapter, registry, and index surfaces plus matching tests and packaging scripts.

Files Change
packages/json-renderer/** New package: compose/envelope/node/bindings/registry/adapter, zod + JSON-Schema export, constraints, fields, and tests.
packages/blocks-schema/** Refactored to re-export from json-renderer; build script changed to makage clean && makage build-ts && makage assets.
packages/blocks-renderer/** Adapter/bindings/registry updated to depend on json-renderer; types and index re-exports adjusted.
scripts/check-packed-packages.ts, scripts/pack-local.ts, package.json Release packaging updated for the new dist-layout publishing and packed-consumer verification.

Reviewed commit: e623d25

@pyramation
pyramation merged commit 1848398 into main Aug 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant