@jxsuite/schema 3.0.0 → 4.0.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/package.json +4 -4
- package/schema.json +52 -26
- package/src/a11y.ts +91 -95
- package/src/asset-paths.ts +23 -1
- package/src/component-meta.ts +123 -55
- package/src/dialogs.ts +20 -19
- package/src/doc-ops.ts +59 -35
- package/src/extension-registry.ts +15 -8
- package/src/format-registry.ts +504 -31
- package/src/guards.ts +89 -14
- package/src/ijson.ts +8 -8
- package/src/json-layout.ts +64 -25
- package/src/locale.ts +23 -18
- package/src/media-type.ts +26 -12
- package/src/overlays.ts +50 -45
- package/src/parse.ts +42 -24
- package/src/project-schemas.ts +94 -66
- package/src/schema.ts +50 -38
- package/src/validate-project.ts +34 -11
- package/types.ts +116 -47
package/src/overlays.ts
CHANGED
|
@@ -25,8 +25,16 @@
|
|
|
25
25
|
* @docs framework/concepts/overlays
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import {
|
|
29
|
-
|
|
28
|
+
import {
|
|
29
|
+
getNestedStyle,
|
|
30
|
+
isBoolean,
|
|
31
|
+
isJsonObject,
|
|
32
|
+
isNestedStyle,
|
|
33
|
+
isNodeObject,
|
|
34
|
+
isNonNullObject,
|
|
35
|
+
isString,
|
|
36
|
+
} from "./guards";
|
|
37
|
+
import type { JsonObject, JxElement, JxStyle } from "../types";
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
40
|
* The three keywords the `popover` attribute accepts.
|
|
@@ -95,24 +103,24 @@ export interface PopoverDefect {
|
|
|
95
103
|
// ─── Reading a node ─────────────────────────────────────────────────────────────
|
|
96
104
|
|
|
97
105
|
/** The attribute bag, whatever shape the author wrote it in. */
|
|
98
|
-
export function attrs(node: JxElement):
|
|
99
|
-
return isJsonObject(node.attributes) ?
|
|
106
|
+
export function attrs(node: JxElement): JsonObject {
|
|
107
|
+
return isJsonObject(node.attributes) ? node.attributes : {};
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
/** An attribute's literal string value, or null when absent or bound. */
|
|
103
111
|
export function literalAttr(node: JxElement, name: string): string | null {
|
|
104
112
|
const value = attrs(node)[name];
|
|
105
|
-
return
|
|
113
|
+
return isString(value) ? value : null;
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
/** The element's tag, lowercased, or "" when it is a tag expression or absent. */
|
|
109
117
|
export function tagOf(node: JxElement): string {
|
|
110
|
-
return
|
|
118
|
+
return isString(node.tagName) ? node.tagName.toLowerCase() : "";
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
/** How the author addresses this node in a message: its id if it has one, else its tag. */
|
|
114
122
|
export function labelOf(node: JxElement, fallback = "the popover"): string {
|
|
115
|
-
const id =
|
|
123
|
+
const id = isString(node.id) ? node.id : literalAttr(node, "id");
|
|
116
124
|
if (id) {
|
|
117
125
|
return `#${id}`;
|
|
118
126
|
}
|
|
@@ -130,7 +138,7 @@ export function labelOf(node: JxElement, fallback = "the popover"): string {
|
|
|
130
138
|
*/
|
|
131
139
|
export function popoverModeOf(node: JxElement): string | null {
|
|
132
140
|
const raw = attrs(node).popover;
|
|
133
|
-
return
|
|
141
|
+
return isString(raw) ? raw : null;
|
|
134
142
|
}
|
|
135
143
|
|
|
136
144
|
/**
|
|
@@ -172,18 +180,19 @@ const INVOKER_ATTRS = ["popovertarget", "popovertargetaction", "command", "comma
|
|
|
172
180
|
* @param definitions The element documents in scope.
|
|
173
181
|
* @returns {OverlayScope} The two tag sets, ready to pass to any rule here.
|
|
174
182
|
*/
|
|
175
|
-
export function overlayScopeFor(definitions: Iterable<JxElement>): OverlayScope {
|
|
183
|
+
export function overlayScopeFor(definitions: Iterable<JxElement | null>): OverlayScope {
|
|
176
184
|
const popoverTags = new Set<string>();
|
|
177
185
|
const invokerTags = new Set<string>();
|
|
178
186
|
for (const def of definitions) {
|
|
179
|
-
|
|
180
|
-
|
|
187
|
+
// A definition file whose JSON is a bare `null` still parses, so the compiler can pass one.
|
|
188
|
+
const tag = def !== null && isString(def.tagName) ? def.tagName : "";
|
|
189
|
+
if (def === null || !tag.includes("-")) {
|
|
181
190
|
continue;
|
|
182
191
|
}
|
|
183
192
|
if (declaresPopover(def)) {
|
|
184
193
|
popoverTags.add(tag);
|
|
185
194
|
}
|
|
186
|
-
const observed =
|
|
195
|
+
const observed = def.observedAttributes;
|
|
187
196
|
if (Array.isArray(observed) && INVOKER_ATTRS.every((attr) => observed.includes(attr))) {
|
|
188
197
|
invokerTags.add(tag);
|
|
189
198
|
}
|
|
@@ -206,7 +215,7 @@ export function declaresPopover(node: JxElement): boolean {
|
|
|
206
215
|
to every rule here: no `base-display` finding, no `no-open-rule` finding, and its tag missing
|
|
207
216
|
from the scope, so consumers of that definition went unrecognised too. `idOf` below already
|
|
208
217
|
reads a key either way, for the same reason. */
|
|
209
|
-
return "popover" in attrs(node) || "popover" in
|
|
218
|
+
return "popover" in attrs(node) || "popover" in node;
|
|
210
219
|
}
|
|
211
220
|
|
|
212
221
|
/**
|
|
@@ -221,7 +230,7 @@ export function isPopover(node: JxElement, scope?: OverlayScope): boolean {
|
|
|
221
230
|
|
|
222
231
|
/** The node's own `id`, from either the top-level key or the attribute bag. */
|
|
223
232
|
export function idOf(node: JxElement): string | null {
|
|
224
|
-
if (
|
|
233
|
+
if (isString(node.id) && node.id !== "") {
|
|
225
234
|
return node.id;
|
|
226
235
|
}
|
|
227
236
|
const attr = literalAttr(node, "id");
|
|
@@ -258,25 +267,20 @@ export function* walk(
|
|
|
258
267
|
const { children } = root;
|
|
259
268
|
if (Array.isArray(children)) {
|
|
260
269
|
for (const [index, child] of children.entries()) {
|
|
261
|
-
if (child
|
|
262
|
-
yield* walk(child
|
|
270
|
+
if (isNonNullObject(child)) {
|
|
271
|
+
yield* walk(child, [...path, "children", index], inside, scope);
|
|
263
272
|
}
|
|
264
273
|
}
|
|
265
|
-
} else if (isJsonObject(children) &&
|
|
266
|
-
yield* walk(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
inside,
|
|
270
|
-
scope,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
const template = isJsonObject(root.map) ? (root.map as JxElement) : null;
|
|
274
|
+
} else if (isJsonObject(children) && isNodeObject(children.map)) {
|
|
275
|
+
yield* walk(children.map, [...path, "children", "map"], inside, scope);
|
|
276
|
+
}
|
|
277
|
+
const template = isNodeObject(root.map) ? root.map : null;
|
|
274
278
|
if (template) {
|
|
275
279
|
yield* walk(template, [...path, "map"], inside, scope);
|
|
276
280
|
}
|
|
277
|
-
const cases = isJsonObject(root.cases) ?
|
|
281
|
+
const cases = isJsonObject(root.cases) ? root.cases : {};
|
|
278
282
|
for (const [key, branch] of Object.entries(cases)) {
|
|
279
|
-
if (branch
|
|
283
|
+
if (isNonNullObject(branch)) {
|
|
280
284
|
yield* walk(branch, [...path, "cases", key], inside, scope);
|
|
281
285
|
}
|
|
282
286
|
}
|
|
@@ -493,7 +497,8 @@ function hasAnchorFallback(style: JxStyle | undefined): boolean {
|
|
|
493
497
|
|
|
494
498
|
// ─── The rules ──────────────────────────────────────────────────────────────────
|
|
495
499
|
|
|
496
|
-
|
|
500
|
+
/** The sentence every overlay and dialog defect ends with, pointing at the concept page. */
|
|
501
|
+
export const OVERLAY_DOCS = "See docs/framework/concepts/overlays.";
|
|
497
502
|
|
|
498
503
|
/**
|
|
499
504
|
* Every popover defect in a document, in document order.
|
|
@@ -551,7 +556,7 @@ function invokerDefects(
|
|
|
551
556
|
"mixin, which HTML includes into `HTMLButtonElement` and `HTMLInputElement` and into " +
|
|
552
557
|
"nothing else. Elsewhere they parse and do nothing. A link inside an open popover " +
|
|
553
558
|
"dismisses it by navigating away, which is why this looks like it works — but it does " +
|
|
554
|
-
`not for a new tab or a same-page fragment. Remove both attributes. ${
|
|
559
|
+
`not for a new tab or a same-page fragment. Remove both attributes. ${OVERLAY_DOCS}`,
|
|
555
560
|
fix: "invoker",
|
|
556
561
|
message: `<${tag || "element"}> carries popovertarget, which only <button> and <input> support.`,
|
|
557
562
|
path,
|
|
@@ -568,7 +573,7 @@ function invokerDefects(
|
|
|
568
573
|
out.push({
|
|
569
574
|
detail:
|
|
570
575
|
`No element in this document declares \`popover\` with the id "${target}". ${known}` +
|
|
571
|
-
`A target that names nothing does nothing, silently. ${
|
|
576
|
+
`A target that names nothing does nothing, silently. ${OVERLAY_DOCS}`,
|
|
572
577
|
message: `popovertarget="${target}" names no popover in this document.`,
|
|
573
578
|
path,
|
|
574
579
|
rule: "target-missing",
|
|
@@ -579,7 +584,7 @@ function invokerDefects(
|
|
|
579
584
|
detail:
|
|
580
585
|
`This control is inside ${labelOf(enclosing)} but targets "${target}". Dismissing a ` +
|
|
581
586
|
"different popover leaves the one the reader is looking at open. This is usually a " +
|
|
582
|
-
`copy-paste between two panels in the same file. ${
|
|
587
|
+
`copy-paste between two panels in the same file. ${OVERLAY_DOCS}`,
|
|
583
588
|
message: `A control inside ${labelOf(enclosing)} targets "${target}" instead.`,
|
|
584
589
|
path,
|
|
585
590
|
rule: "target-mismatch",
|
|
@@ -589,7 +594,7 @@ function invokerDefects(
|
|
|
589
594
|
}
|
|
590
595
|
|
|
591
596
|
if (tag === "a" && enclosing !== null && target === null) {
|
|
592
|
-
const href = literalAttr(node, "href") ?? (
|
|
597
|
+
const href = literalAttr(node, "href") ?? (isString(node.href) ? node.href : null);
|
|
593
598
|
const blank = literalAttr(node, "target") === "_blank";
|
|
594
599
|
const fragment = href !== null && href.includes("#");
|
|
595
600
|
if (blank || fragment) {
|
|
@@ -600,7 +605,7 @@ function invokerDefects(
|
|
|
600
605
|
detail:
|
|
601
606
|
`A link inside a panel normally dismisses it by navigating away. This one does not: ${why}` +
|
|
602
607
|
"There is no declarative fix — a link cannot carry `popovertarget`. Move it out of the " +
|
|
603
|
-
`panel, make it a <button>, or accept it deliberately. ${
|
|
608
|
+
`panel, make it a <button>, or accept it deliberately. ${OVERLAY_DOCS}`,
|
|
604
609
|
message: `A link inside ${labelOf(enclosing)} does not dismiss it.`,
|
|
605
610
|
path,
|
|
606
611
|
rule: "link-does-not-dismiss",
|
|
@@ -622,25 +627,25 @@ function panelDefects(
|
|
|
622
627
|
const label = labelOf(node);
|
|
623
628
|
const mode = popoverModeOf(node);
|
|
624
629
|
|
|
625
|
-
if (mode !== null && mode !== "" && !(
|
|
630
|
+
if (mode !== null && mode !== "" && !POPOVER_MODES.some((known) => known === mode)) {
|
|
626
631
|
out.push({
|
|
627
632
|
detail:
|
|
628
633
|
"`popover` is an enumerated attribute taking `auto`, `manual` or `hint`. Anything else " +
|
|
629
634
|
"falls back to the invalid-value default `manual`, which gives up light dismiss and gives " +
|
|
630
|
-
`up Escape — so the panel opens and there is no way to close it. Write "auto". ${
|
|
635
|
+
`up Escape — so the panel opens and there is no way to close it. Write "auto". ${OVERLAY_DOCS}`,
|
|
631
636
|
fix: "mode",
|
|
632
637
|
message: `${label} declares popover="${mode}", which is not one of auto, manual or hint.`,
|
|
633
638
|
path,
|
|
634
639
|
rule: "invalid-mode",
|
|
635
640
|
severity: "error",
|
|
636
641
|
});
|
|
637
|
-
} else if (
|
|
642
|
+
} else if (isBoolean(attrs(node).popover)) {
|
|
638
643
|
out.push({
|
|
639
644
|
detail:
|
|
640
645
|
"`popover` carries its value in its text, so a boolean is not the right shape. Written " +
|
|
641
646
|
'as `true` it reaches two of the compiler\'s emitters as `popover="true"`, which is not ' +
|
|
642
647
|
"one of the three keywords, so HTML falls back to `manual`: no light dismiss, no Escape, " +
|
|
643
|
-
`and the trigger stops toggling. Write "auto". ${
|
|
648
|
+
`and the trigger stops toggling. Write "auto". ${OVERLAY_DOCS}`,
|
|
644
649
|
fix: "mode",
|
|
645
650
|
message: `${label} writes popover as a boolean.`,
|
|
646
651
|
path,
|
|
@@ -662,7 +667,7 @@ function panelDefects(
|
|
|
662
667
|
"none }`. That rule is UA-origin, so any author `display` beats it at any specificity, and " +
|
|
663
668
|
"the panel is laid out on every page whether it is open or not — usually parked off the " +
|
|
664
669
|
"side of the viewport by the transform meant to animate it, so nothing looks wrong until " +
|
|
665
|
-
`you scroll sideways. \`display\` belongs in \`:popover-open\` and nowhere else. ${
|
|
670
|
+
`you scroll sideways. \`display\` belongs in \`:popover-open\` and nowhere else. ${OVERLAY_DOCS}`,
|
|
666
671
|
fix: "display",
|
|
667
672
|
message: `${label} sets display in its base rule.`,
|
|
668
673
|
path,
|
|
@@ -676,7 +681,7 @@ function panelDefects(
|
|
|
676
681
|
detail:
|
|
677
682
|
`The same defect as a base \`display\`, arriving through \`${key}\` and therefore only at ` +
|
|
678
683
|
"one width — which is what makes it hard to see. Gate the TRIGGER on a breakpoint, never " +
|
|
679
|
-
`the panel: the popover mechanism is what hides the panel. ${
|
|
684
|
+
`the panel: the popover mechanism is what hides the panel. ${OVERLAY_DOCS}`,
|
|
680
685
|
fix: "display",
|
|
681
686
|
message: `${label} sets display inside ${key}.`,
|
|
682
687
|
path,
|
|
@@ -690,7 +695,7 @@ function panelDefects(
|
|
|
690
695
|
detail:
|
|
691
696
|
"A second hiding mechanism the browser already provides. Once `display` leaves the base " +
|
|
692
697
|
"rule this one only adds a frame of flicker on open, and it hides the element from " +
|
|
693
|
-
`assistive technology in a way \`display: none\` already did. ${
|
|
698
|
+
`assistive technology in a way \`display: none\` already did. ${OVERLAY_DOCS}`,
|
|
694
699
|
fix: "display",
|
|
695
700
|
message: `${label} also hides itself with visibility.`,
|
|
696
701
|
path,
|
|
@@ -704,7 +709,7 @@ function panelDefects(
|
|
|
704
709
|
detail:
|
|
705
710
|
"Nothing changes when it opens, so no transition on it can run and — if `display` is not " +
|
|
706
711
|
"in the base rule — the panel has no `display` at all while open. This is usually a panel " +
|
|
707
|
-
`that was styled before it was made a popover. ${
|
|
712
|
+
`that was styled before it was made a popover. ${OVERLAY_DOCS}`,
|
|
708
713
|
message: `${label} has no :popover-open rule.`,
|
|
709
714
|
path,
|
|
710
715
|
rule: "no-open-rule",
|
|
@@ -720,7 +725,7 @@ function panelDefects(
|
|
|
720
725
|
`${label} declares flex or grid properties, but neither its base rule nor its ` +
|
|
721
726
|
"`:popover-open` rule sets a `display` for them to apply to — and the base rule must not " +
|
|
722
727
|
"(that is what keeps the closed panel hidden). So it opens with the UA's `display: block` " +
|
|
723
|
-
`and the layout silently does nothing. Put \`display: flex\` in \`:popover-open\`. ${
|
|
728
|
+
`and the layout silently does nothing. Put \`display: flex\` in \`:popover-open\`. ${OVERLAY_DOCS}`,
|
|
724
729
|
fix: "open-display",
|
|
725
730
|
message: `${label} is laid out with flex or grid but never sets display.`,
|
|
726
731
|
path,
|
|
@@ -736,7 +741,7 @@ function panelDefects(
|
|
|
736
741
|
"`display …allow-discrete` keeps the box alive while it animates out; `overlay " +
|
|
737
742
|
"…allow-discrete` is what keeps it in the TOP LAYER for those same milliseconds. " +
|
|
738
743
|
"Without it the panel drops behind the page content and the backdrop disappears a frame " +
|
|
739
|
-
`early, so the exit reads as a flicker rather than an animation. ${
|
|
744
|
+
`early, so the exit reads as a flicker rather than an animation. ${OVERLAY_DOCS}`,
|
|
740
745
|
message: `${label} transitions display but not overlay.`,
|
|
741
746
|
path,
|
|
742
747
|
rule: "cut-exit",
|
|
@@ -753,7 +758,7 @@ function panelDefects(
|
|
|
753
758
|
"`position: fixed` box with four `auto` insets falls back to its STATIC position, which " +
|
|
754
759
|
"for a panel declared inside a header is in the middle of the header. Use `position-area` " +
|
|
755
760
|
"with `position-try-fallbacks`, or add a `@supports` block that positions it without " +
|
|
756
|
-
`anchors. ${
|
|
761
|
+
`anchors. ${OVERLAY_DOCS}`,
|
|
757
762
|
message: `${label} is anchor-positioned with no fallback.`,
|
|
758
763
|
path,
|
|
759
764
|
rule: "unsafe-anchor",
|
|
@@ -767,7 +772,7 @@ function panelDefects(
|
|
|
767
772
|
detail:
|
|
768
773
|
"No `<button>` or `<input>` in this document names it with `popovertarget`. If it is " +
|
|
769
774
|
"opened from another file or from a function that calls `showPopover()`, this is fine; " +
|
|
770
|
-
`otherwise there is no way for a reader to open it. ${
|
|
775
|
+
`otherwise there is no way for a reader to open it. ${OVERLAY_DOCS}`,
|
|
771
776
|
message: `${label} has no invoker in this document.`,
|
|
772
777
|
path,
|
|
773
778
|
rule: "no-invoker",
|
package/src/parse.ts
CHANGED
|
@@ -15,20 +15,20 @@
|
|
|
15
15
|
* one thing on one keyboard and another thing on another is a defect nothing downstream can see.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { JxClassDef, JxDocument, ProjectConfig } from "../types";
|
|
19
|
-
import { isJsonObject } from "./guards";
|
|
18
|
+
import type { JsonObject, JsonValue, JxClassDef, JxDocument, ProjectConfig } from "../types";
|
|
19
|
+
import { isJsonObject, isString } from "./guards";
|
|
20
20
|
import { describeIJsonProblem, findIJsonProblems } from "./ijson";
|
|
21
21
|
|
|
22
22
|
// ─── Error helpers ──────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
24
|
/** Extract a human-readable message from a caught `unknown`. */
|
|
25
|
-
export function errorMessage(
|
|
26
|
-
return
|
|
25
|
+
export function errorMessage(cause: unknown): string {
|
|
26
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Coerce a caught `unknown` to an Error without losing the original. */
|
|
30
|
-
export function toError(
|
|
31
|
-
return
|
|
30
|
+
export function toError(cause: unknown): Error {
|
|
31
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// ─── Unicode normalization ──────────────────────────────────────────────────────
|
|
@@ -62,35 +62,40 @@ const NON_ASCII = /[^\p{ASCII}]/u;
|
|
|
62
62
|
* escape into a combining mark, so normalizing the raw text would miss exactly the documents a
|
|
63
63
|
* generator wrote.
|
|
64
64
|
*
|
|
65
|
-
* @param {
|
|
66
|
-
* @returns {
|
|
65
|
+
* @param {JsonValue} value - Any parsed JSON value
|
|
66
|
+
* @returns {JsonValue} The same shape, with all text in NFC
|
|
67
67
|
*/
|
|
68
|
-
function normalizeIdentifiers(value:
|
|
69
|
-
if (
|
|
68
|
+
function normalizeIdentifiers(value: JsonValue): JsonValue {
|
|
69
|
+
if (isString(value)) {
|
|
70
70
|
return NON_ASCII.test(value) ? value.normalize("NFC") : value;
|
|
71
71
|
}
|
|
72
72
|
if (Array.isArray(value)) {
|
|
73
73
|
return value.map((item) => normalizeIdentifiers(item));
|
|
74
74
|
}
|
|
75
75
|
if (isJsonObject(value)) {
|
|
76
|
-
|
|
77
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
78
|
-
/*
|
|
79
|
-
* A later key wins on collision, matching what `JSON.parse` does with a literal duplicate.
|
|
80
|
-
* It cannot happen through this boundary — I-JSON already rejected duplicate keys, and two
|
|
81
|
-
* keys differing only by normalization form are not duplicates to `JSON.parse` — so this is
|
|
82
|
-
* a statement about which is authoritative rather than a branch anything reaches.
|
|
83
|
-
*/
|
|
84
|
-
out[NON_ASCII.test(key) ? key.normalize("NFC") : key] = normalizeIdentifiers(entry);
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
76
|
+
return normalizeObject(value);
|
|
87
77
|
}
|
|
88
78
|
return value;
|
|
89
79
|
}
|
|
90
80
|
|
|
81
|
+
/** {@link normalizeIdentifiers} for an object, keeping the object type for the parse helpers. */
|
|
82
|
+
function normalizeObject(value: JsonObject): JsonObject {
|
|
83
|
+
const out: JsonObject = {};
|
|
84
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
85
|
+
/*
|
|
86
|
+
* A later key wins on collision, matching what `JSON.parse` does with a literal duplicate.
|
|
87
|
+
* It cannot happen through this boundary — I-JSON already rejected duplicate keys, and two
|
|
88
|
+
* keys differing only by normalization form are not duplicates to `JSON.parse` — so this is
|
|
89
|
+
* a statement about which is authoritative rather than a branch anything reaches.
|
|
90
|
+
*/
|
|
91
|
+
out[NON_ASCII.test(key) ? key.normalize("NFC") : key] = normalizeIdentifiers(entry);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
// ─── Parse helpers ──────────────────────────────────────────────────────────────
|
|
92
97
|
|
|
93
|
-
function parseObject(text: string, sourcePath: string, what: string):
|
|
98
|
+
function parseObject(text: string, sourcePath: string, what: string): JsonObject {
|
|
94
99
|
let parsed: unknown;
|
|
95
100
|
try {
|
|
96
101
|
parsed = JSON.parse(text);
|
|
@@ -112,16 +117,25 @@ function parseObject(text: string, sourcePath: string, what: string): Record<str
|
|
|
112
117
|
const described = problems.map((problem) => describeIJsonProblem(problem)).join("; ");
|
|
113
118
|
throw new Error(`Invalid ${what} at ${sourcePath}: ${described}`);
|
|
114
119
|
}
|
|
115
|
-
return
|
|
120
|
+
return normalizeObject(parsed);
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
/** Parse a Jx document (.json page/layout/component source). */
|
|
119
124
|
export function parseJxDocument(text: string, sourcePath: string): JxDocument {
|
|
125
|
+
/*
|
|
126
|
+
* SAFETY: every JxDocument member is optional, so any JSON object is a structurally admissible
|
|
127
|
+
* document. This boundary verifies the object shape and I-JSON only; member-level checking is the
|
|
128
|
+
* schema's job (`validateDocument`), which callers run when they need it.
|
|
129
|
+
*/
|
|
120
130
|
return parseObject(text, sourcePath, "Jx document") as JxDocument;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
/** Parse a project.json site configuration. */
|
|
124
134
|
export function parseProjectConfig(text: string, sourcePath: string): ProjectConfig {
|
|
135
|
+
/*
|
|
136
|
+
* SAFETY: every ProjectConfig member is optional, so any JSON object is a structurally admissible
|
|
137
|
+
* config; member-level checking is `validateProject`'s job against the per-project schema.
|
|
138
|
+
*/
|
|
125
139
|
return parseObject(text, sourcePath, "project config") as ProjectConfig;
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -130,5 +144,9 @@ export function parseProjectConfig(text: string, sourcePath: string): ProjectCon
|
|
|
130
144
|
* discriminator but is not required — existing class files omit it.
|
|
131
145
|
*/
|
|
132
146
|
export function parseClassDef(text: string, sourcePath: string): JxClassDef {
|
|
133
|
-
|
|
147
|
+
/*
|
|
148
|
+
* SAFETY: only the object shape is verified here. The type states the canonical form, and a class
|
|
149
|
+
* file that omits `$prototype` (accepted on purpose, see above) is the one known deviation from it.
|
|
150
|
+
*/
|
|
151
|
+
return parseObject(text, sourcePath, "class definition") as JxClassDef;
|
|
134
152
|
}
|