@sigil-dev/compiler 0.7.5 → 0.7.6

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