@jxsuite/schema 3.0.0 → 4.1.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.
@@ -18,6 +18,8 @@ import { createRequire } from "node:module";
18
18
  import { isAbsolute, relative, resolve } from "node:path";
19
19
  import { fileURLToPath, pathToFileURL } from "node:url";
20
20
 
21
+ import type { JsonObject, JsonValue } from "../types";
22
+
21
23
  export interface ProjectValidationResult {
22
24
  valid: boolean;
23
25
  errors: unknown[] | null;
@@ -26,18 +28,29 @@ export interface ProjectValidationResult {
26
28
  // Minimal structural types for the optional `ajv` peer dependency (no direct dependency, and it
27
29
  // Ships no types usable from here) — same pattern as validateDocument in src/schema.ts.
28
30
  interface AjvValidateFn {
29
- (doc: unknown): boolean;
31
+ (doc: JsonValue): boolean;
30
32
  errors?: unknown[] | null;
31
33
  }
32
34
  interface AjvInstance {
33
- compileAsync: (schema: unknown) => Promise<AjvValidateFn>;
35
+ compileAsync: (schema: JsonValue) => Promise<AjvValidateFn>;
34
36
  }
35
37
  type AjvCtor = new (opts: {
36
38
  allErrors: boolean;
37
39
  strict: boolean;
38
- loadSchema: (uri: string) => Promise<Record<string, unknown>>;
40
+ loadSchema: (uri: string) => Promise<JsonValue>;
39
41
  }) => AjvInstance;
40
42
 
43
+ /**
44
+ * Read and parse a JSON file. `JSON.parse` without a reviver only ever produces JSON values, so the
45
+ * declared type is exact; what shape of JSON it is stays for the caller to check.
46
+ *
47
+ * @param {string} path - Absolute path to a JSON file
48
+ * @returns {JsonValue}
49
+ */
50
+ function readJson(path: string): JsonValue {
51
+ return JSON.parse(readFileSync(path, "utf8"));
52
+ }
53
+
41
54
  /**
42
55
  * Validate `<projectRoot>/project.json` against `<projectRoot>/project.schema.json`.
43
56
  *
@@ -62,15 +75,25 @@ export async function validateProjectFile(projectRoot: string): Promise<ProjectV
62
75
  );
63
76
  }
64
77
 
65
- const project = JSON.parse(readFileSync(projectPath, "utf8")) as Record<string, unknown>;
66
- const entrySchema = JSON.parse(readFileSync(schemaPath, "utf8")) as Record<string, unknown>;
78
+ const project = readJson(projectPath);
79
+ /*
80
+ * SAFETY: project.schema.json is written by `jx schema` (emitProjectSchema), which always emits a
81
+ * JSON object. A hand-damaged file that is not one fails loudly either way: the `$id` write below
82
+ * throws on a primitive in this strict-mode module, and ajv rejects an array as a schema.
83
+ */
84
+ const entrySchema = readJson(schemaPath) as JsonObject;
67
85
  // The committed entry document is machine-path-free; injecting its file URL as $id makes its
68
86
  // Relative ./node_modules/... refs resolve against the project root.
69
87
  entrySchema.$id = pathToFileURL(schemaPath).href;
70
88
 
71
89
  let Ajv: AjvCtor;
72
90
  try {
73
- // The entry document is JSON Schema 2020-12, so use the matching Ajv build.
91
+ /*
92
+ * The entry document is JSON Schema 2020-12, so use the matching Ajv build.
93
+ * SAFETY: the default export of `ajv/dist/2020` is the Ajv2020 class. AjvCtor restates, in
94
+ * JSON terms, only the three constructor options, `compileAsync` and the validate function's
95
+ * call and `errors` used below; ajv reports a schema value it cannot use as a thrown error.
96
+ */
74
97
  ({ default: Ajv } = (await import("ajv/dist/2020")) as { default: AjvCtor });
75
98
  } catch (error) {
76
99
  throw new Error("Project validation requires ajv: bun add ajv", { cause: error });
@@ -94,9 +117,9 @@ export async function validateProjectFile(projectRoot: string): Promise<ProjectV
94
117
  *
95
118
  * @param {string} uri - The `$ref` target ajv could not resolve from already-loaded resources
96
119
  * @param {string} root - Absolute project root
97
- * @returns {Promise<Record<string, unknown>>}
120
+ * @returns {Promise<JsonValue>}
98
121
  */
99
- async function loadProjectSchemaRef(uri: string, root: string): Promise<Record<string, unknown>> {
122
+ async function loadProjectSchemaRef(uri: string, root: string): Promise<JsonValue> {
100
123
  if (!uri.startsWith("file:")) {
101
124
  throw new Error(
102
125
  `Cannot resolve schema ref "${uri}": only project-relative file refs are loadable ` +
@@ -111,7 +134,7 @@ async function loadProjectSchemaRef(uri: string, root: string): Promise<Record<s
111
134
  }
112
135
 
113
136
  if (existsSync(filePath)) {
114
- return JSON.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
137
+ return readJson(filePath);
115
138
  }
116
139
 
117
140
  // Host fallback: <root>/node_modules/<specifier> is absent — resolve the specifier through the
@@ -143,7 +166,7 @@ async function loadProjectSchemaRef(uri: string, root: string): Promise<Record<s
143
166
  new URL(`../${specifier.slice(ownPrefix.length)}`, import.meta.url),
144
167
  );
145
168
  if (existsSync(ownFile)) {
146
- return JSON.parse(readFileSync(ownFile, "utf8")) as Record<string, unknown>;
169
+ return readJson(ownFile);
147
170
  }
148
171
  }
149
172
  throw new Error(
@@ -153,5 +176,5 @@ async function loadProjectSchemaRef(uri: string, root: string): Promise<Record<s
153
176
  );
154
177
  }
155
178
  }
156
- return JSON.parse(readFileSync(resolved, "utf8")) as Record<string, unknown>;
179
+ return readJson(resolved);
157
180
  }
package/types.ts CHANGED
@@ -62,13 +62,54 @@ export interface JsonObject {
62
62
  [key: string]: JsonValue;
63
63
  }
64
64
 
65
+ /**
66
+ * What a Jx document object holds under a key its interface leaves open.
67
+ *
68
+ * Documents are JSON, so this is the JSON value space widened by the typed shapes this module's
69
+ * declared keys use (an interface is never assignable to {@link JsonObject}). Each index signature
70
+ * adds `undefined`, for a key an editor has cleared. Narrow it with the guards in
71
+ * `@jxsuite/schema/guards`.
72
+ */
73
+ export type JxDocumentValue =
74
+ | JsonPrimitive
75
+ | JxRef
76
+ | JxExpressionNode
77
+ | JxExpressionDef
78
+ | JxStatement
79
+ | CemParameter
80
+ | CemEvent
81
+ | JxFunctionDef
82
+ | JxServerFnDef
83
+ | JxPrototypeDef
84
+ | JxStateObject
85
+ | JxHeadEntry
86
+ | JxStyle
87
+ | JxElement
88
+ | JxMutableNode
89
+ | JxMappedArray
90
+ | { $expression: JxTagExpression }
91
+ | JxClassParamDef
92
+ | JxClassFieldDef
93
+ | JxClassCtorDef
94
+ | JxClassMethodDef
95
+ | JxClassDefs
96
+ | ImageConfig
97
+ | DeployConfig
98
+ | HeadersConfig
99
+ | ManifestConfig
100
+ | SecurityTxtConfig
101
+ | ServiceWorkerConfig
102
+ | JxDocumentValue[]
103
+ | { [key: string]: JxDocumentValue };
104
+
65
105
  // ─── Binding Model ──────────────────────────────────────────────────────────────
66
106
  // Any bindable position in a document holds either a literal value or a JSON
67
107
  // Pointer reference into the reactive scope.
68
108
 
69
109
  /**
70
- * A JSON Pointer reference into document state, e.g. `{ $ref: "#/state/count" }`. (A type literal
71
- * rather than an interface so it stays assignable to JsonValue.)
110
+ * A JSON Pointer reference into document state, e.g. `{ $ref: "#/state/count" }`. Every member is
111
+ * JSON, but as an interface it has no implicit index signature, so it is not assignable to
112
+ * `JsonObject`; {@link JxDocumentValue} names it for that reason.
72
113
  */
73
114
  export interface JxRef {
74
115
  $ref: string;
@@ -111,7 +152,7 @@ export interface JxExpressionDef {
111
152
  * callable formula invoked via the `call` operator, with `$args/<name>` refs in its body.
112
153
  */
113
154
  parameters?: (string | CemParameter)[];
114
- [key: string]: unknown;
155
+ [key: string]: JxDocumentValue | undefined;
115
156
  }
116
157
 
117
158
  // ─── State Definition Model ─────────────────────────────────────────────────────
@@ -197,7 +238,7 @@ export interface JxFunctionDef {
197
238
  emits?: CemEvent[];
198
239
  description?: string;
199
240
  deprecated?: string | boolean;
200
- [key: string]: unknown;
241
+ [key: string]: JxDocumentValue | undefined;
201
242
  }
202
243
 
203
244
  /** A `timing: "server"` function proxy — executed server-side, no `$prototype`. */
@@ -209,7 +250,7 @@ export interface JxServerFnDef {
209
250
  /** Named arguments forwarded to the server function: literals or `$ref` bindings. */
210
251
  arguments?: Record<string, JsonValue>;
211
252
  debounce?: number;
212
- [key: string]: unknown;
253
+ [key: string]: JxDocumentValue | undefined;
213
254
  }
214
255
 
215
256
  // ─── Consumer-Friendly Types ────────────────────────────────────────────────────
@@ -223,9 +264,9 @@ export interface JxHeadEntry {
223
264
  * Element content. An **object** is a structured data block — the compiler serializes it to JSON
224
265
  * inside the tag, which is how JSON-LD is authored (site-architecture.md §8.5).
225
266
  */
226
- textContent?: string | Record<string, unknown>;
267
+ textContent?: string | JsonObject;
227
268
  children?: (JxHeadEntry | string)[];
228
- [key: string]: unknown;
269
+ [key: string]: JxDocumentValue | undefined;
229
270
  }
230
271
 
231
272
  export interface ImageConfig {
@@ -241,7 +282,7 @@ export interface ImageConfig {
241
282
  remoteDomains?: string[];
242
283
  }
243
284
 
244
- /** A CEM-compatible function parameter. JSON-precise (assignable to JsonValue). */
285
+ /** A CEM-compatible function parameter. Every member is JSON (see {@link JxDocumentValue}). */
245
286
  export interface CemParameter {
246
287
  name: string;
247
288
  /** Parameter type — JSON Schema or CEM `{ text }` format. */
@@ -251,7 +292,7 @@ export interface CemParameter {
251
292
  default?: JsonValue;
252
293
  }
253
294
 
254
- /** A CEM-compatible event a function dispatches. JSON-precise (assignable to JsonValue). */
295
+ /** A CEM-compatible event a function dispatches. Every member is JSON (see {@link JxDocumentValue}). */
255
296
  export interface CemEvent {
256
297
  name: string;
257
298
  type?: JsonValue;
@@ -281,8 +322,13 @@ export interface JxStyle {
281
322
  * template and not a third `$switch` spelling.
282
323
  */
283
324
  export type JxTagExpression =
284
- | { operator: "?:"; target: unknown; value: string; initial: string }
285
- | { operator: "switch"; target: unknown; cases: Record<string, string>; default: string };
325
+ | { operator: "?:"; target: JxExpressionOperand; value: string; initial: string }
326
+ | {
327
+ operator: "switch";
328
+ target: JxExpressionOperand;
329
+ cases: Record<string, string>;
330
+ default: string;
331
+ };
286
332
 
287
333
  /**
288
334
  * An ELEMENT's tag. The document root's and a head entry's stay `string` — see
@@ -323,7 +369,7 @@ export interface JxElement {
323
369
  $src?: string;
324
370
  observedAttributes?: string[];
325
371
  state?: Record<string, JxStateDefinition>;
326
- [key: string]: unknown;
372
+ [key: string]: JxDocumentValue | undefined;
327
373
  }
328
374
 
329
375
  export interface JxMappedArray {
@@ -400,7 +446,7 @@ export interface JxStateObject {
400
446
  /** Whether property changes reflect back to the HTML attribute. */
401
447
  reflects?: boolean;
402
448
  deprecated?: string | boolean;
403
- [key: string]: unknown;
449
+ [key: string]: JxDocumentValue | undefined;
404
450
  }
405
451
 
406
452
  /** A `$prototype` instance entry (Request, Storage, ContentCollection, custom classes, …). */
@@ -443,7 +489,7 @@ export interface JxPrototypeDef {
443
489
  indexes?: { name: string; keyPath: string; unique?: boolean }[];
444
490
  /** FormData prototype config. */
445
491
  fields?: Record<string, JsonValue>;
446
- [key: string]: unknown;
492
+ [key: string]: JxDocumentValue | undefined;
447
493
  }
448
494
 
449
495
  // ─── Event Binding Model ────────────────────────────────────────────────────────
@@ -470,11 +516,11 @@ export type ClassConstructorDef = FromSchema<typeof classConstructorDefSchema>;
470
516
  export interface JxClassParamDef {
471
517
  identifier?: string;
472
518
  $ref?: string;
473
- type?: unknown;
519
+ type?: JsonValue;
474
520
  format?: string;
475
521
  default?: JsonValue;
476
522
  description?: string;
477
- [key: string]: unknown;
523
+ [key: string]: JxDocumentValue | undefined;
478
524
  }
479
525
 
480
526
  export interface JxClassFieldDef {
@@ -482,12 +528,12 @@ export interface JxClassFieldDef {
482
528
  access?: "public" | "private" | "protected";
483
529
  scope?: "instance" | "static";
484
530
  identifier?: string;
485
- type?: unknown;
531
+ type?: JsonValue;
486
532
  $prototype?: string;
487
533
  initializer?: JsonValue;
488
534
  default?: JsonValue;
489
535
  description?: string;
490
- [key: string]: unknown;
536
+ [key: string]: JxDocumentValue | undefined;
491
537
  }
492
538
 
493
539
  export interface JxClassCtorDef {
@@ -497,7 +543,7 @@ export interface JxClassCtorDef {
497
543
  superCall?: { arguments?: string[] };
498
544
  body?: string | string[];
499
545
  description?: string;
500
- [key: string]: unknown;
546
+ [key: string]: JxDocumentValue | undefined;
501
547
  }
502
548
 
503
549
  export interface JxClassMethodDef {
@@ -513,7 +559,7 @@ export interface JxClassMethodDef {
513
559
  getter?: { body?: string };
514
560
  setter?: { parameters?: JxClassParamDef[]; body?: string };
515
561
  description?: string;
516
- [key: string]: unknown;
562
+ [key: string]: JxDocumentValue | undefined;
517
563
  }
518
564
 
519
565
  /** A .class.json document: `$prototype: "Class"` with fields/constructor/methods in `$defs`. */
@@ -525,20 +571,24 @@ export interface JxClassDef {
525
571
  description?: string;
526
572
  extends?: string | JxRef;
527
573
  $implementation?: string;
528
- $defs?: {
529
- fields?: Record<string, JxClassFieldDef>;
530
- /**
531
- * The constructor definition. On objects parsed from JSON without an explicit "constructor"
532
- * key, plain property access yields `Object.prototype.constructor` (a Function) — narrow with
533
- * `typeof === "object"` before use.
534
- */
535
- // oxlint-disable-next-line typescript/ban-types, typescript/no-unsafe-function-type -- only `Function` absorbs the inherited Object.prototype.constructor on plain object literals
536
- constructor?: JxClassCtorDef | Function;
537
- methods?: Record<string, JxClassMethodDef>;
538
- parameters?: Record<string, JxClassParamDef>;
539
- [key: string]: unknown;
540
- };
541
- [key: string]: unknown;
574
+ $defs?: JxClassDefs;
575
+ [key: string]: JxDocumentValue | undefined;
576
+ }
577
+
578
+ /** The `$defs` of a .class.json document: the class's members, by role. */
579
+ export interface JxClassDefs {
580
+ fields?: Record<string, JxClassFieldDef>;
581
+ /**
582
+ * The constructor definition. On objects parsed from JSON without an explicit "constructor" key,
583
+ * plain property access yields `Object.prototype.constructor` (a Function) — narrow with `typeof
584
+ * === "object"` before use.
585
+ */
586
+ // oxlint-disable-next-line typescript/ban-types, typescript/no-unsafe-function-type -- only `Function` absorbs the inherited Object.prototype.constructor on plain object literals
587
+ constructor?: JxClassCtorDef | Function;
588
+ methods?: Record<string, JxClassMethodDef>;
589
+ parameters?: Record<string, JxClassParamDef>;
590
+ /** Any other definitions a class document keeps beside its members. */
591
+ [key: string]: JxDocumentValue | JxClassDefs["constructor"];
542
592
  }
543
593
 
544
594
  /** The adapters the compiler actually implements (site-loader VALID_ADAPTERS + "static"). */
@@ -669,11 +719,11 @@ export interface ProjectConfig {
669
719
  $schema?: string;
670
720
  name?: string;
671
721
  url?: string;
672
- state?: Record<string, unknown>;
722
+ state?: Record<string, JxStateDefinition>;
673
723
  $media?: Record<string, string>;
674
724
  $elements?: (string | JxElement)[];
675
725
  $head?: JxHeadEntry[];
676
- $defs?: Record<string, unknown>;
726
+ $defs?: Record<string, JsonValue>;
677
727
  build?: {
678
728
  adapter?: AdapterId | (string & Record<never, never>);
679
729
  deploy?: DeployConfig;
@@ -686,7 +736,7 @@ export interface ProjectConfig {
686
736
  * two bundler backends agree on semantics but not on minified bytes.
687
737
  */
688
738
  minify?: boolean;
689
- [key: string]: unknown;
739
+ [key: string]: JxDocumentValue | undefined;
690
740
  };
691
741
  images?: ImageConfig;
692
742
  imports?: Record<string, string>;
@@ -731,10 +781,10 @@ export interface ProjectConfig {
731
781
  */
732
782
  shadow?: "open" | "closed" | false;
733
783
  charset?: string;
734
- [key: string]: unknown;
784
+ [key: string]: JxDocumentValue | undefined;
735
785
  };
736
786
  style?: JxStyle;
737
- [key: string]: unknown;
787
+ [key: string]: JxDocumentValue | undefined;
738
788
  }
739
789
 
740
790
  // ─── Editor/Mutation Types ──────────────────────────────────────────────────────
@@ -751,13 +801,13 @@ export type JxAttributeValue = Bindable<string | number | boolean>;
751
801
  export type JxPathsDef =
752
802
  | { values: JsonValue[]; param?: string }
753
803
  | { $ref: string; param?: string; field?: string }
754
- | { param?: string; field?: string; [discriminator: string]: unknown }
804
+ | { param?: string; field?: string; [discriminator: string]: JsonValue | undefined }
755
805
  | Record<string, JsonValue>[];
756
806
 
757
807
  /**
758
808
  * The editor's working representation of a document node. Known properties are fully typed;
759
- * unrecognized keys (open schema) surface as `unknown` and must be narrowed with the guards in
760
- * `@jxsuite/schema/guards` before use.
809
+ * unrecognized keys (open schema) surface as {@link JxDocumentValue} and must be narrowed with the
810
+ * guards in `@jxsuite/schema/guards` before use.
761
811
  */
762
812
  export interface JxMutableNode {
763
813
  tagName?: JxElementTagName;
@@ -798,7 +848,7 @@ export interface JxMutableNode {
798
848
  $defs?: Record<string, JsonValue>;
799
849
  imports?: Record<string, string>;
800
850
  observedAttributes?: string[];
801
- [key: string]: unknown;
851
+ [key: string]: JxDocumentValue | undefined;
802
852
  }
803
853
 
804
854
  // ─── Paths ──────────────────────────────────────────────────────────────────────
@@ -817,13 +867,20 @@ export interface ContentTypeSchemaField {
817
867
  items?: ContentTypeSchemaField;
818
868
  required?: string[];
819
869
  description?: string;
820
- [key: string]: unknown;
870
+ [key: string]: ContentTypeSchemaValue | undefined;
821
871
  }
822
872
 
873
+ /** A keyword's value inside a content-type schema: JSON, or a nested field schema. */
874
+ export type ContentTypeSchemaValue =
875
+ | JsonPrimitive
876
+ | ContentTypeSchemaField
877
+ | ContentTypeSchemaValue[]
878
+ | { [key: string]: ContentTypeSchemaValue };
879
+
823
880
  export interface ContentTypeSchema {
824
881
  properties?: Record<string, ContentTypeSchemaField>;
825
882
  required?: string[];
826
- [key: string]: unknown;
883
+ [key: string]: ContentTypeSchemaValue | undefined;
827
884
  }
828
885
 
829
886
  // ─── Content Wire Types ─────────────────────────────────────────────────────────
@@ -838,10 +895,22 @@ export interface TocEntry {
838
895
  id: string;
839
896
  }
840
897
 
898
+ /**
899
+ * A field of a loaded content entry's `data`: what front matter parses to, plus the entry a
900
+ * reference field resolves to. YAML has a timestamp type the parser returns as a `Date`, so this is
901
+ * wider than {@link JsonValue}.
902
+ */
903
+ export type ContentFieldValue =
904
+ | JsonPrimitive
905
+ | Date
906
+ | ContentLoaderEntry
907
+ | ContentFieldValue[]
908
+ | { [key: string]: ContentFieldValue };
909
+
841
910
  /** One loaded content entry, as produced by a format class's `load` capability. */
842
911
  export interface ContentLoaderEntry {
843
912
  id: string;
844
- data: Record<string, unknown>;
913
+ data: Record<string, ContentFieldValue | undefined>;
845
914
  body: string | null;
846
915
  $children?: (JxElement | string)[];
847
916
  _meta?: {
@@ -856,7 +925,7 @@ export interface ContentLoaderEntry {
856
925
  */
857
926
  mtime?: string;
858
927
  /** Authored text of a field the date-coercion pass rewrote, keyed by field name. */
859
- rawDates?: Record<string, unknown>;
928
+ rawDates?: Record<string, ContentFieldValue>;
860
929
  /**
861
930
  * The entry's source file, relative to its content type's source directory, `/`-separated and
862
931
  * with its extension (`Frappe/Bench Operations.md`). Stamped by the content loader for every