@sigil-dev/compiler 0.9.4 → 0.9.5

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.
Files changed (40) hide show
  1. package/index.ts +4 -4
  2. package/jsr.json +8 -8
  3. package/package.json +2 -1
  4. package/src/babel/handlers/dom/derived.ts +62 -62
  5. package/src/babel/handlers/dom/effect.ts +15 -15
  6. package/src/babel/handlers/dom/globals.ts +105 -105
  7. package/src/babel/handlers/dom/state.ts +107 -107
  8. package/src/babel/handlers/ssr/derived.ts +27 -27
  9. package/src/babel/handlers/ssr/state.ts +11 -11
  10. package/src/babel/index.ts +8 -0
  11. package/src/babel/jsx/anchor-mount.ts +385 -385
  12. package/src/babel/jsx/children.ts +124 -124
  13. package/src/babel/jsx/element.ts +848 -850
  14. package/src/babel/jsx/fragment.ts +282 -282
  15. package/src/babel/jsx/index.ts +2 -2
  16. package/src/babel/jsx/keyed-list.ts +309 -309
  17. package/src/babel/jsx/ssr.ts +351 -324
  18. package/src/babel/jsx/text-node.ts +192 -192
  19. package/src/babel/jsx/utils.ts +179 -179
  20. package/src/babel/util/bind.ts +139 -139
  21. package/src/babel/util/css.ts +151 -151
  22. package/src/babel/util/dead-code.ts +28 -28
  23. package/src/babel/util/helpers.ts +46 -46
  24. package/src/babel/util/magic.ts +11 -11
  25. package/src/bun-plugin.ts +92 -92
  26. package/src/vite/index.ts +58 -58
  27. package/test/async-components.test.ts +59 -59
  28. package/test/components.test.ts +129 -129
  29. package/test/containsSignal.test.ts +288 -288
  30. package/test/fragments.test.ts +90 -90
  31. package/test/helpers/transform.ts +45 -45
  32. package/test/hydration.test.ts +147 -147
  33. package/test/jsx.test.ts +227 -225
  34. package/test/keyed-lists.test.ts +126 -126
  35. package/test/reactivity.test.ts +225 -225
  36. package/test/scoped-css.test.ts +129 -129
  37. package/test/ssr.test.ts +130 -130
  38. package/test/style-object.test.ts +13 -0
  39. package/test/tree-shaking.test.ts +78 -78
  40. package/tsconfig.json +30 -30
