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.
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Common types shared across codemods and the CLI runner.
3
+ *
4
+ * Per research-4 §B.6, the contract every codemod must honour is:
5
+ * - It can run in `--dry-run` mode and produce a diff WITHOUT writing.
6
+ * - It can produce machine-readable output (`--json`).
7
+ * - It is conservative: when in doubt, skip the file rather than emit
8
+ * a partial / dangerous transform. The agent that drives the codemod
9
+ * will re-attempt the file manually.
10
+ */
11
+ interface CodemodOptions {
12
+ /** Files to operate on. Each entry is an absolute path to a .ts/.tsx
13
+ * file. The CLI is responsible for expanding globs into this list. */
14
+ readonly files: readonly string[];
15
+ /** Don't write files; return what the diff WOULD be. */
16
+ readonly dryRun?: boolean;
17
+ /** Per-codemod options (free-form bag of strings). Each codemod
18
+ * documents which keys it reads. */
19
+ readonly options?: Record<string, string | undefined>;
20
+ }
21
+ interface FileChange {
22
+ readonly path: string;
23
+ /** `none` if the file was unchanged; otherwise the new content. */
24
+ readonly before: string;
25
+ readonly after: string;
26
+ /** `true` if the codemod made any change to the file's text. */
27
+ readonly changed: boolean;
28
+ /** Non-fatal observations (e.g. "skipped: nested JSX expression
29
+ * too complex"). Hosts surface these to the user. */
30
+ readonly notes?: readonly string[];
31
+ }
32
+ interface CodemodResult {
33
+ readonly codemod: string;
34
+ readonly version: string;
35
+ readonly files: readonly FileChange[];
36
+ /** Summary counts. The CLI uses these to print the recap. */
37
+ readonly summary: {
38
+ readonly total: number;
39
+ readonly changed: number;
40
+ readonly skipped: number;
41
+ };
42
+ }
43
+ interface Codemod {
44
+ /** Stable id used in the manifest, e.g. `wrap-handler-with-mutation`. */
45
+ readonly name: string;
46
+ /** Free-text. Surfaced in `--help` and `--list`. */
47
+ readonly description: string;
48
+ /** Runs the codemod against `options.files`. Pure function from
49
+ * options to result — does NOT write files unless `dryRun` is false. */
50
+ run(options: CodemodOptions): Promise<CodemodResult> | CodemodResult;
51
+ }
52
+
53
+ /**
54
+ * Codemod registry, Nx-style.
55
+ *
56
+ * Each entry pairs a codemod name with the version of acture at which
57
+ * it was first published. The CLI uses this to:
58
+ * - `--list` the catalog,
59
+ * - look up a codemod by name,
60
+ * - emit a JSON manifest for tooling (`acture-codemods --manifest`).
61
+ *
62
+ * Per research-4 §B.5: the v1.2 scope is two of the five planned
63
+ * codemods. The other three (`redux-action-to-command`,
64
+ * `usestate-mutation-to-command`, `rtk-thunk-to-command`) are tracked in
65
+ * the manifest as `status: 'planned'` so users see what's coming.
66
+ */
67
+
68
+ interface ManifestEntry {
69
+ readonly name: string;
70
+ readonly description: string;
71
+ readonly status: 'shipped' | 'planned';
72
+ readonly since?: string;
73
+ readonly codemod?: Codemod;
74
+ }
75
+ declare const MANIFEST: readonly ManifestEntry[];
76
+ declare function findCodemod(name: string): Codemod | undefined;
77
+ declare function listShipped(): readonly ManifestEntry[];
78
+
79
+ /**
80
+ * Programmatic runner used by both the CLI and library consumers.
81
+ *
82
+ * Looks up a codemod in the manifest, validates the options, and invokes
83
+ * the codemod's `run`. Returns the same `CodemodResult` shape the CLI
84
+ * emits as JSON.
85
+ */
86
+
87
+ declare function runCodemod(name: string, options: CodemodOptions): Promise<CodemodResult>;
88
+
89
+ /**
90
+ * `wrap-handler-with-mutation`
91
+ *
92
+ * Find every `onClick`, `onChange`, `onSubmit` JSX attribute whose value
93
+ * is an expression and wrap it with `wrapMutation(...)`. Adds the import
94
+ * if missing.
95
+ *
96
+ * Examples:
97
+ * <button onClick={save}>Save</button>
98
+ * →
99
+ * <button onClick={wrapMutation(save)}>Save</button>
100
+ *
101
+ * <form onSubmit={(e) => handler(e)}>
102
+ * →
103
+ * <form onSubmit={wrapMutation((e) => handler(e))}>
104
+ *
105
+ * Idempotent: if the expression is already a call to `wrapMutation`, we
106
+ * leave it alone.
107
+ *
108
+ * Conservative: we skip the attribute (and surface a note) if the
109
+ * expression contains anything we don't know how to wrap cleanly. The
110
+ * agent will re-attempt by hand. Specifically, we skip:
111
+ * - Attributes that aren't `onClick` / `onChange` / `onSubmit` by
112
+ * default (configurable via `--events`).
113
+ * - Attribute values that aren't JsxExpression containers (literal
114
+ * strings, etc.).
115
+ *
116
+ * This is the simplest of the v1.2 codemods — pure structural rewrite,
117
+ * no type info needed (research-4 §B.5 row 4).
118
+ */
119
+
120
+ declare const wrapHandlerWithMutation: Codemod;
121
+
122
+ /**
123
+ * `extract-onclick-to-command`
124
+ *
125
+ * Lift an inline `onClick={() => …}` (or `onSubmit`/`onChange`) into a
126
+ * named module-level command registered with `defineCommand`, and
127
+ * replace the JSX expression with a reference to the command's id
128
+ * dispatched via the registry.
129
+ *
130
+ * Example transform (input):
131
+ * <button onClick={() => store.save()}>Save</button>
132
+ *
133
+ * Example transform (output):
134
+ * const __cmd_handleSave = defineCommand({
135
+ * id: 'app.wrapped.handleSave',
136
+ * title: 'Handle Save',
137
+ * execute: () => { store.save(); return ok(undefined); },
138
+ * });
139
+ *
140
+ * <button onClick={() => registry.dispatch(__cmd_handleSave.id)}>Save</button>
141
+ *
142
+ * **Scope (research-4 §B.5):** This codemod is intentionally narrow.
143
+ * It handles arrow-function-with-block / arrow-function-expression
144
+ * inline handlers that take no parameters and return nothing useful
145
+ * (the common case for buttons). Handlers that:
146
+ * - take parameters (e.g. event objects),
147
+ * - return data the caller uses,
148
+ * - close over local component state that needs to flow into params,
149
+ * are SKIPPED with a note. The agent re-attempts those by hand —
150
+ * conservatism over coverage is the rule (research-4 §B.6).
151
+ *
152
+ * Options (read from `--option key=value` on the CLI):
153
+ * - `id-prefix` default `app.wrapped` — the prefix for
154
+ * generated command ids.
155
+ * - `registry-import` default `./acture/registry` — module to import
156
+ * the `registry` symbol from.
157
+ * - `acture-import` default `acture` — module to import
158
+ * `defineCommand` and `ok` from.
159
+ */
160
+
161
+ declare const extractOnClickToCommand: Codemod;
162
+
163
+ /**
164
+ * `redux-action-to-command`
165
+ *
166
+ * Convert Redux-style `dispatch({ type: 'X', payload: ... })` call sites
167
+ * into `registry.dispatch('X', <payload>)`. Adds the `registry` import
168
+ * if missing.
169
+ *
170
+ * Example transform:
171
+ *
172
+ * dispatch({ type: 'cart/addItem', payload: { id, qty } });
173
+ * →
174
+ * registry.dispatch('cart/addItem', { id, qty });
175
+ *
176
+ * dispatch({ type: 'cart/clear' });
177
+ * →
178
+ * registry.dispatch('cart/clear');
179
+ *
180
+ * Structurally identical to the `azizhk/dispatch-your-reducer` gist
181
+ * (research-4 §B.3 ref [29]). Conservative:
182
+ * - Skip when the action argument isn't an object literal.
183
+ * - Skip when the `type` field isn't a string literal (e.g.
184
+ * `dispatch({ type: actionType, ... })` would need type inference).
185
+ * - Skip when there are keys other than `type` and `payload` — those
186
+ * usually carry Redux-internal metadata that doesn't translate.
187
+ * - Skip when the callee identifier isn't in the configured list
188
+ * (default: `dispatch`, configurable via `--option callees`).
189
+ *
190
+ * Options (from `--option key=value`):
191
+ * - `callees` comma-separated list of dispatch-like callees.
192
+ * Default `dispatch`. Extend with `dispatch,storeDispatch`
193
+ * if your codebase uses multiple names.
194
+ * - `registry-import` default `./acture/registry`. Imported as
195
+ * `{ registry }`.
196
+ * - `id-rewrite` one of `keep` (default), `dot` (rewrite slash to
197
+ * dot — `cart/addItem` → `app.cart.addItem`).
198
+ */
199
+
200
+ declare const reduxActionToCommand: Codemod;
201
+
202
+ /**
203
+ * `usestate-mutation-to-command`
204
+ *
205
+ * Wrap inline `onClick`/`onChange`/`onSubmit` arrow handlers whose body
206
+ * is composed of useState-setter calls (`setX(...)`) with `wrapMutation`,
207
+ * deriving a command id from the setter name. Per research-4 §B.5
208
+ * row 3 — a targeted variant of `wrap-handler-with-mutation` that
209
+ * specifically lifts useState mutations.
210
+ *
211
+ * Example:
212
+ *
213
+ * <button onClick={() => setCount(count + 1)}>+</button>
214
+ * →
215
+ * <button onClick={wrapMutation(
216
+ * () => setCount(count + 1),
217
+ * { id: 'app.state.setCount' },
218
+ * )}>+</button>
219
+ *
220
+ * <button onClick={() => { setOpen(true); setActive('a'); }}>...</button>
221
+ * →
222
+ * <button onClick={wrapMutation(
223
+ * () => { setOpen(true); setActive('a'); },
224
+ * { id: 'app.state.setOpen' },
225
+ * )}>...</button>
226
+ *
227
+ * The id is derived from the FIRST setter call in the body (if multiple
228
+ * setters are present), with `app.state` as the default prefix.
229
+ *
230
+ * Why this is its own codemod (vs. the general
231
+ * `wrap-handler-with-mutation`): the general codemod doesn't know the
232
+ * handler's *intent*. By gating on `setX` calls we get higher-quality
233
+ * generated ids and avoid wrapping handlers that have side effects
234
+ * other than state mutation.
235
+ *
236
+ * Conservative gates (the agent re-attempts skipped handlers by hand):
237
+ * - Body must contain at least one identifier-form CallExpression
238
+ * whose callee matches `^set[A-Z]`.
239
+ * - All top-level statements / expressions in the body must be one of:
240
+ * a CallExpression of a `set*` function, or an existing
241
+ * `wrapMutation(...)` call (idempotency). Anything else → skip.
242
+ *
243
+ * Options (from `--option key=value`):
244
+ * - `id-prefix` default `app.state` — prefix for generated ids.
245
+ * - `setter-pattern` default `^set[A-Z]` — regex for identifying
246
+ * setter identifiers. Override if the codebase
247
+ * uses a different convention.
248
+ * - `events` default `onClick,onChange,onSubmit`.
249
+ * - `import-from` default `acture-migration`.
250
+ */
251
+
252
+ declare const useStateMutationToCommand: Codemod;
253
+
254
+ /**
255
+ * `rtk-thunk-to-command`
256
+ *
257
+ * Convert RTK's `createAsyncThunk(id, payloadCreator)` into an acture
258
+ * async command: `defineCommand({ id, title, execute })`. The original
259
+ * payload creator becomes `execute`, with `return X` rewritten to
260
+ * `return ok(X)` so the result type matches acture's `Result<R>`
261
+ * contract.
262
+ *
263
+ * Example transform (input):
264
+ *
265
+ * export const fetchUser = createAsyncThunk(
266
+ * 'users/fetchUser',
267
+ * async (id: string) => {
268
+ * const res = await fetch(`/users/${id}`);
269
+ * return await res.json();
270
+ * },
271
+ * );
272
+ *
273
+ * Example transform (output):
274
+ *
275
+ * export const fetchUser = defineCommand({
276
+ * id: 'users/fetchUser',
277
+ * title: 'Fetch User',
278
+ * execute: async (id: string) => {
279
+ * const res = await fetch(`/users/${id}`);
280
+ * return ok(await res.json());
281
+ * },
282
+ * });
283
+ *
284
+ * Research-4 §B.5 row 5. This is the type-aware codemod in the v1
285
+ * planned set — but in practice the "type awareness" is minimal: we
286
+ * just need to recognise the payload creator's signature (single arg of
287
+ * any type), not derive its zod schema. Inferring `params` is left to
288
+ * the user — we emit a note in `FileChange.notes` reminding them to add
289
+ * a `params:` field if they want palette / MCP / AI surfaces to see a
290
+ * typed parameter.
291
+ *
292
+ * Conservative gates (skipped with a note rather than half-transformed):
293
+ * - Skip if `createAsyncThunk` has fewer or more than 2 arguments
294
+ * (3rd arg is options — `extraReducers`, `condition`, `idGenerator`
295
+ * etc. — none of which map cleanly to a defineCommand spec).
296
+ * - Skip if the 1st arg isn't a string literal id.
297
+ * - Skip if the 2nd arg isn't an arrow function or function expression.
298
+ *
299
+ * Options (from `--option key=value`):
300
+ * - `acture-import` default `acture` — module from which to import
301
+ * `defineCommand` and `ok`.
302
+ * - `title-from` default `id-last-segment` — strategy for
303
+ * deriving the title. Other value: `id` (use the
304
+ * whole id verbatim).
305
+ */
306
+
307
+ declare const rtkThunkToCommand: Codemod;
308
+
309
+ export { type Codemod, type CodemodOptions, type CodemodResult, type FileChange, MANIFEST, type ManifestEntry, extractOnClickToCommand, findCodemod, listShipped, reduxActionToCommand, rtkThunkToCommand, runCodemod, useStateMutationToCommand, wrapHandlerWithMutation };