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/LICENSE +190 -0
- package/README.md +85 -0
- package/dist/cli.js +957 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +746 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +309 -0
- package/dist/index.d.ts +309 -0
- package/dist/index.js +736 -0
- package/dist/index.js.map +1 -0
- package/migrations.json +37 -0
- package/package.json +62 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|