Skip to content

Commit

Permalink
feat: Implemented the codemod along with some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jackyef committed Nov 16, 2019
1 parent 110c890 commit e8807bf
Show file tree
Hide file tree
Showing 13 changed files with 164 additions and 50 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ dist/
lib/
build/
/website/.cache/
/website/public/
/website/public/
__testfixtures__/
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ CHANGELOG.md
package.json
lerna.json
/website/.cache/
/website/public/
/website/public/
__testfixtures__/
11 changes: 11 additions & 0 deletions packages/codemod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# @loadable/codemod

This package is a collection of codemod that can be used to help making big changes easier to a project, for example: migrating from `react-loadable` to `@loadable/component`

## Notes about `react-loadable-to-loadable-component` transform
`react-loadable-to-loadable-component` transform will help codemod all of your `Loadable()` declaration to `loadable()` with mostly equivalent params, barring some behavior that do not exist in `@loadable/component` such as `Loadable.Map()`, `timeout`, `delay`, etc.

After running the codemod, you will still need to update some of your code manually, namely:
1. Using `loadableReady` to hydrate your app on the client side.
2. Updating your webpack configuration to use `@loadable`
3. Updating your server side rendering code to use `ChunkExtractor`
76 changes: 39 additions & 37 deletions packages/codemod/bin/main.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,70 @@
#!/usr/bin/env node

/* eslint-disable no-console */
const yargs = require('yargs');
const execa = require('execa');
const path = require('path');
const fs = require('fs');
const CodemodError = require('./utils/CodemodError');
const yargs = require('yargs')
const execa = require('execa')
const path = require('path')
const fs = require('fs')
const CodemodError = require('./utils/CodemodError')

const jscodeshiftExecutable = require.resolve('.bin/jscodeshift');
const transformsDir = path.resolve(__dirname, '../transforms');
const jscodeshiftExecutable = require.resolve('.bin/jscodeshift')
const transformsDir = path.resolve(__dirname, '../transforms')

const { argv } = yargs;
const { argv } = yargs

try {
const selectedCodemod = argv._[0];
const directoryToApplyTo = argv._[1];
const selectedCodemod = argv._[0]
const directoryToApplyTo = argv._[1]

if (!selectedCodemod || !directoryToApplyTo) {
throw new CodemodError({
type: 'Invalid params'
});
type: 'Invalid params',
})
}

const availableTransforms = fs.readdirSync(transformsDir);
const availableTransforms = fs
.readdirSync(transformsDir)
.filter(v => v !== '__tests__' && v !== '__testfixtures__')
.map(v => v.replace('.js', ''))

if (!availableTransforms.some(t => t === selectedCodemod)) {
throw new CodemodError({
type: 'Unrecognised transform',
payload: selectedCodemod,
});
})
}
/**
* We need to run the following
*
* jscodeshift --parser babylon -t ./packages/lite-codegen/codemod/upgrade-loadable-component/jscodeshift.js
*/

const result = execa.sync(jscodeshiftExecutable, [
'--parser babylon',
`-t ${transformsDir}/${selectedCodemod}/index.js`,
], {
stdio: 'inherit',
stripEof: false
});
const result = execa.sync(
jscodeshiftExecutable,
['--parser babylon', `-t ${transformsDir}/${selectedCodemod}.js`],
{
stdio: 'inherit',
stripEof: false,
},
)

if (result.error) {
throw result.error;
throw result.error
}

} catch (err) {
if (err.type === 'Invalid params') {
console.error('Invalid params passed!');
console.error('@loadable/codemod requires 2 params to be passed, the name of the codemod, and a directory to apply the codemod to.');
console.error('Example: npx run @loadable/codemod react-loadable-to-loadable-component ./src/client');
console.error('Invalid params passed!')
console.error(
'@loadable/codemod requires 2 params to be passed, the name of the codemod, and a directory to apply the codemod to.',
)
console.error(
'Example: npx @loadable/codemod react-loadable-to-loadable-component ./src/client',
)

process.exit(1);
}
process.exit(1)
}

if (err.type === 'Unrecognised transform') {
console.error(`Unrecognised transform passed: '${err.payload}'`);
console.error(`Unrecognised transform passed: '${err.payload}'`)

process.exit(1);
}
process.exit(1)
}

// For other errors, just re-throw it
throw err;
throw err
}
2 changes: 1 addition & 1 deletion packages/codemod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"repository": "[email protected]:smooth-code/loadable-components.git",
"author": "Jacky Efendi <[email protected]>",
"bin": {
"@loadable/codemod": "./bin/main.js"
"loadable-codemod": "./bin/main.js"
},
"publishConfig": {
"access": "public"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* eslint-disable */
import Loadable from 'react-loadable'

const CustomLinkLoadable = Loadable({
loader: () =>
import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'),
loading: () => <div>loading...</div>,
delay: 0,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable */
import loadable from '@loadable/component'

const CustomLinkLoadable = loadable(() =>
import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), {
fallback: (() => <div>loading...</div>)(),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable */
import Loadable from 'react-loadable'

const CustomLinkLoadable = Loadable({
loader: () =>
import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'),
loading: (props) => {
if (props.error || props.timedOut) {
throw new Error('Failed to load custom link chunk')
} else if (props.loading) {
return <div>loading...</div>;
}
},
delay: 0,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable */
import loadable from '@loadable/component'

const CustomLinkLoadable = loadable(() =>
import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), {
fallback: null,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
import Loadable from 'react-loadable'

const Loading = props => {
if (props.error || props.timedOut) {
throw new Error('Failed to load custom link chunk')
} else {
return null
}
}

const CustomLinkLoadable = Loadable({
loader: () =>
import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'),
loading: Loading,
delay: 0,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable */
import loadable from '@loadable/component'

const Loading = props => {
if (props.error || props.timedOut) {
throw new Error('Failed to load custom link chunk')
} else {
return null
}
}

const CustomLinkLoadable = loadable(() =>
import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), {
fallback: null,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
jest.autoMockOff();

const { defineTest } = require('jscodeshift/dist/testUtils');

defineTest(__dirname, 'react-loadable-to-loadable-component', null, 'react-loadable-to-loadable-component_expr');
defineTest(__dirname, 'react-loadable-to-loadable-component', null, 'react-loadable-to-loadable-component_arrow-no-params');
defineTest(__dirname, 'react-loadable-to-loadable-component', null, 'react-loadable-to-loadable-component_arrow-w-params');
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,67 @@ export default (file, api) => {
});

// Change Loadable({ ... }) invocation to loadable(() => {}, { ... }) invocation
root.find(j.CallExpression, { callee: { name: 'Loadable' } }).forEach(({ node }) => {
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') {
prop.value = prop.value.body; // change () => <div /> to <div />
// if it's an ArrowFunctionExpression like `() => <div>loading...</div>`,

if (prop.value.params && prop.value.params.length > 0) {
// If the function accept params, we can't safely transform it. We can just make it null
prop.value = 'null';
} 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`, we can't know if it accept params or not,
// so just set it to null
prop.value = 'null';
}

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';
});

// remove errorLoading function if exist (maybe just do this manually)

// set correct fallback if previous loader was a function (might need to do manually)

return root.toSource({ quote: 'single', trailingComma: true });
};

/**
* yarn jscodeshift -t ./codemod/upgrade-loadable-component/jscodeshift.js ./codemod/upgrade-loadable-component/dummy.js
*/

0 comments on commit e8807bf

Please sign in to comment.