@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
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
ATTR_HELPER,
|
|
11
11
|
attrHelperSource,
|
|
12
12
|
booleanAttrValue,
|
|
13
|
+
definitionHasSlot,
|
|
14
|
+
LIT_PART_GUARD,
|
|
13
15
|
PREFORMATTED_TAGS,
|
|
16
|
+
SLOT_CAPTURE_HELPER,
|
|
17
|
+
SLOT_DISTRIBUTE_HELPER,
|
|
18
|
+
slotHelperSource,
|
|
14
19
|
collectStyles,
|
|
15
20
|
compileExpression,
|
|
16
21
|
compileOperandSource,
|
|
@@ -23,35 +28,52 @@ import {
|
|
|
23
28
|
srcExportName,
|
|
24
29
|
srcImportBinding,
|
|
25
30
|
tagNameToClassName,
|
|
31
|
+
templateValueSource,
|
|
26
32
|
} from "../shared.ts";
|
|
27
33
|
import {
|
|
28
34
|
bodyReturnsValue,
|
|
29
35
|
hasStructuredBody,
|
|
30
36
|
isExpressionDef,
|
|
37
|
+
isBoolean,
|
|
31
38
|
isFunctionDef,
|
|
32
39
|
isMappedArray,
|
|
33
40
|
isNamedFormulaDef,
|
|
41
|
+
isNonNullObject,
|
|
42
|
+
isNumber,
|
|
43
|
+
isPrototypeDef,
|
|
34
44
|
isRef,
|
|
45
|
+
isString,
|
|
35
46
|
isTagExpression,
|
|
36
47
|
paramNames,
|
|
37
48
|
} from "@jxsuite/schema/guards";
|
|
38
49
|
import { parseJxDocument } from "@jxsuite/schema/parse";
|
|
39
|
-
import type { ExpressionNode } from "../shared.ts";
|
|
40
50
|
import { resolveShadowMode } from "../shadow.ts";
|
|
41
51
|
import type { ShadowMode } from "../shadow.ts";
|
|
42
52
|
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
43
53
|
import { escapeToken, objectKey, refAccessor, refSegments } from "@jxsuite/runtime/pointer";
|
|
44
54
|
import type {
|
|
55
|
+
JsonPrimitive,
|
|
45
56
|
JxDocument,
|
|
57
|
+
JxDocumentValue,
|
|
46
58
|
JxExpressionDef,
|
|
59
|
+
JxExpressionNode,
|
|
47
60
|
JxFunctionDef,
|
|
48
61
|
JxMappedArray,
|
|
49
62
|
JxMutableNode,
|
|
50
63
|
JxPrototypeDef,
|
|
64
|
+
JxRef,
|
|
65
|
+
JxStateDefinition,
|
|
51
66
|
JxStyle,
|
|
52
67
|
ProjectConfig,
|
|
53
68
|
} from "@jxsuite/schema/types";
|
|
54
69
|
|
|
70
|
+
/**
|
|
71
|
+
* A member of a node's `children` (or a `$switch` case) as a parsed document holds it: an element,
|
|
72
|
+
* a text string, or — since the document is JSON — a number, boolean or `null`, rendered as text or
|
|
73
|
+
* skipped.
|
|
74
|
+
*/
|
|
75
|
+
type LitChild = JxMutableNode | JsonPrimitive;
|
|
76
|
+
|
|
55
77
|
/** Options accepted by compileElement. */
|
|
56
78
|
export interface CompileElementOptions {
|
|
57
79
|
/** Resolve an `$elements` ref path to its emitted JS module path. */
|
|
@@ -67,7 +89,6 @@ export interface CompileElementOptions {
|
|
|
67
89
|
basePath?: string | null;
|
|
68
90
|
/** Project `defaults`, read for `defaults.shadow` (spec.md §16.6). */
|
|
69
91
|
defaults?: ProjectConfig["defaults"];
|
|
70
|
-
[key: string]: unknown;
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
/**
|
|
@@ -93,7 +114,7 @@ export async function compileElement(
|
|
|
93
114
|
async function processElement(srcPath: string | JxDocument, parentDir: string | null) {
|
|
94
115
|
let doc: JxDocument;
|
|
95
116
|
let filePath: string | null;
|
|
96
|
-
if (
|
|
117
|
+
if (isString(srcPath)) {
|
|
97
118
|
const { readFileSync } = await import("node:fs");
|
|
98
119
|
const { resolve } = await import("node:path");
|
|
99
120
|
filePath = parentDir ? resolve(parentDir, srcPath) : resolve(srcPath);
|
|
@@ -106,13 +127,12 @@ export async function compileElement(
|
|
|
106
127
|
} else {
|
|
107
128
|
const { extname } = await import("node:path");
|
|
108
129
|
const ext = extname(filePath).toLowerCase();
|
|
109
|
-
const entry = formats?.byExtension
|
|
130
|
+
const entry = formats?.byExtension(ext, "parse");
|
|
110
131
|
if (!entry) {
|
|
111
132
|
const { unknownFormatError } = await import("../site/format-host.ts");
|
|
112
133
|
throw unknownFormatError(filePath, ext);
|
|
113
134
|
}
|
|
114
|
-
|
|
115
|
-
doc = (await entry.call("parse", readFileSync(filePath, "utf8"))) as JxDocument;
|
|
135
|
+
doc = await entry.parseDocument(readFileSync(filePath, "utf8"));
|
|
116
136
|
}
|
|
117
137
|
} else {
|
|
118
138
|
doc = srcPath;
|
|
@@ -138,7 +158,7 @@ export async function compileElement(
|
|
|
138
158
|
if (Array.isArray(doc.$elements)) {
|
|
139
159
|
for (const elRef of doc.$elements) {
|
|
140
160
|
const refPath = isRef(elRef) ? elRef.$ref : elRef;
|
|
141
|
-
if (
|
|
161
|
+
if (!isString(refPath)) {
|
|
142
162
|
continue;
|
|
143
163
|
}
|
|
144
164
|
|
|
@@ -174,12 +194,19 @@ export async function compileElement(
|
|
|
174
194
|
return { files };
|
|
175
195
|
}
|
|
176
196
|
|
|
197
|
+
/** Options accepted by compileElementPage: the page shell's, plus everything compileElement takes. */
|
|
198
|
+
export interface CompileElementPageOptions extends CompileElementOptions {
|
|
199
|
+
title?: string;
|
|
200
|
+
reactivitySrc?: string;
|
|
201
|
+
litHtmlSrc?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
177
204
|
/**
|
|
178
205
|
* Compile a Jx custom element document to a complete HTML page with an import map for CDN
|
|
179
206
|
* dependencies.
|
|
180
207
|
*
|
|
181
|
-
* @param {string |
|
|
182
|
-
* @param {
|
|
208
|
+
* @param {string | JxDocument} sourcePath
|
|
209
|
+
* @param {CompileElementPageOptions} [opts]
|
|
183
210
|
* @returns {Promise<{
|
|
184
211
|
* html: string;
|
|
185
212
|
* files: { path: string; content: string; tagName: string }[];
|
|
@@ -187,12 +214,7 @@ export async function compileElement(
|
|
|
187
214
|
*/
|
|
188
215
|
export async function compileElementPage(
|
|
189
216
|
sourcePath: string | JxDocument,
|
|
190
|
-
opts: {
|
|
191
|
-
title?: string;
|
|
192
|
-
reactivitySrc?: string;
|
|
193
|
-
litHtmlSrc?: string;
|
|
194
|
-
[key: string]: unknown;
|
|
195
|
-
} = {},
|
|
217
|
+
opts: CompileElementPageOptions = {},
|
|
196
218
|
) {
|
|
197
219
|
const {
|
|
198
220
|
title = "Jx App",
|
|
@@ -240,11 +262,11 @@ export async function compileElementPage(
|
|
|
240
262
|
* like { type, default, description } now correctly extract the `default` value instead of dumping
|
|
241
263
|
* the whole object.
|
|
242
264
|
*
|
|
243
|
-
* @param {
|
|
265
|
+
* @param {JxStateDefinition} def
|
|
244
266
|
* @returns {string | undefined}
|
|
245
267
|
*/
|
|
246
|
-
function extractInitialValue(def:
|
|
247
|
-
if (def
|
|
268
|
+
function extractInitialValue(def: JxStateDefinition) {
|
|
269
|
+
if (!isNonNullObject(def) || Array.isArray(def)) {
|
|
248
270
|
return JSON.stringify(def);
|
|
249
271
|
}
|
|
250
272
|
// Expanded signal with explicit default
|
|
@@ -286,21 +308,25 @@ function collectCallableRefs(doc: JxDocument) {
|
|
|
286
308
|
const keys = new Set<string>();
|
|
287
309
|
const seen = new Set<object>();
|
|
288
310
|
|
|
289
|
-
const addStateRef = (ref:
|
|
290
|
-
if (
|
|
311
|
+
const addStateRef = (ref: string) => {
|
|
312
|
+
if (ref.startsWith("#/state/")) {
|
|
313
|
+
/* SAFETY: `String.prototype.split` always returns at least one element (the whole string
|
|
314
|
+
when the separator is absent), so index 0 is present. */
|
|
291
315
|
keys.add(ref.slice("#/state/".length).split("/")[0] as string);
|
|
292
316
|
}
|
|
293
317
|
};
|
|
294
318
|
|
|
295
|
-
const visit = (node:
|
|
296
|
-
if (
|
|
319
|
+
const visit = (node: JxDocumentValue | undefined) => {
|
|
320
|
+
if (isString(node)) {
|
|
297
321
|
// `${state.formatDate(d)}` in a template, or `state.helper(x)` in another function's body.
|
|
298
322
|
for (const call of node.matchAll(/\bstate\.([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
323
|
+
/* SAFETY: the pattern's one capture group is not optional, so every match fills
|
|
324
|
+
index 1 with the called member's name. */
|
|
299
325
|
keys.add(call[1] as string);
|
|
300
326
|
}
|
|
301
327
|
return;
|
|
302
328
|
}
|
|
303
|
-
if (!node ||
|
|
329
|
+
if (!isNonNullObject(node) || seen.has(node)) {
|
|
304
330
|
return;
|
|
305
331
|
}
|
|
306
332
|
seen.add(node);
|
|
@@ -310,10 +336,13 @@ function collectCallableRefs(doc: JxDocument) {
|
|
|
310
336
|
}
|
|
311
337
|
return;
|
|
312
338
|
}
|
|
313
|
-
|
|
339
|
+
/* SAFETY: `node` is a non-null, non-array object reached from `doc.children` or `doc.state`,
|
|
340
|
+
and every such object in a parsed Jx document is a JSON object whose members are document
|
|
341
|
+
values; the view only reads members, and each is checked before use. */
|
|
342
|
+
const record = node as Readonly<Record<string, JxDocumentValue | undefined>>;
|
|
314
343
|
// An `$expression` `call` node names its callee in `target` (spec.md §19.4c).
|
|
315
344
|
if (record.operator === "call" && isRef(record.target)) {
|
|
316
|
-
addStateRef(
|
|
345
|
+
addStateRef(record.target.$ref);
|
|
317
346
|
}
|
|
318
347
|
for (const [key, val] of Object.entries(record)) {
|
|
319
348
|
if (key.startsWith("on") && key !== "observedAttributes" && isRef(val)) {
|
|
@@ -360,10 +389,9 @@ export function emitElementModule(
|
|
|
360
389
|
const defs = doc.state ?? {};
|
|
361
390
|
const callableRefs = collectCallableRefs(doc);
|
|
362
391
|
for (const [key, def] of Object.entries(defs)) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (d.$lazy === true) {
|
|
392
|
+
if (isFunctionDef(def) && def.$src) {
|
|
393
|
+
const srcPath = def.$src;
|
|
394
|
+
if (def.$lazy === true) {
|
|
367
395
|
/*
|
|
368
396
|
* A computed reads its value synchronously, and a lazy binding can only ever hand back a
|
|
369
397
|
* promise. Silently producing `computed(() => Promise)` would render "[object Promise]" in
|
|
@@ -376,19 +404,20 @@ export function emitElementModule(
|
|
|
376
404
|
`Remove "$lazy", or call ${key}() from a handler instead of binding its value.`,
|
|
377
405
|
);
|
|
378
406
|
}
|
|
379
|
-
|
|
380
|
-
|
|
407
|
+
let lazyBindings = lazySrcMap.get(srcPath);
|
|
408
|
+
if (!lazyBindings) {
|
|
409
|
+
lazyBindings = [];
|
|
410
|
+
lazySrcMap.set(srcPath, lazyBindings);
|
|
381
411
|
}
|
|
382
|
-
|
|
383
|
-
binding: key,
|
|
384
|
-
exportName: srcExportName(key, d),
|
|
385
|
-
});
|
|
412
|
+
lazyBindings.push({ binding: key, exportName: srcExportName(key, def) });
|
|
386
413
|
continue;
|
|
387
414
|
}
|
|
388
|
-
|
|
389
|
-
|
|
415
|
+
let bindings = srcImportMap.get(srcPath);
|
|
416
|
+
if (!bindings) {
|
|
417
|
+
bindings = [];
|
|
418
|
+
srcImportMap.set(srcPath, bindings);
|
|
390
419
|
}
|
|
391
|
-
|
|
420
|
+
bindings.push(srcImportBinding(key, def));
|
|
392
421
|
}
|
|
393
422
|
}
|
|
394
423
|
for (const [srcPath, names] of srcImportMap) {
|
|
@@ -401,10 +430,9 @@ export function emitElementModule(
|
|
|
401
430
|
// `$prototype: "Request"` entries each get an auto-fetch effect in connectedCallback, plus the
|
|
402
431
|
// Machinery to stop those effects again on disconnect. Both are emitted only when one exists, so
|
|
403
432
|
// Documents without a Request produce exactly the same module as before.
|
|
404
|
-
const requestEntries = Object.entries(defs).
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
});
|
|
433
|
+
const requestEntries = Object.entries(defs).flatMap(([key, def]): [string, JxPrototypeDef][] =>
|
|
434
|
+
isPrototypeDef(def) && def.$prototype === "Request" ? [[key, def]] : [],
|
|
435
|
+
);
|
|
408
436
|
|
|
409
437
|
lines.push(
|
|
410
438
|
`import { reactive, computed, effect, stop } from '@vue/reactivity';`,
|
|
@@ -414,6 +442,13 @@ export function emitElementModule(
|
|
|
414
442
|
`import { render, html, nothing } from 'lit-html';`,
|
|
415
443
|
attrHelperSource(),
|
|
416
444
|
);
|
|
445
|
+
/* Light-DOM slot distribution, by the rule the interpreter applies (spec.md §8.5): only a module
|
|
446
|
+
whose template draws a slot carries it, and a shadow component never does — its light children
|
|
447
|
+
ARE the slotted content, and a real `<slot>` distributes them. */
|
|
448
|
+
const hasSlot = shadow === null && definitionHasSlot(doc.children);
|
|
449
|
+
if (hasSlot) {
|
|
450
|
+
lines.push(slotHelperSource());
|
|
451
|
+
}
|
|
417
452
|
|
|
418
453
|
/*
|
|
419
454
|
* `$lazy` swaps the static import for a memoized dynamic one. A static import is a download, a
|
|
@@ -460,32 +495,30 @@ export function emitElementModule(
|
|
|
460
495
|
/** `"${…}"` state entries — computed, exactly as the runtime treats them. */
|
|
461
496
|
const templateEntries: [string, string][] = [];
|
|
462
497
|
for (const [key, def] of Object.entries(defs)) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
} else if (isMutating(node.operator)) {
|
|
469
|
-
functionEntries.push([key, d]);
|
|
498
|
+
if (isExpressionDef(def)) {
|
|
499
|
+
if (isNamedFormulaDef(def)) {
|
|
500
|
+
formulaEntries.push([key, def]);
|
|
501
|
+
} else if (isMutating(def.$expression.operator)) {
|
|
502
|
+
functionEntries.push([key, def]);
|
|
470
503
|
} else {
|
|
471
|
-
computedEntries.push([key,
|
|
504
|
+
computedEntries.push([key, def]);
|
|
472
505
|
}
|
|
473
|
-
} else if (isFunctionDef(
|
|
474
|
-
if (
|
|
506
|
+
} else if (isFunctionDef(def)) {
|
|
507
|
+
if (def.$src && !isString(def.body)) {
|
|
475
508
|
// Bodyless `$src` entry — classify by document usage, not by an absent body. `body` and
|
|
476
509
|
// `$src` are mutually exclusive (spec.md §5.3 4d), so this is the only shape a
|
|
477
510
|
// Spec-conformant external Function takes.
|
|
478
511
|
if (callableRefs.has(key) || LIFECYCLE_KEYS.has(key)) {
|
|
479
|
-
functionEntries.push([key,
|
|
512
|
+
functionEntries.push([key, def]);
|
|
480
513
|
} else {
|
|
481
|
-
computedEntries.push([key,
|
|
514
|
+
computedEntries.push([key, def]);
|
|
482
515
|
}
|
|
483
|
-
} else if (
|
|
484
|
-
computedEntries.push([key,
|
|
516
|
+
} else if (isString(def.body) && bodyReturnsValue(def.body)) {
|
|
517
|
+
computedEntries.push([key, def]);
|
|
485
518
|
} else {
|
|
486
|
-
functionEntries.push([key,
|
|
519
|
+
functionEntries.push([key, def]);
|
|
487
520
|
}
|
|
488
|
-
} else if (
|
|
521
|
+
} else if (isString(def) && def.includes("${")) {
|
|
489
522
|
/* A TEMPLATE STRING IS A COMPUTED, and the runtime has always said so — `runtime.ts`'s
|
|
490
523
|
second state pass is `if (typeof def === "string" && def.includes("${")) state[key] =
|
|
491
524
|
computed(() => evaluateTemplate(def, state))`, and `StateEntry`'s own schema description
|
|
@@ -501,7 +534,7 @@ export function emitElementModule(
|
|
|
501
534
|
templateEntries.push([key, def]);
|
|
502
535
|
} else {
|
|
503
536
|
// Use extractInitialValue to get the correct initial value
|
|
504
|
-
const initVal = extractInitialValue(
|
|
537
|
+
const initVal = extractInitialValue(def);
|
|
505
538
|
if (initVal !== undefined) {
|
|
506
539
|
stateEntries.push([key, initVal]);
|
|
507
540
|
}
|
|
@@ -523,7 +556,7 @@ export function emitElementModule(
|
|
|
523
556
|
for (const [key, def] of functionEntries) {
|
|
524
557
|
lines.push("");
|
|
525
558
|
if (isExpressionDef(def)) {
|
|
526
|
-
const compiled = compileExpression(def.$expression
|
|
559
|
+
const compiled = compileExpression(def.$expression, {
|
|
527
560
|
eventParam: "e",
|
|
528
561
|
statePrefix: "s",
|
|
529
562
|
});
|
|
@@ -547,7 +580,7 @@ export function emitElementModule(
|
|
|
547
580
|
} else {
|
|
548
581
|
const args = def.parameters ? paramNames(def.parameters) : (def.arguments ?? ["state"]);
|
|
549
582
|
const paramList = args.join(", ");
|
|
550
|
-
const body =
|
|
583
|
+
const body = isString(def.body) ? def.body : "";
|
|
551
584
|
// Every call site invokes a state function as `(state, event)` — the template's
|
|
552
585
|
// `s.fn(s, e)`, `onMount`, `onUnmount`. Declared names that already start with `state` line
|
|
553
586
|
// Up positionally, so they are emitted directly. Any other declaration — including the
|
|
@@ -591,7 +624,7 @@ export function emitElementModule(
|
|
|
591
624
|
// Emit named formulas — scope callables mapping positional args onto declared parameters
|
|
592
625
|
for (const [key, def] of formulaEntries) {
|
|
593
626
|
lines.push("");
|
|
594
|
-
const compiled = compileExpression(def.$expression
|
|
627
|
+
const compiled = compileExpression(def.$expression, {
|
|
595
628
|
eventParam: "e",
|
|
596
629
|
statePrefix: "this.state",
|
|
597
630
|
});
|
|
@@ -600,13 +633,14 @@ export function emitElementModule(
|
|
|
600
633
|
);
|
|
601
634
|
}
|
|
602
635
|
|
|
603
|
-
/* Emitted with the same `state.` → `this.state.` rewrite the inline-body path below uses
|
|
604
|
-
|
|
605
|
-
|
|
636
|
+
/* Emitted with the same `state.` → `this.state.` rewrite the inline-body path below uses. A
|
|
637
|
+
single expression keeps its own type and anything else is a template literal, exactly as the
|
|
638
|
+
runtime and the build-time scope evaluate it (spec.md §5.3), so a component reads the same
|
|
639
|
+
value hydrated as it did prerendered. */
|
|
606
640
|
for (const [key, template] of templateEntries) {
|
|
607
641
|
lines.push(
|
|
608
642
|
"",
|
|
609
|
-
` ${refAccessor("this.state", escapeToken(key))} = computed(() =>
|
|
643
|
+
` ${refAccessor("this.state", escapeToken(key))} = computed(() => ${templateValueSource(template.replaceAll("state.", "this.state."))});`,
|
|
610
644
|
);
|
|
611
645
|
}
|
|
612
646
|
|
|
@@ -614,7 +648,7 @@ export function emitElementModule(
|
|
|
614
648
|
for (const [key, def] of computedEntries) {
|
|
615
649
|
lines.push("");
|
|
616
650
|
if (isExpressionDef(def)) {
|
|
617
|
-
const compiled = compileExpression(def.$expression
|
|
651
|
+
const compiled = compileExpression(def.$expression, {
|
|
618
652
|
eventParam: "e",
|
|
619
653
|
statePrefix: "this.state",
|
|
620
654
|
});
|
|
@@ -627,10 +661,7 @@ export function emitElementModule(
|
|
|
627
661
|
);
|
|
628
662
|
} else {
|
|
629
663
|
lines.push(` ${refAccessor("this.state", escapeToken(key))} = computed(() => {`);
|
|
630
|
-
const body = (
|
|
631
|
-
"state.",
|
|
632
|
-
"this.state.",
|
|
633
|
-
);
|
|
664
|
+
const body = (isString(def.body) ? def.body : "").replaceAll("state.", "this.state.");
|
|
634
665
|
lines.push(` ${body}`, " });");
|
|
635
666
|
}
|
|
636
667
|
}
|
|
@@ -742,14 +773,14 @@ export function emitElementModule(
|
|
|
742
773
|
// Keep fetching after removal, and every re-insertion would stack another effect on top.
|
|
743
774
|
for (const [key, def] of requestEntries) {
|
|
744
775
|
lines.push(
|
|
745
|
-
emitRequestFetch(key, def
|
|
776
|
+
emitRequestFetch(key, def, {
|
|
746
777
|
collect: "this.#effects",
|
|
747
778
|
indent: " ",
|
|
748
779
|
statePrefix: "this.state",
|
|
749
780
|
}),
|
|
750
781
|
);
|
|
751
782
|
}
|
|
752
|
-
if (doc.style
|
|
783
|
+
if (isNonNullObject(doc.style)) {
|
|
753
784
|
const dynamicStyles: [string, string][] = [];
|
|
754
785
|
for (const [prop, value] of Object.entries(doc.style)) {
|
|
755
786
|
if (
|
|
@@ -761,12 +792,9 @@ export function emitElementModule(
|
|
|
761
792
|
) {
|
|
762
793
|
continue;
|
|
763
794
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const cssProp = camelToKebab(prop);
|
|
768
|
-
if (typeof value === "string" && value.includes("${")) {
|
|
769
|
-
dynamicStyles.push([cssProp, value]);
|
|
795
|
+
// Only a template declaration is dynamic; nested blocks and literal values are static CSS.
|
|
796
|
+
if (isString(value) && value.includes("${")) {
|
|
797
|
+
dynamicStyles.push([camelToKebab(prop), value]);
|
|
770
798
|
}
|
|
771
799
|
}
|
|
772
800
|
if (dynamicStyles.length > 0) {
|
|
@@ -781,19 +809,17 @@ export function emitElementModule(
|
|
|
781
809
|
lines.push(" }));");
|
|
782
810
|
}
|
|
783
811
|
}
|
|
784
|
-
const hasSlot = treeHasSlot(doc.children);
|
|
785
812
|
/*
|
|
786
|
-
* The slot emulation is a light-DOM construction:
|
|
787
|
-
* then
|
|
813
|
+
* The slot emulation is a light-DOM construction: capture the host's children, render over them,
|
|
814
|
+
* then replace each literal `<slot>` with its group (spec.md §8.5). Inside a shadow root that
|
|
788
815
|
* would be actively wrong — the light children ARE the slotted content and must stay in the
|
|
789
816
|
* light tree, where a real `<slot>` distributes them. This is the single largest behavioural
|
|
790
|
-
* difference between the two modes, and the reason shadow cannot be the default.
|
|
817
|
+
* difference between the two modes, and the reason shadow cannot be the default. Captured before
|
|
818
|
+
* `data-jx-prerendered` goes and before the clear: a prerendered instance's children are its
|
|
819
|
+
* markup, and capture recovers what the prerender placed from its brackets instead.
|
|
791
820
|
*/
|
|
792
|
-
if (hasSlot
|
|
793
|
-
|
|
794
|
-
lines.push(
|
|
795
|
-
" const _slotted = Array.from(this.childNodes).filter(n => n.nodeType === 1 || (n.nodeType === 3 && n.textContent.trim()));",
|
|
796
|
-
);
|
|
821
|
+
if (hasSlot) {
|
|
822
|
+
lines.push(` const _slotted = ${SLOT_CAPTURE_HELPER}(this);`);
|
|
797
823
|
}
|
|
798
824
|
lines.push(
|
|
799
825
|
" if (this.hasAttribute('data-jx-prerendered')) {",
|
|
@@ -834,15 +860,9 @@ export function emitElementModule(
|
|
|
834
860
|
" this.#effects.push(effect(() => render(this.template(), _root)));",
|
|
835
861
|
);
|
|
836
862
|
}
|
|
837
|
-
if (hasSlot
|
|
838
|
-
//
|
|
839
|
-
lines.push(
|
|
840
|
-
" const _slot = this.querySelector('slot');",
|
|
841
|
-
" if (_slot && _slotted.length > 0) {",
|
|
842
|
-
" for (const n of _slotted) _slot.before(n);",
|
|
843
|
-
" _slot.remove();",
|
|
844
|
-
" }",
|
|
845
|
-
);
|
|
863
|
+
if (hasSlot) {
|
|
864
|
+
// Once, after the first render: a slot a later render draws stays literal (spec.md §8.5).
|
|
865
|
+
lines.push(` ${SLOT_DISTRIBUTE_HELPER}(this, _slotted);`);
|
|
846
866
|
}
|
|
847
867
|
lines.push(
|
|
848
868
|
// Lifecycle: onMount (spec §16.4) — same microtask contract as the runtime
|
|
@@ -922,38 +942,50 @@ function emitLitChildren(
|
|
|
922
942
|
preformatted = false,
|
|
923
943
|
inMap = false,
|
|
924
944
|
scope: HoistScope | null = null,
|
|
945
|
+
/** The children are an instance's, so each child part ends at a guard rather than a sibling. */
|
|
946
|
+
guardParts = false,
|
|
925
947
|
) {
|
|
926
948
|
if (!children) {
|
|
927
949
|
return "";
|
|
928
950
|
}
|
|
929
951
|
|
|
952
|
+
const guard = guardParts ? LIT_PART_GUARD : "";
|
|
953
|
+
|
|
930
954
|
// Legacy whole-children repeater: the array IS the children slot.
|
|
931
955
|
if (isMappedArray(children)) {
|
|
932
|
-
return emitMappedArray(children, indent)
|
|
956
|
+
return `${emitMappedArray(children, indent)}${guard}`;
|
|
933
957
|
}
|
|
934
958
|
|
|
935
959
|
if (!Array.isArray(children)) {
|
|
936
960
|
return "";
|
|
937
961
|
}
|
|
938
962
|
|
|
939
|
-
|
|
963
|
+
/* Mixed children: elements/text plus array pseudo-elements expanded inline among siblings. Among
|
|
964
|
+
an instance's children, a child that emits a lit child part — a binding, a repeater, a chosen
|
|
965
|
+
tag — is followed by a guard: distribution moves its neighbours, and a part that ended at one
|
|
966
|
+
would clear its new parent on the next update (`LIT_PART_GUARD`). */
|
|
940
967
|
return children
|
|
941
|
-
.map((child
|
|
942
|
-
isMappedArray(child)
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
968
|
+
.map((child) => {
|
|
969
|
+
if (isMappedArray(child)) {
|
|
970
|
+
return `${emitMappedArray(child, indent)}${guard}`;
|
|
971
|
+
}
|
|
972
|
+
const text = emitLitNode(child, preformatted ? "" : indent, preformatted, inMap, scope);
|
|
973
|
+
const isPart =
|
|
974
|
+
(isString(child) && child.includes("${")) ||
|
|
975
|
+
(isNonNullObject(child) && isTagExpression(child.tagName));
|
|
976
|
+
return isPart ? `${text}${guard}` : text;
|
|
977
|
+
})
|
|
946
978
|
.join(preformatted ? "" : "\n");
|
|
947
979
|
}
|
|
948
980
|
|
|
949
981
|
/**
|
|
950
|
-
* @param {
|
|
982
|
+
* @param {LitChild} def
|
|
951
983
|
* @param {string} indent
|
|
952
984
|
* @param {boolean} [preformatted]
|
|
953
985
|
* @returns {string}
|
|
954
986
|
*/
|
|
955
987
|
function emitLitNode(
|
|
956
|
-
def:
|
|
988
|
+
def: LitChild,
|
|
957
989
|
indent: string,
|
|
958
990
|
preformatted = false,
|
|
959
991
|
inMap = false,
|
|
@@ -962,16 +994,16 @@ function emitLitNode(
|
|
|
962
994
|
innerOverride: string | null = null,
|
|
963
995
|
): string {
|
|
964
996
|
// String children are text nodes
|
|
965
|
-
if (
|
|
997
|
+
if (isString(def)) {
|
|
966
998
|
if (def.includes("${")) {
|
|
967
999
|
return `${indent}${toLitTextContent(def)}`;
|
|
968
1000
|
}
|
|
969
1001
|
return `${indent}${escapeHtml(def)}`;
|
|
970
1002
|
}
|
|
971
|
-
if (
|
|
1003
|
+
if (isNumber(def) || isBoolean(def)) {
|
|
972
1004
|
return `${indent}${escapeHtml(String(def))}`;
|
|
973
1005
|
}
|
|
974
|
-
if (!def
|
|
1006
|
+
if (!isNonNullObject(def)) {
|
|
975
1007
|
return "";
|
|
976
1008
|
}
|
|
977
1009
|
|
|
@@ -992,7 +1024,7 @@ function emitLitNode(
|
|
|
992
1024
|
eventParam: "e",
|
|
993
1025
|
statePrefix: "s",
|
|
994
1026
|
});
|
|
995
|
-
const branches =
|
|
1027
|
+
const branches: [string | boolean, string][] =
|
|
996
1028
|
expression.operator === "?:"
|
|
997
1029
|
? [
|
|
998
1030
|
[true, expression.value],
|
|
@@ -1002,17 +1034,24 @@ function emitLitNode(
|
|
|
1002
1034
|
// The inner content is identical across candidates — only the tag differs — so it is emitted
|
|
1003
1035
|
// Once and every branch references it. Skipped when the candidates disagree about being
|
|
1004
1036
|
// Preformatted, because `white-space` decides how the subtree itself is indented.
|
|
1005
|
-
const candidateTags = branches.map(([, candidate]) =>
|
|
1037
|
+
const candidateTags = branches.map(([, candidate]) => candidate);
|
|
1006
1038
|
const sharedPre = candidateTags.map((t) => preformatted || PREFORMATTED_TAGS.has(t));
|
|
1007
1039
|
let innerRef: string | null = null;
|
|
1008
1040
|
if (scope && sharedPre.every((p) => p === sharedPre[0])) {
|
|
1009
|
-
const sharedInner = emitNodeInner(
|
|
1041
|
+
const sharedInner = emitNodeInner(
|
|
1042
|
+
def,
|
|
1043
|
+
`${indent} `,
|
|
1044
|
+
sharedPre[0] === true,
|
|
1045
|
+
inMap,
|
|
1046
|
+
scope,
|
|
1047
|
+
candidateTags.some((t) => t.includes("-")),
|
|
1048
|
+
);
|
|
1010
1049
|
if (sharedInner.trim() !== "") {
|
|
1011
1050
|
innerRef = hoistSubtree(scope, sharedInner);
|
|
1012
1051
|
}
|
|
1013
1052
|
}
|
|
1014
|
-
const rendered: [string | boolean
|
|
1015
|
-
const asLiteral = { ...def, tagName: candidate
|
|
1053
|
+
const rendered: [string | boolean, string][] = branches.map(([key, candidate]) => {
|
|
1054
|
+
const asLiteral = { ...def, tagName: candidate };
|
|
1016
1055
|
return [key, emitLitNode(asLiteral, `${indent} `, preformatted, inMap, scope, innerRef)];
|
|
1017
1056
|
});
|
|
1018
1057
|
if (expression.operator === "?:") {
|
|
@@ -1026,9 +1065,9 @@ ${yes}
|
|
|
1026
1065
|
${no}
|
|
1027
1066
|
\`}`;
|
|
1028
1067
|
}
|
|
1029
|
-
const entries = rendered
|
|
1030
|
-
|
|
1031
|
-
|
|
1068
|
+
const entries = rendered.flatMap(([key, text]) =>
|
|
1069
|
+
key === "__default__" ? [] : [` ${JSON.stringify(key)}: html\`\n${text}\n \``],
|
|
1070
|
+
);
|
|
1032
1071
|
const [, fallback] = rendered.at(-1)!;
|
|
1033
1072
|
return (
|
|
1034
1073
|
`${indent}${"$"}{{\n${entries.join(",\n")}\n}` +
|
|
@@ -1049,17 +1088,17 @@ ${no}
|
|
|
1049
1088
|
§8.3). A LITERAL boolean is decided now; a BOUND one cannot be, because its resolved type
|
|
1050
1089
|
is not known until the effect runs — so it goes through the emitted helper, whose `null`
|
|
1051
1090
|
becomes lit's `nothing` and takes the attribute off. */
|
|
1052
|
-
if (
|
|
1091
|
+
if (isRef(val)) {
|
|
1053
1092
|
parts.push(`${key}="\${${ATTR_HELPER}('${key}', ${refToExpr(val.$ref)}) ?? nothing}"`);
|
|
1054
|
-
} else if (
|
|
1093
|
+
} else if (isString(val) && isWholeTemplate(val)) {
|
|
1055
1094
|
parts.push(
|
|
1056
1095
|
`${key}="\${${ATTR_HELPER}('${key}', ${toLitExpr(val).slice(2, -1)}) ?? nothing}"`,
|
|
1057
1096
|
);
|
|
1058
|
-
} else if (
|
|
1097
|
+
} else if (isString(val) && val.includes("${")) {
|
|
1059
1098
|
/* A template with literal text around it — `"page ${n}"` — is a string by construction, so
|
|
1060
1099
|
there is no boolean to decide and wrapping it would only add a call. */
|
|
1061
1100
|
parts.push(`${key}="${toLitExpr(val)}"`);
|
|
1062
|
-
} else if (
|
|
1101
|
+
} else if (isBoolean(val)) {
|
|
1063
1102
|
const text = booleanAttrValue(key, val);
|
|
1064
1103
|
if (text !== null) {
|
|
1065
1104
|
parts.push(text === "" ? key : `${key}="${text}"`);
|
|
@@ -1097,9 +1136,9 @@ ${no}
|
|
|
1097
1136
|
continue;
|
|
1098
1137
|
}
|
|
1099
1138
|
|
|
1100
|
-
if (
|
|
1101
|
-
parts.push(`.${key}="\${${refToExpr(
|
|
1102
|
-
} else if (
|
|
1139
|
+
if (isBoundRef(val)) {
|
|
1140
|
+
parts.push(`.${key}="\${${refToExpr(val.$ref)}}"`);
|
|
1141
|
+
} else if (isString(val) && val.includes("${")) {
|
|
1103
1142
|
parts.push(`.${key}="${toLitExpr(val)}"`);
|
|
1104
1143
|
} else {
|
|
1105
1144
|
// A literal IDL value (`value: "a"`, `checked: true`) is still a property the author asked
|
|
@@ -1112,9 +1151,9 @@ ${no}
|
|
|
1112
1151
|
|
|
1113
1152
|
if (def.$props) {
|
|
1114
1153
|
for (const [key, val] of Object.entries(def.$props)) {
|
|
1115
|
-
if (
|
|
1116
|
-
parts.push(`.${key}="\${${refToExpr(
|
|
1117
|
-
} else if (
|
|
1154
|
+
if (isBoundRef(val)) {
|
|
1155
|
+
parts.push(`.${key}="\${${refToExpr(val.$ref)}}"`);
|
|
1156
|
+
} else if (isString(val) && val.includes("${")) {
|
|
1118
1157
|
// A template value is a binding, not text. JSON-quoting it emitted the template's own source
|
|
1119
1158
|
// As the prop value (`.label="${"${state.x}"}"`), so the component received the literal
|
|
1120
1159
|
// String `${state.x}` forever.
|
|
@@ -1134,20 +1173,20 @@ ${no}
|
|
|
1134
1173
|
continue;
|
|
1135
1174
|
}
|
|
1136
1175
|
const eventName = key.slice(2).toLowerCase();
|
|
1137
|
-
if (
|
|
1176
|
+
if (isBoundRef(val)) {
|
|
1138
1177
|
parts.push(
|
|
1139
1178
|
mapCtx
|
|
1140
|
-
? `@${eventName}="\${(e) => { ${mapCtx}${refToExpr(
|
|
1141
|
-
: `@${eventName}="\${(e) => ${refToExpr(
|
|
1142
|
-
);
|
|
1143
|
-
} else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
|
|
1144
|
-
const compiled = compileExpression(
|
|
1145
|
-
(val as Record<string, unknown>).$expression as ExpressionNode,
|
|
1146
|
-
{
|
|
1147
|
-
eventParam: "e",
|
|
1148
|
-
statePrefix: "s",
|
|
1149
|
-
},
|
|
1179
|
+
? `@${eventName}="\${(e) => { ${mapCtx}${refToExpr(val.$ref)}(s, e); }}"`
|
|
1180
|
+
: `@${eventName}="\${(e) => ${refToExpr(val.$ref)}(s, e)}"`,
|
|
1150
1181
|
);
|
|
1182
|
+
} else if (isNonNullObject(val) && "$expression" in val) {
|
|
1183
|
+
/* SAFETY: an `on*` binding object carrying `$expression` is a declarative handler, and the
|
|
1184
|
+
document schema types that member as an expression node (spec.md §19). It is passed on
|
|
1185
|
+
unvalidated, exactly as authored, so a malformed node fails inside compileExpression. */
|
|
1186
|
+
const compiled = compileExpression(val.$expression as JxExpressionNode, {
|
|
1187
|
+
eventParam: "e",
|
|
1188
|
+
statePrefix: "s",
|
|
1189
|
+
});
|
|
1151
1190
|
parts.push(`@${eventName}="\${(e) => { ${mapCtx}${compiled}; }}"`);
|
|
1152
1191
|
} else if (isFunctionDef(val)) {
|
|
1153
1192
|
parts.push(`@${eventName}="\${(e) => { ${mapCtx}${inlineHandlerBody(val)} }}"`);
|
|
@@ -1166,12 +1205,18 @@ ${no}
|
|
|
1166
1205
|
return `${indent}<${tag}${attrsStr}\n${indent}>`;
|
|
1167
1206
|
}
|
|
1168
1207
|
|
|
1169
|
-
|
|
1208
|
+
// A custom element's children are distributed into its slots, so their parts need guards.
|
|
1209
|
+
const inner = innerOverride ?? emitNodeInner(def, indent, inPre, inMap, scope, tag.includes("-"));
|
|
1210
|
+
|
|
1211
|
+
/* A `<slot>` is replaced after the first render, so no part may end at it or at its last child:
|
|
1212
|
+
one guard before the slot (where an adjacent part ends) and one before `</slot>` (where a last
|
|
1213
|
+
fallback binding ends, and which travels with the fallback when it is shown). */
|
|
1214
|
+
const guard = tag === "slot" ? LIT_PART_GUARD : "";
|
|
1170
1215
|
|
|
1171
1216
|
// Inside a <pre>, the newline before `>` would land in the element's own text.
|
|
1172
1217
|
return inPre
|
|
1173
|
-
?
|
|
1174
|
-
: `${indent}<${tag}${attrsStr}\n${indent}>${inner}</${tag}>`;
|
|
1218
|
+
? `${guard}<${tag}${attrsStr}>${inner}${guard}</${tag}>`
|
|
1219
|
+
: `${indent}${guard}<${tag}${attrsStr}\n${indent}>${inner}${guard}</${tag}>`;
|
|
1175
1220
|
}
|
|
1176
1221
|
|
|
1177
1222
|
/**
|
|
@@ -1193,13 +1238,15 @@ function emitNodeInner(
|
|
|
1193
1238
|
inPre: boolean,
|
|
1194
1239
|
inMap: boolean,
|
|
1195
1240
|
scope: HoistScope | null,
|
|
1241
|
+
/** `def` is a custom element, whose children are distributed: see `emitLitChildren`. */
|
|
1242
|
+
guardParts = false,
|
|
1196
1243
|
): string {
|
|
1197
1244
|
if (def.$switch) {
|
|
1198
|
-
const switchRef = isRef(def.$switch) ? def.$switch.$ref :
|
|
1245
|
+
const switchRef = isRef(def.$switch) ? def.$switch.$ref : def.$switch;
|
|
1199
1246
|
const switchExpr = refToExpr(switchRef);
|
|
1200
|
-
const cases = (def.cases ?? {}) as Record<string, JxMutableNode>;
|
|
1201
1247
|
const rendered: [string, string][] = [];
|
|
1202
|
-
|
|
1248
|
+
// A case value is document data, so a `null` or empty one can reach here and is skipped.
|
|
1249
|
+
for (const [key, caseDef] of Object.entries<LitChild>(def.cases ?? {})) {
|
|
1203
1250
|
if (!caseDef || isRef(caseDef)) {
|
|
1204
1251
|
continue;
|
|
1205
1252
|
}
|
|
@@ -1207,10 +1254,9 @@ function emitNodeInner(
|
|
|
1207
1254
|
}
|
|
1208
1255
|
// Cases that emit byte-identical subtrees share one hoisted `const` rather than repeating it
|
|
1209
1256
|
// Once per case — the authored document names the subtree once (#126).
|
|
1257
|
+
const texts = rendered.map(([, text]) => text);
|
|
1210
1258
|
const repeated = new Set(
|
|
1211
|
-
|
|
1212
|
-
.map(([, text]) => text)
|
|
1213
|
-
.filter((text, i, all) => all.indexOf(text) !== i && text.trim() !== ""),
|
|
1259
|
+
texts.filter((text, i) => texts.indexOf(text) !== i && text.trim() !== ""),
|
|
1214
1260
|
);
|
|
1215
1261
|
const caseEntries = rendered.map(([key, text]) => {
|
|
1216
1262
|
const body = scope && repeated.has(text) ? hoistSubtree(scope, text) : text;
|
|
@@ -1219,15 +1265,18 @@ function emitNodeInner(
|
|
|
1219
1265
|
return `\${{\n${caseEntries.join(",\n")}\n}[${switchExpr}]}`;
|
|
1220
1266
|
}
|
|
1221
1267
|
if (def.textContent !== undefined) {
|
|
1222
|
-
|
|
1268
|
+
const text = toLitTextContent(def.textContent);
|
|
1269
|
+
const bound =
|
|
1270
|
+
isRef(def.textContent) || (isString(def.textContent) && def.textContent.includes("${"));
|
|
1271
|
+
return guardParts && bound ? `${text}${LIT_PART_GUARD}` : text;
|
|
1223
1272
|
}
|
|
1224
1273
|
if (def.innerHTML !== undefined) {
|
|
1225
1274
|
return def.innerHTML;
|
|
1226
1275
|
}
|
|
1227
1276
|
if (def.children) {
|
|
1228
1277
|
return inPre
|
|
1229
|
-
? emitLitChildren(def.children, def.style, "", true, inMap, scope)
|
|
1230
|
-
: `\n${emitLitChildren(def.children, def.style, `${indent} `, false, inMap, scope)}\n${indent}`;
|
|
1278
|
+
? emitLitChildren(def.children, def.style, "", true, inMap, scope, guardParts)
|
|
1279
|
+
: `\n${emitLitChildren(def.children, def.style, `${indent} `, false, inMap, scope, guardParts)}\n${indent}`;
|
|
1231
1280
|
}
|
|
1232
1281
|
return "";
|
|
1233
1282
|
}
|
|
@@ -1252,9 +1301,9 @@ function emitMappedArray(arrayDef: JxMappedArray, indent: string) {
|
|
|
1252
1301
|
// `${item…}`/`${index…}` templates resolve against the map callback's own parameters.
|
|
1253
1302
|
if (mapDef.attributes) {
|
|
1254
1303
|
for (const [key, val] of Object.entries(mapDef.attributes)) {
|
|
1255
|
-
if (
|
|
1304
|
+
if (isRef(val)) {
|
|
1256
1305
|
parts.push(`${key}="\${${mapRefToExpr(val.$ref)}}"`);
|
|
1257
|
-
} else if (
|
|
1306
|
+
} else if (isString(val) && val.includes("${")) {
|
|
1258
1307
|
parts.push(`${key}="${toLitExpr(val)}"`);
|
|
1259
1308
|
} else {
|
|
1260
1309
|
parts.push(`${key}="${val}"`);
|
|
@@ -1290,9 +1339,9 @@ function emitMappedArray(arrayDef: JxMappedArray, indent: string) {
|
|
|
1290
1339
|
continue;
|
|
1291
1340
|
}
|
|
1292
1341
|
|
|
1293
|
-
if (
|
|
1294
|
-
parts.push(`.${key}="\${${mapRefToExpr(
|
|
1295
|
-
} else if (
|
|
1342
|
+
if (isBoundRef(val)) {
|
|
1343
|
+
parts.push(`.${key}="\${${mapRefToExpr(val.$ref)}}"`);
|
|
1344
|
+
} else if (isString(val) && val.includes("${")) {
|
|
1296
1345
|
parts.push(`.${key}="${toLitExpr(val)}"`);
|
|
1297
1346
|
} else {
|
|
1298
1347
|
parts.push(`.${key}="\${${JSON.stringify(val)}}"`);
|
|
@@ -1303,7 +1352,7 @@ function emitMappedArray(arrayDef: JxMappedArray, indent: string) {
|
|
|
1303
1352
|
for (const [key, val] of Object.entries(mapDef.$props)) {
|
|
1304
1353
|
if (isRef(val)) {
|
|
1305
1354
|
parts.push(`.${key}="\${${mapRefToExpr(val.$ref)}}"`);
|
|
1306
|
-
} else if (
|
|
1355
|
+
} else if (isString(val) && val.includes("${")) {
|
|
1307
1356
|
// `$props: { label: "${$map.item.name}" }` is the ordinary way to pass per-item data to a
|
|
1308
1357
|
// Component in a list; JSON-quoting it handed over the template source instead.
|
|
1309
1358
|
parts.push(`.${key}="${toLitExpr(val)}"`);
|
|
@@ -1326,7 +1375,7 @@ function emitMappedArray(arrayDef: JxMappedArray, indent: string) {
|
|
|
1326
1375
|
if (isRef(val)) {
|
|
1327
1376
|
parts.push(`@${eventName}="\${(e) => { s.$map = $map; ${refToExpr(val.$ref)}(s, e); }}"`);
|
|
1328
1377
|
} else if (isExpressionDef(val)) {
|
|
1329
|
-
const compiled = compileExpression(val.$expression
|
|
1378
|
+
const compiled = compileExpression(val.$expression, {
|
|
1330
1379
|
eventParam: "e",
|
|
1331
1380
|
statePrefix: "s",
|
|
1332
1381
|
});
|
|
@@ -1366,6 +1415,17 @@ function emitMappedArray(arrayDef: JxMappedArray, indent: string) {
|
|
|
1366
1415
|
return `${indent}\${${itemsExpr}.map(${callback})}`;
|
|
1367
1416
|
}
|
|
1368
1417
|
|
|
1418
|
+
/**
|
|
1419
|
+
* Whether a document value binds through a non-empty `$ref` pointer. An empty pointer names no
|
|
1420
|
+
* state, so the emitters treat that object as a literal value rather than a binding.
|
|
1421
|
+
*
|
|
1422
|
+
* @param {JxDocumentValue | undefined} value
|
|
1423
|
+
* @returns {boolean}
|
|
1424
|
+
*/
|
|
1425
|
+
function isBoundRef(value: JxDocumentValue | undefined): value is JxRef {
|
|
1426
|
+
return isRef(value) && value.$ref !== "";
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1369
1429
|
/**
|
|
1370
1430
|
* Convert a $ref string to a JS expression using `s` (this.state alias).
|
|
1371
1431
|
*
|
|
@@ -1445,19 +1505,15 @@ function isWholeTemplate(str: string): boolean {
|
|
|
1445
1505
|
* Convert textContent value to lit-html text content. Bug fix: handles $ref objects, which
|
|
1446
1506
|
* previously produced [object Object].
|
|
1447
1507
|
*
|
|
1448
|
-
* @param {
|
|
1508
|
+
* @param {string | null | JxRef} value
|
|
1449
1509
|
* @returns {string}
|
|
1450
1510
|
*/
|
|
1451
|
-
function toLitTextContent(value:
|
|
1511
|
+
function toLitTextContent(value: string | null | JxRef) {
|
|
1452
1512
|
// Handle $ref objects → emit as lit expression
|
|
1453
|
-
if (
|
|
1454
|
-
value
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
) {
|
|
1458
|
-
return `\${${refToExpr((value as JxMutableNode).$ref as string)}}`;
|
|
1459
|
-
}
|
|
1460
|
-
if (typeof value === "string" && value.includes("${")) {
|
|
1513
|
+
if (isRef(value)) {
|
|
1514
|
+
return `\${${refToExpr(value.$ref)}}`;
|
|
1515
|
+
}
|
|
1516
|
+
if (isString(value) && value.includes("${")) {
|
|
1461
1517
|
return toLitExpr(value);
|
|
1462
1518
|
}
|
|
1463
1519
|
return String(value);
|
|
@@ -1475,7 +1531,7 @@ function inlineHandlerBody(def: JxFunctionDef) {
|
|
|
1475
1531
|
statePrefix: "s",
|
|
1476
1532
|
});
|
|
1477
1533
|
}
|
|
1478
|
-
const body =
|
|
1534
|
+
const body = isString(def.body) ? def.body : "";
|
|
1479
1535
|
const declared = def.parameters ? paramNames(def.parameters) : (def.arguments ?? []);
|
|
1480
1536
|
if (declared.length > 0) {
|
|
1481
1537
|
// The call site supplies only `(e)`, so map the declared names onto the arguments the handler
|
|
@@ -1493,7 +1549,7 @@ function inlineHandlerBody(def: JxFunctionDef) {
|
|
|
1493
1549
|
* @returns {string}
|
|
1494
1550
|
*/
|
|
1495
1551
|
function emitStyleString(styleDef: JxStyle | null | undefined) {
|
|
1496
|
-
if (!styleDef
|
|
1552
|
+
if (!isNonNullObject(styleDef)) {
|
|
1497
1553
|
return "";
|
|
1498
1554
|
}
|
|
1499
1555
|
|
|
@@ -1509,11 +1565,8 @@ function emitStyleString(styleDef: JxStyle | null | undefined) {
|
|
|
1509
1565
|
continue;
|
|
1510
1566
|
}
|
|
1511
1567
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
if (typeof value === "string" && value.includes("${")) {
|
|
1568
|
+
// Only a template declaration is dynamic; nested blocks and literal values are static CSS.
|
|
1569
|
+
if (isString(value) && value.includes("${")) {
|
|
1517
1570
|
const cssProp = camelToKebab(prop);
|
|
1518
1571
|
parts.push(`${cssProp}: ${toLitExpr(value)}`);
|
|
1519
1572
|
}
|
|
@@ -1521,27 +1574,3 @@ function emitStyleString(styleDef: JxStyle | null | undefined) {
|
|
|
1521
1574
|
|
|
1522
1575
|
return parts.join("; ");
|
|
1523
1576
|
}
|
|
1524
|
-
|
|
1525
|
-
/**
|
|
1526
|
-
* Check if a children tree contains a `<slot>` element.
|
|
1527
|
-
*
|
|
1528
|
-
* @param {JxMutableNode["children"]} children
|
|
1529
|
-
* @returns {boolean}
|
|
1530
|
-
*/
|
|
1531
|
-
function treeHasSlot(children: JxMutableNode["children"]): boolean {
|
|
1532
|
-
if (!Array.isArray(children)) {
|
|
1533
|
-
return false;
|
|
1534
|
-
}
|
|
1535
|
-
for (const child of children) {
|
|
1536
|
-
if (!child || typeof child !== "object") {
|
|
1537
|
-
continue;
|
|
1538
|
-
}
|
|
1539
|
-
if (child.tagName === "slot") {
|
|
1540
|
-
return true;
|
|
1541
|
-
}
|
|
1542
|
-
if (treeHasSlot(child.children)) {
|
|
1543
|
-
return true;
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
return false;
|
|
1547
|
-
}
|