@reckona/mreact-compiler 0.0.152 → 0.0.154
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/dist/emit-server-shared.js +1 -1
- package/dist/emit-server-shared.js.map +1 -1
- package/dist/emit-server.d.ts.map +1 -1
- package/dist/emit-server.js +81 -8
- package/dist/emit-server.js.map +1 -1
- package/dist/ir.d.ts +2 -1
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js.map +1 -1
- package/dist/oxc-bindings.d.ts.map +1 -1
- package/dist/oxc-bindings.js +28 -1
- package/dist/oxc-bindings.js.map +1 -1
- package/dist/oxc-compat-create-element.d.ts +11 -0
- package/dist/oxc-compat-create-element.d.ts.map +1 -0
- package/dist/oxc-compat-create-element.js +489 -0
- package/dist/oxc-compat-create-element.js.map +1 -0
- package/dist/oxc-dom-lowering.js +1 -1
- package/dist/oxc-dom-lowering.js.map +1 -1
- package/dist/oxc-render-values.js +8 -0
- package/dist/oxc-render-values.js.map +1 -1
- package/dist/oxc-runtime-emit.d.ts.map +1 -1
- package/dist/oxc-runtime-emit.js +5 -1
- package/dist/oxc-runtime-emit.js.map +1 -1
- package/dist/oxc-transform.d.ts.map +1 -1
- package/dist/oxc-transform.js +6 -0
- package/dist/oxc-transform.js.map +1 -1
- package/dist/oxc.d.ts.map +1 -1
- package/dist/oxc.js +79 -11
- package/dist/oxc.js.map +1 -1
- package/dist/transform.d.ts +1 -0
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +8 -5
- package/dist/transform.js.map +1 -1
- package/package.json +2 -2
- package/src/emit-server-shared.ts +1 -1
- package/src/emit-server.ts +103 -4
- package/src/ir.ts +4 -1
- package/src/oxc-bindings.ts +36 -1
- package/src/oxc-compat-create-element.ts +705 -0
- package/src/oxc-dom-lowering.ts +1 -1
- package/src/oxc-render-values.ts +10 -0
- package/src/oxc-runtime-emit.ts +6 -1
- package/src/oxc-transform.ts +6 -0
- package/src/oxc.ts +130 -4
- package/src/transform.ts +9 -5
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import type { AttributeIr, JsxNodeIr } from "./ir.js";
|
|
2
|
+
import { readArray, readObject, readSource, unwrapOxcParentheses } from "./oxc-node-utils.js";
|
|
3
|
+
|
|
4
|
+
// Lowers statically analyzable react-compat createElement() call trees into
|
|
5
|
+
// the element IR so they compile through the server string pipeline instead
|
|
6
|
+
// of being interpreted per request. Any unsupported shape anywhere in a tree
|
|
7
|
+
// bails the WHOLE tree out (callers keep the source verbatim) so the
|
|
8
|
+
// interpreter semantics never get half-applied.
|
|
9
|
+
|
|
10
|
+
const COMPAT_CREATE_ELEMENT_SOURCES = new Set(["react", "@reckona/mreact-compat"]);
|
|
11
|
+
|
|
12
|
+
export function collectCompatCreateElementNames(program: unknown): Set<string> {
|
|
13
|
+
const names = new Set<string>();
|
|
14
|
+
|
|
15
|
+
for (const statement of readArray(readObject(program).body)) {
|
|
16
|
+
const declaration = readObject(statement);
|
|
17
|
+
|
|
18
|
+
if (declaration.type !== "ImportDeclaration") {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const source = readObject(declaration.source);
|
|
23
|
+
|
|
24
|
+
if (typeof source.value !== "string" || !COMPAT_CREATE_ELEMENT_SOURCES.has(source.value)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const specifier of readArray(declaration.specifiers)) {
|
|
29
|
+
const specifierObject = readObject(specifier);
|
|
30
|
+
|
|
31
|
+
if (specifierObject.type !== "ImportSpecifier") {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const imported = readObject(specifierObject.imported);
|
|
36
|
+
const local = readObject(specifierObject.local);
|
|
37
|
+
|
|
38
|
+
if (imported.name === "createElement" && typeof local.name === "string") {
|
|
39
|
+
names.add(local.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return names;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Mirrors packages/react-compat/src/server-render.ts attribute serialization
|
|
48
|
+
// for compile-time-known prop values. Keep these tables in sync with the
|
|
49
|
+
// interpreter; parity tests compare emitted bytes against renderToString.
|
|
50
|
+
const COMPAT_HTML_ATTRIBUTE_ALIASES: Record<string, string> = {
|
|
51
|
+
acceptCharset: "accept-charset",
|
|
52
|
+
autoFocus: "autofocus",
|
|
53
|
+
autoPlay: "autoplay",
|
|
54
|
+
charSet: "charset",
|
|
55
|
+
className: "class",
|
|
56
|
+
colSpan: "colspan",
|
|
57
|
+
contentEditable: "contenteditable",
|
|
58
|
+
crossOrigin: "crossorigin",
|
|
59
|
+
encType: "enctype",
|
|
60
|
+
formAction: "formaction",
|
|
61
|
+
frameBorder: "frameborder",
|
|
62
|
+
htmlFor: "for",
|
|
63
|
+
httpEquiv: "http-equiv",
|
|
64
|
+
maxLength: "maxlength",
|
|
65
|
+
minLength: "minlength",
|
|
66
|
+
noValidate: "novalidate",
|
|
67
|
+
playsInline: "playsinline",
|
|
68
|
+
rowSpan: "rowspan",
|
|
69
|
+
spellCheck: "spellcheck",
|
|
70
|
+
srcDoc: "srcdoc",
|
|
71
|
+
srcSet: "srcset",
|
|
72
|
+
tabIndex: "tabindex",
|
|
73
|
+
useMap: "usemap",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const COMPAT_BOOLEANISH_STRING_ATTRIBUTES = new Set([
|
|
77
|
+
"contenteditable",
|
|
78
|
+
"draggable",
|
|
79
|
+
"spellcheck",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// URL-bearing attributes always emit through the dynamic path so the runtime
|
|
83
|
+
// scheme guard applies; static literal lowering must not bypass it.
|
|
84
|
+
const URL_ATTRIBUTE_NAMES = new Set([
|
|
85
|
+
"action",
|
|
86
|
+
"formaction",
|
|
87
|
+
"href",
|
|
88
|
+
"src",
|
|
89
|
+
"srcset",
|
|
90
|
+
"xlink:href",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const VALID_ATTRIBUTE_NAME = /^[A-Za-z_][\w.\-:]*$/;
|
|
94
|
+
|
|
95
|
+
interface CompatCreateElementScope {
|
|
96
|
+
names: ReadonlySet<string>;
|
|
97
|
+
shadowed: ReadonlySet<string>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isActiveCreateElementName(scope: CompatCreateElementScope, name: string): boolean {
|
|
101
|
+
return scope.names.has(name) && !scope.shadowed.has(name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isEventHandlerPropName(name: string): boolean {
|
|
105
|
+
return (
|
|
106
|
+
name.length > 1 &&
|
|
107
|
+
(name.charCodeAt(0) | 32) === 111 &&
|
|
108
|
+
(name.charCodeAt(1) | 32) === 110
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isBooleanishStringAttributeName(attributeName: string): boolean {
|
|
113
|
+
const lowerCased = attributeName.toLowerCase();
|
|
114
|
+
return lowerCased.startsWith("aria-") || COMPAT_BOOLEANISH_STRING_ATTRIBUTES.has(lowerCased);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isDataAttributeName(attributeName: string): boolean {
|
|
118
|
+
return attributeName.toLowerCase().startsWith("data-");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readStaticPropertyKey(property: Record<string, unknown>): string | undefined {
|
|
122
|
+
if (property.computed === true) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const key = readObject(property.key);
|
|
127
|
+
|
|
128
|
+
if (key.type === "Identifier" && typeof key.name === "string") {
|
|
129
|
+
return key.name;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (key.type === "Literal" && typeof key.value === "string") {
|
|
133
|
+
return key.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type StaticLiteral =
|
|
140
|
+
| { kind: "value"; value: string | number | boolean | null }
|
|
141
|
+
| { kind: "undefined" };
|
|
142
|
+
|
|
143
|
+
function readStaticLiteral(expression: Record<string, unknown>): StaticLiteral | undefined {
|
|
144
|
+
if (expression.type === "Literal") {
|
|
145
|
+
const value = expression.value;
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
value === null ||
|
|
149
|
+
typeof value === "string" ||
|
|
150
|
+
typeof value === "number" ||
|
|
151
|
+
typeof value === "boolean"
|
|
152
|
+
) {
|
|
153
|
+
return { kind: "value", value };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (expression.type === "Identifier" && expression.name === "undefined") {
|
|
160
|
+
return { kind: "undefined" };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (expression.type === "TemplateLiteral") {
|
|
164
|
+
const expressions = readArray(expression.expressions);
|
|
165
|
+
const quasis = readArray(expression.quasis);
|
|
166
|
+
|
|
167
|
+
if (expressions.length === 0 && quasis.length === 1) {
|
|
168
|
+
const cooked = readObject(readObject(quasis[0]).value).cooked;
|
|
169
|
+
|
|
170
|
+
if (typeof cooked === "string") {
|
|
171
|
+
return { kind: "value", value: cooked };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface LoweredProps {
|
|
180
|
+
attributes: AttributeIr[];
|
|
181
|
+
keyCode?: string;
|
|
182
|
+
childrenProp?: Record<string, unknown>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function lowerCreateElementProps(
|
|
186
|
+
code: string,
|
|
187
|
+
propsArgument: Record<string, unknown> | undefined,
|
|
188
|
+
): LoweredProps | undefined {
|
|
189
|
+
if (
|
|
190
|
+
propsArgument === undefined ||
|
|
191
|
+
(propsArgument.type === "Literal" && propsArgument.value === null) ||
|
|
192
|
+
(propsArgument.type === "Identifier" && propsArgument.name === "undefined")
|
|
193
|
+
) {
|
|
194
|
+
return { attributes: [] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (propsArgument.type !== "ObjectExpression") {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const attributes: AttributeIr[] = [];
|
|
202
|
+
let keyCode: string | undefined;
|
|
203
|
+
let childrenProp: Record<string, unknown> | undefined;
|
|
204
|
+
|
|
205
|
+
for (const property of readArray(propsArgument.properties)) {
|
|
206
|
+
const propertyObject = readObject(property);
|
|
207
|
+
|
|
208
|
+
if (
|
|
209
|
+
propertyObject.type !== "Property" ||
|
|
210
|
+
propertyObject.kind !== "init" ||
|
|
211
|
+
propertyObject.method === true ||
|
|
212
|
+
propertyObject.shorthand === true
|
|
213
|
+
) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const name = readStaticPropertyKey(propertyObject);
|
|
218
|
+
|
|
219
|
+
if (name === undefined) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const value = unwrapOxcParentheses(readObject(propertyObject.value));
|
|
224
|
+
|
|
225
|
+
if (name === "key") {
|
|
226
|
+
keyCode = readSource(code, value);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (name === "ref" || isEventHandlerPropName(name)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (name === "children") {
|
|
235
|
+
childrenProp = value;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (name === "dangerouslySetInnerHTML" || name === "suppressHydrationWarning") {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (name === "style") {
|
|
244
|
+
const styleProp = lowerStyleProp(code, value);
|
|
245
|
+
|
|
246
|
+
if (styleProp === undefined) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (styleProp.attribute !== undefined) {
|
|
251
|
+
attributes.push(styleProp.attribute);
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const attributeName = COMPAT_HTML_ATTRIBUTE_ALIASES[name] ?? name;
|
|
257
|
+
|
|
258
|
+
if (!VALID_ATTRIBUTE_NAME.test(attributeName) || isEventHandlerPropName(attributeName)) {
|
|
259
|
+
// The interpreter drops these at runtime; keeping them out of the
|
|
260
|
+
// compiled output matches its bytes.
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const literal = readStaticLiteral(value);
|
|
265
|
+
|
|
266
|
+
if (literal === undefined || URL_ATTRIBUTE_NAMES.has(attributeName.toLowerCase())) {
|
|
267
|
+
attributes.push({
|
|
268
|
+
kind: "dynamic-attr",
|
|
269
|
+
name: attributeName,
|
|
270
|
+
code: readSource(code, value),
|
|
271
|
+
serialization: "compat",
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const staticAttribute = serializeStaticCompatAttribute(attributeName, literal);
|
|
277
|
+
|
|
278
|
+
if (staticAttribute !== undefined) {
|
|
279
|
+
attributes.push(staticAttribute);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
attributes,
|
|
285
|
+
...(keyCode === undefined ? {} : { keyCode }),
|
|
286
|
+
...(childrenProp === undefined ? {} : { childrenProp }),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function serializeStaticCompatAttribute(
|
|
291
|
+
attributeName: string,
|
|
292
|
+
literal: StaticLiteral,
|
|
293
|
+
): AttributeIr | undefined {
|
|
294
|
+
if (literal.kind === "undefined" || literal.value === null) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const value = literal.value;
|
|
299
|
+
|
|
300
|
+
if (typeof value === "boolean") {
|
|
301
|
+
if (isBooleanishStringAttributeName(attributeName) || isDataAttributeName(attributeName)) {
|
|
302
|
+
return { kind: "static-attr", name: attributeName, value: value ? "true" : "false" };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return value ? { kind: "static-attr", name: attributeName, value: "" } : undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { kind: "static-attr", name: attributeName, value: String(value) };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// undefined bails the whole tree; { attribute: undefined } drops the prop.
|
|
312
|
+
function lowerStyleProp(
|
|
313
|
+
code: string,
|
|
314
|
+
value: Record<string, unknown>,
|
|
315
|
+
): { attribute?: AttributeIr } | undefined {
|
|
316
|
+
if (value.type === "ObjectExpression") {
|
|
317
|
+
for (const property of readArray(value.properties)) {
|
|
318
|
+
const propertyObject = readObject(property);
|
|
319
|
+
|
|
320
|
+
if (
|
|
321
|
+
propertyObject.type !== "Property" ||
|
|
322
|
+
propertyObject.kind !== "init" ||
|
|
323
|
+
propertyObject.computed === true ||
|
|
324
|
+
propertyObject.method === true ||
|
|
325
|
+
readStaticPropertyKey(propertyObject) === undefined
|
|
326
|
+
) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
attribute: {
|
|
333
|
+
kind: "dynamic-attr",
|
|
334
|
+
name: "style",
|
|
335
|
+
code: readSource(code, value),
|
|
336
|
+
serialization: "compat",
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// The interpreter's renderStyleAttribute only serializes objects; string
|
|
342
|
+
// and other primitive style values are dropped, so match those bytes by
|
|
343
|
+
// dropping them at compile time too.
|
|
344
|
+
if (readStaticLiteral(value) !== undefined) {
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
attribute: {
|
|
350
|
+
kind: "dynamic-attr",
|
|
351
|
+
name: "style",
|
|
352
|
+
code: readSource(code, value),
|
|
353
|
+
serialization: "compat",
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function analyzeCompatCreateElementRoot(
|
|
359
|
+
code: string,
|
|
360
|
+
expression: Record<string, unknown>,
|
|
361
|
+
scope: CompatCreateElementScope,
|
|
362
|
+
): JsxNodeIr | undefined {
|
|
363
|
+
return lowerCreateElementCall(code, unwrapOxcParentheses(expression), scope);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function lowerCreateElementCall(
|
|
367
|
+
code: string,
|
|
368
|
+
expression: Record<string, unknown>,
|
|
369
|
+
scope: CompatCreateElementScope,
|
|
370
|
+
): JsxNodeIr | undefined {
|
|
371
|
+
if (expression.type !== "CallExpression" || expression.optional === true) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const callee = readObject(expression.callee);
|
|
376
|
+
|
|
377
|
+
if (
|
|
378
|
+
callee.type !== "Identifier" ||
|
|
379
|
+
typeof callee.name !== "string" ||
|
|
380
|
+
!isActiveCreateElementName(scope, callee.name)
|
|
381
|
+
) {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const args = readArray(expression.arguments);
|
|
386
|
+
|
|
387
|
+
if (args.length === 0) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const argument of args) {
|
|
392
|
+
if (readObject(argument).type === "SpreadElement") {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const tagArgument = unwrapOxcParentheses(readObject(args[0]));
|
|
398
|
+
|
|
399
|
+
if (tagArgument.type !== "Literal" || typeof tagArgument.value !== "string") {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const props = lowerCreateElementProps(
|
|
404
|
+
code,
|
|
405
|
+
args.length > 1 ? unwrapOxcParentheses(readObject(args[1])) : undefined,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (props === undefined) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const childArguments = args.slice(2).map((argument) => unwrapOxcParentheses(readObject(argument)));
|
|
413
|
+
const childSources =
|
|
414
|
+
childArguments.length > 0
|
|
415
|
+
? childArguments
|
|
416
|
+
: props.childrenProp === undefined
|
|
417
|
+
? []
|
|
418
|
+
: [props.childrenProp];
|
|
419
|
+
const children: JsxNodeIr[] = [];
|
|
420
|
+
|
|
421
|
+
for (const child of childSources) {
|
|
422
|
+
const lowered = lowerCreateElementChild(code, child, scope);
|
|
423
|
+
|
|
424
|
+
if (lowered === undefined) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
children.push(...lowered);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
kind: "element",
|
|
433
|
+
tagName: tagArgument.value,
|
|
434
|
+
...(props.keyCode === undefined ? {} : { keyCode: props.keyCode }),
|
|
435
|
+
attributes: props.attributes,
|
|
436
|
+
children,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function lowerCreateElementChild(
|
|
441
|
+
code: string,
|
|
442
|
+
child: Record<string, unknown>,
|
|
443
|
+
scope: CompatCreateElementScope,
|
|
444
|
+
): JsxNodeIr[] | undefined {
|
|
445
|
+
const literal = readStaticLiteral(child);
|
|
446
|
+
|
|
447
|
+
if (literal !== undefined) {
|
|
448
|
+
if (literal.kind === "undefined" || literal.value === null || typeof literal.value === "boolean") {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return [{ kind: "text", value: String(literal.value) }];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (child.type === "ArrayExpression") {
|
|
456
|
+
const children: JsxNodeIr[] = [];
|
|
457
|
+
|
|
458
|
+
for (const element of readArray(child.elements)) {
|
|
459
|
+
const elementObject = readObject(element);
|
|
460
|
+
|
|
461
|
+
if (elementObject.type === "SpreadElement") {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const lowered = lowerCreateElementChild(code, unwrapOxcParentheses(elementObject), scope);
|
|
466
|
+
|
|
467
|
+
if (lowered === undefined) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
children.push(...lowered);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return children;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (child.type === "CallExpression") {
|
|
478
|
+
const callee = readObject(child.callee);
|
|
479
|
+
|
|
480
|
+
if (
|
|
481
|
+
callee.type === "Identifier" &&
|
|
482
|
+
typeof callee.name === "string" &&
|
|
483
|
+
isActiveCreateElementName(scope, callee.name)
|
|
484
|
+
) {
|
|
485
|
+
const lowered = lowerCreateElementCall(code, child, scope);
|
|
486
|
+
return lowered === undefined ? undefined : [lowered];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const list = lowerCreateElementListCall(code, child, scope);
|
|
490
|
+
|
|
491
|
+
if (list !== undefined) {
|
|
492
|
+
return [list];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Provably-string expressions escape directly (and stay batchable); the
|
|
497
|
+
// interpreter would emit the same escaped bytes for them.
|
|
498
|
+
if (isProvablyStringExpression(child, scope)) {
|
|
499
|
+
return [{ kind: "expr", code: readSource(code, child) }];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Unknown expressions stay dynamic: the runtime helper escapes primitives
|
|
503
|
+
// and falls back to the interpreter for element values, so children that
|
|
504
|
+
// evaluate to react nodes keep rendering instead of stringifying.
|
|
505
|
+
return [{ kind: "expr", code: readSource(code, child), renderMode: "compat-child" }];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function isProvablyStringExpression(
|
|
509
|
+
expression: Record<string, unknown>,
|
|
510
|
+
scope: CompatCreateElementScope,
|
|
511
|
+
): boolean {
|
|
512
|
+
if (expression.type === "TemplateLiteral") {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (expression.type === "CallExpression" && expression.optional !== true) {
|
|
517
|
+
const callee = readObject(expression.callee);
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
callee.type === "Identifier" &&
|
|
521
|
+
callee.name === "String" &&
|
|
522
|
+
!scope.shadowed.has("String") &&
|
|
523
|
+
readArray(expression.arguments).length === 1 &&
|
|
524
|
+
readObject(readArray(expression.arguments)[0]).type !== "SpreadElement"
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (expression.type === "BinaryExpression" && expression.operator === "+") {
|
|
529
|
+
return (
|
|
530
|
+
isProvablyStringExpression(unwrapOxcParentheses(readObject(expression.left)), scope) ||
|
|
531
|
+
isProvablyStringExpression(unwrapOxcParentheses(readObject(expression.right)), scope)
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (expression.type === "Literal") {
|
|
536
|
+
return typeof expression.value === "string";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function lowerCreateElementListCall(
|
|
543
|
+
code: string,
|
|
544
|
+
expression: Record<string, unknown>,
|
|
545
|
+
scope: CompatCreateElementScope,
|
|
546
|
+
): JsxNodeIr | undefined {
|
|
547
|
+
if (expression.optional === true) {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const callee = readObject(expression.callee);
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
callee.type !== "MemberExpression" ||
|
|
555
|
+
callee.computed === true ||
|
|
556
|
+
callee.optional === true ||
|
|
557
|
+
readObject(callee.property).name !== "map"
|
|
558
|
+
) {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const args = readArray(expression.arguments);
|
|
563
|
+
|
|
564
|
+
if (args.length !== 1) {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const renderer = readObject(args[0]);
|
|
569
|
+
|
|
570
|
+
if (renderer.type !== "ArrowFunctionExpression" || renderer.async === true) {
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const parameters = readArray(renderer.params).map((parameter) => readObject(parameter));
|
|
575
|
+
|
|
576
|
+
if (parameters.some((parameter) => parameter.type !== "Identifier")) {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const [itemName, indexName, arrayName] = parameters.map((parameter) =>
|
|
581
|
+
typeof parameter.name === "string" ? parameter.name : undefined,
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
if (itemName === undefined) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const rendererScope: CompatCreateElementScope = {
|
|
589
|
+
names: scope.names,
|
|
590
|
+
shadowed: new Set([
|
|
591
|
+
...scope.shadowed,
|
|
592
|
+
...[itemName, indexName, arrayName].filter((name): name is string => name !== undefined),
|
|
593
|
+
]),
|
|
594
|
+
};
|
|
595
|
+
const body = unwrapOxcParentheses(readObject(renderer.body));
|
|
596
|
+
|
|
597
|
+
if (body.type === "BlockStatement") {
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const rendered = lowerCreateElementCall(code, body, rendererScope);
|
|
602
|
+
|
|
603
|
+
if (rendered === undefined || rendered.kind !== "element") {
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
kind: "list",
|
|
609
|
+
itemsCode: readSource(code, readObject(callee.object)),
|
|
610
|
+
itemName,
|
|
611
|
+
...(indexName === undefined ? {} : { indexName }),
|
|
612
|
+
...(arrayName === undefined ? {} : { arrayName }),
|
|
613
|
+
...(rendered.keyCode === undefined ? {} : { keyCode: rendered.keyCode }),
|
|
614
|
+
children: [rendered],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Detection-side predicate: a direct `return createElement(...)` (or arrow
|
|
619
|
+
// implicit body) that the converter can fully lower. Detection and lowering
|
|
620
|
+
// share lowerCreateElementCall so they can never disagree.
|
|
621
|
+
export function hasLowerableCompatCreateElementReturn(
|
|
622
|
+
code: string,
|
|
623
|
+
functionLike: Record<string, unknown>,
|
|
624
|
+
names: ReadonlySet<string>,
|
|
625
|
+
): boolean {
|
|
626
|
+
if (names.size === 0) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const shadowed = collectFunctionShadowedNames(functionLike, names);
|
|
631
|
+
const scope: CompatCreateElementScope = { names, shadowed };
|
|
632
|
+
const body = unwrapOxcParentheses(readObject(functionLike.body));
|
|
633
|
+
|
|
634
|
+
if (body.type !== "BlockStatement") {
|
|
635
|
+
return lowerCreateElementCall(code, body, scope) !== undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
for (const statement of readArray(body.body)) {
|
|
639
|
+
const statementObject = readObject(statement);
|
|
640
|
+
|
|
641
|
+
if (statementObject.type !== "ReturnStatement") {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const argument = unwrapOxcParentheses(readObject(statementObject.argument));
|
|
646
|
+
|
|
647
|
+
return lowerCreateElementCall(code, argument, scope) !== undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function collectFunctionShadowedNames(
|
|
654
|
+
functionLike: Record<string, unknown>,
|
|
655
|
+
names: ReadonlySet<string>,
|
|
656
|
+
): Set<string> {
|
|
657
|
+
const shadowed = new Set<string>();
|
|
658
|
+
|
|
659
|
+
for (const parameter of readArray(functionLike.params)) {
|
|
660
|
+
const parameterObject = readObject(parameter);
|
|
661
|
+
|
|
662
|
+
if (parameterObject.type === "Identifier" && typeof parameterObject.name === "string") {
|
|
663
|
+
if (names.has(parameterObject.name)) {
|
|
664
|
+
shadowed.add(parameterObject.name);
|
|
665
|
+
}
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Destructured parameters could shadow anything; treat every tracked
|
|
670
|
+
// name as shadowed rather than analyzing the pattern.
|
|
671
|
+
for (const name of names) {
|
|
672
|
+
shadowed.add(name);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const body = readObject(functionLike.body);
|
|
677
|
+
|
|
678
|
+
if (body.type !== "BlockStatement") {
|
|
679
|
+
return shadowed;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
for (const statement of readArray(body.body)) {
|
|
683
|
+
const statementObject = readObject(statement);
|
|
684
|
+
|
|
685
|
+
if (statementObject.type === "VariableDeclaration") {
|
|
686
|
+
for (const declarator of readArray(statementObject.declarations)) {
|
|
687
|
+
const id = readObject(readObject(declarator).id);
|
|
688
|
+
|
|
689
|
+
if (id.type === "Identifier" && typeof id.name === "string" && names.has(id.name)) {
|
|
690
|
+
shadowed.add(id.name);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (
|
|
696
|
+
statementObject.type === "FunctionDeclaration" &&
|
|
697
|
+
typeof readObject(statementObject.id).name === "string" &&
|
|
698
|
+
names.has(readObject(statementObject.id).name as string)
|
|
699
|
+
) {
|
|
700
|
+
shadowed.add(readObject(statementObject.id).name as string);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return shadowed;
|
|
705
|
+
}
|
package/src/oxc-dom-lowering.ts
CHANGED
|
@@ -26,7 +26,7 @@ export function lowerOxcDomNodeExpression(
|
|
|
26
26
|
|
|
27
27
|
if (right !== undefined && unwrapped.operator === "||") {
|
|
28
28
|
const left = readSource(code, readObject(unwrapped.left));
|
|
29
|
-
return `((${left}) ?
|
|
29
|
+
return `(() => { const _left = (${left}); return _left ? _left : ${right}; })()`;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|