acture-codemods 1.1.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/dist/index.cjs ADDED
@@ -0,0 +1,746 @@
1
+ 'use strict';
2
+
3
+ var tsMorph = require('ts-morph');
4
+
5
+ // src/codemods/wrap-handler-with-mutation.ts
6
+ var DEFAULT_EVENTS = /* @__PURE__ */ new Set(["onClick", "onChange", "onSubmit"]);
7
+ function resolveOptions(opts) {
8
+ const raw = opts?.["events"];
9
+ const events = raw ? new Set(raw.split(",").map((s) => s.trim()).filter(Boolean)) : DEFAULT_EVENTS;
10
+ return {
11
+ events,
12
+ importFrom: opts?.["import-from"] ?? "acture-migration",
13
+ importName: opts?.["import-name"] ?? "wrapMutation"
14
+ };
15
+ }
16
+ var wrapHandlerWithMutation = {
17
+ name: "wrap-handler-with-mutation",
18
+ description: "Wrap onClick/onChange/onSubmit handler expressions with wrapMutation(). Adds the import if missing.",
19
+ async run(options) {
20
+ const resolved = resolveOptions(options.options);
21
+ const project = new tsMorph.Project({
22
+ useInMemoryFileSystem: false,
23
+ skipAddingFilesFromTsConfig: true,
24
+ compilerOptions: {
25
+ allowJs: false,
26
+ jsx: 4
27
+ /* ReactJSX */
28
+ }
29
+ });
30
+ const fileChanges = [];
31
+ let totalChanged = 0;
32
+ let totalSkipped = 0;
33
+ for (const path of options.files) {
34
+ const sourceFile = project.addSourceFileAtPath(path);
35
+ const before = sourceFile.getFullText();
36
+ const notes = [];
37
+ let attrCount = 0;
38
+ sourceFile.getDescendantsOfKind(tsMorph.SyntaxKind.JsxAttribute).forEach((attr) => {
39
+ const nameNode = attr.getNameNode();
40
+ const name = nameNode.getText();
41
+ if (!resolved.events.has(name)) return;
42
+ const initializer = attr.getInitializer();
43
+ if (!initializer || initializer.getKind() !== tsMorph.SyntaxKind.JsxExpression) {
44
+ notes.push(`${path}: skipped ${name} \u2014 not a {...} expression`);
45
+ return;
46
+ }
47
+ const expr = initializer.asKindOrThrow(tsMorph.SyntaxKind.JsxExpression).getExpression();
48
+ if (!expr) {
49
+ notes.push(`${path}: skipped ${name} \u2014 empty expression`);
50
+ return;
51
+ }
52
+ if (isAlreadyWrapped(expr, resolved.importName)) return;
53
+ const inner = expr.getText();
54
+ initializer.replaceWithText(`{${resolved.importName}(${inner})}`);
55
+ attrCount++;
56
+ });
57
+ if (attrCount > 0) {
58
+ ensureImport(sourceFile, resolved.importName, resolved.importFrom);
59
+ }
60
+ const after = sourceFile.getFullText();
61
+ const changed = before !== after;
62
+ if (changed) totalChanged++;
63
+ else totalSkipped++;
64
+ fileChanges.push({
65
+ path,
66
+ before,
67
+ after,
68
+ changed,
69
+ ...notes.length > 0 ? { notes } : {}
70
+ });
71
+ if (changed && !options.dryRun) {
72
+ await sourceFile.save();
73
+ }
74
+ project.removeSourceFile(sourceFile);
75
+ }
76
+ return {
77
+ codemod: "wrap-handler-with-mutation",
78
+ version: "1.0.0",
79
+ files: fileChanges,
80
+ summary: {
81
+ total: options.files.length,
82
+ changed: totalChanged,
83
+ skipped: totalSkipped
84
+ }
85
+ };
86
+ }
87
+ };
88
+ function isAlreadyWrapped(expr, importName) {
89
+ if (expr.getKind() !== tsMorph.SyntaxKind.CallExpression) return false;
90
+ const callee = expr.asKindOrThrow(tsMorph.SyntaxKind.CallExpression).getExpression();
91
+ return callee.getText() === importName;
92
+ }
93
+ function ensureImport(sourceFile, importName, importFrom) {
94
+ const existing = sourceFile.getImportDeclaration(
95
+ (d) => d.getModuleSpecifierValue() === importFrom
96
+ );
97
+ if (existing) {
98
+ const named = existing.getNamedImports().map((n) => n.getName());
99
+ if (!named.includes(importName)) {
100
+ existing.addNamedImport(importName);
101
+ }
102
+ return;
103
+ }
104
+ sourceFile.addImportDeclaration({
105
+ moduleSpecifier: importFrom,
106
+ namedImports: [importName]
107
+ });
108
+ }
109
+ var SUPPORTED_EVENTS = /* @__PURE__ */ new Set(["onClick", "onSubmit", "onChange"]);
110
+ function resolveOptions2(opts) {
111
+ return {
112
+ idPrefix: opts?.["id-prefix"] ?? "app.wrapped",
113
+ registryImport: opts?.["registry-import"] ?? "./acture/registry",
114
+ actureImport: opts?.["acture-import"] ?? "acture"
115
+ };
116
+ }
117
+ var extractOnClickToCommand = {
118
+ name: "extract-onclick-to-command",
119
+ description: "Lift inline onClick / onSubmit / onChange arrow handlers into module-level defineCommand calls.",
120
+ async run(options) {
121
+ const resolved = resolveOptions2(options.options);
122
+ const project = new tsMorph.Project({
123
+ useInMemoryFileSystem: false,
124
+ skipAddingFilesFromTsConfig: true,
125
+ compilerOptions: {
126
+ allowJs: false,
127
+ jsx: 4
128
+ /* ReactJSX */
129
+ }
130
+ });
131
+ const fileChanges = [];
132
+ let totalChanged = 0;
133
+ let totalSkipped = 0;
134
+ for (const path of options.files) {
135
+ const sourceFile = project.addSourceFileAtPath(path);
136
+ const before = sourceFile.getFullText();
137
+ const notes = [];
138
+ const liftedCommands = [];
139
+ sourceFile.getDescendantsOfKind(tsMorph.SyntaxKind.JsxAttribute).forEach((attr) => {
140
+ const lifted = liftAttribute(attr, resolved, sourceFile, liftedCommands.length, notes);
141
+ if (lifted) liftedCommands.push(lifted);
142
+ });
143
+ if (liftedCommands.length > 0) {
144
+ ensureImports(sourceFile, resolved);
145
+ insertCommandDecls(sourceFile, liftedCommands);
146
+ }
147
+ const after = sourceFile.getFullText();
148
+ const changed = before !== after;
149
+ if (changed) totalChanged++;
150
+ else totalSkipped++;
151
+ fileChanges.push({
152
+ path,
153
+ before,
154
+ after,
155
+ changed,
156
+ ...notes.length > 0 ? { notes } : {}
157
+ });
158
+ if (changed && !options.dryRun) {
159
+ await sourceFile.save();
160
+ }
161
+ project.removeSourceFile(sourceFile);
162
+ }
163
+ return {
164
+ codemod: "extract-onclick-to-command",
165
+ version: "1.0.0",
166
+ files: fileChanges,
167
+ summary: {
168
+ total: options.files.length,
169
+ changed: totalChanged,
170
+ skipped: totalSkipped
171
+ }
172
+ };
173
+ }
174
+ };
175
+ function liftAttribute(attr, options, sourceFile, liftedIndex, notes) {
176
+ const name = attr.getNameNode().getText();
177
+ if (!SUPPORTED_EVENTS.has(name)) return null;
178
+ const initializer = attr.getInitializer();
179
+ if (!initializer || initializer.getKind() !== tsMorph.SyntaxKind.JsxExpression) return null;
180
+ const expr = initializer.asKindOrThrow(tsMorph.SyntaxKind.JsxExpression).getExpression();
181
+ if (!expr || expr.getKind() !== tsMorph.SyntaxKind.ArrowFunction) return null;
182
+ const arrow = expr.asKindOrThrow(tsMorph.SyntaxKind.ArrowFunction);
183
+ if (arrow.getParameters().length > 0) {
184
+ notes.push(`${sourceFile.getFilePath()}: skipped ${name} \u2014 handler takes parameters`);
185
+ return null;
186
+ }
187
+ const verb = deriveVerb(arrow, name);
188
+ const varName = liftedIndex === 0 ? `__cmd_${verb}` : `__cmd_${verb}_${liftedIndex}`;
189
+ const commandId = `${options.idPrefix}.${verb}`;
190
+ const title = prettify(verb);
191
+ const body = arrowBodyToExecuteBody(arrow);
192
+ const spec = `const ${varName} = defineCommand({
193
+ id: ${JSON.stringify(commandId)},
194
+ title: ${JSON.stringify(title)},
195
+ execute: () => {
196
+ ${body}
197
+ return ok(undefined);
198
+ },
199
+ });`;
200
+ initializer.replaceWithText(`{() => registry.dispatch(${varName}.id)}`);
201
+ return { varName, spec };
202
+ }
203
+ function deriveVerb(arrow, attrName) {
204
+ const text = arrow.getBody().getText().trim();
205
+ const idMatch = /^([a-zA-Z_$][\w$]*)/.exec(text);
206
+ const base = idMatch?.[1] ?? attrName.replace(/^on/, "").toLowerCase();
207
+ return camelize(base);
208
+ }
209
+ function camelize(s) {
210
+ return s.replace(/^./, (c) => c.toLowerCase()).replace(/[^a-zA-Z0-9]/g, "");
211
+ }
212
+ function prettify(verb) {
213
+ return verb.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
214
+ }
215
+ function arrowBodyToExecuteBody(arrow) {
216
+ const body = arrow.getBody();
217
+ if (body.getKind() === tsMorph.SyntaxKind.Block) {
218
+ const text = body.getText();
219
+ const inner = text.slice(1, -1).trim();
220
+ return inner.split("\n").map((line) => ` ${line}`).join("\n");
221
+ }
222
+ return ` ${body.getText()};`;
223
+ }
224
+ function ensureImports(sourceFile, options) {
225
+ addNamedImport(sourceFile, options.actureImport, "defineCommand");
226
+ addNamedImport(sourceFile, options.actureImport, "ok");
227
+ addNamedImport(sourceFile, options.registryImport, "registry");
228
+ }
229
+ function addNamedImport(sourceFile, from, name) {
230
+ const existing = sourceFile.getImportDeclaration(
231
+ (d) => d.getModuleSpecifierValue() === from
232
+ );
233
+ if (existing) {
234
+ if (!existing.getNamedImports().some((n) => n.getName() === name)) {
235
+ existing.addNamedImport(name);
236
+ }
237
+ return;
238
+ }
239
+ sourceFile.addImportDeclaration({
240
+ moduleSpecifier: from,
241
+ namedImports: [name]
242
+ });
243
+ }
244
+ function insertCommandDecls(sourceFile, commands) {
245
+ const imports = sourceFile.getImportDeclarations();
246
+ const insertAfter = imports.length > 0 ? imports[imports.length - 1].getEnd() : 0;
247
+ const text = "\n\n" + commands.map((c) => c.spec).join("\n\n") + "\n";
248
+ sourceFile.insertText(insertAfter, text);
249
+ }
250
+ function resolveOptions3(opts) {
251
+ const raw = opts?.["callees"];
252
+ const callees = raw ? new Set(raw.split(",").map((s) => s.trim()).filter(Boolean)) : /* @__PURE__ */ new Set(["dispatch"]);
253
+ const idRewrite = opts?.["id-rewrite"] === "dot" ? "dot" : "keep";
254
+ return {
255
+ callees,
256
+ registryImport: opts?.["registry-import"] ?? "./acture/registry",
257
+ idRewrite
258
+ };
259
+ }
260
+ var reduxActionToCommand = {
261
+ name: "redux-action-to-command",
262
+ description: "Convert dispatch({type, payload}) call sites to registry.dispatch(id, payload). Adds the registry import.",
263
+ async run(options) {
264
+ const resolved = resolveOptions3(options.options);
265
+ const project = new tsMorph.Project({
266
+ useInMemoryFileSystem: false,
267
+ skipAddingFilesFromTsConfig: true,
268
+ compilerOptions: {
269
+ allowJs: false,
270
+ jsx: 4
271
+ /* ReactJSX */
272
+ }
273
+ });
274
+ const fileChanges = [];
275
+ let totalChanged = 0;
276
+ let totalSkipped = 0;
277
+ for (const path of options.files) {
278
+ const sourceFile = project.addSourceFileAtPath(path);
279
+ const before = sourceFile.getFullText();
280
+ const notes = [];
281
+ let rewriteCount = 0;
282
+ sourceFile.getDescendantsOfKind(tsMorph.SyntaxKind.CallExpression).forEach((call) => {
283
+ if (rewriteOne(call, resolved, notes, path)) rewriteCount++;
284
+ });
285
+ if (rewriteCount > 0) {
286
+ ensureRegistryImport(sourceFile, resolved.registryImport);
287
+ }
288
+ const after = sourceFile.getFullText();
289
+ const changed = before !== after;
290
+ if (changed) totalChanged++;
291
+ else totalSkipped++;
292
+ fileChanges.push({
293
+ path,
294
+ before,
295
+ after,
296
+ changed,
297
+ ...notes.length > 0 ? { notes } : {}
298
+ });
299
+ if (changed && !options.dryRun) {
300
+ await sourceFile.save();
301
+ }
302
+ project.removeSourceFile(sourceFile);
303
+ }
304
+ return {
305
+ codemod: "redux-action-to-command",
306
+ version: "1.0.0",
307
+ files: fileChanges,
308
+ summary: {
309
+ total: options.files.length,
310
+ changed: totalChanged,
311
+ skipped: totalSkipped
312
+ }
313
+ };
314
+ }
315
+ };
316
+ function rewriteOne(call, options, notes, path) {
317
+ const callee = call.getExpression();
318
+ if (callee.getKind() !== tsMorph.SyntaxKind.Identifier) return false;
319
+ const calleeName = callee.getText();
320
+ if (!options.callees.has(calleeName)) return false;
321
+ const args = call.getArguments();
322
+ if (args.length !== 1) return false;
323
+ const arg = args[0];
324
+ if (arg.getKind() !== tsMorph.SyntaxKind.ObjectLiteralExpression) return false;
325
+ const obj = arg.asKindOrThrow(tsMorph.SyntaxKind.ObjectLiteralExpression);
326
+ const props = obj.getProperties();
327
+ let typeLiteral = null;
328
+ let payloadText = null;
329
+ let foreignKey = false;
330
+ for (const p of props) {
331
+ if (p.getKind() !== tsMorph.SyntaxKind.PropertyAssignment) {
332
+ foreignKey = true;
333
+ continue;
334
+ }
335
+ const pa = p.asKindOrThrow(tsMorph.SyntaxKind.PropertyAssignment);
336
+ const name = pa.getName();
337
+ if (name === "type") {
338
+ const init = pa.getInitializerOrThrow();
339
+ if (init.getKind() === tsMorph.SyntaxKind.StringLiteral) {
340
+ typeLiteral = init.asKindOrThrow(tsMorph.SyntaxKind.StringLiteral).getLiteralText();
341
+ } else {
342
+ notes.push(`${path}: skipped ${calleeName}({...}) \u2014 non-literal type`);
343
+ return false;
344
+ }
345
+ } else if (name === "payload") {
346
+ payloadText = pa.getInitializerOrThrow().getText();
347
+ } else {
348
+ foreignKey = true;
349
+ }
350
+ }
351
+ if (!typeLiteral) return false;
352
+ if (foreignKey) {
353
+ notes.push(`${path}: skipped ${calleeName}({ type: '${typeLiteral}', ...}) \u2014 has extra keys`);
354
+ return false;
355
+ }
356
+ const rewrittenId = options.idRewrite === "dot" ? rewriteIdDot(typeLiteral) : typeLiteral;
357
+ const replacement = payloadText ? `registry.dispatch(${JSON.stringify(rewrittenId)}, ${payloadText})` : `registry.dispatch(${JSON.stringify(rewrittenId)})`;
358
+ call.replaceWithText(replacement);
359
+ return true;
360
+ }
361
+ function rewriteIdDot(slashId) {
362
+ if (!slashId.includes("/")) return slashId;
363
+ return "app." + slashId.replace(/\//g, ".");
364
+ }
365
+ function ensureRegistryImport(sourceFile, from) {
366
+ const existing = sourceFile.getImportDeclaration(
367
+ (d) => d.getModuleSpecifierValue() === from
368
+ );
369
+ if (existing) {
370
+ if (!existing.getNamedImports().some((n) => n.getName() === "registry")) {
371
+ existing.addNamedImport("registry");
372
+ }
373
+ return;
374
+ }
375
+ sourceFile.addImportDeclaration({
376
+ moduleSpecifier: from,
377
+ namedImports: ["registry"]
378
+ });
379
+ }
380
+ var DEFAULT_EVENTS2 = ["onClick", "onChange", "onSubmit"];
381
+ function resolveOptions4(opts) {
382
+ const rawEvents = opts?.["events"];
383
+ const events = rawEvents ? new Set(rawEvents.split(",").map((s) => s.trim()).filter(Boolean)) : new Set(DEFAULT_EVENTS2);
384
+ const setterPattern = opts?.["setter-pattern"] ? new RegExp(opts["setter-pattern"]) : /^set[A-Z]/;
385
+ return {
386
+ idPrefix: opts?.["id-prefix"] ?? "app.state",
387
+ setterPattern,
388
+ events,
389
+ importFrom: opts?.["import-from"] ?? "acture-migration"
390
+ };
391
+ }
392
+ var useStateMutationToCommand = {
393
+ name: "usestate-mutation-to-command",
394
+ description: "Wrap inline handlers whose body is composed of useState setter calls with wrapMutation. Derives a command id from the setter name.",
395
+ async run(options) {
396
+ const resolved = resolveOptions4(options.options);
397
+ const project = new tsMorph.Project({
398
+ useInMemoryFileSystem: false,
399
+ skipAddingFilesFromTsConfig: true,
400
+ compilerOptions: {
401
+ allowJs: false,
402
+ jsx: 4
403
+ /* ReactJSX */
404
+ }
405
+ });
406
+ const fileChanges = [];
407
+ let totalChanged = 0;
408
+ let totalSkipped = 0;
409
+ for (const path of options.files) {
410
+ const sourceFile = project.addSourceFileAtPath(path);
411
+ const before = sourceFile.getFullText();
412
+ const notes = [];
413
+ let wrapCount = 0;
414
+ sourceFile.getDescendantsOfKind(tsMorph.SyntaxKind.JsxAttribute).forEach((attr) => {
415
+ if (rewriteOne2(attr, resolved, notes, path)) wrapCount++;
416
+ });
417
+ if (wrapCount > 0) {
418
+ ensureImport2(sourceFile, "wrapMutation", resolved.importFrom);
419
+ }
420
+ const after = sourceFile.getFullText();
421
+ const changed = before !== after;
422
+ if (changed) totalChanged++;
423
+ else totalSkipped++;
424
+ fileChanges.push({
425
+ path,
426
+ before,
427
+ after,
428
+ changed,
429
+ ...notes.length > 0 ? { notes } : {}
430
+ });
431
+ if (changed && !options.dryRun) {
432
+ await sourceFile.save();
433
+ }
434
+ project.removeSourceFile(sourceFile);
435
+ }
436
+ return {
437
+ codemod: "usestate-mutation-to-command",
438
+ version: "1.0.0",
439
+ files: fileChanges,
440
+ summary: {
441
+ total: options.files.length,
442
+ changed: totalChanged,
443
+ skipped: totalSkipped
444
+ }
445
+ };
446
+ }
447
+ };
448
+ function rewriteOne2(attr, options, notes, path) {
449
+ const name = attr.getNameNode().getText();
450
+ if (!options.events.has(name)) return false;
451
+ const initializer = attr.getInitializer();
452
+ if (!initializer || initializer.getKind() !== tsMorph.SyntaxKind.JsxExpression) return false;
453
+ const expr = initializer.asKindOrThrow(tsMorph.SyntaxKind.JsxExpression).getExpression();
454
+ if (!expr || expr.getKind() !== tsMorph.SyntaxKind.ArrowFunction) return false;
455
+ const arrow = expr.asKindOrThrow(tsMorph.SyntaxKind.ArrowFunction);
456
+ if (isInsideWrapMutation(arrow)) return false;
457
+ const setterName = findFirstSetter(arrow, options.setterPattern);
458
+ if (!setterName) return false;
459
+ const body = arrow.getBody();
460
+ if (!isSetterOnlyBody(body, options.setterPattern)) {
461
+ notes.push(`${path}: skipped ${name} \u2014 body has non-setter statements`);
462
+ return false;
463
+ }
464
+ const id = `${options.idPrefix}.${setterName}`;
465
+ const inner = arrow.getText();
466
+ initializer.replaceWithText(`{wrapMutation(${inner}, { id: ${JSON.stringify(id)} })}`);
467
+ return true;
468
+ }
469
+ function isInsideWrapMutation(arrow) {
470
+ const parent = arrow.getParent();
471
+ if (!parent) return false;
472
+ if (parent.getKind() !== tsMorph.SyntaxKind.CallExpression) return false;
473
+ const callee = parent.asKindOrThrow(tsMorph.SyntaxKind.CallExpression).getExpression();
474
+ return callee.getText() === "wrapMutation";
475
+ }
476
+ function findFirstSetter(arrow, pattern) {
477
+ const body = arrow.getBody();
478
+ const calls = body.getDescendantsOfKind(tsMorph.SyntaxKind.CallExpression);
479
+ if (body.getKind() === tsMorph.SyntaxKind.CallExpression) {
480
+ const call = body.asKindOrThrow(tsMorph.SyntaxKind.CallExpression);
481
+ const callee = call.getExpression();
482
+ if (callee.getKind() === tsMorph.SyntaxKind.Identifier && pattern.test(callee.getText())) {
483
+ return callee.getText();
484
+ }
485
+ }
486
+ for (const call of calls) {
487
+ const callee = call.getExpression();
488
+ if (callee.getKind() === tsMorph.SyntaxKind.Identifier && pattern.test(callee.getText())) {
489
+ return callee.getText();
490
+ }
491
+ }
492
+ return null;
493
+ }
494
+ function isSetterOnlyBody(body, pattern) {
495
+ if (body.getKind() === tsMorph.SyntaxKind.Block) {
496
+ const block = body.asKindOrThrow(tsMorph.SyntaxKind.Block);
497
+ for (const stmt of block.getStatements()) {
498
+ if (stmt.getKind() !== tsMorph.SyntaxKind.ExpressionStatement) return false;
499
+ const inner = stmt.asKindOrThrow(tsMorph.SyntaxKind.ExpressionStatement).getExpression();
500
+ if (!isSetterCall(inner, pattern)) return false;
501
+ }
502
+ return true;
503
+ }
504
+ return isSetterCall(body, pattern);
505
+ }
506
+ function isSetterCall(node, pattern) {
507
+ if (node.getKind() !== tsMorph.SyntaxKind.CallExpression) return false;
508
+ const callee = node.asKindOrThrow(tsMorph.SyntaxKind.CallExpression).getExpression();
509
+ if (callee.getKind() !== tsMorph.SyntaxKind.Identifier) return false;
510
+ return pattern.test(callee.getText());
511
+ }
512
+ function ensureImport2(sourceFile, importName, importFrom) {
513
+ const existing = sourceFile.getImportDeclaration(
514
+ (d) => d.getModuleSpecifierValue() === importFrom
515
+ );
516
+ if (existing) {
517
+ if (!existing.getNamedImports().some((n) => n.getName() === importName)) {
518
+ existing.addNamedImport(importName);
519
+ }
520
+ return;
521
+ }
522
+ sourceFile.addImportDeclaration({
523
+ moduleSpecifier: importFrom,
524
+ namedImports: [importName]
525
+ });
526
+ }
527
+ function resolveOptions5(opts) {
528
+ return {
529
+ actureImport: opts?.["acture-import"] ?? "acture",
530
+ titleFrom: opts?.["title-from"] === "id" ? "id" : "id-last-segment"
531
+ };
532
+ }
533
+ var rtkThunkToCommand = {
534
+ name: "rtk-thunk-to-command",
535
+ description: "Convert createAsyncThunk(id, payloadCreator) into defineCommand({id, title, execute}). Rewrites return X to return ok(X).",
536
+ async run(options) {
537
+ const resolved = resolveOptions5(options.options);
538
+ const project = new tsMorph.Project({
539
+ useInMemoryFileSystem: false,
540
+ skipAddingFilesFromTsConfig: true,
541
+ compilerOptions: {
542
+ allowJs: false,
543
+ jsx: 4
544
+ /* ReactJSX */
545
+ }
546
+ });
547
+ const fileChanges = [];
548
+ let totalChanged = 0;
549
+ let totalSkipped = 0;
550
+ for (const path of options.files) {
551
+ const sourceFile = project.addSourceFileAtPath(path);
552
+ const before = sourceFile.getFullText();
553
+ const notes = [];
554
+ let rewriteCount = 0;
555
+ const thunkCalls = sourceFile.getDescendantsOfKind(tsMorph.SyntaxKind.CallExpression).filter((call) => {
556
+ const callee = call.getExpression();
557
+ return callee.getKind() === tsMorph.SyntaxKind.Identifier && callee.getText() === "createAsyncThunk";
558
+ });
559
+ for (const call of thunkCalls) {
560
+ if (call.wasForgotten()) continue;
561
+ if (rewriteOne3(call, resolved, notes, path)) rewriteCount++;
562
+ }
563
+ if (rewriteCount > 0) {
564
+ ensureImport3(sourceFile, "defineCommand", resolved.actureImport);
565
+ ensureImport3(sourceFile, "ok", resolved.actureImport);
566
+ }
567
+ const after = sourceFile.getFullText();
568
+ const changed = before !== after;
569
+ if (changed) totalChanged++;
570
+ else totalSkipped++;
571
+ fileChanges.push({
572
+ path,
573
+ before,
574
+ after,
575
+ changed,
576
+ ...notes.length > 0 ? { notes } : {}
577
+ });
578
+ if (changed && !options.dryRun) {
579
+ await sourceFile.save();
580
+ }
581
+ project.removeSourceFile(sourceFile);
582
+ }
583
+ return {
584
+ codemod: "rtk-thunk-to-command",
585
+ version: "1.0.0",
586
+ files: fileChanges,
587
+ summary: {
588
+ total: options.files.length,
589
+ changed: totalChanged,
590
+ skipped: totalSkipped
591
+ }
592
+ };
593
+ }
594
+ };
595
+ function rewriteOne3(call, options, notes, path) {
596
+ const callee = call.getExpression();
597
+ if (callee.getKind() !== tsMorph.SyntaxKind.Identifier) return false;
598
+ if (callee.getText() !== "createAsyncThunk") return false;
599
+ const args = call.getArguments();
600
+ if (args.length !== 2) {
601
+ notes.push(`${path}: skipped createAsyncThunk(...) \u2014 expected exactly 2 args`);
602
+ return false;
603
+ }
604
+ const idArg = args[0];
605
+ if (idArg.getKind() !== tsMorph.SyntaxKind.StringLiteral) {
606
+ notes.push(`${path}: skipped createAsyncThunk(...) \u2014 non-literal id`);
607
+ return false;
608
+ }
609
+ const id = idArg.asKindOrThrow(tsMorph.SyntaxKind.StringLiteral).getLiteralText();
610
+ const fnArg = args[1];
611
+ const fnKind = fnArg.getKind();
612
+ if (fnKind !== tsMorph.SyntaxKind.ArrowFunction && fnKind !== tsMorph.SyntaxKind.FunctionExpression) {
613
+ notes.push(`${path}: skipped createAsyncThunk(...) \u2014 payload creator is not a function`);
614
+ return false;
615
+ }
616
+ const fn = fnArg;
617
+ rewriteReturnsToOk(fn, notes, path, id);
618
+ const executeText = fnToExecuteText(fn);
619
+ const title = options.titleFrom === "id" ? id : deriveTitle(id);
620
+ const replacement = `defineCommand({
621
+ id: ${JSON.stringify(id)},
622
+ title: ${JSON.stringify(title)},
623
+ execute: ${executeText},
624
+ })`;
625
+ call.replaceWithText(replacement);
626
+ notes.push(
627
+ `${path}: createAsyncThunk('${id}') \u2192 defineCommand. Add a params: <zod schema> if you want palette/MCP/AI to see a typed parameter.`
628
+ );
629
+ return true;
630
+ }
631
+ function rewriteReturnsToOk(fn, notes, path, id) {
632
+ const body = fn.getBody();
633
+ if (body.getKind() !== tsMorph.SyntaxKind.Block) {
634
+ const text = body.getText();
635
+ body.replaceWithText(`ok(${text})`);
636
+ return;
637
+ }
638
+ let hadAnyReturn = false;
639
+ body.getDescendantsOfKind(tsMorph.SyntaxKind.ReturnStatement).forEach((ret) => {
640
+ hadAnyReturn = true;
641
+ const expr = ret.getExpression();
642
+ if (!expr) {
643
+ ret.replaceWithText("return ok(undefined);");
644
+ } else {
645
+ ret.replaceWithText(`return ok(${expr.getText()});`);
646
+ }
647
+ });
648
+ if (!hadAnyReturn) {
649
+ notes.push(
650
+ `${path}: createAsyncThunk('${id}') had no return \u2014 appended return ok(undefined);`
651
+ );
652
+ body.replaceWithText(body.getText().replace(/\}$/, "\n return ok(undefined);\n}"));
653
+ }
654
+ }
655
+ function fnToExecuteText(fn) {
656
+ return fn.getText();
657
+ }
658
+ function deriveTitle(id) {
659
+ const last = id.split("/").pop()?.split(".").pop() ?? id;
660
+ return last.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
661
+ }
662
+ function ensureImport3(sourceFile, importName, importFrom) {
663
+ const existing = sourceFile.getImportDeclaration(
664
+ (d) => d.getModuleSpecifierValue() === importFrom
665
+ );
666
+ if (existing) {
667
+ if (!existing.getNamedImports().some((n) => n.getName() === importName)) {
668
+ existing.addNamedImport(importName);
669
+ }
670
+ return;
671
+ }
672
+ sourceFile.addImportDeclaration({
673
+ moduleSpecifier: importFrom,
674
+ namedImports: [importName]
675
+ });
676
+ }
677
+
678
+ // src/manifest.ts
679
+ var MANIFEST = [
680
+ {
681
+ name: wrapHandlerWithMutation.name,
682
+ description: wrapHandlerWithMutation.description,
683
+ status: "shipped",
684
+ since: "1.0.0",
685
+ codemod: wrapHandlerWithMutation
686
+ },
687
+ {
688
+ name: extractOnClickToCommand.name,
689
+ description: extractOnClickToCommand.description,
690
+ status: "shipped",
691
+ since: "1.0.0",
692
+ codemod: extractOnClickToCommand
693
+ },
694
+ {
695
+ name: reduxActionToCommand.name,
696
+ description: reduxActionToCommand.description,
697
+ status: "shipped",
698
+ since: "1.1.0",
699
+ codemod: reduxActionToCommand
700
+ },
701
+ {
702
+ name: useStateMutationToCommand.name,
703
+ description: useStateMutationToCommand.description,
704
+ status: "shipped",
705
+ since: "1.1.0",
706
+ codemod: useStateMutationToCommand
707
+ },
708
+ {
709
+ name: rtkThunkToCommand.name,
710
+ description: rtkThunkToCommand.description,
711
+ status: "shipped",
712
+ since: "1.1.0",
713
+ codemod: rtkThunkToCommand
714
+ }
715
+ ];
716
+ function findCodemod(name) {
717
+ const entry = MANIFEST.find((m) => m.name === name);
718
+ return entry?.codemod;
719
+ }
720
+ function listShipped() {
721
+ return MANIFEST.filter((m) => m.status === "shipped");
722
+ }
723
+
724
+ // src/runner.ts
725
+ async function runCodemod(name, options) {
726
+ const codemod = findCodemod(name);
727
+ if (!codemod) {
728
+ const known = MANIFEST.filter((m) => m.status === "shipped").map((m) => m.name).join(", ");
729
+ throw new Error(
730
+ `Unknown codemod "${name}". Available: ${known || "(none shipped yet)"}`
731
+ );
732
+ }
733
+ return await codemod.run(options);
734
+ }
735
+
736
+ exports.MANIFEST = MANIFEST;
737
+ exports.extractOnClickToCommand = extractOnClickToCommand;
738
+ exports.findCodemod = findCodemod;
739
+ exports.listShipped = listShipped;
740
+ exports.reduxActionToCommand = reduxActionToCommand;
741
+ exports.rtkThunkToCommand = rtkThunkToCommand;
742
+ exports.runCodemod = runCodemod;
743
+ exports.useStateMutationToCommand = useStateMutationToCommand;
744
+ exports.wrapHandlerWithMutation = wrapHandlerWithMutation;
745
+ //# sourceMappingURL=index.cjs.map
746
+ //# sourceMappingURL=index.cjs.map