@jxsuite/schema 1.8.0 → 1.8.1

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 CHANGED
@@ -10,6 +10,7 @@
10
10
  "exports": {
11
11
  ".": "./src/schema.ts",
12
12
  "./asset-paths": "./src/asset-paths.ts",
13
+ "./component-meta": "./src/component-meta.ts",
13
14
  "./extension-registry": "./src/extension-registry.ts",
14
15
  "./format-registry": "./src/format-registry.ts",
15
16
  "./project-schemas": "./src/project-schemas.ts",
@@ -54,7 +55,7 @@
54
55
  "upgrade": "bunx npm-check-updates -u && bun install"
55
56
  },
56
57
  "type": "module",
57
- "version": "1.8.0",
58
+ "version": "1.8.1",
58
59
  "devDependencies": {
59
60
  "json-schema-to-ts": "^3.1.1"
60
61
  }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Component metadata, derived from a parsed component document.
3
+ *
4
+ * Discovery — "what components does this project have, and what props do they take" — is a pure
5
+ * function of the document. It reads `tagName`, `state` and any `<slot>` in the tree; it executes
6
+ * nothing. That distinction is what lets a backend with a no-project-JS posture discover JSON
7
+ * components safely, which Jx Cloud could not do while this logic lived inside two Bun servers.
8
+ *
9
+ * It lives here because THREE backends need the same answer and two of them had already written it
10
+ * twice: `packages/server/src/studio-api.ts` (the dev server's `/__studio/components`) and
11
+ * `packages/desktop/src/project-session.ts`. A third copy in the cloud adapter would have made
12
+ * "which entries in `state` count as props" a question with three answers.
13
+ *
14
+ * @license MIT
15
+ */
16
+
17
+ import type { JsonValue } from "../types";
18
+
19
+ /** One `<slot>` found in a component's tree. */
20
+ export interface ComponentSlotDef {
21
+ /** The slot's name; `""` for the unnamed slot. */
22
+ name: string;
23
+ /** Fallback children rendered when nothing is slotted in. */
24
+ fallback?: unknown[];
25
+ }
26
+
27
+ /** One editable prop, derived from a `state` entry. */
28
+ export interface ComponentPropDef {
29
+ name: string;
30
+ type?: unknown;
31
+ default?: JsonValue;
32
+ format?: unknown;
33
+ }
34
+
35
+ /** What discovery reports for one component document. */
36
+ export interface ComponentMetaOut {
37
+ tagName: string;
38
+ $id: string | null;
39
+ path: string;
40
+ props: ComponentPropDef[];
41
+ slots?: ComponentSlotDef[];
42
+ hasElements: boolean;
43
+ }
44
+
45
+ /**
46
+ * Collect slot definitions from a parsed component tree.
47
+ *
48
+ * Whitespace-only names count as unnamed (`""`), and only static `children` arrays are walked — a
49
+ * slot produced by a `$map` or a `$switch` has no fixed identity to report.
50
+ *
51
+ * @param {unknown} node - A node of the component document.
52
+ * @param {ComponentSlotDef[]} [out] - Accumulator, for the recursive walk.
53
+ * @returns {ComponentSlotDef[]}
54
+ */
55
+ export function collectSlotDefs(node: unknown, out: ComponentSlotDef[] = []): ComponentSlotDef[] {
56
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
57
+ return out;
58
+ }
59
+ const el = node as Record<string, unknown>;
60
+ if (el.tagName === "slot") {
61
+ const attrs = el.attributes as Record<string, unknown> | undefined;
62
+ const rawName = attrs?.name;
63
+ const name = typeof rawName === "string" ? rawName.trim() : "";
64
+ const { children } = el;
65
+ out.push({
66
+ name,
67
+ ...(Array.isArray(children) && children.length > 0 ? { fallback: children } : {}),
68
+ });
69
+ }
70
+ if (Array.isArray(el.children)) {
71
+ for (const c of el.children) {
72
+ collectSlotDefs(c, out);
73
+ }
74
+ }
75
+ return out;
76
+ }
77
+
78
+ /**
79
+ * Whether a `state` entry is an editable prop.
80
+ *
81
+ * The shorthand form (`"title": "Hello"`) always is. The full form is one only when it is plain
82
+ * data: a computed value, a handler or a prototype entry is machinery the author does not type
83
+ * into, and offering it as a prop would put a text box in front of a function.
84
+ *
85
+ * @param {unknown} def - The `state` entry.
86
+ * @returns {boolean}
87
+ */
88
+ function isPropEntry(def: unknown): boolean {
89
+ if (def == null) {
90
+ return false;
91
+ }
92
+ if (typeof def !== "object") {
93
+ return true;
94
+ }
95
+ const obj = def as Record<string, unknown>;
96
+ return !obj.$prototype && !obj.$handler && !obj.$compute;
97
+ }
98
+
99
+ /**
100
+ * Metadata for one component document, or null when the document is not a component.
101
+ *
102
+ * A component is a document whose `tagName` is a valid custom-element name — it must contain a
103
+ * hyphen. That single test is what keeps a page or a data file from being reported as a component
104
+ * when a whole project tree is scanned.
105
+ *
106
+ * @param {unknown} doc - The parsed document.
107
+ * @param {string} path - Project-relative path, reported verbatim.
108
+ * @returns {ComponentMetaOut | null}
109
+ */
110
+ export function componentMetaFrom(doc: unknown, path: string): ComponentMetaOut | null {
111
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
112
+ return null;
113
+ }
114
+ const content = doc as Record<string, unknown>;
115
+ const { tagName } = content;
116
+ if (typeof tagName !== "string" || !tagName.includes("-")) {
117
+ return null;
118
+ }
119
+ const slots = collectSlotDefs(content);
120
+ const state = (content.state ?? {}) as Record<string, unknown>;
121
+ return {
122
+ $id: typeof content.$id === "string" && content.$id ? content.$id : null,
123
+ hasElements: Array.isArray(content.$elements) && content.$elements.length > 0,
124
+ path,
125
+ props: Object.entries(state)
126
+ .filter(([, def]) => isPropEntry(def))
127
+ .map(([name, def]) => {
128
+ if (typeof def !== "object" || def === null) {
129
+ // Shorthand: the value IS the default, and its runtime type is the only type on offer.
130
+ return { default: def as JsonValue, name, type: typeof def };
131
+ }
132
+ const obj = def as Record<string, unknown>;
133
+ return {
134
+ default: obj.default as JsonValue,
135
+ format: obj.format,
136
+ name,
137
+ type: obj.type,
138
+ };
139
+ }),
140
+ ...(slots.length > 0 ? { slots } : {}),
141
+ tagName,
142
+ };
143
+ }