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