@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 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-62%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
  }
@@ -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 === "`") && (i === 0 || inner[i - 1] !== "\\")) {
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 === "`") && (i === 0 || inner[i - 1] !== "\\")) {
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
- if (changed) {
439
- // Handle: import React, { useMemo, useCallback, ... } from "react"; (single or double quotes, semicolon optional)
440
- content = content.replace(
441
- /import React, \{([^}]*)\} from (["'])react\2[^\S\n]*;?/g,
442
- (match, imports, quote) => {
443
- const semi = match.trimEnd().endsWith(";") ? ";" : "";
444
- const cleaned = imports
445
- .split(",")
446
- .map((s) => s.trim())
447
- .filter((s) => s && s !== "useMemo" && s !== "useCallback")
448
- .join(", ");
449
- if (!cleaned) return `import React from ${quote}react${quote}${semi}`;
450
- return `import React, { ${cleaned} } from ${quote}react${quote}${semi}`;
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
- // Handle: import { useMemo, useCallback, ... } from "react"; (single or double quotes, semicolon optional)
455
- content = content.replace(
456
- /import \{([^}]*)\} from (["'])react\2[^\S\n]*;?/g,
457
- (match, imports, quote) => {
458
- const semi = match.trimEnd().endsWith(";") ? ";" : "";
459
- const cleaned = imports
460
- .split(",")
461
- .map((s) => s.trim())
462
- .filter((s) => s && s !== "useMemo" && s !== "useCallback")
463
- .join(", ");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinksharpe/react-compiler-unmemo",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
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",
@@ -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)");