@thinksharpe/react-compiler-unmemo 0.5.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.
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * fix-type-annotations.mjs
5
+ *
6
+ * Post-migration helper that attempts to restore type annotations
7
+ * that were lost when useMemo<Type>() generics were stripped.
8
+ *
9
+ * Scans for common patterns (untyped `columns` arrays, `formFields` arrays)
10
+ * and infers the correct type annotation from surrounding code context.
11
+ *
12
+ * Can be used standalone or imported by run.mjs.
13
+ *
14
+ * Standalone usage:
15
+ * node fix-type-annotations.mjs --dir <path> [--dry-run]
16
+ */
17
+
18
+ import fs from "fs";
19
+ import { glob } from "glob";
20
+ import path from "path";
21
+
22
+ // ─── Fix Patterns ────────────────────────────────────────────────────────────
23
+
24
+ // Each pattern describes a variable that may have lost its type annotation
25
+ // when useMemo<Type>() was stripped. The script tries to infer the correct
26
+ // type from surrounding code context.
27
+ const fixPatterns = [
28
+ {
29
+ // const columns = [ -> const columns: ColumnsType<T> = [
30
+ description: "ColumnsType on columns arrays",
31
+ filePattern: /\.(tsx|ts)$/,
32
+ search: /^(\s*const columns)\s*=\s*\[/m,
33
+ alreadyTyped: /const columns\s*:\s*ColumnsType/,
34
+ getType: (content) => {
35
+ // Infer T from SorterResult<T>, Table<T>, or ColumnType<T> usage
36
+ const sorterMatch = content.match(/SorterResult<(\w+)>/);
37
+ if (sorterMatch) return `ColumnsType<${sorterMatch[1]}>`;
38
+ const tableMatch = content.match(/Table<(\w+)>/);
39
+ if (tableMatch) return `ColumnsType<${tableMatch[1]}>`;
40
+ const colMatch = content.match(/ColumnType<(\w+)>/);
41
+ if (colMatch) return `ColumnsType<${colMatch[1]}>`;
42
+ return null;
43
+ },
44
+ replace: (groups, type) => `${groups[1]}: ${type} = [`,
45
+ },
46
+ {
47
+ // const formFields = [ -> const formFields: FormFieldProps[] = [
48
+ description: "FormFieldProps[] on formFields arrays",
49
+ filePattern: /\.(tsx|ts)$/,
50
+ search: /^(\s*const formFields)\s*=\s*\[/m,
51
+ alreadyTyped: /const formFields\s*:\s*FormFieldProps/,
52
+ getType: () => "FormFieldProps[]",
53
+ replace: (groups, type) => `${groups[1]}: ${type} = [`,
54
+ },
55
+ ];
56
+
57
+ // ─── Processing ──────────────────────────────────────────────────────────────
58
+
59
+ function processFile(filePath, targetDir, { dryRun = false } = {}) {
60
+ let content = fs.readFileSync(filePath, "utf8");
61
+ let changed = false;
62
+ const fixes = [];
63
+
64
+ for (const pattern of fixPatterns) {
65
+ if (!pattern.filePattern.test(filePath)) continue;
66
+ if (pattern.alreadyTyped.test(content)) continue;
67
+
68
+ const match = content.match(pattern.search);
69
+ if (!match) continue;
70
+
71
+ const type = pattern.getType(content);
72
+ if (!type) continue;
73
+
74
+ content = content.replace(pattern.search, (fullMatch, ...groups) => {
75
+ return pattern.replace([fullMatch, ...groups], type);
76
+ });
77
+ changed = true;
78
+ fixes.push({ type, description: pattern.description });
79
+ console.log(` ✓ ${path.relative(targetDir, filePath)} — added ${type}`);
80
+ }
81
+
82
+ if (changed && !dryRun) {
83
+ fs.writeFileSync(filePath, content);
84
+ }
85
+
86
+ return { changed, fixes };
87
+ }
88
+
89
+ // ─── Run ─────────────────────────────────────────────────────────────────────
90
+
91
+ export function run(targetDir, { fileGlob = "src/**/*.{tsx,ts}", dryRun = false } = {}) {
92
+ console.log("╔══════════════════════════════════════════════════╗");
93
+ console.log("║ Fix Type Annotations After Hook Removal ║");
94
+ console.log("╚══════════════════════════════════════════════════╝");
95
+ console.log();
96
+
97
+ if (!fs.existsSync(targetDir)) {
98
+ console.error(`Error: Target directory not found: ${targetDir}`);
99
+ return { fixCount: 0, errors: [] };
100
+ }
101
+
102
+ const files = glob.sync(fileGlob, {
103
+ cwd: targetDir,
104
+ absolute: true,
105
+ });
106
+
107
+ let fixCount = 0;
108
+ const errors = [];
109
+
110
+ for (const file of files) {
111
+ try {
112
+ const { fixes } = processFile(file, targetDir, { dryRun });
113
+ fixCount += fixes.length;
114
+ } catch (err) {
115
+ errors.push({ file: path.relative(targetDir, file), error: err.message });
116
+ console.error(` ✗ ${path.relative(targetDir, file)}: ${err.message}`);
117
+ }
118
+ }
119
+
120
+ console.log();
121
+ console.log(`Fixed ${fixCount} type annotations${dryRun ? " (dry run)" : ""}.`);
122
+
123
+ return { fixCount, errors };
124
+ }
125
+
126
+ // ─── Standalone CLI ──────────────────────────────────────────────────────────
127
+
128
+ const isMain = process.argv[1] && (process.argv[1].endsWith("fix-type-annotations.mjs") || process.argv[1].endsWith("fix-type-annotations"));
129
+
130
+ if (isMain) {
131
+ const args = process.argv.slice(2);
132
+ const write = args.includes("--write");
133
+ const dryRun = !write;
134
+
135
+ const dirIdx = args.indexOf("--dir");
136
+ const targetDir = dirIdx !== -1 && args[dirIdx + 1] ? path.resolve(args[dirIdx + 1]) : null;
137
+
138
+ const filesIdx = args.indexOf("--files");
139
+ const fileGlob = filesIdx !== -1 && args[filesIdx + 1] ? args[filesIdx + 1] : undefined;
140
+
141
+ if (!targetDir) {
142
+ console.error("Usage: node fix-type-annotations.mjs --dir <path> [--write] [--files <glob>]");
143
+ process.exit(1);
144
+ }
145
+
146
+ const result = run(targetDir, { fileGlob, dryRun });
147
+ if (dryRun) {
148
+ console.log("Dry run complete. Run with --write to apply changes.");
149
+ } else {
150
+ console.log("Run your build tool to check for remaining type errors.");
151
+ }
152
+ process.exit(result.errors.length > 0 ? 1 : 0);
153
+ }
@@ -0,0 +1,522 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * remove-hooks.mjs
5
+ *
6
+ * Removes useMemo and useCallback hooks from React/TypeScript files
7
+ * to leverage React Compiler automatic optimization.
8
+ *
9
+ * Can be used standalone or imported by run.mjs.
10
+ *
11
+ * Standalone usage:
12
+ * node remove-hooks.mjs --dir <path> [options]
13
+ *
14
+ * Options:
15
+ * --dir <path> Target directory (required when run standalone)
16
+ * --files <glob> File glob pattern (default: src/**\/*.{tsx,ts})
17
+ * --dry-run Show what would change without writing files
18
+ * --verbose Show detailed output for each transformation
19
+ */
20
+
21
+ import fs from "fs";
22
+ import { glob } from "glob";
23
+ import path from "path";
24
+
25
+ // ─── Core Parsing Helpers ────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Find the matching closing paren for an opening paren at startIdx.
29
+ * Counts nested parens/brackets/braces and respects strings and template literals.
30
+ */
31
+ export function findClosingParen(content, startIdx) {
32
+ let depth = 0;
33
+ let inString = false;
34
+ let stringChar = "";
35
+ let inTemplate = false;
36
+ let templateDepth = 0;
37
+
38
+ for (let i = startIdx; i < content.length; i++) {
39
+ const ch = content[i];
40
+ const prev = i > 0 ? content[i - 1] : "";
41
+
42
+ // Handle escape sequences
43
+ if ((inString || inTemplate) && ch === "\\" && prev !== "\\") {
44
+ i++; // skip next char
45
+ continue;
46
+ }
47
+
48
+ // Handle string boundaries
49
+ if (!inTemplate && (ch === '"' || ch === "'" || ch === "`")) {
50
+ if (ch === "`") {
51
+ if (!inTemplate && !inString) {
52
+ inTemplate = true;
53
+ templateDepth = 0;
54
+ continue;
55
+ }
56
+ } else if (!inTemplate) {
57
+ if (!inString) {
58
+ inString = true;
59
+ stringChar = ch;
60
+ continue;
61
+ } else if (ch === stringChar) {
62
+ inString = false;
63
+ continue;
64
+ }
65
+ }
66
+ }
67
+
68
+ // Handle template literal end
69
+ if (inTemplate && ch === "`" && templateDepth === 0) {
70
+ inTemplate = false;
71
+ continue;
72
+ }
73
+
74
+ // Handle template literal expressions ${...}
75
+ if (inTemplate && ch === "$" && content[i + 1] === "{") {
76
+ templateDepth++;
77
+ i++;
78
+ continue;
79
+ }
80
+ if (inTemplate && templateDepth > 0 && ch === "}") {
81
+ templateDepth--;
82
+ continue;
83
+ }
84
+
85
+ if (inString || inTemplate) continue;
86
+
87
+ if (ch === "(") depth++;
88
+ if (ch === ")") {
89
+ depth--;
90
+ if (depth === 0) return i;
91
+ }
92
+ }
93
+ return -1;
94
+ }
95
+
96
+ /**
97
+ * Strip the trailing dependency array from the inner content of useMemo/useCallback.
98
+ * e.g. "() => foo, [a, b]" -> "() => foo"
99
+ * Handles nested brackets/parens at depth 0.
100
+ */
101
+ export function stripDepsArray(inner) {
102
+ let depth = 0;
103
+ let lastCommaIdx = -1;
104
+ let inString = false;
105
+ let stringChar = "";
106
+
107
+ for (let i = 0; i < inner.length; i++) {
108
+ const ch = inner[i];
109
+
110
+ // Simple string tracking
111
+ if ((ch === '"' || ch === "'" || ch === "`") && (i === 0 || inner[i - 1] !== "\\")) {
112
+ if (!inString) {
113
+ inString = true;
114
+ stringChar = ch;
115
+ continue;
116
+ } else if (ch === stringChar) {
117
+ inString = false;
118
+ continue;
119
+ }
120
+ }
121
+ if (inString) continue;
122
+
123
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
124
+ if (ch === ")" || ch === "]" || ch === "}") depth--;
125
+ if (ch === "," && depth === 0) {
126
+ // Check if what follows (after whitespace/newlines) is "["
127
+ const rest = inner.slice(i + 1).trimStart();
128
+ if (rest.startsWith("[")) {
129
+ lastCommaIdx = i;
130
+ }
131
+ }
132
+ }
133
+ if (lastCommaIdx === -1) return inner;
134
+ return inner.slice(0, lastCommaIdx);
135
+ }
136
+
137
+ /**
138
+ * Find the arrow "=>" at depth 0 in the inner content.
139
+ * Returns { params, body } or null.
140
+ */
141
+ export function unwrapArrowFn(inner) {
142
+ let depth = 0;
143
+ let inString = false;
144
+ let stringChar = "";
145
+
146
+ for (let i = 0; i < inner.length - 1; i++) {
147
+ const ch = inner[i];
148
+
149
+ if ((ch === '"' || ch === "'" || ch === "`") && (i === 0 || inner[i - 1] !== "\\")) {
150
+ if (!inString) {
151
+ inString = true;
152
+ stringChar = ch;
153
+ continue;
154
+ } else if (ch === stringChar) {
155
+ inString = false;
156
+ continue;
157
+ }
158
+ }
159
+ if (inString) continue;
160
+
161
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
162
+ if (ch === ")" || ch === "]" || ch === "}") depth--;
163
+ if (ch === "=" && inner[i + 1] === ">" && depth === 0) {
164
+ const params = inner.slice(0, i).trim();
165
+ const body = inner.slice(i + 2).trim();
166
+ return { params, body };
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Check if a position in the content is inside a comment (line or block).
174
+ */
175
+ export function isInsideComment(content, idx) {
176
+ // Check for line comment: find the start of the line
177
+ const lineStart = content.lastIndexOf("\n", idx) + 1;
178
+ const linePrefix = content.slice(lineStart, idx);
179
+ if (linePrefix.includes("//")) return true;
180
+
181
+ // Check for block comment: find the last /* before idx
182
+ let lastBlockOpen = content.lastIndexOf("/*", idx);
183
+ if (lastBlockOpen !== -1) {
184
+ let lastBlockClose = content.lastIndexOf("*/", idx);
185
+ if (lastBlockClose < lastBlockOpen) return true;
186
+ }
187
+
188
+ return false;
189
+ }
190
+
191
+ // ─── Main Processing ─────────────────────────────────────────────────────────
192
+
193
+ export function processFile(filePath, { dryRun = false } = {}) {
194
+ let content = fs.readFileSync(filePath, "utf8");
195
+ const original = content;
196
+ let changed = false;
197
+ const transformations = [];
198
+
199
+ // Phase 1: Strip generic type params like useMemo<ColumnsType<Foo>>( -> useMemo(
200
+ const hookNames = [
201
+ "React.useMemo",
202
+ "React.useCallback",
203
+ "useMemo",
204
+ "useCallback",
205
+ ];
206
+ for (const hookName of hookNames) {
207
+ let searchFrom = 0;
208
+ while (true) {
209
+ const hookIdx = content.indexOf(hookName + "<", searchFrom);
210
+ if (hookIdx === -1) break;
211
+
212
+ // Make sure this isn't part of a larger identifier
213
+ if (hookIdx > 0) {
214
+ const prevChar = content[hookIdx - 1];
215
+ if (/[a-zA-Z0-9_$.]/.test(prevChar) && !hookName.startsWith("React.")) {
216
+ searchFrom = hookIdx + 1;
217
+ continue;
218
+ }
219
+ }
220
+
221
+ if (isInsideComment(content, hookIdx)) {
222
+ searchFrom = hookIdx + 1;
223
+ continue;
224
+ }
225
+
226
+ const angleStart = hookIdx + hookName.length;
227
+ // Count nested angle brackets to find the matching '>'
228
+ let depth = 0;
229
+ let angleEnd = -1;
230
+ for (let i = angleStart; i < content.length; i++) {
231
+ if (content[i] === "<") depth++;
232
+ if (content[i] === ">") {
233
+ depth--;
234
+ if (depth === 0) {
235
+ angleEnd = i;
236
+ break;
237
+ }
238
+ }
239
+ }
240
+ if (angleEnd === -1 || content[angleEnd + 1] !== "(") {
241
+ searchFrom = hookIdx + 1;
242
+ continue;
243
+ }
244
+
245
+ const genericType = content.slice(angleStart + 1, angleEnd);
246
+ content =
247
+ content.slice(0, angleStart) + content.slice(angleEnd + 1);
248
+ changed = true;
249
+ transformations.push({
250
+ type: "strip-generic",
251
+ hook: hookName,
252
+ genericType,
253
+ });
254
+ }
255
+ }
256
+
257
+ // Phase 2: Replace useMemo(...) and useCallback(...) calls
258
+ const patterns = [
259
+ "React.useMemo(",
260
+ "React.useCallback(",
261
+ "useMemo(",
262
+ "useCallback(",
263
+ ];
264
+
265
+ let safety = 0;
266
+ while (safety++ < 200) {
267
+ let found = false;
268
+
269
+ 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)) {
286
+ continue;
287
+ }
288
+ }
289
+
290
+ if (isInsideComment(content, idx)) continue;
291
+
292
+ const openParenIdx = idx + pattern.length - 1;
293
+ const closeParenIdx = findClosingParen(content, openParenIdx);
294
+ if (closeParenIdx === -1) continue;
295
+
296
+ const inner = content.slice(openParenIdx + 1, closeParenIdx);
297
+ const withoutDeps = stripDepsArray(inner).trim();
298
+
299
+ const isMemo = pattern.includes("useMemo");
300
+ const isCallback = pattern.includes("useCallback");
301
+
302
+ const arrow = unwrapArrowFn(withoutDeps);
303
+
304
+ let replacement;
305
+ if (isMemo && arrow) {
306
+ const body = arrow.body;
307
+ if (body.startsWith("{")) {
308
+ // Multi-statement body: check for simple { return X; } pattern
309
+ const trimmed = body.slice(1, -1).trim();
310
+ const returnMatch = trimmed.match(/^return\s+([\s\S]+?);?\s*$/);
311
+ if (returnMatch) {
312
+ replacement = returnMatch[1].trim();
313
+ if (replacement.endsWith(";"))
314
+ replacement = replacement.slice(0, -1);
315
+ // Wrap in parens if it starts with ( to preserve grouping
316
+ // or if it's a multi-line expression
317
+ if (
318
+ replacement.includes("\n") &&
319
+ !replacement.startsWith("(") &&
320
+ !replacement.startsWith("[") &&
321
+ !replacement.startsWith("{")
322
+ ) {
323
+ replacement = `(\n${replacement}\n)`;
324
+ }
325
+ } else {
326
+ // Complex body - wrap in IIFE
327
+ replacement = `(() => ${body})()`;
328
+ }
329
+ } else {
330
+ // Simple expression body
331
+ replacement = body;
332
+ }
333
+ } else if (isCallback && arrow) {
334
+ replacement = `${arrow.params} => ${arrow.body}`;
335
+ } else {
336
+ // Fallback: just remove the wrapper
337
+ replacement = withoutDeps;
338
+ }
339
+
340
+ const beforeMatch = content.slice(
341
+ Math.max(0, idx - 50),
342
+ idx,
343
+ );
344
+ const lineNum =
345
+ content.slice(0, idx).split("\n").length;
346
+
347
+ content =
348
+ content.slice(0, idx) +
349
+ replacement +
350
+ content.slice(closeParenIdx + 1);
351
+ changed = true;
352
+ found = true;
353
+
354
+ transformations.push({
355
+ type: isMemo ? "useMemo" : "useCallback",
356
+ line: lineNum,
357
+ pattern,
358
+ });
359
+
360
+ break; // restart since indices changed
361
+ }
362
+
363
+ if (!found) break;
364
+ }
365
+
366
+ // Phase 3: Clean up imports
367
+ if (changed) {
368
+ // Handle: import React, { useMemo, useCallback, ... } from "react";
369
+ content = content.replace(
370
+ /import React, \{([^}]*)\} from "react";/g,
371
+ (match, imports) => {
372
+ const cleaned = imports
373
+ .split(",")
374
+ .map((s) => s.trim())
375
+ .filter((s) => s && s !== "useMemo" && s !== "useCallback")
376
+ .join(", ");
377
+ if (!cleaned) return 'import React from "react";';
378
+ return `import React, { ${cleaned} } from "react";`;
379
+ },
380
+ );
381
+
382
+ // Handle: import { useMemo, useCallback, ... } from "react";
383
+ content = content.replace(
384
+ /import \{([^}]*)\} from "react";/g,
385
+ (match, imports) => {
386
+ const cleaned = imports
387
+ .split(",")
388
+ .map((s) => s.trim())
389
+ .filter((s) => s && s !== "useMemo" && s !== "useCallback")
390
+ .join(", ");
391
+ if (!cleaned) return ""; // Remove empty import entirely
392
+ return `import { ${cleaned} } from "react";`;
393
+ },
394
+ );
395
+
396
+ // Clean up any resulting blank lines from removed imports
397
+ content = content.replace(/\n\n\n+/g, "\n\n");
398
+ }
399
+
400
+ if (changed && !dryRun) {
401
+ fs.writeFileSync(filePath, content);
402
+ }
403
+
404
+ return { changed, transformations, original, result: content };
405
+ }
406
+
407
+ // ─── Run ─────────────────────────────────────────────────────────────────────
408
+
409
+ // Run the hook removal on a directory.
410
+ // targetDir: absolute path to the project root
411
+ // options: { fileGlob, dryRun, verbose }
412
+ export function run(targetDir, { fileGlob = "src/**/*.{tsx,ts}", dryRun = false, verbose = false } = {}) {
413
+ console.log("╔══════════════════════════════════════════════════╗");
414
+ console.log("║ React Compiler - Remove useMemo/useCallback ║");
415
+ console.log("╚══════════════════════════════════════════════════╝");
416
+ console.log();
417
+
418
+ if (!fs.existsSync(targetDir)) {
419
+ console.error(`Error: Target directory not found: ${targetDir}`);
420
+ return { changedCount: 0, totalTransformations: 0, errors: [{ file: targetDir, error: "not found" }], remaining: [] };
421
+ }
422
+
423
+ const files = glob.sync(fileGlob, {
424
+ cwd: targetDir,
425
+ absolute: true,
426
+ });
427
+
428
+ console.log(`Target: ${targetDir}`);
429
+ console.log(`Pattern: ${fileGlob}`);
430
+ console.log(`Files: ${files.length}`);
431
+ console.log(`Mode: ${dryRun ? "DRY RUN" : "LIVE"}`);
432
+ console.log();
433
+
434
+ let changedCount = 0;
435
+ let totalTransformations = 0;
436
+ const errors = [];
437
+
438
+ for (const file of files) {
439
+ try {
440
+ const { changed, transformations } = processFile(file, { dryRun });
441
+ if (changed) {
442
+ changedCount++;
443
+ totalTransformations += transformations.length;
444
+ const rel = path.relative(targetDir, file);
445
+ console.log(` ✓ ${rel} (${transformations.length} changes)`);
446
+ if (verbose) {
447
+ for (const t of transformations) {
448
+ if (t.type === "strip-generic") {
449
+ console.log(` ↳ stripped generic <${t.genericType}> from ${t.hook}`);
450
+ } else {
451
+ console.log(` ↳ removed ${t.type} at line ${t.line}`);
452
+ }
453
+ }
454
+ }
455
+ }
456
+ } catch (err) {
457
+ const rel = path.relative(targetDir, file);
458
+ errors.push({ file: rel, error: err.message });
459
+ console.error(` ✗ ${rel}: ${err.message}`);
460
+ }
461
+ }
462
+
463
+ console.log();
464
+ console.log(`Modified: ${changedCount} files`);
465
+ console.log(`Changes: ${totalTransformations} transformations`);
466
+ if (dryRun) console.log(`(dry run — no files were written)`);
467
+
468
+ if (errors.length > 0) {
469
+ console.log();
470
+ console.log(`Errors: ${errors.length}`);
471
+ for (const e of errors) {
472
+ console.log(` - ${e.file}: ${e.error}`);
473
+ }
474
+ }
475
+
476
+ // Report remaining
477
+ const remaining = [];
478
+ for (const file of files) {
479
+ const content = fs.readFileSync(file, "utf8");
480
+ if (/\buseMemo\b/.test(content) || /\buseCallback\b/.test(content)) {
481
+ remaining.push(path.relative(targetDir, file));
482
+ }
483
+ }
484
+ if (remaining.length > 0) {
485
+ console.log();
486
+ console.log(`⚠ ${remaining.length} files still reference useMemo/useCallback:`);
487
+ remaining.forEach((f) => console.log(` - ${f}`));
488
+ }
489
+
490
+ return { changedCount, totalTransformations, errors, remaining };
491
+ }
492
+
493
+ // ─── Standalone CLI ──────────────────────────────────────────────────────────
494
+
495
+ const isMain = process.argv[1] && (process.argv[1].endsWith("remove-hooks.mjs") || process.argv[1].endsWith("remove-hooks"));
496
+
497
+ if (isMain) {
498
+ const args = process.argv.slice(2);
499
+ const write = args.includes("--write");
500
+ const dryRun = !write;
501
+ const verbose = args.includes("--verbose");
502
+
503
+ const dirIdx = args.indexOf("--dir");
504
+ const targetDir = dirIdx !== -1 && args[dirIdx + 1] ? path.resolve(args[dirIdx + 1]) : null;
505
+
506
+ const filesIdx = args.indexOf("--files");
507
+ const fileGlob = filesIdx !== -1 && args[filesIdx + 1] ? args[filesIdx + 1] : undefined;
508
+
509
+ if (!targetDir) {
510
+ console.error("Usage: node remove-hooks.mjs --dir <path> [--write] [--files <glob>] [--verbose]");
511
+ process.exit(1);
512
+ }
513
+
514
+ const result = run(targetDir, { fileGlob, dryRun, verbose });
515
+ console.log();
516
+ if (dryRun) {
517
+ console.log("Dry run complete. Run with --write to apply changes.");
518
+ } else {
519
+ console.log("Done. Run your build tool to verify no type errors were introduced.");
520
+ }
521
+ process.exit(result.errors.length > 0 ? 1 : 0);
522
+ }