From 69b805e8374f14c37ba23ae68223bb8760b9007d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Gomes?= Date: Wed, 2 Oct 2019 14:57:05 +0100 Subject: [PATCH] Merge pull request #8 from Automattic/add/remove-create-reducer Add codemod to remove create-reducer --- packages/calypso-codemods/config.js | 21 +- .../__snapshots__/codemod.spec.js.snap | 105 +++++++ .../remove-create-reducer/codemod.spec.js | 1 + .../remove-create-reducer/create-reducer.js | 48 +++ .../transforms/remove-create-reducer.js | 292 ++++++++++++++++++ 5 files changed, 456 insertions(+), 11 deletions(-) create mode 100644 packages/calypso-codemods/tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap create mode 100644 packages/calypso-codemods/tests/remove-create-reducer/codemod.spec.js create mode 100644 packages/calypso-codemods/tests/remove-create-reducer/create-reducer.js create mode 100644 packages/calypso-codemods/transforms/remove-create-reducer.js diff --git a/packages/calypso-codemods/config.js b/packages/calypso-codemods/config.js index 0829c5802eeff..62383a3eefbd0 100644 --- a/packages/calypso-codemods/config.js +++ b/packages/calypso-codemods/config.js @@ -1,12 +1,7 @@ -const jscodeshiftArgs = [ - '--extensions=js,jsx', -]; +const jscodeshiftArgs = [ '--extensions=js,jsx' ]; // Used primarily by 5to6-codemod transformations -const recastArgs = [ - '--useTabs=true', - '--arrayBracketSpacing=true', -]; +const recastArgs = [ '--useTabs=true', '--arrayBracketSpacing=true' ]; const recastOptions = { arrayBracketSpacing: true, @@ -17,18 +12,19 @@ const recastOptions = { objects: true, arrays: true, parameters: false, - } + }, }; + const commonArgs = { '5to6': [ // Recast options via 5to6 ...recastArgs, ], - 'react': [ + react: [ // Recast options via react-codemod `--printOptions=${ JSON.stringify( recastOptions ) }`, ], -} +}; const codemodArgs = { 'commonjs-exports': [ @@ -40,11 +36,13 @@ const codemodArgs = { ...commonArgs[ '5to6' ], '--transform=node_modules/5to6-codemod/transforms/cjs.js', ], + 'commonjs-imports-hoist': [ ...commonArgs[ '5to6' ], '--transform=node_modules/5to6-codemod/transforms/cjs.js', '--hoist=true', ], + 'named-exports-from-default': [ ...commonArgs[ '5to6' ], '--transform=node_modules/5to6-codemod/transforms/named-export-generation.js', @@ -58,11 +56,12 @@ const codemodArgs = { '--pure-component=true', '--mixin-module-name="react-pure-render/mixin"', // Your days are numbered, pure-render-mixin! ], + 'react-proptypes': [ ...commonArgs[ 'react' ], '--transform=node_modules/react-codemod/transforms/React-PropTypes-to-prop-types.js', ], -} +}; module.exports = { codemodArgs, diff --git a/packages/calypso-codemods/tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap b/packages/calypso-codemods/tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap new file mode 100644 index 0000000000000..5d67afaf2f787 --- /dev/null +++ b/packages/calypso-codemods/tests/remove-create-reducer/__snapshots__/codemod.spec.js.snap @@ -0,0 +1,105 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`create-reducer.js 1`] = ` +"// This comment should be preserved even if the line below is removed. +import { withSchemaValidation, withoutPersistence } from 'state/utils'; + +const COMPUTED_IDENTIFIER = 'COMPUTED_IDENTIFIER'; + +const isFetchingSettings = withoutPersistence((state = false, action) => { + switch (action.type) { + case COMPUTED_IDENTIFIER: + return 'computed_id'; + case 'COMPUTED_STRING': + return state; + case \\"NON_COMPUTED_STRING\\": + return action.thing; + case \\"2\\": + return 2; + case \\"FUNCTION_HANDLER\\": + return function( s, a ) { + return s; + }(state, action); + case \\"ARROW_FUNCTION_HANDLER\\": + return state; + case \\"ARROW_FUNCTION_WITH_DESTRUCT\\": + { + const { thing } = action; + return thing; + } + case \\"VARIABLE_HANDLER\\": + return f(state, action); + } + + return state; +}); + +function f() { + return 'a function reducer'; +} + +const persistentReducer = (state = false, action) => { + switch (action.type) { + case COMPUTED_IDENTIFIER: + return \\"computed_id\\"; + case \\"SERIALIZE\\": + return state; + } + + return state; +}; + +persistentReducer.hasCustomPersistence = true; + +export const exportedPersistentReducer = (state = false, action) => { + switch (action.type) { + case COMPUTED_IDENTIFIER: + return \\"computed_id\\"; + case \\"SERIALIZE\\": + return state; + } + + return state; +}; + +exportedPersistentReducer.hasCustomPersistence = true; + +const persistentReducerArray = []; +reducerArray[0] = (state = false, action) => { + switch (action.type) { + case COMPUTED_IDENTIFIER: + return \\"computed_id\\"; + case \\"DESERIALIZE\\": + return state; + } + + return state; +}; + +reducerArray[0].hasCustomPersistence = true; + +const persistentReducerObj = { + key: // TODO: HANDLE PERSISTENCE + (state = false, action) => { + switch (action.type) { + case COMPUTED_IDENTIFIER: + return \\"computed_id\\"; + case \\"DESERIALIZE\\": + return state; + } + + return state; + } +}; + +const validatedReducer = withSchemaValidation(schema, (state = false, action) => { + switch (action.type) { + case COMPUTED_IDENTIFIER: + return \\"computed_id\\"; + } + + return state; +}); + +" +`; diff --git a/packages/calypso-codemods/tests/remove-create-reducer/codemod.spec.js b/packages/calypso-codemods/tests/remove-create-reducer/codemod.spec.js new file mode 100644 index 0000000000000..612212ec70d40 --- /dev/null +++ b/packages/calypso-codemods/tests/remove-create-reducer/codemod.spec.js @@ -0,0 +1 @@ +test_folder(__dirname); diff --git a/packages/calypso-codemods/tests/remove-create-reducer/create-reducer.js b/packages/calypso-codemods/tests/remove-create-reducer/create-reducer.js new file mode 100644 index 0000000000000..94a853adda9ba --- /dev/null +++ b/packages/calypso-codemods/tests/remove-create-reducer/create-reducer.js @@ -0,0 +1,48 @@ +// This comment should be preserved even if the line below is removed. +import { createReducer, createReducerWithValidation } from 'state/utils'; + +const COMPUTED_IDENTIFIER = 'COMPUTED_IDENTIFIER'; + +const isFetchingSettings = createReducer( false, { + [ COMPUTED_IDENTIFIER ]: () => 'computed_id', + [ 'COMPUTED_STRING' ]: state => state, + NON_COMPUTED_STRING: ( state, action ) => action.thing, + 2: () => 2, + FUNCTION_HANDLER: function( s, a ) { + return s; + }, + ARROW_FUNCTION_HANDLER: ( state, action ) => state, + ARROW_FUNCTION_WITH_DESTRUCT: ( state, { thing } ) => thing, + VARIABLE_HANDLER: f, +} ); + +function f() { + return 'a function reducer'; +} + +const persistentReducer = createReducer(false, { + [COMPUTED_IDENTIFIER]: () => "computed_id", + ["SERIALIZE"]: state => state, +}); + +export const exportedPersistentReducer = createReducer(false, { + [COMPUTED_IDENTIFIER]: () => "computed_id", + ["SERIALIZE"]: state => state, +}); + +const persistentReducerArray = []; +reducerArray[0] = createReducer(false, { + [COMPUTED_IDENTIFIER]: () => "computed_id", + ["DESERIALIZE"]: state => state, +}); + +const persistentReducerObj = { + key: createReducer(false, { + [COMPUTED_IDENTIFIER]: () => "computed_id", + ["DESERIALIZE"]: state => state, + }) +}; + +const validatedReducer = createReducerWithValidation(false, { + [COMPUTED_IDENTIFIER]: () => "computed_id", +}, schema); diff --git a/packages/calypso-codemods/transforms/remove-create-reducer.js b/packages/calypso-codemods/transforms/remove-create-reducer.js new file mode 100644 index 0000000000000..404109235efed --- /dev/null +++ b/packages/calypso-codemods/transforms/remove-create-reducer.js @@ -0,0 +1,292 @@ +function arrowFunctionBodyToCase(j, test, body) { + if (body.type === "BlockStatement") { + return j.switchCase(test, [body]); + } + return j.switchCase(test, [j.returnStatement(body)]); +} + +function getCases(j, handlerMap) { + let hasPersistence = false; + + const cases = handlerMap.properties.map(actionNode => { + const test = actionNode.computed + ? actionNode.key + : j.literal(actionNode.key.name || String(actionNode.key.value)); + const fn = actionNode.value; + + if ( + test.type === "Identifier" && + (test.name === "SERIALIZE" || test.name === "DESERIALIZE") + ) { + hasPersistence = true; + } + + if ( + test.type === "Literal" && + (test.value === "SERIALIZE" || test.value === "DESERIALIZE") + ) { + hasPersistence = true; + } + + // If it's an arrow function without parameters, just return the body. + if (fn.type === "ArrowFunctionExpression" && fn.params.length === 0) { + return arrowFunctionBodyToCase(j, test, fn.body); + } + + // If it's an arrow function with the right parameter names, just return the body. + if ( + fn.type === "ArrowFunctionExpression" && + fn.params[0].name === "state" && + (fn.params.length === 1 || + (fn.params.length === 2 && fn.params[1].name === "action")) + ) { + return arrowFunctionBodyToCase(j, test, fn.body); + } + + // If it's an arrow function with a deconstructed action, do magic. + if ( + fn.type === "ArrowFunctionExpression" && + fn.params[0].name === "state" && + (fn.params.length === 2 && fn.params[1].type === "ObjectPattern") + ) { + const declaration = j.variableDeclaration("const", [ + j.variableDeclarator(fn.params[1], j.identifier("action")) + ]); + const prevBody = + fn.body.type === "BlockStatement" + ? fn.body.body + : [j.returnStatement(fn.body)]; + const body = j.blockStatement([declaration, ...prevBody]); + return arrowFunctionBodyToCase(j, test, body); + } + + return j.switchCase(test, [ + j.returnStatement( + j.callExpression(actionNode.value, [ + j.identifier("state"), + j.identifier("action") + ]) + ) + ]); + }); + + return { cases, hasPersistence }; +} + +function handlePersistence(j, createReducerPath, newNode) { + const parent = createReducerPath.parentPath; + const grandParentValue = + parent && + parent.parentPath.value && + parent.parentPath.value.length === 1 && + parent.parentPath.value[0]; + const greatGrandParent = + grandParentValue && + parent && + parent.parentPath && + parent.parentPath.parentPath; + + if ( + parent && + grandParentValue && + greatGrandParent && + parent.value.type === "VariableDeclarator" && + grandParentValue.type === "VariableDeclarator" && + greatGrandParent.value.type === "VariableDeclaration" + ) { + const varName = parent.value.id.name; + const persistenceNode = j.expressionStatement( + j.assignmentExpression( + "=", + j.memberExpression( + j.identifier(varName), + j.identifier("hasCustomPersistence"), + false + ), + j.literal(true) + ) + ); + + if (greatGrandParent.parentPath.value.type === "ExportNamedDeclaration") { + // Handle `export const reducer = ...` case. + greatGrandParent.parentPath.insertAfter(persistenceNode); + } else { + // Handle `const reducer = ...` case. + greatGrandParent.insertAfter(persistenceNode); + } + } else if (parent && parent.value.type === "AssignmentExpression") { + const persistenceNode = j.expressionStatement( + j.assignmentExpression( + "=", + j.memberExpression( + parent.value.left, + j.identifier("hasCustomPersistence"), + false + ), + j.literal(true) + ) + ); + parent.parentPath.insertAfter(persistenceNode); + } else { + newNode.comments = newNode.comments || []; + newNode.comments.push( + j.commentLine(" TODO: HANDLE PERSISTENCE", true, false) + ); + } + + return newNode; +} + +export default function transformer(file, api) { + const j = api.jscodeshift; + + const root = j(file.source); + + let usedWithoutPersistence = false; + + // Handle createReducer + root + .find( + j.CallExpression, + node => + node.callee.type === "Identifier" && + node.callee.name === "createReducer" + ) + .forEach(createReducerPath => { + if (createReducerPath.value.arguments.length !== 2) { + throw new Error("Unable to translate createReducer"); + } + + const [defaultState, handlerMap] = createReducerPath.value.arguments; + + const { cases, hasPersistence } = getCases(j, handlerMap); + + let newNode = j.arrowFunctionExpression( + [ + j.assignmentPattern(j.identifier("state"), defaultState), + j.identifier("action") + ], + + j.blockStatement([ + j.switchStatement( + j.memberExpression(j.identifier("action"), j.identifier("type")), + cases + ), + j.returnStatement(j.identifier("state")) + ]) + ); + + if (hasPersistence) { + newNode = handlePersistence(j, createReducerPath, newNode); + } else { + usedWithoutPersistence = true; + newNode = j.callExpression(j.identifier("withoutPersistence"), [ + newNode + ]); + } + + createReducerPath.replace(newNode); + }); + + // Handle createReducerWithValidation + root + .find( + j.CallExpression, + node => + node.callee.type === "Identifier" && + node.callee.name === "createReducerWithValidation" + ) + .forEach(createReducerPath => { + if (createReducerPath.value.arguments.length !== 3) { + throw new Error("Unable to translate createReducerWithValidation"); + } + + const [ + defaultState, + handlerMap, + schema + ] = createReducerPath.value.arguments; + + const { cases } = getCases(j, handlerMap); + + const newNode = j.callExpression(j.identifier("withSchemaValidation"), [ + schema, + j.arrowFunctionExpression( + [ + j.assignmentPattern(j.identifier("state"), defaultState), + j.identifier("action") + ], + + j.blockStatement([ + j.switchStatement( + j.memberExpression(j.identifier("action"), j.identifier("type")), + cases + ), + j.returnStatement(j.identifier("state")) + ]) + ) + ]); + + createReducerPath.replace(newNode); + }); + + // Handle imports. + root + .find( + j.ImportDeclaration, + node => + node.specifiers && + node.specifiers.some( + s => + s && + s.imported && + (s.imported.name === "createReducer" || + s.imported.name === "createReducerWithValidation") + ) + ) + .forEach(nodePath => { + const filtered = nodePath.value.specifiers.filter( + s => + s.imported.name !== "createReducer" && + s.imported.name !== "createReducerWithValidation" + ); + + if ( + nodePath.value.specifiers.find( + s => s.imported.name === "createReducerWithValidation" + ) + ) { + if (!filtered.find(s => s.imported.name === "withSchemaValidation")) { + filtered.push( + j.importSpecifier( + j.identifier("withSchemaValidation"), + j.identifier("withSchemaValidation") + ) + ); + } + } + + if (usedWithoutPersistence) { + if (!filtered.find(s => s.imported.name === "withoutPersistence")) { + filtered.push( + j.importSpecifier( + j.identifier("withoutPersistence"), + j.identifier("withoutPersistence") + ) + ); + } + } + + if (filtered.length === 0) { + const { comments } = nodePath.node; + const { parentPath } = nodePath; + const nextNode = parentPath.value[nodePath.name + 1]; + j(nodePath).remove(); + nextNode.comments = comments; + } else { + nodePath.value.specifiers = filtered; + } + }); + + return root.toSource(); +}