Generating a schema tree
Most of a schema tree is mechanical: the envelope, the
widget union, the layout union, the vendored core copy and the package index all follow from
one list of widget types. @golemui/schemas generates those from a manifest and ships a CLI
that scaffolds the whole package, so the real work is writing one component schema per widget.
The widget manifest
Section titled “The widget manifest”The single source of truth is a list of { type, schemaFile, kind } entries, one per widget
type. This is the shape gui uses:
export const guiWidgetManifest: readonly WidgetManifestEntry[] = [ { type: 'textinput', schemaFile: 'textinput.schema.json', kind: 'input' }, { type: 'dateTimeCalendar', schemaFile: 'datetimecalendar.schema.json', kind: 'input', }, { type: 'flex', schemaFile: 'flex.schema.json', kind: 'layout' }, { type: 'alert', schemaFile: 'alert.schema.json', kind: 'display' }, // No schemaFile: the renderer props contain a render function, which JSON // cannot express. Listing it in knownWidgetTypes rejects it in JSON forms. { type: 'renderer', kind: 'display' }, { type: 'button', schemaFile: 'button.schema.json', kind: 'action' },];type is the literal that appears in a JSON form, kind is one of input, layout,
display or action, and schemaFile names a file inside components/. Leaving schemaFile
off marks a type that exists in the builder but has no JSON representation.
The generator config
Section titled “The generator config”The manifest goes into an ImplementationSchemaConfig alongside the texts the generator cannot
derive:
| Field | Meaning |
|---|---|
implementation | Implementation name, used in generated titles |
idBase | Absolute base URL of the published tree, with a trailing slash |
manifest | The widget manifest |
formTitle | title of the generated form envelope |
statesDescription | Description of the envelope’s states property, naming your suffixable root props |
libRootSchemaFiles | Handwritten root schema files to re-export, for example validators.schema.json |
includeSchemalessTypesInKnownWidgetTypes | List builder-only types in the enum so using them in JSON fails with a clear error |
includeCustomWidgetFallback | Whether the widget union includes the open custom.schema.json branch |
emitEditorBundle | Also emit the self-contained editor bundle. Off by default |
generatorPath | Path of the generator entry point, written into the generated-file markers |
manifestPath | File holding the manifest, for the same markers. Defaults to src/lib/widget-manifest.ts |
regenerateCommand | Command that rebuilds the tree, for the same markers. Defaults to npm run generate:schemas |
What the generator writes
Section titled “What the generator writes”generateImplementationSchemas(config, packageRoot) emits widgets.schema.json,
form.schema.json, layout-widget.schema.json, the vendored core/ copy, the editor bundle
when enabled, and the package index with a COMPONENT_SCHEMAS_BY_TYPE map. Everything lands
under <packageRoot>/src/lib/, the index at <packageRoot>/src/index.ts, and both are
overwritten on every run, so hand-written exports belong in another file.
The index exports annotated constants rather than raw JSON modules, so consumers of the built
package need neither resolveJsonModule nor import attributes. The generated source itself
does default-import the JSON files, so the tsconfig that compiles the tree in your repository
needs resolveJsonModule: true and esModuleInterop (or allowSyntheticDefaultImports).
Output goes through prettier when both prettier and a prettier config resolve, and falls back to plain two-space indentation otherwise. Either way it is deterministic: a second run is byte-identical.
The scaffolder CLI
Section titled “The scaffolder CLI”@golemui/schemas ships a golemui-schemas bin that creates the whole schema package for a
widget set living outside the golemui repository:
npx @golemui/schemas init --name kendo --id-base https://example.com/schemas/kendo/--dir <path> picks the target directory, --force overwrites starter files that already
exist. Run it with no flags in a terminal and it prompts for each one, defaulting the directory
to schemas; without a TTY every flag is required and a missing one is an error rather than a
silent default. A missing trailing slash on --id-base is added for you.
init writes the tree and then runs generate once:
<dir>/ schemas.config.mjs the manifest and config, owned by you src/ index.ts generated lib/ form.schema.json generated form.editor.schema.json generated (self-contained editor bundle) widgets.schema.json generated layout-widget.schema.json generated core/common.schema.json generated (vendored, $id rebased) validators.schema.json starter copy of the standard set, owned by you components/ flex.schema.json starter template (layout, children ref, reserved type name) example-input.schema.json starter template showing the full component pattern examples/ example.form.json editor fixture, its $schema points at the editor bundle test/ schemas.spec.ts Ajv 2020 registration and validation skeletonThe second command rebuilds every generated file from schemas.config.mjs, read from the
current directory or from --dir <path>:
npx @golemui/schemas generate --dir schemasThe scaffolded test/schemas.spec.ts needs ajv (2020 dialect) and vitest as devDependencies
of your own repository, neither is a dependency of @golemui/schemas. It validates
examples/example.form.json, which uses the starter widget type <name>-input, so it fails as
soon as you customize the manifest. Replace the example together with the manifest, and
validating every examples/*.form.json in a loop is the shape that keeps working.
Ownership and regeneration
Section titled “Ownership and regeneration”schemas.config.mjs, the component schemas and the validators schema are yours. Everything
else, including the vendored core copy, is rebuilt from the installed @golemui/schemas, so
picking up a core change means bumping that dependency and rerunning generate.
Generated files are never edited by hand. The workflow is: edit the manifest or a handwritten schema, rerun the generator, and let CI catch a forgotten rerun:
npx @golemui/schemas generate --dir schemasgit diff --exit-code -- schemasWriting a component schema
Section titled “Writing a component schema”One schema per widget type is the real per-implementation work. The pattern, as established by
the gui components:
kindandtypeas consts.- A
$refto the base widget defs in the vendoredcore/common.schema.json. - Per-prop
$defs, withadditionalProperties: falseinsideprops. patternPropertiesentries for the state-suffixed form of each suffixable prop, so"label.myState"is accepted alongside"label".unevaluatedProperties: falseat the widget root.defaultValuedeclared on every input widget type, typed per widget. It is a core input field, and a schema that omits it rejects JSON forms that work at runtime.
src/lib/components/example-input.schema.json in a scaffolded tree is a working example of all
six, and is the file to copy for each new widget.
See also
Section titled “See also”- JSON schemas: the tree layout, the composition rules, and how to load it
- Widget Sets overview: how the JSON half fits with the rest
- Authoring pipeline: the TypeScript half, whose widget types this manifest mirrors