@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
package/src/shared.ts
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
import {
|
|
10
10
|
COLOR_SCHEME_ATTR,
|
|
11
11
|
COLOR_SCHEME_STORAGE_KEY,
|
|
12
|
+
MAX_COMPONENT_NESTING,
|
|
12
13
|
RESERVED_KEYS,
|
|
14
|
+
SLOT_ATTR,
|
|
15
|
+
SLOT_MARK,
|
|
16
|
+
SLOT_RECORD_KEY,
|
|
17
|
+
SLOT_WHITESPACE,
|
|
13
18
|
booleanAttrValue,
|
|
14
19
|
buildStyleRules,
|
|
15
20
|
enumeratedAttrNames,
|
|
@@ -26,30 +31,39 @@ import {
|
|
|
26
31
|
childrenContainArray,
|
|
27
32
|
hasStructuredBody,
|
|
28
33
|
isExpressionDef,
|
|
34
|
+
isBoolean,
|
|
35
|
+
isFunction,
|
|
29
36
|
isFunctionDef,
|
|
30
37
|
isMappedArray,
|
|
31
38
|
isNamedFormulaDef,
|
|
39
|
+
isNodeObject,
|
|
40
|
+
isNonNullObject,
|
|
41
|
+
isNumber,
|
|
32
42
|
isPrototypeDef,
|
|
33
43
|
isRef,
|
|
34
|
-
isSchemaOnlyDef
|
|
44
|
+
isSchemaOnlyDef,
|
|
35
45
|
isServerFnDef,
|
|
46
|
+
isString,
|
|
36
47
|
isTagExpression,
|
|
37
48
|
isTemplateString,
|
|
38
49
|
paramNames,
|
|
39
50
|
tagNameCandidates,
|
|
40
51
|
} from "@jxsuite/schema/guards";
|
|
52
|
+
import type { TagNameSlot } from "@jxsuite/schema/guards";
|
|
41
53
|
import { resolveShadowMode, styleScopePrefix } from "./shadow.ts";
|
|
42
54
|
import type { ShadowMode } from "./shadow.ts";
|
|
43
|
-
import type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
44
55
|
import { readPath } from "@jxsuite/runtime/pointer";
|
|
56
|
+
import type { JxScope } from "@jxsuite/runtime/types";
|
|
45
57
|
import type {
|
|
58
|
+
CemParameter,
|
|
46
59
|
JsonValue,
|
|
60
|
+
JxDocumentValue,
|
|
47
61
|
JxElement,
|
|
62
|
+
JxMappedArray,
|
|
48
63
|
JxMutableNode,
|
|
49
64
|
JxPrototypeDef,
|
|
50
65
|
JxRef,
|
|
51
66
|
JxStateDefinition,
|
|
52
|
-
JxStateObject,
|
|
53
67
|
JxStyle,
|
|
54
68
|
ProjectConfig,
|
|
55
69
|
} from "@jxsuite/schema/types";
|
|
@@ -80,22 +94,51 @@ export { compileStatements, runStatements } from "@jxsuite/runtime/statements";
|
|
|
80
94
|
* declared parameter names (honoring CemParameter defaults) as the `_args` object the compiled
|
|
81
95
|
* body's `$args/` refs read from. Call sites stay positional, matching the interpreter.
|
|
82
96
|
*/
|
|
83
|
-
export function emitFormulaFn(
|
|
97
|
+
export function emitFormulaFn(
|
|
98
|
+
def: { parameters?: readonly FormulaParameter[] | undefined },
|
|
99
|
+
compiledBody: string,
|
|
100
|
+
): string {
|
|
84
101
|
const entries: string[] = [];
|
|
85
102
|
for (const [i, p] of (def.parameters ?? []).entries()) {
|
|
86
|
-
const name =
|
|
103
|
+
const name = formulaParameterName(p);
|
|
87
104
|
if (!name) {
|
|
88
105
|
continue;
|
|
89
106
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
? `_a[${i}] === undefined ? ${JSON.stringify((p as { default?: unknown }).default)} : _a[${i}]`
|
|
107
|
+
const arg = hasParameterDefault(p)
|
|
108
|
+
? `_a[${i}] === undefined ? ${JSON.stringify(p.default)} : _a[${i}]`
|
|
93
109
|
: `_a[${i}]`;
|
|
94
110
|
entries.push(`${JSON.stringify(name)}: ${arg}`);
|
|
95
111
|
}
|
|
96
112
|
return `(..._a) => { const _args = { ${entries.join(", ")} }; return ${compiledBody}; }`;
|
|
97
113
|
}
|
|
98
114
|
|
|
115
|
+
/**
|
|
116
|
+
* One declared formula parameter as a document holds it: a bare name or a CEM parameter object. A
|
|
117
|
+
* hand-edited list may also carry a `null` hole, which both the emitted and the build-time callable
|
|
118
|
+
* skip like an unnamed entry.
|
|
119
|
+
*/
|
|
120
|
+
type FormulaParameter = string | CemParameter | null;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The name a formula parameter binds under `$args/`, or "" for an entry that names nothing.
|
|
124
|
+
*
|
|
125
|
+
* @param {FormulaParameter} p
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function formulaParameterName(p: FormulaParameter): string {
|
|
129
|
+
return isString(p) ? p : (p?.name ?? "");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether a formula parameter declares a `default`, which fills an argument the call site omits.
|
|
134
|
+
*
|
|
135
|
+
* @param {FormulaParameter} p
|
|
136
|
+
* @returns {boolean}
|
|
137
|
+
*/
|
|
138
|
+
function hasParameterDefault(p: FormulaParameter): p is CemParameter {
|
|
139
|
+
return isNonNullObject(p) && "default" in p;
|
|
140
|
+
}
|
|
141
|
+
|
|
99
142
|
/**
|
|
100
143
|
* Emit the import-clause binding for one Function-def `$src` entry.
|
|
101
144
|
*
|
|
@@ -109,10 +152,10 @@ export function emitFormulaFn(def: { parameters?: unknown[] }, compiledBody: str
|
|
|
109
152
|
* only reachable as `default as key`.
|
|
110
153
|
*
|
|
111
154
|
* @param {string} key - State key, and the local binding name
|
|
112
|
-
* @param {
|
|
155
|
+
* @param {SrcExportOwner} def - The Function definition
|
|
113
156
|
* @returns {string}
|
|
114
157
|
*/
|
|
115
|
-
export function srcImportBinding(key: string, def:
|
|
158
|
+
export function srcImportBinding(key: string, def: SrcExportOwner) {
|
|
116
159
|
const exportName = srcExportName(key, def);
|
|
117
160
|
return exportName === key ? key : `${exportName} as ${key}`;
|
|
118
161
|
}
|
|
@@ -125,14 +168,23 @@ export function srcImportBinding(key: string, def: unknown) {
|
|
|
125
168
|
* to, where the local name is chosen by the generated code and only the export name is fixed.
|
|
126
169
|
*
|
|
127
170
|
* @param {string} key - State key
|
|
128
|
-
* @param {
|
|
171
|
+
* @param {SrcExportOwner} def - The Function definition
|
|
129
172
|
* @returns {string}
|
|
130
173
|
*/
|
|
131
|
-
export function srcExportName(key: string, def:
|
|
132
|
-
const declared =
|
|
133
|
-
return
|
|
174
|
+
export function srcExportName(key: string, def: SrcExportOwner): string {
|
|
175
|
+
const declared = def?.$export;
|
|
176
|
+
return isString(declared) && declared !== "" ? declared : key;
|
|
134
177
|
}
|
|
135
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The Function definition a `$src` export is named on, as the document holds it. `$export` is read
|
|
181
|
+
* unchecked from document data, so anything but a non-empty string falls back to the state key.
|
|
182
|
+
*/
|
|
183
|
+
type SrcExportOwner =
|
|
184
|
+
| { $export?: JxDocumentValue | undefined; [key: string]: JxDocumentValue | undefined }
|
|
185
|
+
| null
|
|
186
|
+
| undefined;
|
|
187
|
+
|
|
136
188
|
/**
|
|
137
189
|
* Emit the auto-fetch `effect()` for a `$prototype: "Request"` state entry.
|
|
138
190
|
*
|
|
@@ -173,7 +225,7 @@ export function emitRequestFetch(
|
|
|
173
225
|
|
|
174
226
|
if (isTemplateUrl) {
|
|
175
227
|
lines.push(
|
|
176
|
-
` const url = \`${withStatePrefix(url
|
|
228
|
+
` const url = \`${withStatePrefix(url, statePrefix)}\`;`,
|
|
177
229
|
' if (!url || url === "undefined" || url.includes("undefined")) return;',
|
|
178
230
|
);
|
|
179
231
|
} else {
|
|
@@ -188,10 +240,9 @@ export function emitRequestFetch(
|
|
|
188
240
|
fetchOpts.push(`headers: ${JSON.stringify(def.headers)}`);
|
|
189
241
|
}
|
|
190
242
|
if (def.body) {
|
|
191
|
-
const bodyStr =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
: JSON.stringify(def.body);
|
|
243
|
+
const bodyStr = isNonNullObject(def.body)
|
|
244
|
+
? JSON.stringify(JSON.stringify(def.body))
|
|
245
|
+
: JSON.stringify(def.body);
|
|
195
246
|
fetchOpts.push(`body: ${bodyStr}`);
|
|
196
247
|
}
|
|
197
248
|
|
|
@@ -229,7 +280,6 @@ function withStatePrefix(str: string, statePrefix: string) {
|
|
|
229
280
|
`\${${expr.replaceAll(/(?<!this\.)\bstate\./g, `${statePrefix}.`)}}`,
|
|
230
281
|
);
|
|
231
282
|
}
|
|
232
|
-
export type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
233
283
|
|
|
234
284
|
/**
|
|
235
285
|
* Non-enumerable marker on a build-time scope listing the state keys that hold no build-time value.
|
|
@@ -267,7 +317,7 @@ export function isClassJsonSrc(src?: unknown): src is string {
|
|
|
267
317
|
* @returns {boolean}
|
|
268
318
|
*/
|
|
269
319
|
export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
270
|
-
if (!def
|
|
320
|
+
if (!isNonNullObject(def)) {
|
|
271
321
|
return false;
|
|
272
322
|
}
|
|
273
323
|
|
|
@@ -277,25 +327,20 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
277
327
|
if (k === "$site" || k === "$page") {
|
|
278
328
|
continue;
|
|
279
329
|
}
|
|
330
|
+
if (!isNonNullObject(d) || Array.isArray(d)) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
280
333
|
// Skip timing: "compiler" entries — resolved at build time, baked into static HTML
|
|
281
|
-
if (
|
|
282
|
-
d &&
|
|
283
|
-
typeof d === "object" &&
|
|
284
|
-
!Array.isArray(d) &&
|
|
285
|
-
(d as JxPrototypeDef).timing === "compiler"
|
|
286
|
-
) {
|
|
334
|
+
if (d.timing === "compiler") {
|
|
287
335
|
continue;
|
|
288
336
|
}
|
|
289
|
-
if (
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
if ((d as JxPrototypeDef).$prototype) {
|
|
337
|
+
if (d.$prototype) {
|
|
293
338
|
return true;
|
|
294
339
|
}
|
|
295
|
-
if ("default" in
|
|
340
|
+
if ("default" in d) {
|
|
296
341
|
return true;
|
|
297
342
|
}
|
|
298
|
-
if (
|
|
343
|
+
if (isSchemaOnlyDef(d)) {
|
|
299
344
|
continue;
|
|
300
345
|
}
|
|
301
346
|
return true;
|
|
@@ -317,11 +362,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
317
362
|
if (RESERVED_KEYS.has(key)) {
|
|
318
363
|
continue;
|
|
319
364
|
}
|
|
320
|
-
if (
|
|
321
|
-
val !== null &&
|
|
322
|
-
typeof val === "object" &&
|
|
323
|
-
typeof (val as JxMutableNode).$ref === "string"
|
|
324
|
-
) {
|
|
365
|
+
if (isRef(val)) {
|
|
325
366
|
return true;
|
|
326
367
|
}
|
|
327
368
|
if (isTemplateString(val)) {
|
|
@@ -329,7 +370,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
329
370
|
}
|
|
330
371
|
}
|
|
331
372
|
|
|
332
|
-
if (def.style
|
|
373
|
+
if (isNonNullObject(def.style)) {
|
|
333
374
|
for (const val of Object.values(def.style)) {
|
|
334
375
|
if (isTemplateString(val)) {
|
|
335
376
|
return true;
|
|
@@ -337,7 +378,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
337
378
|
}
|
|
338
379
|
}
|
|
339
380
|
|
|
340
|
-
if (def.attributes
|
|
381
|
+
if (isNonNullObject(def.attributes)) {
|
|
341
382
|
for (const val of Object.values(def.attributes)) {
|
|
342
383
|
if (isTemplateString(val)) {
|
|
343
384
|
return true;
|
|
@@ -355,7 +396,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
355
396
|
* @returns {boolean}
|
|
356
397
|
*/
|
|
357
398
|
export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
358
|
-
if (!def
|
|
399
|
+
if (!isNonNullObject(def)) {
|
|
359
400
|
return false;
|
|
360
401
|
}
|
|
361
402
|
|
|
@@ -370,11 +411,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
370
411
|
if (RESERVED_KEYS.has(key)) {
|
|
371
412
|
continue;
|
|
372
413
|
}
|
|
373
|
-
if (
|
|
374
|
-
val !== null &&
|
|
375
|
-
typeof val === "object" &&
|
|
376
|
-
typeof (val as JxMutableNode).$ref === "string"
|
|
377
|
-
) {
|
|
414
|
+
if (isRef(val)) {
|
|
378
415
|
return true;
|
|
379
416
|
}
|
|
380
417
|
if (isTemplateString(val)) {
|
|
@@ -382,7 +419,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
382
419
|
}
|
|
383
420
|
}
|
|
384
421
|
|
|
385
|
-
if (def.style
|
|
422
|
+
if (isNonNullObject(def.style)) {
|
|
386
423
|
for (const val of Object.values(def.style)) {
|
|
387
424
|
if (isTemplateString(val)) {
|
|
388
425
|
return true;
|
|
@@ -390,7 +427,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
390
427
|
}
|
|
391
428
|
}
|
|
392
429
|
|
|
393
|
-
if (def.attributes
|
|
430
|
+
if (isNonNullObject(def.attributes)) {
|
|
394
431
|
for (const val of Object.values(def.attributes)) {
|
|
395
432
|
if (isTemplateString(val)) {
|
|
396
433
|
return true;
|
|
@@ -408,7 +445,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
408
445
|
* @returns {boolean}
|
|
409
446
|
*/
|
|
410
447
|
export function hasAnyIsland(def: JxElement | JxMutableNode | string): boolean {
|
|
411
|
-
if (!def
|
|
448
|
+
if (!isNonNullObject(def)) {
|
|
412
449
|
return false;
|
|
413
450
|
}
|
|
414
451
|
if (isDynamic(def)) {
|
|
@@ -422,33 +459,60 @@ export function hasAnyIsland(def: JxElement | JxMutableNode | string): boolean {
|
|
|
422
459
|
|
|
423
460
|
// ─── Scope / value resolution ─────────────────────────────────────────────────
|
|
424
461
|
|
|
425
|
-
/**
|
|
426
|
-
* @param {JxElement | JxMutableNode | null} raw
|
|
427
|
-
* @param {Record<string, unknown> | null} [parentScope]
|
|
428
|
-
* @param {Record<string, unknown>} [scopeDefs]
|
|
429
|
-
* @param {Record<string, string>} [media]
|
|
430
|
-
* @returns {CompileContext}
|
|
431
|
-
*/
|
|
462
|
+
/** What a compile target threads down the tree: the scope in effect, its definitions and `$media`. */
|
|
432
463
|
export interface CompileContext {
|
|
433
|
-
scope:
|
|
434
|
-
|
|
464
|
+
scope: JxScope;
|
|
465
|
+
/** The `state` definitions the scope was built from. */
|
|
466
|
+
scopeDefs: Record<string, JxStateDefinition>;
|
|
435
467
|
media: Record<string, string>;
|
|
436
468
|
/** True inside a `<pre>`-like subtree, where emitters must not indent nested children. */
|
|
437
469
|
preformatted?: boolean;
|
|
438
470
|
}
|
|
439
471
|
|
|
472
|
+
/**
|
|
473
|
+
* @param {JxElement | JxMutableNode | null} raw
|
|
474
|
+
* @param {JxScope | null} [parentScope]
|
|
475
|
+
* @param {Record<string, JxStateDefinition>} [scopeDefs]
|
|
476
|
+
* @param {Record<string, string>} [media]
|
|
477
|
+
* @returns {CompileContext}
|
|
478
|
+
*/
|
|
440
479
|
export function createCompileContext(
|
|
441
480
|
raw: JxElement | JxMutableNode | null,
|
|
442
|
-
parentScope:
|
|
443
|
-
scopeDefs: Record<string,
|
|
481
|
+
parentScope: JxScope | null = null,
|
|
482
|
+
scopeDefs: Record<string, JxStateDefinition> = {},
|
|
444
483
|
media: Record<string, string> = {},
|
|
445
484
|
): CompileContext {
|
|
446
|
-
const scope
|
|
485
|
+
const scope = raw?.state
|
|
447
486
|
? buildInitialScope(raw.state, parentScope)
|
|
448
|
-
: (parentScope ?? (
|
|
487
|
+
: (parentScope ?? emptyScope());
|
|
449
488
|
return { media, scope, scopeDefs };
|
|
450
489
|
}
|
|
451
490
|
|
|
491
|
+
/**
|
|
492
|
+
* A fresh scope inheriting from `parent`, or from nothing at all, so no `Object.prototype` member
|
|
493
|
+
* can answer for an undeclared state key.
|
|
494
|
+
*
|
|
495
|
+
* @param {JxScope | null} [parent]
|
|
496
|
+
* @returns {JxScope}
|
|
497
|
+
*/
|
|
498
|
+
function emptyScope(parent: JxScope | null = null): JxScope {
|
|
499
|
+
/* SAFETY: `Object.create` returns a new object with no own keys whose prototype is `parent` (a
|
|
500
|
+
scope) or null, so every key it answers is a scope member or absent — exactly a JxScope. */
|
|
501
|
+
return Object.create(parent) as JxScope;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The runtime-only marks `buildInitialScope` attached to `scope` or to a scope it inherits from.
|
|
506
|
+
*
|
|
507
|
+
* @param {JxScope} scope
|
|
508
|
+
* @returns {Set<string> | undefined}
|
|
509
|
+
*/
|
|
510
|
+
function runtimeOnlyKeysOf(scope: JxScope): Set<string> | undefined {
|
|
511
|
+
/* SAFETY: `RUNTIME_ONLY_KEYS` is module-private and `buildInitialScope` is the only writer, which
|
|
512
|
+
always stores a `Set<string>` under it; a scope built anywhere else answers undefined. */
|
|
513
|
+
return (scope as { [RUNTIME_ONLY_KEYS]?: Set<string> })[RUNTIME_ONLY_KEYS];
|
|
514
|
+
}
|
|
515
|
+
|
|
452
516
|
/**
|
|
453
517
|
* `state.x = …`, `state.x += …`, `state.x++` — every form a string handler body can use to write a
|
|
454
518
|
* state entry. `===`/`!==`/`<=`/`>=` are excluded by the `(?!=)` guard and by the operator set.
|
|
@@ -469,36 +533,43 @@ const STATE_MUTATING_METHOD_RE =
|
|
|
469
533
|
* distinguishes it from a constant — but if a handler assigns to it, interpolating it at build time
|
|
470
534
|
* replaces the template in the emitted output and the element is dead for the life of the page.
|
|
471
535
|
*
|
|
472
|
-
* @param {
|
|
536
|
+
* @param {JxDocumentValue | undefined} node
|
|
473
537
|
* @param {Set<string>} into
|
|
474
538
|
*/
|
|
475
|
-
function collectAssignedStateKeys(node:
|
|
476
|
-
if (
|
|
539
|
+
function collectAssignedStateKeys(node: JxDocumentValue | undefined, into: Set<string>) {
|
|
540
|
+
if (isString(node)) {
|
|
477
541
|
for (const m of node.matchAll(STATE_ASSIGNMENT_RE)) {
|
|
542
|
+
// SAFETY: the pattern's one capture group is outside any optional part, so a match has it.
|
|
478
543
|
into.add(m[1] as string);
|
|
479
544
|
}
|
|
480
545
|
for (const m of node.matchAll(STATE_MUTATING_METHOD_RE)) {
|
|
546
|
+
// SAFETY: the pattern's one capture group is outside any optional part, so a match has it.
|
|
481
547
|
into.add(m[1] as string);
|
|
482
548
|
}
|
|
483
549
|
return;
|
|
484
550
|
}
|
|
485
|
-
if (!node || typeof node !== "object") {
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
551
|
if (Array.isArray(node)) {
|
|
489
552
|
for (const entry of node) {
|
|
490
553
|
collectAssignedStateKeys(entry, into);
|
|
491
554
|
}
|
|
492
555
|
return;
|
|
493
556
|
}
|
|
494
|
-
|
|
557
|
+
if (!isNonNullObject(node)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
/* SAFETY: `node` is a non-array object inside a handler body or an expression, so it is a
|
|
561
|
+
document object, and every member of a document object is itself a document value. */
|
|
562
|
+
const rec = node as Record<string, JxDocumentValue | undefined>;
|
|
495
563
|
// A mutating expression names its destination as `target: { $ref: "#/state/x" }`.
|
|
496
|
-
const
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
564
|
+
const { operator, target } = rec;
|
|
565
|
+
if (
|
|
566
|
+
isString(operator) &&
|
|
567
|
+
isMutating(operator) &&
|
|
568
|
+
isRef(target) &&
|
|
569
|
+
target.$ref.startsWith("#/state/")
|
|
570
|
+
) {
|
|
571
|
+
// SAFETY: `String.prototype.split` always returns at least one element.
|
|
572
|
+
into.add(target.$ref.slice("#/state/".length).split("/")[0] as string);
|
|
502
573
|
}
|
|
503
574
|
for (const value of Object.values(rec)) {
|
|
504
575
|
collectAssignedStateKeys(value, into);
|
|
@@ -507,18 +578,18 @@ function collectAssignedStateKeys(node: unknown, into: Set<string>) {
|
|
|
507
578
|
|
|
508
579
|
/**
|
|
509
580
|
* @param {Record<string, JxStateDefinition>} [defs]
|
|
510
|
-
* @param {
|
|
511
|
-
* @returns {
|
|
581
|
+
* @param {JxScope | null} [parentScope]
|
|
582
|
+
* @returns {JxScope}
|
|
512
583
|
*/
|
|
513
584
|
export function buildInitialScope(
|
|
514
585
|
defs: Record<string, JxStateDefinition> = {},
|
|
515
|
-
parentScope:
|
|
586
|
+
parentScope: JxScope | null = null,
|
|
516
587
|
) {
|
|
517
|
-
const scope =
|
|
588
|
+
const scope = emptyScope(parentScope);
|
|
518
589
|
// Keys whose real value only exists after hydration, so prerender must not interpolate them —
|
|
519
590
|
// See `readsRuntimeOnlyState`. Seeded from the parent so nested scopes inherit the marks.
|
|
520
|
-
const inherited =
|
|
521
|
-
const runtimeOnly = new Set<string>(inherited
|
|
591
|
+
const inherited = parentScope ? runtimeOnlyKeysOf(parentScope) : undefined;
|
|
592
|
+
const runtimeOnly = new Set<string>(inherited);
|
|
522
593
|
// Attached up front, and mutated in place by the passes below, so the template pass at the end can
|
|
523
594
|
// Consult the marks the earlier passes added.
|
|
524
595
|
Object.defineProperty(scope, RUNTIME_ONLY_KEYS, {
|
|
@@ -534,12 +605,8 @@ export function buildInitialScope(
|
|
|
534
605
|
// Builder cannot open, so a write that only happens there is still missed (issue #125).
|
|
535
606
|
const assigned = new Set<string>();
|
|
536
607
|
for (const def of Object.values(defs)) {
|
|
537
|
-
if (
|
|
538
|
-
collectAssignedStateKeys(
|
|
539
|
-
(def as { body?: unknown; $expression?: unknown }).body ??
|
|
540
|
-
(def as { $expression?: unknown }).$expression,
|
|
541
|
-
assigned,
|
|
542
|
-
);
|
|
608
|
+
if (isFunctionDef(def) || isExpressionDef(def)) {
|
|
609
|
+
collectAssignedStateKeys(def.body ?? def.$expression, assigned);
|
|
543
610
|
}
|
|
544
611
|
}
|
|
545
612
|
for (const key of assigned) {
|
|
@@ -549,17 +616,16 @@ export function buildInitialScope(
|
|
|
549
616
|
}
|
|
550
617
|
|
|
551
618
|
for (const [key, def] of Object.entries(defs)) {
|
|
552
|
-
if (
|
|
619
|
+
if (!isNonNullObject(def) || Array.isArray(def)) {
|
|
553
620
|
setOwnScopeValue(scope, key, cloneValue(def));
|
|
554
621
|
continue;
|
|
555
622
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
setOwnScopeValue(scope, key, cloneValue(d.default));
|
|
623
|
+
if ("default" in def) {
|
|
624
|
+
setOwnScopeValue(scope, key, cloneValue(def.default));
|
|
559
625
|
continue;
|
|
560
626
|
}
|
|
561
|
-
if (!
|
|
562
|
-
setOwnScopeValue(scope, key, cloneValue(
|
|
627
|
+
if (!def.$prototype && !("$expression" in def) && !isSchemaOnlyDef(def)) {
|
|
628
|
+
setOwnScopeValue(scope, key, cloneValue(def));
|
|
563
629
|
}
|
|
564
630
|
}
|
|
565
631
|
|
|
@@ -572,34 +638,32 @@ export function buildInitialScope(
|
|
|
572
638
|
const computedBodies: [string, string][] = [];
|
|
573
639
|
|
|
574
640
|
for (const [key, def] of Object.entries(defs)) {
|
|
575
|
-
if (
|
|
641
|
+
if (isTemplateString(def)) {
|
|
576
642
|
templateDefs.push([key, def]);
|
|
577
643
|
continue;
|
|
578
644
|
}
|
|
579
|
-
if (!def
|
|
645
|
+
if (!isNonNullObject(def)) {
|
|
580
646
|
continue;
|
|
581
647
|
}
|
|
582
648
|
if (isExpressionDef(def)) {
|
|
583
|
-
const node = def.$expression
|
|
649
|
+
const node = def.$expression;
|
|
584
650
|
if (isNamedFormulaDef(def)) {
|
|
585
651
|
// Named formula: callable, positional args mapped onto its declared parameters.
|
|
586
|
-
const params = def.parameters
|
|
587
|
-
setOwnScopeValue(scope, key, (...argValues:
|
|
588
|
-
const args:
|
|
652
|
+
const params: readonly FormulaParameter[] = def.parameters;
|
|
653
|
+
setOwnScopeValue(scope, key, (...argValues: JxScope[string][]) => {
|
|
654
|
+
const args: JxScope = {};
|
|
589
655
|
for (const [i, p] of params.entries()) {
|
|
590
|
-
const name =
|
|
656
|
+
const name = formulaParameterName(p);
|
|
591
657
|
if (!name) {
|
|
592
658
|
continue;
|
|
593
659
|
}
|
|
594
660
|
args[name] =
|
|
595
|
-
argValues[i] === undefined &&
|
|
596
|
-
? p.default
|
|
597
|
-
: argValues[i];
|
|
661
|
+
argValues[i] === undefined && hasParameterDefault(p) ? p.default : argValues[i];
|
|
598
662
|
}
|
|
599
663
|
return evaluateExpression(node, scope, null, { args });
|
|
600
664
|
});
|
|
601
665
|
} else if (isMutating(node.operator)) {
|
|
602
|
-
setOwnScopeValue(scope, key, (s:
|
|
666
|
+
setOwnScopeValue(scope, key, (s: JxScope, event: Event) =>
|
|
603
667
|
evaluateExpression(node, s, event),
|
|
604
668
|
);
|
|
605
669
|
} else {
|
|
@@ -609,12 +673,13 @@ export function buildInitialScope(
|
|
|
609
673
|
}
|
|
610
674
|
if (isFunctionDef(def)) {
|
|
611
675
|
if (hasStructuredBody(def)) {
|
|
612
|
-
// Structured body (spec §20): a side-effecting handler in the build-time scope.
|
|
676
|
+
// Structured body (spec §20): a side-effecting handler in the build-time scope. Build-time
|
|
677
|
+
// Code may call it bare, so the scope it runs against falls back to this one.
|
|
613
678
|
const { body } = def;
|
|
614
|
-
setOwnScopeValue(scope, key, (s
|
|
679
|
+
setOwnScopeValue(scope, key, (s?: JxScope, event?: Event) => {
|
|
615
680
|
void runStatements(body, s ?? scope, event ?? null);
|
|
616
681
|
});
|
|
617
|
-
} else if (
|
|
682
|
+
} else if (isString(def.body)) {
|
|
618
683
|
const declared = def.parameters ? paramNames(def.parameters) : (def.arguments ?? []);
|
|
619
684
|
const { body } = def;
|
|
620
685
|
// Declared names bind by name, not by position (spec.md §5.3 4d): a parameter literally
|
|
@@ -623,8 +688,13 @@ export function buildInitialScope(
|
|
|
623
688
|
// The older behaviour here — produced a duplicate parameter for the `["state", "event"]`
|
|
624
689
|
// Form that examples/components/task-manager.json uses, leaving `state` undefined.
|
|
625
690
|
const params = declared.includes("state") ? declared : ["state", ...declared];
|
|
626
|
-
|
|
627
|
-
|
|
691
|
+
/* SAFETY: `new Function` returns an ordinary function of exactly `params`; `invoke` below is
|
|
692
|
+
its only caller and passes the scope or the event in each position, and what the body
|
|
693
|
+
returns is one more scope member. */
|
|
694
|
+
const fn = new Function(...params, body) as (
|
|
695
|
+
...args: (JxScope | Event | undefined)[]
|
|
696
|
+
) => JxScope[string];
|
|
697
|
+
const invoke = (state: JxScope, event?: Event) =>
|
|
628
698
|
fn(...params.map((name) => (name === "state" ? state : event)));
|
|
629
699
|
if (bodyReturnsValue(body)) {
|
|
630
700
|
defineLazyScopeValue(scope, key, () => invoke(scope));
|
|
@@ -677,11 +747,11 @@ export function buildInitialScope(
|
|
|
677
747
|
}
|
|
678
748
|
|
|
679
749
|
/**
|
|
680
|
-
* @param {
|
|
750
|
+
* @param {JxScope} scope
|
|
681
751
|
* @param {string} key
|
|
682
|
-
* @param {
|
|
752
|
+
* @param {JxScope[string]} value - Any scope member: document data or a build-time callable
|
|
683
753
|
*/
|
|
684
|
-
export function setOwnScopeValue(scope:
|
|
754
|
+
export function setOwnScopeValue(scope: JxScope, key: string, value: JxScope[string]) {
|
|
685
755
|
Object.defineProperty(scope, key, {
|
|
686
756
|
configurable: true,
|
|
687
757
|
enumerable: true,
|
|
@@ -691,15 +761,11 @@ export function setOwnScopeValue(scope: Record<string, unknown>, key: string, va
|
|
|
691
761
|
}
|
|
692
762
|
|
|
693
763
|
/**
|
|
694
|
-
* @param {
|
|
764
|
+
* @param {JxScope} scope
|
|
695
765
|
* @param {string} key
|
|
696
|
-
* @param {() =>
|
|
766
|
+
* @param {() => JxScope[string]} getter - Computes the member on read
|
|
697
767
|
*/
|
|
698
|
-
export function defineLazyScopeValue(
|
|
699
|
-
scope: Record<string, unknown>,
|
|
700
|
-
key: string,
|
|
701
|
-
getter: () => unknown,
|
|
702
|
-
) {
|
|
768
|
+
export function defineLazyScopeValue(scope: JxScope, key: string, getter: () => JxScope[string]) {
|
|
703
769
|
Object.defineProperty(scope, key, {
|
|
704
770
|
configurable: true,
|
|
705
771
|
enumerable: true,
|
|
@@ -708,16 +774,17 @@ export function defineLazyScopeValue(
|
|
|
708
774
|
}
|
|
709
775
|
|
|
710
776
|
/**
|
|
711
|
-
* @param {
|
|
712
|
-
*
|
|
777
|
+
* @param {JxDocumentValue | undefined} value - A bindable document value: a `$ref`, a template
|
|
778
|
+
* string, or a literal returned as is
|
|
779
|
+
* @param {JxScope | null} scope
|
|
713
780
|
* @returns {unknown}
|
|
714
781
|
*/
|
|
715
|
-
export function resolveStaticValue(value:
|
|
716
|
-
if (
|
|
717
|
-
return resolveRefValue(
|
|
782
|
+
export function resolveStaticValue(value: JxDocumentValue | undefined, scope: JxScope | null) {
|
|
783
|
+
if (isRef(value)) {
|
|
784
|
+
return resolveRefValue(value.$ref, scope!);
|
|
718
785
|
}
|
|
719
786
|
if (isTemplateString(value)) {
|
|
720
|
-
return evaluateStaticTemplate(value
|
|
787
|
+
return evaluateStaticTemplate(value, scope);
|
|
721
788
|
}
|
|
722
789
|
return value;
|
|
723
790
|
}
|
|
@@ -731,18 +798,22 @@ export function isRefObject(value: unknown): value is JxRef {
|
|
|
731
798
|
}
|
|
732
799
|
|
|
733
800
|
/**
|
|
734
|
-
* @param {
|
|
735
|
-
*
|
|
801
|
+
* @param {JsonValue | undefined} refValue - A `$ref` member as the document holds it; anything but
|
|
802
|
+
* a string is returned as is
|
|
803
|
+
* @param {JxScope} scope
|
|
736
804
|
* @returns {unknown}
|
|
737
805
|
*/
|
|
738
|
-
export function resolveRefValue(refValue:
|
|
739
|
-
if (
|
|
806
|
+
export function resolveRefValue(refValue: JsonValue | undefined, scope: JxScope) {
|
|
807
|
+
if (!isString(refValue)) {
|
|
740
808
|
return refValue;
|
|
741
809
|
}
|
|
742
810
|
if (refValue.startsWith("$map/")) {
|
|
743
811
|
const parts = refValue.split("/");
|
|
744
812
|
const key = parts[1]!;
|
|
745
|
-
|
|
813
|
+
/* SAFETY: `?.` stops at null and undefined, and every other value answers a member read by
|
|
814
|
+
name; the member comes back as a scope member, so the view claims nothing about its type. */
|
|
815
|
+
const row = scope.$map as JxScope | null | undefined;
|
|
816
|
+
const base = row?.[key] ?? scope[`$map/${key}`];
|
|
746
817
|
return parts.length > 2 ? getPathValue(base, parts.slice(2).join("/")) : base;
|
|
747
818
|
}
|
|
748
819
|
if (refValue.startsWith("#/state/")) {
|
|
@@ -770,43 +841,58 @@ export function resolveRefValue(refValue: unknown, scope: Record<string, unknown
|
|
|
770
841
|
* formulas (spec.md §19.4c) are meant to be used during prerender.
|
|
771
842
|
*
|
|
772
843
|
* @param {string} str
|
|
773
|
-
* @param {
|
|
844
|
+
* @param {JxScope | null} scope
|
|
774
845
|
* @returns {boolean}
|
|
775
846
|
*/
|
|
776
|
-
function readsRuntimeOnlyState(str: string, scope:
|
|
847
|
+
function readsRuntimeOnlyState(str: string, scope: JxScope | null) {
|
|
777
848
|
if (!scope) {
|
|
778
849
|
return false;
|
|
779
850
|
}
|
|
780
|
-
const runtimeOnly = (scope
|
|
851
|
+
const runtimeOnly = runtimeOnlyKeysOf(scope);
|
|
781
852
|
for (const match of str.matchAll(/\bstate\.([A-Za-z_$][\w$]*)\s*(\()?/g)) {
|
|
782
853
|
if (match[2]) {
|
|
783
854
|
continue;
|
|
784
855
|
}
|
|
856
|
+
// SAFETY: capture group 1 is outside any optional part of the pattern, so a match has it.
|
|
785
857
|
const key = match[1] as string;
|
|
786
|
-
if (runtimeOnly
|
|
858
|
+
if (runtimeOnly?.has(key)) {
|
|
787
859
|
return true;
|
|
788
860
|
}
|
|
789
861
|
// Reading the key can run a computed's getter, which is free to throw on partial build-time
|
|
790
862
|
// Data. A throw says nothing about whether the entry is runtime-only, so it is not a mark.
|
|
791
|
-
let value:
|
|
863
|
+
let value: JxScope[string];
|
|
792
864
|
try {
|
|
793
865
|
value = scope[key];
|
|
794
866
|
} catch {
|
|
795
867
|
continue;
|
|
796
868
|
}
|
|
797
|
-
if (
|
|
869
|
+
if (isFunction(value)) {
|
|
798
870
|
return true;
|
|
799
871
|
}
|
|
800
872
|
}
|
|
801
873
|
return false;
|
|
802
874
|
}
|
|
803
875
|
|
|
876
|
+
/**
|
|
877
|
+
* The JavaScript expression a Shape 3 template string compiles to (spec.md §5.3): the expression
|
|
878
|
+
* itself, parenthesized, when the template is ONE expression, so it keeps its own type, and a
|
|
879
|
+
* template literal otherwise, which is a string by construction. The same line
|
|
880
|
+
* {@link evaluateStaticTemplate} and the runtime draw, so a prerendered page, a hydrated component
|
|
881
|
+
* and Studio's canvas read `"${state.a === state.b}"` as the same boolean.
|
|
882
|
+
*
|
|
883
|
+
* @param {string} template - Template source, already rewritten to the target's state prefix
|
|
884
|
+
* @returns {string} A JavaScript expression
|
|
885
|
+
*/
|
|
886
|
+
export function templateValueSource(template: string): string {
|
|
887
|
+
return isSingleExpression(template) ? `(${template.slice(2, -1)})` : `\`${template}\``;
|
|
888
|
+
}
|
|
889
|
+
|
|
804
890
|
/**
|
|
805
891
|
* @param {string} str
|
|
806
|
-
* @param {
|
|
892
|
+
* @param {JxScope | null} scope
|
|
807
893
|
* @returns {unknown}
|
|
808
894
|
*/
|
|
809
|
-
export function evaluateStaticTemplate(str: string, scope:
|
|
895
|
+
export function evaluateStaticTemplate(str: string, scope: JxScope | null) {
|
|
810
896
|
if (readsRuntimeOnlyState(str, scope)) {
|
|
811
897
|
return null;
|
|
812
898
|
}
|
|
@@ -822,7 +908,9 @@ export function evaluateStaticTemplate(str: string, scope: Record<string, unknow
|
|
|
822
908
|
const values = [scope, scope?.$map, scope?.$site, scope?.$page];
|
|
823
909
|
try {
|
|
824
910
|
const body = isSingleExpression(str) ? `return (${str.slice(2, -1)})` : `return \`${str}\``;
|
|
825
|
-
|
|
911
|
+
/* SAFETY: `new Function` compiles exactly the four parameters in `args`, and its body returns a
|
|
912
|
+
template's value, which is whatever the scope it reads holds: one more scope member. */
|
|
913
|
+
const fn = new Function(...args, body) as (...a: JxScope[string][]) => JxScope[string];
|
|
826
914
|
return fn(...values);
|
|
827
915
|
} catch {
|
|
828
916
|
return null;
|
|
@@ -830,20 +918,24 @@ export function evaluateStaticTemplate(str: string, scope: Record<string, unknow
|
|
|
830
918
|
}
|
|
831
919
|
|
|
832
920
|
/**
|
|
833
|
-
* @param {
|
|
921
|
+
* @param {JxScope[string]} base - A scope, or any member read from one
|
|
834
922
|
* @param {string} path
|
|
835
923
|
* @returns {unknown}
|
|
836
924
|
*/
|
|
837
|
-
export function getPathValue(base:
|
|
925
|
+
export function getPathValue(base: JxScope[string], path: string) {
|
|
838
926
|
return readPath(base, path);
|
|
839
927
|
}
|
|
840
928
|
|
|
841
929
|
/**
|
|
842
|
-
*
|
|
843
|
-
*
|
|
930
|
+
* A deep copy of an object, so a scope never aliases the document it was built from; anything else
|
|
931
|
+
* is returned as is.
|
|
932
|
+
*
|
|
933
|
+
* @template T
|
|
934
|
+
* @param {T} value
|
|
935
|
+
* @returns {T}
|
|
844
936
|
*/
|
|
845
|
-
export function cloneValue(value:
|
|
846
|
-
if (value
|
|
937
|
+
export function cloneValue<T>(value: T): T {
|
|
938
|
+
if (!isNonNullObject(value)) {
|
|
847
939
|
return value;
|
|
848
940
|
}
|
|
849
941
|
return structuredClone(value);
|
|
@@ -857,12 +949,12 @@ export function cloneValue(value: unknown) {
|
|
|
857
949
|
* class handle), so only a per-instance value is written on the element.
|
|
858
950
|
*
|
|
859
951
|
* @param {JxStyle | null | undefined} style
|
|
860
|
-
* @param {
|
|
952
|
+
* @param {JxScope | null} scope
|
|
861
953
|
* @returns {string} `prop: value; prop: value`, or "" when nothing resolves
|
|
862
954
|
*/
|
|
863
955
|
export function inlineStyleDeclarations(
|
|
864
956
|
style: JxStyle | null | undefined,
|
|
865
|
-
scope:
|
|
957
|
+
scope: JxScope | null,
|
|
866
958
|
): string {
|
|
867
959
|
if (!style || !scope) {
|
|
868
960
|
return "";
|
|
@@ -875,9 +967,6 @@ export function inlineStyleDeclarations(
|
|
|
875
967
|
!k.startsWith("&") &&
|
|
876
968
|
!k.startsWith("[") &&
|
|
877
969
|
!k.startsWith("@") &&
|
|
878
|
-
v !== null &&
|
|
879
|
-
typeof v !== "object" &&
|
|
880
|
-
typeof v === "string" &&
|
|
881
970
|
isTemplateString(v),
|
|
882
971
|
)
|
|
883
972
|
.map(([k, v]) => {
|
|
@@ -892,18 +981,14 @@ export function inlineStyleDeclarations(
|
|
|
892
981
|
* Build an HTML attribute string from a static element definition.
|
|
893
982
|
*
|
|
894
983
|
* @param {JxElement | JxMutableNode} def
|
|
895
|
-
* @param {
|
|
984
|
+
* @param {JxScope | null} scope
|
|
896
985
|
* @param {string} [hostStyle] - Declarations resolved by the caller against ANOTHER scope, folded
|
|
897
986
|
* into the same `style` attribute after the node's own. A nested component instance is the one
|
|
898
987
|
* caller: its own `style` resolves against the parent's scope, while the definition's host style
|
|
899
988
|
* resolves against the instance's, and an element cannot carry two `style` attributes.
|
|
900
989
|
* @returns {string}
|
|
901
990
|
*/
|
|
902
|
-
export function buildAttrs(
|
|
903
|
-
def: JxElement | JxMutableNode,
|
|
904
|
-
scope: Record<string, unknown> | null,
|
|
905
|
-
hostStyle = "",
|
|
906
|
-
) {
|
|
991
|
+
export function buildAttrs(def: JxElement | JxMutableNode, scope: JxScope | null, hostStyle = "") {
|
|
907
992
|
let out = "";
|
|
908
993
|
|
|
909
994
|
const id = resolveStaticValue(def.id, scope);
|
|
@@ -971,18 +1056,14 @@ export function buildAttrs(
|
|
|
971
1056
|
* Sidebar's own not-the-current-page marker, and second-guessing it would be this same defect
|
|
972
1057
|
* Pointed the other way.
|
|
973
1058
|
*/
|
|
974
|
-
if (
|
|
1059
|
+
if (isBoolean(value)) {
|
|
975
1060
|
const text = booleanAttrValue(k, value);
|
|
976
1061
|
if (text !== null) {
|
|
977
1062
|
out += text === "" ? ` ${k}` : ` ${k}="${escapeHtml(text)}"`;
|
|
978
1063
|
}
|
|
979
1064
|
continue;
|
|
980
1065
|
}
|
|
981
|
-
if (
|
|
982
|
-
value !== null &&
|
|
983
|
-
value !== undefined &&
|
|
984
|
-
(typeof value === "string" || typeof value === "number")
|
|
985
|
-
) {
|
|
1066
|
+
if (isString(value) || isNumber(value)) {
|
|
986
1067
|
out += ` ${k}="${escapeHtml(String(value))}"`;
|
|
987
1068
|
}
|
|
988
1069
|
}
|
|
@@ -1017,30 +1098,34 @@ export function childSeparator(preformatted: boolean | undefined, indent = " ")
|
|
|
1017
1098
|
return preformatted ? "" : `\n${indent}`;
|
|
1018
1099
|
}
|
|
1019
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* A {@link CompileContext} as a static emitter hands it to a node, before that node's own scope is
|
|
1103
|
+
* known: the scope in effect may still be none.
|
|
1104
|
+
*/
|
|
1105
|
+
export type InnerCompileContext = Omit<CompileContext, "scope"> & { scope: JxScope | null };
|
|
1106
|
+
|
|
1020
1107
|
/**
|
|
1021
1108
|
* Build the inner HTML (textContent or children) for a node.
|
|
1022
1109
|
*
|
|
1023
1110
|
* @param {JxElement} def
|
|
1024
1111
|
* @param {JxElement | null} raw
|
|
1025
|
-
* @param {
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
* }
|
|
1031
|
-
* @param {(def: unknown, raw: unknown, context: unknown) => string} childCompiler
|
|
1112
|
+
* @param {InnerCompileContext} context
|
|
1113
|
+
* @param {(
|
|
1114
|
+
* def: JxElement | string,
|
|
1115
|
+
* raw: JxElement | string,
|
|
1116
|
+
* context: InnerCompileContext,
|
|
1117
|
+
* ) => string} childCompiler
|
|
1032
1118
|
* @returns {string}
|
|
1033
1119
|
*/
|
|
1034
1120
|
export function buildInner(
|
|
1035
1121
|
def: JxElement,
|
|
1036
1122
|
raw: JxElement | null,
|
|
1037
|
-
context:
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
childCompiler: (def: unknown, raw: unknown, context: unknown) => string,
|
|
1123
|
+
context: InnerCompileContext,
|
|
1124
|
+
childCompiler: (
|
|
1125
|
+
def: JxElement | string,
|
|
1126
|
+
raw: JxElement | string,
|
|
1127
|
+
context: InnerCompileContext,
|
|
1128
|
+
) => string,
|
|
1044
1129
|
) {
|
|
1045
1130
|
const source = raw ?? def;
|
|
1046
1131
|
|
|
@@ -1049,13 +1134,14 @@ export function buildInner(
|
|
|
1049
1134
|
return value == null ? "" : escapeHtml(String(value));
|
|
1050
1135
|
}
|
|
1051
1136
|
if (source.innerHTML) {
|
|
1052
|
-
|
|
1137
|
+
const value = resolveStaticValue(source.innerHTML, context.scope);
|
|
1138
|
+
return value == null ? "" : String(value);
|
|
1053
1139
|
}
|
|
1054
1140
|
if (Array.isArray(source.children)) {
|
|
1055
1141
|
const rawChildren = raw?.children;
|
|
1056
1142
|
return source.children
|
|
1057
|
-
.map((c
|
|
1058
|
-
const childRaw = (rawChildren
|
|
1143
|
+
.map((c, i) => {
|
|
1144
|
+
const childRaw = (Array.isArray(rawChildren) ? rawChildren[i] : undefined) ?? c;
|
|
1059
1145
|
return childCompiler(c, childRaw, context);
|
|
1060
1146
|
})
|
|
1061
1147
|
.join(childSeparator(context.preformatted));
|
|
@@ -1127,7 +1213,7 @@ function recordDroppedReactive(
|
|
|
1127
1213
|
value: string | JxRef,
|
|
1128
1214
|
selector: string | null,
|
|
1129
1215
|
): null {
|
|
1130
|
-
const source =
|
|
1216
|
+
const source = isString(value) ? value : value.$ref;
|
|
1131
1217
|
_droppedReactive.push({ property, selector, source });
|
|
1132
1218
|
return null;
|
|
1133
1219
|
}
|
|
@@ -1196,6 +1282,154 @@ export function attrHelperSource(): string {
|
|
|
1196
1282
|
);
|
|
1197
1283
|
}
|
|
1198
1284
|
|
|
1285
|
+
/** The capture helper a light-DOM component module calls before its first render. */
|
|
1286
|
+
export const SLOT_CAPTURE_HELPER = "__jxCaptureSlotted";
|
|
1287
|
+
|
|
1288
|
+
/** The distribution helper a light-DOM component module calls after its first render. */
|
|
1289
|
+
export const SLOT_DISTRIBUTE_HELPER = "__jxDistributeSlots";
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* The slot rule (spec.md §8.5) as source for a generated module: `captureSlotted` and
|
|
1293
|
+
* `distributeSlots` from `@jxsuite/runtime`, inlined for the reason `attrHelperSource` gives, with
|
|
1294
|
+
* the whitespace pattern, the bracket word, the attribute and the record key SERIALIZED from the
|
|
1295
|
+
* runtime's exports rather than retyped. The record lives on `globalThis` under the same registered
|
|
1296
|
+
* symbol, so a group one module (or the interpreter) records is read by the element that captures
|
|
1297
|
+
* next. `slot-distribution-agreement.test.ts` runs this source and the runtime's functions over one
|
|
1298
|
+
* fixture table, which is what keeps the two from drifting.
|
|
1299
|
+
*
|
|
1300
|
+
* @returns {string} Two `function` declarations and their two constants, for a module's preamble.
|
|
1301
|
+
*/
|
|
1302
|
+
export function slotHelperSource(): string {
|
|
1303
|
+
const ws = `new RegExp(${JSON.stringify(SLOT_WHITESPACE.source)}, ${JSON.stringify(SLOT_WHITESPACE.flags)})`;
|
|
1304
|
+
const key = `Symbol.for(${JSON.stringify(SLOT_RECORD_KEY)})`;
|
|
1305
|
+
const attr = JSON.stringify(SLOT_ATTR);
|
|
1306
|
+
const mark = JSON.stringify(SLOT_MARK);
|
|
1307
|
+
return [
|
|
1308
|
+
`const __jxSlotWs = ${ws};`,
|
|
1309
|
+
`const __jxSlotRec = globalThis[${key}] || (globalThis[${key}] = new WeakMap());`,
|
|
1310
|
+
`function ${SLOT_CAPTURE_HELPER}(host) {`,
|
|
1311
|
+
` const id = host.getAttribute(${attr});`,
|
|
1312
|
+
` if (id === null) return Array.from(host.childNodes);`,
|
|
1313
|
+
` host.removeAttribute(${attr});`,
|
|
1314
|
+
` const open = ${mark} + ' ' + id + ' ';`,
|
|
1315
|
+
` const close = '/' + ${mark} + ' ' + id;`,
|
|
1316
|
+
` const openers = [];`,
|
|
1317
|
+
` const w = host.ownerDocument.createTreeWalker(host, 128);`,
|
|
1318
|
+
` for (let n = w.nextNode(); n; n = w.nextNode()) if (n.data.startsWith(open)) openers.push(n);`,
|
|
1319
|
+
` const out = [];`,
|
|
1320
|
+
` for (const o of openers) {`,
|
|
1321
|
+
` const name = decodeURIComponent(o.data.slice(open.length));`,
|
|
1322
|
+
` for (let n = o.nextSibling; n; n = n.nextSibling) {`,
|
|
1323
|
+
` if (n.nodeType === 8 && n.data === close) break;`,
|
|
1324
|
+
` __jxSlotRec.set(n, name);`,
|
|
1325
|
+
` out.push(n);`,
|
|
1326
|
+
` }`,
|
|
1327
|
+
` }`,
|
|
1328
|
+
` return out;`,
|
|
1329
|
+
`}`,
|
|
1330
|
+
`function ${SLOT_DISTRIBUTE_HELPER}(host, captured) {`,
|
|
1331
|
+
` const slots = Array.from(host.querySelectorAll('slot'));`,
|
|
1332
|
+
` if (slots.length === 0) return;`,
|
|
1333
|
+
` const groups = new Map();`,
|
|
1334
|
+
` for (const n of captured) {`,
|
|
1335
|
+
` if (n.nodeType === 3 && __jxSlotWs.test(n.data)) continue;`,
|
|
1336
|
+
` const r = __jxSlotRec.get(n);`,
|
|
1337
|
+
` __jxSlotRec.delete(n);`,
|
|
1338
|
+
` const name = r !== undefined ? r : n.nodeType === 1 ? n.getAttribute('slot') || '' : '';`,
|
|
1339
|
+
` let g = groups.get(name);`,
|
|
1340
|
+
` if (!g) { g = []; groups.set(name, g); }`,
|
|
1341
|
+
` g.push(n);`,
|
|
1342
|
+
` }`,
|
|
1343
|
+
` const taken = new Set();`,
|
|
1344
|
+
` for (const slot of slots) {`,
|
|
1345
|
+
` const name = slot.getAttribute('name') || '';`,
|
|
1346
|
+
` let g = [];`,
|
|
1347
|
+
` if (!taken.has(name)) { taken.add(name); g = groups.get(name) || []; }`,
|
|
1348
|
+
` const content = g.some((n) => n.nodeType === 1 || (n.nodeType === 3 && n.data !== ''));`,
|
|
1349
|
+
` const placed = content ? g : g.concat(Array.from(slot.childNodes));`,
|
|
1350
|
+
` const p = slot.parentElement;`,
|
|
1351
|
+
` if (p && p !== host && p.localName.includes('-')) {`,
|
|
1352
|
+
` const r = __jxSlotRec.get(slot);`,
|
|
1353
|
+
` const f = r !== undefined ? r : slot.getAttribute('slot') || '';`,
|
|
1354
|
+
` for (const n of placed) __jxSlotRec.set(n, f);`,
|
|
1355
|
+
` }`,
|
|
1356
|
+
` slot.replaceWith(...placed);`,
|
|
1357
|
+
` }`,
|
|
1358
|
+
`}`,
|
|
1359
|
+
].join("\n");
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* An empty comment written where a lit child part would otherwise end at a node distribution moves.
|
|
1364
|
+
*
|
|
1365
|
+
* Lit ends a child part at the node that followed its marker when the template was cloned. A `$map`
|
|
1366
|
+
* or `$switch` written directly before a `<slot>` ends at that slot, and once the slot is replaced
|
|
1367
|
+
* the part's next update clears every later sibling of its new parent and throws; a binding among
|
|
1368
|
+
* an instance's children ends at a named sibling, which moves to another group, in the same way. A
|
|
1369
|
+
* comment ends the part instead: it travels with its group, and it affects neither `:empty` nor a
|
|
1370
|
+
* child combinator, so a slot still "leaves no node" in the selector tree §16.6 is about.
|
|
1371
|
+
*/
|
|
1372
|
+
export const LIT_PART_GUARD = "<!---->";
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Whether a definition's tree draws a `<slot>` anywhere a render can reach: `children`, a
|
|
1376
|
+
* `$switch`'s `cases`, or a repeater's `map` (a whole-children repeater included).
|
|
1377
|
+
*
|
|
1378
|
+
* @param {JxMutableNode["children"]} children
|
|
1379
|
+
* @returns {boolean}
|
|
1380
|
+
*/
|
|
1381
|
+
export function definitionHasSlot(children: SlotChildren): boolean {
|
|
1382
|
+
for (const child of slotChildList(children)) {
|
|
1383
|
+
if (isMappedArray(child)) {
|
|
1384
|
+
if (child.map && definitionHasSlot([child.map])) {
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
if (!isNodeObject(child)) {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (child.tagName === "slot") {
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
const cases = Object.values(child.cases ?? {}).filter(
|
|
1396
|
+
(c) => isNodeObject(c) && !isRefObject(c),
|
|
1397
|
+
);
|
|
1398
|
+
if (definitionHasSlot(cases) || definitionHasSlot(child.children)) {
|
|
1399
|
+
return true;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/** A node's `children` as either document shape writes it. */
|
|
1406
|
+
export type SlotChildren =
|
|
1407
|
+
| JxElement["children"]
|
|
1408
|
+
| JxMutableNode["children"]
|
|
1409
|
+
| readonly StaticChild[]
|
|
1410
|
+
| null
|
|
1411
|
+
| undefined;
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* `children` as a list: an array as it is, a whole-children repeater as its one member, nothing as
|
|
1415
|
+
* nothing.
|
|
1416
|
+
*
|
|
1417
|
+
* @param {SlotChildren} children
|
|
1418
|
+
* @returns {readonly (StaticChild | JxMappedArray)[]}
|
|
1419
|
+
*/
|
|
1420
|
+
function slotChildList(children: SlotChildren): readonly (StaticChild | JxMappedArray)[] {
|
|
1421
|
+
if (isMappedArray(children)) {
|
|
1422
|
+
return [children];
|
|
1423
|
+
}
|
|
1424
|
+
// Document JSON can hold anything here; what is neither a list nor a repeater holds no child.
|
|
1425
|
+
if (!Array.isArray(children)) {
|
|
1426
|
+
return [];
|
|
1427
|
+
}
|
|
1428
|
+
/* SAFETY: every array member of `SlotChildren` is a list of children; `Array.isArray` narrows the
|
|
1429
|
+
union to `any[]`, so the cast only restores the element type it already had. */
|
|
1430
|
+
return children as readonly StaticChild[];
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1199
1433
|
/**
|
|
1200
1434
|
* Walk the entire document tree and collect all static nested CSS rules.
|
|
1201
1435
|
*
|
|
@@ -1230,7 +1464,7 @@ export function compileStyles(
|
|
|
1230
1464
|
`color-scheme` triplet a scheme query in `$media` still calls for, which is exactly what a
|
|
1231
1465
|
page with a scheme query and no project style has always carried. */
|
|
1232
1466
|
const projectSheet = buildSiteStyleCSS(
|
|
1233
|
-
projectStyle
|
|
1467
|
+
isNonNullObject(projectStyle) ? projectStyle : {},
|
|
1234
1468
|
mediaQueries,
|
|
1235
1469
|
(value) => value,
|
|
1236
1470
|
recordDroppedReactive,
|
|
@@ -1276,7 +1510,7 @@ export function collectStyles(
|
|
|
1276
1510
|
prefix = "jx",
|
|
1277
1511
|
) {
|
|
1278
1512
|
const counter = counterArg ?? { n: 0 };
|
|
1279
|
-
if (!def
|
|
1513
|
+
if (!isNonNullObject(def)) {
|
|
1280
1514
|
return;
|
|
1281
1515
|
}
|
|
1282
1516
|
|
|
@@ -1307,16 +1541,9 @@ export function collectStyles(
|
|
|
1307
1541
|
}
|
|
1308
1542
|
|
|
1309
1543
|
// $switch case nodes render in place of the container's content — collect their styles too
|
|
1310
|
-
if (def.cases
|
|
1311
|
-
for (const c of Object.values(def.cases)) {
|
|
1312
|
-
collectStyles(
|
|
1313
|
-
c as JxElement | JxMutableNode | string,
|
|
1314
|
-
rules,
|
|
1315
|
-
mediaQueries,
|
|
1316
|
-
selector,
|
|
1317
|
-
counter,
|
|
1318
|
-
prefix,
|
|
1319
|
-
);
|
|
1544
|
+
if (isNonNullObject(def.cases)) {
|
|
1545
|
+
for (const c of Object.values<JxElement | JxMutableNode>(def.cases)) {
|
|
1546
|
+
collectStyles(c, rules, mediaQueries, selector, counter, prefix);
|
|
1320
1547
|
}
|
|
1321
1548
|
}
|
|
1322
1549
|
}
|
|
@@ -1380,18 +1607,13 @@ export function collectSrcImports(doc: JxElement) {
|
|
|
1380
1607
|
* @param {Set<string>} srcs
|
|
1381
1608
|
*/
|
|
1382
1609
|
function _walkSrc(def: JxElement | JxMutableNode | string, srcs: Set<string>) {
|
|
1383
|
-
if (!def
|
|
1610
|
+
if (!isNonNullObject(def)) {
|
|
1384
1611
|
return;
|
|
1385
1612
|
}
|
|
1386
1613
|
if (def.state) {
|
|
1387
1614
|
for (const d of Object.values(def.state)) {
|
|
1388
|
-
if (
|
|
1389
|
-
d
|
|
1390
|
-
typeof d === "object" &&
|
|
1391
|
-
(d as JxMutableNode).$prototype === "Function" &&
|
|
1392
|
-
(d as JxMutableNode).$src
|
|
1393
|
-
) {
|
|
1394
|
-
srcs.add((d as JxMutableNode).$src as string);
|
|
1615
|
+
if (isFunctionDef(d) && d.$src) {
|
|
1616
|
+
srcs.add(d.$src);
|
|
1395
1617
|
}
|
|
1396
1618
|
}
|
|
1397
1619
|
}
|
|
@@ -1422,7 +1644,7 @@ function _walkServerEntries(
|
|
|
1422
1644
|
def: JxElement | JxMutableNode | string,
|
|
1423
1645
|
entries: Map<string, { key: string; exportName: string; src: string }>,
|
|
1424
1646
|
) {
|
|
1425
|
-
if (!def
|
|
1647
|
+
if (!isNonNullObject(def)) {
|
|
1426
1648
|
return;
|
|
1427
1649
|
}
|
|
1428
1650
|
if (def.state) {
|
|
@@ -1461,15 +1683,12 @@ export const SELF_CLOSING = new Set<string>([
|
|
|
1461
1683
|
* precisely because the old `${…}` form was left as text and then re-resolved against the page
|
|
1462
1684
|
* scope, where a component's own state is undefined.
|
|
1463
1685
|
*
|
|
1464
|
-
* @param {
|
|
1465
|
-
* @param {
|
|
1686
|
+
* @param {TagNameSlot} tagName
|
|
1687
|
+
* @param {JxScope | null} scope
|
|
1466
1688
|
* @returns {string}
|
|
1467
1689
|
*/
|
|
1468
|
-
export function resolveStaticTagName(
|
|
1469
|
-
tagName
|
|
1470
|
-
scope: Record<string, unknown> | null,
|
|
1471
|
-
): string {
|
|
1472
|
-
if (typeof tagName === "string") {
|
|
1690
|
+
export function resolveStaticTagName(tagName: TagNameSlot, scope: JxScope | null): string {
|
|
1691
|
+
if (isString(tagName)) {
|
|
1473
1692
|
return tagName;
|
|
1474
1693
|
}
|
|
1475
1694
|
const candidates = tagNameCandidates(tagName);
|
|
@@ -1480,10 +1699,14 @@ export function resolveStaticTagName(
|
|
|
1480
1699
|
return candidates[0]!;
|
|
1481
1700
|
}
|
|
1482
1701
|
const expression = tagName.$expression;
|
|
1702
|
+
/* SAFETY: a tag expression is parsed from a document, so its `target` is a document value (a
|
|
1703
|
+
`$ref`, a template string or a literal); the schema leaves it untyped only because any operand
|
|
1704
|
+
may stand there. */
|
|
1705
|
+
const target = expression.target as JxDocumentValue | undefined;
|
|
1483
1706
|
if (expression.operator === "?:") {
|
|
1484
|
-
return resolveStaticValue(
|
|
1707
|
+
return resolveStaticValue(target, scope) ? expression.value : expression.initial;
|
|
1485
1708
|
}
|
|
1486
|
-
const key = resolveStaticValue(
|
|
1709
|
+
const key = resolveStaticValue(target, scope);
|
|
1487
1710
|
return expression.cases[String(key)] ?? expression.default;
|
|
1488
1711
|
}
|
|
1489
1712
|
|
|
@@ -1506,6 +1729,120 @@ export interface ComponentPrerenderContext {
|
|
|
1506
1729
|
defaults?: ProjectConfig["defaults"] | undefined;
|
|
1507
1730
|
/** The instances being expanded, outermost first; empty at a page-level instance. */
|
|
1508
1731
|
path?: readonly PrerenderFrame[] | undefined;
|
|
1732
|
+
/**
|
|
1733
|
+
* The page's counter for `data-jx-slots` ids. Nested contexts are spread copies, so they share
|
|
1734
|
+
* it; without one (a unit test, a definition preview) no instance is stamped or bracketed.
|
|
1735
|
+
*/
|
|
1736
|
+
slotIds?: { n: number } | undefined;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/**
|
|
1740
|
+
* An instance's children, grouped by the slot they name, as the static renderer places them
|
|
1741
|
+
* (spec.md §8.5). One render consumes a fill: `taken` records which names a slot already took.
|
|
1742
|
+
*/
|
|
1743
|
+
export interface SlotFill {
|
|
1744
|
+
/** Group name (`""` is the unnamed group) → its rendered HTML and whether it holds content. */
|
|
1745
|
+
groups: Map<string, { html: string; content: boolean }>;
|
|
1746
|
+
taken: Set<string>;
|
|
1747
|
+
/** The instance's `data-jx-slots` id, whose brackets wrap each placed group; null for none. */
|
|
1748
|
+
id: number | null;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* The next `data-jx-slots` id of the page being rendered, or null without a counter.
|
|
1753
|
+
*
|
|
1754
|
+
* @param {ComponentPrerenderContext | null} context
|
|
1755
|
+
* @returns {number | null}
|
|
1756
|
+
*/
|
|
1757
|
+
export function nextSlotId(context: ComponentPrerenderContext | null): number | null {
|
|
1758
|
+
if (!context?.slotIds) {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
context.slotIds.n += 1;
|
|
1762
|
+
return context.slotIds.n;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Group an instance's children by slot name, as definitions, applying the rule the interpreter's
|
|
1767
|
+
* `distributeSlots` applies to nodes: a string or number whose rendered text is only HTML
|
|
1768
|
+
* whitespace is dropped; an element names its group by its resolved `attributes.slot`, else its
|
|
1769
|
+
* resolved node-level `slot` (which is then rendered as the attribute, so a recovered child still
|
|
1770
|
+
* names its group); everything else joins the unnamed group. An element, `$switch` or repeater
|
|
1771
|
+
* child is content; a string or number is when its text is not empty.
|
|
1772
|
+
*
|
|
1773
|
+
* @param {StaticChild} children
|
|
1774
|
+
* @param {JxScope | null} scope - The scope the children resolve in (the parent's)
|
|
1775
|
+
* @param {(child: StaticChild) => string} render - Renders one child
|
|
1776
|
+
* @param {number | null} [id]
|
|
1777
|
+
* @returns {SlotFill}
|
|
1778
|
+
*/
|
|
1779
|
+
export function createSlotFill(
|
|
1780
|
+
children: SlotChildren,
|
|
1781
|
+
scope: JxScope | null,
|
|
1782
|
+
render: (child: StaticChild) => string,
|
|
1783
|
+
id: number | null = null,
|
|
1784
|
+
): SlotFill {
|
|
1785
|
+
const groups: SlotFill["groups"] = new Map();
|
|
1786
|
+
for (const child of slotChildList(children)) {
|
|
1787
|
+
let name = "";
|
|
1788
|
+
let content = true;
|
|
1789
|
+
let html: string;
|
|
1790
|
+
if (isString(child) || isNumber(child) || isBoolean(child)) {
|
|
1791
|
+
html = render(child);
|
|
1792
|
+
if (SLOT_WHITESPACE.test(html)) {
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
content = html !== "";
|
|
1796
|
+
} else if (isMappedArray(child)) {
|
|
1797
|
+
// A repeater renders nothing statically; it is content all the same, as its rows will be.
|
|
1798
|
+
html = "";
|
|
1799
|
+
} else if (!isNodeObject(child)) {
|
|
1800
|
+
html = render(child);
|
|
1801
|
+
content = html !== "";
|
|
1802
|
+
} else {
|
|
1803
|
+
const fromAttr = resolveStaticValue(child.attributes?.slot, scope);
|
|
1804
|
+
const fromNode = isString(child.slot) ? resolveStaticValue(child.slot, scope) : null;
|
|
1805
|
+
if (isString(fromAttr) && fromAttr !== "") {
|
|
1806
|
+
name = fromAttr;
|
|
1807
|
+
html = render(child);
|
|
1808
|
+
} else if (isString(fromNode) && fromNode !== "") {
|
|
1809
|
+
name = fromNode;
|
|
1810
|
+
const { slot: _slot, ...rest } = child;
|
|
1811
|
+
html = render({ ...rest, attributes: { ...rest.attributes, slot: fromNode } });
|
|
1812
|
+
} else {
|
|
1813
|
+
html = render(child);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
const group = groups.get(name);
|
|
1817
|
+
if (group) {
|
|
1818
|
+
group.html = group.html === "" ? html : `${group.html}\n${html}`;
|
|
1819
|
+
group.content ||= content;
|
|
1820
|
+
} else {
|
|
1821
|
+
groups.set(name, { content, html });
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return { groups, id, taken: new Set() };
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Mark every slot name inside a fallback that is not rendered as taken, in tree order, so the
|
|
1829
|
+
* prerender's answer matches the DOM tiers', where such a slot is still met by `querySelectorAll`.
|
|
1830
|
+
*
|
|
1831
|
+
* @param {StaticChild} node
|
|
1832
|
+
* @param {JxScope | null} scope
|
|
1833
|
+
* @param {SlotFill} slots
|
|
1834
|
+
*/
|
|
1835
|
+
function takeSlotNames(children: SlotChildren, scope: JxScope | null, slots: SlotFill): void {
|
|
1836
|
+
for (const node of slotChildList(children)) {
|
|
1837
|
+
if (isMappedArray(node) || !isNodeObject(node)) {
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
if (node.tagName === "slot") {
|
|
1841
|
+
const name = resolveStaticValue(node.attributes?.name, scope);
|
|
1842
|
+
slots.taken.add(isString(name) ? name : "");
|
|
1843
|
+
}
|
|
1844
|
+
takeSlotNames(node.children, scope, slots);
|
|
1845
|
+
}
|
|
1509
1846
|
}
|
|
1510
1847
|
|
|
1511
1848
|
/** One instance on the expansion path: its tag, and its resolved props as an identity. */
|
|
@@ -1515,17 +1852,13 @@ interface PrerenderFrame {
|
|
|
1515
1852
|
key: string;
|
|
1516
1853
|
}
|
|
1517
1854
|
|
|
1518
|
-
/**
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
* what turns the second into a diagnostic instead of a stack overflow. Thirty-two is far beyond any
|
|
1526
|
-
* component library's real depth and far below the call stack's.
|
|
1527
|
-
*/
|
|
1528
|
-
export const MAX_COMPONENT_NESTING = 32;
|
|
1855
|
+
/** What {@link liftPropsAttributes} splits an instance's attributes into. */
|
|
1856
|
+
export interface LiftedPropsAttributes {
|
|
1857
|
+
/** The `props.*` values keyed by prop name, or null when the instance wrote none. */
|
|
1858
|
+
lifted: Record<string, JsonValue> | null;
|
|
1859
|
+
/** Every other attribute, unchanged. */
|
|
1860
|
+
rest: NonNullable<JxElement["attributes"]>;
|
|
1861
|
+
}
|
|
1529
1862
|
|
|
1530
1863
|
/**
|
|
1531
1864
|
* Lift literal `props.*` attribute keys off an instance's attributes.
|
|
@@ -1538,20 +1871,17 @@ export const MAX_COMPONENT_NESTING = 32;
|
|
|
1538
1871
|
* definition every page shares.
|
|
1539
1872
|
*
|
|
1540
1873
|
* @param {Record<string, JxAttributeValue> | undefined} attributes
|
|
1541
|
-
* @returns {
|
|
1542
|
-
*
|
|
1874
|
+
* @returns {LiftedPropsAttributes} The lifted props (null when there were none) and the attributes
|
|
1875
|
+
* that remain
|
|
1543
1876
|
*/
|
|
1544
|
-
export function liftPropsAttributes(attributes: JxElement["attributes"]): {
|
|
1545
|
-
lifted: Record<string, JsonValue> | null;
|
|
1546
|
-
rest: NonNullable<JxElement["attributes"]>;
|
|
1547
|
-
} {
|
|
1877
|
+
export function liftPropsAttributes(attributes: JxElement["attributes"]): LiftedPropsAttributes {
|
|
1548
1878
|
let lifted: Record<string, JsonValue> | null = null;
|
|
1549
1879
|
const rest: NonNullable<JxElement["attributes"]> = {};
|
|
1550
1880
|
for (const [key, value] of Object.entries(attributes ?? {})) {
|
|
1551
1881
|
if (key.startsWith("props.") && key.length > "props.".length) {
|
|
1552
1882
|
lifted ??= {};
|
|
1553
|
-
|
|
1554
|
-
|
|
1883
|
+
/* SAFETY: a JxAttributeValue is a JSON primitive or a `{ $ref }` object with one string
|
|
1884
|
+
member, so it is a JsonValue; only the interface's missing index signature hides that. */
|
|
1555
1885
|
lifted[key.slice("props.".length)] = value as JsonValue;
|
|
1556
1886
|
} else {
|
|
1557
1887
|
rest[key] = value;
|
|
@@ -1571,33 +1901,32 @@ export function liftPropsAttributes(attributes: JxElement["attributes"]): {
|
|
|
1571
1901
|
*
|
|
1572
1902
|
* @param {JxElement} doc - Component definition
|
|
1573
1903
|
* @param {Record<string, JsonValue> | null} props - Instance-specific prop values
|
|
1574
|
-
* @returns {
|
|
1904
|
+
* @returns {JxScope}
|
|
1575
1905
|
*/
|
|
1576
1906
|
export function buildInstanceScope(
|
|
1577
1907
|
doc: JxElement,
|
|
1578
1908
|
props: Record<string, JsonValue> | null,
|
|
1579
|
-
):
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
"default" in existing
|
|
1591
|
-
? { .../** @type {JxStateObject} */ existing, default: value }
|
|
1592
|
-
: (value as JxStateDefinition);
|
|
1593
|
-
} else {
|
|
1594
|
-
stateDefs[key] = value as JxStateDefinition;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1909
|
+
): JxScope {
|
|
1910
|
+
if (!props) {
|
|
1911
|
+
return buildInitialScope(doc.state ?? {}, null);
|
|
1912
|
+
}
|
|
1913
|
+
const stateDefs = { ...doc.state };
|
|
1914
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1915
|
+
const existing = stateDefs[key];
|
|
1916
|
+
stateDefs[key] =
|
|
1917
|
+
isNonNullObject(existing) && !Array.isArray(existing) && "default" in existing
|
|
1918
|
+
? { ...existing, default: value }
|
|
1919
|
+
: value;
|
|
1597
1920
|
}
|
|
1598
1921
|
return buildInitialScope(stateDefs, null);
|
|
1599
1922
|
}
|
|
1600
1923
|
|
|
1924
|
+
/**
|
|
1925
|
+
* Resolved host declarations by (camelCase) property: each is what its template read from the
|
|
1926
|
+
* instance's scope.
|
|
1927
|
+
*/
|
|
1928
|
+
export type ResolvedHostStyle = Record<string, JxScope[string]>;
|
|
1929
|
+
|
|
1601
1930
|
/**
|
|
1602
1931
|
* The definition's host `style` entries that depend on the instance, resolved against it.
|
|
1603
1932
|
*
|
|
@@ -1607,26 +1936,22 @@ export function buildInstanceScope(
|
|
|
1607
1936
|
* one.
|
|
1608
1937
|
*
|
|
1609
1938
|
* @param {JxStyle | null | undefined} style - The definition's `style`
|
|
1610
|
-
* @param {
|
|
1611
|
-
* @returns {
|
|
1939
|
+
* @param {JxScope} scope - The instance's scope (`buildInstanceScope`)
|
|
1940
|
+
* @returns {ResolvedHostStyle}
|
|
1612
1941
|
*/
|
|
1613
1942
|
export function resolveHostStyle(
|
|
1614
1943
|
style: JxStyle | null | undefined,
|
|
1615
|
-
scope:
|
|
1616
|
-
):
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
for (const [prop, value] of Object.entries(style)) {
|
|
1622
|
-
if (typeof value === "string" && isTemplateString(value)) {
|
|
1623
|
-
const val = resolveStaticValue(value, scope);
|
|
1624
|
-
if (val != null) {
|
|
1625
|
-
resolved[prop] = val;
|
|
1944
|
+
scope: JxScope,
|
|
1945
|
+
): ResolvedHostStyle {
|
|
1946
|
+
return Object.fromEntries(
|
|
1947
|
+
Object.entries(style ?? {}).flatMap(([prop, value]) => {
|
|
1948
|
+
if (!isTemplateString(value)) {
|
|
1949
|
+
return [];
|
|
1626
1950
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1951
|
+
const val = resolveStaticValue(value, scope);
|
|
1952
|
+
return val == null ? [] : [[prop, val] as const];
|
|
1953
|
+
}),
|
|
1954
|
+
);
|
|
1630
1955
|
}
|
|
1631
1956
|
|
|
1632
1957
|
/**
|
|
@@ -1638,15 +1963,15 @@ export function resolveHostStyle(
|
|
|
1638
1963
|
* the shape spec.md §17.2 recommends when every child is a bare string — prerendered to nothing.
|
|
1639
1964
|
*
|
|
1640
1965
|
* @param {JxElement | JxMutableNode} node
|
|
1641
|
-
* @param {
|
|
1642
|
-
* @param {
|
|
1966
|
+
* @param {JxScope | null} scope
|
|
1967
|
+
* @param {SlotFill | null} slots
|
|
1643
1968
|
* @param {ComponentPrerenderContext | null} context
|
|
1644
1969
|
* @returns {string}
|
|
1645
1970
|
*/
|
|
1646
1971
|
function renderInner(
|
|
1647
1972
|
node: JxElement | JxMutableNode,
|
|
1648
|
-
scope:
|
|
1649
|
-
|
|
1973
|
+
scope: JxScope | null,
|
|
1974
|
+
slots: SlotFill | null,
|
|
1650
1975
|
context: ComponentPrerenderContext | null,
|
|
1651
1976
|
): string {
|
|
1652
1977
|
if (node.textContent !== undefined) {
|
|
@@ -1655,52 +1980,61 @@ function renderInner(
|
|
|
1655
1980
|
}
|
|
1656
1981
|
if (node.innerHTML) {
|
|
1657
1982
|
const val = resolveStaticValue(node.innerHTML, scope);
|
|
1658
|
-
return val != null ? String(val) :
|
|
1983
|
+
return val != null ? String(val) : node.innerHTML;
|
|
1659
1984
|
}
|
|
1660
1985
|
if (Array.isArray(node.children)) {
|
|
1661
1986
|
return node.children
|
|
1662
|
-
.map((c: JxElement | JxMutableNode | string) =>
|
|
1663
|
-
renderStaticNode(c, scope, slotContent, context),
|
|
1664
|
-
)
|
|
1987
|
+
.map((c: JxElement | JxMutableNode | string) => renderStaticNode(c, scope, slots, context))
|
|
1665
1988
|
.join("\n");
|
|
1666
1989
|
}
|
|
1667
1990
|
return "";
|
|
1668
1991
|
}
|
|
1669
1992
|
|
|
1993
|
+
/**
|
|
1994
|
+
* A child slot as the static renderer meets it: a node or a text string, plus the JSON scalars,
|
|
1995
|
+
* holes and nested lists a hand-written `children` array may also carry.
|
|
1996
|
+
*/
|
|
1997
|
+
export type StaticChild =
|
|
1998
|
+
| JxElement
|
|
1999
|
+
| JxMutableNode
|
|
2000
|
+
| string
|
|
2001
|
+
| number
|
|
2002
|
+
| boolean
|
|
2003
|
+
| null
|
|
2004
|
+
| undefined
|
|
2005
|
+
| StaticChild[];
|
|
2006
|
+
|
|
1670
2007
|
/**
|
|
1671
2008
|
* Recursively render a Jx node tree to static HTML for pre-rendering.
|
|
1672
2009
|
*
|
|
1673
|
-
* @param {
|
|
1674
|
-
* @param {
|
|
1675
|
-
* @param {
|
|
2010
|
+
* @param {StaticChild} node
|
|
2011
|
+
* @param {JxScope | null} scope
|
|
2012
|
+
* @param {SlotFill | null} [slots] - The instance's children by slot name; `null` leaves a `<slot>`
|
|
2013
|
+
* literal (a shadow root, or a definition rendered with no instance)
|
|
1676
2014
|
* @param {ComponentPrerenderContext | null} [context] - The component registry, when a custom
|
|
1677
2015
|
* element tag met here should be expanded rather than emitted bare
|
|
1678
2016
|
* @returns {string}
|
|
1679
2017
|
*/
|
|
1680
2018
|
export function renderStaticNode(
|
|
1681
|
-
node:
|
|
1682
|
-
scope:
|
|
1683
|
-
|
|
2019
|
+
node: StaticChild,
|
|
2020
|
+
scope: JxScope | null,
|
|
2021
|
+
slots: SlotFill | null = null,
|
|
1684
2022
|
context: ComponentPrerenderContext | null = null,
|
|
1685
2023
|
): string {
|
|
1686
|
-
if (
|
|
2024
|
+
if (isString(node)) {
|
|
1687
2025
|
if (isTemplateString(node) && scope) {
|
|
1688
2026
|
const val = evaluateStaticTemplate(node, scope);
|
|
1689
2027
|
return val != null ? escapeHtml(String(val)) : escapeHtml(node);
|
|
1690
2028
|
}
|
|
1691
2029
|
return escapeHtml(node);
|
|
1692
2030
|
}
|
|
1693
|
-
if (
|
|
2031
|
+
if (isNumber(node) || isBoolean(node)) {
|
|
1694
2032
|
return escapeHtml(String(node));
|
|
1695
2033
|
}
|
|
1696
2034
|
if (Array.isArray(node)) {
|
|
1697
|
-
return (
|
|
1698
|
-
.map((c: JxElement | JxMutableNode | string): string =>
|
|
1699
|
-
renderStaticNode(c, scope, slotContent, context),
|
|
1700
|
-
)
|
|
1701
|
-
.join("\n");
|
|
2035
|
+
return node.map((c): string => renderStaticNode(c, scope, slots, context)).join("\n");
|
|
1702
2036
|
}
|
|
1703
|
-
if (!node
|
|
2037
|
+
if (!isNonNullObject(node)) {
|
|
1704
2038
|
return "";
|
|
1705
2039
|
}
|
|
1706
2040
|
|
|
@@ -1716,13 +2050,10 @@ export function renderStaticNode(
|
|
|
1716
2050
|
const switchTag = node.tagName ?? "div";
|
|
1717
2051
|
const attrs = buildAttrs(node, scope);
|
|
1718
2052
|
const key = resolveStaticValue(node.$switch, scope);
|
|
1719
|
-
const caseDef =
|
|
1720
|
-
key == null
|
|
1721
|
-
? undefined
|
|
1722
|
-
: (node.cases as Record<string, JxElement | string> | undefined)?.[String(key)];
|
|
2053
|
+
const caseDef = key == null ? undefined : node.cases?.[String(key)];
|
|
1723
2054
|
const inner =
|
|
1724
2055
|
caseDef !== undefined && !isRefObject(caseDef)
|
|
1725
|
-
? renderStaticNode(caseDef, scope,
|
|
2056
|
+
? renderStaticNode(caseDef, scope, slots, context)
|
|
1726
2057
|
: "";
|
|
1727
2058
|
return `<${switchTag}${attrs}>${inner}</${switchTag}>`;
|
|
1728
2059
|
}
|
|
@@ -1733,15 +2064,33 @@ export function renderStaticNode(
|
|
|
1733
2064
|
state does not exist, so SSR silently emitted the fallback branch's tag. */
|
|
1734
2065
|
const tag = resolveStaticTagName(node.tagName, scope);
|
|
1735
2066
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2067
|
+
/* A slot is REPLACED by its group (spec.md §8.5), and leaves no node: the first slot of a name
|
|
2068
|
+
takes the group, and the fallback follows only when the group holds no content. A slot inside
|
|
2069
|
+
a fallback that is not rendered still takes its name, as it does in the DOM tiers. */
|
|
2070
|
+
if (tag === "slot" && slots) {
|
|
2071
|
+
const resolved = resolveStaticValue(node.attributes?.name, scope);
|
|
2072
|
+
const name = isString(resolved) ? resolved : "";
|
|
2073
|
+
let group: { html: string; content: boolean } | undefined;
|
|
2074
|
+
if (!slots.taken.has(name)) {
|
|
2075
|
+
slots.taken.add(name);
|
|
2076
|
+
group = slots.groups.get(name);
|
|
2077
|
+
}
|
|
2078
|
+
let out = group?.html ?? "";
|
|
2079
|
+
if (out !== "" && slots.id !== null) {
|
|
2080
|
+
out = `<!--${SLOT_MARK} ${slots.id} ${encodeURIComponent(name)}-->${out}<!--/${SLOT_MARK} ${slots.id}-->`;
|
|
2081
|
+
}
|
|
2082
|
+
if (group?.content) {
|
|
2083
|
+
takeSlotNames(node.children, scope, slots);
|
|
2084
|
+
return out;
|
|
2085
|
+
}
|
|
2086
|
+
const fallback = renderInner(node, scope, slots, context);
|
|
2087
|
+
return out === "" ? fallback : fallback === "" ? out : `${out}\n${fallback}`;
|
|
1739
2088
|
}
|
|
1740
2089
|
|
|
1741
2090
|
// A registered component: expanded exactly as the page walk expands one, not emitted bare.
|
|
1742
2091
|
const def = context?.componentDefs.get(tag);
|
|
1743
2092
|
if (def && context) {
|
|
1744
|
-
return renderComponentInstance(node, def, scope,
|
|
2093
|
+
return renderComponentInstance(node, def, scope, slots, context);
|
|
1745
2094
|
}
|
|
1746
2095
|
|
|
1747
2096
|
const attrs = buildAttrs(node, scope);
|
|
@@ -1750,7 +2099,7 @@ export function renderStaticNode(
|
|
|
1750
2099
|
return `<${tag}${attrs}>`;
|
|
1751
2100
|
}
|
|
1752
2101
|
|
|
1753
|
-
return `<${tag}${attrs}>${renderInner(node, scope,
|
|
2102
|
+
return `<${tag}${attrs}>${renderInner(node, scope, slots, context)}</${tag}>`;
|
|
1754
2103
|
}
|
|
1755
2104
|
|
|
1756
2105
|
/**
|
|
@@ -1761,7 +2110,10 @@ export function renderStaticNode(
|
|
|
1761
2110
|
* scope, which is what `resolveDocTemplates` does for a page-level instance before the page walk
|
|
1762
2111
|
* reaches it.
|
|
1763
2112
|
* - Slot content: the instance's own children, rendered in the parent's scope and with the parent's
|
|
1764
|
-
*
|
|
2113
|
+
* fill, so a `<slot>` written among them passes the grandparent's group of that name through, and
|
|
2114
|
+
* placed in the definition's slots by name (`createSlotFill`, spec.md §8.5). A live instance
|
|
2115
|
+
* whose definition has a slot carries `data-jx-slots` and brackets each group, so its element
|
|
2116
|
+
* recovers what it was given when it upgrades (`captureSlotted` in the runtime).
|
|
1765
2117
|
* - Shadow mode: a declarative shadow root with the stylesheet link inside it (spec.md §16.6).
|
|
1766
2118
|
* - Host style: the definition's template-string entries resolved against the INSTANCE, written on
|
|
1767
2119
|
* the element — the page walk's `resolvedStyle` in inline form, since there is no page stylesheet
|
|
@@ -1775,8 +2127,8 @@ export function renderStaticNode(
|
|
|
1775
2127
|
*
|
|
1776
2128
|
* @param {JxElement | JxMutableNode} node - The instance as written in the parent's tree
|
|
1777
2129
|
* @param {JxElement} def - The registered definition for its tag
|
|
1778
|
-
* @param {
|
|
1779
|
-
* @param {
|
|
2130
|
+
* @param {JxScope | null} scope - The parent's scope
|
|
2131
|
+
* @param {SlotFill | null} parentSlots - The parent's fill
|
|
1780
2132
|
* @param {ComponentPrerenderContext} context
|
|
1781
2133
|
* @returns {string}
|
|
1782
2134
|
* @docs framework/build
|
|
@@ -1784,15 +2136,20 @@ export function renderStaticNode(
|
|
|
1784
2136
|
function renderComponentInstance(
|
|
1785
2137
|
node: JxElement | JxMutableNode,
|
|
1786
2138
|
def: JxElement,
|
|
1787
|
-
scope:
|
|
1788
|
-
|
|
2139
|
+
scope: JxScope | null,
|
|
2140
|
+
parentSlots: SlotFill | null,
|
|
1789
2141
|
context: ComponentPrerenderContext,
|
|
1790
2142
|
): string {
|
|
2143
|
+
/* SAFETY: the caller looked `def` up in `componentDefs` by the node's resolved string tag, and
|
|
2144
|
+
that registry is keyed by each definition's own `tagName`, so this is that same string. */
|
|
1791
2145
|
const tag = def.tagName as string;
|
|
1792
2146
|
const { lifted, rest: attributes } = liftPropsAttributes(node.attributes);
|
|
1793
2147
|
const written = { ...lifted, ...node.$props };
|
|
1794
2148
|
const props: Record<string, JsonValue> = {};
|
|
1795
2149
|
for (const [key, value] of Object.entries(written)) {
|
|
2150
|
+
/* SAFETY: a prop resolves against the parent's build-time scope, where what a `$ref` or template
|
|
2151
|
+
may read is JSON state, and the props travel on as JSON either way (`data-jx-props` below), as
|
|
2152
|
+
the page walk serializes a page-level instance's. */
|
|
1796
2153
|
props[key] = (resolveStaticValue(value, scope) as JsonValue | null) ?? value;
|
|
1797
2154
|
}
|
|
1798
2155
|
const hasProps = Object.keys(props).length > 0;
|
|
@@ -1814,25 +2171,29 @@ function renderComponentInstance(
|
|
|
1814
2171
|
}
|
|
1815
2172
|
const nested: ComponentPrerenderContext = { ...context, path: [...path, frame] };
|
|
1816
2173
|
|
|
1817
|
-
// The instance's children are the parent's nodes: parent scope, parent
|
|
1818
|
-
const
|
|
1819
|
-
|
|
1820
|
-
? node.children
|
|
1821
|
-
.map((c: JxElement | JxMutableNode | string) =>
|
|
1822
|
-
renderStaticNode(c, scope, slotContent, context),
|
|
1823
|
-
)
|
|
1824
|
-
.join("\n")
|
|
1825
|
-
: null;
|
|
2174
|
+
// The instance's children are the parent's nodes: parent scope, parent fill, parent path.
|
|
2175
|
+
const { children } = node;
|
|
2176
|
+
const renderChild = (c: StaticChild) => renderStaticNode(c, scope, parentSlots, context);
|
|
1826
2177
|
|
|
1827
2178
|
const shadow = resolveShadowMode(def, context.defaults);
|
|
1828
2179
|
const isStatic = isComponentFullyStatic(def);
|
|
1829
2180
|
const instanceScope = buildInstanceScope(def, hasProps ? props : null);
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2181
|
+
let innerHTML: string;
|
|
2182
|
+
let slotId: number | null = null;
|
|
2183
|
+
if (shadow) {
|
|
2184
|
+
// The light children stay siblings of the template, where a real `<slot>` distributes them.
|
|
2185
|
+
const light = Array.isArray(children)
|
|
2186
|
+
? children.map((c: StaticChild) => renderChild(c)).join("\n")
|
|
2187
|
+
: "";
|
|
2188
|
+
innerHTML =
|
|
2189
|
+
`<template shadowrootmode="${shadow}">` +
|
|
1833
2190
|
`<link rel="stylesheet" href="/components/${tag}.css">` +
|
|
1834
|
-
`${
|
|
1835
|
-
|
|
2191
|
+
`${renderInner(def, instanceScope, null, nested)}</template>${light}`;
|
|
2192
|
+
} else {
|
|
2193
|
+
slotId = !isStatic && definitionHasSlot(def.children) ? nextSlotId(context) : null;
|
|
2194
|
+
const fill = createSlotFill(children, scope, renderChild, slotId);
|
|
2195
|
+
innerHTML = renderInner(def, instanceScope, fill, nested);
|
|
2196
|
+
}
|
|
1836
2197
|
|
|
1837
2198
|
const hostStyle = Object.entries(resolveHostStyle(def.style, instanceScope))
|
|
1838
2199
|
.map(([prop, value]) => `${camelToKebab(prop)}: ${value}`)
|
|
@@ -1840,11 +2201,14 @@ function renderComponentInstance(
|
|
|
1840
2201
|
|
|
1841
2202
|
// The element as the page walk would leave it: props consumed, children rendered, stamped. A
|
|
1842
2203
|
// Copy, because `node` is a definition every page shares.
|
|
1843
|
-
const instance
|
|
2204
|
+
const instance = { ...node };
|
|
1844
2205
|
delete instance.$props;
|
|
1845
2206
|
delete instance.children;
|
|
1846
2207
|
instance.attributes =
|
|
1847
2208
|
!isStatic && hasProps ? { ...attributes, "data-jx-props": JSON.stringify(props) } : attributes;
|
|
2209
|
+
if (slotId !== null) {
|
|
2210
|
+
instance.attributes = { ...instance.attributes, [SLOT_ATTR]: String(slotId) };
|
|
2211
|
+
}
|
|
1848
2212
|
if (isStatic) {
|
|
1849
2213
|
instance.$static = true;
|
|
1850
2214
|
} else {
|
|
@@ -1859,7 +2223,8 @@ function renderComponentInstance(
|
|
|
1859
2223
|
* @param {JxElement} doc - Component JSON definition
|
|
1860
2224
|
* @param {Record<string, JsonValue> | null} [propsOverride] - Instance-specific prop values to
|
|
1861
2225
|
* merge into state
|
|
1862
|
-
* @param {
|
|
2226
|
+
* @param {SlotFill | null} [slots] - The instance's children by slot name (`createSlotFill`);
|
|
2227
|
+
* `null` leaves every `<slot>` literal
|
|
1863
2228
|
* @param {ComponentPrerenderContext | null} [context] - The component registry; with one, an
|
|
1864
2229
|
* instance of a registered component inside `doc` is expanded rather than emitted bare
|
|
1865
2230
|
* @returns {string} The pre-rendered innerHTML
|
|
@@ -1867,10 +2232,10 @@ function renderComponentInstance(
|
|
|
1867
2232
|
export function preRenderComponentHtml(
|
|
1868
2233
|
doc: JxElement,
|
|
1869
2234
|
propsOverride: Record<string, JsonValue> | null = null,
|
|
1870
|
-
|
|
2235
|
+
slots: SlotFill | null = null,
|
|
1871
2236
|
context: ComponentPrerenderContext | null = null,
|
|
1872
2237
|
) {
|
|
1873
|
-
return renderInner(doc, buildInstanceScope(doc, propsOverride),
|
|
2238
|
+
return renderInner(doc, buildInstanceScope(doc, propsOverride), slots, context);
|
|
1874
2239
|
}
|
|
1875
2240
|
|
|
1876
2241
|
/**
|
|
@@ -1891,7 +2256,7 @@ export function isComponentFullyStatic(doc: JxElement) {
|
|
|
1891
2256
|
* @returns {boolean}
|
|
1892
2257
|
*/
|
|
1893
2258
|
function _isStaticNode(node: JxElement | string | (JxElement | string)[]): boolean {
|
|
1894
|
-
if (!node
|
|
2259
|
+
if (!isNonNullObject(node)) {
|
|
1895
2260
|
return true;
|
|
1896
2261
|
}
|
|
1897
2262
|
if (Array.isArray(node)) {
|
|
@@ -1910,14 +2275,13 @@ function _isStaticNode(node: JxElement | string | (JxElement | string)[]): boole
|
|
|
1910
2275
|
// Check state entries
|
|
1911
2276
|
if (node.state) {
|
|
1912
2277
|
for (const def of Object.values(node.state)) {
|
|
1913
|
-
if (!def ||
|
|
2278
|
+
if (!isNonNullObject(def) || Array.isArray(def)) {
|
|
1914
2279
|
continue;
|
|
1915
2280
|
}
|
|
1916
|
-
|
|
1917
|
-
if (d.$prototype) {
|
|
2281
|
+
if (def.$prototype) {
|
|
1918
2282
|
return false;
|
|
1919
2283
|
}
|
|
1920
|
-
if (
|
|
2284
|
+
if (def.$ref) {
|
|
1921
2285
|
return false;
|
|
1922
2286
|
}
|
|
1923
2287
|
}
|
|
@@ -1934,8 +2298,8 @@ function _isStaticNode(node: JxElement | string | (JxElement | string)[]): boole
|
|
|
1934
2298
|
if (Array.isArray(node.children)) {
|
|
1935
2299
|
return node.children.every((c) => _isStaticNode(c));
|
|
1936
2300
|
}
|
|
1937
|
-
//
|
|
1938
|
-
if (node.children
|
|
2301
|
+
// The legacy bare children slot: a mapped array builds its children at runtime.
|
|
2302
|
+
if (isMappedArray(node.children)) {
|
|
1939
2303
|
return false;
|
|
1940
2304
|
}
|
|
1941
2305
|
|
|
@@ -2015,7 +2379,7 @@ export function buildComponentCSS(
|
|
|
2015
2379
|
*/
|
|
2016
2380
|
const scope = styleScopePrefix(tagName, shadow);
|
|
2017
2381
|
|
|
2018
|
-
if (styleDef
|
|
2382
|
+
if (isNonNullObject(styleDef)) {
|
|
2019
2383
|
/* The top level is walked here rather than handed straight to the builder, because ONE of its
|
|
2020
2384
|
keys does not mean what `resolveNestedSelector` would make of it: `:host` and `:host(.foo)`
|
|
2021
2385
|
are TRANSLATED per {@link resolveSelector} so a style object means the same thing in both
|
|
@@ -2030,8 +2394,8 @@ export function buildComponentCSS(
|
|
|
2030
2394
|
not an at-rule, not a nested selector — and was dropped without reaching the builder at
|
|
2031
2395
|
all, so the one emitter that could have reported it never saw it. */
|
|
2032
2396
|
if (isRef(value)) {
|
|
2033
|
-
own[prop] = value
|
|
2034
|
-
} else if (value
|
|
2397
|
+
own[prop] = value;
|
|
2398
|
+
} else if (isNonNullObject(value) && !Array.isArray(value)) {
|
|
2035
2399
|
blocks.push([prop, value]);
|
|
2036
2400
|
} else if (!prop.startsWith("@") && !isNestedSelectorKey(prop)) {
|
|
2037
2401
|
own[prop] = value;
|