@golemui/schemas 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -0
- package/cli.cjs +134 -0
- package/cli.js +322 -0
- package/editor-bundle-Jb3IVFC3.js +219 -0
- package/editor-bundle-yoVhdYIT.cjs +3 -0
- package/generate-implementation-schemas-D57DzM2J.cjs +2 -0
- package/generate-implementation-schemas-DxCUi0Jr.js +116 -0
- package/generator.cjs +1 -0
- package/generator.d.ts +1 -0
- package/generator.js +4 -0
- package/index.cjs +1 -0
- package/index.d.ts +6 -0
- package/index.js +18 -0
- package/lib/generator/builders.d.ts +41 -0
- package/lib/generator/editor-bundle.d.ts +18 -0
- package/lib/generator/generate-implementation-schemas.d.ts +14 -0
- package/lib/manifest.types.d.ts +86 -0
- package/package.json +51 -0
- package/schemas/core/common.schema.json +209 -0
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @golemui/schemas
|
|
2
|
+
|
|
3
|
+
Base JSON schema resources shared by every GolemUI widget set implementation, plus the pure
|
|
4
|
+
builder functions that generate an implementation's aggregate schema files from its widget
|
|
5
|
+
manifest.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @golemui/schemas
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
schemas/
|
|
17
|
+
core/common.schema.json shared structural $defs (baseWidget, localizable, chunkRef, ...)
|
|
18
|
+
index.js / index.cjs the JavaScript entry point
|
|
19
|
+
generator.js / generator.cjs the file-writing generator, `@golemui/schemas/generator`
|
|
20
|
+
cli.js the `golemui-schemas` command
|
|
21
|
+
index.d.ts, lib/*.d.ts type declarations
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Core publishes exactly one `$defs` resource: `common.schema.json`. Validation vocabulary is
|
|
25
|
+
implementation-owned, not core contract. Each implementation publishes its own validators
|
|
26
|
+
schema exposing a `#/$defs/validator` entry pointer (the gui set lives at
|
|
27
|
+
`schemas/gui/validators.schema.json` in the published tree).
|
|
28
|
+
|
|
29
|
+
## Entry point
|
|
30
|
+
|
|
31
|
+
The entry point exports the core schema as an object, the builder functions
|
|
32
|
+
(`buildWidgetsSchema`, `buildFormEnvelope`, `buildLayoutWidgetSchema`,
|
|
33
|
+
`buildSchemasPackageIndex`, `buildEditorBundle`), and their types
|
|
34
|
+
(`ImplementationSchemaConfig`, `WidgetManifestEntry`, `WidgetKind`, `SchemaObject`). The raw
|
|
35
|
+
file is also reachable as `@golemui/schemas/schemas/core/common.schema.json`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
39
|
+
import { buildWidgetsSchema, commonSchema } from '@golemui/schemas';
|
|
40
|
+
|
|
41
|
+
const ajv = new Ajv2020();
|
|
42
|
+
ajv.addSchema(commonSchema);
|
|
43
|
+
|
|
44
|
+
const widgets = buildWidgetsSchema({
|
|
45
|
+
implementation: 'gui',
|
|
46
|
+
idBase: 'https://golemui.com/schemas/gui/',
|
|
47
|
+
generatorPath: 'libs/gui/schemas/tools/generate-schemas.ts',
|
|
48
|
+
formTitle: 'Golem Form DSL',
|
|
49
|
+
statesDescription: 'Named boolean conditions keyed by state name.',
|
|
50
|
+
manifest: [{ type: 'textinput', schemaFile: 'textinput.schema.json', kind: 'input' }],
|
|
51
|
+
libRootSchemaFiles: ['validators.schema.json'],
|
|
52
|
+
includeSchemalessTypesInKnownWidgetTypes: true,
|
|
53
|
+
includeCustomWidgetFallback: true,
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## The published site tree
|
|
58
|
+
|
|
59
|
+
The site tree at `https://golemui.com/schemas/` is meant to layer this core resource with one
|
|
60
|
+
directory per implementation:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
schemas/
|
|
64
|
+
core/ from this package
|
|
65
|
+
form.schema.json legacy alias, from this repository (not from npm)
|
|
66
|
+
gui/ from @golemui/gui-schemas (generated aggregates, gui-owned
|
|
67
|
+
validators and ranges defs, vendored core/)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That tree is not published yet. Only the original `/schemas/form.schema.json` monolith is
|
|
71
|
+
live today, and no job in this repository assembles or deploys the rest, so the `$id` values
|
|
72
|
+
in these packages are identifiers rather than URLs that resolve.
|
|
73
|
+
|
|
74
|
+
## The legacy alias
|
|
75
|
+
|
|
76
|
+
`site/form.schema.json` is a three-line schema whose only content is
|
|
77
|
+
`"$ref": "./gui/form.schema.json"`. It keeps the original schema URL
|
|
78
|
+
`https://golemui.com/schemas/form.schema.json` working once the site tree exists, where the
|
|
79
|
+
`gui/` directory sits next to it.
|
|
80
|
+
|
|
81
|
+
It is a website build input and is deliberately not shipped to npm: inside a package the ref
|
|
82
|
+
resolves to nothing, so every way of loading it throws `MissingRefError`. That is also why it
|
|
83
|
+
lives outside `src/`, and why the entry point does not export it.
|
|
84
|
+
|
|
85
|
+
Publishing it is one atomic step with the `gui/` tree. Serving the alias while
|
|
86
|
+
`/schemas/gui/form.schema.json` is still missing breaks the one schema URL that works today,
|
|
87
|
+
which the MCP writes into the `$schema` line of every form definition it generates. A
|
|
88
|
+
post-deploy check should fetch `/schemas/form.schema.json`, follow its `$ref`, and assert that
|
|
89
|
+
both respond with `application/json`.
|
|
90
|
+
|
|
91
|
+
## Generating an implementation tree
|
|
92
|
+
|
|
93
|
+
An implementation declares a widget manifest (`WidgetManifestEntry[]`) and an
|
|
94
|
+
`ImplementationSchemaConfig`, then calls `generateImplementationSchemas` from
|
|
95
|
+
`@golemui/schemas/generator` to produce its `widgets.schema.json` (widget union plus
|
|
96
|
+
`knownWidgetTypes` enum), its `form.schema.json` envelope, its `layout-widget.schema.json`,
|
|
97
|
+
its vendored copy of `schemas/core/`, and its package index source. Set
|
|
98
|
+
`emitEditorBundle: true` to also get `form.editor.schema.json` (see below). The gui
|
|
99
|
+
implementation's entry point is `libs/gui/schemas/tools/generate-schemas.ts`, run with
|
|
100
|
+
`npm run generate:schemas`.
|
|
101
|
+
|
|
102
|
+
Generated files are prettier-formatted when prettier and a prettier config are both
|
|
103
|
+
resolvable, and plainly indented otherwise. Prettier is an optional peer dependency.
|
|
104
|
+
|
|
105
|
+
## Scaffolding a new implementation
|
|
106
|
+
|
|
107
|
+
The `golemui-schemas` command scaffolds and regenerates a schema tree, so an implementation
|
|
108
|
+
needs no generator script of its own:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx @golemui/schemas init --name kendo --id-base https://example.com/schemas/kendo/
|
|
112
|
+
npx @golemui/schemas generate
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`init` writes `schemas.config.mjs` (the manifest and config), a starter validators schema, a
|
|
116
|
+
starter `flex` and example input component schema, an example form and a test skeleton, then
|
|
117
|
+
runs `generate`. Run it with no flags for prompts.
|
|
118
|
+
|
|
119
|
+
`schemas.config.mjs` and the component schemas are the implementer's to edit. Everything
|
|
120
|
+
else, including the vendored core, is rewritten by `generate` from the installed
|
|
121
|
+
`@golemui/schemas`, so updating core means bumping the dependency and rerunning it. A CI step
|
|
122
|
+
that runs `generate` and then `git diff --exit-code` catches a stale tree.
|
|
123
|
+
|
|
124
|
+
## Two entry points: Ajv and editors
|
|
125
|
+
|
|
126
|
+
Ajv registers the per-file tree by `$id`, and resolution works because every file is added up
|
|
127
|
+
front. An editor instead resolves each relative `$ref` against the file's absolute `$id`,
|
|
128
|
+
computes a URL and tries to download it, which fails offline, in untrusted workspaces and
|
|
129
|
+
wherever the `idBase` is not hosted. `form.editor.schema.json` is the same tree inlined into
|
|
130
|
+
one self-contained document with no `$id`s: point a form file's `$schema` at it.
|
|
131
|
+
|
|
132
|
+
Editing workflow for the core file: change it here, then run `npm run generate:schemas`
|
|
133
|
+
so every vendored copy is regenerated. A vendored copy is identical to its source apart from
|
|
134
|
+
an `$id` rebased onto the implementation's own tree, which is what lets that tree be loaded by
|
|
135
|
+
`$id` alone. A drift test enforces this, and CI fails when generated files are stale.
|
package/cli.cjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";const p=require("node:fs"),i=require("node:path"),g=require("node:readline/promises"),b=require("node:url"),v=require("./generate-implementation-schemas-D57DzM2J.cjs"),w="../src/lib/form.editor.schema.json",x=[{kind:"layout",type:"flex",uid:"#root",props:{direction:"column",gap:16},children:[{kind:"input",type:"__IMPLEMENTATION__-input",uid:"email",path:"email",label:"Email address",validator:{type:"string",required:!0,format:"email"},props:{placeholder:"you@example.com"}}]}],j={$schema:w,form:x},S="https://json-schema.org/draft/2020-12/schema",P="__ID_BASE__components/example-input.schema.json",T="STARTER TEMPLATE, owned by you. One component schema per widget type. Copy this file for each widget, then list it in schemas.config.mjs. The pattern to keep: allOf the baseWidget, `kind` and `type` consts, one `$defs` entry per prop, a `.state` suffixed pattern for every suffixable key, `additionalProperties: false` inside `props`, and `unevaluatedProperties: false` at the root.",E="__IMPLEMENTATION__ example input widget",_="object",I={labelDef:{$ref:"../core/common.schema.json#/$defs/localizable"},disabledDef:{$ref:"../core/common.schema.json#/$defs/boolOrWhen"},readonlyDef:{$ref:"../core/common.schema.json#/$defs/boolOrWhen"},hintProp:{type:"string"},placeholderProp:{type:"string"},maxlengthProp:{type:"number"}},q=[{$ref:"../core/common.schema.json#/$defs/baseWidget"}],A={kind:{const:"input"},type:{const:"__IMPLEMENTATION__-input"},path:{$ref:"../core/common.schema.json#/$defs/dotPath"},label:{$ref:"#/$defs/labelDef"},disabled:{$ref:"#/$defs/disabledDef"},readonly:{$ref:"#/$defs/readonlyDef"},on:{$ref:"../core/common.schema.json#/$defs/on"},validator:{$ref:"../validators.schema.json#/$defs/validator"},defaultValue:{type:"string"},props:{type:"object",properties:{hint:{$ref:"#/$defs/hintProp"},placeholder:{$ref:"#/$defs/placeholderProp"},maxlength:{$ref:"#/$defs/maxlengthProp"}},patternProperties:{"^hint\\.[^.]+$":{$ref:"#/$defs/hintProp"},"^placeholder\\.[^.]+$":{$ref:"#/$defs/placeholderProp"},"^maxlength\\.[^.]+$":{$ref:"#/$defs/maxlengthProp"}},additionalProperties:!1}},O={"^label\\.[^.]+$":{$ref:"#/$defs/labelDef"},"^disabled\\.[^.]+$":{$ref:"#/$defs/disabledDef"},"^readonly\\.[^.]+$":{$ref:"#/$defs/readonlyDef"},"^validator\\.[^.]+$":{$ref:"../validators.schema.json#/$defs/validator"}},M=["kind","type","path"],L=!1,N={$schema:S,$id:P,$comment:T,title:E,type:_,$defs:I,allOf:q,properties:A,patternProperties:O,required:M,unevaluatedProperties:L},k="https://json-schema.org/draft/2020-12/schema",z="__ID_BASE__components/flex.schema.json",R="STARTER TEMPLATE, owned by you. `flex` is a reserved widget type: keep the name and the `children` array, change the props to match your layout widget.",C="__IMPLEMENTATION__ flex widget",D="object",W={directionProp:{enum:["row","row-reverse","column","column-reverse"]},justifyProp:{enum:["start","center","end","stretch","space-between","space-around"]},alignProp:{enum:["start","center","end","stretch","baseline"]},wrapProp:{type:"boolean"},gapProp:{type:"number"},paddingProp:{type:"number"}},F=[{$ref:"../core/common.schema.json#/$defs/baseWidget"}],B={kind:{const:"layout"},type:{const:"flex"},children:{type:"array",minItems:1,items:{$ref:"../widgets.schema.json#/$defs/formWidget"}},props:{type:"object",properties:{direction:{$ref:"#/$defs/directionProp"},justify:{$ref:"#/$defs/justifyProp"},align:{$ref:"#/$defs/alignProp"},wrap:{$ref:"#/$defs/wrapProp"},gap:{$ref:"#/$defs/gapProp"},padding:{$ref:"#/$defs/paddingProp"}},patternProperties:{"^direction\\.[^.]+$":{$ref:"#/$defs/directionProp"},"^justify\\.[^.]+$":{$ref:"#/$defs/justifyProp"},"^align\\.[^.]+$":{$ref:"#/$defs/alignProp"},"^wrap\\.[^.]+$":{$ref:"#/$defs/wrapProp"},"^gap\\.[^.]+$":{$ref:"#/$defs/gapProp"},"^padding\\.[^.]+$":{$ref:"#/$defs/paddingProp"}},additionalProperties:!1}},V=["kind","type","children"],U=!1,J={$schema:k,$id:z,$comment:R,title:C,type:D,$defs:W,allOf:F,properties:B,required:V,unevaluatedProperties:U},Y="https://json-schema.org/draft/2020-12/schema",G="__ID_BASE__validators.schema.json",H="STARTER TEMPLATE, owned by you. Edit it freely. The only contract is that it exposes #/$defs/validator, which every component schema references. Copied from the golemui gui validators when the tree was scaffolded, it does not track them afterwards.",K="__IMPLEMENTATION__ validator definitions",Z={localizable:{$ref:"./core/common.schema.json#/$defs/localizable"},stringValidator:{type:"object",properties:{type:{const:"string"},required:{type:"boolean",description:"Makes the field mandatory - validation fails if the value is empty or absent"},minLength:{type:"number",description:"Minimum number of characters the string must contain"},maxLength:{type:"number",description:"Maximum number of characters the string may contain"},pattern:{type:"string",description:"A regular expression pattern the value must match"},format:{type:"string",description:"A named format the string must conform to (e.g. `email`, `url`, `date`, `uuid`)",enum:["email","hostname","ipv4","ipv6","url","uuid","date","time","date-time","duration"]},const:{description:"The string must equal exactly this value"},enum:{type:"array",description:"The string must be one of these allowed values"},messages:{type:"object",description:"Custom error messages that override the default text for each constraint violation",properties:{invalid:{$ref:"#/$defs/localizable",description:"Shown when the value fails general type validation"},required:{$ref:"#/$defs/localizable",description:"Shown when a required field is empty"},minLength:{$ref:"#/$defs/localizable",description:"Shown when the value is shorter than `minLength`"},maxLength:{$ref:"#/$defs/localizable",description:"Shown when the value is longer than `maxLength`"},pattern:{$ref:"#/$defs/localizable",description:"Shown when the value does not match the `pattern` regex"},format:{$ref:"#/$defs/localizable",description:"Shown when the value does not conform to the specified `format`"},enum:{$ref:"#/$defs/localizable",description:"Shown when the value is not in the `enum` list"},const:{$ref:"#/$defs/localizable",description:"Shown when the value does not equal `const`"}},additionalProperties:!1}},required:["type"],additionalProperties:!1},numberValidator:{type:"object",properties:{type:{type:"string",enum:["number","integer"]},required:{type:"boolean",description:"Makes the field mandatory - validation fails if the value is absent"},minimum:{type:"number",description:"The value must be greater than or equal to this number (inclusive)"},maximum:{type:"number",description:"The value must be less than or equal to this number (inclusive)"},exclusiveMinimum:{type:"number",description:"The value must be strictly greater than this number (exclusive)"},exclusiveMaximum:{type:"number",description:"The value must be strictly less than this number (exclusive)"},multipleOf:{type:"number",description:"The value must be a multiple of this number"},const:{description:"The value must equal exactly this number"},enum:{type:"array",description:"The value must be one of these allowed numbers"},messages:{type:"object",description:"Custom error messages that override the default text for each constraint violation",properties:{invalid:{$ref:"#/$defs/localizable",description:"Shown when the value fails general type validation"},minimum:{$ref:"#/$defs/localizable",description:"Shown when the value is below `minimum`"},maximum:{$ref:"#/$defs/localizable",description:"Shown when the value exceeds `maximum`"},exclusiveMinimum:{$ref:"#/$defs/localizable",description:"Shown when the value is not strictly above `exclusiveMinimum`"},exclusiveMaximum:{$ref:"#/$defs/localizable",description:"Shown when the value is not strictly below `exclusiveMaximum`"},multipleOf:{$ref:"#/$defs/localizable",description:"Shown when the value is not a multiple of `multipleOf`"},enum:{$ref:"#/$defs/localizable",description:"Shown when the value is not in the `enum` list"},const:{$ref:"#/$defs/localizable",description:"Shown when the value does not equal `const`"}},additionalProperties:!1}},required:["type"],additionalProperties:!1},booleanValidator:{type:"object",properties:{type:{const:"boolean"},required:{type:"boolean",description:"Makes the field mandatory - validation fails if the value is absent or false (e.g. an unchecked required checkbox)"},const:{description:"The value must equal exactly this boolean (e.g. `true` to require acceptance)"},messages:{type:"object",description:"Custom error messages that override the default text for each constraint violation",properties:{invalid:{$ref:"#/$defs/localizable",description:"Shown when the value fails general type validation"},const:{$ref:"#/$defs/localizable",description:"Shown when the value does not equal `const`"}},additionalProperties:!1}},required:["type"],additionalProperties:!1},arrayValidator:{type:"object",properties:{type:{const:"array"},required:{type:"boolean",description:"Makes the field mandatory - validation fails if the array is empty or absent"},minItems:{type:"number",description:"The array must contain at least this many items"},maxItems:{type:"number",description:"The array must contain no more than this many items"},uniqueItems:{type:"boolean",description:"When true, all items in the array must be distinct values"},messages:{type:"object",description:"Custom error messages that override the default text for each constraint violation",properties:{invalid:{$ref:"#/$defs/localizable",description:"Shown when the value fails general type validation"},required:{$ref:"#/$defs/localizable",description:"Shown when a required array is empty or absent"},minItems:{$ref:"#/$defs/localizable",description:"Shown when the array has fewer items than `minItems`"},maxItems:{$ref:"#/$defs/localizable",description:"Shown when the array has more items than `maxItems`"}},additionalProperties:!1}},required:["type"],additionalProperties:!1},customValidator:{type:"object",description:"A custom validator that delegates validation to application code. The registered validator function receives these constraint keys and must return a Standard Schema V1-compliant schema object (https://standardschema.dev). Add arbitrary constraint keys alongside `type` - they are passed as-is to the registered validator function.",properties:{type:{const:"custom"},required:{type:"boolean",description:"Makes the field mandatory - evaluation is still delegated to the custom validator"}},required:["type"],additionalProperties:!0},validator:{description:"Validation rules for this field. The `type` discriminant selects the validator: `string`, `number`, `integer`, `boolean`, `array`, or `custom`.",oneOf:[{$ref:"#/$defs/stringValidator"},{$ref:"#/$defs/numberValidator"},{$ref:"#/$defs/booleanValidator"},{$ref:"#/$defs/arrayValidator"},{$ref:"#/$defs/customValidator"}]}},Q={$schema:Y,$id:G,$comment:H,title:K,$defs:Z};function X(e){return`${e}-input`}function ee(e,t){const r=e.replace(/__IMPLEMENTATION__/g,t.implementation).replace(/__ID_BASE__/g,t.idBase),o=r.match(/__[A-Z_]+__/);if(o!==null)throw new Error(`Unsubstituted placeholder ${o[0]} in a starter template.`);return r}function c(e,t){return ee(JSON.stringify(e,null,2),t)+`
|
|
3
|
+
`}function te(e){return{"schemas.config.mjs":re(e),"src/lib/validators.schema.json":c(Q,e),"src/lib/components/flex.schema.json":c(J,e),"src/lib/components/example-input.schema.json":c(N,e),"examples/example.form.json":c(j,e),"test/schemas.spec.ts":oe(e)}}function re(e){return`// The manifest and configuration for this implementation's JSON schema tree.
|
|
4
|
+
// You own this file. Every file marked GENERATED is rebuilt from it by
|
|
5
|
+
// \`npx @golemui/schemas generate\`, so add a widget by writing its component schema
|
|
6
|
+
// under src/lib/components/ and listing it in \`manifest\` below.
|
|
7
|
+
|
|
8
|
+
/** @type {import('@golemui/schemas').ImplementationSchemaConfig} */
|
|
9
|
+
export default {
|
|
10
|
+
implementation: '${e.implementation}',
|
|
11
|
+
// Where the tree is published. It only has to be a URL you control: the $ids must be
|
|
12
|
+
// unique and stable, and nothing downloads them (editors read form.editor.schema.json).
|
|
13
|
+
idBase: '${e.idBase}',
|
|
14
|
+
generatorPath: '@golemui/schemas generate',
|
|
15
|
+
manifestPath: 'schemas.config.mjs',
|
|
16
|
+
regenerateCommand: 'npx @golemui/schemas generate',
|
|
17
|
+
formTitle: '${e.implementation} form DSL',
|
|
18
|
+
statesDescription:
|
|
19
|
+
'Named boolean conditions keyed by state name, each mapping to a reactive expression. ' +
|
|
20
|
+
'Root-level widget props such as \`label\`, \`disabled\`, \`readonly\` and \`validator\` accept ' +
|
|
21
|
+
'a \`.stateName\` suffix, as does any key inside \`props\`.',
|
|
22
|
+
// One entry per widget type. \`flex\` and \`repeater\` are reserved names: use them for
|
|
23
|
+
// your layout and repeat widgets, and do not give another widget those types.
|
|
24
|
+
manifest: [
|
|
25
|
+
{ type: 'flex', schemaFile: 'flex.schema.json', kind: 'layout' },
|
|
26
|
+
{
|
|
27
|
+
type: '${X(e.implementation)}',
|
|
28
|
+
schemaFile: 'example-input.schema.json',
|
|
29
|
+
kind: 'input',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
// Handwritten schemas at the src/lib root, re-exported from the generated index.
|
|
33
|
+
libRootSchemaFiles: ['validators.schema.json'],
|
|
34
|
+
includeSchemalessTypesInKnownWidgetTypes: false,
|
|
35
|
+
includeCustomWidgetFallback: false,
|
|
36
|
+
emitEditorBundle: true,
|
|
37
|
+
};
|
|
38
|
+
`}function oe(e){return`// Registers the whole schema tree into one Ajv 2020 instance and validates the
|
|
39
|
+
// example form against it. Run it after every \`npx @golemui/schemas generate\`.
|
|
40
|
+
//
|
|
41
|
+
// Needs \`ajv\` and a test runner. This file is written for vitest, the assertions are
|
|
42
|
+
// the only part to change for another runner.
|
|
43
|
+
|
|
44
|
+
import { readFileSync } from 'node:fs';
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
46
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
47
|
+
import { describe, expect, it } from 'vitest';
|
|
48
|
+
import {
|
|
49
|
+
COMPONENT_SCHEMAS_BY_TYPE,
|
|
50
|
+
commonSchema,
|
|
51
|
+
formSchema,
|
|
52
|
+
layoutWidgetSchema,
|
|
53
|
+
validatorsSchema,
|
|
54
|
+
widgetsSchema,
|
|
55
|
+
} from '../src/index';
|
|
56
|
+
|
|
57
|
+
const ajv = new Ajv2020({ allErrors: false, strict: false });
|
|
58
|
+
|
|
59
|
+
// Leaves before aggregates, the form envelope last. Dedupe through \`ajv.schemas\`:
|
|
60
|
+
// \`ajv.getSchema\` compiles on lookup and fails while ref targets are unregistered.
|
|
61
|
+
const registrationOrder = [
|
|
62
|
+
commonSchema,
|
|
63
|
+
validatorsSchema,
|
|
64
|
+
widgetsSchema,
|
|
65
|
+
layoutWidgetSchema,
|
|
66
|
+
...Object.values(COMPONENT_SCHEMAS_BY_TYPE),
|
|
67
|
+
formSchema,
|
|
68
|
+
];
|
|
69
|
+
for (const schema of registrationOrder) {
|
|
70
|
+
const id = schema['$id'] as string;
|
|
71
|
+
if (!ajv.schemas[id]) {
|
|
72
|
+
ajv.addSchema(schema);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const validate = ajv.getSchema('${e.idBase}form.schema.json');
|
|
77
|
+
if (!validate) {
|
|
78
|
+
throw new Error('The form envelope schema did not register.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadExampleForm(): { form: Array<Record<string, unknown>> } {
|
|
82
|
+
const path = fileURLToPath(new URL('../examples/example.form.json', import.meta.url));
|
|
83
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('${e.implementation} schema tree', () => {
|
|
87
|
+
it('validates the example form', () => {
|
|
88
|
+
const valid = validate(loadExampleForm());
|
|
89
|
+
expect(valid, JSON.stringify(validate.errors, null, 2)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('rejects an unknown widget type', () => {
|
|
93
|
+
const form = loadExampleForm();
|
|
94
|
+
const firstChild = (form.form[0]['children'] as Array<Record<string, unknown>>)[0];
|
|
95
|
+
firstChild['type'] = 'not-a-widget';
|
|
96
|
+
expect(validate(form)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('rejects an unknown validator key', () => {
|
|
100
|
+
const form = loadExampleForm();
|
|
101
|
+
const firstChild = (form.form[0]['children'] as Array<Record<string, unknown>>)[0];
|
|
102
|
+
firstChild['validator'] = { type: 'string', required: true, notAKey: 1 };
|
|
103
|
+
expect(validate(form)).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
`}const l="schemas.config.mjs",u=`golemui-schemas - JSON schema trees for GolemUI implementations
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
npx @golemui/schemas init [options] scaffold a schema tree
|
|
110
|
+
npx @golemui/schemas generate [options] rebuild the generated files
|
|
111
|
+
|
|
112
|
+
Options for init:
|
|
113
|
+
--name <name> implementation name, lowercase, e.g. kendo
|
|
114
|
+
--id-base <url> absolute base URL of the published tree, e.g. https://example.com/schemas/kendo/
|
|
115
|
+
--dir <path> directory to scaffold into (default: schemas)
|
|
116
|
+
--force overwrite existing starter files
|
|
117
|
+
|
|
118
|
+
Options for generate:
|
|
119
|
+
--dir <path> directory holding ${l} (default: the current directory)
|
|
120
|
+
|
|
121
|
+
Both commands accept --help.`;function ae(e){const t=new Map;let r;for(let o=0;o<e.length;o+=1){const a=e[o];if(!a.startsWith("--")){r??=a;continue}const s=a.slice(2),n=s.indexOf("=");if(n!==-1){t.set(s.slice(0,n),s.slice(n+1));continue}const d=e[o+1];d===void 0||d.startsWith("--")?t.set(s,!0):(t.set(s,d),o+=1)}return{command:r,flags:t}}function m(e,t){const r=e.get(t);return typeof r=="string"?r:void 0}async function f(e,t,r){if(process.stdin.isTTY!==!0)throw new Error(`Missing --${t}. There is no terminal to ask on.`);const o=g.createInterface({input:process.stdin,output:process.stdout});try{const a=(await o.question(`${e} [${r}] `)).trim();return a===""?r:a}finally{o.close()}}function se(e){if(!/^[a-z][a-z0-9-]*$/.test(e))throw new Error(`Invalid implementation name "${e}". Use lowercase letters, digits and hyphens, starting with a letter.`)}function ie(e){let t;try{t=new URL(e)}catch{throw new Error(`Invalid --id-base "${e}". It must be an absolute URL.`)}if(t.protocol!=="http:"&&t.protocol!=="https:")throw new Error(`Invalid --id-base "${e}". It must be an http or https URL.`);return e.endsWith("/")?e:`${e}/`}function ne(e,t,r){const o=i.join(e,t);p.mkdirSync(i.dirname(o),{recursive:!0}),p.writeFileSync(o,r,"utf-8"),console.log(`Wrote ${t}`)}async function le(e){const t=m(e,"name")??await f("Implementation name?","name","my-widgets");se(t);const r=ie(m(e,"id-base")??await f("Base URL of the published tree?","id-base",`https://example.com/schemas/${t}/`)),o=m(e,"dir")??await f("Directory?","dir","schemas"),a=i.resolve(process.cwd(),o),s=te({implementation:t,idBase:r}),n=Object.keys(s).filter(h=>p.existsSync(i.join(a,h)));if(n.length>0&&e.get("force")!==!0)throw new Error(`Refusing to overwrite starter files you may have edited: ${n.join(", ")}. Pass --force to replace them.`);for(const[h,y]of Object.entries(s))ne(a,h,y);await $(a);const d=i.relative(process.cwd(),a)||".";console.log(`
|
|
122
|
+
Scaffolded the ${t} schema tree in ${d}.
|
|
123
|
+
|
|
124
|
+
Next:
|
|
125
|
+
1. Add @golemui/schemas to the project's dependencies.
|
|
126
|
+
2. Write one component schema per widget under src/lib/components/, and list each one
|
|
127
|
+
in ${l}. src/lib/components/example-input.schema.json is the pattern to copy.
|
|
128
|
+
3. Rerun \`npx @golemui/schemas generate\` after every edit to a component schema, to
|
|
129
|
+
${l} or to validators.schema.json. A CI step that regenerates and then runs
|
|
130
|
+
\`git diff --exit-code\` catches a forgotten rerun.
|
|
131
|
+
4. Point a form file's "$schema" at src/lib/form.editor.schema.json, as
|
|
132
|
+
examples/example.form.json does. Editors need that bundle, Ajv uses the per-file tree.`)}function de(e,t){if(e===null||typeof e!="object")throw new Error(`${t} must export a configuration object as its default export.`);const r=e,a=["implementation","idBase","generatorPath","formTitle"].filter(s=>typeof r[s]!="string");if(a.length>0)throw new Error(`${t} is missing required string fields: ${a.join(", ")}.`);if(!Array.isArray(r.manifest)||r.manifest.length===0)throw new Error(`${t} must declare a non-empty \`manifest\` array.`);if(!r.idBase.endsWith("/"))throw new Error(`${t}: \`idBase\` must end with a slash.`);return e}async function $(e){const t=i.join(e,l);if(!p.existsSync(t))throw new Error(`No ${l} in ${e}. Run this from the directory holding it, pass --dir, or scaffold a new tree with \`npx @golemui/schemas init\`.`);const r=await import(b.pathToFileURL(t).href),o=de(r.default,l);await v.generateImplementationSchemas(o,e)}async function ce(e){const{command:t,flags:r}=ae(e);if(r.get("help")===!0||t==="help"||t===void 0)return console.log(u),t===void 0&&r.get("help")!==!0?1:0;try{switch(t){case"init":return await le(r),0;case"generate":return await $(i.resolve(process.cwd(),m(r,"dir")??".")),0;default:return console.error(`Unknown command "${t}".
|
|
133
|
+
|
|
134
|
+
${u}`),1}}catch(o){return console.error(o instanceof Error?o.message:o),1}}ce(process.argv.slice(2)).then(e=>{process.exitCode=e});
|
package/cli.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync as u, mkdirSync as b, writeFileSync as v } from "node:fs";
|
|
3
|
+
import { resolve as $, join as h, relative as w, dirname as x } from "node:path";
|
|
4
|
+
import { createInterface as j } from "node:readline/promises";
|
|
5
|
+
import { pathToFileURL as S } from "node:url";
|
|
6
|
+
import { g as P } from "./generate-implementation-schemas-DxCUi0Jr.js";
|
|
7
|
+
const T = "../src/lib/form.editor.schema.json", E = [{ kind: "layout", type: "flex", uid: "#root", props: { direction: "column", gap: 16 }, children: [{ kind: "input", type: "__IMPLEMENTATION__-input", uid: "email", path: "email", label: "Email address", validator: { type: "string", required: !0, format: "email" }, props: { placeholder: "you@example.com" } }] }], _ = {
|
|
8
|
+
$schema: T,
|
|
9
|
+
form: E
|
|
10
|
+
}, I = "https://json-schema.org/draft/2020-12/schema", A = "__ID_BASE__components/example-input.schema.json", q = "STARTER TEMPLATE, owned by you. One component schema per widget type. Copy this file for each widget, then list it in schemas.config.mjs. The pattern to keep: allOf the baseWidget, `kind` and `type` consts, one `$defs` entry per prop, a `.state` suffixed pattern for every suffixable key, `additionalProperties: false` inside `props`, and `unevaluatedProperties: false` at the root.", O = "__IMPLEMENTATION__ example input widget", M = "object", L = { labelDef: { $ref: "../core/common.schema.json#/$defs/localizable" }, disabledDef: { $ref: "../core/common.schema.json#/$defs/boolOrWhen" }, readonlyDef: { $ref: "../core/common.schema.json#/$defs/boolOrWhen" }, hintProp: { type: "string" }, placeholderProp: { type: "string" }, maxlengthProp: { type: "number" } }, N = [{ $ref: "../core/common.schema.json#/$defs/baseWidget" }], k = { kind: { const: "input" }, type: { const: "__IMPLEMENTATION__-input" }, path: { $ref: "../core/common.schema.json#/$defs/dotPath" }, label: { $ref: "#/$defs/labelDef" }, disabled: { $ref: "#/$defs/disabledDef" }, readonly: { $ref: "#/$defs/readonlyDef" }, on: { $ref: "../core/common.schema.json#/$defs/on" }, validator: { $ref: "../validators.schema.json#/$defs/validator" }, defaultValue: { type: "string" }, props: { type: "object", properties: { hint: { $ref: "#/$defs/hintProp" }, placeholder: { $ref: "#/$defs/placeholderProp" }, maxlength: { $ref: "#/$defs/maxlengthProp" } }, patternProperties: { "^hint\\.[^.]+$": { $ref: "#/$defs/hintProp" }, "^placeholder\\.[^.]+$": { $ref: "#/$defs/placeholderProp" }, "^maxlength\\.[^.]+$": { $ref: "#/$defs/maxlengthProp" } }, additionalProperties: !1 } }, z = { "^label\\.[^.]+$": { $ref: "#/$defs/labelDef" }, "^disabled\\.[^.]+$": { $ref: "#/$defs/disabledDef" }, "^readonly\\.[^.]+$": { $ref: "#/$defs/readonlyDef" }, "^validator\\.[^.]+$": { $ref: "../validators.schema.json#/$defs/validator" } }, R = ["kind", "type", "path"], C = !1, D = {
|
|
11
|
+
$schema: I,
|
|
12
|
+
$id: A,
|
|
13
|
+
$comment: q,
|
|
14
|
+
title: O,
|
|
15
|
+
type: M,
|
|
16
|
+
$defs: L,
|
|
17
|
+
allOf: N,
|
|
18
|
+
properties: k,
|
|
19
|
+
patternProperties: z,
|
|
20
|
+
required: R,
|
|
21
|
+
unevaluatedProperties: C
|
|
22
|
+
}, W = "https://json-schema.org/draft/2020-12/schema", F = "__ID_BASE__components/flex.schema.json", B = "STARTER TEMPLATE, owned by you. `flex` is a reserved widget type: keep the name and the `children` array, change the props to match your layout widget.", V = "__IMPLEMENTATION__ flex widget", U = "object", J = { directionProp: { enum: ["row", "row-reverse", "column", "column-reverse"] }, justifyProp: { enum: ["start", "center", "end", "stretch", "space-between", "space-around"] }, alignProp: { enum: ["start", "center", "end", "stretch", "baseline"] }, wrapProp: { type: "boolean" }, gapProp: { type: "number" }, paddingProp: { type: "number" } }, Y = [{ $ref: "../core/common.schema.json#/$defs/baseWidget" }], G = { kind: { const: "layout" }, type: { const: "flex" }, children: { type: "array", minItems: 1, items: { $ref: "../widgets.schema.json#/$defs/formWidget" } }, props: { type: "object", properties: { direction: { $ref: "#/$defs/directionProp" }, justify: { $ref: "#/$defs/justifyProp" }, align: { $ref: "#/$defs/alignProp" }, wrap: { $ref: "#/$defs/wrapProp" }, gap: { $ref: "#/$defs/gapProp" }, padding: { $ref: "#/$defs/paddingProp" } }, patternProperties: { "^direction\\.[^.]+$": { $ref: "#/$defs/directionProp" }, "^justify\\.[^.]+$": { $ref: "#/$defs/justifyProp" }, "^align\\.[^.]+$": { $ref: "#/$defs/alignProp" }, "^wrap\\.[^.]+$": { $ref: "#/$defs/wrapProp" }, "^gap\\.[^.]+$": { $ref: "#/$defs/gapProp" }, "^padding\\.[^.]+$": { $ref: "#/$defs/paddingProp" } }, additionalProperties: !1 } }, H = ["kind", "type", "children"], K = !1, Z = {
|
|
23
|
+
$schema: W,
|
|
24
|
+
$id: F,
|
|
25
|
+
$comment: B,
|
|
26
|
+
title: V,
|
|
27
|
+
type: U,
|
|
28
|
+
$defs: J,
|
|
29
|
+
allOf: Y,
|
|
30
|
+
properties: G,
|
|
31
|
+
required: H,
|
|
32
|
+
unevaluatedProperties: K
|
|
33
|
+
}, Q = "https://json-schema.org/draft/2020-12/schema", X = "__ID_BASE__validators.schema.json", ee = "STARTER TEMPLATE, owned by you. Edit it freely. The only contract is that it exposes #/$defs/validator, which every component schema references. Copied from the golemui gui validators when the tree was scaffolded, it does not track them afterwards.", te = "__IMPLEMENTATION__ validator definitions", re = { localizable: { $ref: "./core/common.schema.json#/$defs/localizable" }, stringValidator: { type: "object", properties: { type: { const: "string" }, required: { type: "boolean", description: "Makes the field mandatory - validation fails if the value is empty or absent" }, minLength: { type: "number", description: "Minimum number of characters the string must contain" }, maxLength: { type: "number", description: "Maximum number of characters the string may contain" }, pattern: { type: "string", description: "A regular expression pattern the value must match" }, format: { type: "string", description: "A named format the string must conform to (e.g. `email`, `url`, `date`, `uuid`)", enum: ["email", "hostname", "ipv4", "ipv6", "url", "uuid", "date", "time", "date-time", "duration"] }, const: { description: "The string must equal exactly this value" }, enum: { type: "array", description: "The string must be one of these allowed values" }, messages: { type: "object", description: "Custom error messages that override the default text for each constraint violation", properties: { invalid: { $ref: "#/$defs/localizable", description: "Shown when the value fails general type validation" }, required: { $ref: "#/$defs/localizable", description: "Shown when a required field is empty" }, minLength: { $ref: "#/$defs/localizable", description: "Shown when the value is shorter than `minLength`" }, maxLength: { $ref: "#/$defs/localizable", description: "Shown when the value is longer than `maxLength`" }, pattern: { $ref: "#/$defs/localizable", description: "Shown when the value does not match the `pattern` regex" }, format: { $ref: "#/$defs/localizable", description: "Shown when the value does not conform to the specified `format`" }, enum: { $ref: "#/$defs/localizable", description: "Shown when the value is not in the `enum` list" }, const: { $ref: "#/$defs/localizable", description: "Shown when the value does not equal `const`" } }, additionalProperties: !1 } }, required: ["type"], additionalProperties: !1 }, numberValidator: { type: "object", properties: { type: { type: "string", enum: ["number", "integer"] }, required: { type: "boolean", description: "Makes the field mandatory - validation fails if the value is absent" }, minimum: { type: "number", description: "The value must be greater than or equal to this number (inclusive)" }, maximum: { type: "number", description: "The value must be less than or equal to this number (inclusive)" }, exclusiveMinimum: { type: "number", description: "The value must be strictly greater than this number (exclusive)" }, exclusiveMaximum: { type: "number", description: "The value must be strictly less than this number (exclusive)" }, multipleOf: { type: "number", description: "The value must be a multiple of this number" }, const: { description: "The value must equal exactly this number" }, enum: { type: "array", description: "The value must be one of these allowed numbers" }, messages: { type: "object", description: "Custom error messages that override the default text for each constraint violation", properties: { invalid: { $ref: "#/$defs/localizable", description: "Shown when the value fails general type validation" }, minimum: { $ref: "#/$defs/localizable", description: "Shown when the value is below `minimum`" }, maximum: { $ref: "#/$defs/localizable", description: "Shown when the value exceeds `maximum`" }, exclusiveMinimum: { $ref: "#/$defs/localizable", description: "Shown when the value is not strictly above `exclusiveMinimum`" }, exclusiveMaximum: { $ref: "#/$defs/localizable", description: "Shown when the value is not strictly below `exclusiveMaximum`" }, multipleOf: { $ref: "#/$defs/localizable", description: "Shown when the value is not a multiple of `multipleOf`" }, enum: { $ref: "#/$defs/localizable", description: "Shown when the value is not in the `enum` list" }, const: { $ref: "#/$defs/localizable", description: "Shown when the value does not equal `const`" } }, additionalProperties: !1 } }, required: ["type"], additionalProperties: !1 }, booleanValidator: { type: "object", properties: { type: { const: "boolean" }, required: { type: "boolean", description: "Makes the field mandatory - validation fails if the value is absent or false (e.g. an unchecked required checkbox)" }, const: { description: "The value must equal exactly this boolean (e.g. `true` to require acceptance)" }, messages: { type: "object", description: "Custom error messages that override the default text for each constraint violation", properties: { invalid: { $ref: "#/$defs/localizable", description: "Shown when the value fails general type validation" }, const: { $ref: "#/$defs/localizable", description: "Shown when the value does not equal `const`" } }, additionalProperties: !1 } }, required: ["type"], additionalProperties: !1 }, arrayValidator: { type: "object", properties: { type: { const: "array" }, required: { type: "boolean", description: "Makes the field mandatory - validation fails if the array is empty or absent" }, minItems: { type: "number", description: "The array must contain at least this many items" }, maxItems: { type: "number", description: "The array must contain no more than this many items" }, uniqueItems: { type: "boolean", description: "When true, all items in the array must be distinct values" }, messages: { type: "object", description: "Custom error messages that override the default text for each constraint violation", properties: { invalid: { $ref: "#/$defs/localizable", description: "Shown when the value fails general type validation" }, required: { $ref: "#/$defs/localizable", description: "Shown when a required array is empty or absent" }, minItems: { $ref: "#/$defs/localizable", description: "Shown when the array has fewer items than `minItems`" }, maxItems: { $ref: "#/$defs/localizable", description: "Shown when the array has more items than `maxItems`" } }, additionalProperties: !1 } }, required: ["type"], additionalProperties: !1 }, customValidator: { type: "object", description: "A custom validator that delegates validation to application code. The registered validator function receives these constraint keys and must return a Standard Schema V1-compliant schema object (https://standardschema.dev). Add arbitrary constraint keys alongside `type` - they are passed as-is to the registered validator function.", properties: { type: { const: "custom" }, required: { type: "boolean", description: "Makes the field mandatory - evaluation is still delegated to the custom validator" } }, required: ["type"], additionalProperties: !0 }, validator: { description: "Validation rules for this field. The `type` discriminant selects the validator: `string`, `number`, `integer`, `boolean`, `array`, or `custom`.", oneOf: [{ $ref: "#/$defs/stringValidator" }, { $ref: "#/$defs/numberValidator" }, { $ref: "#/$defs/booleanValidator" }, { $ref: "#/$defs/arrayValidator" }, { $ref: "#/$defs/customValidator" }] } }, oe = {
|
|
34
|
+
$schema: Q,
|
|
35
|
+
$id: X,
|
|
36
|
+
$comment: ee,
|
|
37
|
+
title: te,
|
|
38
|
+
$defs: re
|
|
39
|
+
};
|
|
40
|
+
function ae(e) {
|
|
41
|
+
return `${e}-input`;
|
|
42
|
+
}
|
|
43
|
+
function ie(e, t) {
|
|
44
|
+
const r = e.replace(/__IMPLEMENTATION__/g, t.implementation).replace(/__ID_BASE__/g, t.idBase), o = r.match(/__[A-Z_]+__/);
|
|
45
|
+
if (o !== null)
|
|
46
|
+
throw new Error(`Unsubstituted placeholder ${o[0]} in a starter template.`);
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
function d(e, t) {
|
|
50
|
+
return ie(JSON.stringify(e, null, 2), t) + `
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
function se(e) {
|
|
54
|
+
return {
|
|
55
|
+
"schemas.config.mjs": ne(e),
|
|
56
|
+
"src/lib/validators.schema.json": d(oe, e),
|
|
57
|
+
"src/lib/components/flex.schema.json": d(Z, e),
|
|
58
|
+
"src/lib/components/example-input.schema.json": d(
|
|
59
|
+
D,
|
|
60
|
+
e
|
|
61
|
+
),
|
|
62
|
+
"examples/example.form.json": d(_, e),
|
|
63
|
+
"test/schemas.spec.ts": le(e)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function ne(e) {
|
|
67
|
+
return `// The manifest and configuration for this implementation's JSON schema tree.
|
|
68
|
+
// You own this file. Every file marked GENERATED is rebuilt from it by
|
|
69
|
+
// \`npx @golemui/schemas generate\`, so add a widget by writing its component schema
|
|
70
|
+
// under src/lib/components/ and listing it in \`manifest\` below.
|
|
71
|
+
|
|
72
|
+
/** @type {import('@golemui/schemas').ImplementationSchemaConfig} */
|
|
73
|
+
export default {
|
|
74
|
+
implementation: '${e.implementation}',
|
|
75
|
+
// Where the tree is published. It only has to be a URL you control: the $ids must be
|
|
76
|
+
// unique and stable, and nothing downloads them (editors read form.editor.schema.json).
|
|
77
|
+
idBase: '${e.idBase}',
|
|
78
|
+
generatorPath: '@golemui/schemas generate',
|
|
79
|
+
manifestPath: 'schemas.config.mjs',
|
|
80
|
+
regenerateCommand: 'npx @golemui/schemas generate',
|
|
81
|
+
formTitle: '${e.implementation} form DSL',
|
|
82
|
+
statesDescription:
|
|
83
|
+
'Named boolean conditions keyed by state name, each mapping to a reactive expression. ' +
|
|
84
|
+
'Root-level widget props such as \`label\`, \`disabled\`, \`readonly\` and \`validator\` accept ' +
|
|
85
|
+
'a \`.stateName\` suffix, as does any key inside \`props\`.',
|
|
86
|
+
// One entry per widget type. \`flex\` and \`repeater\` are reserved names: use them for
|
|
87
|
+
// your layout and repeat widgets, and do not give another widget those types.
|
|
88
|
+
manifest: [
|
|
89
|
+
{ type: 'flex', schemaFile: 'flex.schema.json', kind: 'layout' },
|
|
90
|
+
{
|
|
91
|
+
type: '${ae(e.implementation)}',
|
|
92
|
+
schemaFile: 'example-input.schema.json',
|
|
93
|
+
kind: 'input',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
// Handwritten schemas at the src/lib root, re-exported from the generated index.
|
|
97
|
+
libRootSchemaFiles: ['validators.schema.json'],
|
|
98
|
+
includeSchemalessTypesInKnownWidgetTypes: false,
|
|
99
|
+
includeCustomWidgetFallback: false,
|
|
100
|
+
emitEditorBundle: true,
|
|
101
|
+
};
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
function le(e) {
|
|
105
|
+
return `// Registers the whole schema tree into one Ajv 2020 instance and validates the
|
|
106
|
+
// example form against it. Run it after every \`npx @golemui/schemas generate\`.
|
|
107
|
+
//
|
|
108
|
+
// Needs \`ajv\` and a test runner. This file is written for vitest, the assertions are
|
|
109
|
+
// the only part to change for another runner.
|
|
110
|
+
|
|
111
|
+
import { readFileSync } from 'node:fs';
|
|
112
|
+
import { fileURLToPath } from 'node:url';
|
|
113
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
114
|
+
import { describe, expect, it } from 'vitest';
|
|
115
|
+
import {
|
|
116
|
+
COMPONENT_SCHEMAS_BY_TYPE,
|
|
117
|
+
commonSchema,
|
|
118
|
+
formSchema,
|
|
119
|
+
layoutWidgetSchema,
|
|
120
|
+
validatorsSchema,
|
|
121
|
+
widgetsSchema,
|
|
122
|
+
} from '../src/index';
|
|
123
|
+
|
|
124
|
+
const ajv = new Ajv2020({ allErrors: false, strict: false });
|
|
125
|
+
|
|
126
|
+
// Leaves before aggregates, the form envelope last. Dedupe through \`ajv.schemas\`:
|
|
127
|
+
// \`ajv.getSchema\` compiles on lookup and fails while ref targets are unregistered.
|
|
128
|
+
const registrationOrder = [
|
|
129
|
+
commonSchema,
|
|
130
|
+
validatorsSchema,
|
|
131
|
+
widgetsSchema,
|
|
132
|
+
layoutWidgetSchema,
|
|
133
|
+
...Object.values(COMPONENT_SCHEMAS_BY_TYPE),
|
|
134
|
+
formSchema,
|
|
135
|
+
];
|
|
136
|
+
for (const schema of registrationOrder) {
|
|
137
|
+
const id = schema['$id'] as string;
|
|
138
|
+
if (!ajv.schemas[id]) {
|
|
139
|
+
ajv.addSchema(schema);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const validate = ajv.getSchema('${e.idBase}form.schema.json');
|
|
144
|
+
if (!validate) {
|
|
145
|
+
throw new Error('The form envelope schema did not register.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function loadExampleForm(): { form: Array<Record<string, unknown>> } {
|
|
149
|
+
const path = fileURLToPath(new URL('../examples/example.form.json', import.meta.url));
|
|
150
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
describe('${e.implementation} schema tree', () => {
|
|
154
|
+
it('validates the example form', () => {
|
|
155
|
+
const valid = validate(loadExampleForm());
|
|
156
|
+
expect(valid, JSON.stringify(validate.errors, null, 2)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('rejects an unknown widget type', () => {
|
|
160
|
+
const form = loadExampleForm();
|
|
161
|
+
const firstChild = (form.form[0]['children'] as Array<Record<string, unknown>>)[0];
|
|
162
|
+
firstChild['type'] = 'not-a-widget';
|
|
163
|
+
expect(validate(form)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects an unknown validator key', () => {
|
|
167
|
+
const form = loadExampleForm();
|
|
168
|
+
const firstChild = (form.form[0]['children'] as Array<Record<string, unknown>>)[0];
|
|
169
|
+
firstChild['validator'] = { type: 'string', required: true, notAKey: 1 };
|
|
170
|
+
expect(validate(form)).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
const n = "schemas.config.mjs", f = `golemui-schemas - JSON schema trees for GolemUI implementations
|
|
176
|
+
|
|
177
|
+
Usage:
|
|
178
|
+
npx @golemui/schemas init [options] scaffold a schema tree
|
|
179
|
+
npx @golemui/schemas generate [options] rebuild the generated files
|
|
180
|
+
|
|
181
|
+
Options for init:
|
|
182
|
+
--name <name> implementation name, lowercase, e.g. kendo
|
|
183
|
+
--id-base <url> absolute base URL of the published tree, e.g. https://example.com/schemas/kendo/
|
|
184
|
+
--dir <path> directory to scaffold into (default: schemas)
|
|
185
|
+
--force overwrite existing starter files
|
|
186
|
+
|
|
187
|
+
Options for generate:
|
|
188
|
+
--dir <path> directory holding ${n} (default: the current directory)
|
|
189
|
+
|
|
190
|
+
Both commands accept --help.`;
|
|
191
|
+
function de(e) {
|
|
192
|
+
const t = /* @__PURE__ */ new Map();
|
|
193
|
+
let r;
|
|
194
|
+
for (let o = 0; o < e.length; o += 1) {
|
|
195
|
+
const a = e[o];
|
|
196
|
+
if (!a.startsWith("--")) {
|
|
197
|
+
r ??= a;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const i = a.slice(2), s = i.indexOf("=");
|
|
201
|
+
if (s !== -1) {
|
|
202
|
+
t.set(i.slice(0, s), i.slice(s + 1));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const l = e[o + 1];
|
|
206
|
+
l === void 0 || l.startsWith("--") ? t.set(i, !0) : (t.set(i, l), o += 1);
|
|
207
|
+
}
|
|
208
|
+
return { command: r, flags: t };
|
|
209
|
+
}
|
|
210
|
+
function c(e, t) {
|
|
211
|
+
const r = e.get(t);
|
|
212
|
+
return typeof r == "string" ? r : void 0;
|
|
213
|
+
}
|
|
214
|
+
async function p(e, t, r) {
|
|
215
|
+
if (process.stdin.isTTY !== !0)
|
|
216
|
+
throw new Error(`Missing --${t}. There is no terminal to ask on.`);
|
|
217
|
+
const o = j({ input: process.stdin, output: process.stdout });
|
|
218
|
+
try {
|
|
219
|
+
const a = (await o.question(`${e} [${r}] `)).trim();
|
|
220
|
+
return a === "" ? r : a;
|
|
221
|
+
} finally {
|
|
222
|
+
o.close();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function ce(e) {
|
|
226
|
+
if (!/^[a-z][a-z0-9-]*$/.test(e))
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Invalid implementation name "${e}". Use lowercase letters, digits and hyphens, starting with a letter.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
function me(e) {
|
|
232
|
+
let t;
|
|
233
|
+
try {
|
|
234
|
+
t = new URL(e);
|
|
235
|
+
} catch {
|
|
236
|
+
throw new Error(`Invalid --id-base "${e}". It must be an absolute URL.`);
|
|
237
|
+
}
|
|
238
|
+
if (t.protocol !== "http:" && t.protocol !== "https:")
|
|
239
|
+
throw new Error(`Invalid --id-base "${e}". It must be an http or https URL.`);
|
|
240
|
+
return e.endsWith("/") ? e : `${e}/`;
|
|
241
|
+
}
|
|
242
|
+
function pe(e, t, r) {
|
|
243
|
+
const o = h(e, t);
|
|
244
|
+
b(x(o), { recursive: !0 }), v(o, r, "utf-8"), console.log(`Wrote ${t}`);
|
|
245
|
+
}
|
|
246
|
+
async function he(e) {
|
|
247
|
+
const t = c(e, "name") ?? await p("Implementation name?", "name", "my-widgets");
|
|
248
|
+
ce(t);
|
|
249
|
+
const r = me(
|
|
250
|
+
c(e, "id-base") ?? await p(
|
|
251
|
+
"Base URL of the published tree?",
|
|
252
|
+
"id-base",
|
|
253
|
+
`https://example.com/schemas/${t}/`
|
|
254
|
+
)
|
|
255
|
+
), o = c(e, "dir") ?? await p("Directory?", "dir", "schemas"), a = $(process.cwd(), o), i = se({ implementation: t, idBase: r }), s = Object.keys(i).filter((m) => u(h(a, m)));
|
|
256
|
+
if (s.length > 0 && e.get("force") !== !0)
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Refusing to overwrite starter files you may have edited: ${s.join(", ")}. Pass --force to replace them.`
|
|
259
|
+
);
|
|
260
|
+
for (const [m, g] of Object.entries(i))
|
|
261
|
+
pe(a, m, g);
|
|
262
|
+
await y(a);
|
|
263
|
+
const l = w(process.cwd(), a) || ".";
|
|
264
|
+
console.log(`
|
|
265
|
+
Scaffolded the ${t} schema tree in ${l}.
|
|
266
|
+
|
|
267
|
+
Next:
|
|
268
|
+
1. Add @golemui/schemas to the project's dependencies.
|
|
269
|
+
2. Write one component schema per widget under src/lib/components/, and list each one
|
|
270
|
+
in ${n}. src/lib/components/example-input.schema.json is the pattern to copy.
|
|
271
|
+
3. Rerun \`npx @golemui/schemas generate\` after every edit to a component schema, to
|
|
272
|
+
${n} or to validators.schema.json. A CI step that regenerates and then runs
|
|
273
|
+
\`git diff --exit-code\` catches a forgotten rerun.
|
|
274
|
+
4. Point a form file's "$schema" at src/lib/form.editor.schema.json, as
|
|
275
|
+
examples/example.form.json does. Editors need that bundle, Ajv uses the per-file tree.`);
|
|
276
|
+
}
|
|
277
|
+
function fe(e, t) {
|
|
278
|
+
if (e === null || typeof e != "object")
|
|
279
|
+
throw new Error(`${t} must export a configuration object as its default export.`);
|
|
280
|
+
const r = e, a = ["implementation", "idBase", "generatorPath", "formTitle"].filter((i) => typeof r[i] != "string");
|
|
281
|
+
if (a.length > 0)
|
|
282
|
+
throw new Error(`${t} is missing required string fields: ${a.join(", ")}.`);
|
|
283
|
+
if (!Array.isArray(r.manifest) || r.manifest.length === 0)
|
|
284
|
+
throw new Error(`${t} must declare a non-empty \`manifest\` array.`);
|
|
285
|
+
if (!r.idBase.endsWith("/"))
|
|
286
|
+
throw new Error(`${t}: \`idBase\` must end with a slash.`);
|
|
287
|
+
return e;
|
|
288
|
+
}
|
|
289
|
+
async function y(e) {
|
|
290
|
+
const t = h(e, n);
|
|
291
|
+
if (!u(t))
|
|
292
|
+
throw new Error(
|
|
293
|
+
`No ${n} in ${e}. Run this from the directory holding it, pass --dir, or scaffold a new tree with \`npx @golemui/schemas init\`.`
|
|
294
|
+
);
|
|
295
|
+
const r = await import(
|
|
296
|
+
/* @vite-ignore */
|
|
297
|
+
S(t).href
|
|
298
|
+
), o = fe(r.default, n);
|
|
299
|
+
await P(o, e);
|
|
300
|
+
}
|
|
301
|
+
async function ue(e) {
|
|
302
|
+
const { command: t, flags: r } = de(e);
|
|
303
|
+
if (r.get("help") === !0 || t === "help" || t === void 0)
|
|
304
|
+
return console.log(f), t === void 0 && r.get("help") !== !0 ? 1 : 0;
|
|
305
|
+
try {
|
|
306
|
+
switch (t) {
|
|
307
|
+
case "init":
|
|
308
|
+
return await he(r), 0;
|
|
309
|
+
case "generate":
|
|
310
|
+
return await y($(process.cwd(), c(r, "dir") ?? ".")), 0;
|
|
311
|
+
default:
|
|
312
|
+
return console.error(`Unknown command "${t}".
|
|
313
|
+
|
|
314
|
+
${f}`), 1;
|
|
315
|
+
}
|
|
316
|
+
} catch (o) {
|
|
317
|
+
return console.error(o instanceof Error ? o.message : o), 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
ue(process.argv.slice(2)).then((e) => {
|
|
321
|
+
process.exitCode = e;
|
|
322
|
+
});
|