@@ -1,324 +1,351 @@
1
- import { types as t } from "@babel/core";
2
- import { isGlobalComponent } from "../handlers/dom/globals";
3
- import { getTagName } from "../util/helpers";
4
-
5
- const VOID_ELEMENTS = new Set([
6
- "area",
7
- "base",
8
- "br",
9
- "col",
10
- "embed",
11
- "hr",
12
- "img",
13
- "input",
14
- "link",
15
- "meta",
16
- "param",
17
- "source",
18
- "track",
19
- "wbr",
20
- ]);
21
-
22
- const ATTR_MAP_SSR: Record<string, string> = {
23
- className: "class",
24
- htmlFor: "for",
25
- };
26
- //biome-ignore lint/suspicious/noShadowRestrictedNames: shut up
27
- function escape(expr: t.Expression): t.CallExpression {
28
- return t.callExpression(t.identifier("__e"), [expr]);
29
- }
30
-
31
- // builds a template literal from alternating string parts and expressions
32
- function buildTemplate(parts: Array<string | t.Expression>): t.TemplateLiteral {
33
- const quasis: t.TemplateElement[] = [];
34
- const exprs: t.Expression[] = [];
35
- let current = "";
36
-
37
- for (const part of parts) {
38
- if (typeof part === "string") {
39
- current += part;
40
- } else {
41
- quasis.push(t.templateElement({ raw: current, cooked: current }));
42
- current = "";
43
- exprs.push(part);
44
- }
45
- }
46
- quasis.push(t.templateElement({ raw: current, cooked: current }, true));
47
- return t.templateLiteral(quasis, exprs);
48
- }
49
-
50
- /**
51
- * <Component /> is always JSX so its always safe to render as is
52
- * but the compiler can also register its own safe variables, like:
53
- * ```ts
54
- * const rendered = <Component foo="bar" />
55
- * return <div>{rendered}</div>
56
- * ```
57
- * @param expr
58
- * @param safeIdents
59
- * @returns
60
- */
61
- /*
62
- function isSSRSafe(expr: t.Expression, safeIdents: Set<string>): boolean {
63
- // direct JSX - compiled to HTML string by this very plugin
64
- if (t.isJSXElement(expr) || t.isJSXFragment(expr)) return true;
65
-
66
- // component call - returns HTML string by convention
67
- if (
68
- t.isCallExpression(expr) &&
69
- t.isIdentifier(expr.callee) &&
70
- /^[A-Z]/.test(expr.callee.name)
71
- ) return true;
72
-
73
- // registered by compiler (e.g. children)
74
- if (t.isIdentifier(expr)) {
75
- // if sigil as a compiler doesnt have an insane CVE
76
- // this will always be true
77
- if (expr.name === "children") return true;
78
- return safeIdents.has(expr.name);
79
- }
80
-
81
- // condition && <JSX> — right side produces HTML
82
- if (t.isLogicalExpression(expr))
83
- return isSSRSafe(expr.right as t.Expression, safeIdents);
84
-
85
- // condition ? <JSX> : <JSX> — both branches must be checked
86
- if (t.isConditionalExpression(expr)) {
87
- return (
88
- isSSRSafe(expr.consequent as t.Expression, safeIdents) ||
89
- isSSRSafe(expr.alternate as t.Expression, safeIdents)
90
- );
91
- }
92
- if (
93
- t.isCallExpression(expr) &&
94
- t.isMemberExpression(expr.callee) &&
95
- t.isIdentifier(expr.callee.property) &&
96
- expr.callee.property.name === "map"
97
- ) {
98
- const cb = expr.arguments[0];
99
- if (t.isArrowFunctionExpression(cb) || t.isFunctionExpression(cb)) {
100
- const body = cb.body;
101
- if (t.isJSXElement(body) || t.isJSXFragment(body)) return true;
102
- if (t.isBlockStatement(body)) {
103
- for (const stmt of body.body) {
104
- if (
105
- t.isReturnStatement(stmt) &&
106
- stmt.argument &&
107
- (t.isJSXElement(stmt.argument) || t.isJSXFragment(stmt.argument))
108
- ) return true;
109
- }
110
- }
111
- }
112
- }
113
- return false;
114
- }
115
- */
116
- export function processElementSSR(
117
- node: t.JSXElement,
118
- signals: Set<string>,
119
- ): t.TemplateLiteral {
120
- const tagName = getTagName(node.openingElement.name);
121
- const isComponent = /^[A-Z]/.test(tagName);
122
-
123
- if (isComponent) {
124
- // Global components (Window/Document/Body) produce no SSR output
125
- if (isGlobalComponent(tagName)) {
126
- return t.templateLiteral(
127
- [t.templateElement({ raw: "", cooked: "" }, true)],
128
- [],
129
- );
130
- }
131
- return processComponentSSR(node, tagName, signals);
132
- }
133
- let dangerousHTML: t.Expression | null = null;
134
- const parts: Array<string | t.Expression> = [];
135
- parts.push(`<${tagName}`);
136
-
137
- // attributes
138
- for (const attr of node.openingElement.attributes) {
139
- if (!t.isJSXAttribute(attr)) continue;
140
-
141
- // namespaced attributes are client-only directives
142
- // (transition:/in:/out:/bind:) — SSR drops them entirely
143
- if (t.isJSXNamespacedName(attr.name)) continue;
144
-
145
- const attrName = (attr.name as t.JSXIdentifier).name;
146
-
147
- if (attrName === "innerHTML") {
148
- if (t.isJSXExpressionContainer(attr.value)) {
149
- dangerousHTML = attr.value.expression as t.Expression;
150
- }
151
- continue; // don't output as attribute
152
- }
153
- // drop event handlers
154
- if (attrName.startsWith("on")) continue;
155
- // drop client-only props
156
- if (attrName === "use") continue;
157
- if (attrName.startsWith("bind")) continue;
158
-
159
- const domAttr = ATTR_MAP_SSR[attrName] ?? attrName;
160
-
161
- if (t.isStringLiteral(attr.value)) {
162
- parts.push(` ${domAttr}="${attr.value.value}"`);
163
- } else if (t.isJSXExpressionContainer(attr.value)) {
164
- const expr = attr.value.expression as t.Expression;
165
- parts.push(` ${domAttr}="`);
166
- parts.push(escape(expr));
167
- parts.push(`"`);
168
- }
169
- }
170
-
171
- if (VOID_ELEMENTS.has(tagName)) {
172
- parts.push(">");
173
- return buildTemplate(parts);
174
- }
175
-
176
- parts.push(">");
177
-
178
- if (dangerousHTML) {
179
- parts.push(dangerousHTML);
180
- parts.push(`</${tagName}>`);
181
- return buildTemplate(parts);
182
- }
183
-
184
- // children
185
- for (const child of node.children) {
186
- if (t.isJSXText(child)) {
187
- const text = child.value;
188
- if (!text.trim()) continue;
189
- const normalized = text.replace(/\s*\n\s*/g, " ");
190
- parts.push(normalized);
191
- } else if (t.isJSXElement(child)) {
192
- parts.push(processElementSSR(child, signals));
193
- } else if (t.isJSXFragment(child)) {
194
- parts.push(processFragmentSSR(child, signals));
195
- } else if (t.isJSXExpressionContainer(child)) {
196
- if (t.isJSXEmptyExpression(child.expression)) continue;
197
- const expr = child.expression as t.Expression;
198
- if (t.isJSXElement(expr)) {
199
- parts.push(processElementSSR(expr, signals));
200
- } else if (t.isJSXFragment(expr)) {
201
- parts.push(processFragmentSSR(expr, signals));
202
- } else {
203
- // Delimit dynamic text segments so SSR produces separate text
204
- // nodes that the hydrate compiler can claim individually.
205
- parts.push("<!--g-->");
206
- parts.push(escape(expr));
207
- parts.push("<!--/g-->");
208
- }
209
- }
210
- }
211
-
212
- parts.push(`</${tagName}>`);
213
- return buildTemplate(parts);
214
- }
215
-
216
- function processComponentSSR(
217
- node: t.JSXElement,
218
- tagName: string,
219
- signals: Set<string>,
220
- ): t.TemplateLiteral {
221
- const props = t.objectExpression([]);
222
-
223
- for (const attr of node.openingElement.attributes) {
224
- if (!t.isJSXAttribute(attr)) continue;
225
- const attrName = (attr.name as t.JSXIdentifier).name;
226
- // data-key is a framework-internal attribute for HTML elements only; skip for components
227
- if (attrName === "data-key" || attrName === "key") continue;
228
- // use StringLiteral key for hyphenated names, Identifier otherwise
229
- const propKey = /[^a-zA-Z_$0-9]/.test(attrName)
230
- ? t.stringLiteral(attrName)
231
- : t.identifier(attrName);
232
- if (t.isStringLiteral(attr.value)) {
233
- props.properties.push(t.objectProperty(propKey, attr.value));
234
- } else if (t.isJSXExpressionContainer(attr.value)) {
235
- props.properties.push(
236
- t.objectProperty(propKey, attr.value.expression as t.Expression),
237
- );
238
- }
239
- }
240
-
241
- // children as string
242
- const childParts: Array<string | t.Expression> = [];
243
- for (const child of node.children) {
244
- if (t.isJSXText(child)) {
245
- const text = child.value;
246
- if (!text.trim()) continue;
247
- const normalized = text.replace(/\s*\n\s*/g, " ");
248
- childParts.push(normalized);
249
- } else if (t.isJSXElement(child)) {
250
- childParts.push(processElementSSR(child, signals));
251
- } else if (t.isJSXFragment(child)) {
252
- childParts.push(processFragmentSSR(child, signals));
253
- } else if (t.isJSXExpressionContainer(child)) {
254
- if (t.isJSXEmptyExpression(child.expression)) continue;
255
- const expr = child.expression as t.Expression;
256
- if (t.isJSXElement(expr)) {
257
- childParts.push(processElementSSR(expr, signals));
258
- } else if (t.isJSXFragment(expr)) {
259
- childParts.push(processFragmentSSR(expr, signals));
260
- } else {
261
- childParts.push("<!--g-->");
262
- childParts.push(escape(expr));
263
- childParts.push("<!--/g-->");
264
- }
265
- }
266
- }
267
-
268
- if (childParts.length > 0) {
269
- props.properties.push(
270
- t.objectProperty(
271
- t.identifier("children"),
272
- t.callExpression(t.identifier("__h"), [buildTemplate(childParts)]),
273
- ),
274
- );
275
- }
276
-
277
- // Component({ ...props }) - result is already a string
278
- const call = t.callExpression(t.identifier("__h"), [
279
- t.callExpression(t.identifier(tagName), [props]),
280
- ]);
281
-
282
- // wrap in template so return type is consistent
283
- return t.templateLiteral(
284
- [
285
- t.templateElement({ raw: "", cooked: "" }),
286
- t.templateElement({ raw: "", cooked: "" }, true),
287
- ],
288
- [call],
289
- );
290
- }
291
-
292
- export function processFragmentSSR(
293
- node: t.JSXFragment,
294
- signals: Set<string>,
295
- ): t.TemplateLiteral {
296
- const parts: Array<string | t.Expression> = [];
297
-
298
- for (const child of node.children) {
299
- if (t.isJSXText(child)) {
300
- const text = child.value;
301
- if (!text.trim()) continue;
302
- const normalized = text.replace(/\s*\n\s*/g, " ");
303
- parts.push(normalized);
304
- } else if (t.isJSXElement(child)) {
305
- parts.push(processElementSSR(child, signals));
306
- } else if (t.isJSXFragment(child)) {
307
- parts.push(processFragmentSSR(child, signals));
308
- } else if (t.isJSXExpressionContainer(child)) {
309
- if (t.isJSXEmptyExpression(child.expression)) continue;
310
- const expr = child.expression as t.Expression;
311
- if (t.isJSXElement(expr)) {
312
- parts.push(processElementSSR(expr, signals));
313
- } else if (t.isJSXFragment(expr)) {
314
- parts.push(processFragmentSSR(expr, signals));
315
- } else {
316
- parts.push("<!--g-->");
317
- parts.push(escape(expr));
318
- parts.push("<!--/g-->");
319
- }
320
- }
321
- }
322
-
323
- return buildTemplate(parts);
324
- }
1
+ import { types as t } from "@babel/core";
2
+ import { isGlobalComponent } from "../handlers/dom/globals";
3
+ import { getTagName } from "../util/helpers";
4
+
5
+ const VOID_ELEMENTS = new Set([
6
+ "area",
7
+ "base",
8
+ "br",
9
+ "col",
10
+ "embed",
11
+ "hr",
12
+ "img",
13
+ "input",
14
+ "link",
15
+ "meta",
16
+ "param",
17
+ "source",
18
+ "track",
19
+ "wbr",
20
+ ]);
21
+
22
+ const ATTR_MAP_SSR: Record<string, string> = {
23
+ className: "class",
24
+ htmlFor: "for",
25
+ };
26
+ //biome-ignore lint/suspicious/noShadowRestrictedNames: shut up
27
+ function escape(expr: t.Expression): t.CallExpression {
28
+ return t.callExpression(t.identifier("__e"), [expr]);
29
+ }
30
+
31
+ // builds a template literal from alternating string parts and expressions
32
+ function buildTemplate(parts: Array<string | t.Expression>): t.TemplateLiteral {
33
+ const quasis: t.TemplateElement[] = [];
34
+ const exprs: t.Expression[] = [];
35
+ let current = "";
36
+
37
+ for (const part of parts) {
38
+ if (typeof part === "string") {
39
+ current += part;
40
+ } else {
41
+ quasis.push(t.templateElement({ raw: current, cooked: current }));
42
+ current = "";
43
+ exprs.push(part);
44
+ }
45
+ }
46
+ quasis.push(t.templateElement({ raw: current, cooked: current }, true));
47
+ return t.templateLiteral(quasis, exprs);
48
+ }
49
+
50
+ /**
51
+ * <Component /> is always JSX so its always safe to render as is
52
+ * but the compiler can also register its own safe variables, like:
53
+ * ```ts
54
+ * const rendered = <Component foo="bar" />
55
+ * return <div>{rendered}</div>
56
+ * ```
57
+ * @param expr
58
+ * @param safeIdents
59
+ * @returns
60
+ */
61
+ /*
62
+ function isSSRSafe(expr: t.Expression, safeIdents: Set<string>): boolean {
63
+ // direct JSX - compiled to HTML string by this very plugin
64
+ if (t.isJSXElement(expr) || t.isJSXFragment(expr)) return true;
65
+
66
+ // component call - returns HTML string by convention
67
+ if (
68
+ t.isCallExpression(expr) &&
69
+ t.isIdentifier(expr.callee) &&
70
+ /^[A-Z]/.test(expr.callee.name)
71
+ ) return true;
72
+
73
+ // registered by compiler (e.g. children)
74
+ if (t.isIdentifier(expr)) {
75
+ // if sigil as a compiler doesnt have an insane CVE
76
+ // this will always be true
77
+ if (expr.name === "children") return true;
78
+ return safeIdents.has(expr.name);
79
+ }
80
+
81
+ // condition && <JSX> — right side produces HTML
82
+ if (t.isLogicalExpression(expr))
83
+ return isSSRSafe(expr.right as t.Expression, safeIdents);
84
+
85
+ // condition ? <JSX> : <JSX> — both branches must be checked
86
+ if (t.isConditionalExpression(expr)) {
87
+ return (
88
+ isSSRSafe(expr.consequent as t.Expression, safeIdents) ||
89
+ isSSRSafe(expr.alternate as t.Expression, safeIdents)
90
+ );
91
+ }
92
+ if (
93
+ t.isCallExpression(expr) &&
94
+ t.isMemberExpression(expr.callee) &&
95
+ t.isIdentifier(expr.callee.property) &&
96
+ expr.callee.property.name === "map"
97
+ ) {
98
+ const cb = expr.arguments[0];
99
+ if (t.isArrowFunctionExpression(cb) || t.isFunctionExpression(cb)) {
100
+ const body = cb.body;
101
+ if (t.isJSXElement(body) || t.isJSXFragment(body)) return true;
102
+ if (t.isBlockStatement(body)) {
103
+ for (const stmt of body.body) {
104
+ if (
105
+ t.isReturnStatement(stmt) &&
106
+ stmt.argument &&
107
+ (t.isJSXElement(stmt.argument) || t.isJSXFragment(stmt.argument))
108
+ ) return true;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ return false;
114
+ }
115
+ */
116
+ export function processElementSSR(
117
+ node: t.JSXElement,
118
+ signals: Set<string>,
119
+ ): t.TemplateLiteral {
120
+ const tagName = getTagName(node.openingElement.name);
121
+ const isComponent = /^[A-Z]/.test(tagName);
122
+
123
+ if (isComponent) {
124
+ // Global components (Window/Document/Body) produce no SSR output
125
+ if (isGlobalComponent(tagName)) {
126
+ return t.templateLiteral(
127
+ [t.templateElement({ raw: "", cooked: "" }, true)],
128
+ [],
129
+ );
130
+ }
131
+ return processComponentSSR(node, tagName, signals);
132
+ }
133
+ let dangerousHTML: t.Expression | null = null;
134
+ const parts: Array<string | t.Expression> = [];
135
+ parts.push(`<${tagName}`);
136
+
137
+ // attributes
138
+ for (const attr of node.openingElement.attributes) {
139
+ if (!t.isJSXAttribute(attr)) continue;
140
+
141
+ // namespaced attributes are client-only directives
142
+ // (transition:/in:/out:/bind:) — SSR drops them entirely
143
+ if (t.isJSXNamespacedName(attr.name)) continue;
144
+
145
+ const attrName = (attr.name as t.JSXIdentifier).name;
146
+
147
+ if (attrName === "innerHTML") {
148
+ if (t.isJSXExpressionContainer(attr.value)) {
149
+ dangerousHTML = attr.value.expression as t.Expression;
150
+ }
151
+ continue; // don't output as attribute
152
+ }
153
+ // drop event handlers
154
+ if (attrName.startsWith("on")) continue;
155
+ // drop client-only props
156
+ if (attrName === "use") continue;
157
+ if (attrName.startsWith("bind")) continue;
158
+
159
+ const domAttr = ATTR_MAP_SSR[attrName] ?? attrName;
160
+
161
+ if (t.isStringLiteral(attr.value)) {
162
+ parts.push(` ${domAttr}="${attr.value.value}"`);
163
+ } else if (t.isJSXExpressionContainer(attr.value)) {
164
+ const expr = attr.value.expression as t.Expression;
165
+ if (
166
+ attrName === "style" &&
167
+ t.isObjectExpression(expr) &&
168
+ expr.properties.every((prop) => t.isObjectProperty(prop))
169
+ ) {
170
+ // style={{ key: value, ... }} — serialize to real "key:value;"
171
+ // CSS text instead of the generic String(expr) path below, which
172
+ // would stringify the object itself to the literal "[object Object]"
173
+ parts.push(` style="`);
174
+ for (const prop of expr.properties as t.ObjectProperty[]) {
175
+ const key = t.isIdentifier(prop.key)
176
+ ? prop.key.name
177
+ : t.isStringLiteral(prop.key)
178
+ ? prop.key.value
179
+ : null;
180
+ if (key === null) continue;
181
+ const cssKey = key.replace(
182
+ /[A-Z]/g,
183
+ (letter) => `-${letter.toLowerCase()}`,
184
+ );
185
+ parts.push(`${cssKey}:`);
186
+ parts.push(escape(prop.value as t.Expression));
187
+ parts.push(";");
188
+ }
189
+ parts.push(`"`);
190
+ } else {
191
+ parts.push(` ${domAttr}="`);
192
+ parts.push(escape(expr));
193
+ parts.push(`"`);
194
+ }
195
+ }
196
+ }
197
+
198
+ if (VOID_ELEMENTS.has(tagName)) {
199
+ parts.push(">");
200
+ return buildTemplate(parts);
201
+ }
202
+
203
+ parts.push(">");
204
+
205
+ if (dangerousHTML) {
206
+ parts.push(dangerousHTML);
207
+ parts.push(`</${tagName}>`);
208
+ return buildTemplate(parts);
209
+ }
210
+
211
+ // children
212
+ for (const child of node.children) {
213
+ if (t.isJSXText(child)) {
214
+ const text = child.value;
215
+ if (!text.trim()) continue;
216
+ const normalized = text.replace(/\s*\n\s*/g, " ");
217
+ parts.push(normalized);
218
+ } else if (t.isJSXElement(child)) {
219
+ parts.push(processElementSSR(child, signals));
220
+ } else if (t.isJSXFragment(child)) {
221
+ parts.push(processFragmentSSR(child, signals));
222
+ } else if (t.isJSXExpressionContainer(child)) {
223
+ if (t.isJSXEmptyExpression(child.expression)) continue;
224
+ const expr = child.expression as t.Expression;
225
+ if (t.isJSXElement(expr)) {
226
+ parts.push(processElementSSR(expr, signals));
227
+ } else if (t.isJSXFragment(expr)) {
228
+ parts.push(processFragmentSSR(expr, signals));
229
+ } else {
230
+ // Delimit dynamic text segments so SSR produces separate text
231
+ // nodes that the hydrate compiler can claim individually.
232
+ parts.push("<!--g-->");
233
+ parts.push(escape(expr));
234
+ parts.push("<!--/g-->");
235
+ }
236
+ }
237
+ }
238
+
239
+ parts.push(`</${tagName}>`);
240
+ return buildTemplate(parts);
241
+ }
242
+
243
+ function processComponentSSR(
244
+ node: t.JSXElement,
245
+ tagName: string,
246
+ signals: Set<string>,
247
+ ): t.TemplateLiteral {
248
+ const props = t.objectExpression([]);
249
+
250
+ for (const attr of node.openingElement.attributes) {
251
+ if (!t.isJSXAttribute(attr)) continue;
252
+ const attrName = (attr.name as t.JSXIdentifier).name;
253
+ // data-key is a framework-internal attribute for HTML elements only; skip for components
254
+ if (attrName === "data-key" || attrName === "key") continue;
255
+ // use StringLiteral key for hyphenated names, Identifier otherwise
256
+ const propKey = /[^a-zA-Z_$0-9]/.test(attrName)
257
+ ? t.stringLiteral(attrName)
258
+ : t.identifier(attrName);
259
+ if (t.isStringLiteral(attr.value)) {
260
+ props.properties.push(t.objectProperty(propKey, attr.value));
261
+ } else if (t.isJSXExpressionContainer(attr.value)) {
262
+ props.properties.push(
263
+ t.objectProperty(propKey, attr.value.expression as t.Expression),
264
+ );
265
+ }
266
+ }
267
+
268
+ // children as string
269
+ const childParts: Array<string | t.Expression> = [];
270
+ for (const child of node.children) {
271
+ if (t.isJSXText(child)) {
272
+ const text = child.value;
273
+ if (!text.trim()) continue;
274
+ const normalized = text.replace(/\s*\n\s*/g, " ");
275
+ childParts.push(normalized);
276
+ } else if (t.isJSXElement(child)) {
277
+ childParts.push(processElementSSR(child, signals));
278
+ } else if (t.isJSXFragment(child)) {
279
+ childParts.push(processFragmentSSR(child, signals));
280
+ } else if (t.isJSXExpressionContainer(child)) {
281
+ if (t.isJSXEmptyExpression(child.expression)) continue;
282
+ const expr = child.expression as t.Expression;
283
+ if (t.isJSXElement(expr)) {
284
+ childParts.push(processElementSSR(expr, signals));
285
+ } else if (t.isJSXFragment(expr)) {
286
+ childParts.push(processFragmentSSR(expr, signals));
287
+ } else {
288
+ childParts.push("<!--g-->");
289
+ childParts.push(escape(expr));
290
+ childParts.push("<!--/g-->");
291
+ }
292
+ }
293
+ }
294
+
295
+ if (childParts.length > 0) {
296
+ props.properties.push(
297
+ t.objectProperty(
298
+ t.identifier("children"),
299
+ t.callExpression(t.identifier("__h"), [buildTemplate(childParts)]),
300
+ ),
301
+ );
302
+ }
303
+
304
+ // Component({ ...props }) - result is already a string
305
+ const call = t.callExpression(t.identifier("__h"), [
306
+ t.callExpression(t.identifier(tagName), [props]),
307
+ ]);
308
+
309
+ // wrap in template so return type is consistent
310
+ return t.templateLiteral(
311
+ [
312
+ t.templateElement({ raw: "", cooked: "" }),
313
+ t.templateElement({ raw: "", cooked: "" }, true),
314
+ ],
315
+ [call],
316
+ );
317
+ }
318
+
319
+ export function processFragmentSSR(
320
+ node: t.JSXFragment,
321
+ signals: Set<string>,
322
+ ): t.TemplateLiteral {
323
+ const parts: Array<string | t.Expression> = [];
324
+
325
+ for (const child of node.children) {
326
+ if (t.isJSXText(child)) {
327
+ const text = child.value;
328
+ if (!text.trim()) continue;
329
+ const normalized = text.replace(/\s*\n\s*/g, " ");
330
+ parts.push(normalized);
331
+ } else if (t.isJSXElement(child)) {
332
+ parts.push(processElementSSR(child, signals));
333
+ } else if (t.isJSXFragment(child)) {
334
+ parts.push(processFragmentSSR(child, signals));
335
+ } else if (t.isJSXExpressionContainer(child)) {
336
+ if (t.isJSXEmptyExpression(child.expression)) continue;
337
+ const expr = child.expression as t.Expression;
338
+ if (t.isJSXElement(expr)) {
339
+ parts.push(processElementSSR(expr, signals));
340
+ } else if (t.isJSXFragment(expr)) {
341
+ parts.push(processFragmentSSR(expr, signals));
342
+ } else {
343
+ parts.push("<!--g-->");
344
+ parts.push(escape(expr));
345
+ parts.push("<!--/g-->");
346
+ }
347
+ }
348
+ }
349
+
350
+ return buildTemplate(parts);
351
+ }