@jxsuite/compiler 5.1.3 → 6.0.2
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 +194 -191
- package/dist/compiler.js +97 -94
- package/package.json +8 -7
- package/src/shared.ts +344 -34
- package/src/site/project-sections.ts +4 -0
- package/src/site/site-build.ts +43 -18
- package/src/targets/compile-client.ts +15 -5
- package/src/targets/compile-element.ts +72 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@apidevtools/json-schema-ref-parser": "^16.0.5",
|
|
49
|
-
"@jxsuite/create": "^1.3.
|
|
50
|
-
"@jxsuite/runtime": "^
|
|
51
|
-
"@jxsuite/schema": "^
|
|
52
|
-
"@jxsuite/site": "^2.0
|
|
49
|
+
"@jxsuite/create": "^1.3.17",
|
|
50
|
+
"@jxsuite/runtime": "^5.1.0",
|
|
51
|
+
"@jxsuite/schema": "^4.0.0",
|
|
52
|
+
"@jxsuite/site": "^2.1.0",
|
|
53
53
|
"@vue/reactivity": "3.5.44",
|
|
54
54
|
"esbuild": "^0.28.2",
|
|
55
55
|
"lit-html": "^3.3.3",
|
|
@@ -57,8 +57,9 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@happy-dom/global-registrator": "^21.0.3",
|
|
60
|
-
"@jxsuite/connector": "^0.
|
|
61
|
-
"@jxsuite/parser": "^
|
|
60
|
+
"@jxsuite/connector": "^0.7.0",
|
|
61
|
+
"@jxsuite/parser": "^4.0.1",
|
|
62
|
+
"@jxsuite/test-support": "^0.0.0",
|
|
62
63
|
"@types/mdast": "^4.0.4"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/src/shared.ts
CHANGED
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
COLOR_SCHEME_STORAGE_KEY,
|
|
12
12
|
MAX_COMPONENT_NESTING,
|
|
13
13
|
RESERVED_KEYS,
|
|
14
|
+
SLOT_ATTR,
|
|
15
|
+
SLOT_MARK,
|
|
16
|
+
SLOT_RECORD_KEY,
|
|
17
|
+
SLOT_WHITESPACE,
|
|
14
18
|
booleanAttrValue,
|
|
15
19
|
buildStyleRules,
|
|
16
20
|
enumeratedAttrNames,
|
|
@@ -32,6 +36,7 @@ import {
|
|
|
32
36
|
isFunctionDef,
|
|
33
37
|
isMappedArray,
|
|
34
38
|
isNamedFormulaDef,
|
|
39
|
+
isNodeObject,
|
|
35
40
|
isNonNullObject,
|
|
36
41
|
isNumber,
|
|
37
42
|
isPrototypeDef,
|
|
@@ -54,6 +59,7 @@ import type {
|
|
|
54
59
|
JsonValue,
|
|
55
60
|
JxDocumentValue,
|
|
56
61
|
JxElement,
|
|
62
|
+
JxMappedArray,
|
|
57
63
|
JxMutableNode,
|
|
58
64
|
JxPrototypeDef,
|
|
59
65
|
JxRef,
|
|
@@ -867,6 +873,20 @@ function readsRuntimeOnlyState(str: string, scope: JxScope | null) {
|
|
|
867
873
|
return false;
|
|
868
874
|
}
|
|
869
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
|
+
|
|
870
890
|
/**
|
|
871
891
|
* @param {string} str
|
|
872
892
|
* @param {JxScope | null} scope
|
|
@@ -1262,6 +1282,154 @@ export function attrHelperSource(): string {
|
|
|
1262
1282
|
);
|
|
1263
1283
|
}
|
|
1264
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
|
+
|
|
1265
1433
|
/**
|
|
1266
1434
|
* Walk the entire document tree and collect all static nested CSS rules.
|
|
1267
1435
|
*
|
|
@@ -1561,6 +1729,120 @@ export interface ComponentPrerenderContext {
|
|
|
1561
1729
|
defaults?: ProjectConfig["defaults"] | undefined;
|
|
1562
1730
|
/** The instances being expanded, outermost first; empty at a page-level instance. */
|
|
1563
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
|
+
}
|
|
1564
1846
|
}
|
|
1565
1847
|
|
|
1566
1848
|
/** One instance on the expansion path: its tag, and its resolved props as an identity. */
|
|
@@ -1682,14 +1964,14 @@ export function resolveHostStyle(
|
|
|
1682
1964
|
*
|
|
1683
1965
|
* @param {JxElement | JxMutableNode} node
|
|
1684
1966
|
* @param {JxScope | null} scope
|
|
1685
|
-
* @param {
|
|
1967
|
+
* @param {SlotFill | null} slots
|
|
1686
1968
|
* @param {ComponentPrerenderContext | null} context
|
|
1687
1969
|
* @returns {string}
|
|
1688
1970
|
*/
|
|
1689
1971
|
function renderInner(
|
|
1690
1972
|
node: JxElement | JxMutableNode,
|
|
1691
1973
|
scope: JxScope | null,
|
|
1692
|
-
|
|
1974
|
+
slots: SlotFill | null,
|
|
1693
1975
|
context: ComponentPrerenderContext | null,
|
|
1694
1976
|
): string {
|
|
1695
1977
|
if (node.textContent !== undefined) {
|
|
@@ -1702,9 +1984,7 @@ function renderInner(
|
|
|
1702
1984
|
}
|
|
1703
1985
|
if (Array.isArray(node.children)) {
|
|
1704
1986
|
return node.children
|
|
1705
|
-
.map((c: JxElement | JxMutableNode | string) =>
|
|
1706
|
-
renderStaticNode(c, scope, slotContent, context),
|
|
1707
|
-
)
|
|
1987
|
+
.map((c: JxElement | JxMutableNode | string) => renderStaticNode(c, scope, slots, context))
|
|
1708
1988
|
.join("\n");
|
|
1709
1989
|
}
|
|
1710
1990
|
return "";
|
|
@@ -1729,7 +2009,8 @@ export type StaticChild =
|
|
|
1729
2009
|
*
|
|
1730
2010
|
* @param {StaticChild} node
|
|
1731
2011
|
* @param {JxScope | null} scope
|
|
1732
|
-
* @param {
|
|
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)
|
|
1733
2014
|
* @param {ComponentPrerenderContext | null} [context] - The component registry, when a custom
|
|
1734
2015
|
* element tag met here should be expanded rather than emitted bare
|
|
1735
2016
|
* @returns {string}
|
|
@@ -1737,7 +2018,7 @@ export type StaticChild =
|
|
|
1737
2018
|
export function renderStaticNode(
|
|
1738
2019
|
node: StaticChild,
|
|
1739
2020
|
scope: JxScope | null,
|
|
1740
|
-
|
|
2021
|
+
slots: SlotFill | null = null,
|
|
1741
2022
|
context: ComponentPrerenderContext | null = null,
|
|
1742
2023
|
): string {
|
|
1743
2024
|
if (isString(node)) {
|
|
@@ -1751,7 +2032,7 @@ export function renderStaticNode(
|
|
|
1751
2032
|
return escapeHtml(String(node));
|
|
1752
2033
|
}
|
|
1753
2034
|
if (Array.isArray(node)) {
|
|
1754
|
-
return node.map((c): string => renderStaticNode(c, scope,
|
|
2035
|
+
return node.map((c): string => renderStaticNode(c, scope, slots, context)).join("\n");
|
|
1755
2036
|
}
|
|
1756
2037
|
if (!isNonNullObject(node)) {
|
|
1757
2038
|
return "";
|
|
@@ -1772,7 +2053,7 @@ export function renderStaticNode(
|
|
|
1772
2053
|
const caseDef = key == null ? undefined : node.cases?.[String(key)];
|
|
1773
2054
|
const inner =
|
|
1774
2055
|
caseDef !== undefined && !isRefObject(caseDef)
|
|
1775
|
-
? renderStaticNode(caseDef, scope,
|
|
2056
|
+
? renderStaticNode(caseDef, scope, slots, context)
|
|
1776
2057
|
: "";
|
|
1777
2058
|
return `<${switchTag}${attrs}>${inner}</${switchTag}>`;
|
|
1778
2059
|
}
|
|
@@ -1783,15 +2064,33 @@ export function renderStaticNode(
|
|
|
1783
2064
|
state does not exist, so SSR silently emitted the fallback branch's tag. */
|
|
1784
2065
|
const tag = resolveStaticTagName(node.tagName, scope);
|
|
1785
2066
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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}`;
|
|
1789
2088
|
}
|
|
1790
2089
|
|
|
1791
2090
|
// A registered component: expanded exactly as the page walk expands one, not emitted bare.
|
|
1792
2091
|
const def = context?.componentDefs.get(tag);
|
|
1793
2092
|
if (def && context) {
|
|
1794
|
-
return renderComponentInstance(node, def, scope,
|
|
2093
|
+
return renderComponentInstance(node, def, scope, slots, context);
|
|
1795
2094
|
}
|
|
1796
2095
|
|
|
1797
2096
|
const attrs = buildAttrs(node, scope);
|
|
@@ -1800,7 +2099,7 @@ export function renderStaticNode(
|
|
|
1800
2099
|
return `<${tag}${attrs}>`;
|
|
1801
2100
|
}
|
|
1802
2101
|
|
|
1803
|
-
return `<${tag}${attrs}>${renderInner(node, scope,
|
|
2102
|
+
return `<${tag}${attrs}>${renderInner(node, scope, slots, context)}</${tag}>`;
|
|
1804
2103
|
}
|
|
1805
2104
|
|
|
1806
2105
|
/**
|
|
@@ -1811,7 +2110,10 @@ export function renderStaticNode(
|
|
|
1811
2110
|
* scope, which is what `resolveDocTemplates` does for a page-level instance before the page walk
|
|
1812
2111
|
* reaches it.
|
|
1813
2112
|
* - Slot content: the instance's own children, rendered in the parent's scope and with the parent's
|
|
1814
|
-
*
|
|
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).
|
|
1815
2117
|
* - Shadow mode: a declarative shadow root with the stylesheet link inside it (spec.md §16.6).
|
|
1816
2118
|
* - Host style: the definition's template-string entries resolved against the INSTANCE, written on
|
|
1817
2119
|
* the element — the page walk's `resolvedStyle` in inline form, since there is no page stylesheet
|
|
@@ -1826,7 +2128,7 @@ export function renderStaticNode(
|
|
|
1826
2128
|
* @param {JxElement | JxMutableNode} node - The instance as written in the parent's tree
|
|
1827
2129
|
* @param {JxElement} def - The registered definition for its tag
|
|
1828
2130
|
* @param {JxScope | null} scope - The parent's scope
|
|
1829
|
-
* @param {
|
|
2131
|
+
* @param {SlotFill | null} parentSlots - The parent's fill
|
|
1830
2132
|
* @param {ComponentPrerenderContext} context
|
|
1831
2133
|
* @returns {string}
|
|
1832
2134
|
* @docs framework/build
|
|
@@ -1835,7 +2137,7 @@ function renderComponentInstance(
|
|
|
1835
2137
|
node: JxElement | JxMutableNode,
|
|
1836
2138
|
def: JxElement,
|
|
1837
2139
|
scope: JxScope | null,
|
|
1838
|
-
|
|
2140
|
+
parentSlots: SlotFill | null,
|
|
1839
2141
|
context: ComponentPrerenderContext,
|
|
1840
2142
|
): string {
|
|
1841
2143
|
/* SAFETY: the caller looked `def` up in `componentDefs` by the node's resolved string tag, and
|
|
@@ -1869,25 +2171,29 @@ function renderComponentInstance(
|
|
|
1869
2171
|
}
|
|
1870
2172
|
const nested: ComponentPrerenderContext = { ...context, path: [...path, frame] };
|
|
1871
2173
|
|
|
1872
|
-
// The instance's children are the parent's nodes: parent scope, parent
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
? node.children
|
|
1876
|
-
.map((c: JxElement | JxMutableNode | string) =>
|
|
1877
|
-
renderStaticNode(c, scope, slotContent, context),
|
|
1878
|
-
)
|
|
1879
|
-
.join("\n")
|
|
1880
|
-
: 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);
|
|
1881
2177
|
|
|
1882
2178
|
const shadow = resolveShadowMode(def, context.defaults);
|
|
1883
2179
|
const isStatic = isComponentFullyStatic(def);
|
|
1884
2180
|
const instanceScope = buildInstanceScope(def, hasProps ? props : null);
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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}">` +
|
|
1888
2190
|
`<link rel="stylesheet" href="/components/${tag}.css">` +
|
|
1889
|
-
`${
|
|
1890
|
-
|
|
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
|
+
}
|
|
1891
2197
|
|
|
1892
2198
|
const hostStyle = Object.entries(resolveHostStyle(def.style, instanceScope))
|
|
1893
2199
|
.map(([prop, value]) => `${camelToKebab(prop)}: ${value}`)
|
|
@@ -1900,6 +2206,9 @@ function renderComponentInstance(
|
|
|
1900
2206
|
delete instance.children;
|
|
1901
2207
|
instance.attributes =
|
|
1902
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
|
+
}
|
|
1903
2212
|
if (isStatic) {
|
|
1904
2213
|
instance.$static = true;
|
|
1905
2214
|
} else {
|
|
@@ -1914,7 +2223,8 @@ function renderComponentInstance(
|
|
|
1914
2223
|
* @param {JxElement} doc - Component JSON definition
|
|
1915
2224
|
* @param {Record<string, JsonValue> | null} [propsOverride] - Instance-specific prop values to
|
|
1916
2225
|
* merge into state
|
|
1917
|
-
* @param {
|
|
2226
|
+
* @param {SlotFill | null} [slots] - The instance's children by slot name (`createSlotFill`);
|
|
2227
|
+
* `null` leaves every `<slot>` literal
|
|
1918
2228
|
* @param {ComponentPrerenderContext | null} [context] - The component registry; with one, an
|
|
1919
2229
|
* instance of a registered component inside `doc` is expanded rather than emitted bare
|
|
1920
2230
|
* @returns {string} The pre-rendered innerHTML
|
|
@@ -1922,10 +2232,10 @@ function renderComponentInstance(
|
|
|
1922
2232
|
export function preRenderComponentHtml(
|
|
1923
2233
|
doc: JxElement,
|
|
1924
2234
|
propsOverride: Record<string, JsonValue> | null = null,
|
|
1925
|
-
|
|
2235
|
+
slots: SlotFill | null = null,
|
|
1926
2236
|
context: ComponentPrerenderContext | null = null,
|
|
1927
2237
|
) {
|
|
1928
|
-
return renderInner(doc, buildInstanceScope(doc, propsOverride),
|
|
2238
|
+
return renderInner(doc, buildInstanceScope(doc, propsOverride), slots, context);
|
|
1929
2239
|
}
|
|
1930
2240
|
|
|
1931
2241
|
/**
|
|
@@ -29,12 +29,15 @@ export type ProjectSections = Record<string, ProjectSectionData>;
|
|
|
29
29
|
* @param {string} projectRoot - Absolute path to the project root
|
|
30
30
|
* @param {ProjectConfig | undefined} projectConfig - Already-loaded project config
|
|
31
31
|
* @param {ExtensionRegistry} registry - The project's extension registry
|
|
32
|
+
* @param {{ lazy?: boolean }} [options] - `lazy`: the host serves one page at a time, so an owner
|
|
33
|
+
* may load its section on demand (specs/extensions.md §8). A build leaves it off.
|
|
32
34
|
* @returns {Promise<ProjectSections>} Section key → projectData result
|
|
33
35
|
*/
|
|
34
36
|
export async function loadProjectSections(
|
|
35
37
|
projectRoot: string,
|
|
36
38
|
projectConfig: ProjectConfig | undefined,
|
|
37
39
|
registry: ExtensionRegistry,
|
|
40
|
+
options: { lazy?: boolean } = {},
|
|
38
41
|
): Promise<ProjectSections> {
|
|
39
42
|
const sections: ProjectSections = {};
|
|
40
43
|
const io = createNodeFormatIO(projectRoot);
|
|
@@ -51,6 +54,7 @@ export async function loadProjectSections(
|
|
|
51
54
|
projectConfig,
|
|
52
55
|
registry,
|
|
53
56
|
root: projectRoot,
|
|
57
|
+
...(options.lazy === true && { lazy: true }),
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
return sections;
|