@jxsuite/compiler 1.4.0 → 1.5.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/dist/cli.js +155 -156
- package/dist/compiler.js +105 -107
- package/package.json +6 -6
- package/src/shared.ts +236 -8
- package/src/site/schema-command.ts +37 -77
- package/src/site/site-build.ts +53 -14
- package/src/site/validate-command.ts +133 -37
- package/src/targets/compile-client.ts +63 -67
- package/src/targets/compile-element.ts +246 -31
- package/src/targets/compile-static.ts +8 -2
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extends the original project.json check into the full compliance walk:
|
|
5
5
|
*
|
|
6
|
-
* 1.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 1. The committed entry documents are SELF-CONTAINED and SINGLE-RESOURCE: every `$ref` is a
|
|
7
|
+
* root-relative JSON Pointer that resolves inside the file — the editor-resolution guarantee of
|
|
8
|
+
* the committed form (specs/extensions.md §5.4). Checked first and fatal, since walks 2–4
|
|
9
|
+
* compile those documents;
|
|
10
|
+
* 2. `project.json` against the generated entry schema (validateProjectFile);
|
|
9
11
|
* 3. Every document under `components/`, `pages/`, and `layouts/` against the bundled document schema
|
|
10
12
|
* (core + extension `$paths` union — the same schema the studio consumes);
|
|
11
13
|
* 4. Every project-local `*.class.json` against the generated class schema;
|
|
@@ -94,33 +96,39 @@ export async function validateProjectTree(
|
|
|
94
96
|
const issues: ProjectTreeIssue[] = [];
|
|
95
97
|
let checked = 0;
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
issues.push({ errors: projectResult.errors ?? [], file: "project.json" });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 2. Committed entry documents must be self-contained (the editor-resolution guarantee).
|
|
99
|
+
/* 1. Committed entry documents must be self-contained (the editor-resolution guarantee). This
|
|
100
|
+
runs FIRST because every later walk compiles one of them: an unresolvable ref surfaces from ajv
|
|
101
|
+
as a thrown MissingRefError, so diagnosing it here turns a stack trace into "regenerate with
|
|
102
|
+
`jx schema`". Nothing downstream is meaningful until it holds, hence the early return. */
|
|
105
103
|
for (const name of ["project.schema.json", "document.schema.json"]) {
|
|
106
104
|
const path = resolve(root, name);
|
|
107
105
|
if (!existsSync(path)) {
|
|
108
106
|
continue;
|
|
109
107
|
}
|
|
110
108
|
checked += 1;
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
if (
|
|
109
|
+
const entry = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
110
|
+
const unresolvable = collectUnresolvableRefs(entry);
|
|
111
|
+
if (unresolvable.length > 0) {
|
|
114
112
|
issues.push({
|
|
115
|
-
errors:
|
|
113
|
+
errors: unresolvable.map(({ reason, ref }) => ({
|
|
116
114
|
message:
|
|
117
|
-
|
|
118
|
-
`regenerate with \`jx schema\``,
|
|
115
|
+
`$ref "${ref}" ${reason} — committed entry documents must be self-contained ` +
|
|
116
|
+
`single-resource schemas; regenerate with \`jx schema\``,
|
|
119
117
|
})),
|
|
120
118
|
file: name,
|
|
121
119
|
});
|
|
122
120
|
}
|
|
123
121
|
}
|
|
122
|
+
if (issues.length > 0) {
|
|
123
|
+
return { checked, issues, valid: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. project.json against the generated entry schema.
|
|
127
|
+
const projectResult = await validateProjectFile(root);
|
|
128
|
+
checked += 1;
|
|
129
|
+
if (!projectResult.valid) {
|
|
130
|
+
issues.push({ errors: projectResult.errors ?? [], file: "project.json" });
|
|
131
|
+
}
|
|
124
132
|
|
|
125
133
|
const { addFormats, Ajv } = await loadAjv();
|
|
126
134
|
|
|
@@ -186,30 +194,118 @@ export async function validateProjectTree(
|
|
|
186
194
|
return { checked, issues, valid: issues.length === 0 };
|
|
187
195
|
}
|
|
188
196
|
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Every `$ref` a committed entry document may carry must be a root-relative JSON Pointer that
|
|
199
|
+
* resolves inside the same file. Three ways that fails, all of which break editor resolution: a
|
|
200
|
+
* relative path (needs Node module resolution), a URI (VS Code fetches it over the network instead
|
|
201
|
+
* of matching an embedded `$id`), and a pointer with no node at the other end.
|
|
202
|
+
*
|
|
203
|
+
* Walks only subschema keyword positions, since `examples`/`default`/`const`/`enum` hold instance
|
|
204
|
+
* data and Jx document examples legitimately contain `$ref` strings of their own.
|
|
205
|
+
*
|
|
206
|
+
* @param {Record<string, unknown>} entry - Parsed committed entry document
|
|
207
|
+
* @returns {{ ref: string; reason: string }[]} One entry per offending `$ref`
|
|
208
|
+
*/
|
|
209
|
+
function collectUnresolvableRefs(
|
|
210
|
+
entry: Record<string, unknown>,
|
|
211
|
+
): { ref: string; reason: string }[] {
|
|
212
|
+
const out: { ref: string; reason: string }[] = [];
|
|
213
|
+
const seen = new Set<Record<string, unknown>>();
|
|
214
|
+
|
|
215
|
+
const resolvePointer = (pointer: string): boolean => {
|
|
216
|
+
let current: unknown = entry;
|
|
217
|
+
for (const raw of pointer.slice(1).split("/")) {
|
|
218
|
+
if (raw === "") {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const segment = raw.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
222
|
+
if (typeof current !== "object" || current === null) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
current = (current as Record<string, unknown>)[segment];
|
|
226
|
+
if (current === undefined) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const walk = (node: unknown): void => {
|
|
234
|
+
if (!isRecord(node) || seen.has(node)) {
|
|
235
|
+
return;
|
|
194
236
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
!
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
out.push(value);
|
|
237
|
+
seen.add(node);
|
|
238
|
+
const ref = node.$ref;
|
|
239
|
+
if (typeof ref === "string") {
|
|
240
|
+
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(ref)) {
|
|
241
|
+
out.push({ reason: "is an absolute URI editors resolve over the network", ref });
|
|
242
|
+
} else if (!ref.startsWith("#")) {
|
|
243
|
+
out.push({ reason: "is a relative path only Node module resolution can follow", ref });
|
|
244
|
+
} else if (ref.length > 1 && !ref.startsWith("#/")) {
|
|
245
|
+
out.push({ reason: "is an $anchor reference, which has no root-pointer form", ref });
|
|
246
|
+
} else if (ref.length > 1 && !resolvePointer(ref.slice(1))) {
|
|
247
|
+
out.push({ reason: "points at no node in this document", ref });
|
|
248
|
+
}
|
|
208
249
|
}
|
|
209
|
-
|
|
210
|
-
|
|
250
|
+
for (const [keyword, value] of Object.entries(node)) {
|
|
251
|
+
if (!SUBSCHEMA_KEYWORDS.has(keyword)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (Array.isArray(value)) {
|
|
255
|
+
for (const item of value) {
|
|
256
|
+
walk(item);
|
|
257
|
+
}
|
|
258
|
+
} else if (isRecord(value)) {
|
|
259
|
+
// Map-valued keywords hold subschemas one level down; single-valued ones are the schema.
|
|
260
|
+
walk(value);
|
|
261
|
+
if (MAP_SUBSCHEMA_KEYWORDS.has(keyword)) {
|
|
262
|
+
for (const item of Object.values(value)) {
|
|
263
|
+
walk(item);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
walk(entry);
|
|
270
|
+
return out;
|
|
211
271
|
}
|
|
212
272
|
|
|
273
|
+
/** True for a plain object (a candidate subschema node). */
|
|
274
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
275
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Keywords whose values map names to subschemas. */
|
|
279
|
+
const MAP_SUBSCHEMA_KEYWORDS = new Set([
|
|
280
|
+
"$defs",
|
|
281
|
+
"definitions",
|
|
282
|
+
"dependencies",
|
|
283
|
+
"dependentSchemas",
|
|
284
|
+
"patternProperties",
|
|
285
|
+
"properties",
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
/** Every keyword position a subschema can occupy (see {@link MAP_SUBSCHEMA_KEYWORDS} for maps). */
|
|
289
|
+
const SUBSCHEMA_KEYWORDS = new Set([
|
|
290
|
+
...MAP_SUBSCHEMA_KEYWORDS,
|
|
291
|
+
"additionalItems",
|
|
292
|
+
"additionalProperties",
|
|
293
|
+
"allOf",
|
|
294
|
+
"anyOf",
|
|
295
|
+
"contains",
|
|
296
|
+
"contentSchema",
|
|
297
|
+
"else",
|
|
298
|
+
"if",
|
|
299
|
+
"items",
|
|
300
|
+
"not",
|
|
301
|
+
"oneOf",
|
|
302
|
+
"prefixItems",
|
|
303
|
+
"propertyNames",
|
|
304
|
+
"then",
|
|
305
|
+
"unevaluatedItems",
|
|
306
|
+
"unevaluatedProperties",
|
|
307
|
+
]);
|
|
308
|
+
|
|
213
309
|
/** Recursively list `.json` files under a directory (absent directories yield nothing). */
|
|
214
310
|
function walkJsonFiles(dir: string): string[] {
|
|
215
311
|
if (!existsSync(dir)) {
|
|
@@ -16,21 +16,26 @@ import {
|
|
|
16
16
|
DEFAULT_LIT_HTML_SRC,
|
|
17
17
|
DEFAULT_REACTIVITY_SRC,
|
|
18
18
|
buildAttrs,
|
|
19
|
+
childSeparator,
|
|
19
20
|
colorSchemePrePaintScript,
|
|
20
21
|
compileExpression,
|
|
21
22
|
compileStatements,
|
|
22
23
|
compileStyles,
|
|
23
24
|
createCompileContext,
|
|
24
25
|
emitFormulaFn,
|
|
26
|
+
emitRequestFetch,
|
|
25
27
|
escapeHtml,
|
|
26
28
|
isMutating,
|
|
27
29
|
isRefObject,
|
|
28
30
|
isSchemaOnly,
|
|
29
31
|
isTemplateString,
|
|
32
|
+
PREFORMATTED_TAGS,
|
|
30
33
|
pureSchemeOf,
|
|
31
34
|
resolveStaticValue,
|
|
35
|
+
srcImportBinding,
|
|
32
36
|
} from "../shared.ts";
|
|
33
37
|
import {
|
|
38
|
+
bodyReturnsValue,
|
|
34
39
|
hasStructuredBody,
|
|
35
40
|
isExpandedSignal,
|
|
36
41
|
isExpressionDef,
|
|
@@ -47,7 +52,6 @@ import type {
|
|
|
47
52
|
JxDocument,
|
|
48
53
|
JxMappedArray,
|
|
49
54
|
JxMutableNode,
|
|
50
|
-
JxPrototypeDef,
|
|
51
55
|
JxStyle,
|
|
52
56
|
} from "@jxsuite/schema/types";
|
|
53
57
|
|
|
@@ -161,11 +165,11 @@ export function compileClient(
|
|
|
161
165
|
if (!srcImportMap.has(def.$src)) {
|
|
162
166
|
srcImportMap.set(def.$src, new Set());
|
|
163
167
|
}
|
|
164
|
-
(srcImportMap.get(def.$src) as Set<string>).add(key);
|
|
168
|
+
(srcImportMap.get(def.$src) as Set<string>).add(srcImportBinding(key, def));
|
|
165
169
|
|
|
166
170
|
// $src functions always produce computed entries (they return values)
|
|
167
171
|
computedEntries.push([key, `() => { return ${key}(state); }`]);
|
|
168
|
-
} else if (typeof def.body === "string" && def.body
|
|
172
|
+
} else if (typeof def.body === "string" && bodyReturnsValue(def.body)) {
|
|
169
173
|
// Body contains return → computed
|
|
170
174
|
computedEntries.push([key, `() => { ${def.body} }`]);
|
|
171
175
|
} else {
|
|
@@ -200,7 +204,7 @@ export function compileClient(
|
|
|
200
204
|
// $prototype: "Request"
|
|
201
205
|
if (def.$prototype === "Request") {
|
|
202
206
|
stateEntries.push([key, null]);
|
|
203
|
-
initBlocks.push(
|
|
207
|
+
initBlocks.push(emitRequestFetch(key, def));
|
|
204
208
|
continue;
|
|
205
209
|
}
|
|
206
210
|
|
|
@@ -335,6 +339,8 @@ function buildClientNode(
|
|
|
335
339
|
);
|
|
336
340
|
|
|
337
341
|
const tag = def.tagName ?? "div";
|
|
342
|
+
// `white-space` inherits, so once inside a <pre> the whole subtree stays whitespace-significant.
|
|
343
|
+
nextContext.preformatted = context.preformatted === true || PREFORMATTED_TAGS.has(tag);
|
|
338
344
|
const bindAttrs = [];
|
|
339
345
|
let needsBind = false;
|
|
340
346
|
|
|
@@ -530,9 +536,14 @@ function buildClientNode(
|
|
|
530
536
|
const listKey = `_children${counter.l}`;
|
|
531
537
|
counter.l += 1;
|
|
532
538
|
const childrenTpl = source.children
|
|
533
|
-
.map((c) => emitChildLit(c as JxMutableNode))
|
|
534
|
-
.join("
|
|
535
|
-
bindings.set(
|
|
539
|
+
.map((c) => emitChildLit(c as JxMutableNode, nextContext.preformatted))
|
|
540
|
+
.join(childSeparator(nextContext.preformatted, " "));
|
|
541
|
+
bindings.set(
|
|
542
|
+
listKey,
|
|
543
|
+
nextContext.preformatted
|
|
544
|
+
? `() => html\`${childrenTpl}\``
|
|
545
|
+
: `() => html\`\n ${childrenTpl}\n \``,
|
|
546
|
+
);
|
|
536
547
|
bindAttrs.push(`:render="${listKey}"`);
|
|
537
548
|
needsBind = true;
|
|
538
549
|
const bindAttrStr2 = ` ${bindAttrs.join(" ")}`;
|
|
@@ -551,7 +562,7 @@ function buildClientNode(
|
|
|
551
562
|
counter,
|
|
552
563
|
);
|
|
553
564
|
})
|
|
554
|
-
.join(
|
|
565
|
+
.join(childSeparator(nextContext.preformatted));
|
|
555
566
|
}
|
|
556
567
|
|
|
557
568
|
// Self-closing tags
|
|
@@ -570,13 +581,16 @@ function buildClientNode(
|
|
|
570
581
|
* index.
|
|
571
582
|
*
|
|
572
583
|
* @param {JxMutableNode} def
|
|
584
|
+
* @param {boolean} [preformatted]
|
|
573
585
|
* @returns {string}
|
|
574
586
|
*/
|
|
575
|
-
function emitLitMapTemplate(def: JxMutableNode | undefined) {
|
|
587
|
+
function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false) {
|
|
576
588
|
if (!def) {
|
|
577
589
|
return "";
|
|
578
590
|
}
|
|
579
591
|
const tag = def.tagName ?? "div";
|
|
592
|
+
// `white-space` inherits, so once inside a <pre> the whole subtree stays whitespace-significant.
|
|
593
|
+
const inPre = preformatted || PREFORMATTED_TAGS.has(tag);
|
|
580
594
|
let attrs = "";
|
|
581
595
|
|
|
582
596
|
if (def.id) {
|
|
@@ -586,6 +600,20 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
|
|
|
586
600
|
attrs += ` class="${mapRefsToLit(def.className)}"`;
|
|
587
601
|
}
|
|
588
602
|
|
|
603
|
+
// $props → lit property bindings. The element target emits these; the client target emitted
|
|
604
|
+
// Nothing, so a component in a list silently received none of its per-item props.
|
|
605
|
+
if (def.$props && typeof def.$props === "object") {
|
|
606
|
+
for (const [k, v] of Object.entries(def.$props)) {
|
|
607
|
+
if (isRefObject(v)) {
|
|
608
|
+
attrs += ` .${k}="\${${mapRefToClientExpr((v as { $ref: string }).$ref)}}"`;
|
|
609
|
+
} else if (typeof v === "string" && isTemplateString(v)) {
|
|
610
|
+
attrs += ` .${k}="${mapRefsToLit(v)}"`;
|
|
611
|
+
} else {
|
|
612
|
+
attrs += ` .${k}="\${${JSON.stringify(v)}}"`;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
589
617
|
// Attributes object
|
|
590
618
|
if (def.attributes && typeof def.attributes === "object") {
|
|
591
619
|
for (const [k, v] of Object.entries(def.attributes)) {
|
|
@@ -674,7 +702,8 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
|
|
|
674
702
|
// Legacy whole-children repeater nested inside a map template.
|
|
675
703
|
inner = `\n ${emitArrayHole(def.children)}\n `;
|
|
676
704
|
} else if (Array.isArray(def.children)) {
|
|
677
|
-
|
|
705
|
+
const rendered = def.children.map((c) => emitChildLit(c, inPre));
|
|
706
|
+
inner = inPre ? rendered.join("") : `\n ${rendered.join("\n ")}\n `;
|
|
678
707
|
}
|
|
679
708
|
|
|
680
709
|
const voidTags = new Set(["input", "br", "hr", "img", "meta", "link"]);
|
|
@@ -689,9 +718,10 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
|
|
|
689
718
|
* via a `.map()` hole), or a nested element.
|
|
690
719
|
*
|
|
691
720
|
* @param {JxMutableNode | string} c
|
|
721
|
+
* @param {boolean} [preformatted]
|
|
692
722
|
* @returns {string}
|
|
693
723
|
*/
|
|
694
|
-
function emitChildLit(c: JxMutableNode | string): string {
|
|
724
|
+
function emitChildLit(c: JxMutableNode | string, preformatted = false): string {
|
|
695
725
|
if (typeof c === "string") {
|
|
696
726
|
return isTemplateString(c) ? mapRefsToLit(c) : escapeHtml(c);
|
|
697
727
|
}
|
|
@@ -701,7 +731,7 @@ function emitChildLit(c: JxMutableNode | string): string {
|
|
|
701
731
|
if (isMappedArray(c)) {
|
|
702
732
|
return emitArrayHole(c);
|
|
703
733
|
}
|
|
704
|
-
return emitLitMapTemplate(c);
|
|
734
|
+
return emitLitMapTemplate(c, preformatted);
|
|
705
735
|
}
|
|
706
736
|
|
|
707
737
|
/**
|
|
@@ -888,61 +918,8 @@ function emitClientModule(
|
|
|
888
918
|
}
|
|
889
919
|
|
|
890
920
|
// ─── Prototype init emitters ─────────────────────────────────────────────────
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
* @param {string} key
|
|
894
|
-
* @param {JxMutableNode} def
|
|
895
|
-
* @returns {string}
|
|
896
|
-
*/
|
|
897
|
-
function emitRequestInit(key: string, def: JxPrototypeDef) {
|
|
898
|
-
const { url } = def;
|
|
899
|
-
const method = def.method ?? "GET";
|
|
900
|
-
const isTemplateUrl = isTemplateString(url);
|
|
901
|
-
|
|
902
|
-
if (def.manual) {
|
|
903
|
-
return `// ${key}: manual Request — fetch triggered by user action`;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const lines: string[] = [
|
|
907
|
-
`// ${key}: auto-fetch from ${isTemplateUrl ? "(dynamic URL)" : url}`,
|
|
908
|
-
"effect(() => {",
|
|
909
|
-
];
|
|
910
|
-
|
|
911
|
-
if (isTemplateUrl) {
|
|
912
|
-
lines.push(
|
|
913
|
-
` const url = \`${url}\`;`,
|
|
914
|
-
' if (!url || url === "undefined" || url.includes("undefined")) return;',
|
|
915
|
-
);
|
|
916
|
-
} else {
|
|
917
|
-
lines.push(` const url = ${JSON.stringify(url)};`);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
const fetchOpts: string[] = [];
|
|
921
|
-
if (method !== "GET") {
|
|
922
|
-
fetchOpts.push(`method: ${JSON.stringify(method)}`);
|
|
923
|
-
}
|
|
924
|
-
if (def.headers) {
|
|
925
|
-
fetchOpts.push(`headers: ${JSON.stringify(def.headers)}`);
|
|
926
|
-
}
|
|
927
|
-
if (def.body) {
|
|
928
|
-
const bodyStr =
|
|
929
|
-
typeof def.body === "object"
|
|
930
|
-
? JSON.stringify(JSON.stringify(def.body))
|
|
931
|
-
: JSON.stringify(def.body);
|
|
932
|
-
fetchOpts.push(`body: ${bodyStr}`);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
const optsStr = fetchOpts.length > 0 ? `, { ${fetchOpts.join(", ")} }` : "";
|
|
936
|
-
lines.push(
|
|
937
|
-
` fetch(url${optsStr})`,
|
|
938
|
-
" .then(r => r.ok ? r.json() : Promise.reject(r.statusText))",
|
|
939
|
-
` .then(d => { state.${key} = d; })`,
|
|
940
|
-
` .catch(e => { state.${key} = { error: String(e) }; });`,
|
|
941
|
-
"});",
|
|
942
|
-
);
|
|
943
|
-
|
|
944
|
-
return lines.join("\n");
|
|
945
|
-
}
|
|
921
|
+
// Request auto-fetch lives in shared.ts (`emitRequestFetch`) so the element target emits the same
|
|
922
|
+
// Effect against `this.state`.
|
|
946
923
|
|
|
947
924
|
/**
|
|
948
925
|
* @param {string} key
|
|
@@ -995,6 +972,25 @@ function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
|
|
|
995
972
|
* @param {string} ref
|
|
996
973
|
* @returns {string}
|
|
997
974
|
*/
|
|
975
|
+
/**
|
|
976
|
+
* Lower a `$ref` to the JS expression that reads it inside a map callback — the mirror of the
|
|
977
|
+
* element target's `refToExpr`/`mapRefToExpr`. Distinct from `refToBindingKey` below, which
|
|
978
|
+
* flattens `/` to `_` to name a binding and would produce nonsense (`state.$map_item`) as an
|
|
979
|
+
* expression.
|
|
980
|
+
*
|
|
981
|
+
* @param {string} ref
|
|
982
|
+
* @returns {string}
|
|
983
|
+
*/
|
|
984
|
+
function mapRefToClientExpr(ref: string) {
|
|
985
|
+
if (ref.startsWith("$map/")) {
|
|
986
|
+
return ref.slice("$map/".length).replaceAll("/", ".");
|
|
987
|
+
}
|
|
988
|
+
if (ref.startsWith("#/state/")) {
|
|
989
|
+
return `state.${ref.slice("#/state/".length).replaceAll("/", ".")}`;
|
|
990
|
+
}
|
|
991
|
+
return `state.${ref.replaceAll("/", ".")}`;
|
|
992
|
+
}
|
|
993
|
+
|
|
998
994
|
function refToBindingKey(ref: string) {
|
|
999
995
|
if (ref.startsWith("#/state/")) {
|
|
1000
996
|
return ref.slice("#/state/".length).replaceAll("/", "_");
|