@jxsuite/compiler 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Compile-static.js — Static HTML compilation
3
+ *
4
+ * Compiles fully static Jx documents to plain HTML/CSS with zero JS. Dynamic child subtrees become
5
+ * hydration islands (custom elements).
6
+ */
7
+
8
+ import {
9
+ isNodeDynamic,
10
+ createCompileContext,
11
+ resolveStaticValue,
12
+ buildAttrs,
13
+ compileStyles,
14
+ escapeHtml,
15
+ } from "../shared.js";
16
+ import { emitElementModule } from "./compile-element.js";
17
+
18
+ /**
19
+ * Compile a static document to HTML, with dynamic subtrees as islands.
20
+ *
21
+ * @param {any} raw - Raw JSON document (with $ref pointers preserved)
22
+ * @param {any} opts
23
+ * @returns {{ html: string; files: { path: string; content: string; tagName: string }[] }}
24
+ */
25
+ export function compileStaticPage(raw, opts) {
26
+ const { title, reactivitySrc, litHtmlSrc } = opts;
27
+
28
+ const rootContext = createCompileContext(raw, null, raw.state ?? {}, raw.$media ?? {});
29
+ const styleBlock = compileStyles(raw, raw.$media ?? {});
30
+ /** @type {{ def: any; tagName: string; className: string }[]} */
31
+ const islands = [];
32
+ const bodyContent = compileNode(raw, false, raw, rootContext, islands);
33
+
34
+ /** @type {{ path: string; content: string; tagName: string }[]} */
35
+ const files = [];
36
+ let importMap = "";
37
+ let moduleScripts = "";
38
+ if (islands.length > 0) {
39
+ for (const island of islands) {
40
+ const moduleContent = emitElementModule(island.def, island.className, []);
41
+ files.push({
42
+ path: `_islands/${island.tagName}.js`,
43
+ content: moduleContent,
44
+ tagName: island.tagName,
45
+ });
46
+ }
47
+ importMap = `<script type="importmap">
48
+ {
49
+ "imports": {
50
+ "@vue/reactivity": "${reactivitySrc}",
51
+ "lit-html": "${litHtmlSrc}"
52
+ }
53
+ }
54
+ </script>`;
55
+ moduleScripts = files
56
+ .map(
57
+ (/** @type {{ path: string }} */ f) => `<script type="module" src="./${f.path}"></script>`,
58
+ )
59
+ .join("\n ");
60
+ }
61
+
62
+ const html = `<!DOCTYPE html>
63
+ <html lang="en">
64
+ <head>
65
+ <meta charset="utf-8">
66
+ <meta name="viewport" content="width=device-width, initial-scale=1">
67
+ <title>${escapeHtml(title)}</title>
68
+ ${importMap}
69
+ ${styleBlock}
70
+ </head>
71
+ <body>
72
+ ${bodyContent}
73
+ ${moduleScripts}
74
+ </body>
75
+ </html>`;
76
+
77
+ return { html, files };
78
+ }
79
+
80
+ // ─── Node compilation ─────────────────────────────────────────────────────────
81
+
82
+ /**
83
+ * Compile a single Jx node to an HTML string. Dynamic nodes become hydration islands; static nodes
84
+ * become plain HTML.
85
+ *
86
+ * @param {any} def
87
+ * @param {boolean} dynamic
88
+ * @param {any} raw
89
+ * @param {any} context
90
+ * @param {{ def: any; tagName: string; className: string }[]} islands
91
+ * @returns {string}
92
+ */
93
+ function compileNode(def, dynamic, raw, context, islands) {
94
+ // String children are text nodes
95
+ if (typeof def === "string") {
96
+ return escapeHtml(def);
97
+ }
98
+ if (typeof def === "number" || typeof def === "boolean") {
99
+ return escapeHtml(String(def));
100
+ }
101
+ if (!def || typeof def !== "object") return "";
102
+
103
+ const nextContext = createCompileContext(
104
+ raw,
105
+ context.scope,
106
+ raw?.state ?? context.scopeDefs,
107
+ raw?.$media ?? context.media,
108
+ );
109
+
110
+ if (dynamic) {
111
+ const n = islands.length;
112
+ const tagName = `jx-island-${n}`;
113
+ const className = `JxIsland${n}`;
114
+ const elementDef = { ...(raw ?? def), tagName };
115
+ islands.push({ def: elementDef, tagName, className });
116
+ return `<${tagName}></${tagName}>`;
117
+ }
118
+
119
+ const tag = def.tagName ?? "div";
120
+ const attrs = buildAttrs(def, nextContext.scope);
121
+ const inner = buildInnerWithIslands(def, raw, nextContext, islands);
122
+
123
+ return `<${tag}${attrs}>${inner}</${tag}>`;
124
+ }
125
+
126
+ /**
127
+ * Build the inner HTML (textContent or children) for a node. For children, emit islands only for
128
+ * those that are actually dynamic.
129
+ *
130
+ * @param {any} def
131
+ * @param {any} raw
132
+ * @param {any} context
133
+ * @param {{ def: any; tagName: string; className: string }[]} islands
134
+ * @returns {string}
135
+ */
136
+ function buildInnerWithIslands(def, raw, context, islands) {
137
+ const source = raw ?? def;
138
+
139
+ if (source.textContent !== undefined) {
140
+ const value = resolveStaticValue(source.textContent, context.scope);
141
+ return value == null ? "" : escapeHtml(String(value));
142
+ }
143
+ if (source.innerHTML) return resolveStaticValue(source.innerHTML, context.scope) ?? "";
144
+ if (Array.isArray(source.children)) {
145
+ const rawChildren = raw?.children;
146
+ return source.children
147
+ .map((/** @type {any} */ c, /** @type {number} */ i) => {
148
+ const childDynamic = isNodeDynamic(c);
149
+ const childRaw = rawChildren?.[i] ?? c;
150
+ return compileNode(c, childDynamic, childRaw, context, islands);
151
+ })
152
+ .join("\n ");
153
+ }
154
+ return "";
155
+ }