@sigil-dev/compiler 0.9.10 → 0.9.12
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/package.json +3 -1
- package/src/babel/handlers/dom/state.ts +4 -5
- package/src/babel/index.ts +75 -26
- package/src/babel/jsx/children.ts +10 -2
- package/src/babel/jsx/element.ts +205 -110
- package/src/babel/jsx/fragment.ts +15 -3
- package/src/babel/jsx/keyed-list.ts +10 -2
- package/src/babel/jsx/ssr.ts +136 -253
- package/src/babel/jsx/utils.ts +47 -1
- package/src/babel/util/bind.ts +4 -0
- package/src/babel/util/css.ts +170 -113
- package/src/babel/util/props.ts +256 -0
- package/src/bun-plugin.ts +28 -17
- package/src/vite/index.ts +4 -1
- package/test/bun-plugin.test.ts +45 -0
- package/test/hydration.test.ts +3 -2
- package/test/jsx.test.ts +26 -4
- package/test/render-parity.test.ts +639 -0
- package/test/scoped-css.test.ts +39 -2
- package/test/ssr.test.ts +1 -1
- package/test/style-object.test.ts +29 -6
package/src/babel/jsx/ssr.ts
CHANGED
|
@@ -1,38 +1,7 @@
|
|
|
1
1
|
import { types as t } from "@babel/core";
|
|
2
2
|
import { isGlobalComponent } from "../handlers/dom/globals";
|
|
3
3
|
import { getTagName } from "../util/helpers";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Attributes that are on by presence alone: checked="" and even
|
|
7
|
-
* checked="false" both mean checked, so these can't go through the generic
|
|
8
|
-
* name="value" path.
|
|
9
|
-
*/
|
|
10
|
-
const BOOLEAN_ATTRS = new Set([
|
|
11
|
-
"allowfullscreen",
|
|
12
|
-
"async",
|
|
13
|
-
"autofocus",
|
|
14
|
-
"autoplay",
|
|
15
|
-
"checked",
|
|
16
|
-
"controls",
|
|
17
|
-
"default",
|
|
18
|
-
"defer",
|
|
19
|
-
"disabled",
|
|
20
|
-
"formnovalidate",
|
|
21
|
-
"hidden",
|
|
22
|
-
"inert",
|
|
23
|
-
"ismap",
|
|
24
|
-
"loop",
|
|
25
|
-
"multiple",
|
|
26
|
-
"muted",
|
|
27
|
-
"nomodule",
|
|
28
|
-
"novalidate",
|
|
29
|
-
"open",
|
|
30
|
-
"playsinline",
|
|
31
|
-
"readonly",
|
|
32
|
-
"required",
|
|
33
|
-
"reversed",
|
|
34
|
-
"selected",
|
|
35
|
-
]);
|
|
4
|
+
import { jsxText } from "./utils";
|
|
36
5
|
|
|
37
6
|
const VOID_ELEMENTS = new Set([
|
|
38
7
|
"area",
|
|
@@ -60,94 +29,89 @@ function escape(expr: t.Expression): t.CallExpression {
|
|
|
60
29
|
return t.callExpression(t.identifier("__e"), [expr]);
|
|
61
30
|
}
|
|
62
31
|
|
|
32
|
+
/** Compile-time HTML escaping for static JSX text and attribute strings. */
|
|
33
|
+
function escapeText(s: string): string {
|
|
34
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
35
|
+
}
|
|
36
|
+
function escapeAttr(s: string): string {
|
|
37
|
+
return s.replace(/&/g, "&").replace(/"/g, """);
|
|
38
|
+
}
|
|
39
|
+
|
|
63
40
|
// builds a template literal from alternating string parts and expressions
|
|
64
41
|
function buildTemplate(parts: Array<string | t.Expression>): t.TemplateLiteral {
|
|
65
42
|
const quasis: t.TemplateElement[] = [];
|
|
66
43
|
const exprs: t.Expression[] = [];
|
|
67
44
|
let current = "";
|
|
45
|
+
// raw must be valid template source: a backslash, backtick or ${ in
|
|
46
|
+
// the page text would otherwise end or corrupt the literal
|
|
47
|
+
const element = (cooked: string, tail = false) =>
|
|
48
|
+
t.templateElement(
|
|
49
|
+
{ raw: cooked.replace(/\\|`|\$\{/g, (m) => `\\${m}`), cooked },
|
|
50
|
+
tail,
|
|
51
|
+
);
|
|
68
52
|
|
|
69
53
|
for (const part of parts) {
|
|
70
54
|
if (typeof part === "string") {
|
|
71
55
|
current += part;
|
|
72
56
|
} else {
|
|
73
|
-
quasis.push(
|
|
57
|
+
quasis.push(element(current));
|
|
74
58
|
current = "";
|
|
75
59
|
exprs.push(part);
|
|
76
60
|
}
|
|
77
61
|
}
|
|
78
|
-
quasis.push(
|
|
62
|
+
quasis.push(element(current, true));
|
|
79
63
|
return t.templateLiteral(quasis, exprs);
|
|
80
64
|
}
|
|
81
65
|
|
|
82
66
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* const rendered = <Component foo="bar" />
|
|
87
|
-
* return <div>{rendered}</div>
|
|
88
|
-
* ```
|
|
89
|
-
* @param expr
|
|
90
|
-
* @param safeIdents
|
|
91
|
-
* @returns
|
|
67
|
+
* Children of an element, component or fragment as HTML parts. `raw` is for
|
|
68
|
+
* <script>: its text is code, so it is neither escaped nor marked for
|
|
69
|
+
* hydration (see processElementSSR).
|
|
92
70
|
*/
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (t.isArrowFunctionExpression(cb) || t.isFunctionExpression(cb)) {
|
|
132
|
-
const body = cb.body;
|
|
133
|
-
if (t.isJSXElement(body) || t.isJSXFragment(body)) return true;
|
|
134
|
-
if (t.isBlockStatement(body)) {
|
|
135
|
-
for (const stmt of body.body) {
|
|
136
|
-
if (
|
|
137
|
-
t.isReturnStatement(stmt) &&
|
|
138
|
-
stmt.argument &&
|
|
139
|
-
(t.isJSXElement(stmt.argument) || t.isJSXFragment(stmt.argument))
|
|
140
|
-
) return true;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
71
|
+
function childrenSSR(
|
|
72
|
+
children: t.JSXElement["children"],
|
|
73
|
+
signals: Set<string>,
|
|
74
|
+
hash: string | undefined,
|
|
75
|
+
raw?: string,
|
|
76
|
+
): Array<string | t.Expression> {
|
|
77
|
+
const parts: Array<string | t.Expression> = [];
|
|
78
|
+
for (const child of children) {
|
|
79
|
+
if (t.isJSXText(child)) {
|
|
80
|
+
const text = jsxText(child);
|
|
81
|
+
if (text !== null) parts.push(raw ? text : escapeText(text));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
let node: t.Node = child;
|
|
85
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
86
|
+
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
87
|
+
node = child.expression;
|
|
88
|
+
}
|
|
89
|
+
if (t.isJSXElement(node)) {
|
|
90
|
+
parts.push(processElementSSR(node, signals, hash));
|
|
91
|
+
} else if (t.isJSXFragment(node)) {
|
|
92
|
+
parts.push(processFragmentSSR(node, signals, hash));
|
|
93
|
+
} else if (raw) {
|
|
94
|
+
parts.push(
|
|
95
|
+
t.callExpression(t.identifier("__r"), [
|
|
96
|
+
node as t.Expression,
|
|
97
|
+
t.stringLiteral(raw),
|
|
98
|
+
]),
|
|
99
|
+
);
|
|
100
|
+
} else if (!t.isJSXSpreadChild(node)) {
|
|
101
|
+
// Delimit dynamic text segments so SSR produces separate text
|
|
102
|
+
// nodes that the hydrate compiler can claim individually.
|
|
103
|
+
parts.push("<!--g-->");
|
|
104
|
+
parts.push(escape(node as t.Expression));
|
|
105
|
+
parts.push("<!--/g-->");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return parts;
|
|
146
109
|
}
|
|
147
|
-
|
|
110
|
+
|
|
148
111
|
export function processElementSSR(
|
|
149
112
|
node: t.JSXElement,
|
|
150
113
|
signals: Set<string>,
|
|
114
|
+
hash?: string,
|
|
151
115
|
): t.TemplateLiteral {
|
|
152
116
|
const tagName = getTagName(node.openingElement.name);
|
|
153
117
|
const isComponent = /^[A-Z]/.test(tagName);
|
|
@@ -160,85 +124,73 @@ export function processElementSSR(
|
|
|
160
124
|
[],
|
|
161
125
|
);
|
|
162
126
|
}
|
|
163
|
-
return processComponentSSR(node, tagName, signals);
|
|
127
|
+
return processComponentSSR(node, tagName, signals, hash);
|
|
164
128
|
}
|
|
165
129
|
let dangerousHTML: t.Expression | null = null;
|
|
130
|
+
// <textarea value> is its content; <select value> picks an <option>
|
|
131
|
+
let textareaValue: t.Expression | null = null;
|
|
132
|
+
let selectValue: t.Expression | null = null;
|
|
133
|
+
let hasClass = false;
|
|
166
134
|
const parts: Array<string | t.Expression> = [];
|
|
167
135
|
parts.push(`<${tagName}`);
|
|
168
136
|
|
|
169
|
-
// attributes
|
|
170
137
|
for (const attr of node.openingElement.attributes) {
|
|
171
|
-
if (
|
|
138
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
139
|
+
parts.push(t.callExpression(t.identifier("__s"), [attr.argument]));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
172
142
|
|
|
173
143
|
// namespaced attributes are client-only directives
|
|
174
144
|
// (transition:/in:/out:/bind:) — SSR drops them entirely
|
|
175
145
|
if (t.isJSXNamespacedName(attr.name)) continue;
|
|
176
146
|
|
|
177
|
-
const attrName =
|
|
147
|
+
const attrName = attr.name.name;
|
|
148
|
+
// drop event handlers and client-only props
|
|
149
|
+
if (/^on[A-Z]/.test(attrName)) continue;
|
|
150
|
+
if (attrName === "use" || /^bind[A-Z]/.test(attrName)) continue;
|
|
151
|
+
|
|
152
|
+
const value: t.Expression | null = t.isStringLiteral(attr.value)
|
|
153
|
+
? attr.value
|
|
154
|
+
: t.isJSXExpressionContainer(attr.value) &&
|
|
155
|
+
!t.isJSXEmptyExpression(attr.value.expression)
|
|
156
|
+
? attr.value.expression
|
|
157
|
+
: null;
|
|
178
158
|
|
|
179
159
|
if (attrName === "innerHTML") {
|
|
180
|
-
|
|
181
|
-
dangerousHTML = attr.value.expression as t.Expression;
|
|
182
|
-
}
|
|
160
|
+
dangerousHTML = value;
|
|
183
161
|
continue; // don't output as attribute
|
|
184
162
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (attrName
|
|
163
|
+
if (tagName === "textarea" && attrName === "value") {
|
|
164
|
+
textareaValue = value;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (tagName === "select" && attrName === "value") {
|
|
168
|
+
selectValue = value;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
190
171
|
|
|
191
172
|
const domAttr = ATTR_MAP_SSR[attrName] ?? attrName;
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
173
|
+
const isClass = domAttr === "class" && !!hash;
|
|
174
|
+
if (domAttr === "class") hasClass = true;
|
|
175
|
+
|
|
176
|
+
if (attr.value === null) {
|
|
177
|
+
// bare attribute: <input disabled>, <script defer>
|
|
178
|
+
parts.push(` ${domAttr}`);
|
|
179
|
+
} else if (t.isStringLiteral(attr.value)) {
|
|
180
|
+
const v = escapeAttr(attr.value.value);
|
|
181
|
+
parts.push(` ${domAttr}="${isClass ? `${v} ${hash}` : v}"`);
|
|
182
|
+
} else if (value && isClass) {
|
|
183
|
+
parts.push(` class="`, escape(value), ` ${hash}"`);
|
|
184
|
+
} else if (value) {
|
|
202
185
|
parts.push(
|
|
203
|
-
t.callExpression(t.identifier("
|
|
186
|
+
t.callExpression(t.identifier("__a"), [
|
|
204
187
|
t.stringLiteral(domAttr),
|
|
205
|
-
|
|
188
|
+
value,
|
|
206
189
|
]),
|
|
207
190
|
);
|
|
208
|
-
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
209
|
-
const expr = attr.value.expression as t.Expression;
|
|
210
|
-
if (
|
|
211
|
-
attrName === "style" &&
|
|
212
|
-
t.isObjectExpression(expr) &&
|
|
213
|
-
expr.properties.every((prop) => t.isObjectProperty(prop))
|
|
214
|
-
) {
|
|
215
|
-
// style={{ key: value, ... }} — serialize to real "key:value;"
|
|
216
|
-
// CSS text instead of the generic String(expr) path below, which
|
|
217
|
-
// would stringify the object itself to the literal "[object Object]"
|
|
218
|
-
parts.push(` style="`);
|
|
219
|
-
for (const prop of expr.properties as t.ObjectProperty[]) {
|
|
220
|
-
const key = t.isIdentifier(prop.key)
|
|
221
|
-
? prop.key.name
|
|
222
|
-
: t.isStringLiteral(prop.key)
|
|
223
|
-
? prop.key.value
|
|
224
|
-
: null;
|
|
225
|
-
if (key === null) continue;
|
|
226
|
-
const cssKey = key.replace(
|
|
227
|
-
/[A-Z]/g,
|
|
228
|
-
(letter) => `-${letter.toLowerCase()}`,
|
|
229
|
-
);
|
|
230
|
-
parts.push(`${cssKey}:`);
|
|
231
|
-
parts.push(escape(prop.value as t.Expression));
|
|
232
|
-
parts.push(";");
|
|
233
|
-
}
|
|
234
|
-
parts.push(`"`);
|
|
235
|
-
} else {
|
|
236
|
-
parts.push(` ${domAttr}="`);
|
|
237
|
-
parts.push(escape(expr));
|
|
238
|
-
parts.push(`"`);
|
|
239
|
-
}
|
|
240
191
|
}
|
|
241
192
|
}
|
|
193
|
+
if (hash && !hasClass) parts.push(` class="${hash}"`);
|
|
242
194
|
|
|
243
195
|
if (VOID_ELEMENTS.has(tagName)) {
|
|
244
196
|
parts.push(">");
|
|
@@ -249,52 +201,29 @@ export function processElementSSR(
|
|
|
249
201
|
|
|
250
202
|
if (dangerousHTML) {
|
|
251
203
|
parts.push(dangerousHTML);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
204
|
+
} else if (textareaValue) {
|
|
205
|
+
parts.push(escape(textareaValue));
|
|
206
|
+
} else {
|
|
207
|
+
// <script> holds raw text: HTML escaping would corrupt the code (&& →
|
|
208
|
+
// &&), and <!--g--> markers are just text inside it, not
|
|
209
|
+
// comments, so there is nothing for hydration to find. __r only guards
|
|
210
|
+
// against the code closing its own element. (<style> never gets here:
|
|
211
|
+
// scoped style blocks are stripped from the source before compiling.)
|
|
212
|
+
const children = childrenSSR(
|
|
213
|
+
node.children,
|
|
214
|
+
signals,
|
|
215
|
+
hash,
|
|
216
|
+
tagName === "script" ? tagName : undefined,
|
|
217
|
+
);
|
|
218
|
+
if (selectValue) {
|
|
267
219
|
parts.push(
|
|
268
|
-
t.callExpression(t.identifier("
|
|
269
|
-
|
|
270
|
-
|
|
220
|
+
t.callExpression(t.identifier("__sel"), [
|
|
221
|
+
buildTemplate(children),
|
|
222
|
+
selectValue,
|
|
271
223
|
]),
|
|
272
224
|
);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (t.isJSXText(child)) {
|
|
276
|
-
const text = child.value;
|
|
277
|
-
if (!text.trim()) continue;
|
|
278
|
-
const normalized = text.replace(/\s*\n\s*/g, " ");
|
|
279
|
-
parts.push(normalized);
|
|
280
|
-
} else if (t.isJSXElement(child)) {
|
|
281
|
-
parts.push(processElementSSR(child, signals));
|
|
282
|
-
} else if (t.isJSXFragment(child)) {
|
|
283
|
-
parts.push(processFragmentSSR(child, signals));
|
|
284
|
-
} else if (t.isJSXExpressionContainer(child)) {
|
|
285
|
-
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
286
|
-
const expr = child.expression as t.Expression;
|
|
287
|
-
if (t.isJSXElement(expr)) {
|
|
288
|
-
parts.push(processElementSSR(expr, signals));
|
|
289
|
-
} else if (t.isJSXFragment(expr)) {
|
|
290
|
-
parts.push(processFragmentSSR(expr, signals));
|
|
291
|
-
} else {
|
|
292
|
-
// Delimit dynamic text segments so SSR produces separate text
|
|
293
|
-
// nodes that the hydrate compiler can claim individually.
|
|
294
|
-
parts.push("<!--g-->");
|
|
295
|
-
parts.push(escape(expr));
|
|
296
|
-
parts.push("<!--/g-->");
|
|
297
|
-
}
|
|
225
|
+
} else {
|
|
226
|
+
parts.push(...children);
|
|
298
227
|
}
|
|
299
228
|
}
|
|
300
229
|
|
|
@@ -306,11 +235,15 @@ function processComponentSSR(
|
|
|
306
235
|
node: t.JSXElement,
|
|
307
236
|
tagName: string,
|
|
308
237
|
signals: Set<string>,
|
|
238
|
+
hash: string | undefined,
|
|
309
239
|
): t.TemplateLiteral {
|
|
310
240
|
const props = t.objectExpression([]);
|
|
311
241
|
|
|
312
242
|
for (const attr of node.openingElement.attributes) {
|
|
313
|
-
if (
|
|
243
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
244
|
+
props.properties.push(t.spreadElement(attr.argument));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
314
247
|
const attrName = (attr.name as t.JSXIdentifier).name;
|
|
315
248
|
// data-key is a framework-internal attribute for HTML elements only; skip for components
|
|
316
249
|
if (attrName === "data-key" || attrName === "key") continue;
|
|
@@ -318,7 +251,9 @@ function processComponentSSR(
|
|
|
318
251
|
const propKey = /[^a-zA-Z_$0-9]/.test(attrName)
|
|
319
252
|
? t.stringLiteral(attrName)
|
|
320
253
|
: t.identifier(attrName);
|
|
321
|
-
if (
|
|
254
|
+
if (attr.value === null) {
|
|
255
|
+
props.properties.push(t.objectProperty(propKey, t.booleanLiteral(true)));
|
|
256
|
+
} else if (t.isStringLiteral(attr.value)) {
|
|
322
257
|
props.properties.push(t.objectProperty(propKey, attr.value));
|
|
323
258
|
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
324
259
|
props.properties.push(
|
|
@@ -327,33 +262,7 @@ function processComponentSSR(
|
|
|
327
262
|
}
|
|
328
263
|
}
|
|
329
264
|
|
|
330
|
-
|
|
331
|
-
const childParts: Array<string | t.Expression> = [];
|
|
332
|
-
for (const child of node.children) {
|
|
333
|
-
if (t.isJSXText(child)) {
|
|
334
|
-
const text = child.value;
|
|
335
|
-
if (!text.trim()) continue;
|
|
336
|
-
const normalized = text.replace(/\s*\n\s*/g, " ");
|
|
337
|
-
childParts.push(normalized);
|
|
338
|
-
} else if (t.isJSXElement(child)) {
|
|
339
|
-
childParts.push(processElementSSR(child, signals));
|
|
340
|
-
} else if (t.isJSXFragment(child)) {
|
|
341
|
-
childParts.push(processFragmentSSR(child, signals));
|
|
342
|
-
} else if (t.isJSXExpressionContainer(child)) {
|
|
343
|
-
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
344
|
-
const expr = child.expression as t.Expression;
|
|
345
|
-
if (t.isJSXElement(expr)) {
|
|
346
|
-
childParts.push(processElementSSR(expr, signals));
|
|
347
|
-
} else if (t.isJSXFragment(expr)) {
|
|
348
|
-
childParts.push(processFragmentSSR(expr, signals));
|
|
349
|
-
} else {
|
|
350
|
-
childParts.push("<!--g-->");
|
|
351
|
-
childParts.push(escape(expr));
|
|
352
|
-
childParts.push("<!--/g-->");
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
265
|
+
const childParts = childrenSSR(node.children, signals, hash);
|
|
357
266
|
if (childParts.length > 0) {
|
|
358
267
|
props.properties.push(
|
|
359
268
|
t.objectProperty(
|
|
@@ -381,33 +290,7 @@ function processComponentSSR(
|
|
|
381
290
|
export function processFragmentSSR(
|
|
382
291
|
node: t.JSXFragment,
|
|
383
292
|
signals: Set<string>,
|
|
293
|
+
hash?: string,
|
|
384
294
|
): t.TemplateLiteral {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
for (const child of node.children) {
|
|
388
|
-
if (t.isJSXText(child)) {
|
|
389
|
-
const text = child.value;
|
|
390
|
-
if (!text.trim()) continue;
|
|
391
|
-
const normalized = text.replace(/\s*\n\s*/g, " ");
|
|
392
|
-
parts.push(normalized);
|
|
393
|
-
} else if (t.isJSXElement(child)) {
|
|
394
|
-
parts.push(processElementSSR(child, signals));
|
|
395
|
-
} else if (t.isJSXFragment(child)) {
|
|
396
|
-
parts.push(processFragmentSSR(child, signals));
|
|
397
|
-
} else if (t.isJSXExpressionContainer(child)) {
|
|
398
|
-
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
399
|
-
const expr = child.expression as t.Expression;
|
|
400
|
-
if (t.isJSXElement(expr)) {
|
|
401
|
-
parts.push(processElementSSR(expr, signals));
|
|
402
|
-
} else if (t.isJSXFragment(expr)) {
|
|
403
|
-
parts.push(processFragmentSSR(expr, signals));
|
|
404
|
-
} else {
|
|
405
|
-
parts.push("<!--g-->");
|
|
406
|
-
parts.push(escape(expr));
|
|
407
|
-
parts.push("<!--/g-->");
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return buildTemplate(parts);
|
|
295
|
+
return buildTemplate(childrenSSR(node.children, signals, hash));
|
|
413
296
|
}
|
package/src/babel/jsx/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { types as t } from "@babel/core";
|
|
2
|
+
import { isImportRead, isStoreRead } from "../util/props.ts";
|
|
2
3
|
|
|
3
4
|
export const ATTR_MAP: Record<string, string> = {
|
|
4
5
|
class: "className",
|
|
@@ -8,15 +9,19 @@ export const ATTR_MAP: Record<string, string> = {
|
|
|
8
9
|
export function containsSignal(
|
|
9
10
|
expr: t.Expression | t.Node,
|
|
10
11
|
signals: Set<string>,
|
|
12
|
+
asCallee = false,
|
|
11
13
|
): boolean {
|
|
12
14
|
if (!expr) return false;
|
|
13
15
|
if (t.isIdentifier(expr)) return signals.has(expr.name);
|
|
14
16
|
if (t.isCallExpression(expr)) {
|
|
17
|
+
if (isStoreRead(expr)) return true;
|
|
15
18
|
return (
|
|
16
|
-
containsSignal(expr.callee as t.Expression, signals) ||
|
|
19
|
+
containsSignal(expr.callee as t.Expression, signals, true) ||
|
|
17
20
|
expr.arguments.some((a) => containsSignal(a as t.Expression, signals))
|
|
18
21
|
);
|
|
19
22
|
}
|
|
23
|
+
// a read on an imported store; `utils.format` as a callee is not one
|
|
24
|
+
if (!asCallee && isImportRead(expr)) return true;
|
|
20
25
|
if (t.isBinaryExpression(expr)) {
|
|
21
26
|
return (
|
|
22
27
|
containsSignal(expr.left as t.Expression, signals) ||
|
|
@@ -177,3 +182,44 @@ export function buildHydrationScope(
|
|
|
177
182
|
]),
|
|
178
183
|
);
|
|
179
184
|
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The text a JSXText child renders, or null when nothing is left. This is
|
|
188
|
+
* the standard JSX whitespace rule (React and Babel use it too): whitespace
|
|
189
|
+
* touching a line break is dropped, lines that are only whitespace vanish,
|
|
190
|
+
* and the remaining lines join with one space. Whitespace that doesn't touch
|
|
191
|
+
* a line break is kept, so `Hello <b>big</b> world` keeps both spaces.
|
|
192
|
+
*
|
|
193
|
+
* Every mode (ssr, hydrate, dom) must go through this: if they disagree,
|
|
194
|
+
* server HTML and client-rendered pages show different text.
|
|
195
|
+
*/
|
|
196
|
+
export function jsxText(child: t.JSXText): string | null {
|
|
197
|
+
const lines = child.value.split(/\r\n|\n|\r/);
|
|
198
|
+
let lastNonEmpty = 0;
|
|
199
|
+
for (let i = 0; i < lines.length; i++) {
|
|
200
|
+
if (/[^ \t]/.test(lines[i] as string)) lastNonEmpty = i;
|
|
201
|
+
}
|
|
202
|
+
let out = "";
|
|
203
|
+
for (let i = 0; i < lines.length; i++) {
|
|
204
|
+
let line = (lines[i] as string).replace(/\t/g, " ");
|
|
205
|
+
if (i !== 0) line = line.replace(/^ +/, "");
|
|
206
|
+
if (i !== lines.length - 1) line = line.replace(/ +$/, "");
|
|
207
|
+
if (!line) continue;
|
|
208
|
+
out += i === lastNonEmpty ? line : `${line} `;
|
|
209
|
+
}
|
|
210
|
+
return out || null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** JSX that runs when the expression is evaluated (not inside a function). */
|
|
214
|
+
export function hasJSX(expr: t.Node): boolean {
|
|
215
|
+
if (t.isJSXElement(expr) || t.isJSXFragment(expr)) return true;
|
|
216
|
+
if (t.isConditionalExpression(expr))
|
|
217
|
+
return hasJSX(expr.consequent) || hasJSX(expr.alternate);
|
|
218
|
+
if (t.isLogicalExpression(expr))
|
|
219
|
+
return hasJSX(expr.left) || hasJSX(expr.right);
|
|
220
|
+
if (t.isParenthesizedExpression(expr) || t.isTSAsExpression(expr))
|
|
221
|
+
return hasJSX(expr.expression);
|
|
222
|
+
if (t.isArrayExpression(expr))
|
|
223
|
+
return expr.elements.some((e) => e !== null && hasJSX(e));
|
|
224
|
+
return false;
|
|
225
|
+
}
|
package/src/babel/util/bind.ts
CHANGED
|
@@ -6,6 +6,10 @@ const BIND_EVENT: Record<string, string> = {
|
|
|
6
6
|
innerHTML: "input",
|
|
7
7
|
textContent: "input",
|
|
8
8
|
group: "change",
|
|
9
|
+
// element state that isn't form input changes on its own event
|
|
10
|
+
scrollTop: "scroll",
|
|
11
|
+
scrollLeft: "scroll",
|
|
12
|
+
open: "toggle",
|
|
9
13
|
};
|
|
10
14
|
|
|
11
15
|
export function buildBind(
|