@jxsuite/compiler 5.1.1 → 5.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +224 -355
- package/dist/compiler.js +126 -257
- package/package.json +8 -8
- package/src/cli.ts +12 -14
- package/src/compiler.ts +45 -23
- package/src/shadow.ts +9 -8
- package/src/shared.ts +350 -296
- package/src/site/asset-mounts.ts +22 -12
- package/src/site/bundler.ts +38 -12
- package/src/site/client-runtime.ts +31 -17
- package/src/site/csp.ts +20 -11
- package/src/site/db-push.ts +38 -24
- package/src/site/dev-command.ts +3 -3
- package/src/site/format-host.ts +56 -45
- package/src/site/headers-emitter.ts +7 -4
- package/src/site/i18n.ts +20 -19
- package/src/site/image-cache.ts +16 -3
- package/src/site/image-optimizer.ts +19 -5
- package/src/site/image-transform.ts +56 -60
- package/src/site/img-loading.ts +11 -5
- package/src/site/layout-resolver.ts +7 -6
- package/src/site/link-relations.ts +10 -6
- package/src/site/locale-negotiation.ts +1 -1
- package/src/site/pages-discovery.ts +65 -47
- package/src/site/preview-server.ts +22 -19
- package/src/site/project-sections.ts +12 -3
- package/src/site/prototype-resolver.ts +94 -42
- package/src/site/schema-command.ts +26 -13
- package/src/site/service-worker.ts +2 -2
- package/src/site/site-build.ts +379 -262
- package/src/site/site-loader.ts +6 -3
- package/src/site/static-host.ts +3 -3
- package/src/site/validate-command.ts +64 -34
- package/src/site/well-known.ts +57 -12
- package/src/targets/compile-class.ts +17 -5
- package/src/targets/compile-client.ts +141 -106
- package/src/targets/compile-element.ts +157 -140
- package/src/targets/compile-server.ts +11 -5
- package/src/targets/compile-static.ts +57 -63
- package/src/types.ts +5 -6
package/src/shared.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
COLOR_SCHEME_ATTR,
|
|
11
11
|
COLOR_SCHEME_STORAGE_KEY,
|
|
12
|
+
MAX_COMPONENT_NESTING,
|
|
12
13
|
RESERVED_KEYS,
|
|
13
14
|
booleanAttrValue,
|
|
14
15
|
buildStyleRules,
|
|
@@ -26,30 +27,37 @@ import {
|
|
|
26
27
|
childrenContainArray,
|
|
27
28
|
hasStructuredBody,
|
|
28
29
|
isExpressionDef,
|
|
30
|
+
isBoolean,
|
|
31
|
+
isFunction,
|
|
29
32
|
isFunctionDef,
|
|
30
33
|
isMappedArray,
|
|
31
34
|
isNamedFormulaDef,
|
|
35
|
+
isNonNullObject,
|
|
36
|
+
isNumber,
|
|
32
37
|
isPrototypeDef,
|
|
33
38
|
isRef,
|
|
34
|
-
isSchemaOnlyDef
|
|
39
|
+
isSchemaOnlyDef,
|
|
35
40
|
isServerFnDef,
|
|
41
|
+
isString,
|
|
36
42
|
isTagExpression,
|
|
37
43
|
isTemplateString,
|
|
38
44
|
paramNames,
|
|
39
45
|
tagNameCandidates,
|
|
40
46
|
} from "@jxsuite/schema/guards";
|
|
47
|
+
import type { TagNameSlot } from "@jxsuite/schema/guards";
|
|
41
48
|
import { resolveShadowMode, styleScopePrefix } from "./shadow.ts";
|
|
42
49
|
import type { ShadowMode } from "./shadow.ts";
|
|
43
|
-
import type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
44
50
|
import { readPath } from "@jxsuite/runtime/pointer";
|
|
51
|
+
import type { JxScope } from "@jxsuite/runtime/types";
|
|
45
52
|
import type {
|
|
53
|
+
CemParameter,
|
|
46
54
|
JsonValue,
|
|
55
|
+
JxDocumentValue,
|
|
47
56
|
JxElement,
|
|
48
57
|
JxMutableNode,
|
|
49
58
|
JxPrototypeDef,
|
|
50
59
|
JxRef,
|
|
51
60
|
JxStateDefinition,
|
|
52
|
-
JxStateObject,
|
|
53
61
|
JxStyle,
|
|
54
62
|
ProjectConfig,
|
|
55
63
|
} from "@jxsuite/schema/types";
|
|
@@ -80,22 +88,51 @@ export { compileStatements, runStatements } from "@jxsuite/runtime/statements";
|
|
|
80
88
|
* declared parameter names (honoring CemParameter defaults) as the `_args` object the compiled
|
|
81
89
|
* body's `$args/` refs read from. Call sites stay positional, matching the interpreter.
|
|
82
90
|
*/
|
|
83
|
-
export function emitFormulaFn(
|
|
91
|
+
export function emitFormulaFn(
|
|
92
|
+
def: { parameters?: readonly FormulaParameter[] | undefined },
|
|
93
|
+
compiledBody: string,
|
|
94
|
+
): string {
|
|
84
95
|
const entries: string[] = [];
|
|
85
96
|
for (const [i, p] of (def.parameters ?? []).entries()) {
|
|
86
|
-
const name =
|
|
97
|
+
const name = formulaParameterName(p);
|
|
87
98
|
if (!name) {
|
|
88
99
|
continue;
|
|
89
100
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
? `_a[${i}] === undefined ? ${JSON.stringify((p as { default?: unknown }).default)} : _a[${i}]`
|
|
101
|
+
const arg = hasParameterDefault(p)
|
|
102
|
+
? `_a[${i}] === undefined ? ${JSON.stringify(p.default)} : _a[${i}]`
|
|
93
103
|
: `_a[${i}]`;
|
|
94
104
|
entries.push(`${JSON.stringify(name)}: ${arg}`);
|
|
95
105
|
}
|
|
96
106
|
return `(..._a) => { const _args = { ${entries.join(", ")} }; return ${compiledBody}; }`;
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
/**
|
|
110
|
+
* One declared formula parameter as a document holds it: a bare name or a CEM parameter object. A
|
|
111
|
+
* hand-edited list may also carry a `null` hole, which both the emitted and the build-time callable
|
|
112
|
+
* skip like an unnamed entry.
|
|
113
|
+
*/
|
|
114
|
+
type FormulaParameter = string | CemParameter | null;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The name a formula parameter binds under `$args/`, or "" for an entry that names nothing.
|
|
118
|
+
*
|
|
119
|
+
* @param {FormulaParameter} p
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
function formulaParameterName(p: FormulaParameter): string {
|
|
123
|
+
return isString(p) ? p : (p?.name ?? "");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether a formula parameter declares a `default`, which fills an argument the call site omits.
|
|
128
|
+
*
|
|
129
|
+
* @param {FormulaParameter} p
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
function hasParameterDefault(p: FormulaParameter): p is CemParameter {
|
|
133
|
+
return isNonNullObject(p) && "default" in p;
|
|
134
|
+
}
|
|
135
|
+
|
|
99
136
|
/**
|
|
100
137
|
* Emit the import-clause binding for one Function-def `$src` entry.
|
|
101
138
|
*
|
|
@@ -109,10 +146,10 @@ export function emitFormulaFn(def: { parameters?: unknown[] }, compiledBody: str
|
|
|
109
146
|
* only reachable as `default as key`.
|
|
110
147
|
*
|
|
111
148
|
* @param {string} key - State key, and the local binding name
|
|
112
|
-
* @param {
|
|
149
|
+
* @param {SrcExportOwner} def - The Function definition
|
|
113
150
|
* @returns {string}
|
|
114
151
|
*/
|
|
115
|
-
export function srcImportBinding(key: string, def:
|
|
152
|
+
export function srcImportBinding(key: string, def: SrcExportOwner) {
|
|
116
153
|
const exportName = srcExportName(key, def);
|
|
117
154
|
return exportName === key ? key : `${exportName} as ${key}`;
|
|
118
155
|
}
|
|
@@ -125,14 +162,23 @@ export function srcImportBinding(key: string, def: unknown) {
|
|
|
125
162
|
* to, where the local name is chosen by the generated code and only the export name is fixed.
|
|
126
163
|
*
|
|
127
164
|
* @param {string} key - State key
|
|
128
|
-
* @param {
|
|
165
|
+
* @param {SrcExportOwner} def - The Function definition
|
|
129
166
|
* @returns {string}
|
|
130
167
|
*/
|
|
131
|
-
export function srcExportName(key: string, def:
|
|
132
|
-
const declared =
|
|
133
|
-
return
|
|
168
|
+
export function srcExportName(key: string, def: SrcExportOwner): string {
|
|
169
|
+
const declared = def?.$export;
|
|
170
|
+
return isString(declared) && declared !== "" ? declared : key;
|
|
134
171
|
}
|
|
135
172
|
|
|
173
|
+
/**
|
|
174
|
+
* The Function definition a `$src` export is named on, as the document holds it. `$export` is read
|
|
175
|
+
* unchecked from document data, so anything but a non-empty string falls back to the state key.
|
|
176
|
+
*/
|
|
177
|
+
type SrcExportOwner =
|
|
178
|
+
| { $export?: JxDocumentValue | undefined; [key: string]: JxDocumentValue | undefined }
|
|
179
|
+
| null
|
|
180
|
+
| undefined;
|
|
181
|
+
|
|
136
182
|
/**
|
|
137
183
|
* Emit the auto-fetch `effect()` for a `$prototype: "Request"` state entry.
|
|
138
184
|
*
|
|
@@ -173,7 +219,7 @@ export function emitRequestFetch(
|
|
|
173
219
|
|
|
174
220
|
if (isTemplateUrl) {
|
|
175
221
|
lines.push(
|
|
176
|
-
` const url = \`${withStatePrefix(url
|
|
222
|
+
` const url = \`${withStatePrefix(url, statePrefix)}\`;`,
|
|
177
223
|
' if (!url || url === "undefined" || url.includes("undefined")) return;',
|
|
178
224
|
);
|
|
179
225
|
} else {
|
|
@@ -188,10 +234,9 @@ export function emitRequestFetch(
|
|
|
188
234
|
fetchOpts.push(`headers: ${JSON.stringify(def.headers)}`);
|
|
189
235
|
}
|
|
190
236
|
if (def.body) {
|
|
191
|
-
const bodyStr =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
: JSON.stringify(def.body);
|
|
237
|
+
const bodyStr = isNonNullObject(def.body)
|
|
238
|
+
? JSON.stringify(JSON.stringify(def.body))
|
|
239
|
+
: JSON.stringify(def.body);
|
|
195
240
|
fetchOpts.push(`body: ${bodyStr}`);
|
|
196
241
|
}
|
|
197
242
|
|
|
@@ -229,7 +274,6 @@ function withStatePrefix(str: string, statePrefix: string) {
|
|
|
229
274
|
`\${${expr.replaceAll(/(?<!this\.)\bstate\./g, `${statePrefix}.`)}}`,
|
|
230
275
|
);
|
|
231
276
|
}
|
|
232
|
-
export type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
233
277
|
|
|
234
278
|
/**
|
|
235
279
|
* Non-enumerable marker on a build-time scope listing the state keys that hold no build-time value.
|
|
@@ -267,7 +311,7 @@ export function isClassJsonSrc(src?: unknown): src is string {
|
|
|
267
311
|
* @returns {boolean}
|
|
268
312
|
*/
|
|
269
313
|
export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
270
|
-
if (!def
|
|
314
|
+
if (!isNonNullObject(def)) {
|
|
271
315
|
return false;
|
|
272
316
|
}
|
|
273
317
|
|
|
@@ -277,25 +321,20 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
277
321
|
if (k === "$site" || k === "$page") {
|
|
278
322
|
continue;
|
|
279
323
|
}
|
|
324
|
+
if (!isNonNullObject(d) || Array.isArray(d)) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
280
327
|
// 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
|
-
) {
|
|
328
|
+
if (d.timing === "compiler") {
|
|
287
329
|
continue;
|
|
288
330
|
}
|
|
289
|
-
if (
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
if ((d as JxPrototypeDef).$prototype) {
|
|
331
|
+
if (d.$prototype) {
|
|
293
332
|
return true;
|
|
294
333
|
}
|
|
295
|
-
if ("default" in
|
|
334
|
+
if ("default" in d) {
|
|
296
335
|
return true;
|
|
297
336
|
}
|
|
298
|
-
if (
|
|
337
|
+
if (isSchemaOnlyDef(d)) {
|
|
299
338
|
continue;
|
|
300
339
|
}
|
|
301
340
|
return true;
|
|
@@ -317,11 +356,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
317
356
|
if (RESERVED_KEYS.has(key)) {
|
|
318
357
|
continue;
|
|
319
358
|
}
|
|
320
|
-
if (
|
|
321
|
-
val !== null &&
|
|
322
|
-
typeof val === "object" &&
|
|
323
|
-
typeof (val as JxMutableNode).$ref === "string"
|
|
324
|
-
) {
|
|
359
|
+
if (isRef(val)) {
|
|
325
360
|
return true;
|
|
326
361
|
}
|
|
327
362
|
if (isTemplateString(val)) {
|
|
@@ -329,7 +364,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
329
364
|
}
|
|
330
365
|
}
|
|
331
366
|
|
|
332
|
-
if (def.style
|
|
367
|
+
if (isNonNullObject(def.style)) {
|
|
333
368
|
for (const val of Object.values(def.style)) {
|
|
334
369
|
if (isTemplateString(val)) {
|
|
335
370
|
return true;
|
|
@@ -337,7 +372,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
337
372
|
}
|
|
338
373
|
}
|
|
339
374
|
|
|
340
|
-
if (def.attributes
|
|
375
|
+
if (isNonNullObject(def.attributes)) {
|
|
341
376
|
for (const val of Object.values(def.attributes)) {
|
|
342
377
|
if (isTemplateString(val)) {
|
|
343
378
|
return true;
|
|
@@ -355,7 +390,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
355
390
|
* @returns {boolean}
|
|
356
391
|
*/
|
|
357
392
|
export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
358
|
-
if (!def
|
|
393
|
+
if (!isNonNullObject(def)) {
|
|
359
394
|
return false;
|
|
360
395
|
}
|
|
361
396
|
|
|
@@ -370,11 +405,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
370
405
|
if (RESERVED_KEYS.has(key)) {
|
|
371
406
|
continue;
|
|
372
407
|
}
|
|
373
|
-
if (
|
|
374
|
-
val !== null &&
|
|
375
|
-
typeof val === "object" &&
|
|
376
|
-
typeof (val as JxMutableNode).$ref === "string"
|
|
377
|
-
) {
|
|
408
|
+
if (isRef(val)) {
|
|
378
409
|
return true;
|
|
379
410
|
}
|
|
380
411
|
if (isTemplateString(val)) {
|
|
@@ -382,7 +413,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
382
413
|
}
|
|
383
414
|
}
|
|
384
415
|
|
|
385
|
-
if (def.style
|
|
416
|
+
if (isNonNullObject(def.style)) {
|
|
386
417
|
for (const val of Object.values(def.style)) {
|
|
387
418
|
if (isTemplateString(val)) {
|
|
388
419
|
return true;
|
|
@@ -390,7 +421,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
390
421
|
}
|
|
391
422
|
}
|
|
392
423
|
|
|
393
|
-
if (def.attributes
|
|
424
|
+
if (isNonNullObject(def.attributes)) {
|
|
394
425
|
for (const val of Object.values(def.attributes)) {
|
|
395
426
|
if (isTemplateString(val)) {
|
|
396
427
|
return true;
|
|
@@ -408,7 +439,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
|
|
|
408
439
|
* @returns {boolean}
|
|
409
440
|
*/
|
|
410
441
|
export function hasAnyIsland(def: JxElement | JxMutableNode | string): boolean {
|
|
411
|
-
if (!def
|
|
442
|
+
if (!isNonNullObject(def)) {
|
|
412
443
|
return false;
|
|
413
444
|
}
|
|
414
445
|
if (isDynamic(def)) {
|
|
@@ -422,33 +453,60 @@ export function hasAnyIsland(def: JxElement | JxMutableNode | string): boolean {
|
|
|
422
453
|
|
|
423
454
|
// ─── Scope / value resolution ─────────────────────────────────────────────────
|
|
424
455
|
|
|
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
|
-
*/
|
|
456
|
+
/** What a compile target threads down the tree: the scope in effect, its definitions and `$media`. */
|
|
432
457
|
export interface CompileContext {
|
|
433
|
-
scope:
|
|
434
|
-
|
|
458
|
+
scope: JxScope;
|
|
459
|
+
/** The `state` definitions the scope was built from. */
|
|
460
|
+
scopeDefs: Record<string, JxStateDefinition>;
|
|
435
461
|
media: Record<string, string>;
|
|
436
462
|
/** True inside a `<pre>`-like subtree, where emitters must not indent nested children. */
|
|
437
463
|
preformatted?: boolean;
|
|
438
464
|
}
|
|
439
465
|
|
|
466
|
+
/**
|
|
467
|
+
* @param {JxElement | JxMutableNode | null} raw
|
|
468
|
+
* @param {JxScope | null} [parentScope]
|
|
469
|
+
* @param {Record<string, JxStateDefinition>} [scopeDefs]
|
|
470
|
+
* @param {Record<string, string>} [media]
|
|
471
|
+
* @returns {CompileContext}
|
|
472
|
+
*/
|
|
440
473
|
export function createCompileContext(
|
|
441
474
|
raw: JxElement | JxMutableNode | null,
|
|
442
|
-
parentScope:
|
|
443
|
-
scopeDefs: Record<string,
|
|
475
|
+
parentScope: JxScope | null = null,
|
|
476
|
+
scopeDefs: Record<string, JxStateDefinition> = {},
|
|
444
477
|
media: Record<string, string> = {},
|
|
445
478
|
): CompileContext {
|
|
446
|
-
const scope
|
|
479
|
+
const scope = raw?.state
|
|
447
480
|
? buildInitialScope(raw.state, parentScope)
|
|
448
|
-
: (parentScope ?? (
|
|
481
|
+
: (parentScope ?? emptyScope());
|
|
449
482
|
return { media, scope, scopeDefs };
|
|
450
483
|
}
|
|
451
484
|
|
|
485
|
+
/**
|
|
486
|
+
* A fresh scope inheriting from `parent`, or from nothing at all, so no `Object.prototype` member
|
|
487
|
+
* can answer for an undeclared state key.
|
|
488
|
+
*
|
|
489
|
+
* @param {JxScope | null} [parent]
|
|
490
|
+
* @returns {JxScope}
|
|
491
|
+
*/
|
|
492
|
+
function emptyScope(parent: JxScope | null = null): JxScope {
|
|
493
|
+
/* SAFETY: `Object.create` returns a new object with no own keys whose prototype is `parent` (a
|
|
494
|
+
scope) or null, so every key it answers is a scope member or absent — exactly a JxScope. */
|
|
495
|
+
return Object.create(parent) as JxScope;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* The runtime-only marks `buildInitialScope` attached to `scope` or to a scope it inherits from.
|
|
500
|
+
*
|
|
501
|
+
* @param {JxScope} scope
|
|
502
|
+
* @returns {Set<string> | undefined}
|
|
503
|
+
*/
|
|
504
|
+
function runtimeOnlyKeysOf(scope: JxScope): Set<string> | undefined {
|
|
505
|
+
/* SAFETY: `RUNTIME_ONLY_KEYS` is module-private and `buildInitialScope` is the only writer, which
|
|
506
|
+
always stores a `Set<string>` under it; a scope built anywhere else answers undefined. */
|
|
507
|
+
return (scope as { [RUNTIME_ONLY_KEYS]?: Set<string> })[RUNTIME_ONLY_KEYS];
|
|
508
|
+
}
|
|
509
|
+
|
|
452
510
|
/**
|
|
453
511
|
* `state.x = …`, `state.x += …`, `state.x++` — every form a string handler body can use to write a
|
|
454
512
|
* state entry. `===`/`!==`/`<=`/`>=` are excluded by the `(?!=)` guard and by the operator set.
|
|
@@ -469,36 +527,43 @@ const STATE_MUTATING_METHOD_RE =
|
|
|
469
527
|
* distinguishes it from a constant — but if a handler assigns to it, interpolating it at build time
|
|
470
528
|
* replaces the template in the emitted output and the element is dead for the life of the page.
|
|
471
529
|
*
|
|
472
|
-
* @param {
|
|
530
|
+
* @param {JxDocumentValue | undefined} node
|
|
473
531
|
* @param {Set<string>} into
|
|
474
532
|
*/
|
|
475
|
-
function collectAssignedStateKeys(node:
|
|
476
|
-
if (
|
|
533
|
+
function collectAssignedStateKeys(node: JxDocumentValue | undefined, into: Set<string>) {
|
|
534
|
+
if (isString(node)) {
|
|
477
535
|
for (const m of node.matchAll(STATE_ASSIGNMENT_RE)) {
|
|
536
|
+
// SAFETY: the pattern's one capture group is outside any optional part, so a match has it.
|
|
478
537
|
into.add(m[1] as string);
|
|
479
538
|
}
|
|
480
539
|
for (const m of node.matchAll(STATE_MUTATING_METHOD_RE)) {
|
|
540
|
+
// SAFETY: the pattern's one capture group is outside any optional part, so a match has it.
|
|
481
541
|
into.add(m[1] as string);
|
|
482
542
|
}
|
|
483
543
|
return;
|
|
484
544
|
}
|
|
485
|
-
if (!node || typeof node !== "object") {
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
545
|
if (Array.isArray(node)) {
|
|
489
546
|
for (const entry of node) {
|
|
490
547
|
collectAssignedStateKeys(entry, into);
|
|
491
548
|
}
|
|
492
549
|
return;
|
|
493
550
|
}
|
|
494
|
-
|
|
551
|
+
if (!isNonNullObject(node)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
/* SAFETY: `node` is a non-array object inside a handler body or an expression, so it is a
|
|
555
|
+
document object, and every member of a document object is itself a document value. */
|
|
556
|
+
const rec = node as Record<string, JxDocumentValue | undefined>;
|
|
495
557
|
// A mutating expression names its destination as `target: { $ref: "#/state/x" }`.
|
|
496
|
-
const
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
558
|
+
const { operator, target } = rec;
|
|
559
|
+
if (
|
|
560
|
+
isString(operator) &&
|
|
561
|
+
isMutating(operator) &&
|
|
562
|
+
isRef(target) &&
|
|
563
|
+
target.$ref.startsWith("#/state/")
|
|
564
|
+
) {
|
|
565
|
+
// SAFETY: `String.prototype.split` always returns at least one element.
|
|
566
|
+
into.add(target.$ref.slice("#/state/".length).split("/")[0] as string);
|
|
502
567
|
}
|
|
503
568
|
for (const value of Object.values(rec)) {
|
|
504
569
|
collectAssignedStateKeys(value, into);
|
|
@@ -507,18 +572,18 @@ function collectAssignedStateKeys(node: unknown, into: Set<string>) {
|
|
|
507
572
|
|
|
508
573
|
/**
|
|
509
574
|
* @param {Record<string, JxStateDefinition>} [defs]
|
|
510
|
-
* @param {
|
|
511
|
-
* @returns {
|
|
575
|
+
* @param {JxScope | null} [parentScope]
|
|
576
|
+
* @returns {JxScope}
|
|
512
577
|
*/
|
|
513
578
|
export function buildInitialScope(
|
|
514
579
|
defs: Record<string, JxStateDefinition> = {},
|
|
515
|
-
parentScope:
|
|
580
|
+
parentScope: JxScope | null = null,
|
|
516
581
|
) {
|
|
517
|
-
const scope =
|
|
582
|
+
const scope = emptyScope(parentScope);
|
|
518
583
|
// Keys whose real value only exists after hydration, so prerender must not interpolate them —
|
|
519
584
|
// See `readsRuntimeOnlyState`. Seeded from the parent so nested scopes inherit the marks.
|
|
520
|
-
const inherited =
|
|
521
|
-
const runtimeOnly = new Set<string>(inherited
|
|
585
|
+
const inherited = parentScope ? runtimeOnlyKeysOf(parentScope) : undefined;
|
|
586
|
+
const runtimeOnly = new Set<string>(inherited);
|
|
522
587
|
// Attached up front, and mutated in place by the passes below, so the template pass at the end can
|
|
523
588
|
// Consult the marks the earlier passes added.
|
|
524
589
|
Object.defineProperty(scope, RUNTIME_ONLY_KEYS, {
|
|
@@ -534,12 +599,8 @@ export function buildInitialScope(
|
|
|
534
599
|
// Builder cannot open, so a write that only happens there is still missed (issue #125).
|
|
535
600
|
const assigned = new Set<string>();
|
|
536
601
|
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
|
-
);
|
|
602
|
+
if (isFunctionDef(def) || isExpressionDef(def)) {
|
|
603
|
+
collectAssignedStateKeys(def.body ?? def.$expression, assigned);
|
|
543
604
|
}
|
|
544
605
|
}
|
|
545
606
|
for (const key of assigned) {
|
|
@@ -549,17 +610,16 @@ export function buildInitialScope(
|
|
|
549
610
|
}
|
|
550
611
|
|
|
551
612
|
for (const [key, def] of Object.entries(defs)) {
|
|
552
|
-
if (
|
|
613
|
+
if (!isNonNullObject(def) || Array.isArray(def)) {
|
|
553
614
|
setOwnScopeValue(scope, key, cloneValue(def));
|
|
554
615
|
continue;
|
|
555
616
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
setOwnScopeValue(scope, key, cloneValue(d.default));
|
|
617
|
+
if ("default" in def) {
|
|
618
|
+
setOwnScopeValue(scope, key, cloneValue(def.default));
|
|
559
619
|
continue;
|
|
560
620
|
}
|
|
561
|
-
if (!
|
|
562
|
-
setOwnScopeValue(scope, key, cloneValue(
|
|
621
|
+
if (!def.$prototype && !("$expression" in def) && !isSchemaOnlyDef(def)) {
|
|
622
|
+
setOwnScopeValue(scope, key, cloneValue(def));
|
|
563
623
|
}
|
|
564
624
|
}
|
|
565
625
|
|
|
@@ -572,34 +632,32 @@ export function buildInitialScope(
|
|
|
572
632
|
const computedBodies: [string, string][] = [];
|
|
573
633
|
|
|
574
634
|
for (const [key, def] of Object.entries(defs)) {
|
|
575
|
-
if (
|
|
635
|
+
if (isTemplateString(def)) {
|
|
576
636
|
templateDefs.push([key, def]);
|
|
577
637
|
continue;
|
|
578
638
|
}
|
|
579
|
-
if (!def
|
|
639
|
+
if (!isNonNullObject(def)) {
|
|
580
640
|
continue;
|
|
581
641
|
}
|
|
582
642
|
if (isExpressionDef(def)) {
|
|
583
|
-
const node = def.$expression
|
|
643
|
+
const node = def.$expression;
|
|
584
644
|
if (isNamedFormulaDef(def)) {
|
|
585
645
|
// Named formula: callable, positional args mapped onto its declared parameters.
|
|
586
|
-
const params = def.parameters
|
|
587
|
-
setOwnScopeValue(scope, key, (...argValues:
|
|
588
|
-
const args:
|
|
646
|
+
const params: readonly FormulaParameter[] = def.parameters;
|
|
647
|
+
setOwnScopeValue(scope, key, (...argValues: JxScope[string][]) => {
|
|
648
|
+
const args: JxScope = {};
|
|
589
649
|
for (const [i, p] of params.entries()) {
|
|
590
|
-
const name =
|
|
650
|
+
const name = formulaParameterName(p);
|
|
591
651
|
if (!name) {
|
|
592
652
|
continue;
|
|
593
653
|
}
|
|
594
654
|
args[name] =
|
|
595
|
-
argValues[i] === undefined &&
|
|
596
|
-
? p.default
|
|
597
|
-
: argValues[i];
|
|
655
|
+
argValues[i] === undefined && hasParameterDefault(p) ? p.default : argValues[i];
|
|
598
656
|
}
|
|
599
657
|
return evaluateExpression(node, scope, null, { args });
|
|
600
658
|
});
|
|
601
659
|
} else if (isMutating(node.operator)) {
|
|
602
|
-
setOwnScopeValue(scope, key, (s:
|
|
660
|
+
setOwnScopeValue(scope, key, (s: JxScope, event: Event) =>
|
|
603
661
|
evaluateExpression(node, s, event),
|
|
604
662
|
);
|
|
605
663
|
} else {
|
|
@@ -609,12 +667,13 @@ export function buildInitialScope(
|
|
|
609
667
|
}
|
|
610
668
|
if (isFunctionDef(def)) {
|
|
611
669
|
if (hasStructuredBody(def)) {
|
|
612
|
-
// Structured body (spec §20): a side-effecting handler in the build-time scope.
|
|
670
|
+
// Structured body (spec §20): a side-effecting handler in the build-time scope. Build-time
|
|
671
|
+
// Code may call it bare, so the scope it runs against falls back to this one.
|
|
613
672
|
const { body } = def;
|
|
614
|
-
setOwnScopeValue(scope, key, (s
|
|
673
|
+
setOwnScopeValue(scope, key, (s?: JxScope, event?: Event) => {
|
|
615
674
|
void runStatements(body, s ?? scope, event ?? null);
|
|
616
675
|
});
|
|
617
|
-
} else if (
|
|
676
|
+
} else if (isString(def.body)) {
|
|
618
677
|
const declared = def.parameters ? paramNames(def.parameters) : (def.arguments ?? []);
|
|
619
678
|
const { body } = def;
|
|
620
679
|
// Declared names bind by name, not by position (spec.md §5.3 4d): a parameter literally
|
|
@@ -623,8 +682,13 @@ export function buildInitialScope(
|
|
|
623
682
|
// The older behaviour here — produced a duplicate parameter for the `["state", "event"]`
|
|
624
683
|
// Form that examples/components/task-manager.json uses, leaving `state` undefined.
|
|
625
684
|
const params = declared.includes("state") ? declared : ["state", ...declared];
|
|
626
|
-
|
|
627
|
-
|
|
685
|
+
/* SAFETY: `new Function` returns an ordinary function of exactly `params`; `invoke` below is
|
|
686
|
+
its only caller and passes the scope or the event in each position, and what the body
|
|
687
|
+
returns is one more scope member. */
|
|
688
|
+
const fn = new Function(...params, body) as (
|
|
689
|
+
...args: (JxScope | Event | undefined)[]
|
|
690
|
+
) => JxScope[string];
|
|
691
|
+
const invoke = (state: JxScope, event?: Event) =>
|
|
628
692
|
fn(...params.map((name) => (name === "state" ? state : event)));
|
|
629
693
|
if (bodyReturnsValue(body)) {
|
|
630
694
|
defineLazyScopeValue(scope, key, () => invoke(scope));
|
|
@@ -677,11 +741,11 @@ export function buildInitialScope(
|
|
|
677
741
|
}
|
|
678
742
|
|
|
679
743
|
/**
|
|
680
|
-
* @param {
|
|
744
|
+
* @param {JxScope} scope
|
|
681
745
|
* @param {string} key
|
|
682
|
-
* @param {
|
|
746
|
+
* @param {JxScope[string]} value - Any scope member: document data or a build-time callable
|
|
683
747
|
*/
|
|
684
|
-
export function setOwnScopeValue(scope:
|
|
748
|
+
export function setOwnScopeValue(scope: JxScope, key: string, value: JxScope[string]) {
|
|
685
749
|
Object.defineProperty(scope, key, {
|
|
686
750
|
configurable: true,
|
|
687
751
|
enumerable: true,
|
|
@@ -691,15 +755,11 @@ export function setOwnScopeValue(scope: Record<string, unknown>, key: string, va
|
|
|
691
755
|
}
|
|
692
756
|
|
|
693
757
|
/**
|
|
694
|
-
* @param {
|
|
758
|
+
* @param {JxScope} scope
|
|
695
759
|
* @param {string} key
|
|
696
|
-
* @param {() =>
|
|
760
|
+
* @param {() => JxScope[string]} getter - Computes the member on read
|
|
697
761
|
*/
|
|
698
|
-
export function defineLazyScopeValue(
|
|
699
|
-
scope: Record<string, unknown>,
|
|
700
|
-
key: string,
|
|
701
|
-
getter: () => unknown,
|
|
702
|
-
) {
|
|
762
|
+
export function defineLazyScopeValue(scope: JxScope, key: string, getter: () => JxScope[string]) {
|
|
703
763
|
Object.defineProperty(scope, key, {
|
|
704
764
|
configurable: true,
|
|
705
765
|
enumerable: true,
|
|
@@ -708,16 +768,17 @@ export function defineLazyScopeValue(
|
|
|
708
768
|
}
|
|
709
769
|
|
|
710
770
|
/**
|
|
711
|
-
* @param {
|
|
712
|
-
*
|
|
771
|
+
* @param {JxDocumentValue | undefined} value - A bindable document value: a `$ref`, a template
|
|
772
|
+
* string, or a literal returned as is
|
|
773
|
+
* @param {JxScope | null} scope
|
|
713
774
|
* @returns {unknown}
|
|
714
775
|
*/
|
|
715
|
-
export function resolveStaticValue(value:
|
|
716
|
-
if (
|
|
717
|
-
return resolveRefValue(
|
|
776
|
+
export function resolveStaticValue(value: JxDocumentValue | undefined, scope: JxScope | null) {
|
|
777
|
+
if (isRef(value)) {
|
|
778
|
+
return resolveRefValue(value.$ref, scope!);
|
|
718
779
|
}
|
|
719
780
|
if (isTemplateString(value)) {
|
|
720
|
-
return evaluateStaticTemplate(value
|
|
781
|
+
return evaluateStaticTemplate(value, scope);
|
|
721
782
|
}
|
|
722
783
|
return value;
|
|
723
784
|
}
|
|
@@ -731,18 +792,22 @@ export function isRefObject(value: unknown): value is JxRef {
|
|
|
731
792
|
}
|
|
732
793
|
|
|
733
794
|
/**
|
|
734
|
-
* @param {
|
|
735
|
-
*
|
|
795
|
+
* @param {JsonValue | undefined} refValue - A `$ref` member as the document holds it; anything but
|
|
796
|
+
* a string is returned as is
|
|
797
|
+
* @param {JxScope} scope
|
|
736
798
|
* @returns {unknown}
|
|
737
799
|
*/
|
|
738
|
-
export function resolveRefValue(refValue:
|
|
739
|
-
if (
|
|
800
|
+
export function resolveRefValue(refValue: JsonValue | undefined, scope: JxScope) {
|
|
801
|
+
if (!isString(refValue)) {
|
|
740
802
|
return refValue;
|
|
741
803
|
}
|
|
742
804
|
if (refValue.startsWith("$map/")) {
|
|
743
805
|
const parts = refValue.split("/");
|
|
744
806
|
const key = parts[1]!;
|
|
745
|
-
|
|
807
|
+
/* SAFETY: `?.` stops at null and undefined, and every other value answers a member read by
|
|
808
|
+
name; the member comes back as a scope member, so the view claims nothing about its type. */
|
|
809
|
+
const row = scope.$map as JxScope | null | undefined;
|
|
810
|
+
const base = row?.[key] ?? scope[`$map/${key}`];
|
|
746
811
|
return parts.length > 2 ? getPathValue(base, parts.slice(2).join("/")) : base;
|
|
747
812
|
}
|
|
748
813
|
if (refValue.startsWith("#/state/")) {
|
|
@@ -770,31 +835,32 @@ export function resolveRefValue(refValue: unknown, scope: Record<string, unknown
|
|
|
770
835
|
* formulas (spec.md §19.4c) are meant to be used during prerender.
|
|
771
836
|
*
|
|
772
837
|
* @param {string} str
|
|
773
|
-
* @param {
|
|
838
|
+
* @param {JxScope | null} scope
|
|
774
839
|
* @returns {boolean}
|
|
775
840
|
*/
|
|
776
|
-
function readsRuntimeOnlyState(str: string, scope:
|
|
841
|
+
function readsRuntimeOnlyState(str: string, scope: JxScope | null) {
|
|
777
842
|
if (!scope) {
|
|
778
843
|
return false;
|
|
779
844
|
}
|
|
780
|
-
const runtimeOnly = (scope
|
|
845
|
+
const runtimeOnly = runtimeOnlyKeysOf(scope);
|
|
781
846
|
for (const match of str.matchAll(/\bstate\.([A-Za-z_$][\w$]*)\s*(\()?/g)) {
|
|
782
847
|
if (match[2]) {
|
|
783
848
|
continue;
|
|
784
849
|
}
|
|
850
|
+
// SAFETY: capture group 1 is outside any optional part of the pattern, so a match has it.
|
|
785
851
|
const key = match[1] as string;
|
|
786
|
-
if (runtimeOnly
|
|
852
|
+
if (runtimeOnly?.has(key)) {
|
|
787
853
|
return true;
|
|
788
854
|
}
|
|
789
855
|
// Reading the key can run a computed's getter, which is free to throw on partial build-time
|
|
790
856
|
// Data. A throw says nothing about whether the entry is runtime-only, so it is not a mark.
|
|
791
|
-
let value:
|
|
857
|
+
let value: JxScope[string];
|
|
792
858
|
try {
|
|
793
859
|
value = scope[key];
|
|
794
860
|
} catch {
|
|
795
861
|
continue;
|
|
796
862
|
}
|
|
797
|
-
if (
|
|
863
|
+
if (isFunction(value)) {
|
|
798
864
|
return true;
|
|
799
865
|
}
|
|
800
866
|
}
|
|
@@ -803,10 +869,10 @@ function readsRuntimeOnlyState(str: string, scope: Record<string, unknown>) {
|
|
|
803
869
|
|
|
804
870
|
/**
|
|
805
871
|
* @param {string} str
|
|
806
|
-
* @param {
|
|
872
|
+
* @param {JxScope | null} scope
|
|
807
873
|
* @returns {unknown}
|
|
808
874
|
*/
|
|
809
|
-
export function evaluateStaticTemplate(str: string, scope:
|
|
875
|
+
export function evaluateStaticTemplate(str: string, scope: JxScope | null) {
|
|
810
876
|
if (readsRuntimeOnlyState(str, scope)) {
|
|
811
877
|
return null;
|
|
812
878
|
}
|
|
@@ -822,7 +888,9 @@ export function evaluateStaticTemplate(str: string, scope: Record<string, unknow
|
|
|
822
888
|
const values = [scope, scope?.$map, scope?.$site, scope?.$page];
|
|
823
889
|
try {
|
|
824
890
|
const body = isSingleExpression(str) ? `return (${str.slice(2, -1)})` : `return \`${str}\``;
|
|
825
|
-
|
|
891
|
+
/* SAFETY: `new Function` compiles exactly the four parameters in `args`, and its body returns a
|
|
892
|
+
template's value, which is whatever the scope it reads holds: one more scope member. */
|
|
893
|
+
const fn = new Function(...args, body) as (...a: JxScope[string][]) => JxScope[string];
|
|
826
894
|
return fn(...values);
|
|
827
895
|
} catch {
|
|
828
896
|
return null;
|
|
@@ -830,20 +898,24 @@ export function evaluateStaticTemplate(str: string, scope: Record<string, unknow
|
|
|
830
898
|
}
|
|
831
899
|
|
|
832
900
|
/**
|
|
833
|
-
* @param {
|
|
901
|
+
* @param {JxScope[string]} base - A scope, or any member read from one
|
|
834
902
|
* @param {string} path
|
|
835
903
|
* @returns {unknown}
|
|
836
904
|
*/
|
|
837
|
-
export function getPathValue(base:
|
|
905
|
+
export function getPathValue(base: JxScope[string], path: string) {
|
|
838
906
|
return readPath(base, path);
|
|
839
907
|
}
|
|
840
908
|
|
|
841
909
|
/**
|
|
842
|
-
*
|
|
843
|
-
*
|
|
910
|
+
* A deep copy of an object, so a scope never aliases the document it was built from; anything else
|
|
911
|
+
* is returned as is.
|
|
912
|
+
*
|
|
913
|
+
* @template T
|
|
914
|
+
* @param {T} value
|
|
915
|
+
* @returns {T}
|
|
844
916
|
*/
|
|
845
|
-
export function cloneValue(value:
|
|
846
|
-
if (value
|
|
917
|
+
export function cloneValue<T>(value: T): T {
|
|
918
|
+
if (!isNonNullObject(value)) {
|
|
847
919
|
return value;
|
|
848
920
|
}
|
|
849
921
|
return structuredClone(value);
|
|
@@ -857,12 +929,12 @@ export function cloneValue(value: unknown) {
|
|
|
857
929
|
* class handle), so only a per-instance value is written on the element.
|
|
858
930
|
*
|
|
859
931
|
* @param {JxStyle | null | undefined} style
|
|
860
|
-
* @param {
|
|
932
|
+
* @param {JxScope | null} scope
|
|
861
933
|
* @returns {string} `prop: value; prop: value`, or "" when nothing resolves
|
|
862
934
|
*/
|
|
863
935
|
export function inlineStyleDeclarations(
|
|
864
936
|
style: JxStyle | null | undefined,
|
|
865
|
-
scope:
|
|
937
|
+
scope: JxScope | null,
|
|
866
938
|
): string {
|
|
867
939
|
if (!style || !scope) {
|
|
868
940
|
return "";
|
|
@@ -875,9 +947,6 @@ export function inlineStyleDeclarations(
|
|
|
875
947
|
!k.startsWith("&") &&
|
|
876
948
|
!k.startsWith("[") &&
|
|
877
949
|
!k.startsWith("@") &&
|
|
878
|
-
v !== null &&
|
|
879
|
-
typeof v !== "object" &&
|
|
880
|
-
typeof v === "string" &&
|
|
881
950
|
isTemplateString(v),
|
|
882
951
|
)
|
|
883
952
|
.map(([k, v]) => {
|
|
@@ -892,18 +961,14 @@ export function inlineStyleDeclarations(
|
|
|
892
961
|
* Build an HTML attribute string from a static element definition.
|
|
893
962
|
*
|
|
894
963
|
* @param {JxElement | JxMutableNode} def
|
|
895
|
-
* @param {
|
|
964
|
+
* @param {JxScope | null} scope
|
|
896
965
|
* @param {string} [hostStyle] - Declarations resolved by the caller against ANOTHER scope, folded
|
|
897
966
|
* into the same `style` attribute after the node's own. A nested component instance is the one
|
|
898
967
|
* caller: its own `style` resolves against the parent's scope, while the definition's host style
|
|
899
968
|
* resolves against the instance's, and an element cannot carry two `style` attributes.
|
|
900
969
|
* @returns {string}
|
|
901
970
|
*/
|
|
902
|
-
export function buildAttrs(
|
|
903
|
-
def: JxElement | JxMutableNode,
|
|
904
|
-
scope: Record<string, unknown> | null,
|
|
905
|
-
hostStyle = "",
|
|
906
|
-
) {
|
|
971
|
+
export function buildAttrs(def: JxElement | JxMutableNode, scope: JxScope | null, hostStyle = "") {
|
|
907
972
|
let out = "";
|
|
908
973
|
|
|
909
974
|
const id = resolveStaticValue(def.id, scope);
|
|
@@ -971,18 +1036,14 @@ export function buildAttrs(
|
|
|
971
1036
|
* Sidebar's own not-the-current-page marker, and second-guessing it would be this same defect
|
|
972
1037
|
* Pointed the other way.
|
|
973
1038
|
*/
|
|
974
|
-
if (
|
|
1039
|
+
if (isBoolean(value)) {
|
|
975
1040
|
const text = booleanAttrValue(k, value);
|
|
976
1041
|
if (text !== null) {
|
|
977
1042
|
out += text === "" ? ` ${k}` : ` ${k}="${escapeHtml(text)}"`;
|
|
978
1043
|
}
|
|
979
1044
|
continue;
|
|
980
1045
|
}
|
|
981
|
-
if (
|
|
982
|
-
value !== null &&
|
|
983
|
-
value !== undefined &&
|
|
984
|
-
(typeof value === "string" || typeof value === "number")
|
|
985
|
-
) {
|
|
1046
|
+
if (isString(value) || isNumber(value)) {
|
|
986
1047
|
out += ` ${k}="${escapeHtml(String(value))}"`;
|
|
987
1048
|
}
|
|
988
1049
|
}
|
|
@@ -1017,30 +1078,34 @@ export function childSeparator(preformatted: boolean | undefined, indent = " ")
|
|
|
1017
1078
|
return preformatted ? "" : `\n${indent}`;
|
|
1018
1079
|
}
|
|
1019
1080
|
|
|
1081
|
+
/**
|
|
1082
|
+
* A {@link CompileContext} as a static emitter hands it to a node, before that node's own scope is
|
|
1083
|
+
* known: the scope in effect may still be none.
|
|
1084
|
+
*/
|
|
1085
|
+
export type InnerCompileContext = Omit<CompileContext, "scope"> & { scope: JxScope | null };
|
|
1086
|
+
|
|
1020
1087
|
/**
|
|
1021
1088
|
* Build the inner HTML (textContent or children) for a node.
|
|
1022
1089
|
*
|
|
1023
1090
|
* @param {JxElement} def
|
|
1024
1091
|
* @param {JxElement | null} raw
|
|
1025
|
-
* @param {
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
* }
|
|
1031
|
-
* @param {(def: unknown, raw: unknown, context: unknown) => string} childCompiler
|
|
1092
|
+
* @param {InnerCompileContext} context
|
|
1093
|
+
* @param {(
|
|
1094
|
+
* def: JxElement | string,
|
|
1095
|
+
* raw: JxElement | string,
|
|
1096
|
+
* context: InnerCompileContext,
|
|
1097
|
+
* ) => string} childCompiler
|
|
1032
1098
|
* @returns {string}
|
|
1033
1099
|
*/
|
|
1034
1100
|
export function buildInner(
|
|
1035
1101
|
def: JxElement,
|
|
1036
1102
|
raw: JxElement | null,
|
|
1037
|
-
context:
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
childCompiler: (def: unknown, raw: unknown, context: unknown) => string,
|
|
1103
|
+
context: InnerCompileContext,
|
|
1104
|
+
childCompiler: (
|
|
1105
|
+
def: JxElement | string,
|
|
1106
|
+
raw: JxElement | string,
|
|
1107
|
+
context: InnerCompileContext,
|
|
1108
|
+
) => string,
|
|
1044
1109
|
) {
|
|
1045
1110
|
const source = raw ?? def;
|
|
1046
1111
|
|
|
@@ -1049,13 +1114,14 @@ export function buildInner(
|
|
|
1049
1114
|
return value == null ? "" : escapeHtml(String(value));
|
|
1050
1115
|
}
|
|
1051
1116
|
if (source.innerHTML) {
|
|
1052
|
-
|
|
1117
|
+
const value = resolveStaticValue(source.innerHTML, context.scope);
|
|
1118
|
+
return value == null ? "" : String(value);
|
|
1053
1119
|
}
|
|
1054
1120
|
if (Array.isArray(source.children)) {
|
|
1055
1121
|
const rawChildren = raw?.children;
|
|
1056
1122
|
return source.children
|
|
1057
|
-
.map((c
|
|
1058
|
-
const childRaw = (rawChildren
|
|
1123
|
+
.map((c, i) => {
|
|
1124
|
+
const childRaw = (Array.isArray(rawChildren) ? rawChildren[i] : undefined) ?? c;
|
|
1059
1125
|
return childCompiler(c, childRaw, context);
|
|
1060
1126
|
})
|
|
1061
1127
|
.join(childSeparator(context.preformatted));
|
|
@@ -1127,7 +1193,7 @@ function recordDroppedReactive(
|
|
|
1127
1193
|
value: string | JxRef,
|
|
1128
1194
|
selector: string | null,
|
|
1129
1195
|
): null {
|
|
1130
|
-
const source =
|
|
1196
|
+
const source = isString(value) ? value : value.$ref;
|
|
1131
1197
|
_droppedReactive.push({ property, selector, source });
|
|
1132
1198
|
return null;
|
|
1133
1199
|
}
|
|
@@ -1230,7 +1296,7 @@ export function compileStyles(
|
|
|
1230
1296
|
`color-scheme` triplet a scheme query in `$media` still calls for, which is exactly what a
|
|
1231
1297
|
page with a scheme query and no project style has always carried. */
|
|
1232
1298
|
const projectSheet = buildSiteStyleCSS(
|
|
1233
|
-
projectStyle
|
|
1299
|
+
isNonNullObject(projectStyle) ? projectStyle : {},
|
|
1234
1300
|
mediaQueries,
|
|
1235
1301
|
(value) => value,
|
|
1236
1302
|
recordDroppedReactive,
|
|
@@ -1276,7 +1342,7 @@ export function collectStyles(
|
|
|
1276
1342
|
prefix = "jx",
|
|
1277
1343
|
) {
|
|
1278
1344
|
const counter = counterArg ?? { n: 0 };
|
|
1279
|
-
if (!def
|
|
1345
|
+
if (!isNonNullObject(def)) {
|
|
1280
1346
|
return;
|
|
1281
1347
|
}
|
|
1282
1348
|
|
|
@@ -1307,16 +1373,9 @@ export function collectStyles(
|
|
|
1307
1373
|
}
|
|
1308
1374
|
|
|
1309
1375
|
// $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
|
-
);
|
|
1376
|
+
if (isNonNullObject(def.cases)) {
|
|
1377
|
+
for (const c of Object.values<JxElement | JxMutableNode>(def.cases)) {
|
|
1378
|
+
collectStyles(c, rules, mediaQueries, selector, counter, prefix);
|
|
1320
1379
|
}
|
|
1321
1380
|
}
|
|
1322
1381
|
}
|
|
@@ -1380,18 +1439,13 @@ export function collectSrcImports(doc: JxElement) {
|
|
|
1380
1439
|
* @param {Set<string>} srcs
|
|
1381
1440
|
*/
|
|
1382
1441
|
function _walkSrc(def: JxElement | JxMutableNode | string, srcs: Set<string>) {
|
|
1383
|
-
if (!def
|
|
1442
|
+
if (!isNonNullObject(def)) {
|
|
1384
1443
|
return;
|
|
1385
1444
|
}
|
|
1386
1445
|
if (def.state) {
|
|
1387
1446
|
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);
|
|
1447
|
+
if (isFunctionDef(d) && d.$src) {
|
|
1448
|
+
srcs.add(d.$src);
|
|
1395
1449
|
}
|
|
1396
1450
|
}
|
|
1397
1451
|
}
|
|
@@ -1422,7 +1476,7 @@ function _walkServerEntries(
|
|
|
1422
1476
|
def: JxElement | JxMutableNode | string,
|
|
1423
1477
|
entries: Map<string, { key: string; exportName: string; src: string }>,
|
|
1424
1478
|
) {
|
|
1425
|
-
if (!def
|
|
1479
|
+
if (!isNonNullObject(def)) {
|
|
1426
1480
|
return;
|
|
1427
1481
|
}
|
|
1428
1482
|
if (def.state) {
|
|
@@ -1461,15 +1515,12 @@ export const SELF_CLOSING = new Set<string>([
|
|
|
1461
1515
|
* precisely because the old `${…}` form was left as text and then re-resolved against the page
|
|
1462
1516
|
* scope, where a component's own state is undefined.
|
|
1463
1517
|
*
|
|
1464
|
-
* @param {
|
|
1465
|
-
* @param {
|
|
1518
|
+
* @param {TagNameSlot} tagName
|
|
1519
|
+
* @param {JxScope | null} scope
|
|
1466
1520
|
* @returns {string}
|
|
1467
1521
|
*/
|
|
1468
|
-
export function resolveStaticTagName(
|
|
1469
|
-
tagName
|
|
1470
|
-
scope: Record<string, unknown> | null,
|
|
1471
|
-
): string {
|
|
1472
|
-
if (typeof tagName === "string") {
|
|
1522
|
+
export function resolveStaticTagName(tagName: TagNameSlot, scope: JxScope | null): string {
|
|
1523
|
+
if (isString(tagName)) {
|
|
1473
1524
|
return tagName;
|
|
1474
1525
|
}
|
|
1475
1526
|
const candidates = tagNameCandidates(tagName);
|
|
@@ -1480,10 +1531,14 @@ export function resolveStaticTagName(
|
|
|
1480
1531
|
return candidates[0]!;
|
|
1481
1532
|
}
|
|
1482
1533
|
const expression = tagName.$expression;
|
|
1534
|
+
/* SAFETY: a tag expression is parsed from a document, so its `target` is a document value (a
|
|
1535
|
+
`$ref`, a template string or a literal); the schema leaves it untyped only because any operand
|
|
1536
|
+
may stand there. */
|
|
1537
|
+
const target = expression.target as JxDocumentValue | undefined;
|
|
1483
1538
|
if (expression.operator === "?:") {
|
|
1484
|
-
return resolveStaticValue(
|
|
1539
|
+
return resolveStaticValue(target, scope) ? expression.value : expression.initial;
|
|
1485
1540
|
}
|
|
1486
|
-
const key = resolveStaticValue(
|
|
1541
|
+
const key = resolveStaticValue(target, scope);
|
|
1487
1542
|
return expression.cases[String(key)] ?? expression.default;
|
|
1488
1543
|
}
|
|
1489
1544
|
|
|
@@ -1515,17 +1570,13 @@ interface PrerenderFrame {
|
|
|
1515
1570
|
key: string;
|
|
1516
1571
|
}
|
|
1517
1572
|
|
|
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;
|
|
1573
|
+
/** What {@link liftPropsAttributes} splits an instance's attributes into. */
|
|
1574
|
+
export interface LiftedPropsAttributes {
|
|
1575
|
+
/** The `props.*` values keyed by prop name, or null when the instance wrote none. */
|
|
1576
|
+
lifted: Record<string, JsonValue> | null;
|
|
1577
|
+
/** Every other attribute, unchanged. */
|
|
1578
|
+
rest: NonNullable<JxElement["attributes"]>;
|
|
1579
|
+
}
|
|
1529
1580
|
|
|
1530
1581
|
/**
|
|
1531
1582
|
* Lift literal `props.*` attribute keys off an instance's attributes.
|
|
@@ -1538,20 +1589,17 @@ export const MAX_COMPONENT_NESTING = 32;
|
|
|
1538
1589
|
* definition every page shares.
|
|
1539
1590
|
*
|
|
1540
1591
|
* @param {Record<string, JxAttributeValue> | undefined} attributes
|
|
1541
|
-
* @returns {
|
|
1542
|
-
*
|
|
1592
|
+
* @returns {LiftedPropsAttributes} The lifted props (null when there were none) and the attributes
|
|
1593
|
+
* that remain
|
|
1543
1594
|
*/
|
|
1544
|
-
export function liftPropsAttributes(attributes: JxElement["attributes"]): {
|
|
1545
|
-
lifted: Record<string, JsonValue> | null;
|
|
1546
|
-
rest: NonNullable<JxElement["attributes"]>;
|
|
1547
|
-
} {
|
|
1595
|
+
export function liftPropsAttributes(attributes: JxElement["attributes"]): LiftedPropsAttributes {
|
|
1548
1596
|
let lifted: Record<string, JsonValue> | null = null;
|
|
1549
1597
|
const rest: NonNullable<JxElement["attributes"]> = {};
|
|
1550
1598
|
for (const [key, value] of Object.entries(attributes ?? {})) {
|
|
1551
1599
|
if (key.startsWith("props.") && key.length > "props.".length) {
|
|
1552
1600
|
lifted ??= {};
|
|
1553
|
-
|
|
1554
|
-
|
|
1601
|
+
/* SAFETY: a JxAttributeValue is a JSON primitive or a `{ $ref }` object with one string
|
|
1602
|
+
member, so it is a JsonValue; only the interface's missing index signature hides that. */
|
|
1555
1603
|
lifted[key.slice("props.".length)] = value as JsonValue;
|
|
1556
1604
|
} else {
|
|
1557
1605
|
rest[key] = value;
|
|
@@ -1571,33 +1619,32 @@ export function liftPropsAttributes(attributes: JxElement["attributes"]): {
|
|
|
1571
1619
|
*
|
|
1572
1620
|
* @param {JxElement} doc - Component definition
|
|
1573
1621
|
* @param {Record<string, JsonValue> | null} props - Instance-specific prop values
|
|
1574
|
-
* @returns {
|
|
1622
|
+
* @returns {JxScope}
|
|
1575
1623
|
*/
|
|
1576
1624
|
export function buildInstanceScope(
|
|
1577
1625
|
doc: JxElement,
|
|
1578
1626
|
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
|
-
}
|
|
1627
|
+
): JxScope {
|
|
1628
|
+
if (!props) {
|
|
1629
|
+
return buildInitialScope(doc.state ?? {}, null);
|
|
1630
|
+
}
|
|
1631
|
+
const stateDefs = { ...doc.state };
|
|
1632
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1633
|
+
const existing = stateDefs[key];
|
|
1634
|
+
stateDefs[key] =
|
|
1635
|
+
isNonNullObject(existing) && !Array.isArray(existing) && "default" in existing
|
|
1636
|
+
? { ...existing, default: value }
|
|
1637
|
+
: value;
|
|
1597
1638
|
}
|
|
1598
1639
|
return buildInitialScope(stateDefs, null);
|
|
1599
1640
|
}
|
|
1600
1641
|
|
|
1642
|
+
/**
|
|
1643
|
+
* Resolved host declarations by (camelCase) property: each is what its template read from the
|
|
1644
|
+
* instance's scope.
|
|
1645
|
+
*/
|
|
1646
|
+
export type ResolvedHostStyle = Record<string, JxScope[string]>;
|
|
1647
|
+
|
|
1601
1648
|
/**
|
|
1602
1649
|
* The definition's host `style` entries that depend on the instance, resolved against it.
|
|
1603
1650
|
*
|
|
@@ -1607,26 +1654,22 @@ export function buildInstanceScope(
|
|
|
1607
1654
|
* one.
|
|
1608
1655
|
*
|
|
1609
1656
|
* @param {JxStyle | null | undefined} style - The definition's `style`
|
|
1610
|
-
* @param {
|
|
1611
|
-
* @returns {
|
|
1657
|
+
* @param {JxScope} scope - The instance's scope (`buildInstanceScope`)
|
|
1658
|
+
* @returns {ResolvedHostStyle}
|
|
1612
1659
|
*/
|
|
1613
1660
|
export function resolveHostStyle(
|
|
1614
1661
|
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;
|
|
1662
|
+
scope: JxScope,
|
|
1663
|
+
): ResolvedHostStyle {
|
|
1664
|
+
return Object.fromEntries(
|
|
1665
|
+
Object.entries(style ?? {}).flatMap(([prop, value]) => {
|
|
1666
|
+
if (!isTemplateString(value)) {
|
|
1667
|
+
return [];
|
|
1626
1668
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1669
|
+
const val = resolveStaticValue(value, scope);
|
|
1670
|
+
return val == null ? [] : [[prop, val] as const];
|
|
1671
|
+
}),
|
|
1672
|
+
);
|
|
1630
1673
|
}
|
|
1631
1674
|
|
|
1632
1675
|
/**
|
|
@@ -1638,14 +1681,14 @@ export function resolveHostStyle(
|
|
|
1638
1681
|
* the shape spec.md §17.2 recommends when every child is a bare string — prerendered to nothing.
|
|
1639
1682
|
*
|
|
1640
1683
|
* @param {JxElement | JxMutableNode} node
|
|
1641
|
-
* @param {
|
|
1684
|
+
* @param {JxScope | null} scope
|
|
1642
1685
|
* @param {string | null} slotContent
|
|
1643
1686
|
* @param {ComponentPrerenderContext | null} context
|
|
1644
1687
|
* @returns {string}
|
|
1645
1688
|
*/
|
|
1646
1689
|
function renderInner(
|
|
1647
1690
|
node: JxElement | JxMutableNode,
|
|
1648
|
-
scope:
|
|
1691
|
+
scope: JxScope | null,
|
|
1649
1692
|
slotContent: string | null,
|
|
1650
1693
|
context: ComponentPrerenderContext | null,
|
|
1651
1694
|
): string {
|
|
@@ -1655,7 +1698,7 @@ function renderInner(
|
|
|
1655
1698
|
}
|
|
1656
1699
|
if (node.innerHTML) {
|
|
1657
1700
|
const val = resolveStaticValue(node.innerHTML, scope);
|
|
1658
|
-
return val != null ? String(val) :
|
|
1701
|
+
return val != null ? String(val) : node.innerHTML;
|
|
1659
1702
|
}
|
|
1660
1703
|
if (Array.isArray(node.children)) {
|
|
1661
1704
|
return node.children
|
|
@@ -1667,40 +1710,50 @@ function renderInner(
|
|
|
1667
1710
|
return "";
|
|
1668
1711
|
}
|
|
1669
1712
|
|
|
1713
|
+
/**
|
|
1714
|
+
* A child slot as the static renderer meets it: a node or a text string, plus the JSON scalars,
|
|
1715
|
+
* holes and nested lists a hand-written `children` array may also carry.
|
|
1716
|
+
*/
|
|
1717
|
+
export type StaticChild =
|
|
1718
|
+
| JxElement
|
|
1719
|
+
| JxMutableNode
|
|
1720
|
+
| string
|
|
1721
|
+
| number
|
|
1722
|
+
| boolean
|
|
1723
|
+
| null
|
|
1724
|
+
| undefined
|
|
1725
|
+
| StaticChild[];
|
|
1726
|
+
|
|
1670
1727
|
/**
|
|
1671
1728
|
* Recursively render a Jx node tree to static HTML for pre-rendering.
|
|
1672
1729
|
*
|
|
1673
|
-
* @param {
|
|
1674
|
-
* @param {
|
|
1730
|
+
* @param {StaticChild} node
|
|
1731
|
+
* @param {JxScope | null} scope
|
|
1675
1732
|
* @param {string | null} [slotContent] - HTML to substitute for `<slot>` elements
|
|
1676
1733
|
* @param {ComponentPrerenderContext | null} [context] - The component registry, when a custom
|
|
1677
1734
|
* element tag met here should be expanded rather than emitted bare
|
|
1678
1735
|
* @returns {string}
|
|
1679
1736
|
*/
|
|
1680
1737
|
export function renderStaticNode(
|
|
1681
|
-
node:
|
|
1682
|
-
scope:
|
|
1738
|
+
node: StaticChild,
|
|
1739
|
+
scope: JxScope | null,
|
|
1683
1740
|
slotContent: string | null = null,
|
|
1684
1741
|
context: ComponentPrerenderContext | null = null,
|
|
1685
1742
|
): string {
|
|
1686
|
-
if (
|
|
1743
|
+
if (isString(node)) {
|
|
1687
1744
|
if (isTemplateString(node) && scope) {
|
|
1688
1745
|
const val = evaluateStaticTemplate(node, scope);
|
|
1689
1746
|
return val != null ? escapeHtml(String(val)) : escapeHtml(node);
|
|
1690
1747
|
}
|
|
1691
1748
|
return escapeHtml(node);
|
|
1692
1749
|
}
|
|
1693
|
-
if (
|
|
1750
|
+
if (isNumber(node) || isBoolean(node)) {
|
|
1694
1751
|
return escapeHtml(String(node));
|
|
1695
1752
|
}
|
|
1696
1753
|
if (Array.isArray(node)) {
|
|
1697
|
-
return (
|
|
1698
|
-
.map((c: JxElement | JxMutableNode | string): string =>
|
|
1699
|
-
renderStaticNode(c, scope, slotContent, context),
|
|
1700
|
-
)
|
|
1701
|
-
.join("\n");
|
|
1754
|
+
return node.map((c): string => renderStaticNode(c, scope, slotContent, context)).join("\n");
|
|
1702
1755
|
}
|
|
1703
|
-
if (!node
|
|
1756
|
+
if (!isNonNullObject(node)) {
|
|
1704
1757
|
return "";
|
|
1705
1758
|
}
|
|
1706
1759
|
|
|
@@ -1716,10 +1769,7 @@ export function renderStaticNode(
|
|
|
1716
1769
|
const switchTag = node.tagName ?? "div";
|
|
1717
1770
|
const attrs = buildAttrs(node, scope);
|
|
1718
1771
|
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)];
|
|
1772
|
+
const caseDef = key == null ? undefined : node.cases?.[String(key)];
|
|
1723
1773
|
const inner =
|
|
1724
1774
|
caseDef !== undefined && !isRefObject(caseDef)
|
|
1725
1775
|
? renderStaticNode(caseDef, scope, slotContent, context)
|
|
@@ -1775,7 +1825,7 @@ export function renderStaticNode(
|
|
|
1775
1825
|
*
|
|
1776
1826
|
* @param {JxElement | JxMutableNode} node - The instance as written in the parent's tree
|
|
1777
1827
|
* @param {JxElement} def - The registered definition for its tag
|
|
1778
|
-
* @param {
|
|
1828
|
+
* @param {JxScope | null} scope - The parent's scope
|
|
1779
1829
|
* @param {string | null} slotContent - The parent's slot content
|
|
1780
1830
|
* @param {ComponentPrerenderContext} context
|
|
1781
1831
|
* @returns {string}
|
|
@@ -1784,15 +1834,20 @@ export function renderStaticNode(
|
|
|
1784
1834
|
function renderComponentInstance(
|
|
1785
1835
|
node: JxElement | JxMutableNode,
|
|
1786
1836
|
def: JxElement,
|
|
1787
|
-
scope:
|
|
1837
|
+
scope: JxScope | null,
|
|
1788
1838
|
slotContent: string | null,
|
|
1789
1839
|
context: ComponentPrerenderContext,
|
|
1790
1840
|
): string {
|
|
1841
|
+
/* SAFETY: the caller looked `def` up in `componentDefs` by the node's resolved string tag, and
|
|
1842
|
+
that registry is keyed by each definition's own `tagName`, so this is that same string. */
|
|
1791
1843
|
const tag = def.tagName as string;
|
|
1792
1844
|
const { lifted, rest: attributes } = liftPropsAttributes(node.attributes);
|
|
1793
1845
|
const written = { ...lifted, ...node.$props };
|
|
1794
1846
|
const props: Record<string, JsonValue> = {};
|
|
1795
1847
|
for (const [key, value] of Object.entries(written)) {
|
|
1848
|
+
/* SAFETY: a prop resolves against the parent's build-time scope, where what a `$ref` or template
|
|
1849
|
+
may read is JSON state, and the props travel on as JSON either way (`data-jx-props` below), as
|
|
1850
|
+
the page walk serializes a page-level instance's. */
|
|
1796
1851
|
props[key] = (resolveStaticValue(value, scope) as JsonValue | null) ?? value;
|
|
1797
1852
|
}
|
|
1798
1853
|
const hasProps = Object.keys(props).length > 0;
|
|
@@ -1840,7 +1895,7 @@ function renderComponentInstance(
|
|
|
1840
1895
|
|
|
1841
1896
|
// The element as the page walk would leave it: props consumed, children rendered, stamped. A
|
|
1842
1897
|
// Copy, because `node` is a definition every page shares.
|
|
1843
|
-
const instance
|
|
1898
|
+
const instance = { ...node };
|
|
1844
1899
|
delete instance.$props;
|
|
1845
1900
|
delete instance.children;
|
|
1846
1901
|
instance.attributes =
|
|
@@ -1891,7 +1946,7 @@ export function isComponentFullyStatic(doc: JxElement) {
|
|
|
1891
1946
|
* @returns {boolean}
|
|
1892
1947
|
*/
|
|
1893
1948
|
function _isStaticNode(node: JxElement | string | (JxElement | string)[]): boolean {
|
|
1894
|
-
if (!node
|
|
1949
|
+
if (!isNonNullObject(node)) {
|
|
1895
1950
|
return true;
|
|
1896
1951
|
}
|
|
1897
1952
|
if (Array.isArray(node)) {
|
|
@@ -1910,14 +1965,13 @@ function _isStaticNode(node: JxElement | string | (JxElement | string)[]): boole
|
|
|
1910
1965
|
// Check state entries
|
|
1911
1966
|
if (node.state) {
|
|
1912
1967
|
for (const def of Object.values(node.state)) {
|
|
1913
|
-
if (!def ||
|
|
1968
|
+
if (!isNonNullObject(def) || Array.isArray(def)) {
|
|
1914
1969
|
continue;
|
|
1915
1970
|
}
|
|
1916
|
-
|
|
1917
|
-
if (d.$prototype) {
|
|
1971
|
+
if (def.$prototype) {
|
|
1918
1972
|
return false;
|
|
1919
1973
|
}
|
|
1920
|
-
if (
|
|
1974
|
+
if (def.$ref) {
|
|
1921
1975
|
return false;
|
|
1922
1976
|
}
|
|
1923
1977
|
}
|
|
@@ -1934,8 +1988,8 @@ function _isStaticNode(node: JxElement | string | (JxElement | string)[]): boole
|
|
|
1934
1988
|
if (Array.isArray(node.children)) {
|
|
1935
1989
|
return node.children.every((c) => _isStaticNode(c));
|
|
1936
1990
|
}
|
|
1937
|
-
//
|
|
1938
|
-
if (node.children
|
|
1991
|
+
// The legacy bare children slot: a mapped array builds its children at runtime.
|
|
1992
|
+
if (isMappedArray(node.children)) {
|
|
1939
1993
|
return false;
|
|
1940
1994
|
}
|
|
1941
1995
|
|
|
@@ -2015,7 +2069,7 @@ export function buildComponentCSS(
|
|
|
2015
2069
|
*/
|
|
2016
2070
|
const scope = styleScopePrefix(tagName, shadow);
|
|
2017
2071
|
|
|
2018
|
-
if (styleDef
|
|
2072
|
+
if (isNonNullObject(styleDef)) {
|
|
2019
2073
|
/* The top level is walked here rather than handed straight to the builder, because ONE of its
|
|
2020
2074
|
keys does not mean what `resolveNestedSelector` would make of it: `:host` and `:host(.foo)`
|
|
2021
2075
|
are TRANSLATED per {@link resolveSelector} so a style object means the same thing in both
|
|
@@ -2030,8 +2084,8 @@ export function buildComponentCSS(
|
|
|
2030
2084
|
not an at-rule, not a nested selector — and was dropped without reaching the builder at
|
|
2031
2085
|
all, so the one emitter that could have reported it never saw it. */
|
|
2032
2086
|
if (isRef(value)) {
|
|
2033
|
-
own[prop] = value
|
|
2034
|
-
} else if (value
|
|
2087
|
+
own[prop] = value;
|
|
2088
|
+
} else if (isNonNullObject(value) && !Array.isArray(value)) {
|
|
2035
2089
|
blocks.push([prop, value]);
|
|
2036
2090
|
} else if (!prop.startsWith("@") && !isNestedSelectorKey(prop)) {
|
|
2037
2091
|
own[prop] = value;
|