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(
+ ,
+ 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);
+};