@thinksharpe/react-compiler-unmemo 0.5.0 → 0.5.4

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
@@ -1,7 +1,9 @@
1
+
1
2
  # react-compiler-unmemo
2
3
 
3
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
4
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)
5
7
 
6
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.
7
9
 
@@ -74,12 +76,12 @@ cd ./my-app && npx tsc --noEmit
74
76
  | *(no flag)* | Preview changes without writing files | **dry-run** |
75
77
  | `--write` | Apply changes to files | off |
76
78
  | `--verbose` | Log every transformation | off |
77
- | `--files <glob>` | Limit to specific file patterns | `helpers/**/*.{tsx,ts}` |
79
+ | `--files <glob>` | Limit to specific file patterns | `src/**/*.{tsx,ts}` |
78
80
  | `--skip-fix` | Skip type annotation repair step | off |
79
81
 
80
82
  ```bash
81
- # Only process specific directory
82
- node react-compiler-unmemo.mjs ./my-app --files "hooks/**/*.ts" --write
83
+ # Only process hooks directory
84
+ node react-compiler-unmemo.mjs ./my-app --files "src/hooks/**/*.ts" --write
83
85
 
84
86
  # app directory
85
87
  node react-compiler-unmemo.mjs ./my-app --files "app/**/*.{tsx,ts}"
@@ -188,6 +188,73 @@ export function isInsideComment(content, idx) {
188
188
  return false;
189
189
  }
190
190
 
191
+ /**
192
+ * Check if a position in the content is inside a string literal, template
193
+ * literal, or comment. Scans from the start of the content to correctly
194
+ * track nested context.
195
+ */
196
+ export function isInsideStringOrComment(content, idx) {
197
+ let i = 0;
198
+ while (i < idx) {
199
+ const ch = content[i];
200
+
201
+ // Line comment — skip to end of line
202
+ if (ch === "/" && content[i + 1] === "/") {
203
+ const eol = content.indexOf("\n", i + 2);
204
+ if (eol === -1) return true; // idx is in this comment (rest of file)
205
+ if (idx < eol) return true;
206
+ i = eol + 1;
207
+ continue;
208
+ }
209
+
210
+ // Block comment — skip to closing */
211
+ if (ch === "/" && content[i + 1] === "*") {
212
+ const close = content.indexOf("*/", i + 2);
213
+ if (close === -1) return true; // unclosed block comment
214
+ if (idx < close + 2) return true;
215
+ i = close + 2;
216
+ continue;
217
+ }
218
+
219
+ // Template literal
220
+ if (ch === "`") {
221
+ i++;
222
+ let templateDepth = 0;
223
+ while (i < content.length) {
224
+ if (content[i] === "\\" ) {
225
+ if (idx === i || idx === i + 1) return true;
226
+ i += 2; continue;
227
+ }
228
+ if (content[i] === "`" && templateDepth === 0) { i++; break; }
229
+ if (content[i] === "$" && content[i + 1] === "{") { templateDepth++; i += 2; continue; }
230
+ if (content[i] === "}" && templateDepth > 0) { templateDepth--; i++; continue; }
231
+ if (i === idx) return true;
232
+ i++;
233
+ }
234
+ continue;
235
+ }
236
+
237
+ // String literals
238
+ if (ch === "'" || ch === '"') {
239
+ const quote = ch;
240
+ i++;
241
+ while (i < content.length) {
242
+ if (content[i] === "\\" ) {
243
+ if (idx === i || idx === i + 1) return true;
244
+ i += 2; continue;
245
+ }
246
+ if (content[i] === quote) { i++; break; }
247
+ if (i === idx) return true;
248
+ i++;
249
+ }
250
+ continue;
251
+ }
252
+
253
+ i++;
254
+ }
255
+ return false;
256
+ }
257
+
191
258
  // ─── Main Processing ─────────────────────────────────────────────────────────
192
259
 
193
260
  export function processFile(filePath, { dryRun = false } = {}) {
@@ -218,7 +285,7 @@ export function processFile(filePath, { dryRun = false } = {}) {
218
285
  }
219
286
  }
220
287
 
