Skip to content

Commit 3c0546a

Browse files
authored
Merge pull request #3525 from reduxjs/create-callback-codemod
2 parents 32a7dcf + de564e1 commit 3c0546a

File tree

11 files changed

+340
-3
lines changed

11 files changed

+340
-3
lines changed

docs/api/codemods.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ hide_title: true
99

1010
# Codemods
1111

12-
Per [the description in `1.9.0-alpha.0`](https://github.com/reduxjs/redux-toolkit/releases/tag/v1.9.0-alpha.0), we plan to remove the "object" argument from `createReducer` and `createSlice.extraReducers` in the future RTK 2.0 major version. In `1.9.0-alpha.0`, we added a one-shot runtime warning to each of those APIs.
12+
Per [the description in `1.9.0`](https://github.com/reduxjs/redux-toolkit/releases/tag/v1.9.0), we have removed the "object" argument from `createReducer` and `createSlice.extraReducers` in the RTK 2.0 major version. We've also added a new optional form of `createSlice.reducers` that uses a callback instead of an object.
1313

1414
To simplify upgrading codebases, we've published a set of codemods that will automatically transform the deprecated "object" syntax into the equivalent "builder" syntax.
1515

16-
The codemods package is available on NPM as [**`@reduxjs/rtk-codemods`**](https://www.npmjs.com/package/@reduxjs/rtk-codemods). It currently contains two codemods: `createReducerBuilder` and `createSliceBuilder`.
16+
The codemods package is available on NPM as [**`@reduxjs/rtk-codemods`**](https://www.npmjs.com/package/@reduxjs/rtk-codemods). It currently contains these codemods:
17+
18+
- `createReducerBuilder`: migrates `createReducer` calls that use the removed object syntax to the builder callback syntax
19+
- `createSliceBuilder`: migrates `createSlice` calls that use the removed object syntax for `extraReducers` to the builder callback syntax
20+
- `createSliceReducerBuilder`: migrates `createSlice` calls that use the still-standard object syntax for `reducers` to the optional new builder callback syntax, including uses of prepared reducers
1721

1822
To run the codemods against your codebase, run `npx @reduxjs/rtk-codemods <TRANSFORM NAME> path/of/files/ or/some**/*glob.js`.
1923

packages/rtk-codemods/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"eslint-config-prettier": "^8.3.0",
3232
"eslint-plugin-node": "^11.1.0",
3333
"eslint-plugin-prettier": "^3.4.0",
34-
"prettier": "^2.2.1"
34+
"prettier": "^2.2.1",
35+
"vitest": "^0.30.1"
3536
},
3637
"engines": {
3738
"node": ">= 16"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# createSliceReducerBuilder
2+
3+
Rewrites uses of Redux Toolkit's `createSlice` API to use the "builder callback" syntax for the `reducers` field, to make it easier to add prepared reducers and thunks inside of `createSlice`.
4+
5+
Note that unlike the `createReducerBuilder` and `createSliceBuilder` transforms (which both were fixes for deprecated/removed overloads), this is entirely optional. You do not _need_ to apply this to an entire codebase unless you specifically want to. Otherwise, feel free to apply to to specific slice files as needed.
6+
7+
Should work with both JS and TS files.
8+
9+
## Usage
10+
11+
```
12+
npx @reduxjs/rtk-codemods createSliceReducerBuilder path/of/files/ or/some**/*glob.js
13+
14+
# or
15+
16+
yarn global add @reduxjs/rtk-codemods
17+
@reduxjs/rtk-codemods createSliceReducerBuilder path/of/files/ or/some**/*glob.js
18+
```
19+
20+
## Local Usage
21+
22+
```
23+
node ./bin/cli.js createSliceReducerBuilder path/of/files/ or/some**/*glob.js
24+
```
25+
26+
## Input / Output
27+
28+
<!--FIXTURES_TOC_START-->
29+
<!--FIXTURES_TOC_END-->
30+
31+
<!--FIXTURES_CONTENT_START-->
32+
<!--FIXTURES_CONTENT_END-->
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const aSlice = createSlice({
2+
name: 'name',
3+
initialState: todoAdapter.getInitialState(),
4+
reducers: {
5+
property: () => {},
6+
method(state, action: PayloadAction<Todo>) {
7+
todoAdapter.addOne(state, action);
8+
},
9+
identifier: todoAdapter.removeOne,
10+
preparedProperty: {
11+
prepare: (todo: Todo) => ({ payload: { id: nanoid(), ...todo } }),
12+
reducer: () => {}
13+
},
14+
preparedMethod: {
15+
prepare(todo: Todo) {
16+
return { payload: { id: nanoid(), ...todo } }
17+
},
18+
reducer(state, action: PayloadAction<Todo>) {
19+
todoAdapter.addOne(state, action);
20+
}
21+
},
22+
preparedIdentifier: {
23+
prepare: withPayload(),
24+
reducer: todoAdapter.setMany
25+
},
26+
}
27+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const aSlice = createSlice({
2+
name: 'name',
3+
initialState: todoAdapter.getInitialState(),
4+
5+
reducers: (create) => ({
6+
property: create.reducer(() => {}),
7+
8+
method: create.reducer((state, action: PayloadAction<Todo>) => {
9+
todoAdapter.addOne(state, action);
10+
}),
11+
12+
identifier: create.reducer(todoAdapter.removeOne),
13+
preparedProperty: create.preparedReducer((todo: Todo) => ({ payload: { id: nanoid(), ...todo } }), () => {}),
14+
15+
preparedMethod: create.preparedReducer((todo: Todo) => {
16+
return { payload: { id: nanoid(), ...todo } }
17+
}, (state, action: PayloadAction<Todo>) => {
18+
todoAdapter.addOne(state, action);
19+
}),
20+
21+
preparedIdentifier: create.preparedReducer(withPayload(), todoAdapter.setMany)
22+
})
23+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const aSlice = createSlice({
2+
name: 'name',
3+
initialState: todoAdapter.getInitialState(),
4+
reducers: {
5+
property: () => {},
6+
method(state, action) {
7+
todoAdapter.setMany(state, action);
8+
},
9+
identifier: todoAdapter.removeOne,
10+
preparedProperty: {
11+
prepare: (todo) => ({ payload: { id: nanoid(), ...todo } }),
12+
reducer: () => {}
13+
},
14+
preparedMethod: {
15+
prepare(todo) {
16+
return { payload: { id: nanoid(), ...todo } }
17+
},
18+
reducer(state, action) {
19+
todoAdapter.setMany(state, action);
20+
}
21+
},
22+
preparedIdentifier: {
23+
prepare: withPayload(),
24+
reducer: todoAdapter.setMany
25+
},
26+
}
27+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const aSlice = createSlice({
2+
name: 'name',
3+
initialState: todoAdapter.getInitialState(),
4+
5+
reducers: (create) => ({
6+
property: create.reducer(() => {}),
7+
8+
method: create.reducer((state, action) => {
9+
todoAdapter.setMany(state, action);
10+
}),
11+
12+
identifier: create.reducer(todoAdapter.removeOne),
13+
preparedProperty: create.preparedReducer((todo) => ({ payload: { id: nanoid(), ...todo } }), () => {}),
14+
15+
preparedMethod: create.preparedReducer((todo) => {
16+
return { payload: { id: nanoid(), ...todo } }
17+
}, (state, action) => {
18+
todoAdapter.setMany(state, action);
19+
}),
20+
21+
preparedIdentifier: create.preparedReducer(withPayload(), todoAdapter.setMany)
22+
})
23+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import path from 'path';
2+
import transform, { parser } from './index';
3+
4+
import { runTransformTest } from '../../transformTestUtils';
5+
6+
runTransformTest(
7+
'createSliceReducerBuilder',
8+
transform,
9+
parser,
10+
path.join(__dirname, '__testfixtures__')
11+
);
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/* eslint-disable node/no-extraneous-import */
2+
/* eslint-disable node/no-unsupported-features/es-syntax */
3+
import type { ExpressionKind, SpreadElementKind } from 'ast-types/gen/kinds';
4+
import type {
5+
CallExpression,
6+
JSCodeshift,
7+
ObjectExpression,
8+
ObjectProperty,
9+
Transform,
10+
} from 'jscodeshift';
11+
12+
function creatorCall(j: JSCodeshift, type: 'reducer', reducer: ExpressionKind): CallExpression;
13+
// eslint-disable-next-line no-redeclare
14+
function creatorCall(
15+
j: JSCodeshift,
16+
type: 'preparedReducer',
17+
prepare: ExpressionKind,
18+
reducer: ExpressionKind
19+
): CallExpression;
20+
// eslint-disable-next-line no-redeclare
21+
function creatorCall(
22+
j: JSCodeshift,
23+
type: 'reducer' | 'preparedReducer',
24+
...rest: Array<ExpressionKind | SpreadElementKind>
25+
) {
26+
return j.callExpression(j.memberExpression(j.identifier('create'), j.identifier(type)), rest);
27+
}
28+
29+
export function reducerPropsToBuilderExpression(j: JSCodeshift, defNode: ObjectExpression) {
30+
const returnedObject = j.objectExpression([]);
31+
for (let property of defNode.properties) {
32+
let finalProp: ObjectProperty | undefined;
33+
switch (property.type) {
34+
case 'ObjectMethod': {
35+
const { key, params, body } = property;
36+
finalProp = j.objectProperty(
37+
key,
38+
creatorCall(j, 'reducer', j.arrowFunctionExpression(params, body))
39+
);
40+
break;
41+
}
42+
case 'ObjectProperty': {
43+
const { key } = property;
44+
45+
switch (property.value.type) {
46+
case 'ObjectExpression': {
47+
let preparedReducerParams: { prepare?: ExpressionKind; reducer?: ExpressionKind } = {};
48+
49+
for (const objProp of property.value.properties) {
50+
switch (objProp.type) {
51+
case 'ObjectMethod': {
52+
const { key, params, body } = objProp;
53+
if (
54+
key.type === 'Identifier' &&
55+
(key.name === 'reducer' || key.name === 'prepare')
56+
) {
57+
preparedReducerParams[key.name] = j.arrowFunctionExpression(params, body);
58+
}
59+
break;
60+
}
61+
case 'ObjectProperty': {
62+
const { key, value } = objProp;
63+
64+
let finalExpression: ExpressionKind | undefined = undefined;
65+
66+
switch (value.type) {
67+
case 'ArrowFunctionExpression':
68+
case 'FunctionExpression':
69+
case 'Identifier':
70+
case 'MemberExpression':
71+
case 'CallExpression': {
72+
finalExpression = value;
73+
}
74+
}
75+
76+
if (
77+
key.type === 'Identifier' &&
78+
(key.name === 'reducer' || key.name === 'prepare') &&
79+
finalExpression
80+
) {
81+
preparedReducerParams[key.name] = finalExpression;
82+
}
83+
break;
84+
}
85+
}
86+
}
87+
88+
if (preparedReducerParams.prepare && preparedReducerParams.reducer) {
89+
finalProp = j.objectProperty(
90+
key,
91+
creatorCall(
92+
j,
93+
'preparedReducer',
94+
preparedReducerParams.prepare,
95+
preparedReducerParams.reducer
96+
)
97+
);
98+
} else if (preparedReducerParams.reducer) {
99+
finalProp = j.objectProperty(
100+
key,
101+
creatorCall(j, 'reducer', preparedReducerParams.reducer)
102+
);
103+
}
104+
break;
105+
}
106+
case 'ArrowFunctionExpression':
107+
case 'FunctionExpression':
108+
case 'Identifier':
109+
case 'MemberExpression':
110+
case 'CallExpression': {
111+
const { value } = property;
112+
finalProp = j.objectProperty(key, creatorCall(j, 'reducer', value));
113+
break;
114+
}
115+
}
116+
break;
117+
}
118+
}
119+
if (!finalProp) {
120+
continue;
121+
}
122+
returnedObject.properties.push(finalProp);
123+
}
124+
125+
return j.arrowFunctionExpression([j.identifier('create')], returnedObject, true);
126+
}
127+
128+
const transform: Transform = (file, api) => {
129+
const j = api.jscodeshift;
130+
131+
return (
132+
j(file.source)
133+
// @ts-ignore some expression mismatch
134+
.find(j.CallExpression, {
135+
callee: { name: 'createSlice' },
136+
// @ts-ignore some expression mismatch
137+
arguments: { 0: { type: 'ObjectExpression' } },
138+
})
139+
140+
.filter((path) => {
141+
const createSliceArgsObject = path.node.arguments[0] as ObjectExpression;
142+
return createSliceArgsObject.properties.some(
143+
(p) =>
144+
p.type === 'ObjectProperty' &&
145+
p.key.type === 'Identifier' &&
146+
p.key.name === 'reducers' &&
147+
p.value.type === 'ObjectExpression'
148+
);
149+
})
150+
.forEach((path) => {
151+
const createSliceArgsObject = path.node.arguments[0] as ObjectExpression;
152+
j(path).replaceWith(
153+
j.callExpression(j.identifier('createSlice'), [
154+
j.objectExpression(
155+
createSliceArgsObject.properties.map((p) => {
156+
if (
157+
p.type === 'ObjectProperty' &&
158+
p.key.type === 'Identifier' &&
159+
p.key.name === 'reducers' &&
160+
p.value.type === 'ObjectExpression'
161+
) {
162+
const expressionStatement = reducerPropsToBuilderExpression(j, p.value);
163+
return j.objectProperty(p.key, expressionStatement);
164+
}
165+
return p;
166+
})
167+
),
168+
])
169+
);
170+
})
171+
.toSource({
172+
arrowParensAlways: true,
173+
})
174+
);
175+
};
176+
177+
export const parser = 'tsx';
178+
179+
export default transform;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
3+
const { runTransformTest } = require('codemod-cli');
4+
5+
runTransformTest({
6+
name: 'createSliceReducerBuilder',
7+
path: require.resolve('./index.ts'),
8+
fixtureDir: `${__dirname}/__testfixtures__/`,
9+
});

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6934,6 +6934,7 @@ __metadata:
69346934
eslint-plugin-prettier: ^3.4.0
69356935
prettier: ^2.2.1
69366936
typescript: ^4.8.0
6937+
vitest: ^0.30.1
69376938
bin:
69386939
rtk-codemods: ./bin/cli.js
69396940
languageName: unknown

0 commit comments

Comments
 (0)