@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.
- package/index.ts +4 -4
- package/jsr.json +8 -8
- package/package.json +2 -1
- package/src/babel/handlers/dom/derived.ts +62 -62
- package/src/babel/handlers/dom/effect.ts +15 -15
- package/src/babel/handlers/dom/globals.ts +105 -105
- package/src/babel/handlers/dom/state.ts +107 -107
- package/src/babel/handlers/ssr/derived.ts +27 -27
- package/src/babel/handlers/ssr/state.ts +11 -11
- package/src/babel/index.ts +8 -0
- package/src/babel/jsx/anchor-mount.ts +385 -385
- package/src/babel/jsx/children.ts +124 -124
- package/src/babel/jsx/element.ts +848 -850
- package/src/babel/jsx/fragment.ts +282 -282
- package/src/babel/jsx/index.ts +2 -2
- package/src/babel/jsx/keyed-list.ts +309 -309
- package/src/babel/jsx/ssr.ts +351 -324
- package/src/babel/jsx/text-node.ts +192 -192
- package/src/babel/jsx/utils.ts +179 -179
- package/src/babel/util/bind.ts +139 -139
- package/src/babel/util/css.ts +151 -151
- package/src/babel/util/dead-code.ts +28 -28
- package/src/babel/util/helpers.ts +46 -46
- package/src/babel/util/magic.ts +11 -11
- package/src/bun-plugin.ts +92 -92
- package/src/vite/index.ts +58 -58
- package/test/async-components.test.ts +59 -59
- package/test/components.test.ts +129 -129
- package/test/containsSignal.test.ts +288 -288
- package/test/fragments.test.ts +90 -90
- package/test/helpers/transform.ts +45 -45
- package/test/hydration.test.ts +147 -147
- package/test/jsx.test.ts +227 -225
- package/test/keyed-lists.test.ts +126 -126
- package/test/reactivity.test.ts +225 -225
- package/test/scoped-css.test.ts +129 -129
- package/test/ssr.test.ts +130 -130
- package/test/style-object.test.ts +13 -0
- package/test/tree-shaking.test.ts +78 -78
- package/tsconfig.json +30 -30
package/src/babel/jsx/ssr.ts
CHANGED
|
@@ -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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
t.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|