Skip to content

write create callback codemod #3525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/api/codemods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ hide_title: true

# Codemods

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.
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.

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

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`.
The codemods package is available on NPM as [**`@reduxjs/rtk-codemods`**](https://www.npmjs.com/package/@reduxjs/rtk-codemods). It currently contains these codemods:

- `createReducerBuilder`: migrates `createReducer` calls that use the removed object syntax to the builder callback syntax
- `createSliceBuilder`: migrates `createSlice` calls that use the removed object syntax for `extraReducers` to the builder callback syntax
- `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

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

Expand Down
3 changes: 2 additions & 1 deletion packages/rtk-codemods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"prettier": "^2.2.1"
"prettier": "^2.2.1",
"vitest": "^0.30.1"
},
"engines": {
"node": ">= 16"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# createSliceReducerBuilder

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`.

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.

Should work with both JS and TS files.

## Usage

```
npx @reduxjs/rtk-codemods createSliceReducerBuilder path/of/files/ or/some**/*glob.js

# or

yarn global add @reduxjs/rtk-codemods
@reduxjs/rtk-codemods createSliceReducerBuilder path/of/files/ or/some**/*glob.js
```

## Local Usage

```
node ./bin/cli.js createSliceReducerBuilder path/of/files/ or/some**/*glob.js
```

## Input / Output

<!--FIXTURES_TOC_START-->
<!--FIXTURES_TOC_END-->

<!--FIXTURES_CONTENT_START-->
<!--FIXTURES_CONTENT_END-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const aSlice = createSlice({
name: 'name',
initialState: todoAdapter.getInitialState(),
reducers: {
property: () => {},
method(state, action: PayloadAction<Todo>) {
todoAdapter.addOne(state, action);
},
identifier: todoAdapter.removeOne,
preparedProperty: {
prepare: (todo: Todo) => ({ payload: { id: nanoid(), ...todo } }),
reducer: () => {}
},
preparedMethod: {
prepare(todo: Todo) {
return { payload: { id: nanoid(), ...todo } }
},
reducer(state, action: PayloadAction<Todo>) {
todoAdapter.addOne(state, action);
}
},
preparedIdentifier: {
prepare: withPayload(),
reducer: todoAdapter.setMany
},
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const aSlice = createSlice({
name: 'name',
initialState: todoAdapter.getInitialState(),

reducers: (create) => ({
property: create.reducer(() => {}),

method: create.reducer((state, action: PayloadAction<Todo>) => {
todoAdapter.addOne(state, action);
}),

identifier: create.reducer(todoAdapter.removeOne),
preparedProperty: create.preparedReducer((todo: Todo) => ({ payload: { id: nanoid(), ...todo } }), () => {}),

preparedMethod: create.preparedReducer((todo: Todo) => {
return { payload: { id: nanoid(), ...todo } }
}, (state, action: PayloadAction<Todo>) => {
todoAdapter.addOne(state, action);
}),

preparedIdentifier: create.preparedReducer(withPayload(), todoAdapter.setMany)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const aSlice = createSlice({
name: 'name',
initialState: todoAdapter.getInitialState(),
reducers: {
property: () => {},
method(state, action) {
todoAdapter.setMany(state, action);
},
identifier: todoAdapter.removeOne,
preparedProperty: {
prepare: (todo) => ({ payload: { id: nanoid(), ...todo } }),
reducer: () => {}
},
preparedMethod: {
prepare(todo) {
return { payload: { id: nanoid(), ...todo } }
},
reducer(state, action) {
todoAdapter.setMany(state, action);
}
},
preparedIdentifier: {
prepare: withPayload(),
reducer: todoAdapter.setMany
},
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const aSlice = createSlice({
name: 'name',
initialState: todoAdapter.getInitialState(),

reducers: (create) => ({
property: create.reducer(() => {}),

method: create.reducer((state, action) => {
todoAdapter.setMany(state, action);
}),

identifier: create.reducer(todoAdapter.removeOne),
preparedProperty: create.preparedReducer((todo) => ({ payload: { id: nanoid(), ...todo } }), () => {}),

preparedMethod: create.preparedReducer((todo) => {
return { payload: { id: nanoid(), ...todo } }
}, (state, action) => {
todoAdapter.setMany(state, action);
}),

preparedIdentifier: create.preparedReducer(withPayload(), todoAdapter.setMany)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import path from 'path';
import transform, { parser } from './index';

import { runTransformTest } from '../../transformTestUtils';

runTransformTest(
'createSliceReducerBuilder',
transform,
parser,
path.join(__dirname, '__testfixtures__')
);
179 changes: 179 additions & 0 deletions packages/rtk-codemods/transforms/createSliceReducerBuilder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/* eslint-disable node/no-extraneous-import */
/* eslint-disable node/no-unsupported-features/es-syntax */
import type { ExpressionKind, SpreadElementKind } from 'ast-types/gen/kinds';
import type {
CallExpression,
JSCodeshift,
ObjectExpression,
ObjectProperty,
Transform,
} from 'jscodeshift';

function creatorCall(j: JSCodeshift, type: 'reducer', reducer: ExpressionKind): CallExpression;
// eslint-disable-next-line no-redeclare
function creatorCall(
j: JSCodeshift,
type: 'preparedReducer',
prepare: ExpressionKind,
reducer: ExpressionKind
): CallExpression;
// eslint-disable-next-line no-redeclare
function creatorCall(
j: JSCodeshift,
type: 'reducer' | 'preparedReducer',
...rest: Array<ExpressionKind | SpreadElementKind>
) {
return j.callExpression(j.memberExpression(j.identifier('create'), j.identifier(type)), rest);
}

export function reducerPropsToBuilderExpression(j: JSCodeshift, defNode: ObjectExpression) {
const returnedObject = j.objectExpression([]);
for (let property of defNode.properties) {
let finalProp: ObjectProperty | undefined;
switch (property.type) {
case 'ObjectMethod': {
const { key, params, body } = property;
finalProp = j.objectProperty(
key,
creatorCall(j, 'reducer', j.arrowFunctionExpression(params, body))
);
break;
}
case 'ObjectProperty': {
const { key } = property;

switch (property.value.type) {
case 'ObjectExpression': {
let preparedReducerParams: { prepare?: ExpressionKind; reducer?: ExpressionKind } = {};

for (const objProp of property.value.properties) {
switch (objProp.type) {
case 'ObjectMethod': {
const { key, params, body } = objProp;
if (
key.type === 'Identifier' &&
(key.name === 'reducer' || key.name === 'prepare')
) {
preparedReducerParams[key.name] = j.arrowFunctionExpression(params, body);
}
break;
}
case 'ObjectProperty': {
const { key, value } = objProp;

let finalExpression: ExpressionKind | undefined = undefined;

switch (value.type) {
case 'ArrowFunctionExpression':
case 'FunctionExpression':
case 'Identifier':
case 'MemberExpression':
case 'CallExpression': {
finalExpression = value;
}
}

if (
key.type === 'Identifier' &&
(key.name === 'reducer' || key.name === 'prepare') &&
finalExpression
) {
preparedReducerParams[key.name] = finalExpression;
}
break;
}
}
}

if (preparedReducerParams.prepare && preparedReducerParams.reducer) {
finalProp = j.objectProperty(
key,
creatorCall(
j,
'preparedReducer',
preparedReducerParams.prepare,
preparedReducerParams.reducer
)
);
} else if (preparedReducerParams.reducer) {
finalProp = j.objectProperty(
key,
creatorCall(j, 'reducer', preparedReducerParams.reducer)
);
}
break;
}
case 'ArrowFunctionExpression':
case 'FunctionExpression':
case 'Identifier':
case 'MemberExpression':
case 'CallExpression': {
const { value } = property;
finalProp = j.objectProperty(key, creatorCall(j, 'reducer', value));
break;
}
}
break;
}
}
if (!finalProp) {
continue;
}
returnedObject.properties.push(finalProp);
}

return j.arrowFunctionExpression([j.identifier('create')], returnedObject, true);
}

const transform: Transform = (file, api) => {
const j = api.jscodeshift;

return (
j(file.source)
// @ts-ignore some expression mismatch
.find(j.CallExpression, {
callee: { name: 'createSlice' },
// @ts-ignore some expression mismatch
arguments: { 0: { type: 'ObjectExpression' } },
})

.filter((path) => {
const createSliceArgsObject = path.node.arguments[0] as ObjectExpression;
return createSliceArgsObject.properties.some(
(p) =>
p.type === 'ObjectProperty' &&
p.key.type === 'Identifier' &&
p.key.name === 'reducers' &&
p.value.type === 'ObjectExpression'
);
})
.forEach((path) => {
const createSliceArgsObject = path.node.arguments[0] as ObjectExpression;
j(path).replaceWith(
j.callExpression(j.identifier('createSlice'), [
j.objectExpression(
createSliceArgsObject.properties.map((p) => {
if (
p.type === 'ObjectProperty' &&
p.key.type === 'Identifier' &&
p.key.name === 'reducers' &&
p.value.type === 'ObjectExpression'
) {
const expressionStatement = reducerPropsToBuilderExpression(j, p.value);
return j.objectProperty(p.key, expressionStatement);
}
return p;
})
),
])
);
})
.toSource({
arrowParensAlways: true,
})
);
};

export const parser = 'tsx';

export default transform;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

const { runTransformTest } = require('codemod-cli');

runTransformTest({
name: 'createSliceReducerBuilder',
path: require.resolve('./index.ts'),
fixtureDir: `${__dirname}/__testfixtures__/`,
});
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6934,6 +6934,7 @@ __metadata:
eslint-plugin-prettier: ^3.4.0
prettier: ^2.2.1
typescript: ^4.8.0
vitest: ^0.30.1
bin:
rtk-codemods: ./bin/cli.js
languageName: unknown
Expand Down