@jxsuite/schema 0.23.2 → 0.25.0

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/src/schema.ts ADDED
@@ -0,0 +1,529 @@
1
+ /**
2
+ * jx-schema.ts — Jx JSON Schema 2020-12 meta-schema generator
3
+ * @version 2.0.0
4
+ * @license MIT
5
+ *
6
+ * Generates comprehensive JSON Schema 2020-12 documents that validate Jx source files.
7
+ * Individual schema definitions live in ../defs/ as the single source of truth.
8
+ * This module composes them and injects web standards data derived at generation time from:
9
+ *
10
+ * webref/elements — HTML element tag names
11
+ * webref/css — CSS property names (camelCase CSSOM)
12
+ * webref/idl — DOM EventHandler attribute names
13
+ *
14
+ * Usage:
15
+ * import { generateSchema } from './schema.js';
16
+ * const schema = await generateSchema();
17
+ * fs.writeFileSync('schema.json', JSON.stringify(schema, null, 2));
18
+ *
19
+ * CLI:
20
+ * bun run schema.ts [output-path]
21
+ *
22
+ * @module jx-schema
23
+ */
24
+
25
+ import { listAll as listElements } from "@webref/elements";
26
+ import css from "@webref/css";
27
+ import idl from "@webref/idl";
28
+
29
+ import { tagNameSchema } from "../defs/tag-name.schema";
30
+ import {
31
+ stringOrRefSchema,
32
+ boolOrRefSchema,
33
+ numberOrRefSchema,
34
+ } from "../defs/string-or-ref.schema";
35
+ import { headEntrySchema } from "../defs/head-entry.schema";
36
+ import { cemParameterSchema, cemEventSchema } from "../defs/cem.schema";
37
+ import { jsonSchemaTypeSchema } from "../defs/json-schema-type.schema";
38
+ import {
39
+ internalRefSchema,
40
+ stateRefSchema,
41
+ externalRefSchema,
42
+ globalRefSchema,
43
+ parentRefSchema,
44
+ mapRefSchema,
45
+ anyRefSchema,
46
+ refObjectSchema,
47
+ externalComponentRefSchema,
48
+ } from "../defs/ref-object.schema";
49
+ import { styleObjectSchema } from "../defs/style-object.schema";
50
+ import { arrayNamespaceSchema, childrenValueSchema } from "../defs/children-value.schema";
51
+ import {
52
+ attributesObjectSchema,
53
+ propsObjectSchema,
54
+ elementPropertyValueSchema,
55
+ switchDefSchema,
56
+ switchNodeSchema,
57
+ elementDefSchema,
58
+ } from "../defs/element-def.schema";
59
+ import { typedStateDefSchema } from "../defs/typed-state-def.schema";
60
+ import { functionDefSchema } from "../defs/function-def.schema";
61
+ import { externalClassDefSchema } from "../defs/external-class-def.schema";
62
+ import { pureTypeDefSchema } from "../defs/pure-type-def.schema";
63
+ import {
64
+ expressionPointerSchema,
65
+ expressionLiteralSchema,
66
+ expressionOperandSchema,
67
+ expressionNodeSchema,
68
+ expressionEntrySchema,
69
+ unaryOperatorSchema,
70
+ binaryOperatorSchema,
71
+ assignmentOperatorSchema,
72
+ noArgMethodSchema,
73
+ oneArgMethodSchema,
74
+ spliceMethodSchema,
75
+ reduceMethodSchema,
76
+ mapFilterMethodSchema,
77
+ } from "../defs/expression-node.schema";
78
+ import {
79
+ stateEntrySchema,
80
+ stateMapSchema,
81
+ defsMapSchema,
82
+ typeDefEntrySchema,
83
+ } from "../defs/state-entry.schema";
84
+ import {
85
+ classParameterDefSchema,
86
+ classFieldDefSchema,
87
+ classConstructorDefSchema,
88
+ classMethodDefSchema,
89
+ classDefSchema,
90
+ } from "../defs/class-def.schema";
91
+ import { contentTypeDefSchema } from "../defs/content-type-def.schema";
92
+ import { imageConfigSchema } from "../defs/image-config.schema";
93
+ import { projectConfigSchema } from "../defs/project-config.schema";
94
+
95
+ // ─── Web standards data loader ────────────────────────────────────────────────
96
+
97
+ async function loadWebData() {
98
+ const [elementsData, cssData, idlData] = await Promise.all([
99
+ listElements(),
100
+ css.listAll(),
101
+ idl.parseAll(),
102
+ ]);
103
+
104
+ const tagSet = new Set<string>();
105
+ for (const { elements } of Object.values(elementsData)) {
106
+ for (const el of elements) {
107
+ if (!el.obsolete) tagSet.add(el.name);
108
+ }
109
+ }
110
+ const tagExamples = [...tagSet].sort();
111
+
112
+ const cssSet = new Set<string>();
113
+ for (const prop of cssData.properties) {
114
+ for (const decl of prop.styleDeclaration ?? []) {
115
+ cssSet.add(decl);
116
+ }
117
+ }
118
+ const cssProps = [...cssSet].sort();
119
+
120
+ const handlerSet = new Set<string>();
121
+ for (const ast of Object.values(idlData)) {
122
+ for (const def of ast) {
123
+ if (def.type !== "interface" && def.type !== "interface mixin") continue;
124
+ if (!def.members) continue;
125
+ for (const member of def.members) {
126
+ if (
127
+ member.type === "attribute" &&
128
+ member.name?.startsWith("on") &&
129
+ typeof member.idlType?.idlType === "string" &&
130
+ member.idlType.idlType === "EventHandler"
131
+ ) {
132
+ handlerSet.add(member.name);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ const eventHandlers = [...handlerSet].sort();
138
+
139
+ return { tagExamples, cssProps, eventHandlers };
140
+ }
141
+
142
+ // ─── Injection helpers ─────────────────────────────────────────────────────────
143
+
144
+ function buildEventHandlerProperties(eventHandlers: string[]) {
145
+ const properties: Record<string, any> = {};
146
+ for (const name of eventHandlers) {
147
+ properties[name] = {
148
+ description: `Event handler for the "${name.slice(2)}" event.`,
149
+ oneOf: [{ $ref: "#/$defs/RefObject" }, { $ref: "#/$defs/ExpressionEntry" }],
150
+ };
151
+ }
152
+ return properties;
153
+ }
154
+
155
+ function buildCssProperties(cssProps: string[]) {
156
+ const properties: Record<string, any> = {};
157
+ for (const name of cssProps) {
158
+ properties[name] = { oneOf: [{ type: "string" }, { type: "number" }] };
159
+ }
160
+ return properties;
161
+ }
162
+
163
+ // ─── Generator ────────────────────────────────────────────────────────────────
164
+
165
+ export async function generateSchema() {
166
+ const { tagExamples, cssProps, eventHandlers } = await loadWebData();
167
+
168
+ return {
169
+ $schema: "https://json-schema.org/draft/2020-12/schema",
170
+ $id: "https://jxsuite.com/schema/v1",
171
+ title: "Jx Document",
172
+ description:
173
+ "Schema for Jx component files. " +
174
+ "A Jx document is a JSON object that declaratively describes a reactive " +
175
+ "web component: its structure (DOM tree), styling, type definitions ($defs), " +
176
+ "runtime state, and inline or external functions. Reactivity is powered by @vue/reactivity.",
177
+ type: "object",
178
+
179
+ properties: {
180
+ $schema: {
181
+ description: "URI identifying the Jx dialect version. Enables schema-aware IDE tooling.",
182
+ type: "string",
183
+ examples: ["https://jxsuite.com/schema/v1"],
184
+ },
185
+ $id: {
186
+ description: "Component identifier string. Used by tooling and the builder.",
187
+ type: "string",
188
+ examples: ["Counter", "TodoApp", "UserCard"],
189
+ },
190
+ $defs: {
191
+ description:
192
+ "Pure JSON Schema type definitions for this component. " +
193
+ "All entries are reusable type schemas — no runtime artifacts are produced. " +
194
+ "Referenced from state entries via $ref. Naming convention: PascalCase.",
195
+ $ref: "#/$defs/DefsMap",
196
+ },
197
+ state: {
198
+ description:
199
+ "Runtime variables for this component. All entries are reactive by default. " +
200
+ "Entry shape is determined by value type: " +
201
+ "scalar/array → reactive property, string with ${} → computed, " +
202
+ "object with $prototype → function or data source, " +
203
+ "object with type and default → typed reactive property.",
204
+ $ref: "#/$defs/StateMap",
205
+ },
206
+ $media: {
207
+ description:
208
+ "Named media breakpoints following CSS @custom-media convention. " +
209
+ "Keys use the CSS custom property -- prefix.",
210
+ type: "object",
211
+ additionalProperties: { type: "string" },
212
+ examples: [
213
+ {
214
+ "--sm": "(min-width: 640px)",
215
+ "--md": "(min-width: 768px)",
216
+ "--dark": "(prefers-color-scheme: dark)",
217
+ },
218
+ ],
219
+ },
220
+ $elements: {
221
+ description:
222
+ "Custom element dependencies. Items are either $ref objects pointing to JX " +
223
+ "element definitions, or bare npm package name strings for web component libraries.",
224
+ type: "array",
225
+ items: {
226
+ oneOf: [
227
+ {
228
+ type: "object",
229
+ required: ["$ref"],
230
+ properties: { $ref: { type: "string" } },
231
+ additionalProperties: false,
232
+ },
233
+ {
234
+ type: "string",
235
+ description: "npm package specifier (must declare customElements in package.json)",
236
+ },
237
+ ],
238
+ },
239
+ },
240
+ $head: {
241
+ description:
242
+ "Page-level <head> entries. Array of element definitions for meta tags, " +
243
+ "link tags, script tags, etc. Merged with layout and site-level $head entries.",
244
+ type: "array",
245
+ items: { $ref: "#/$defs/ElementDef" },
246
+ },
247
+ $layout: {
248
+ description:
249
+ "Layout reference for pages. String path to a layout JSON file, " +
250
+ "or false to opt out of the default layout.",
251
+ oneOf: [{ type: "string" }, { type: "boolean", const: false }],
252
+ examples: ["./layouts/base.json"],
253
+ },
254
+ $paths: {
255
+ description:
256
+ "Dynamic route parameters. Maps parameter names to data sources " +
257
+ "for generating one page per entry at build time.",
258
+ type: "object",
259
+ },
260
+ title: {
261
+ description:
262
+ "Page title. Can be a static string or a template string with ${} expressions.",
263
+ $ref: "#/$defs/StringOrRef",
264
+ },
265
+ imports: {
266
+ description:
267
+ "Import map: $prototype names to .class.json file paths. " +
268
+ "Allows state entries to reference external classes by name without $src.",
269
+ type: "object",
270
+ additionalProperties: { type: "string" },
271
+ },
272
+ observedAttributes: {
273
+ description:
274
+ "HTML attributes the custom element watches for changes. " +
275
+ "Follows the Web Components observedAttributes convention.",
276
+ type: "array",
277
+ items: { type: "string" },
278
+ },
279
+ cases: {
280
+ description:
281
+ "Switch cases object. Maps case values to element definitions or external " +
282
+ "component refs. Used alongside $switch for dynamic component rendering.",
283
+ type: "object",
284
+ additionalProperties: {
285
+ oneOf: [{ $ref: "#/$defs/ElementDef" }, { $ref: "#/$defs/ExternalComponentRef" }],
286
+ },
287
+ },
288
+ tagName: { $ref: "#/$defs/TagName" },
289
+ children: { $ref: "#/$defs/ChildrenValue" },
290
+ style: { $ref: "#/$defs/StyleObject" },
291
+ attributes: { $ref: "#/$defs/AttributesObject" },
292
+ },
293
+ additionalProperties: { $ref: "#/$defs/ElementPropertyValue" },
294
+
295
+ $defs: {
296
+ DefsMap: defsMapSchema,
297
+ TypeDefEntry: typeDefEntrySchema,
298
+ StateMap: stateMapSchema,
299
+ StateEntry: stateEntrySchema,
300
+ TypedStateDef: typedStateDefSchema,
301
+ PureTypeDef: pureTypeDefSchema,
302
+ FunctionDef: functionDefSchema,
303
+ ClassDef: classDefSchema,
304
+ ClassParameterDef: classParameterDefSchema,
305
+ ClassFieldDef: classFieldDefSchema,
306
+ ClassConstructorDef: classConstructorDefSchema,
307
+ ClassMethodDef: classMethodDefSchema,
308
+ ExternalClassDef: externalClassDefSchema,
309
+ ExpressionPointer: expressionPointerSchema,
310
+ ExpressionLiteral: expressionLiteralSchema,
311
+ ExpressionOperand: expressionOperandSchema,
312
+ UnaryOperator: unaryOperatorSchema,
313
+ BinaryOperator: binaryOperatorSchema,
314
+ AssignmentOperator: assignmentOperatorSchema,
315
+ NoArgMethod: noArgMethodSchema,
316
+ OneArgMethod: oneArgMethodSchema,
317
+ SpliceMethod: spliceMethodSchema,
318
+ ReduceMethod: reduceMethodSchema,
319
+ MapFilterMethod: mapFilterMethodSchema,
320
+ ExpressionNode: expressionNodeSchema,
321
+ ExpressionEntry: expressionEntrySchema,
322
+
323
+ // Element defs — with webref data injected
324
+ ElementDef: {
325
+ ...elementDefSchema,
326
+ properties: {
327
+ ...elementDefSchema.properties,
328
+ ...buildEventHandlerProperties(eventHandlers),
329
+ },
330
+ },
331
+ ChildrenValue: childrenValueSchema,
332
+ ArrayNamespace: arrayNamespaceSchema,
333
+ SwitchDef: switchDefSchema,
334
+ SwitchNode: switchNodeSchema,
335
+ StyleObject: {
336
+ ...styleObjectSchema,
337
+ properties: buildCssProperties(cssProps),
338
+ },
339
+ AttributesObject: attributesObjectSchema,
340
+ PropsObject: propsObjectSchema,
341
+ RefObject: refObjectSchema,
342
+ AnyRef: anyRefSchema,
343
+ InternalRef: internalRefSchema,
344
+ StateRef: stateRefSchema,
345
+ ExternalRef: externalRefSchema,
346
+ ExternalComponentRef: externalComponentRefSchema,
347
+ GlobalRef: globalRefSchema,
348
+ ParentRef: parentRefSchema,
349
+ MapRef: mapRefSchema,
350
+ ElementPropertyValue: elementPropertyValueSchema,
351
+ StringOrRef: stringOrRefSchema,
352
+ BoolOrRef: boolOrRefSchema,
353
+ NumberOrRef: numberOrRefSchema,
354
+ CemParameter: cemParameterSchema,
355
+ CemEvent: cemEventSchema,
356
+ TagName: {
357
+ ...tagNameSchema,
358
+ examples: [...tagExamples, "my-counter", "todo-app", "user-card"],
359
+ },
360
+ JsonSchemaType: jsonSchemaTypeSchema,
361
+ HeadEntry: headEntrySchema,
362
+ ImageConfig: imageConfigSchema,
363
+ ContentTypeDef: contentTypeDefSchema,
364
+ },
365
+ };
366
+ }
367
+
368
+ // ─── Project Schema Generator ────────────────────────────────────────────────
369
+
370
+ export function generateProjectSchema() {
371
+ return {
372
+ $schema: "https://json-schema.org/draft/2020-12/schema",
373
+ $id: "https://jxsuite.com/schema/project/v1",
374
+ title: "Jx Project",
375
+ description:
376
+ "Schema for Jx project.json files. " +
377
+ "A project.json file is the root anchor file for a Jx project, " +
378
+ "declaring site metadata, default settings, global styles, content types, " +
379
+ "and build configuration.",
380
+ type: "object",
381
+ properties: projectConfigSchema.properties,
382
+ additionalProperties: false,
383
+ $defs: {
384
+ ImageConfig: imageConfigSchema,
385
+ ContentTypeDef: contentTypeDefSchema,
386
+ },
387
+ };
388
+ }
389
+
390
+ // ─── Class Schema Generator ─────────────────────────────────────────────────
391
+
392
+ export function generateClassSchema() {
393
+ return {
394
+ $schema: "https://json-schema.org/draft/2020-12/schema",
395
+ $id: "https://jxsuite.com/schema/class/v1",
396
+ title: "Jx Class Definition",
397
+ description:
398
+ "Schema for Jx .class.json files. A class definition describes a schema-defined " +
399
+ "class with fields, constructor, methods, and type parameters. Optionally points " +
400
+ "to a JS module via $implementation for hybrid execution.",
401
+ type: "object",
402
+ required: ["$prototype", "title"],
403
+ properties: {
404
+ $schema: { type: "string" },
405
+ $id: { type: "string" },
406
+ $prototype: {
407
+ description: 'Must be "Class" for class definition files.',
408
+ type: "string",
409
+ const: "Class",
410
+ },
411
+ title: {
412
+ description: "PascalCase class name, used as the export name.",
413
+ type: "string",
414
+ examples: ["MarkdownFile", "DataSource", "Calculator"],
415
+ },
416
+ description: { type: "string" },
417
+ extends: {
418
+ description: "Base class — string name or $ref to another .class.json.",
419
+ oneOf: [
420
+ { type: "string" },
421
+ {
422
+ type: "object",
423
+ required: ["$ref"],
424
+ properties: { $ref: { type: "string" } },
425
+ additionalProperties: false,
426
+ },
427
+ ],
428
+ },
429
+ $implementation: {
430
+ description: "Relative path to a JS module containing the actual class implementation.",
431
+ type: "string",
432
+ examples: ["./md.js", "./lib/calculator.js"],
433
+ },
434
+ $defs: {
435
+ description: "Class members: parameters, returnTypes, fields, constructor, methods.",
436
+ type: "object",
437
+ properties: {
438
+ parameters: {
439
+ description: "Reusable typed parameter schemas, keyed by name.",
440
+ type: "object",
441
+ additionalProperties: { $ref: "#/$defs/ClassParameterDef" },
442
+ },
443
+ returnTypes: {
444
+ description: "Output type schemas, keyed by name.",
445
+ type: "object",
446
+ additionalProperties: { type: "object" },
447
+ },
448
+ fields: {
449
+ description: "Class fields with role, access, scope, and type information.",
450
+ type: "object",
451
+ additionalProperties: { $ref: "#/$defs/ClassFieldDef" },
452
+ },
453
+ constructor: { $ref: "#/$defs/ClassConstructorDef" },
454
+ methods: {
455
+ description: "Class methods and accessors.",
456
+ type: "object",
457
+ additionalProperties: { $ref: "#/$defs/ClassMethodDef" },
458
+ },
459
+ },
460
+ },
461
+ },
462
+ additionalProperties: false,
463
+ $defs: {
464
+ ClassParameterDef: classParameterDefSchema,
465
+ ClassFieldDef: classFieldDefSchema,
466
+ ClassConstructorDef: classConstructorDefSchema,
467
+ ClassMethodDef: classMethodDefSchema,
468
+ },
469
+ };
470
+ }
471
+
472
+ // ─── Public API ───────────────────────────────────────────────────────────────
473
+
474
+ export async function generateSchemaString() {
475
+ return JSON.stringify(await generateSchema(), null, 2);
476
+ }
477
+
478
+ export async function validateDocument(doc: Record<string, unknown>) {
479
+ let Ajv, addFormats;
480
+ try {
481
+ // @ts-ignore — optional peer dependency
482
+ ({ default: Ajv } = await import("ajv"));
483
+ // @ts-ignore — optional peer dependency
484
+ ({ default: addFormats } = await import("ajv-formats"));
485
+ } catch {
486
+ throw new Error("Schema validation requires ajv and ajv-formats: bun add ajv ajv-formats");
487
+ }
488
+
489
+ const ajv = new Ajv({ allErrors: true, strict: false, ownProperties: true });
490
+ addFormats(ajv);
491
+
492
+ const schema = await generateSchema();
493
+ const validate = ajv.compile(schema);
494
+ const valid = validate(doc);
495
+
496
+ return { valid, errors: validate.errors ?? null };
497
+ }
498
+
499
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
500
+
501
+ if (process.argv[1] && process.argv[1].endsWith("schema.ts")) {
502
+ const { writeFileSync } = await import("node:fs");
503
+ const { resolve, dirname } = await import("node:path");
504
+
505
+ const schemaDir = dirname(resolve(process.argv[1], ".."));
506
+
507
+ const componentSchema = await generateSchema();
508
+ const projectSchema = generateProjectSchema();
509
+ const classSchema = generateClassSchema();
510
+
511
+ const componentStr = JSON.stringify(componentSchema, null, 2);
512
+ const projectStr = JSON.stringify(projectSchema, null, 2);
513
+ const classStr = JSON.stringify(classSchema, null, 2);
514
+
515
+ const [, , out] = process.argv;
516
+
517
+ if (out) {
518
+ writeFileSync(out, componentStr, "utf8");
519
+ console.error(`Jx component schema written to ${out}`);
520
+ } else {
521
+ writeFileSync(resolve(schemaDir, "schema.json"), componentStr, "utf8");
522
+ writeFileSync(resolve(schemaDir, "project-schema.json"), projectStr, "utf8");
523
+ writeFileSync(resolve(schemaDir, "class-schema.json"), classStr, "utf8");
524
+ console.error("Generated:");
525
+ console.error(" schema.json (component)");
526
+ console.error(" project-schema.json");
527
+ console.error(" class-schema.json");
528
+ }
529
+ }