@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 +5 -3
- package/helpers/remove-hooks.mjs +101 -28
- package/package.json +3 -2
- package/react-compiler-unmemo.mjs +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
|
|
1
2
|
# react-compiler-unmemo
|
|
2
3
|
|
|
3
4
|
[](https://opensource.org/licenses/MIT)
|
|
4
5
|
[](https://nodejs.org)
|
|
6
|
+
[](./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 | `
|
|
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
|
|
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}"
|
package/helpers/remove-hooks.mjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
378
|
-
return `import React, { ${cleaned} } from
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
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:
|