@jxsuite/schema 1.2.0 → 1.3.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.
@@ -105,6 +105,24 @@
105
105
  },
106
106
  "required": ["$ref"],
107
107
  "type": "object"
108
+ },
109
+ "StyleObject": {
110
+ "additionalProperties": {
111
+ "oneOf": [
112
+ {
113
+ "type": "string"
114
+ },
115
+ {
116
+ "type": "number"
117
+ },
118
+ {
119
+ "$ref": "#/$defs/StyleObject"
120
+ }
121
+ ]
122
+ },
123
+ "description": "CSS style definition. camelCase property names follow CSSOM convention. Keys starting with :, ., &, or [ are treated as nested CSS selectors. Keys matching $media breakpoint names (or other @-prefixed at-rules) group nested rules. Nesting is recursive: selector and at-rule groups may nest to arbitrary depth (e.g. breakpoint → selector → pseudo-class), mirroring the compiler's recursive emission.",
124
+ "properties": {},
125
+ "type": "object"
108
126
  }
109
127
  },
110
128
  "$id": "https://jxsuite.com/schema/project/core/v2",
@@ -366,11 +384,11 @@
366
384
  "type": "number"
367
385
  },
368
386
  {
369
- "type": "object"
387
+ "$ref": "#/$defs/StyleObject"
370
388
  }
371
389
  ]
372
390
  },
373
- "description": "Global CSS styles applied to the <body> element. Uses the same camelCase CSSOM convention as component styles.",
391
+ "description": "Global CSS styles applied to the <body> element. Uses the same camelCase CSSOM convention and recursive selector/at-rule nesting as component styles.",
374
392
  "type": "object"
375
393
  },
