@thinksharpe/react-compiler-unmemo 0.5.5 → 0.6.0
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 +41 -11
- package/package.json +3 -2
- package/react-compiler-unmemo.mjs +2 -2
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
|
}
|
|
@@ -124,7 +137,7 @@ export function stripDepsArray(inner) {
|
|
|
124
137
|
const ch = inner[i];
|
|
125
138
|
|
|
126
139
|
// Simple string tracking
|
|
127
|
-
if ((ch === '"' || ch === "'" || ch === "`") && (
|
|
140
|
+
if ((ch === '"' || ch === "'" || ch === "`") && !isEscaped(inner, i)) {
|
|
128
141
|
if (!inString) {
|
|
129
142
|
inString = true;
|
|
130
143
|
stringChar = ch;
|
|
@@ -178,7 +191,7 @@ export function unwrapArrowFn(inner) {
|
|
|
178
191
|
for (let i = 0; i < inner.length - 1; i++) {
|
|
179
192
|
const ch = inner[i];
|
|
180
193
|
|
|
181
|
-
if ((ch === '"' || ch === "'" || ch === "`") && (
|
|
194
|
+
if ((ch === '"' || ch === "'" || ch === "`") && !isEscaped(inner, i)) {
|
|
182
195
|
if (!inString) {
|
|
183
196
|
inString = true;
|
|
184
197
|
stringChar = ch;
|
|
@@ -312,6 +325,8 @@ export function processFile(filePath, { dryRun = false } = {}) {
|
|
|
312
325
|
const transformations = [];
|
|
313
326
|
|
|
314
327
|
// Phase 1: Strip generic type params like useMemo<ColumnsType<Foo>>( -> useMemo(
|
|
328
|
+
// Store stripped generics so Phase 2 can apply them as type annotations
|
|
329
|
+
const strippedGenerics = new Map();
|
|
315
330
|
const hookNames = [
|
|
316
331
|
"React.useMemo",
|
|
317
332
|
"React.useCallback",
|
|
@@ -360,6 +375,8 @@ export function processFile(filePath, { dryRun = false } = {}) {
|
|
|
360
375
|
const genericType = content.slice(angleStart + 1, angleEnd);
|
|
361
376
|
content =
|
|
362
377
|
content.slice(0, angleStart) + content.slice(angleEnd + 1);
|
|
378
|
+
// After stripping, the hook call starts at hookIdx — store the generic
|
|
379
|
+
strippedGenerics.set(hookIdx, genericType);
|
|
363
380
|
changed = true;
|
|
364
381
|
transformations.push({
|
|
365
382
|
type: "strip-generic",
|
|
@@ -456,15 +473,23 @@ export function processFile(filePath, { dryRun = false } = {}) {
|
|
|
456
473
|
replacement = withoutDeps;
|
|
457
474
|
}
|
|
458
475
|
|
|
459
|
-
const beforeMatch = content.slice(
|
|
460
|
-
Math.max(0, idx - 50),
|
|
461
|
-
idx,
|
|
462
|
-
);
|
|
463
476
|
const lineNum =
|
|
464
477
|
content.slice(0, idx).split("\n").length;
|
|
465
478
|
|
|
479
|
+
// If Phase 1 stripped a generic type, inject it as a type annotation
|
|
480
|
+
const genericType = strippedGenerics.get(idx);
|
|
481
|
+
let prefix = content.slice(0, idx);
|
|
482
|
+
if (genericType) {
|
|
483
|
+
const declMatch = prefix.match(/((?:const|let|var)\s+\w+)\s*=\s*$/);
|
|
484
|
+
if (declMatch) {
|
|
485
|
+
const insertPos = prefix.length - declMatch[0].length + declMatch[1].length;
|
|
486
|
+
prefix = prefix.slice(0, insertPos) + ": " + genericType + prefix.slice(insertPos);
|
|
487
|
+
}
|
|
488
|
+
strippedGenerics.delete(idx);
|
|
489
|
+
}
|
|
490
|
+
|
|
466
491
|
content =
|
|
467
|
-
|
|
492
|
+
prefix +
|
|
468
493
|
replacement +
|
|
469
494
|
content.slice(closeParenIdx + 1);
|
|
470
495
|
changed = true;
|
|
@@ -482,8 +507,8 @@ export function processFile(filePath, { dryRun = false } = {}) {
|
|
|
482
507
|
if (!found) break;
|
|
483
508
|
}
|
|
484
509
|
|
|
485
|
-
// Phase 3: Clean up imports (
|
|
486
|
-
|
|
510
|
+
// Phase 3: Clean up imports (always run — hooks are unnecessary with React Compiler)
|
|
511
|
+
{
|
|
487
512
|
const importPatterns = [
|
|
488
513
|
{
|
|
489
514
|
regex: /import React, \{([^}]*)\} from (["'])react\2[^\S\n]*;?/g,
|
|
@@ -534,6 +559,11 @@ export function processFile(filePath, { dryRun = false } = {}) {
|
|
|
534
559
|
content = content.replace(/\n\n\n+/g, "\n\n");
|
|
535
560
|
}
|
|
536
561
|
|
|
562
|
+
// Track if imports were cleaned even without hook call changes
|
|
563
|
+
if (content !== original) {
|
|
564
|
+
changed = true;
|
|
565
|
+
}
|
|
566
|
+
|
|
537
567
|
if (changed && !dryRun) {
|
|
538
568
|
fs.writeFileSync(filePath, content);
|
|
539
569
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinksharpe/react-compiler-unmemo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Remove useMemo and useCallback hooks from React codebases to leverage React Compiler automatic optimization",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"automatic-memoization"
|
|
26
26
|
],
|
|
27
27
|
"bin": {
|
|
28
|
-
"react-compiler-unmemo": "react-compiler-unmemo.mjs"
|
|
28
|
+
"react-compiler-unmemo": "react-compiler-unmemo.mjs",
|
|
29
|
+
"unmemo": "react-compiler-unmemo.mjs"
|
|
29
30
|
},
|
|
30
31
|
"main": "./react-compiler-unmemo.mjs",
|
|
31
32
|
"exports": {
|
|
@@ -34,7 +34,7 @@ import { run as removeHooks } from "./helpers/remove-hooks.mjs";
|
|
|
34
34
|
|
|
35
35
|
const args = process.argv.slice(2);
|
|
36
36
|
|
|
37
|
-
if (args.includes("--version") || args.includes("-v")) {
|
|
37
|
+
if (args.includes("--version") || args.includes("-v") || args[0] === "version") {
|
|
38
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
39
39
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"));
|
|
40
40
|
console.log(pkg.version);
|
|
@@ -63,7 +63,7 @@ if (!targetDir) {
|
|
|
63
63
|
console.log(" --verbose Show detailed output per transformation");
|
|
64
64
|
console.log(" --files <glob> File glob pattern (default: src/**/*.{tsx,ts})");
|
|
65
65
|
console.log(" --skip-fix Skip the type annotation fix step");
|
|
66
|
-
console.log(" --version, -v Show version number");
|
|
66
|
+
console.log(" --version, -v Show version number (use 'version' subcommand with npx)");
|
|
67
67
|
console.log();
|
|
68
68
|
console.log("Examples:");
|
|
69
69
|
console.log(" npx react-compiler-unmemo ./my-react-app # preview (safe default)");
|