@jxsuite/compiler 5.1.2 → 6.0.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/dist/cli.js +218 -346
- package/dist/compiler.js +130 -258
- package/package.json +11 -10
- package/src/cli.ts +12 -14
- package/src/compiler.ts +45 -23
- package/src/shadow.ts +9 -8
- package/src/shared.ts +693 -329
- package/src/site/asset-mounts.ts +22 -12
- package/src/site/bundler.ts +38 -12
- package/src/site/client-runtime.ts +31 -17
- package/src/site/csp.ts +20 -11
- package/src/site/db-push.ts +38 -24
- package/src/site/dev-command.ts +3 -3
- package/src/site/format-host.ts +56 -45
- package/src/site/headers-emitter.ts +7 -4
- package/src/site/i18n.ts +20 -19
- package/src/site/image-cache.ts +16 -3
- package/src/site/image-optimizer.ts +19 -5
- package/src/site/image-transform.ts +56 -60
- package/src/site/img-loading.ts +11 -5
- package/src/site/layout-resolver.ts +7 -6
- package/src/site/link-relations.ts +10 -6
- package/src/site/locale-negotiation.ts +1 -1
- package/src/site/pages-discovery.ts +65 -47
- package/src/site/preview-server.ts +22 -19
- package/src/site/project-sections.ts +16 -3
- package/src/site/prototype-resolver.ts +94 -42
- package/src/site/schema-command.ts +26 -13
- package/src/site/service-worker.ts +2 -2
- package/src/site/site-build.ts +421 -279
- package/src/site/site-loader.ts +6 -3
- package/src/site/static-host.ts +3 -3
- package/src/site/validate-command.ts +64 -34
- package/src/site/well-known.ts +57 -12
- package/src/targets/compile-class.ts +17 -5
- package/src/targets/compile-client.ts +156 -111
- package/src/targets/compile-element.ts +227 -198
- package/src/targets/compile-server.ts +11 -5
- package/src/targets/compile-static.ts +57 -63
- package/src/types.ts +5 -6
|
@@ -32,25 +32,31 @@ import {
|
|
|
32
32
|
isRefObject,
|
|
33
33
|
isSchemaOnly,
|
|
34
34
|
isTemplateString,
|
|
35
|
+
LIT_PART_GUARD,
|
|
35
36
|
PREFORMATTED_TAGS,
|
|
36
37
|
pureSchemeOf,
|
|
37
38
|
resolveStaticValue,
|
|
38
39
|
srcImportBinding,
|
|
40
|
+
templateValueSource,
|
|
39
41
|
} from "../shared.ts";
|
|
40
42
|
import {
|
|
41
43
|
bodyReturnsValue,
|
|
42
44
|
hasStructuredBody,
|
|
45
|
+
isBoolean,
|
|
43
46
|
isExpandedSignal,
|
|
44
47
|
isExpressionDef,
|
|
45
48
|
isFunctionDef,
|
|
46
49
|
isMappedArray,
|
|
47
50
|
isNamedFormulaDef,
|
|
51
|
+
isNonNullObject,
|
|
52
|
+
isNumber,
|
|
48
53
|
isPrototypeDef,
|
|
54
|
+
isString,
|
|
49
55
|
isTagExpression,
|
|
50
56
|
paramNames,
|
|
51
57
|
tagNameCandidates,
|
|
52
58
|
} from "@jxsuite/schema/guards";
|
|
53
|
-
import type { CompileContext
|
|
59
|
+
import type { CompileContext } from "../shared.ts";
|
|
54
60
|
import {
|
|
55
61
|
escapeToken,
|
|
56
62
|
objectKey,
|
|
@@ -60,13 +66,21 @@ import {
|
|
|
60
66
|
} from "@jxsuite/runtime/pointer";
|
|
61
67
|
import type {
|
|
62
68
|
JsonObject,
|
|
69
|
+
JsonPrimitive,
|
|
63
70
|
JsonValue,
|
|
64
71
|
JxDocument,
|
|
72
|
+
JxExpressionNode,
|
|
65
73
|
JxMappedArray,
|
|
66
74
|
JxMutableNode,
|
|
67
75
|
JxStyle,
|
|
68
76
|
} from "@jxsuite/schema/types";
|
|
69
77
|
|
|
78
|
+
/**
|
|
79
|
+
* A repeater as `isMappedArray` proves it: only `$prototype` is checked, so a parsed document can
|
|
80
|
+
* reach the emitter without `items`, which then renders as an empty list.
|
|
81
|
+
*/
|
|
82
|
+
type RepeaterSlot = Omit<JxMappedArray, "items"> & { items?: JxMappedArray["items"] };
|
|
83
|
+
|
|
70
84
|
/** A compiled event handler entry: parameter names + function body. */
|
|
71
85
|
interface HandlerDef {
|
|
72
86
|
args?: string[] | undefined;
|
|
@@ -95,7 +109,7 @@ export function compileClient(
|
|
|
95
109
|
modulePath?: string;
|
|
96
110
|
projectStyle?: JxStyle | null;
|
|
97
111
|
prePaintScheme?: boolean;
|
|
98
|
-
|
|
112
|
+
rewriteSrc?: (specifier: string) => string;
|
|
99
113
|
},
|
|
100
114
|
) {
|
|
101
115
|
const {
|
|
@@ -106,11 +120,7 @@ export function compileClient(
|
|
|
106
120
|
} = opts;
|
|
107
121
|
|
|
108
122
|
const context = createCompileContext(raw, null, raw.state ?? {}, raw.$media ?? {});
|
|
109
|
-
const styleBlock = compileStyles(
|
|
110
|
-
raw,
|
|
111
|
-
raw.$media ?? {},
|
|
112
|
-
(opts.projectStyle ?? null) as JxStyle | null,
|
|
113
|
-
);
|
|
123
|
+
const styleBlock = compileStyles(raw, raw.$media ?? {}, opts.projectStyle ?? null);
|
|
114
124
|
|
|
115
125
|
// Collectors for bindings and handlers
|
|
116
126
|
const counter = { h: 0, l: 0, m: 0, needsLit: false, s: 0, sw: 0, t: 0 };
|
|
@@ -129,11 +139,11 @@ export function compileClient(
|
|
|
129
139
|
|
|
130
140
|
const defs = raw.state ?? {};
|
|
131
141
|
for (const [key, def] of Object.entries(defs)) {
|
|
132
|
-
if (def
|
|
142
|
+
if (!isNonNullObject(def) || Array.isArray(def)) {
|
|
133
143
|
// Naked primitive or array → reactive state
|
|
134
|
-
if (
|
|
144
|
+
if (isTemplateString(def)) {
|
|
135
145
|
// Template string → computed on state so other computeds can ref it
|
|
136
|
-
computedEntries.push([key, `() =>
|
|
146
|
+
computedEntries.push([key, `() => ${templateValueSource(def)}`]);
|
|
137
147
|
} else {
|
|
138
148
|
stateEntries.push([key, def]);
|
|
139
149
|
}
|
|
@@ -142,7 +152,7 @@ export function compileClient(
|
|
|
142
152
|
|
|
143
153
|
// $expression: Shape 5
|
|
144
154
|
if (isExpressionDef(def)) {
|
|
145
|
-
const node = def.$expression
|
|
155
|
+
const node = def.$expression;
|
|
146
156
|
const compiled = compileExpression(node, {
|
|
147
157
|
eventParam: "e",
|
|
148
158
|
statePrefix: "state",
|
|
@@ -162,10 +172,11 @@ export function compileClient(
|
|
|
162
172
|
// $prototype: "Function"
|
|
163
173
|
if (isFunctionDef(def)) {
|
|
164
174
|
const args = def.parameters ? paramNames(def.parameters) : def.arguments;
|
|
165
|
-
|
|
175
|
+
const { body } = def;
|
|
176
|
+
if (Array.isArray(body)) {
|
|
166
177
|
// Structured body (spec §20) → an event handler compiled from statements.
|
|
167
178
|
// With parameters, a positional callable like a named formula.
|
|
168
|
-
const compiled = compileStatements(
|
|
179
|
+
const compiled = compileStatements(body, { eventParam: "e", statePrefix: "state" });
|
|
169
180
|
if (def.parameters && def.parameters.length > 0) {
|
|
170
181
|
initBlocks.push(
|
|
171
182
|
`state[${JSON.stringify(key)}] = ${emitFormulaFn(def, `(() => {\n${compiled}\n})()`)};`,
|
|
@@ -174,19 +185,21 @@ export function compileClient(
|
|
|
174
185
|
onEntries.push([key, { args: ["state", "e"], body: compiled }]);
|
|
175
186
|
}
|
|
176
187
|
} else if (def.$src) {
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
let names = srcImportMap.get(def.$src);
|
|
189
|
+
if (!names) {
|
|
190
|
+
names = new Set();
|
|
191
|
+
srcImportMap.set(def.$src, names);
|
|
179
192
|
}
|
|
180
|
-
|
|
193
|
+
names.add(srcImportBinding(key, def));
|
|
181
194
|
|
|
182
195
|
// $src functions always produce computed entries (they return values)
|
|
183
196
|
computedEntries.push([key, `() => { return ${key}(state); }`]);
|
|
184
|
-
} else if (
|
|
197
|
+
} else if (isString(body) && bodyReturnsValue(body)) {
|
|
185
198
|
// Body contains return → computed
|
|
186
|
-
computedEntries.push([key, `() => { ${
|
|
199
|
+
computedEntries.push([key, `() => { ${body} }`]);
|
|
187
200
|
} else {
|
|
188
201
|
// No return → event handler
|
|
189
|
-
onEntries.push([key, { args: args ?? ["state"], body
|
|
202
|
+
onEntries.push([key, { args: args ?? ["state"], body }]);
|
|
190
203
|
}
|
|
191
204
|
continue;
|
|
192
205
|
}
|
|
@@ -228,12 +241,17 @@ export function compileClient(
|
|
|
228
241
|
}
|
|
229
242
|
}
|
|
230
243
|
|
|
231
|
-
|
|
244
|
+
/*
|
|
245
|
+
* Plain object → reactive state.
|
|
246
|
+
* SAFETY: `raw.state` is parsed JSON, so this entry is structurally a JsonObject; the checks
|
|
247
|
+
* above already took the primitives and arrays and every function, expression, schema-only,
|
|
248
|
+
* expanded-signal and storage/request/cookie shape, leaving the naked object literal.
|
|
249
|
+
*/
|
|
232
250
|
stateEntries.push([key, def as JsonObject]);
|
|
233
251
|
}
|
|
234
252
|
|
|
235
253
|
// Build HTML tree with data-bind markers
|
|
236
|
-
const bodyContent = buildClientNode(raw,
|
|
254
|
+
const bodyContent = buildClientNode(raw, context, bindings, handlers, counter);
|
|
237
255
|
|
|
238
256
|
// Merge inline-discovered bindings/handlers
|
|
239
257
|
for (const [key, expr] of bindings) {
|
|
@@ -257,7 +275,7 @@ export function compileClient(
|
|
|
257
275
|
srcImportMap,
|
|
258
276
|
counter,
|
|
259
277
|
reactivitySrc,
|
|
260
|
-
opts.rewriteSrc
|
|
278
|
+
opts.rewriteSrc,
|
|
261
279
|
);
|
|
262
280
|
|
|
263
281
|
// Build importmap entries — always include lit-html since compiled custom elements need it
|
|
@@ -300,8 +318,7 @@ ${importmapEntries.join(",\n")}
|
|
|
300
318
|
// ─── HTML tree walker ─────────────────────────────────────────────────────────
|
|
301
319
|
|
|
302
320
|
/**
|
|
303
|
-
* @param {JxMutableNode} def
|
|
304
|
-
* @param {JxMutableNode} raw
|
|
321
|
+
* @param {JxMutableNode | JsonPrimitive} def - A node or child slot as the document holds it
|
|
305
322
|
* @param {CompileContext} context
|
|
306
323
|
* @param {Map<string, string>} bindings
|
|
307
324
|
* @param {Map<string, HandlerDef>} handlers
|
|
@@ -317,8 +334,7 @@ ${importmapEntries.join(",\n")}
|
|
|
317
334
|
* @returns {string}
|
|
318
335
|
*/
|
|
319
336
|
function buildClientNode(
|
|
320
|
-
def: JxMutableNode,
|
|
321
|
-
raw: JxMutableNode,
|
|
337
|
+
def: JxMutableNode | JsonPrimitive,
|
|
322
338
|
context: CompileContext,
|
|
323
339
|
bindings: Map<string, string>,
|
|
324
340
|
handlers: Map<string, HandlerDef>,
|
|
@@ -333,21 +349,21 @@ function buildClientNode(
|
|
|
333
349
|
},
|
|
334
350
|
) {
|
|
335
351
|
// String children are text nodes
|
|
336
|
-
if (
|
|
352
|
+
if (isString(def)) {
|
|
337
353
|
return escapeHtml(def);
|
|
338
354
|
}
|
|
339
|
-
if (
|
|
355
|
+
if (isNumber(def) || isBoolean(def)) {
|
|
340
356
|
return escapeHtml(String(def));
|
|
341
357
|
}
|
|
342
|
-
if (!def
|
|
358
|
+
if (!isNonNullObject(def)) {
|
|
343
359
|
return "";
|
|
344
360
|
}
|
|
345
361
|
|
|
346
362
|
const nextContext = createCompileContext(
|
|
347
|
-
|
|
363
|
+
def,
|
|
348
364
|
context.scope,
|
|
349
|
-
|
|
350
|
-
|
|
365
|
+
def.state ?? context.scopeDefs,
|
|
366
|
+
def.$media ?? context.media,
|
|
351
367
|
);
|
|
352
368
|
|
|
353
369
|
/* REFUSED, LOUDLY. A chosen tag has no implementation in this target: the element's identity is
|
|
@@ -371,11 +387,11 @@ function buildClientNode(
|
|
|
371
387
|
|
|
372
388
|
// TextContent bindings
|
|
373
389
|
if (def.textContent !== undefined) {
|
|
374
|
-
const tc =
|
|
390
|
+
const tc = def.textContent;
|
|
375
391
|
if (isRefObject(tc)) {
|
|
376
|
-
const key = refToBindingKey(
|
|
392
|
+
const key = refToBindingKey(tc.$ref);
|
|
377
393
|
bindAttrs.push(`:text-content="${key}"`);
|
|
378
|
-
addRefBinding(bindings, key,
|
|
394
|
+
addRefBinding(bindings, key, tc.$ref);
|
|
379
395
|
needsBind = true;
|
|
380
396
|
} else if (isTemplateString(tc)) {
|
|
381
397
|
const key = `_t${counter.t}`;
|
|
@@ -396,29 +412,32 @@ function buildClientNode(
|
|
|
396
412
|
const key = refToBindingKey(val.$ref);
|
|
397
413
|
bindAttrs.push(`@${eventName}="${key}"`);
|
|
398
414
|
needsBind = true;
|
|
399
|
-
} else if (hasStructuredBody(val)) {
|
|
400
|
-
const key = `_h${counter.h}`;
|
|
401
|
-
counter.h += 1;
|
|
402
|
-
bindAttrs.push(`@${eventName}="${key}"`);
|
|
403
|
-
handlers.set(key, {
|
|
404
|
-
args: ["state", "e"],
|
|
405
|
-
body: compileStatements(val.body, { eventParam: "e", statePrefix: "state" }),
|
|
406
|
-
});
|
|
407
|
-
needsBind = true;
|
|
408
415
|
} else if (isFunctionDef(val)) {
|
|
409
416
|
const key = `_h${counter.h}`;
|
|
410
417
|
counter.h += 1;
|
|
411
418
|
bindAttrs.push(`@${eventName}="${key}"`);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
419
|
+
const { body } = val;
|
|
420
|
+
// A structured body (spec §20) compiles from statements; source text is used as written.
|
|
421
|
+
handlers.set(
|
|
422
|
+
key,
|
|
423
|
+
Array.isArray(body)
|
|
424
|
+
? {
|
|
425
|
+
args: ["state", "e"],
|
|
426
|
+
body: compileStatements(body, { eventParam: "e", statePrefix: "state" }),
|
|
427
|
+
}
|
|
428
|
+
: {
|
|
429
|
+
args: val.parameters
|
|
430
|
+
? paramNames(val.parameters)
|
|
431
|
+
: (val.arguments ?? ["state", "event"]),
|
|
432
|
+
body,
|
|
433
|
+
},
|
|
434
|
+
);
|
|
416
435
|
needsBind = true;
|
|
417
436
|
} else if (isExpressionDef(val)) {
|
|
418
437
|
const key = `_h${counter.h}`;
|
|
419
438
|
counter.h += 1;
|
|
420
439
|
bindAttrs.push(`@${eventName}="${key}"`);
|
|
421
|
-
const compiled = compileExpression(val.$expression
|
|
440
|
+
const compiled = compileExpression(val.$expression, {
|
|
422
441
|
eventParam: "e",
|
|
423
442
|
statePrefix: "state",
|
|
424
443
|
});
|
|
@@ -428,7 +447,7 @@ function buildClientNode(
|
|
|
428
447
|
}
|
|
429
448
|
|
|
430
449
|
// Dynamic style properties
|
|
431
|
-
if (def.style
|
|
450
|
+
if (isNonNullObject(def.style)) {
|
|
432
451
|
for (const [prop, val] of Object.entries(def.style)) {
|
|
433
452
|
if (
|
|
434
453
|
prop.startsWith(":") ||
|
|
@@ -439,7 +458,7 @@ function buildClientNode(
|
|
|
439
458
|
) {
|
|
440
459
|
continue;
|
|
441
460
|
}
|
|
442
|
-
if (val
|
|
461
|
+
if (isNonDeclarationValue(val)) {
|
|
443
462
|
continue;
|
|
444
463
|
}
|
|
445
464
|
if (isTemplateString(val)) {
|
|
@@ -453,12 +472,12 @@ function buildClientNode(
|
|
|
453
472
|
}
|
|
454
473
|
|
|
455
474
|
// Dynamic attributes
|
|
456
|
-
if (def.attributes
|
|
475
|
+
if (isNonNullObject(def.attributes)) {
|
|
457
476
|
for (const [attr, val] of Object.entries(def.attributes)) {
|
|
458
477
|
if (isRefObject(val)) {
|
|
459
|
-
const key = refToBindingKey(
|
|
478
|
+
const key = refToBindingKey(val.$ref);
|
|
460
479
|
bindAttrs.push(`:attr.${attr}="${key}"`);
|
|
461
|
-
addRefBinding(bindings, key,
|
|
480
|
+
addRefBinding(bindings, key, val.$ref);
|
|
462
481
|
needsBind = true;
|
|
463
482
|
} else if (isTemplateString(val)) {
|
|
464
483
|
const key = `_t${counter.t}`;
|
|
@@ -488,9 +507,9 @@ function buildClientNode(
|
|
|
488
507
|
continue;
|
|
489
508
|
}
|
|
490
509
|
if (isRefObject(val)) {
|
|
491
|
-
const key = refToBindingKey(
|
|
510
|
+
const key = refToBindingKey(val.$ref);
|
|
492
511
|
bindAttrs.push(`:${camelToKebab(prop)}="${key}"`);
|
|
493
|
-
addRefBinding(bindings, key,
|
|
512
|
+
addRefBinding(bindings, key, val.$ref);
|
|
494
513
|
needsBind = true;
|
|
495
514
|
} else if (isTemplateString(val)) {
|
|
496
515
|
const key = `_t${counter.t}`;
|
|
@@ -508,7 +527,7 @@ function buildClientNode(
|
|
|
508
527
|
|
|
509
528
|
// Inner content
|
|
510
529
|
let inner = "";
|
|
511
|
-
const source =
|
|
530
|
+
const source = def;
|
|
512
531
|
if (source.$switch) {
|
|
513
532
|
// ─── $switch → lit-html render binding ───
|
|
514
533
|
// Without this branch the node fell through to the generic element path, which emitted a
|
|
@@ -521,10 +540,11 @@ function buildClientNode(
|
|
|
521
540
|
const swKey = `_sw${counter.sw}`;
|
|
522
541
|
counter.sw += 1;
|
|
523
542
|
const discriminant = isRefObject(source.$switch)
|
|
524
|
-
? `state.${refToBindingKey(
|
|
543
|
+
? `state.${refToBindingKey(source.$switch.$ref)}`
|
|
525
544
|
: `\`${String(source.$switch)}\``;
|
|
526
|
-
const cases =
|
|
527
|
-
|
|
545
|
+
const cases = source.cases ?? {};
|
|
546
|
+
// Parsed JSON, so a case slot can hold `null` or a bare value the declared type leaves out.
|
|
547
|
+
const caseEntries = Object.entries<JxMutableNode | JsonPrimitive>(cases)
|
|
528
548
|
// An external `$ref` case cannot be fetched at compile time, exactly as in the static target.
|
|
529
549
|
.filter(([, caseDef]) => caseDef && !isRefObject(caseDef))
|
|
530
550
|
.map(
|
|
@@ -545,7 +565,8 @@ function buildClientNode(
|
|
|
545
565
|
} else if (source.innerHTML) {
|
|
546
566
|
// ResolveStaticValue may return null if innerHTML contains `${` from rendered content
|
|
547
567
|
// (e.g., code examples) that isn't an actual template expression. Fall back to raw value.
|
|
548
|
-
|
|
568
|
+
const resolved = resolveStaticValue(source.innerHTML, nextContext.scope);
|
|
569
|
+
inner = resolved == null ? source.innerHTML : String(resolved);
|
|
549
570
|
} else if (isMappedArray(source.children)) {
|
|
550
571
|
// ─── Mapped array → lit-html render binding ───
|
|
551
572
|
counter.needsLit = true;
|
|
@@ -588,7 +609,7 @@ function buildClientNode(
|
|
|
588
609
|
const listKey = `_children${counter.l}`;
|
|
589
610
|
counter.l += 1;
|
|
590
611
|
const childrenTpl = source.children
|
|
591
|
-
.map((c) => emitChildLit(c
|
|
612
|
+
.map((c) => emitChildLit(c, nextContext.preformatted))
|
|
592
613
|
.join(childSeparator(nextContext.preformatted, " "));
|
|
593
614
|
bindings.set(
|
|
594
615
|
listKey,
|
|
@@ -601,19 +622,8 @@ function buildClientNode(
|
|
|
601
622
|
const bindAttrStr2 = ` ${bindAttrs.join(" ")}`;
|
|
602
623
|
return `<${tag}${staticAttrs} data-bind${bindAttrStr2}></${tag}>`;
|
|
603
624
|
} else if (Array.isArray(source.children)) {
|
|
604
|
-
const rawChildren = Array.isArray(raw?.children) ? raw.children : undefined;
|
|
605
625
|
inner = source.children
|
|
606
|
-
.map((c
|
|
607
|
-
const childRaw = rawChildren?.[i] ?? c;
|
|
608
|
-
return buildClientNode(
|
|
609
|
-
c as JxMutableNode,
|
|
610
|
-
childRaw as JxMutableNode,
|
|
611
|
-
nextContext,
|
|
612
|
-
bindings,
|
|
613
|
-
handlers,
|
|
614
|
-
counter,
|
|
615
|
-
);
|
|
616
|
-
})
|
|
626
|
+
.map((c) => buildClientNode(c, nextContext, bindings, handlers, counter))
|
|
617
627
|
.join(childSeparator(nextContext.preformatted));
|
|
618
628
|
}
|
|
619
629
|
|
|
@@ -632,11 +642,12 @@ function buildClientNode(
|
|
|
632
642
|
* Compile a map definition to a lit-html template string. Converts $map.item → item, $map.index →
|
|
633
643
|
* index.
|
|
634
644
|
*
|
|
635
|
-
* @param {JxMutableNode} def
|
|
645
|
+
* @param {JxMutableNode | null | undefined} def - Absent, or a JSON `null` child slot, renders
|
|
646
|
+
* nothing
|
|
636
647
|
* @param {boolean} [preformatted]
|
|
637
648
|
* @returns {string}
|
|
638
649
|
*/
|
|
639
|
-
function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false) {
|
|
650
|
+
function emitLitMapTemplate(def: JxMutableNode | null | undefined, preformatted = false) {
|
|
640
651
|
if (!def) {
|
|
641
652
|
return "";
|
|
642
653
|
}
|
|
@@ -663,11 +674,11 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
663
674
|
|
|
664
675
|
// $props → lit property bindings. The element target emits these; the client target emitted
|
|
665
676
|
// Nothing, so a component in a list silently received none of its per-item props.
|
|
666
|
-
if (def.$props
|
|
677
|
+
if (isNonNullObject(def.$props)) {
|
|
667
678
|
for (const [k, v] of Object.entries(def.$props)) {
|
|
668
679
|
if (isRefObject(v)) {
|
|
669
|
-
attrs += ` .${k}="\${${mapRefToClientExpr(
|
|
670
|
-
} else if (
|
|
680
|
+
attrs += ` .${k}="\${${mapRefToClientExpr(v.$ref)}}"`;
|
|
681
|
+
} else if (isTemplateString(v)) {
|
|
671
682
|
attrs += ` .${k}="${mapRefsToLit(v)}"`;
|
|
672
683
|
} else {
|
|
673
684
|
attrs += ` .${k}="\${${JSON.stringify(v)}}"`;
|
|
@@ -676,9 +687,9 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
676
687
|
}
|
|
677
688
|
|
|
678
689
|
// Attributes object
|
|
679
|
-
if (def.attributes
|
|
690
|
+
if (isNonNullObject(def.attributes)) {
|
|
680
691
|
for (const [k, v] of Object.entries(def.attributes)) {
|
|
681
|
-
if (
|
|
692
|
+
if (isTemplateString(v)) {
|
|
682
693
|
attrs += ` ${k}="${mapRefsToLit(v)}"`;
|
|
683
694
|
continue;
|
|
684
695
|
}
|
|
@@ -688,7 +699,7 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
688
699
|
as it hydrates (spec.md §8.3). Stringifying here emitted `open="false"`, which HTML reads
|
|
689
700
|
by presence and therefore as an OPEN element: the author wrote "closed" and the page
|
|
690
701
|
rendered open. */
|
|
691
|
-
if (
|
|
702
|
+
if (isBoolean(v)) {
|
|
692
703
|
const text = booleanAttrValue(k, v);
|
|
693
704
|
if (text !== null) {
|
|
694
705
|
attrs += text === "" ? ` ${k}` : ` ${k}="${escapeHtml(text)}"`;
|
|
@@ -700,7 +711,7 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
700
711
|
}
|
|
701
712
|
|
|
702
713
|
// Style → inline CSS
|
|
703
|
-
if (def.style
|
|
714
|
+
if (isNonNullObject(def.style)) {
|
|
704
715
|
const parts: string[] = [];
|
|
705
716
|
for (const [k, v] of Object.entries(def.style)) {
|
|
706
717
|
if (
|
|
@@ -712,7 +723,7 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
712
723
|
) {
|
|
713
724
|
continue;
|
|
714
725
|
}
|
|
715
|
-
if (v
|
|
726
|
+
if (isNonDeclarationValue(v)) {
|
|
716
727
|
continue;
|
|
717
728
|
}
|
|
718
729
|
const cssProp = camelToKebab(k);
|
|
@@ -734,22 +745,30 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
734
745
|
}
|
|
735
746
|
const eventName = prop.slice(2).toLowerCase();
|
|
736
747
|
if (isRefObject(val)) {
|
|
737
|
-
const key = refToBindingKey(
|
|
748
|
+
const key = refToBindingKey(val.$ref);
|
|
738
749
|
attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; on.${key}(e); }}`;
|
|
739
750
|
} else if (hasStructuredBody(val)) {
|
|
740
751
|
const compiled = compileStatements(val.body, { eventParam: "e", statePrefix: "state" });
|
|
741
752
|
attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; ${compiled} }}`;
|
|
742
|
-
} else if (
|
|
743
|
-
|
|
753
|
+
} else if (isFunctionDef(val)) {
|
|
754
|
+
/*
|
|
755
|
+
* SAFETY: hasStructuredBody above took every statement-array body, so `body` is source text
|
|
756
|
+
* or absent. An absent one (a `$src` function) reaches replaceAll and throws, as it always
|
|
757
|
+
* has: a repeater handler is emitted inline and needs inline source.
|
|
758
|
+
*/
|
|
759
|
+
const body = mapRefsToLit(val.body as string);
|
|
744
760
|
attrs += ` @${eventName}=\${(e) => { ${body} }}`;
|
|
745
|
-
} else if (val &&
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
761
|
+
} else if (isNonNullObject(val) && "$expression" in val) {
|
|
762
|
+
/*
|
|
763
|
+
* SAFETY: an `on*` member carrying `$expression` is a Shape 5 expression entry, whose
|
|
764
|
+
* `$expression` the document schema defines as an expression node. Presence is all this
|
|
765
|
+
* branch has ever tested; a malformed node is not filtered here but fails inside
|
|
766
|
+
* compileExpression, as it did before.
|
|
767
|
+
*/
|
|
768
|
+
const compiled = compileExpression(val.$expression as JxExpressionNode, {
|
|
769
|
+
eventParam: "e",
|
|
770
|
+
statePrefix: "state",
|
|
771
|
+
});
|
|
753
772
|
attrs += ` @${eventName}=\${(e) => { state.$map = { item, index }; ${compiled}; }}`;
|
|
754
773
|
}
|
|
755
774
|
}
|
|
@@ -759,15 +778,19 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
759
778
|
attrs += ` contenteditable="${def.contentEditable}"`;
|
|
760
779
|
}
|
|
761
780
|
|
|
781
|
+
/* A row can hold a component instance, whose children its slots redistribute; a child part
|
|
782
|
+
there ends at a guard rather than at a sibling that may move (`LIT_PART_GUARD`). */
|
|
783
|
+
const guard = tag.includes("-") ? LIT_PART_GUARD : "";
|
|
784
|
+
|
|
762
785
|
// Inner content
|
|
763
786
|
let inner = "";
|
|
764
787
|
if (def.textContent !== undefined) {
|
|
765
788
|
const tc = String(def.textContent);
|
|
766
789
|
if (isTemplateString(tc)) {
|
|
767
|
-
inner = mapRefsToLit(tc)
|
|
790
|
+
inner = `${mapRefsToLit(tc)}${guard}`;
|
|
768
791
|
} else if (isRefObject(def.textContent)) {
|
|
769
|
-
const path = refToBindingKey(
|
|
770
|
-
inner = `\${state.${path}}`;
|
|
792
|
+
const path = refToBindingKey(def.textContent.$ref);
|
|
793
|
+
inner = `\${state.${path}}${guard}`;
|
|
771
794
|
} else {
|
|
772
795
|
inner = escapeHtml(tc);
|
|
773
796
|
}
|
|
@@ -775,9 +798,13 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
775
798
|
inner = mapRefsToLit(String(def.innerHTML));
|
|
776
799
|
} else if (isMappedArray(def.children)) {
|
|
777
800
|
// Legacy whole-children repeater nested inside a map template.
|
|
778
|
-
inner = `\n ${emitArrayHole(def.children)}\n `;
|
|
801
|
+
inner = `\n ${emitArrayHole(def.children)}${guard}\n `;
|
|
779
802
|
} else if (Array.isArray(def.children)) {
|
|
780
|
-
const rendered = def.children.map((c) =>
|
|
803
|
+
const rendered = def.children.map((c) => {
|
|
804
|
+
const text = emitChildLit(c, inPre);
|
|
805
|
+
const isPart = isMappedArray(c) || (isString(c) && isTemplateString(c));
|
|
806
|
+
return isPart ? `${text}${guard}` : text;
|
|
807
|
+
});
|
|
781
808
|
inner = inPre ? rendered.join("") : `\n ${rendered.join("\n ")}\n `;
|
|
782
809
|
}
|
|
783
810
|
|
|
@@ -792,15 +819,15 @@ function emitLitMapTemplate(def: JxMutableNode | undefined, preformatted = false
|
|
|
792
819
|
* Emit one child of a lit template: a string text node, an array pseudo-element (expanded inline
|
|
793
820
|
* via a `.map()` hole), or a nested element.
|
|
794
821
|
*
|
|
795
|
-
* @param {JxMutableNode |
|
|
822
|
+
* @param {JxMutableNode | JsonPrimitive} c - A child slot as the document holds it
|
|
796
823
|
* @param {boolean} [preformatted]
|
|
797
824
|
* @returns {string}
|
|
798
825
|
*/
|
|
799
|
-
function emitChildLit(c: JxMutableNode |
|
|
800
|
-
if (
|
|
826
|
+
function emitChildLit(c: JxMutableNode | JsonPrimitive, preformatted = false): string {
|
|
827
|
+
if (isString(c)) {
|
|
801
828
|
return isTemplateString(c) ? mapRefsToLit(c) : escapeHtml(c);
|
|
802
829
|
}
|
|
803
|
-
if (
|
|
830
|
+
if (isNumber(c) || isBoolean(c)) {
|
|
804
831
|
return escapeHtml(String(c));
|
|
805
832
|
}
|
|
806
833
|
if (isMappedArray(c)) {
|
|
@@ -813,13 +840,13 @@ function emitChildLit(c: JxMutableNode | string, preformatted = false): string {
|
|
|
813
840
|
* Emit a lit-html `${(items ?? []).map(...)}` hole for a mapped array, expanding its template
|
|
814
841
|
* inline among sibling nodes (wrapper-less).
|
|
815
842
|
*
|
|
816
|
-
* @param {
|
|
843
|
+
* @param {RepeaterSlot} arrayDef
|
|
817
844
|
* @returns {string}
|
|
818
845
|
*/
|
|
819
|
-
function emitArrayHole(arrayDef:
|
|
846
|
+
function emitArrayHole(arrayDef: RepeaterSlot): string {
|
|
820
847
|
const { items } = arrayDef;
|
|
821
848
|
const itemsExpr = isRefObject(items)
|
|
822
|
-
? `state.${refToBindingKey(
|
|
849
|
+
? `state.${refToBindingKey(items.$ref)}`
|
|
823
850
|
: JSON.stringify(items ?? []);
|
|
824
851
|
const tpl = emitLitMapTemplate(arrayDef.map);
|
|
825
852
|
return `\${(${itemsExpr} ?? []).map((item, index) => html\`${tpl}\`)}`;
|
|
@@ -1013,10 +1040,15 @@ function emitClientModule(
|
|
|
1013
1040
|
* @param {string} key
|
|
1014
1041
|
* @param {string} storeName
|
|
1015
1042
|
* @param {string} storageKey
|
|
1016
|
-
* @param {
|
|
1043
|
+
* @param {JsonValue} defaultVal
|
|
1017
1044
|
* @returns {string}
|
|
1018
1045
|
*/
|
|
1019
|
-
function emitStorageInit(
|
|
1046
|
+
function emitStorageInit(
|
|
1047
|
+
key: string,
|
|
1048
|
+
storeName: string,
|
|
1049
|
+
storageKey: string,
|
|
1050
|
+
defaultVal: JsonValue,
|
|
1051
|
+
) {
|
|
1020
1052
|
const lines: string[] = [
|
|
1021
1053
|
`// ${key}: ${storeName} (key: "${storageKey}")`,
|
|
1022
1054
|
"try {",
|
|
@@ -1037,10 +1069,10 @@ function emitStorageInit(key: string, storeName: string, storageKey: string, def
|
|
|
1037
1069
|
/**
|
|
1038
1070
|
* @param {string} key
|
|
1039
1071
|
* @param {string} cookieName
|
|
1040
|
-
* @param {
|
|
1072
|
+
* @param {JsonValue} defaultVal
|
|
1041
1073
|
* @returns {string}
|
|
1042
1074
|
*/
|
|
1043
|
-
function emitCookieInit(key: string, cookieName: string, defaultVal:
|
|
1075
|
+
function emitCookieInit(key: string, cookieName: string, defaultVal: JsonValue) {
|
|
1044
1076
|
const lines: string[] = [
|
|
1045
1077
|
`// ${key}: Cookie (name: "${cookieName}")`,
|
|
1046
1078
|
"{",
|
|
@@ -1056,6 +1088,19 @@ function emitCookieInit(key: string, cookieName: string, defaultVal: unknown) {
|
|
|
1056
1088
|
|
|
1057
1089
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
1058
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* Whether a style entry is something other than one declaration value: a nested block, a block
|
|
1093
|
+
* list, a `$ref`, or a JSON `null`. None of those is emitted as an inline declaration.
|
|
1094
|
+
*
|
|
1095
|
+
* @param {JxStyle[string] | null} value
|
|
1096
|
+
* @returns {boolean}
|
|
1097
|
+
*/
|
|
1098
|
+
function isNonDeclarationValue(
|
|
1099
|
+
value: JxStyle[string] | null,
|
|
1100
|
+
): value is Exclude<JxStyle[string], string | number | undefined> | null {
|
|
1101
|
+
return value === null || isNonNullObject(value);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1059
1104
|
/**
|
|
1060
1105
|
* @param {string} ref
|
|
1061
1106
|
* @returns {string}
|