@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 CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
5
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](https://nodejs.org)
6
- [![Tests](https://img.shields.io/badge/tests-69%20passing-brightgreen?style=flat-square)](./tests)
6
+ [![Tests](https://img.shields.io/badge/tests-74%20passing-brightgreen?style=flat-square)](./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
 
@@ -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 === "\\" && prev !== "\\") {
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 === "`") && (i === 0 || inner[i - 1] !== "\\")) {
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 === "`") && (i === 0 || inner[i - 1] !== "\\")) {
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
- content.slice(0, idx) +
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 (skip matches inside strings/comments)
486
- if (changed) {
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.5.5",
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)");