@jxsuite/compiler 0.28.5 → 0.30.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 +114 -109
- package/dist/compiler.js +93 -88
- package/package.json +8 -7
- package/src/cli.ts +38 -29
- package/src/compile-cli.ts +15 -7
- package/src/compiler.ts +6 -14
- package/src/shared.ts +27 -31
- package/src/site/content-loader.ts +7 -5
- package/src/site/context-injection.ts +1 -1
- package/src/site/image-cache.ts +2 -2
- package/src/site/image-optimizer.ts +11 -4
- package/src/site/image-transform.ts +5 -5
- package/src/site/pages-discovery.ts +4 -5
- package/src/site/site-build.ts +95 -42
- package/src/targets/compile-class.ts +7 -11
- package/src/targets/compile-client.ts +102 -42
- package/src/targets/compile-element.ts +82 -70
- package/src/targets/compile-static.ts +8 -0
package/src/site/site-build.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
rmSync,
|
|
18
18
|
writeFileSync,
|
|
19
19
|
} from "node:fs";
|
|
20
|
-
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
21
21
|
import { isMappedArray, isRef } from "@jxsuite/schema/guards";
|
|
22
22
|
import { loadProjectConfig } from "./site-loader.ts";
|
|
23
23
|
import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
|
|
@@ -54,6 +54,7 @@ import type {
|
|
|
54
54
|
JxAttributeValue,
|
|
55
55
|
JxElement,
|
|
56
56
|
JxHeadEntry,
|
|
57
|
+
JxMappedArray,
|
|
57
58
|
JxMutableNode,
|
|
58
59
|
JxStateDefinition,
|
|
59
60
|
JxStyle,
|
|
@@ -157,7 +158,11 @@ export async function buildSite(
|
|
|
157
158
|
formats: formatRegistry,
|
|
158
159
|
});
|
|
159
160
|
for (const f of result.files) {
|
|
160
|
-
|
|
161
|
+
// Reduce the emitted path (an absolute source path with .json swapped to .js) to a bare
|
|
162
|
+
// Filename for the dist/components/ output. basename handles both / and \ — a
|
|
163
|
+
// Forward-slash-only split left the full drive path on Windows, so resolve() then wrote
|
|
164
|
+
// The sidecar back into the source tree instead of dist.
|
|
165
|
+
const outName = basename(f.path);
|
|
161
166
|
writeFileSync(resolve(componentOutDir, outName), f.content, "utf8");
|
|
162
167
|
if (f.tagName) {
|
|
163
168
|
compiledComponentTags.push(f.tagName);
|
|
@@ -188,8 +193,7 @@ export async function buildSite(
|
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
// ── 5b. Collect server entries from components (for site-wide bundling) ──
|
|
191
|
-
|
|
192
|
-
const siteServerEntries = [];
|
|
196
|
+
const siteServerEntries: { exportName: string; src: string }[] = [];
|
|
193
197
|
if (projectConfig.build.adapter) {
|
|
194
198
|
for (const [, doc] of componentDefs) {
|
|
195
199
|
const entries = collectServerEntries(doc);
|
|
@@ -207,8 +211,7 @@ export async function buildSite(
|
|
|
207
211
|
|
|
208
212
|
const cfImages = projectConfig.images.optimize && projectConfig.images.service === "cloudflare";
|
|
209
213
|
const imageCache = projectConfig.images.optimize && !cfImages ? loadCache(projectRoot) : null;
|
|
210
|
-
|
|
211
|
-
const imageMetaCache = cfImages ? new Map() : null;
|
|
214
|
+
const imageMetaCache: ImageMetaCache | null = cfImages ? new Map() : null;
|
|
212
215
|
if (cfImages) {
|
|
213
216
|
console.log(
|
|
214
217
|
`images.service is "cloudflare" — srcsets use /cdn-cgi/image transform URLs. ` +
|
|
@@ -274,7 +277,7 @@ export async function buildSite(
|
|
|
274
277
|
mode: "export",
|
|
275
278
|
})) as string;
|
|
276
279
|
if (content) {
|
|
277
|
-
const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]);
|
|
280
|
+
const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]!);
|
|
278
281
|
writeFileSync(sidecarPath, content, "utf8");
|
|
279
282
|
fileCount += 1;
|
|
280
283
|
}
|
|
@@ -326,7 +329,7 @@ export async function buildSite(
|
|
|
326
329
|
const { adapter } = projectConfig.build;
|
|
327
330
|
log("Generating site-wide server worker...");
|
|
328
331
|
|
|
329
|
-
const deduped = new Map();
|
|
332
|
+
const deduped = new Map<string, { exportName: string; src: string }>();
|
|
330
333
|
for (const entry of siteServerEntries) {
|
|
331
334
|
if (!deduped.has(entry.exportName)) {
|
|
332
335
|
deduped.set(entry.exportName, entry);
|
|
@@ -460,7 +463,7 @@ async function compilePage(
|
|
|
460
463
|
// Determine the page title — resolve template strings against the scope
|
|
461
464
|
let title = pageTitle ?? projectConfig.name ?? "Jx Site";
|
|
462
465
|
if (typeof title === "string" && isTemplateString(title)) {
|
|
463
|
-
title = evaluateStaticTemplate(title, scope) ?? (title as string);
|
|
466
|
+
title = (evaluateStaticTemplate(title, scope) as string | null) ?? (title as string);
|
|
464
467
|
}
|
|
465
468
|
|
|
466
469
|
// Resolve template strings in $head entries
|
|
@@ -595,14 +598,16 @@ function resolveHeadTemplates(headEntries: JxHeadEntry[], scope: Record<string,
|
|
|
595
598
|
resolved.attributes = { ...resolved.attributes };
|
|
596
599
|
for (const [k, v] of Object.entries(resolved.attributes)) {
|
|
597
600
|
if (typeof v === "string" && isTemplateString(v)) {
|
|
598
|
-
resolved.attributes[k] =
|
|
601
|
+
resolved.attributes[k] =
|
|
602
|
+
(evaluateStaticTemplate(v, scope) as string | boolean | null) ??
|
|
603
|
+
(v as string | boolean);
|
|
599
604
|
}
|
|
600
605
|
}
|
|
601
606
|
}
|
|
602
607
|
if (typeof resolved.textContent === "string" && isTemplateString(resolved.textContent)) {
|
|
603
608
|
resolved.textContent =
|
|
604
|
-
evaluateStaticTemplate(resolved.textContent, scope) ??
|
|
605
|
-
|
|
609
|
+
(evaluateStaticTemplate(resolved.textContent, scope) as string | null) ??
|
|
610
|
+
resolved.textContent;
|
|
606
611
|
}
|
|
607
612
|
return resolved;
|
|
608
613
|
});
|
|
@@ -662,11 +667,11 @@ function expandMapTemplate(template: JxElement, scope: Record<string, unknown>):
|
|
|
662
667
|
const node = {} as JxElement;
|
|
663
668
|
for (const [k, v] of Object.entries(template)) {
|
|
664
669
|
if (k === "children" && Array.isArray(v)) {
|
|
665
|
-
node.children = v.map((child) => {
|
|
670
|
+
node.children = (v as (string | JxElement)[]).map((child) => {
|
|
666
671
|
if (typeof child === "string") {
|
|
667
672
|
return child;
|
|
668
673
|
}
|
|
669
|
-
return expandMapTemplate(
|
|
674
|
+
return expandMapTemplate(child, scope);
|
|
670
675
|
});
|
|
671
676
|
} else if (k === "style" && v && typeof v === "object") {
|
|
672
677
|
const style: JxStyle = { ...(v as JxStyle) };
|
|
@@ -718,10 +723,26 @@ function evaluateMapTemplate(str: string, scope: Record<string, unknown>) {
|
|
|
718
723
|
const index = (scope.$map as Record<string, unknown>)?.index;
|
|
719
724
|
const singleExprMatch = str.match(/^\$\{(.+)\}$/s);
|
|
720
725
|
if (singleExprMatch) {
|
|
721
|
-
const fn = new Function(
|
|
726
|
+
const fn = new Function(
|
|
727
|
+
"state",
|
|
728
|
+
"$map",
|
|
729
|
+
"item",
|
|
730
|
+
"index",
|
|
731
|
+
`return (${singleExprMatch[1]})`,
|
|
732
|
+
) as (
|
|
733
|
+
state: Record<string, unknown>,
|
|
734
|
+
$map: unknown,
|
|
735
|
+
item: unknown,
|
|
736
|
+
index: unknown,
|
|
737
|
+
) => unknown;
|
|
722
738
|
return fn(scope, scope.$map, item, index);
|
|
723
739
|
}
|
|
724
|
-
const fn = new Function("state", "$map", "item", "index", `return \`${str}\``)
|
|
740
|
+
const fn = new Function("state", "$map", "item", "index", `return \`${str}\``) as (
|
|
741
|
+
state: Record<string, unknown>,
|
|
742
|
+
$map: unknown,
|
|
743
|
+
item: unknown,
|
|
744
|
+
index: unknown,
|
|
745
|
+
) => unknown;
|
|
725
746
|
return fn(scope, scope.$map, item, index);
|
|
726
747
|
} catch {
|
|
727
748
|
return null;
|
|
@@ -751,49 +772,38 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
|
|
|
751
772
|
}
|
|
752
773
|
if (typeof node.textContent === "string" && isTemplateString(node.textContent)) {
|
|
753
774
|
node.textContent =
|
|
754
|
-
evaluateStaticTemplate(node.textContent, scope)
|
|
775
|
+
(evaluateStaticTemplate(node.textContent, scope) as string | null) ??
|
|
776
|
+
(node.textContent as string | null);
|
|
755
777
|
}
|
|
756
778
|
if (node.style && typeof node.style === "object") {
|
|
757
779
|
for (const [k, v] of Object.entries(node.style)) {
|
|
758
780
|
if (typeof v === "string" && isTemplateString(v)) {
|
|
759
|
-
node.style[k] =
|
|
781
|
+
node.style[k] =
|
|
782
|
+
(evaluateStaticTemplate(v, scope) as string | number | JxStyle | undefined) ??
|
|
783
|
+
(v as string | number | JxStyle);
|
|
760
784
|
}
|
|
761
785
|
}
|
|
762
786
|
}
|
|
763
787
|
if (node.attributes && typeof node.attributes === "object") {
|
|
764
788
|
for (const [k, v] of Object.entries(node.attributes)) {
|
|
765
789
|
if (typeof v === "string" && isTemplateString(v)) {
|
|
766
|
-
node.attributes[k] = evaluateStaticTemplate(v, scope) ?? v;
|
|
790
|
+
node.attributes[k] = (evaluateStaticTemplate(v, scope) as JxAttributeValue | null) ?? v;
|
|
767
791
|
}
|
|
768
792
|
}
|
|
769
793
|
}
|
|
770
794
|
if (node.$props && typeof node.$props === "object") {
|
|
771
795
|
for (const [k, v] of Object.entries(node.$props)) {
|
|
772
796
|
if (typeof v === "string" && isTemplateString(v)) {
|
|
773
|
-
node.$props[k] = evaluateStaticTemplate(v, scope) ?? v;
|
|
797
|
+
node.$props[k] = (evaluateStaticTemplate(v, scope) as JsonValue | null) ?? v;
|
|
774
798
|
}
|
|
775
799
|
}
|
|
776
800
|
}
|
|
777
801
|
const rawChildren = node.children;
|
|
802
|
+
// Legacy whole-children repeater: expand the items into the node's static children.
|
|
778
803
|
if (isMappedArray(rawChildren)) {
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
items = resolveRefValue(itemsSrc.$ref, scope);
|
|
783
|
-
} else if (Array.isArray(itemsSrc)) {
|
|
784
|
-
items = itemsSrc;
|
|
785
|
-
}
|
|
786
|
-
const mapTemplate = rawChildren.map;
|
|
787
|
-
if (Array.isArray(items) && mapTemplate) {
|
|
788
|
-
node.children = items.map((item, index) => {
|
|
789
|
-
const childScope = Object.create(scope);
|
|
790
|
-
childScope.$map = { index, item };
|
|
791
|
-
childScope["$map/item"] = item;
|
|
792
|
-
childScope["$map/index"] = index;
|
|
793
|
-
const expanded = expandMapTemplate(mapTemplate, childScope);
|
|
794
|
-
resolveDocTemplates(expanded, childScope);
|
|
795
|
-
return expanded;
|
|
796
|
-
});
|
|
804
|
+
const expanded = expandMappedArrayStatic(rawChildren, scope);
|
|
805
|
+
if (expanded) {
|
|
806
|
+
node.children = expanded;
|
|
797
807
|
return;
|
|
798
808
|
}
|
|
799
809
|
}
|
|
@@ -809,7 +819,7 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
|
|
|
809
819
|
} else if (Array.isArray(node.children)) {
|
|
810
820
|
let i = 0;
|
|
811
821
|
while (i < node.children.length) {
|
|
812
|
-
const child = node.children[i]
|
|
822
|
+
const child = node.children[i]!;
|
|
813
823
|
if (typeof child === "string" && isTemplateString(child)) {
|
|
814
824
|
const resolved = evaluateStaticTemplate(child, scope);
|
|
815
825
|
if (Array.isArray(resolved)) {
|
|
@@ -822,12 +832,55 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
|
|
|
822
832
|
continue;
|
|
823
833
|
}
|
|
824
834
|
}
|
|
835
|
+
// Array pseudo-element among siblings: expand its items in place.
|
|
836
|
+
if (isMappedArray(child)) {
|
|
837
|
+
const expanded = expandMappedArrayStatic(child, scope);
|
|
838
|
+
if (expanded) {
|
|
839
|
+
node.children.splice(i, 1, ...expanded);
|
|
840
|
+
i += expanded.length;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
825
844
|
resolveDocTemplates(child, scope);
|
|
826
845
|
i += 1;
|
|
827
846
|
}
|
|
828
847
|
}
|
|
829
848
|
}
|
|
830
849
|
|
|
850
|
+
/**
|
|
851
|
+
* Statically expand a mapped array to its resolved item nodes when `items` resolves to an array at
|
|
852
|
+
* build time, or null otherwise (leave the array node for client-side rendering).
|
|
853
|
+
*
|
|
854
|
+
* @param {JxMappedArray} arrayDef
|
|
855
|
+
* @param {Record<string, unknown>} scope
|
|
856
|
+
* @returns {(JxElement | string)[] | null}
|
|
857
|
+
*/
|
|
858
|
+
function expandMappedArrayStatic(
|
|
859
|
+
arrayDef: JxMappedArray,
|
|
860
|
+
scope: Record<string, unknown>,
|
|
861
|
+
): (JxElement | string)[] | null {
|
|
862
|
+
const itemsSrc = arrayDef.items;
|
|
863
|
+
let items: unknown = null;
|
|
864
|
+
if (isRef(itemsSrc)) {
|
|
865
|
+
items = resolveRefValue(itemsSrc.$ref, scope);
|
|
866
|
+
} else if (Array.isArray(itemsSrc)) {
|
|
867
|
+
items = itemsSrc;
|
|
868
|
+
}
|
|
869
|
+
const mapTemplate = arrayDef.map;
|
|
870
|
+
if (!Array.isArray(items) || !mapTemplate) {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
return (items as unknown[]).map((item: unknown, index) => {
|
|
874
|
+
const childScope = Object.create(scope) as Record<string, unknown>;
|
|
875
|
+
childScope.$map = { index, item };
|
|
876
|
+
childScope["$map/item"] = item;
|
|
877
|
+
childScope["$map/index"] = index;
|
|
878
|
+
const expanded = expandMapTemplate(mapTemplate, childScope);
|
|
879
|
+
resolveDocTemplates(expanded, childScope);
|
|
880
|
+
return expanded;
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
831
884
|
/** Collector for CSS rules extracted from component slot content during expansion. */
|
|
832
885
|
interface SlotCssCollector {
|
|
833
886
|
rules: string[];
|
|
@@ -853,7 +906,7 @@ function expandComponents(
|
|
|
853
906
|
return;
|
|
854
907
|
}
|
|
855
908
|
if (Array.isArray(node)) {
|
|
856
|
-
for (const n of node) {
|
|
909
|
+
for (const n of node as (JxElement | string)[]) {
|
|
857
910
|
expandComponents(n, componentDefs, slotCss);
|
|
858
911
|
}
|
|
859
912
|
return;
|
|
@@ -1012,11 +1065,11 @@ function injectHead(html: string, headEntries: JxHeadEntry[], lang: string) {
|
|
|
1012
1065
|
const existingMatch = html.match(headPattern);
|
|
1013
1066
|
let preservedBlocks = "";
|
|
1014
1067
|
if (existingMatch) {
|
|
1015
|
-
const styles = existingMatch[1]
|
|
1068
|
+
const styles = existingMatch[1]!.match(/<style>[\s\S]*?<\/style>/gi);
|
|
1016
1069
|
if (styles) {
|
|
1017
1070
|
preservedBlocks += `\n ${styles.join("\n ")}`;
|
|
1018
1071
|
}
|
|
1019
|
-
const scripts = existingMatch[1]
|
|
1072
|
+
const scripts = existingMatch[1]!.match(/<script[\s\S]*?<\/script>/gi);
|
|
1020
1073
|
if (scripts) {
|
|
1021
1074
|
preservedBlocks += `\n ${scripts.join("\n ")}`;
|
|
1022
1075
|
}
|
|
@@ -128,16 +128,16 @@ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unk
|
|
|
128
128
|
if (m.role === "accessor") {
|
|
129
129
|
// Getter
|
|
130
130
|
if (m.getter) {
|
|
131
|
-
lines.push(` ${staticPrefix}get ${prefix}${name}() {`);
|
|
132
|
-
lines.push(` ${m.getter.body}`);
|
|
133
|
-
lines.push(" }");
|
|
131
|
+
lines.push(` ${staticPrefix}get ${prefix}${name}() {`, ` ${m.getter.body}`, " }");
|
|
134
132
|
}
|
|
135
133
|
// Setter
|
|
136
134
|
if (m.setter) {
|
|
137
135
|
const setterParams = resolveParams(m.setter.parameters ?? []);
|
|
138
|
-
lines.push(
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
lines.push(
|
|
137
|
+
` ${staticPrefix}set ${prefix}${name}(${setterParams}) {`,
|
|
138
|
+
` ${m.setter.body}`,
|
|
139
|
+
" }",
|
|
140
|
+
);
|
|
141
141
|
}
|
|
142
142
|
} else {
|
|
143
143
|
lines.push(` ${staticPrefix}${asyncPrefix}${prefix}${name}(${params}) {`);
|
|
@@ -148,11 +148,7 @@ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unk
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
lines.push("}");
|
|
152
|
-
lines.push("");
|
|
153
|
-
lines.push(`export { ${className} };`);
|
|
154
|
-
lines.push(`export default ${className};`);
|
|
155
|
-
lines.push("");
|
|
151
|
+
lines.push("}", "", `export { ${className} };`, `export default ${className};`, "");
|
|
156
152
|
|
|
157
153
|
return lines.join("\n");
|
|
158
154
|
}
|
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
JsonObject,
|
|
40
40
|
JsonValue,
|
|
41
41
|
JxDocument,
|
|
42
|
+
JxMappedArray,
|
|
42
43
|
JxMutableNode,
|
|
43
44
|
JxPrototypeDef,
|
|
44
45
|
JxStyle,
|
|
@@ -481,6 +482,22 @@ function buildClientNode(
|
|
|
481
482
|
return `<${tag}${staticAttrs}${dataBindAttr2}${bindAttrStr2}>`;
|
|
482
483
|
}
|
|
483
484
|
return `<${tag}${staticAttrs}${dataBindAttr2}${bindAttrStr2}></${tag}>`;
|
|
485
|
+
} else if (Array.isArray(source.children) && source.children.some((c) => isMappedArray(c))) {
|
|
486
|
+
// ─── Children mix static nodes with array pseudo-elements ───
|
|
487
|
+
// Render the whole children region via one lit binding on this element (wrapper-less, one
|
|
488
|
+
// Lit part per element so multiple arrays among siblings don't collide), with each array
|
|
489
|
+
// Member expanded inline as a `.map()` hole.
|
|
490
|
+
counter.needsLit = true;
|
|
491
|
+
const listKey = `_children${counter.l}`;
|
|
492
|
+
counter.l += 1;
|
|
493
|
+
const childrenTpl = source.children
|
|
494
|
+
.map((c) => emitChildLit(c as JxMutableNode))
|
|
495
|
+
.join("\n ");
|
|
496
|
+
bindings.set(listKey, `() => html\`\n ${childrenTpl}\n \``);
|
|
497
|
+
bindAttrs.push(`:render="${listKey}"`);
|
|
498
|
+
needsBind = true;
|
|
499
|
+
const bindAttrStr2 = ` ${bindAttrs.join(" ")}`;
|
|
500
|
+
return `<${tag}${staticAttrs} data-bind${bindAttrStr2}></${tag}>`;
|
|
484
501
|
} else if (Array.isArray(source.children)) {
|
|
485
502
|
const rawChildren = Array.isArray(raw?.children) ? raw.children : undefined;
|
|
486
503
|
inner = source.children
|
|
@@ -611,10 +628,11 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
|
|
|
611
628
|
}
|
|
612
629
|
} else if (def.innerHTML) {
|
|
613
630
|
inner = mapRefsToLit(String(def.innerHTML));
|
|
631
|
+
} else if (isMappedArray(def.children)) {
|
|
632
|
+
// Legacy whole-children repeater nested inside a map template.
|
|
633
|
+
inner = `\n ${emitArrayHole(def.children)}\n `;
|
|
614
634
|
} else if (Array.isArray(def.children)) {
|
|
615
|
-
inner = `\n ${def.children
|
|
616
|
-
.map((c) => emitLitMapTemplate(c as JxMutableNode))
|
|
617
|
-
.join("\n ")}\n `;
|
|
635
|
+
inner = `\n ${def.children.map((c) => emitChildLit(c)).join("\n ")}\n `;
|
|
618
636
|
}
|
|
619
637
|
|
|
620
638
|
const voidTags = new Set(["input", "br", "hr", "img", "meta", "link"]);
|
|
@@ -624,6 +642,42 @@ function emitLitMapTemplate(def: JxMutableNode | undefined) {
|
|
|
624
642
|
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
625
643
|
}
|
|
626
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Emit one child of a lit template: a string text node, an array pseudo-element (expanded inline
|
|
647
|
+
* via a `.map()` hole), or a nested element.
|
|
648
|
+
*
|
|
649
|
+
* @param {JxMutableNode | string} c
|
|
650
|
+
* @returns {string}
|
|
651
|
+
*/
|
|
652
|
+
function emitChildLit(c: JxMutableNode | string): string {
|
|
653
|
+
if (typeof c === "string") {
|
|
654
|
+
return isTemplateString(c) ? mapRefsToLit(c) : escapeHtml(c);
|
|
655
|
+
}
|
|
656
|
+
if (typeof c === "number" || typeof c === "boolean") {
|
|
657
|
+
return escapeHtml(String(c));
|
|
658
|
+
}
|
|
659
|
+
if (isMappedArray(c)) {
|
|
660
|
+
return emitArrayHole(c);
|
|
661
|
+
}
|
|
662
|
+
return emitLitMapTemplate(c);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Emit a lit-html `${(items ?? []).map(...)}` hole for a mapped array, expanding its template
|
|
667
|
+
* inline among sibling nodes (wrapper-less).
|
|
668
|
+
*
|
|
669
|
+
* @param {JxMappedArray} arrayDef
|
|
670
|
+
* @returns {string}
|
|
671
|
+
*/
|
|
672
|
+
function emitArrayHole(arrayDef: JxMappedArray): string {
|
|
673
|
+
const { items } = arrayDef;
|
|
674
|
+
const itemsExpr = isRefObject(items)
|
|
675
|
+
? `state.${refToBindingKey((items as { $ref: string }).$ref)}`
|
|
676
|
+
: JSON.stringify(items ?? []);
|
|
677
|
+
const tpl = emitLitMapTemplate(arrayDef.map);
|
|
678
|
+
return `\${(${itemsExpr} ?? []).map((item, index) => html\`${tpl}\`)}`;
|
|
679
|
+
}
|
|
680
|
+
|
|
627
681
|
/**
|
|
628
682
|
* Replace $map references: $map.item → item, $map.index → index
|
|
629
683
|
*
|
|
@@ -696,15 +750,12 @@ function emitClientModule(
|
|
|
696
750
|
lines.push(`import { ${[...names].join(", ")} } from '${src}';`);
|
|
697
751
|
}
|
|
698
752
|
|
|
699
|
-
lines.push("");
|
|
700
|
-
|
|
701
753
|
// State — reactive state
|
|
702
|
-
lines.push("const state = reactive({");
|
|
754
|
+
lines.push("", "const state = reactive({");
|
|
703
755
|
for (const [key, val] of stateEntries) {
|
|
704
756
|
lines.push(` ${key}: ${JSON.stringify(val)},`);
|
|
705
757
|
}
|
|
706
|
-
lines.push("});");
|
|
707
|
-
lines.push("");
|
|
758
|
+
lines.push("});", "");
|
|
708
759
|
|
|
709
760
|
// Prototype init blocks (Request fetch, LocalStorage read, etc.)
|
|
710
761
|
if (initBlocks.length > 0) {
|
|
@@ -750,38 +801,43 @@ function emitClientModule(
|
|
|
750
801
|
} else {
|
|
751
802
|
lines.push("const on = {};");
|
|
752
803
|
}
|
|
753
|
-
lines.push("");
|
|
754
|
-
|
|
755
804
|
// Hydration function
|
|
756
|
-
lines.push(
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
805
|
+
lines.push(
|
|
806
|
+
"",
|
|
807
|
+
"function hydrate(root) {",
|
|
808
|
+
" root.querySelectorAll('[data-bind]').forEach(el => {",
|
|
809
|
+
" [...el.attributes].forEach(a => {",
|
|
810
|
+
" if (a.name.startsWith(':')) {",
|
|
811
|
+
" const parts = a.name.slice(1).split('.');",
|
|
812
|
+
" const key = a.value;",
|
|
813
|
+
);
|
|
762
814
|
if (needsLit) {
|
|
763
|
-
lines.push(
|
|
764
|
-
|
|
765
|
-
|
|
815
|
+
lines.push(
|
|
816
|
+
" if (parts[0] === 'render') {",
|
|
817
|
+
" effect(() => { render(bind[key](), el); });",
|
|
818
|
+
" } else if (parts[0] === 'style' && parts.length > 1) {",
|
|
819
|
+
);
|
|
766
820
|
} else {
|
|
767
821
|
lines.push(" if (parts[0] === 'style' && parts.length > 1) {");
|
|
768
822
|
}
|
|
769
|
-
lines.push(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
823
|
+
lines.push(
|
|
824
|
+
" effect(() => { el.style[parts[1]] = bind[key](); });",
|
|
825
|
+
" } else if (parts[0] === 'attr' && parts.length > 1) {",
|
|
826
|
+
" effect(() => { el.setAttribute(parts[1], bind[key]()); });",
|
|
827
|
+
" } else {",
|
|
828
|
+
" const prop = parts[0].replace(/-([a-z])/g, (_, c) => c.toUpperCase());",
|
|
829
|
+
" effect(() => { el[prop] = bind[key](); });",
|
|
830
|
+
" }",
|
|
831
|
+
" } else if (a.name.startsWith('@')) {",
|
|
832
|
+
" el.addEventListener(a.name.slice(1), on[a.value]);",
|
|
833
|
+
" }",
|
|
834
|
+
" });",
|
|
835
|
+
" });",
|
|
836
|
+
"}",
|
|
837
|
+
"",
|
|
838
|
+
"hydrate(document);",
|
|
839
|
+
"",
|
|
840
|
+
);
|
|
785
841
|
|
|
786
842
|
return lines.join("\n");
|
|
787
843
|
}
|
|
@@ -808,8 +864,10 @@ function emitRequestInit(key: string, def: JxPrototypeDef) {
|
|
|
808
864
|
];
|
|
809
865
|
|
|
810
866
|
if (isTemplateUrl) {
|
|
811
|
-
lines.push(
|
|
812
|
-
|
|
867
|
+
lines.push(
|
|
868
|
+
` const url = \`${url}\`;`,
|
|
869
|
+
' if (!url || url === "undefined" || url.includes("undefined")) return;',
|
|
870
|
+
);
|
|
813
871
|
} else {
|
|
814
872
|
lines.push(` const url = ${JSON.stringify(url)};`);
|
|
815
873
|
}
|
|
@@ -830,11 +888,13 @@ function emitRequestInit(key: string, def: JxPrototypeDef) {
|
|
|
830
888
|
}
|
|
831
889
|
|
|
832
890
|
const optsStr = fetchOpts.length > 0 ? `, { ${fetchOpts.join(", ")} }` : "";
|
|
833
|
-
lines.push(
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
891
|
+
lines.push(
|
|
892
|
+
` fetch(url${optsStr})`,
|
|
893
|
+
" .then(r => r.ok ? r.json() : Promise.reject(r.statusText))",
|
|
894
|
+
` .then(d => { state.${key} = d; })`,
|
|
895
|
+
` .catch(e => { state.${key} = { error: String(e) }; });`,
|
|
896
|
+
"});",
|
|
897
|
+
);
|
|
838
898
|
|
|
839
899
|
return lines.join("\n");
|
|
840
900
|
}
|