@jxsuite/schema 0.28.2 → 0.28.3

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.
@@ -66,11 +66,11 @@ export interface CapabilityInfo {
66
66
 
67
67
  export interface FormatHostIO {
68
68
  /** Read and parse a JSON file or URL. */
69
- loadJson(pathOrUrl: string): Promise<Record<string, unknown>>;
69
+ loadJson: (pathOrUrl: string) => Promise<Record<string, unknown>>;
70
70
  /** Import a JS module by path or URL. */
71
- importModule(pathOrUrl: string): Promise<Record<string, unknown>>;
71
+ importModule: (pathOrUrl: string) => Promise<Record<string, unknown>>;
72
72
  /** Resolve a relative reference against a base (node: path.resolve; browser: new URL). */
73
- resolvePath(base: string, ref: string): string;
73
+ resolvePath: (base: string, ref: string) => string;
74
74
  }
75
75
 
76
76
  interface ClassMethodLike {
@@ -149,7 +149,7 @@ export class FormatEntry {
149
149
  | undefined;
150
150
  const fn = Cls?.[cap.identifier];
151
151
  if (typeof fn !== "function") {
152
- throw new Error(
152
+ throw new TypeError(
153
153
  `Format class "${this.name}": implementation export "${title}" has no static "${cap.identifier}" method`,
154
154
  );
155
155
  }
@@ -167,7 +167,9 @@ export class FormatRegistry {
167
167
  for (const cap of FORMAT_CAPABILITIES) {
168
168
  const seen = new Map<string, FormatEntry>();
169
169
  for (const entry of entries) {
170
- if (!entry.capabilities[cap]) continue;
170
+ if (!entry.capabilities[cap]) {
171
+ continue;
172
+ }
171
173
  for (const ext of entry.extensions) {
172
174
  const prior = seen.get(ext);
173
175
  if (prior) {
@@ -208,8 +210,12 @@ export class FormatRegistry {
208
210
  documentExtensions(kind?: FormatDocumentKind): string[] {
209
211
  const exts = new Set<string>();
210
212
  for (const entry of this.#entries) {
211
- if (kind && !entry.documentKinds.includes(kind)) continue;
212
- for (const ext of entry.extensions) exts.add(ext);
213
+ if (kind && !entry.documentKinds.includes(kind)) {
214
+ continue;
215
+ }
216
+ for (const ext of entry.extensions) {
217
+ exts.add(ext);
218
+ }
213
219
  }
214
220
  exts.delete(".json");
215
221
  return [...exts];
@@ -234,16 +240,23 @@ export async function buildFormatRegistry(
234
240
  ): Promise<FormatRegistry> {
235
241
  const entries: FormatEntry[] = [];
236
242
  for (const [name, ref] of Object.entries(imports ?? {})) {
237
- if (typeof ref !== "string" || !ref.endsWith(".class.json")) continue;
238
- const classPath = base ? io.resolvePath(base, ref) : ref;
243
+ if (typeof ref !== "string" || !ref.endsWith(".class.json")) {
244
+ continue;
245
+ }
246
+ let classPath: string;
239
247
  let classDef: ClassDefLike;
240
248
  try {
249
+ classPath = base ? io.resolvePath(base, ref) : ref;
241
250
  classDef = (await io.loadJson(classPath)) as ClassDefLike;
242
251
  } catch {
243
- continue; // unreadable imports are not format classes
252
+ continue; // Unresolvable or unreadable imports are not format classes
253
+ }
254
+ if (!classDef || typeof classDef !== "object") {
255
+ continue;
256
+ }
257
+ if (!classDef.format || !Array.isArray(classDef.format.extensions)) {
258
+ continue;
244
259
  }
245
- if (!classDef || typeof classDef !== "object") continue;
246
- if (!classDef.format || !Array.isArray(classDef.format.extensions)) continue;
247
260
  entries.push(new FormatEntry(name, classPath, classDef, io));
248
261
  }
249
262
  return new FormatRegistry(entries);
@@ -256,7 +269,9 @@ function extractCapabilities(
256
269
  const methods = classDef.$defs?.methods ?? {};
257
270
  for (const [key, method] of Object.entries(methods)) {
258
271
  const role = method.role as FormatCapability | undefined;
259
- if (!role || !FORMAT_CAPABILITIES.includes(role)) continue;
272
+ if (!role || !FORMAT_CAPABILITIES.includes(role)) {
273
+ continue;
274
+ }
260
275
  out[role] = {
261
276
  identifier: method.identifier ?? key,
262
277
  timing: (method.timing as FormatTiming[] | undefined) ?? DEFAULT_TIMING,
package/src/guards.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Type predicate guards for the Jx document model.
3
+ *
4
+ * These narrow `unknown`/union-typed values to precise domain types so call sites get real property
5
+ * checking instead of casts. Every guard mirrors the runtime's actual detection logic (see
6
+ * buildScope's five-shape algorithm in the runtime).
7
+ */
8
+
9
+ import type {
10
+ JsonObject,
11
+ JsonValue,
12
+ JxClassDef,
13
+ JxElement,
14
+ JxEventBinding,
15
+ JxExpressionDef,
16
+ JxFunctionDef,
17
+ JxMappedArray,
18
+ JxMutableNode,
19
+ JxPrototypeDef,
20
+ JxRef,
21
+ JxServerFnDef,
22
+ JxStateObject,
23
+ JxStyle,
24
+ } from "../types";
25
+
26
+ // ─── JSON shape guards ──────────────────────────────────────────────────────────
27
+
28
+ /** A plain object: not null, not an array. The base check for all object-shaped defs. */
29
+ export function isJsonObject(value: unknown): value is JsonObject {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+
33
+ // ─── Binding guards ─────────────────────────────────────────────────────────────
34
+
35
+ /** A `{ $ref: "#/..." }` JSON Pointer reference. */
36
+ export function isRef(value: unknown): value is JxRef {
37
+ return isJsonObject(value) && typeof value.$ref === "string";
38
+ }
39
+
40
+ // ─── State definition guards (the five-shape algorithm) ────────────────────────
41
+
42
+ /** Shape 4: a function declaration — `{ $prototype: "Function", body | $src }`. */
43
+ export function isFunctionDef(value: unknown): value is JxFunctionDef {
44
+ return isJsonObject(value) && value.$prototype === "Function";
45
+ }
46
+
47
+ /** Shape 5: a declarative expression entry — `{ $expression: { operator, target } }`. */
48
+ export function isExpressionDef(value: unknown): value is JxExpressionDef {
49
+ return isJsonObject(value) && isJsonObject(value.$expression);
50
+ }
51
+
52
+ /** A non-Function `$prototype` instance (Request, Storage, ContentCollection, classes, …). */
53
+ export function isPrototypeDef(value: unknown): value is JxPrototypeDef {
54
+ return (
55
+ isJsonObject(value) && typeof value.$prototype === "string" && value.$prototype !== "Function"
56
+ );
57
+ }
58
+
59
+ /** A `timing: "server"` function proxy (no `$prototype`, external `$src`/`$export`). */
60
+ export function isServerFnDef(value: unknown): value is JxServerFnDef {
61
+ return (
62
+ isJsonObject(value) &&
63
+ value.timing === "server" &&
64
+ typeof value.$src === "string" &&
65
+ typeof value.$export === "string" &&
66
+ !value.$prototype
67
+ );
68
+ }
69
+
70
+ /** Shape 2: an expanded signal — an object carrying a `default` value. */
71
+ export function isExpandedSignal(value: unknown): value is JxStateObject & { default: JsonValue } {
72
+ return isJsonObject(value) && !value.$prototype && !value.$expression && "default" in value;
73
+ }
74
+
75
+ /**
76
+ * Schema-only keywords used to detect pure type definitions (Shape 2b). An object with ONLY these
77
+ * keys and no `default` is a type def, not a signal.
78
+ */
79
+ export const SCHEMA_KEYWORDS: ReadonlySet<string> = new Set([
80
+ "type",
81
+ "enum",
82
+ "minimum",
83
+ "maximum",
84
+ "minLength",
85
+ "maxLength",
86
+ "pattern",
87
+ "format",
88
+ "items",
89
+ "properties",
90
+ "required",
91
+ "description",
92
+ "title",
93
+ "$comment",
94
+ ]);
95
+
96
+ /** Shape 2b: a pure type definition — every key is a JSON Schema keyword. */
97
+ export function isSchemaOnlyDef(value: unknown): value is JxStateObject {
98
+ if (!isJsonObject(value)) {
99
+ return false;
100
+ }
101
+ for (const k of Object.keys(value)) {
102
+ if (!SCHEMA_KEYWORDS.has(k)) {
103
+ return false;
104
+ }
105
+ }
106
+ return true;
107
+ }
108
+
109
+ /** True when an object carries at least one JSON Schema keyword. */
110
+ export function hasSchemaKeywords(value: unknown): value is JxStateObject {
111
+ if (!isJsonObject(value)) {
112
+ return false;
113
+ }
114
+ for (const k of Object.keys(value)) {
115
+ if (SCHEMA_KEYWORDS.has(k)) {
116
+ return true;
117
+ }
118
+ }
119
+ return false;
120
+ }
121
+
122
+ // ─── Document structure guards ──────────────────────────────────────────────────
123
+
124
+ /** A mapped (repeater) children object — `{ $prototype: "Array", items, map }`. */
125
+ export function isMappedArray(value: unknown): value is JxMappedArray {
126
+ return isJsonObject(value) && value.$prototype === "Array";
127
+ }
128
+
129
+ /** A .class.json schema-defined class document — `{ $prototype: "Class" }`. */
130
+ export function isClassDef(value: unknown): value is JxClassDef {
131
+ return isJsonObject(value) && value.$prototype === "Class";
132
+ }
133
+
134
+ /** An element-shaped node object (vs. a bare string/number child). */
135
+ export function isNodeObject(value: unknown): value is JxMutableNode & JxElement {
136
+ return isJsonObject(value);
137
+ }
138
+
139
+ /** A `${...}` template-expression string. */
140
+ export function isTemplateString(value?: unknown): value is string {
141
+ return typeof value === "string" && value.includes("${");
142
+ }
143
+
144
+ // ─── Style guards & accessors ───────────────────────────────────────────────────
145
+
146
+ /** A nested style object (selector/media block), as opposed to a scalar CSS value. */
147
+ export function isNestedStyle(value: string | number | JxStyle | undefined): value is JxStyle {
148
+ return typeof value === "object" && value !== null;
149
+ }
150
+
151
+ /** Read a nested style block (`:hover`, `@--md`, `& > li`, …), if present. */
152
+ export function getNestedStyle(style: JxStyle | undefined, key: string): JxStyle | undefined {
153
+ const value = style?.[key];
154
+ return isNestedStyle(value) ? value : undefined;
155
+ }
156
+
157
+ /** Read a nested style block, creating an empty one in place when absent or scalar. */
158
+ export function ensureNestedStyle(style: JxStyle, key: string): JxStyle {
159
+ const existing = style[key];
160
+ if (isNestedStyle(existing)) {
161
+ return existing;
162
+ }
163
+ const created: JxStyle = {};
164
+ style[key] = created;
165
+ return created;
166
+ }
167
+
168
+ // ─── Event binding guards ───────────────────────────────────────────────────────
169
+
170
+ /**
171
+ * The value of an `on*` document key: a `$ref`, an inline function declaration, or a declarative
172
+ * expression.
173
+ */
174
+ export function isEventBinding(value: unknown): value is JxEventBinding {
175
+ return isRef(value) || isFunctionDef(value) || isExpressionDef(value);
176
+ }
177
+
178
+ /** Read the event binding stored under an `on*` key of a node, if there is one. */
179
+ export function getEventBinding(
180
+ node: JxMutableNode | JxElement,
181
+ key: string,
182
+ ): JxEventBinding | undefined {
183
+ const value = node[key];
184
+ return isEventBinding(value) ? value : undefined;
185
+ }
186
+
187
+ // ─── Parameter helpers ──────────────────────────────────────────────────────────
188
+
189
+ /** Resolve `parameters` entries (bare strings or CEM objects) to their names. */
190
+ export function paramNames(parameters: JxFunctionDef["parameters"]): string[] {
191
+ if (!parameters) {
192
+ return [];
193
+ }
194
+ return parameters.map((p) => (typeof p === "string" ? p : p.name));
195
+ }
package/src/parse.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Typed parse boundary for Jx documents and configs.
3
+ *
4
+ * Raw `JSON.parse()` returns `any`, which silently disables checking for everything downstream.
5
+ * These helpers are the single sanctioned crossing point: they parse, structurally verify, and
6
+ * return the domain type — failures carry the source path.
7
+ */
8
+
9
+ import type { JxClassDef, JxDocument, ProjectConfig } from "../types";
10
+ import { isJsonObject } from "./guards";
11
+
12
+ // ─── Error helpers ──────────────────────────────────────────────────────────────
13
+
14
+ /** Extract a human-readable message from a caught `unknown`. */
15
+ export function errorMessage(e: unknown): string {
16
+ return e instanceof Error ? e.message : String(e);
17
+ }
18
+
19
+ /** Coerce a caught `unknown` to an Error without losing the original. */
20
+ export function toError(e: unknown): Error {
21
+ return e instanceof Error ? e : new Error(String(e));
22
+ }
23
+
24
+ // ─── Parse helpers ──────────────────────────────────────────────────────────────
25
+
26
+ function parseObject(text: string, sourcePath: string, what: string): Record<string, unknown> {
27
+ let parsed: unknown;
28
+ try {
29
+ parsed = JSON.parse(text);
30
+ } catch (error) {
31
+ throw new Error(`Failed to parse ${what} at ${sourcePath}: ${errorMessage(error)}`, {
32
+ cause: error,
33
+ });
34
+ }
35
+ if (!isJsonObject(parsed)) {
36
+ throw new Error(`Invalid ${what} at ${sourcePath}: expected a JSON object`);
37
+ }
38
+ return parsed;
39
+ }
40
+
41
+ /** Parse a Jx document (.json page/layout/component source). */
42
+ export function parseJxDocument(text: string, sourcePath: string): JxDocument {
43
+ return parseObject(text, sourcePath, "Jx document") as JxDocument;
44
+ }
45
+
46
+ /** Parse a project.json site configuration. */
47
+ export function parseProjectConfig(text: string, sourcePath: string): ProjectConfig {
48
+ return parseObject(text, sourcePath, "project config") as ProjectConfig;
49
+ }
50
+
51
+ /**
52
+ * Parse a .class.json schema-defined class. Note: `$prototype: "Class"` is the canonical
53
+ * discriminator but is not required — existing class files omit it.
54
+ */
55
+ export function parseClassDef(text: string, sourcePath: string): JxClassDef {
56
+ return parseObject(text, sourcePath, "class definition") as unknown as JxClassDef;
57
+ }