diff --git a/README.md b/README.md index 1e33eb9..7daab1b 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,52 @@ import { Modal } from 'antd'; }); ``` +### migrate your form v3 to v4 + +You can auto migrate your `v3 form` by `antd4-codemod src --migrateform`, when execute this command, you should insure your already upgrade `antd v4`, also recommend your already execute above command.This scripts can't migrate all incompatible `api`, so if your codes aren't standard code(like `Form Form.Item`).We recommend you execute the command migrate form file one by one.When a file migrate, you should check incompatible `api` +如果`Form.Item`不是标准的写法,譬如可能自己封装过为`FormItem`,使用`--formitem=FormItem`来指定新的`Form.Item`命名 +If `Form.Item` in your code is not a standard code, for example, alias `Form.Item` is `FormItem`, you can use `--formitem=FormItem` to rename `Form.Item` + +```diff + +- import { Form } from '@ant-design/compatible'; // remove compatible package +- import '@ant-design/compatible/assets/index.css'; // if not includes compatible package, remove css + + +-
+- +- {getFieldDecorator('username', { +- rules: [{ required: true, message: 'Please input your username!' }], +- initialValue: 'antd', +- })()} ++ ++ ++ + +
+- {getFieldDecorator('password', { +- initialValue: '123456', +- rules: [{ required: true, message: 'Please input your Password!' }], +- })()} ++ +
+ ; + +- export default Form.create({})(Input) // remove Form.create ++ export default Input; + +``` + ## License MIT diff --git a/README.zh-CN.md b/README.zh-CN.md index d9824c9..ca79d54 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -155,6 +155,53 @@ import { Modal } from 'antd'; }); ``` +### migrate your form v3 to v4 + +使用 `antd4-codemod src --migrateform`来自动迁移你的旧版`Form`,使用此命令时确保你已经升级到 v4,并且推荐已经执行上述其他迁移脚本。此脚本并不会帮你迁移所有不兼容的`api`,所以如果不是标准写法的话(`Form Form.Item`)推荐对用此命令对单个文件边迁移边及时修改不兼容`api`. +如果`Form.Item`不是标准的写法,譬如可能自己封装过为`FormItem`,使用`--formitem=FormItem`来指定新的`Form.Item`命名 + +```diff + +- import { Form } from '@ant-design/compatible'; // 去掉兼容包的导入 +- import '@ant-design/compatible/assets/index.css'; // 如果不在有兼容包,去除兼容包css + ++ import { Form } from 'antd'; + +-
+- +- {getFieldDecorator('username', { +- rules: [{ required: true, message: 'Please input your username!' }], +- initialValue: 'antd', +- })()} ++ ++ ++ + +
+- {getFieldDecorator('password', { +- initialValue: '123456', +- rules: [{ required: true, message: 'Please input your Password!' }], +- })()} ++ +
+ ; + + +- export default Form.create({})(Input) // 去除掉Form.create ++ export default Input; + +``` + ## License MIT diff --git a/bin/cli.js b/bin/cli.js index 781da42..5db6203 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -112,9 +112,11 @@ function getRunnerArgs( async function run(filePath, args = {}) { const extraScripts = args.extraScripts ? args.extraScripts.split(',') : []; - + const usedTransformers = args.migrateform + ? ['v3-Form-to-FieldForm'] + : transformers.concat(extraScripts); // eslint-disable-next-line no-restricted-syntax - for (const transformer of transformers.concat(extraScripts)) { + for (const transformer of usedTransformers) { // eslint-disable-next-line no-await-in-loop await transform(transformer, 'babylon', filePath, args); } diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/form-item.input.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/form-item.input.js new file mode 100644 index 0000000..9d1fe5f --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/form-item.input.js @@ -0,0 +1,19 @@ +import { Form } from 'antd'; + +const input = ; + +ReactDOM.render( +
+ + {getFieldDecorator('field1', { + rules: [{ required: true }] + })()} + + + {getFieldDecorator('field2', { + rules: [{ required: true }] + })(input)} + +
, + mountNode +); diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/form-item.output.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/form-item.output.js new file mode 100644 index 0000000..c199429 --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/form-item.output.js @@ -0,0 +1,15 @@ +import { Form } from 'antd'; + +const input = ; + +ReactDOM.render( +
+ + + + + {input} + +
, + mountNode +); diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/getFieldDecorator.input.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/getFieldDecorator.input.js new file mode 100644 index 0000000..b357044 --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/getFieldDecorator.input.js @@ -0,0 +1,19 @@ +import { Form } from 'antd'; + +const input = ; + +ReactDOM.render( +
+
+ {getFieldDecorator('field1', { + rules: [{ required: true }] + })()} +
+
+ {getFieldDecorator('field2', { + rules: [{ required: true }] + })(input)} +
+
, + mountNode +); diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/getFieldDecorator.output.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/getFieldDecorator.output.js new file mode 100644 index 0000000..6047aee --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/getFieldDecorator.output.js @@ -0,0 +1,15 @@ +import { Form } from 'antd'; + +const input = ; + +ReactDOM.render( +
+
+ +
+
+ {input} +
+
, + mountNode +); diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/remove-useless.input.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/remove-useless.input.js new file mode 100644 index 0000000..16dcd81 --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/remove-useless.input.js @@ -0,0 +1,18 @@ +import { Form, Icon as LegacyIcon } from '@ant-design/compatible'; + +class AntForm extends React.Component { + render() { + return ( +
+ + {getFieldDecorator('field1', { + rules: [{ required: true }], + initialValue: 'antd' + })()} + +
+ ); + } +} + +export default Form.create({})(AntForm); diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/remove-useless.output.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/remove-useless.output.js new file mode 100644 index 0000000..ca95579 --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/remove-useless.output.js @@ -0,0 +1,19 @@ +import { Form } from 'antd'; +import { Icon as LegacyIcon } from '@ant-design/compatible'; + +class AntForm extends React.Component { + render() { + return ( +
+ + + +
+ ); + } +} + +export default AntForm; diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/rename-formitem.input.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/rename-formitem.input.js new file mode 100644 index 0000000..7a136ec --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/rename-formitem.input.js @@ -0,0 +1,20 @@ +import { Form } from 'antd'; +import { FormItem } from 'components'; + +const input = ; + +ReactDOM.render( +
+ + {getFieldDecorator('field1', { + rules: [{ required: true }] + })()} + + + {getFieldDecorator('field2', { + rules: [{ required: true }] + })(input)} + +
, + mountNode +); diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/rename-formitem.output.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/rename-formitem.output.js new file mode 100644 index 0000000..248bbaf --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/rename-formitem.output.js @@ -0,0 +1,16 @@ +import { Form } from 'antd'; +import { FormItem } from 'components'; + +const input = ; + +ReactDOM.render( +
+ + + + + {input} + +
, + mountNode +); diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/wrapper-form.input.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/wrapper-form.input.js new file mode 100644 index 0000000..c9e61fc --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/wrapper-form.input.js @@ -0,0 +1,30 @@ +import { Form } from 'antd'; + +const input = ; + +const outerForm = ( + + {getFieldDecorator('outerform', { + initialValue: 'outer' + })()} + +); + +ReactDOM.render( +
+ + {getFieldDecorator('field1', { + rules: [{ required: true }], + initialValue: 'antd' + })()} + + + {getFieldDecorator(field2, { + rules: [{ required: true }], + initialValue: 'antd-1' + })(input)} + + {outerForm} +
, + mountNode +); diff --git a/transforms/__testfixtures__/v3-Form-to-FieldForm/wrapper-form.output.js b/transforms/__testfixtures__/v3-Form-to-FieldForm/wrapper-form.output.js new file mode 100644 index 0000000..f1f3c23 --- /dev/null +++ b/transforms/__testfixtures__/v3-Form-to-FieldForm/wrapper-form.output.js @@ -0,0 +1,27 @@ +import { Form } from 'antd'; + +const input = ; + +const outerForm = ( + + + +); + +ReactDOM.render( +
+ + + + + {input} + + {outerForm} +
, + mountNode +); diff --git a/transforms/__tests__/v3-Form-to-FieldForm.test.js b/transforms/__tests__/v3-Form-to-FieldForm.test.js new file mode 100644 index 0000000..98fb7da --- /dev/null +++ b/transforms/__tests__/v3-Form-to-FieldForm.test.js @@ -0,0 +1,39 @@ +jest.mock('../v3-Form-to-FieldForm', () => { + return Object.assign(require.requireActual('../v3-Form-to-FieldForm'), { + parser: 'babylon', + }); +}); + +const tests = [ + 'getFieldDecorator', + 'form-item', + 'remove-useless', + 'wrapper-form', + { + cmd: 'rename-formitem', + options: { + formitem: 'FormItem', + }, + }, +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +const testUnit = 'v3-Form-to-FieldForm'; + +describe(testUnit, () => { + tests.forEach(test => { + const cmd = typeof test === 'string' ? test : test.cmd; + return defineTest( + __dirname, + testUnit, + { + antdPkgNames: ['antd', '@forked/antd', '@alipay/bigfish/antd'].join( + ',', + ), + ...test.options, + }, + `${testUnit}/${cmd}`, + ); + }); +}); diff --git a/transforms/utils/index.js b/transforms/utils/index.js index 75cd893..4664ca7 100644 --- a/transforms/utils/index.js +++ b/transforms/utils/index.js @@ -212,6 +212,23 @@ function parseStrToArray(antdPkgNames) { .map(n => n.trim()); } +function removeUnusedCompatiblecss(j, root) { + const isCompatibleAllRemoved = !root.find(j.ImportDeclaration, { + source: { + value: '@ant-design/compatible', + }, + }).length; + if (isCompatibleAllRemoved) { + root + .find(j.ImportDeclaration, { + source: { + value: '@ant-design/compatible/assets/index.css', + }, + }) + .replaceWith(); + } +} + module.exports = { parseStrToArray, addModuleDefaultImport, @@ -219,4 +236,5 @@ module.exports = { addSubmoduleImport, removeEmptyModuleImport, useVar, + removeUnusedCompatiblecss, }; diff --git a/transforms/v3-Form-to-FieldForm.js b/transforms/v3-Form-to-FieldForm.js new file mode 100644 index 0000000..fe1fe2e --- /dev/null +++ b/transforms/v3-Form-to-FieldForm.js @@ -0,0 +1,280 @@ +const { printOptions } = require('./utils/config'); +const { + addSubmoduleImport, + removeEmptyModuleImport, + parseStrToArray, + removeUnusedCompatiblecss, +} = require('./utils'); + +function traverseForm(root, api) { + const j = api.jscodeshift; + root + .find(j.JSXElement, { + openingElement: { + name: { + type: 'JSXIdentifier', + name: 'Form', + }, + }, + }) + .forEach(path => { + let initialValue = path.node.openingElement.attributes.find( + att => att.name.name === 'initialValue', + ); + if (!initialValue) { + initialValue = j.jsxAttribute( + j.jsxIdentifier('initialValue'), + j.jsxExpressionContainer(j.objectExpression([])), + ); + path.node.openingElement.attributes.push(initialValue); + } + const state = { + formWrapperInitialValue: initialValue.value.expression.properties, + }; + traverseFormItem(root, api, state); + transformDecorator(root, api, state); + }); +} + +function traverseDecorator( + root, + api, + { outerFormItem, formWrapperInitialValue, formitem }, +) { + const j = api.jscodeshift; + const decorators = root.find(j.CallExpression, { + callee: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'getFieldDecorator', + }, + }, + }); + if (!decorators.length) { + return; + } + let form; + if (outerFormItem) { + form = outerFormItem; + } else { + const formOpen = j.jsxOpeningElement( + j.jsxIdentifier(formitem || 'Form.Item'), + [j.jsxAttribute(j.jsxIdentifier('noStyle'))], + false, + ); + const formClose = j.jsxClosingElement( + j.jsxIdentifier(formitem || 'Form.Item'), + ); + const children = []; + decorators.get(0).node.arguments.forEach(node => { + if (node.type === 'JSXElement') { + children.push(node); + } else { + children.push(j.jsxExpressionContainer(node)); + } + }); + form = j.jsxElement(formOpen, formClose, children, false); + } + decorators.replaceWith(path => { + const fieldName = path.node.callee.arguments[0]; + const options = path.node.callee.arguments[1]; + form.openingElement.attributes.push( + j.jsxAttribute( + j.jsxIdentifier('name'), + fieldName.type === 'StringLiteral' + ? fieldName + : j.jsxExpressionContainer(fieldName), + ), + ); + if (options && options.properties) { + options.properties.forEach(prop => { + if (prop.key.name === 'initialValue' && formWrapperInitialValue) { + const objectProperty = j.objectProperty( + fieldName.type === 'StringLiteral' + ? j.identifier(fieldName.value) + : fieldName, + prop.value, + ); + // computed prop waste 1 hours that I know computed can't pass params + // plz read https://github.com/benjamn/ast-types/blob/master/gen/builders.ts + if (fieldName.type !== 'StringLiteral') { + objectProperty.computed = true; + } + formWrapperInitialValue.push(objectProperty); + return; + } + form.openingElement.attributes.push( + j.jsxAttribute( + j.jsxIdentifier(prop.key.name), + j.jsxExpressionContainer(prop.value), + ), + ); + }); + } + return path.node.arguments; + }); + return form; +} + +function traverseFormItem(root, api, state) { + const j = api.jscodeshift; + const formAst = state.formitem + ? { + name: { + type: 'JSXIdentifier', + name: state.formitem, + }, + } + : { + name: { + type: 'JSXMemberExpression', + object: { + type: 'JSXIdentifier', + name: 'Form', + }, + property: { + type: 'JSXIdentifier', + name: 'Item', + }, + }, + }; + const formItems = root.find(j.JSXElement, { + openingElement: { + type: 'JSXOpeningElement', + ...formAst, + }, + }); + formItems.replaceWith(nodepath => { + const form = traverseDecorator(j(nodepath.node.children), api, { + outerFormItem: nodepath.node, + ...state, + }); + if (form) { + nodepath.node.children.forEach((child, index) => { + if ( + child.type === 'JSXExpressionContainer' && + child.expression.type === 'JSXElement' + ) { + nodepath.node.children[index] = child.expression; + } + }); + } + return nodepath.node; + }); +} + +function transformDecorator(root, api, state) { + const j = api.jscodeshift; + root + .find(j.JSXExpressionContainer, { + expression: { + callee: { + callee: { + type: 'Identifier', + name: 'getFieldDecorator', + }, + }, + }, + }) + .replaceWith(path => { + return traverseDecorator(j(path), api, state); + }); +} +function removeFormCreate(root, api) { + const j = api.jscodeshift; + root + .find(j.CallExpression, { + callee: { + type: 'CallExpression', + callee: { + object: { + type: 'Identifier', + name: 'Form', + }, + property: { + type: 'Identifier', + name: 'create', + }, + }, + }, + }) + .replaceWith(path => path.node.arguments[0]); +} + +function renameCompatibelForm(root, api) { + const j = api.jscodeshift; + const antdPkgNames = parseStrToArray('@ant-design/compatible'); + root + .find(j.Identifier) + .filter( + path => + path.node.name === 'Form' && + path.parent.node.type === 'ImportSpecifier' && + antdPkgNames.includes(path.parent.parent.node.source.value), + ) + .forEach(path => { + const importedComponentName = path.parent.node.imported.name; + const antdPkgName = path.parent.parent.node.source.value; + + // remove old imports + const importDeclaration = path.parent.parent.node; + importDeclaration.specifiers = importDeclaration.specifiers.filter( + specifier => + !specifier.imported || + specifier.imported.name !== importedComponentName, + ); + + // add new import from '@ant-design/compatible' + const localComponentName = path.parent.node.local.name; + addSubmoduleImport(j, root, { + moduleName: 'antd', + importedName: importedComponentName, + localName: localComponentName, + before: antdPkgName, + }); + }); + removeEmptyModuleImport(j, root, '@ant-design/compatible'); + removeUnusedCompatiblecss(j, root); +} + +function removeEmptyInitialValue(root, api) { + const j = api.jscodeshift; + root + .find(j.JSXElement, { + openingElement: { + name: { + type: 'JSXIdentifier', + name: 'Form', + }, + }, + }) + .replaceWith(path => { + const { openingElement } = path.node; + openingElement.attributes = openingElement.attributes.filter(attr => { + if ( + attr.name.name === 'initialValue' && + attr.value.expression.properties && + attr.value.expression.properties.length === 0 + ) { + return false; + } + return true; + }); + return path.node; + }); +} + +module.exports = (fileInfo, api, options) => { + const j = api.jscodeshift; + const root = j(fileInfo.source); + [ + renameCompatibelForm, + removeFormCreate, + traverseForm, + traverseFormItem, + transformDecorator, + removeEmptyInitialValue, + ].forEach(fn => fn(root, api, options)); + return root.toSource(options.printOptions || printOptions); +};