376
394
  "url": {
package/src/guards.ts CHANGED
@@ -174,6 +174,16 @@ export function isTemplateString(value?: unknown): value is string {
174
174
  return typeof value === "string" && value.includes("${");
175
175
  }
176
176
 
177
+ /**
178
+ * Whether a function `body` returns a value — the heuristic that classifies a `$prototype:
179
+ * "Function"` as a computed value vs an event handler. Uses a word boundary so identifiers like
180
+ * `returned` do not match. Shared by the runtime interpreter and the compiler so both agree on the
181
+ * classification.
182
+ */
183
+ export function bodyReturnsValue(body: string): boolean {
184
+ return /\breturn\b/.test(body);
185
+ }
186
+
177
187
  // ─── Style guards & accessors ───────────────────────────────────────────────────
178
188
 
179
189
  /** A nested style object (selector/media block), as opposed to a scalar CSS value. */
package/src/schema.ts CHANGED
@@ -92,7 +92,10 @@ import {
92
92
  classFieldDefSchema,
93
93
  classMethodDefSchema,
94
94
  classParameterDefSchema,
95
+ connectorBlockDefSchema,
95
96
  formatDefSchema,
97
+ projectBlockDefSchema,
98
+ serverBlockDefSchema,
96
99
  studioHintsSchema,
97
100
  } from "../defs/class-def.schema";
98
101
  import { extensionManifestSchema } from "../defs/extension-manifest.schema";
@@ -182,7 +185,11 @@ function buildEventHandlerProperties(eventHandlers: string[]) {
182
185
  for (const name of eventHandlers) {
183
186
  properties[name] = {
184
187
  description: `Event handler for the "${name.slice(2)}" event.`,
185
- oneOf: [{ $ref: "#/$defs/RefObject" }, { $ref: "#/$defs/ExpressionEntry" }],
188
+ oneOf: [
189
+ { $ref: "#/$defs/RefObject" },
190
+ { $ref: "#/$defs/ExpressionEntry" },
191
+ { $ref: "#/$defs/FunctionDef" },
192
+ ],
186
193
  };
187
194
  }
188
195
  return properties;
@@ -216,6 +223,9 @@ export async function generateSchema() {
216
223
  ClassConstructorDef: classConstructorDefSchema,
217
224
  ClassMethodDef: classMethodDefSchema,
218
225
  FormatDef: formatDefSchema,
226
+ ProjectBlockDef: projectBlockDefSchema,
227
+ ServerBlockDef: serverBlockDefSchema,
228
+ ConnectorBlockDef: connectorBlockDefSchema,
219
229
  StudioHints: studioHintsSchema,
220
230
  ExternalClassDef: externalClassDefSchema,
221
231
  ExpressionPointer: expressionPointerSchema,
@@ -415,6 +425,7 @@ export function generateProjectSchema() {
415
425
  return {
416
426
  $defs: {
417
427
  ImageConfig: imageConfigSchema,
428
+ StyleObject: styleObjectSchema,
418
429
  },
419
430
  $id: "https://jxsuite.com/schema/project/v1",
420
431
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -446,6 +457,7 @@ export function generateProjectCoreSchema() {
446
457
  ImageConfig: imageConfigSchema,
447
458
  JxFieldSchema: jxFieldSchemaDef,
448
459
  RelationshipRef: relationshipRefSchema,
460
+ StyleObject: styleObjectSchema,
449
461
  },
450
462
  $id: "https://jxsuite.com/schema/project/core/v2",
451
463
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -514,7 +526,10 @@ export function generateClassSchema() {
514
526
  ClassFieldDef: classFieldDefSchema,
515
527
  ClassMethodDef: classMethodDefSchema,
516
528
  ClassParameterDef: classParameterDefSchema,
529
+ ConnectorBlockDef: connectorBlockDefSchema,
517
530
  FormatDef: formatDefSchema,
531
+ ProjectBlockDef: projectBlockDefSchema,
532
+ ServerBlockDef: serverBlockDefSchema,
518
533
  StudioHints: studioHintsSchema,
519
534
  },
520
535
  $id: "https://jxsuite.com/schema/class/v1",
@@ -565,6 +580,7 @@ export function generateClassSchema() {
565
580
  },
566
581
  $schema: { type: "string" },
567
582
  $studio: { $ref: "#/$defs/StudioHints" },
583
+ connector: { $ref: "#/$defs/ConnectorBlockDef" },
568
584
  description: { type: "string" },
569
585
  extends: {
570
586
  description: "Base class — string name or $ref to another .class.json.",
@@ -579,6 +595,8 @@ export function generateClassSchema() {
579
595
  ],
580
596
  },
581
597
  format: { $ref: "#/$defs/FormatDef" },
598
+ project: { $ref: "#/$defs/ProjectBlockDef" },
599
+ server: { $ref: "#/$defs/ServerBlockDef" },
582
600
  title: {
583
601
  description: "PascalCase class name, used as the export name.",
584
602
  examples: ["MarkdownFile", "DataSource", "Calculator"],
@@ -613,7 +631,19 @@ type AjvCtor = new (opts: {
613
631
  }) => AjvInstance;
614
632
  type AddFormatsFn = (ajv: AjvInstance) => void;
615
633
 
616
- export async function validateDocument(doc: Record<string, unknown>) {
634
+ /**
635
+ * Validate a document against an arbitrary JSON Schema 2020-12 document (e.g. a bundled per-project
636
+ * document.schema.json from `jx schema`). The schema must be self-contained — external refs are not
637
+ * resolved here.
638
+ *
639
+ * @param {Record<string, unknown>} doc - The document to validate
640
+ * @param {Record<string, unknown>} schema - A self-contained 2020-12 schema
641
+ * @returns {Promise<{ errors: unknown[] | null; valid: boolean }>}
642
+ */
643
+ export async function validateWithSchema(
644
+ doc: Record<string, unknown>,
645
+ schema: Record<string, unknown>,
646
+ ) {
617
647
  let Ajv: AjvCtor, addFormats: AddFormatsFn;
618
648
  try {
619
649
  // The generated schema is JSON Schema 2020-12, so use the matching Ajv build
@@ -628,13 +658,21 @@ export async function validateDocument(doc: Record<string, unknown>) {
628
658
  const ajv = new Ajv({ allErrors: true, ownProperties: true, strict: false });
629
659
  addFormats(ajv);
630
660
 
631
- const schema = await generateSchema();
632
661
  const validate = ajv.compile(schema);
633
662
  const valid = validate(doc);
634
663
 
635
664
  return { errors: validate.errors ?? null, valid };
636
665
  }
637
666
 
667
+ export async function validateDocument(doc: Record<string, unknown>) {
668
+ return validateWithSchema(doc, (await generateSchema()) as Record<string, unknown>);
669
+ }
670
+
671
+ /** Validate a .class.json class definition against the generated class schema. */
672
+ export async function validateClass(doc: Record<string, unknown>) {
673
+ return validateWithSchema(doc, generateClassSchema() as Record<string, unknown>);
674
+ }
675
+
638
676
  // ─── CLI ──────────────────────────────────────────────────────────────────────
639
677
 
640
678
  async function runSchemaCli() {
@@ -133,6 +133,19 @@ async function loadProjectSchemaRef(uri: string, root: string): Promise<Record<s
133
133
  const selfRequire = createRequire(import.meta.url);
134
134
  resolved = selfRequire.resolve(specifier);
135
135
  } catch (error) {
136
+ // Exports-map subpath resolution is brittle against version-skewed shadowing installs (a
137
+ // Stale local copy terminates Node's walk-up with ERR_PACKAGE_PATH_NOT_EXPORTED). Our own
138
+ // Schema files are pure data and the running validator's copy is guaranteed to match, so
139
+ // Read them straight off this package's disk as the last resort.
140
+ const ownPrefix = "@jxsuite/schema/";
141
+ if (specifier.startsWith(ownPrefix)) {
142
+ const ownFile = fileURLToPath(
143
+ new URL(`../${specifier.slice(ownPrefix.length)}`, import.meta.url),
144
+ );
145
+ if (existsSync(ownFile)) {
146
+ return JSON.parse(readFileSync(ownFile, "utf8")) as Record<string, unknown>;
147
+ }
148
+ }
136
149
  throw new Error(
137
150
  `Schema ref "${uri}" does not exist and "${specifier}" is not resolvable from the ` +
138
151
  `project or the host`,