221
- if (isInsideComment(content, hookIdx)) {
288
+ if (isInsideStringOrComment(content, hookIdx)) {
222
289
  searchFrom = hookIdx + 1;
223
290
  continue;
224
291
  }
@@ -267,27 +334,31 @@ export function processFile(filePath, { dryRun = false } = {}) {
267
334
  let found = false;
268
335
 
269
336
  for (const pattern of patterns) {
270
- const idx = content.indexOf(pattern);
271
- if (idx === -1) continue;
272
-
273
- // Skip if inside a comment
274
- if (isInsideComment(content, idx)) {
275
- // Replace the pattern temporarily to avoid infinite loop
276
- // We'll restore comments at the end... actually just skip
277
- // by searching for the next occurrence
278
- const nextIdx = content.indexOf(pattern, idx + pattern.length);
279
- if (nextIdx === -1) continue;
280
- }
281
-
282
- // Make sure this isn't part of a larger identifier (e.g. "myUseMemo(")
283
- if (idx > 0 && !pattern.startsWith("React.")) {
284
- const prevChar = content[idx - 1];
285
- if (/[a-zA-Z0-9_$]/.test(prevChar)) {
337
+ // Search forward, skipping occurrences inside strings/comments
338
+ let searchFrom = 0;
339
+ let idx = -1;
340
+ while (true) {
341
+ const candidate = content.indexOf(pattern, searchFrom);
342
+ if (candidate === -1) break;
343
+
344
+ if (isInsideStringOrComment(content, candidate)) {
345
+ searchFrom = candidate + pattern.length;
286
346
  continue;
287
347
  }
288
- }
289
348
 
290
- if (isInsideComment(content, idx)) continue;
349
+ // Make sure this isn't part of a larger identifier (e.g. "myUseMemo(")
350
+ if (candidate > 0 && !pattern.startsWith("React.")) {
351
+ const prevChar = content[candidate - 1];
352
+ if (/[a-zA-Z0-9_$]/.test(prevChar)) {
353
+ searchFrom = candidate + pattern.length;
354
+ continue;
355
+ }
356
+ }
357
+
358
+ idx = candidate;
359
+ break;
360
+ }
361
+ if (idx === -1) continue;
291
362
 
292
363
  const openParenIdx = idx + pattern.length - 1;
293
364
  const closeParenIdx = findClosingParen(content, openParenIdx);
@@ -365,31 +436,33 @@ export function processFile(filePath, { dryRun = false } = {}) {
365
436
 
366
437
  // Phase 3: Clean up imports
367
438
  if (changed) {
368
- // Handle: import React, { useMemo, useCallback, ... } from "react";
439
+ // Handle: import React, { useMemo, useCallback, ... } from "react"; (single or double quotes, semicolon optional)
369
440
  content = content.replace(
370
- /import React, \{([^}]*)\} from "react";/g,
371
- (match, imports) => {
441
+ /import React, \{([^}]*)\} from (["'])react\2[^\S\n]*;?/g,
442
+ (match, imports, quote) => {
443
+ const semi = match.trimEnd().endsWith(";") ? ";" : "";
372
444
  const cleaned = imports
373
445
  .split(",")
374
446
  .map((s) => s.trim())
375
447
  .filter((s) => s && s !== "useMemo" && s !== "useCallback")
376
448
  .join(", ");
377
- if (!cleaned) return 'import React from "react";';
378
- return `import React, { ${cleaned} } from "react";`;
449
+ if (!cleaned) return `import React from ${quote}react${quote}${semi}`;
450
+ return `import React, { ${cleaned} } from ${quote}react${quote}${semi}`;
379
451
  },
380
452
  );
381
453
 
382
- // Handle: import { useMemo, useCallback, ... } from "react";
454
+ // Handle: import { useMemo, useCallback, ... } from "react"; (single or double quotes, semicolon optional)
383
455
  content = content.replace(
384
- /import \{([^}]*)\} from "react";/g,
385
- (match, imports) => {
456
+ /import \{([^}]*)\} from (["'])react\2[^\S\n]*;?/g,
457
+ (match, imports, quote) => {
458
+ const semi = match.trimEnd().endsWith(";") ? ";" : "";
386
459
  const cleaned = imports
387
460
  .split(",")
388
461
  .map((s) => s.trim())
389
462
  .filter((s) => s && s !== "useMemo" && s !== "useCallback")
390
463
  .join(", ");
391
464
  if (!cleaned) return ""; // Remove empty import entirely
392
- return `import { ${cleaned} } from "react";`;
465
+ return `import { ${cleaned} } from ${quote}react${quote}${semi}`;
393
466
  },
394
467
  );
395
468
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinksharpe/react-compiler-unmemo",
3
- "version": "0.5.0",
3
+ "version": "0.5.4",
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",
@@ -39,7 +39,8 @@
39
39
  "LICENSE"
40
40
  ],
41
41
  "scripts": {
42
- "start": "node react-compiler-unmemo.mjs"
42
+ "start": "node react-compiler-unmemo.mjs",
43
+ "test": "node --test tests/**/*.test.mjs"
43
44
  },
44
45
  "engines": {
45
46
  "node": ">=18"
@@ -13,7 +13,7 @@
13
13
  * Options:
14
14
  * --write Apply changes to files (default is dry-run / preview)
15
15
  * --verbose Show detailed output for each transformation
16
- * --files <glob> File glob pattern (default: helpers/**\/*.{tsx,ts})
16
+ * --files <glob> File glob pattern (default: src/**\/*.{tsx,ts})
17
17
  * --skip-fix Skip the type annotation fix step
18
18
  *
19
19
  * Examples: