@jxsuite/schema 0.28.2 → 0.28.4
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/class-schema.json +341 -341
- package/defs/cem.schema.ts +11 -11
- package/defs/children-value.schema.ts +6 -6
- package/defs/class-def.schema.ts +131 -131
- package/defs/content-type-def.schema.ts +18 -18
- package/defs/element-def.schema.ts +36 -36
- package/defs/expression-node.schema.ts +37 -37
- package/defs/external-class-def.schema.ts +41 -41
- package/defs/function-def.schema.ts +31 -31
- package/defs/head-entry.schema.ts +6 -6
- package/defs/image-config.schema.ts +16 -16
- package/defs/json-schema-type.schema.ts +1 -1
- package/defs/project-config.schema.ts +122 -122
- package/defs/pure-type-def.schema.ts +12 -12
- package/defs/ref-object.schema.ts +19 -19
- package/defs/state-entry.schema.ts +3 -3
- package/defs/style-object.schema.ts +7 -7
- package/defs/tag-name.schema.ts +1 -1
- package/defs/typed-state-def.schema.ts +14 -14
- package/package.json +3 -1
- package/project-schema.json +284 -284
- package/schema.json +987 -987
- package/src/format-registry.ts +28 -13
- package/src/guards.ts +195 -0
- package/src/parse.ts +57 -0
- package/src/schema.ts +229 -225
- package/types.ts +340 -51
package/src/format-registry.ts
CHANGED
|
@@ -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)
|
|
69
|
+
loadJson: (pathOrUrl: string) => Promise<Record<string, unknown>>;
|
|
70
70
|
/** Import a JS module by path or URL. */
|
|
71
|
-
importModule(pathOrUrl: string)
|
|
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)
|
|
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
|
|
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])
|
|
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))
|
|
212
|
-
|
|
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"))
|
|
238
|
-
|
|
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))
|
|
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
|
+
}
|