Skip to content
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
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# test fixtures
/__testfixtures__
42 changes: 14 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,41 @@ npx @jowapp/codemod <transform> <path> [...options]
```

- `transform` - name of transform, see available transforms below.
- `path` - files or directory to transform (use quotes otherwise the path(s) will be expanded by your shell, [which may not support globstar](https://medium.com/@jakubsynowiec/you-should-always-quote-your-globs-in-npm-scripts-621887a2a784))
- `path` - files or directory to transform (**use quotes**)
- use the `--dry` option for a dry-run and use `--print` to print the output for comparison

This will start an interactive wizard, and then run the specified transform.

All jscodeshift's options are supported, and defaults are the same than the one of jscodeshift's CLI.
All [jscodeshift's options](https://github.com/facebook/jscodeshift?tab=readme-ov-file#usage-cli) are supported, and defaults are the same than the one of jscodeshift's CLI.

> [!NOTE]
> jscodeshift's option defaults apply only to its CLI, its API has none, except the parser which defaults to `@babel/parser` (see `babel5Compat.js`)

> jscodeshift's option defaults apply only to its CLI, its API has none, except the parser, which defaults to `@babel/parser` (see `babel5Compat.js`)
> All options are also passed to the transformer, which means you can supply custom options that are not listed here.

### Recast Options

Options to [recast](https://github.com/benjamn/recast)'s printer can also be provided through jscodeshift's `printOptions` command line argument, as `react-codemod` does it:

```sh
npx react-codemod <transform> <path> --jscodeshift="--printOptions='{\"quote\":\"double\"}'"
```

### Unit Testing

Unit tests are based on [jscodeshift's test utility](https://github.com/facebook/jscodeshift?tab=readme-ov-file#unit-testing) with `vitest`.

But here the directory structure is a bit different:

```
/subfolder/MyTransform.js
/__tests__/subfolder/MyTransform-test.js
/__testfixtures__/subfolder/MyTransform.input.js
/__testfixtures__/subfolder/MyTransform.output.js
```

## Included Transforms

### React
### Migrate to React 19

#### `react.func-default-props-to-params`

```sh
npx @jowapp/codemod react.func-default-props-to-params <path>
```

Converts functional components default props to default params.
Converts functional components default props onto default parameters.

#### `react.forward-ref-to-prop`

```sh
npx @jowapp/codemod react.forward-ref-to-prop <path>
```

Converts `forwardRef` to regular prop `ref`.
Converts `forwardRef` to regular `ref` prop.

## Testing

```sh
yarn test:run
## or
yarn test:watch
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { forwardRef } from 'react';

const C1 = forwardRef((props, ref) => {
return <input ref={ref} onChange={props.onChange} />;
});

const C2 = forwardRef((props, ref) => {
return <input ref={ref} onChange={props.onChange} />;
});

C1.displayName = 'C1';
C2.displayName = 'C2';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const C1 = (
{
ref,
...props
}
) => {
return <input ref={ref} onChange={props.onChange} />;
};

