fork-loadable-codemod
Version:
Various codemods related to @loadable/components for easier migration/upgrades.
132 lines (108 loc) • 4.59 kB
JavaScript
/* eslint-disable no-param-reassign */
/* eslint-disable no-console */
const chalk = require('chalk')
const invokeWithMockedUpProp = (jscodeshift, file, prop) => {
// We invoke the function previously passed as `loading` to react-loadable with this props
// {
// pastDelay: true,
// error: false,
// timedOut: false,
// }
const j = jscodeshift
const defaultPropsObjProperties = []
defaultPropsObjProperties.push(
j.objectProperty(j.identifier('pastDelay'), j.booleanLiteral(true)),
)
defaultPropsObjProperties.push(
j.objectProperty(j.identifier('error'), j.booleanLiteral(false)),
)
defaultPropsObjProperties.push(
j.objectProperty(j.identifier('timedOut'), j.booleanLiteral(false)),
)
const defaultPropsObj = j.objectExpression(defaultPropsObjProperties)
const callExpr = j.callExpression(prop.value, [defaultPropsObj])
prop.value = callExpr
console.warn(
chalk.yellow(
`[WARN] '${file.path}' has some react-loadable specific logic in it. We could not codemod while keeping all the behaviors the same. Please check this file manually.`,
),
)
}
module.exports = (file, api) => {
const { source } = file
const { jscodeshift: j } = api
const root = j(source)
// Rename `import Loadable from 'react-loadable';` to `import loadable from '@loadable/component';
root.find(j.ImportDeclaration).forEach(({ node }) => {
if (
node.specifiers[0] &&
node.specifiers[0].local.name === 'Loadable' &&
node.source.value === '@7rulnik/react-loadable'
) {
node.specifiers[0].local.name = 'loadable'
node.source.value = '@loadable/component'
}
})
// Change Loadable({ ... }) invocation to loadable(() => {}, { ... }) invocation
root
.find(j.CallExpression, { callee: { name: 'Loadable' } })
.forEach(path => {
const { node } = path
const initialArgsProps = node.arguments[0].properties
let loader // this will be a function returning a dynamic import promise
// loop through the first argument (object) passed to `Loadable({ ... })`
const newProps = initialArgsProps
.map(prop => {
if (prop.key.name === 'loader') {
/**
* In react-loadable, this is the function that returns a dynamic import
* We'll keep it to `loader` variable for now, and remove it from the arg object
*/
loader = prop.value
return undefined
}
if (prop.key.name === 'loading') {
prop.key.name = 'fallback' // rename to fallback
/**
* react-loadable accepts a Function that returns JSX as the `loading` arg.
* @loadable/component accepts a React.Element (what returned from React.createElement() calls)
*
*/
if (prop.value.type === 'ArrowFunctionExpression') {
// if it's an ArrowFunctionExpression like `() => <div>loading...</div>`,
if (
(prop.value.params && prop.value.params.length > 0) ||
prop.value.type === 'Identifier'
) {
// If the function accept props, we can invoke it and pass it a mocked-up props to get the component to
// a should-be-acceptable default state, while also logs out a warning.
// {
// pastDelay: true,
// error: false,
// timedOut: false,
// }
invokeWithMockedUpProp(j, file, prop)
} else {
// If the function doesn't accept any params, we can safely just invoke it directly
// we can change it to `(() => <div>loading...</div>)()`
const callExpr = j.callExpression(prop.value, [])
prop.value = callExpr
}
} else if (prop.value.type === 'Identifier') {
// if it's an identifier like `Loading`, let's just invoke it with a mocked-up props
invokeWithMockedUpProp(j, file, prop)
}
return prop
}
// for all other props, just remove them
return undefined
})
.filter(Boolean)
// add the function that return a dynamic import we stored earlier as the first argument to `loadable()` call
node.arguments.unshift(loader)
node.arguments[1].properties = newProps
node.callee.name = 'loadable'
})
return root.toSource({ quote: 'single', trailingComma: true })
}
module.exports.parser = 'babylon'