@reckona/mreact-compiler 0.0.66 → 0.0.67

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.
@@ -0,0 +1,620 @@
1
+ import type { JsxNodeIr } from "./ir.js";
2
+ import { readArray, readObject, readSource, unwrapOxcParentheses } from "./oxc-node-utils.js";
3
+
4
+ interface ReactiveAliasReplacement {
5
+ end: number;
6
+ start: number;
7
+ text: string;
8
+ }
9
+
10
+ interface ReactiveAliasExpressionState {
11
+ reactive: boolean;
12
+ safe: boolean;
13
+ }
14
+
15
+ export function collectOxcBodyJsxBindingNames(statements: readonly unknown[]): Set<string> {
16
+ const names = new Set<string>();
17
+
18
+ for (const statement of statements) {
19
+ const object = readObject(statement);
20
+
21
+ if (object.type === "ForOfStatement" || object.type === "ForStatement") {
22
+ collectOxcPushJsxBindingNames(readArray(readObject(object.body).body), names);
23
+ continue;
24
+ }
25
+
26
+ if (object.type !== "VariableDeclaration") {
27
+ continue;
28
+ }
29
+
30
+ const declarationKind = typeof object.kind === "string" ? object.kind : "let";
31
+ const isImmutableBinding = declarationKind === "const";
32
+
33
+ for (const declarationValue of readArray(object.declarations)) {
34
+ const declaration = readObject(declarationValue);
35
+ const id = readObject(declaration.id);
36
+ const initializer = unwrapOxcParentheses(readObject(declaration.init));
37
+
38
+ if (typeof id.name !== "string") continue;
39
+ if (!containsOxcJsxSyntax(initializer)) continue;
40
+ if (!isJsxLikeInitializer(initializer)) continue;
41
+ if (!isImmutableBinding && isBindingReassigned(statements, id.name)) {
42
+ continue;
43
+ }
44
+ names.add(id.name);
45
+ }
46
+ }
47
+
48
+ return names;
49
+ }
50
+
51
+ export function collectOxcReactiveReadAliases(
52
+ code: string,
53
+ statements: readonly unknown[],
54
+ ): Map<string, string> {
55
+ const aliases = new Map<string, string>();
56
+
57
+ for (const statement of statements) {
58
+ const object = readObject(statement);
59
+
60
+ if (object.type !== "VariableDeclaration" || object.kind !== "const") {
61
+ continue;
62
+ }
63
+
64
+ for (const declarationValue of readArray(object.declarations)) {
65
+ const declaration = readObject(declarationValue);
66
+ const id = readObject(declaration.id);
67
+ const initializer = unwrapOxcParentheses(readObject(declaration.init));
68
+
69
+ if (typeof id.name !== "string") continue;
70
+ if (!isOxcReactiveAliasExpression(initializer)) continue;
71
+
72
+ aliases.set(id.name, readSource(code, initializer));
73
+ }
74
+ }
75
+
76
+ return aliases;
77
+ }
78
+
79
+ export function rewriteOxcReactiveAliasExpressionCode(
80
+ code: string,
81
+ expression: Record<string, unknown>,
82
+ aliases: ReadonlyMap<string, string> | undefined,
83
+ ): string | undefined {
84
+ if (aliases === undefined || aliases.size === 0) {
85
+ return undefined;
86
+ }
87
+
88
+ const expressionStart = readNumber(expression.start);
89
+ const expressionEnd = readNumber(expression.end);
90
+
91
+ if (expressionStart === undefined || expressionEnd === undefined) {
92
+ return undefined;
93
+ }
94
+
95
+ const replacements: ReactiveAliasReplacement[] = [];
96
+ collectOxcReactiveAliasReplacements(expression, undefined, undefined, aliases, new Set(), replacements);
97
+
98
+ if (replacements.length === 0) {
99
+ return undefined;
100
+ }
101
+
102
+ let source = readSource(code, expression);
103
+
104
+ for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
105
+ const start = replacement.start - expressionStart;
106
+ const end = replacement.end - expressionStart;
107
+
108
+ if (start < 0 || end > source.length || start > end) {
109
+ return undefined;
110
+ }
111
+
112
+ source = `${source.slice(0, start)}${replacement.text}${source.slice(end)}`;
113
+ }
114
+
115
+ return source;
116
+ }
117
+
118
+ function collectOxcReactiveAliasReplacements(
119
+ node: unknown,
120
+ parent: Record<string, unknown> | undefined,
121
+ parentKey: string | undefined,
122
+ aliases: ReadonlyMap<string, string>,
123
+ shadowed: ReadonlySet<string>,
124
+ replacements: ReactiveAliasReplacement[],
125
+ ): void {
126
+ const object = readObject(node);
127
+
128
+ if (typeof object.type !== "string") {
129
+ return;
130
+ }
131
+
132
+ if (object.type.startsWith("TS")) {
133
+ return;
134
+ }
135
+
136
+ if (
137
+ object.type === "Identifier" &&
138
+ typeof object.name === "string" &&
139
+ !shadowed.has(object.name) &&
140
+ isOxcReactiveAliasReference(object, parent, parentKey)
141
+ ) {
142
+ const replacement = aliases.get(object.name);
143
+ const start = readNumber(object.start);
144
+ const end = readNumber(object.end);
145
+
146
+ if (replacement !== undefined && start !== undefined && end !== undefined) {
147
+ replacements.push({
148
+ end,
149
+ start,
150
+ text: isOxcShorthandPropertyValue(object, parent) ? `${object.name}: (${replacement})` : `(${replacement})`,
151
+ });
152
+ }
153
+ return;
154
+ }
155
+
156
+ if (isOxcFunctionNode(object)) {
157
+ const functionShadowed = new Set(shadowed);
158
+ collectOxcBindingNames(object.id, functionShadowed);
159
+ for (const parameter of readArray(object.params)) {
160
+ collectOxcBindingNames(parameter, functionShadowed);
161
+ }
162
+ collectOxcReactiveAliasReplacements(
163
+ object.body,
164
+ object,
165
+ "body",
166
+ aliases,
167
+ addOxcBlockBindingNames(object.body, functionShadowed),
168
+ replacements,
169
+ );
170
+ return;
171
+ }
172
+
173
+ const childShadowed =
174
+ object.type === "BlockStatement" || object.type === "Program"
175
+ ? addOxcBlockBindingNames(object, new Set(shadowed))
176
+ : shadowed;
177
+
178
+ for (const [key, value] of Object.entries(object)) {
179
+ if (key === "type" || key === "start" || key === "end" || key === "loc") {
180
+ continue;
181
+ }
182
+
183
+ if (key === "id" && isOxcDeclarationWithId(object)) {
184
+ continue;
185
+ }
186
+
187
+ if (key === "params" && isOxcFunctionNode(object)) {
188
+ continue;
189
+ }
190
+
191
+ if (key === "key" && isOxcNonComputedKey(object)) {
192
+ continue;
193
+ }
194
+
195
+ if (Array.isArray(value)) {
196
+ for (const item of value) {
197
+ collectOxcReactiveAliasReplacements(item, object, key, aliases, childShadowed, replacements);
198
+ }
199
+ continue;
200
+ }
201
+
202
+ if (typeof value === "object" && value !== null) {
203
+ collectOxcReactiveAliasReplacements(value, object, key, aliases, childShadowed, replacements);
204
+ }
205
+ }
206
+ }
207
+
208
+ function isOxcReactiveAliasReference(
209
+ node: Record<string, unknown>,
210
+ parent: Record<string, unknown> | undefined,
211
+ parentKey: string | undefined,
212
+ ): boolean {
213
+ if (parent === undefined) {
214
+ return true;
215
+ }
216
+
217
+ if (parent.type === "MemberExpression" && parentKey === "property" && parent.computed !== true) {
218
+ return false;
219
+ }
220
+
221
+ if (parentKey === "id" && isOxcDeclarationWithId(parent)) {
222
+ return false;
223
+ }
224
+
225
+ if (parentKey === "params" && isOxcFunctionNode(parent)) {
226
+ return false;
227
+ }
228
+
229
+ if (parentKey === "key" && isOxcNonComputedKey(parent)) {
230
+ return isOxcShorthandPropertyValue(node, parent);
231
+ }
232
+
233
+ if (
234
+ (parent.type === "BreakStatement" ||
235
+ parent.type === "ContinueStatement" ||
236
+ parent.type === "LabeledStatement") &&
237
+ parentKey === "label"
238
+ ) {
239
+ return false;
240
+ }
241
+
242
+ return true;
243
+ }
244
+
245
+ function isOxcShorthandPropertyValue(
246
+ node: Record<string, unknown>,
247
+ parent: Record<string, unknown> | undefined,
248
+ ): boolean {
249
+ const key = readObject(parent?.key);
250
+ return (
251
+ parent !== undefined &&
252
+ (parent.type === "Property" || parent.type === "ObjectProperty") &&
253
+ parent.shorthand === true &&
254
+ parent.value === node &&
255
+ readNumber(key.start) === readNumber(node.start) &&
256
+ readNumber(key.end) === readNumber(node.end)
257
+ );
258
+ }
259
+
260
+ function isOxcNonComputedKey(node: Record<string, unknown>): boolean {
261
+ return (
262
+ (node.type === "Property" ||
263
+ node.type === "ObjectProperty" ||
264
+ node.type === "PropertyDefinition" ||
265
+ node.type === "MethodDefinition") &&
266
+ node.computed !== true
267
+ );
268
+ }
269
+
270
+ function isOxcDeclarationWithId(node: Record<string, unknown>): boolean {
271
+ return (
272
+ node.type === "VariableDeclarator" ||
273
+ node.type === "FunctionDeclaration" ||
274
+ node.type === "FunctionExpression" ||
275
+ node.type === "ClassDeclaration" ||
276
+ node.type === "ClassExpression"
277
+ );
278
+ }
279
+
280
+ function isOxcFunctionNode(node: Record<string, unknown>): boolean {
281
+ return (
282
+ node.type === "ArrowFunctionExpression" ||
283
+ node.type === "FunctionDeclaration" ||
284
+ node.type === "FunctionExpression"
285
+ );
286
+ }
287
+
288
+ function addOxcBlockBindingNames(
289
+ node: unknown,
290
+ shadowed: Set<string>,
291
+ ): ReadonlySet<string> {
292
+ const object = readObject(node);
293
+ const body = readArray(object.body);
294
+
295
+ if (body.length === 0) {
296
+ return shadowed;
297
+ }
298
+
299
+ for (const statement of body) {
300
+ collectOxcStatementBindingNames(statement, shadowed);
301
+ }
302
+
303
+ return shadowed;
304
+ }
305
+
306
+ function collectOxcStatementBindingNames(node: unknown, names: Set<string>): void {
307
+ const object = readObject(node);
308
+
309
+ if (object.type === "VariableDeclaration") {
310
+ for (const declaration of readArray(object.declarations)) {
311
+ collectOxcBindingNames(readObject(declaration).id, names);
312
+ }
313
+ return;
314
+ }
315
+
316
+ if (object.type === "FunctionDeclaration" || object.type === "ClassDeclaration") {
317
+ collectOxcBindingNames(object.id, names);
318
+ }
319
+ }
320
+
321
+ function collectOxcBindingNames(node: unknown, names: Set<string>): void {
322
+ const object = readObject(node);
323
+
324
+ if (object.type === "Identifier" && typeof object.name === "string") {
325
+ names.add(object.name);
326
+ return;
327
+ }
328
+
329
+ if (object.type === "RestElement") {
330
+ collectOxcBindingNames(object.argument, names);
331
+ return;
332
+ }
333
+
334
+ if (object.type === "AssignmentPattern") {
335
+ collectOxcBindingNames(object.left, names);
336
+ return;
337
+ }
338
+
339
+ if (object.type === "ArrayPattern") {
340
+ for (const element of readArray(object.elements)) {
341
+ collectOxcBindingNames(element, names);
342
+ }
343
+ return;
344
+ }
345
+
346
+ if (object.type === "ObjectPattern") {
347
+ for (const property of readArray(object.properties)) {
348
+ collectOxcBindingNames(readObject(property).value ?? readObject(property).argument, names);
349
+ }
350
+ }
351
+ }
352
+
353
+ function readNumber(value: unknown): number | undefined {
354
+ return typeof value === "number" ? value : undefined;
355
+ }
356
+
357
+ export function markOxcRenderValueExpressions(
358
+ nodes: readonly JsxNodeIr[],
359
+ names: Set<string>,
360
+ renderMode: "dynamic" | "html" = "dynamic",
361
+ ): void {
362
+ if (names.size === 0) {
363
+ return;
364
+ }
365
+
366
+ for (const node of nodes) {
367
+ if (node.kind === "expr" && names.has(node.code)) {
368
+ node.renderMode = renderMode;
369
+ continue;
370
+ }
371
+
372
+ if (node.kind === "conditional") {
373
+ markOxcRenderValueExpressions(node.whenTrue, names, renderMode);
374
+ markOxcRenderValueExpressions(node.whenFalse, names, renderMode);
375
+ continue;
376
+ }
377
+
378
+ if (node.kind === "list") {
379
+ markOxcRenderValueExpressions(node.children, names, renderMode);
380
+ continue;
381
+ }
382
+
383
+ if (node.kind === "fragment" || node.kind === "element" || node.kind === "component") {
384
+ markOxcRenderValueExpressions(node.children, names, renderMode);
385
+ }
386
+ }
387
+ }
388
+
389
+ export function isOxcRenderValueExpression(expression: Record<string, unknown>): boolean {
390
+ if (isOxcRendererCallExpression(expression)) {
391
+ return true;
392
+ }
393
+
394
+ if (expression.type !== "MemberExpression") {
395
+ return false;
396
+ }
397
+
398
+ const object = readObject(expression.object);
399
+ const property = readObject(expression.property);
400
+
401
+ return (
402
+ object.type === "Identifier" &&
403
+ object.name === "props" &&
404
+ typeof property.name === "string" &&
405
+ ["children", "fallback", "header", "sidebar", "element"].includes(property.name)
406
+ );
407
+ }
408
+
409
+ function isOxcRendererCallExpression(expression: Record<string, unknown>): boolean {
410
+ if (expression.type !== "CallExpression") {
411
+ return false;
412
+ }
413
+
414
+ const callee = readObject(expression.callee);
415
+
416
+ return (
417
+ callee.type === "Identifier" &&
418
+ typeof callee.name === "string" &&
419
+ /^render[A-Z0-9_$]/.test(callee.name)
420
+ );
421
+ }
422
+
423
+ export function containsOxcJsxSyntax(node: Record<string, unknown>): boolean {
424
+ if (node.type === "JSXElement" || node.type === "JSXFragment") {
425
+ return true;
426
+ }
427
+
428
+ return Object.values(node).some((value) =>
429
+ Array.isArray(value)
430
+ ? value.some((item) => containsOxcJsxSyntax(readObject(item)))
431
+ : typeof value === "object" && value !== null && containsOxcJsxSyntax(readObject(value)),
432
+ );
433
+ }
434
+
435
+ function isJsxLikeInitializer(node: Record<string, unknown>): boolean {
436
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
437
+ if (node.type === "ConditionalExpression") {
438
+ return (
439
+ isJsxLikeInitializer(readObject(node.consequent)) ||
440
+ isJsxLikeInitializer(readObject(node.alternate))
441
+ );
442
+ }
443
+ if (node.type === "LogicalExpression") {
444
+ return (
445
+ isJsxLikeInitializer(readObject(node.left)) || isJsxLikeInitializer(readObject(node.right))
446
+ );
447
+ }
448
+ if (node.type === "ArrayExpression" || node.type === "ObjectExpression") {
449
+ return false;
450
+ }
451
+ return containsOxcJsxSyntax(node);
452
+ }
453
+
454
+ function isBindingReassigned(statements: readonly unknown[], name: string): boolean {
455
+ for (const statement of statements) {
456
+ if (containsAssignmentTo(readObject(statement), name)) return true;
457
+ }
458
+ return false;
459
+ }
460
+
461
+ function isOxcReactiveReadExpression(expression: Record<string, unknown>): boolean {
462
+ if (expression.type !== "CallExpression") {
463
+ return false;
464
+ }
465
+
466
+ const callee = readObject(expression.callee);
467
+
468
+ if (callee.type !== "MemberExpression" || callee.computed === true || callee.optional === true) {
469
+ return false;
470
+ }
471
+
472
+ const property = readObject(callee.property);
473
+
474
+ return property.type === "Identifier" && property.name === "get";
475
+ }
476
+
477
+ function isOxcReactiveAliasExpression(expression: Record<string, unknown>): boolean {
478
+ const state = analyzeOxcReactiveAliasExpression(expression);
479
+ return state.safe && state.reactive;
480
+ }
481
+
482
+ function analyzeOxcReactiveAliasExpression(
483
+ expression: Record<string, unknown>,
484
+ ): ReactiveAliasExpressionState {
485
+ const unwrappedExpression = unwrapOxcParentheses(expression);
486
+
487
+ if (
488
+ unwrappedExpression.type === "Literal" ||
489
+ unwrappedExpression.type === "Identifier" ||
490
+ unwrappedExpression.type === "ThisExpression"
491
+ ) {
492
+ return { reactive: false, safe: true };
493
+ }
494
+
495
+ if (isOxcReactiveReadExpression(unwrappedExpression)) {
496
+ return { reactive: true, safe: true };
497
+ }
498
+
499
+ if (unwrappedExpression.type === "ChainExpression") {
500
+ return analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.expression));
501
+ }
502
+
503
+ if (
504
+ unwrappedExpression.type === "TSAsExpression" ||
505
+ unwrappedExpression.type === "TSSatisfiesExpression" ||
506
+ unwrappedExpression.type === "TSNonNullExpression" ||
507
+ unwrappedExpression.type === "TSInstantiationExpression" ||
508
+ unwrappedExpression.type === "TypeCastExpression"
509
+ ) {
510
+ return analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.expression));
511
+ }
512
+
513
+ if (unwrappedExpression.type === "MemberExpression") {
514
+ const objectState = analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.object));
515
+ const propertyState =
516
+ unwrappedExpression.computed === true
517
+ ? analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.property))
518
+ : { reactive: false, safe: true };
519
+
520
+ return {
521
+ reactive: objectState.reactive || propertyState.reactive,
522
+ safe: objectState.safe && propertyState.safe,
523
+ };
524
+ }
525
+
526
+ if (unwrappedExpression.type === "UnaryExpression") {
527
+ return analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.argument));
528
+ }
529
+
530
+ if (
531
+ unwrappedExpression.type === "BinaryExpression" ||
532
+ unwrappedExpression.type === "LogicalExpression"
533
+ ) {
534
+ const leftState = analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.left));
535
+ const rightState = analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.right));
536
+
537
+ return {
538
+ reactive: leftState.reactive || rightState.reactive,
539
+ safe: leftState.safe && rightState.safe,
540
+ };
541
+ }
542
+
543
+ if (unwrappedExpression.type === "ConditionalExpression") {
544
+ const testState = analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.test));
545
+ const consequentState = analyzeOxcReactiveAliasExpression(
546
+ readObject(unwrappedExpression.consequent),
547
+ );
548
+ const alternateState = analyzeOxcReactiveAliasExpression(
549
+ readObject(unwrappedExpression.alternate),
550
+ );
551
+
552
+ return {
553
+ reactive: testState.reactive || consequentState.reactive || alternateState.reactive,
554
+ safe: testState.safe && consequentState.safe && alternateState.safe,
555
+ };
556
+ }
557
+
558
+ return { reactive: false, safe: false };
559
+ }
560
+
561
+ function containsAssignmentTo(node: Record<string, unknown>, name: string): boolean {
562
+ if (node.type === "AssignmentExpression") {
563
+ const left = readObject(node.left);
564
+ if (left.type === "Identifier" && left.name === name) return true;
565
+ }
566
+ if (node.type === "UpdateExpression") {
567
+ const argument = readObject(node.argument);
568
+ if (argument.type === "Identifier" && argument.name === name) return true;
569
+ }
570
+ for (const value of Object.values(node)) {
571
+ if (Array.isArray(value)) {
572
+ for (const item of value) {
573
+ if (
574
+ typeof item === "object" &&
575
+ item !== null &&
576
+ containsAssignmentTo(readObject(item), name)
577
+ ) {
578
+ return true;
579
+ }
580
+ }
581
+ } else if (typeof value === "object" && value !== null) {
582
+ if (containsAssignmentTo(readObject(value), name)) return true;
583
+ }
584
+ }
585
+ return false;
586
+ }
587
+
588
+ function collectOxcPushJsxBindingNames(statements: readonly unknown[], names: Set<string>): void {
589
+ for (const statement of statements) {
590
+ const object = readObject(statement);
591
+
592
+ if (object.type === "ForOfStatement" || object.type === "ForStatement") {
593
+ collectOxcPushJsxBindingNames(readArray(readObject(object.body).body), names);
594
+ continue;
595
+ }
596
+
597
+ const expression = readObject(object.expression);
598
+
599
+ if (object.type !== "ExpressionStatement" || expression.type !== "CallExpression") {
600
+ continue;
601
+ }
602
+
603
+ const callee = readObject(expression.callee);
604
+ const argument = unwrapOxcParentheses(readObject(readArray(expression.arguments)[0]));
605
+
606
+ if (
607
+ callee.type !== "MemberExpression" ||
608
+ readObject(callee.property).name !== "push" ||
609
+ !containsOxcJsxSyntax(argument)
610
+ ) {
611
+ continue;
612
+ }
613
+
614
+ const target = readObject(callee.object);
615
+
616
+ if (typeof target.name === "string") {
617
+ names.add(target.name);
618
+ }
619
+ }
620
+ }