const C2 = (
{
ref,
...props
}
) => {
return <input ref={ref} onChange={props.onChange} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { forwardRef } from 'react';

const C = forwardRef(function MyInput({ onChange }, ref) {
return <input ref={ref} onChange={onChange} />;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const C = function MyInput(
{
ref,
onChange
}
) {
return <input ref={ref} onChange={onChange} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { forwardRef, useState } from 'react';

const C = forwardRef(function MyInput(props, ref) {
return <input ref={ref} onChange={props.onChange} />;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useState } from 'react';

const C = function MyInput(
{
ref,
...props
}
) {
return <input ref={ref} onChange={props.onChange} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { forwardRef } from 'react';

const C = forwardRef(function MyInput(props, ref) {
return <input ref={ref} onChange={props.onChange} />;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const C = function MyInput(
{
ref,
...props
}
) {
return <input ref={ref} onChange={props.onChange} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useState } from 'react';

const C = (props, ref) => {
return <input ref={ref} onChange={props.onChange} />;
};

export default C;
48 changes: 48 additions & 0 deletions __tests__/react/forward-ref-to-prop.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict'

import path from 'node:path'
import fs from 'fs'
import { defineTest } from 'jscodeshift/dist/testUtils'
import transform from '../../transforms/react/forward-ref-to-prop'

const jsTests = [
'function-expression',
'function-expression-w-multiple-imports',
'arrow-function-expression-w-display-name',
'arrow-function-expression-w-props-object-pattern',
]

const jsTestsNoOutput = ['no-forward-ref']

describe('react.forward-ref-to-prop', () => {
jsTests.forEach((test) =>
defineTest(
path.join(__dirname, '..'),
'transforms/react/forward-ref-to-prop',
null,
`react/forward-ref-to-prop/${test}`,
),
)

describe('transforms/react/forward-ref-to-prop', () => {
it.each(jsTestsNoOutput)('does not transform using "react/forward-ref-to-prop/%s"', (test) => {
const applyTransform = require('jscodeshift/dist/testUtils').applyTransform
const transformOptions = {}
const fixtureDir = path.join(
__dirname,
'..',
'..',
'__testfixtures__',
'react',
'forward-ref-to-prop',
)
const inputPath = path.join(fixtureDir, test + '.input.js')
const inputSource = fs.readFileSync(inputPath, 'utf8')
const output = applyTransform(transform, transformOptions, {
source: inputSource,
path: inputPath,
})
expect(output).toBe('')
})
})
})
125 changes: 125 additions & 0 deletions transforms/react/forward-ref-to-prop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
module.exports = (fileInfo, api, options) => {
// console.log(fileInfo.path)
const j = api.jscodeshift
const printOptions = options.printOptions || {}
const root = j(fileInfo.source)

let isDirty = false

// { ref: refName, ...propsName }
const buildRefAndPropsObjectPattern = (j, refArgName, propArgName) =>
j.objectPattern([
j.objectProperty.from({
shorthand: true,
key: j.identifier('ref'),
value: j.identifier(refArgName),
}),
j.restProperty(j.identifier(propArgName)),
])

const getForwardRefRenderFunction = (j, callExpression) => {
const [renderFunction] = callExpression.arguments
if (
!j.FunctionExpression.check(renderFunction) &&
!j.ArrowFunctionExpression.check(renderFunction)
) {
return null
}
return renderFunction
}

let componentsNames = []

root
.find(j.CallExpression, {
callee: {
type: 'Identifier',
name: 'forwardRef',
},
})
.replaceWith((callExpressionPath) => {
const originalCallExpression = callExpressionPath.value

const renderFunction = getForwardRefRenderFunction(j, callExpressionPath.node)

if (renderFunction === null) {
console.warn('Could not detect render function.')

return originalCallExpression
}

const [propsArg, refArg] = renderFunction.params

if (
!j.Identifier.check(refArg) ||
!(j.Identifier.check(propsArg) || j.ObjectPattern.check(propsArg))
) {
console.warn('Could not detect ref or props arguments.')

return originalCallExpression
}

// remove refArg
renderFunction.params.splice(1, 1)
isDirty = true

componentsNames.push(callExpressionPath.parent.node.id.name)

// if propsArg is ObjectPattern, add ref as new ObjectProperty
if (j.ObjectPattern.check(propsArg)) {
propsArg.properties.unshift(
j.objectProperty.from({
shorthand: true,
key: j.identifier('ref'),
value: j.identifier(refArg.name),
}),
)
}

// if props arg is Identifier, push ref variable declaration to the function body
if (j.Identifier.check(propsArg)) {
renderFunction.params[0] = buildRefAndPropsObjectPattern(j, refArg.name, propsArg.name)
}

return renderFunction
})

if (isDirty) {
// handle import
root
.find(j.ImportDeclaration, {
source: {
value: 'react',
},
})
.forEach((importDeclarationPath) => {
const { specifiers } = importDeclarationPath.node

const specifiersWithoutForwardRef =
specifiers?.filter(
(s) => j.ImportSpecifier.check(s) && s.imported.name !== 'forwardRef',
) ?? []

if (specifiersWithoutForwardRef.length === 0) {
j(importDeclarationPath).remove()
}

importDeclarationPath.node.specifiers = specifiersWithoutForwardRef
})
// handle displayName
componentsNames.forEach((componentName) => {
root
.find(j.AssignmentExpression, {
left: {
object: { name: componentName },
property: { name: 'displayName' },
},
})
.remove()
})
}

return isDirty ? root.toSource(printOptions) : null
}

// module.exports.parser = 'babel'