@thinksharpe/react-compiler-unmemo 0.5.4 → 0.5.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.
- package/README.md +1 -1
- package/helpers/remove-hooks.mjs +115 -33
- package/package.json +1 -1
- package/react-compiler-unmemo.mjs +12 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
|
-
[](./tests)
|
|
7
7
|
|
|
8
8
|
A codemod that removes `useMemo` and `useCallback` from your React codebase so you can adopt [React Compiler](https://react.dev/learn/react-compiler) without the manual cleanup.
|
|
9
9
|
|
package/helpers/remove-hooks.mjs
CHANGED
|
@@ -24,6 +24,20 @@ import path from "path";
|
|
|
24
24
|
|
|
25
25
|
// ─── Core Parsing Helpers ────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Check if the character at idx is escaped by counting consecutive
|
|
29
|
+
* backslashes before it. Odd count means escaped, even means not.
|
|
30
|
+
*/
|
|
31
|
+
export function isEscaped(str, idx) {
|
|
32
|
+
let count = 0;
|
|
33
|
+
let pos = idx - 1;
|
|
34
|
+
while (pos >= 0 && str[pos] === "\\") {
|
|
35
|
+
count++;
|
|
36
|
+
pos--;
|
|
37
|
+
}
|
|
38
|
+
return count % 2 === 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* Find the matching closing paren for an opening paren at startIdx.
|
|
29
43
|
* Counts nested parens/brackets/braces and respects strings and template literals.
|
|
@@ -37,10 +51,9 @@ export function findClosingParen(content, startIdx) {
|
|
|
37
51
|
|
|
38
52
|
for (let i = startIdx; i < content.length; i++) {
|
|
39
53
|
const ch = content[i];
|
|
40
|
-
const prev = i > 0 ? content[i - 1] : "";
|
|
41
54
|
|
|
42
55
|
// Handle escape sequences
|
|
43
|
-
if ((inString || inTemplate) && ch === "\\" &&
|
|
56
|
+
if ((inString || inTemplate) && ch === "\\" && !isEscaped(content, i)) {
|
|
44
57
|
i++; // skip next char
|
|
45
58
|
continue;
|
|
46
59
|
}
|
|
@@ -84,6 +97,22 @@ export function findClosingParen(content, startIdx) {
|
|
|
84
97
|
|
|
85
98
|
if (inString || inTemplate) continue;
|
|
86
99
|
|
|
100
|
+
// Skip line comments
|
|
101
|
+
if (ch === "/" && content[i + 1] === "/") {
|
|
102
|
+
const eol = content.indexOf("\n", i + 2);
|
|
103
|
+
if (eol === -1) return -1;
|
|
104
|
+
i = eol;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Skip block comments
|
|
109
|
+
if (ch === "/" && content[i + 1] === "*") {
|
|
110
|
+
const close = content.indexOf("*/", i + 2);
|
|
111
|
+
if (close === -1) return -1;
|
|
112
|
+
i = close + 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
87
116
|
if (ch === "(") depth++;
|
|
88
117
|
if (ch === ")") {
|
|
89
118
|
depth--;
|
|
@@ -108,7 +137,7 @@ export function stripDepsArray(inner) {
|
|
|
108
137
|
const ch = inner[i];
|
|
109
138
|
|
|
110
139
|
// Simple string tracking
|
|
111
|
-
if ((ch === '"' || ch === "'" || ch === "`") && (
|
|
140
|
+
if ((ch === '"' || ch === "'" || ch === "`") && !isEscaped(inner, i)) {
|
|
112
141
|
if (!inString) {
|
|
113
142
|
inString = true;
|
|
114
143
|
stringChar = ch;
|
|
@@ -120,6 +149,22 @@ export function stripDepsArray(inner) {
|
|
|
120
149
|
}
|
|
121
150
|
if (inString) continue;
|
|
122
151
|
|
|
152
|
+
// Skip line comments
|
|
153
|
+
if (ch === "/" && inner[i + 1] === "/") {
|
|
154
|
+
const eol = inner.indexOf("\n", i + 2);
|
|
155
|
+
if (eol === -1) break;
|
|
156
|
+
i = eol;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Skip block comments
|
|
161
|
+
if (ch === "/" && inner[i + 1] === "*") {
|
|
162
|
+
const close = inner.indexOf("*/", i + 2);
|
|
163
|
+
if (close === -1) break;
|
|
164
|
+
i = close + 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
123
168
|
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
124
169
|
if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
125
170
|
if (ch === "," && depth === 0) {
|
|
@@ -146,7 +191,7 @@ export function unwrapArrowFn(inner) {
|
|
|
146
191
|
for (let i = 0; i < inner.length - 1; i++) {
|
|
147
192
|
const ch = inner[i];
|
|
148
193
|
|
|
149
|
-
if ((ch === '"' || ch === "'" || ch === "`") && (
|
|
194
|
+
if ((ch === '"' || ch === "'" || ch === "`") && !isEscaped(inner, i)) {
|
|
150
195
|
if (!inString) {
|
|
151
196
|
inString = true;
|
|
152
197
|
stringChar = ch;
|
|
@@ -158,6 +203,22 @@ export function unwrapArrowFn(inner) {
|
|
|
158
203
|
}
|
|
159
204
|
if (inString) continue;
|
|
160
205
|
|
|
206
|
+
// Skip line comments
|
|
207
|
+
if (ch === "/" && inner[i + 1] === "/") {
|
|
208
|
+
const eol = inner.indexOf("\n", i + 2);
|
|
209
|
+
if (eol === -1) break;
|
|
210
|
+
i = eol;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Skip block comments
|
|
215
|
+
if (ch === "/" && inner[i + 1] === "*") {
|
|
216
|
+
const close = inner.indexOf("*/", i + 2);
|
|
217
|
+
if (close === -1) break;
|
|
218
|
+
i = close + 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
161
222
|
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
162
223
|
if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
163
224
|
if (ch === "=" && inner[i + 1] === ">" && depth === 0) {
|
|
@@ -434,42 +495,63 @@ export function processFile(filePath, { dryRun = false } = {}) {
|
|
|
434
495
|
if (!found) break;
|
|
435
496
|
}
|
|
436
497
|
|
|
437
|
-
// Phase 3: Clean up imports
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
498
|
+
// Phase 3: Clean up imports (always run — hooks are unnecessary with React Compiler)
|
|
499
|
+
{
|
|
500
|
+
const importPatterns = [
|
|
501
|
+
{
|
|
502
|
+
regex: /import React, \{([^}]*)\} from (["'])react\2[^\S\n]*;?/g,
|
|
503
|
+
replace: (match, imports, quote) => {
|
|
504
|
+
const semi = match.trimEnd().endsWith(";") ? ";" : "";
|
|
505
|
+
const cleaned = imports
|
|
506
|
+
.split(",")
|
|
507
|
+
.map((s) => s.trim())
|
|
508
|
+
.filter((s) => s && s !== "useMemo" && s !== "useCallback")
|
|
509
|
+
.join(", ");
|
|
510
|
+
if (!cleaned) return `import React from ${quote}react${quote}${semi}`;
|
|
511
|
+
return `import React, { ${cleaned} } from ${quote}react${quote}${semi}`;
|
|
512
|
+
},
|
|
451
513
|
},
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (!cleaned) return ""; // Remove empty import entirely
|
|
465
|
-
return `import { ${cleaned} } from ${quote}react${quote}${semi}`;
|
|
514
|
+
{
|
|
515
|
+
regex: /import \{([^}]*)\} from (["'])react\2[^\S\n]*;?/g,
|
|
516
|
+
replace: (match, imports, quote) => {
|
|
517
|
+
const semi = match.trimEnd().endsWith(";") ? ";" : "";
|
|
518
|
+
const cleaned = imports
|
|
519
|
+
.split(",")
|
|
520
|
+
.map((s) => s.trim())
|
|
521
|
+
.filter((s) => s && s !== "useMemo" && s !== "useCallback")
|
|
522
|
+
.join(", ");
|
|
523
|
+
if (!cleaned) return ""; // Remove empty import entirely
|
|
524
|
+
return `import { ${cleaned} } from ${quote}react${quote}${semi}`;
|
|
525
|
+
},
|
|
466
526
|
},
|
|
467
|
-
|
|
527
|
+
];
|
|
528
|
+
|
|
529
|
+
for (const { regex, replace } of importPatterns) {
|
|
530
|
+
let result = "";
|
|
531
|
+
let lastIndex = 0;
|
|
532
|
+
let m;
|
|
533
|
+
regex.lastIndex = 0;
|
|
534
|
+
while ((m = regex.exec(content)) !== null) {
|
|
535
|
+
if (isInsideStringOrComment(content, m.index)) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
result += content.slice(lastIndex, m.index);
|
|
539
|
+
result += replace(m[0], m[1], m[2]);
|
|
540
|
+
lastIndex = m.index + m[0].length;
|
|
541
|
+
}
|
|
542
|
+
result += content.slice(lastIndex);
|
|
543
|
+
content = result;
|
|
544
|
+
}
|
|
468
545
|
|
|
469
546
|
// Clean up any resulting blank lines from removed imports
|
|
470
547
|
content = content.replace(/\n\n\n+/g, "\n\n");
|
|
471
548
|
}
|
|
472
549
|
|
|
550
|
+
// Track if imports were cleaned even without hook call changes
|
|
551
|
+
if (content !== original) {
|
|
552
|
+
changed = true;
|
|
553
|
+
}
|
|
554
|
+
|
|
473
555
|
if (changed && !dryRun) {
|
|
474
556
|
fs.writeFileSync(filePath, content);
|
|
475
557
|
}
|
package/package.json
CHANGED
|
@@ -23,13 +23,24 @@
|
|
|
23
23
|
* npx react-compiler-unmemo /absolute/path/to/project --files "app/**\/*.tsx"
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import fs from "fs";
|
|
26
27
|
import path from "path";
|
|
28
|
+
import { fileURLToPath } from "url";
|
|
29
|
+
|
|
27
30
|
import { run as fixTypes } from "./helpers/fix-type-annotations.mjs";
|
|
28
31
|
import { run as removeHooks } from "./helpers/remove-hooks.mjs";
|
|
29
32
|
|
|
30
33
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
31
34
|
|
|
32
35
|
const args = process.argv.slice(2);
|
|
36
|
+
|
|
37
|
+
if (args.includes("--version") || args.includes("-v") || args[0] === "version") {
|
|
38
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"));
|
|
40
|
+
console.log(pkg.version);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
const write = args.includes("--write");
|
|
34
45
|
const dryRun = !write;
|
|
35
46
|
const verbose = args.includes("--verbose");
|
|
@@ -52,6 +63,7 @@ if (!targetDir) {
|
|
|
52
63
|
console.log(" --verbose Show detailed output per transformation");
|
|
53
64
|
console.log(" --files <glob> File glob pattern (default: src/**/*.{tsx,ts})");
|
|
54
65
|
console.log(" --skip-fix Skip the type annotation fix step");
|
|
66
|
+
console.log(" --version, -v Show version number (use 'version' subcommand with npx)");
|
|
55
67
|
console.log();
|
|
56
68
|
console.log("Examples:");
|
|
57
69
|
console.log(" npx react-compiler-unmemo ./my-react-app # preview (safe default)");
|