Han Solo}
38 | avatar={}
39 | content={
40 |
41 | We supply a series of design principles, practical patterns and high quality design
42 | resources (Sketch and Axure), to help people create their product prototypes beautifully
43 | and efficiently.
44 |
45 | }
46 | datetime={
47 |
48 | 8 hours ago
49 |
50 | }
51 | />
52 | null}
55 | title="Title"
56 | subTitle="This is a subtitle"
57 | />
58 |
59 | >
60 | );
61 | };
62 |
63 | export default App;
64 |
--------------------------------------------------------------------------------
/transforms/__testfixtures__/v5-removed-component-migration/basic.input.js:
--------------------------------------------------------------------------------
1 | import { Comment, PageHeader, BackTop } from 'antd';
2 | import { DislikeFilled, DislikeOutlined, LikeFilled, LikeOutlined } from '@ant-design/icons';
3 | import React, { createElement, useState } from 'react';
4 | const App = () => {
5 | const [likes, setLikes] = useState(0);
6 | const [dislikes, setDislikes] = useState(0);
7 | const [action, setAction] = useState(null);
8 | const like = () => {
9 | setLikes(1);
10 | setDislikes(0);
11 | setAction('liked');
12 | };
13 | const dislike = () => {
14 | setLikes(0);
15 | setDislikes(1);
16 | setAction('disliked');
17 | };
18 | const actions = [
19 |
20 |
21 | {createElement(action === 'liked' ? LikeFilled : LikeOutlined)}
22 | {likes}
23 |
24 | ,
25 |
26 |
27 | {React.createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)}
28 | {dislikes}
29 |
30 | ,
31 | Reply to,
32 | ];
33 | return (
34 | <>
35 | Han Solo}
38 | avatar={}
39 | content={
40 |
41 | We supply a series of design principles, practical patterns and high quality design
42 | resources (Sketch and Axure), to help people create their product prototypes beautifully
43 | and efficiently.
44 |
45 | }
46 | datetime={
47 |
48 | 8 hours ago
49 |
50 | }
51 | />
52 | null}
55 | title="Title"
56 | subTitle="This is a subtitle"
57 | />
58 |
59 | >
60 | );
61 | };
62 |
63 | export default App;
64 |
--------------------------------------------------------------------------------
/.github/workflows/ossar.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 |
6 | # This workflow integrates a collection of open source static analysis tools
7 | # with GitHub code scanning. For documentation, or to provide feedback, visit
8 | # https://github.com/github/ossar-action
9 | name: OSSAR
10 |
11 | on:
12 | push:
13 | branches: [ "main" ]
14 | pull_request:
15 | # The branches below must be a subset of the branches above
16 | branches: [ "main" ]
17 | schedule:
18 | - cron: '24 4 * * 0'
19 |
20 | permissions:
21 | contents: read
22 |
23 | jobs:
24 | OSSAR-Scan:
25 | # OSSAR runs on windows-latest.
26 | # ubuntu-latest and macos-latest support coming soon
27 | permissions:
28 | contents: read # for actions/checkout to fetch code
29 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
30 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
31 | runs-on: windows-latest
32 |
33 | steps:
34 | - name: Checkout repository
35 | uses: actions/checkout@v3
36 |
37 | # Ensure a compatible version of dotnet is installed.
38 | # The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201.
39 | # A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action.
40 | # GitHub hosted runners already have a compatible version of dotnet installed and this step may be skipped.
41 | # For self-hosted runners, ensure dotnet version 3.1.201 or later is installed by including this action:
42 | # - name: Install .NET
43 | # uses: actions/setup-dotnet@v2
44 | # with:
45 | # dotnet-version: '3.1.x'
46 |
47 | # Run open source static analysis tools
48 | - name: Run OSSAR
49 | uses: github/ossar-action@v1
50 | id: ossar
51 |
52 | # Upload results to the Security tab
53 | - name: Upload OSSAR results
54 | uses: github/codeql-action/upload-sarif@v2
55 | with:
56 | sarif_file: ${{ steps.ossar.outputs.sarifFile }}
57 |
--------------------------------------------------------------------------------
/transforms/__testfixtures__/v5-removed-component-migration/basic.output.js:
--------------------------------------------------------------------------------
1 | import { Comment } from '@ant-design/compatible';
2 | import { PageHeader } from '@ant-design/pro-layout';
3 | import { FloatButton } from 'antd';
4 | import { DislikeFilled, DislikeOutlined, LikeFilled, LikeOutlined } from '@ant-design/icons';
5 | import React, { createElement, useState } from 'react';
6 | const App = () => {
7 | const [likes, setLikes] = useState(0);
8 | const [dislikes, setDislikes] = useState(0);
9 | const [action, setAction] = useState(null);
10 | const like = () => {
11 | setLikes(1);
12 | setDislikes(0);
13 | setAction('liked');
14 | };
15 | const dislike = () => {
16 | setLikes(0);
17 | setDislikes(1);
18 | setAction('disliked');
19 | };
20 | const actions = [
21 |
22 |
23 | {createElement(action === 'liked' ? LikeFilled : LikeOutlined)}
24 | {likes}
25 |
26 | ,
27 |
28 |
29 | {React.createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)}
30 | {dislikes}
31 |
32 | ,
33 | Reply to,
34 | ];
35 | return (<>
36 | Han Solo}
39 | avatar={}
40 | content={
41 |
42 | We supply a series of design principles, practical patterns and high quality design
43 | resources (Sketch and Axure), to help people create their product prototypes beautifully
44 | and efficiently.
45 |
46 | }
47 | datetime={
48 |
49 | 8 hours ago
50 |
51 | }
52 | />
53 | null}
56 | title="Title"
57 | subTitle="This is a subtitle"
58 | />
59 |
60 | >);
61 | };
62 |
63 | export default App;
64 |
--------------------------------------------------------------------------------
/transforms/__testfixtures__/v5-removed-component-migration/alias-import.input.js:
--------------------------------------------------------------------------------
1 | import { Comment as AntdComment, Avatar, Tooltip, PageHeader, BackTop as Back } from 'antd';
2 | import { DislikeFilled, DislikeOutlined, LikeFilled, LikeOutlined } from '@ant-design/icons';
3 | import React, { createElement, useState } from 'react';
4 | const App = () => {
5 | const [likes, setLikes] = useState(0);
6 | const [dislikes, setDislikes] = useState(0);
7 | const [action, setAction] = useState(null);
8 | const like = () => {
9 | setLikes(1);
10 | setDislikes(0);
11 | setAction('liked');
12 | };
13 | const dislike = () => {
14 | setLikes(0);
15 | setDislikes(1);
16 | setAction('disliked');
17 | };
18 | const actions = [
19 |
20 |
21 | {createElement(action === 'liked' ? LikeFilled : LikeOutlined)}
22 | {likes}
23 |
24 | ,
25 |
26 |
27 | {React.createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)}
28 | {dislikes}
29 |
30 | ,
31 | Reply to,
32 | ];
33 | return (
34 | <>
35 | Han Solo}
38 | avatar={}
39 | content={
40 |
41 | We supply a series of design principles, practical patterns and high quality design
42 | resources (Sketch and Axure), to help people create their product prototypes beautifully
43 | and efficiently.
44 |
45 | }
46 | datetime={
47 |
48 | 8 hours ago
49 |
50 | }
51 | />
52 | null}
55 | title="Title"
56 | subTitle="This is a subtitle"
57 | />
58 |
59 | >
60 | );
61 | };
62 |
63 | export default App;
64 |
--------------------------------------------------------------------------------
/transforms/__testfixtures__/v5-removed-component-migration/empty.output.js:
--------------------------------------------------------------------------------
1 | import { Comment } from '@ant-design/compatible';
2 | import { PageHeader } from '@ant-design/pro-layout';
3 | import { DislikeFilled, DislikeOutlined, LikeFilled, LikeOutlined } from '@ant-design/icons';
4 | import React, { createElement, useState } from 'react';
5 | const App = () => {
6 | const [likes, setLikes] = useState(0);
7 | const [dislikes, setDislikes] = useState(0);
8 | const [action, setAction] = useState(null);
9 | const like = () => {
10 | setLikes(1);
11 | setDislikes(0);
12 | setAction('liked');
13 | };
14 | const dislike = () => {
15 | setLikes(0);
16 | setDislikes(1);
17 | setAction('disliked');
18 | };
19 | const actions = [
20 |
21 |
22 | {createElement(action === 'liked' ? LikeFilled : LikeOutlined)}
23 | {likes}
24 |
25 | ,
26 |
27 |
28 | {React.createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)}
29 | {dislikes}
30 |
31 | ,
32 | Reply to,
33 | ];
34 | return (
35 | <>
36 | Han Solo}
39 | avatar={}
40 | content={
41 |
42 | We supply a series of design principles, practical patterns and high quality design
43 | resources (Sketch and Axure), to help people create their product prototypes beautifully
44 | and efficiently.
45 |
46 | }
47 | datetime={
48 |
49 | 8 hours ago
50 |
51 | }
52 | />
53 | null}
56 | title="Title"
57 | subTitle="This is a subtitle"
58 | />
59 |
60 | >
61 | );
62 | };
63 |
64 | export default App;
65 |
--------------------------------------------------------------------------------
/transforms/__testfixtures__/v5-removed-component-migration/alias-import.output.js:
--------------------------------------------------------------------------------
1 | import { Comment as AntdComment } from '@ant-design/compatible';
2 | import { PageHeader } from '@ant-design/pro-layout';
3 | import { Avatar, FloatButton, Tooltip } from 'antd';
4 | import { DislikeFilled, DislikeOutlined, LikeFilled, LikeOutlined } from '@ant-design/icons';
5 | import React, { createElement, useState } from 'react';
6 | const App = () => {
7 | const [likes, setLikes] = useState(0);
8 | const [dislikes, setDislikes] = useState(0);
9 | const [action, setAction] = useState(null);
10 | const like = () => {
11 | setLikes(1);
12 | setDislikes(0);
13 | setAction('liked');
14 | };
15 | const dislike = () => {
16 | setLikes(0);
17 | setDislikes(1);
18 | setAction('disliked');
19 | };
20 | const actions = [
21 |
22 |
23 | {createElement(action === 'liked' ? LikeFilled : LikeOutlined)}
24 | {likes}
25 |
26 | ,
27 |
28 |
29 | {React.createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)}
30 | {dislikes}
31 |
32 | ,
33 | Reply to,
34 | ];
35 | return (<>
36 | Han Solo}
39 | avatar={}
40 | content={
41 |
42 | We supply a series of design principles, practical patterns and high quality design
43 | resources (Sketch and Axure), to help people create their product prototypes beautifully
44 | and efficiently.
45 |
46 | }
47 | datetime={
48 |
49 | 8 hours ago
50 |
51 | }
52 | />
53 | null}
56 | title="Title"
57 | subTitle="This is a subtitle"
58 | />
59 |
60 | >);
61 | };
62 |
63 | export default App;
64 |
--------------------------------------------------------------------------------
/transforms/v5-removed-component-migration.js:
--------------------------------------------------------------------------------
1 | const {
2 | addSubmoduleImport,
3 | removeEmptyModuleImport,
4 | parseStrToArray,
5 | } = require('./utils');
6 | const { printOptions } = require('./utils/config');
7 | const { markDependency } = require('./utils/marker');
8 |
9 | const removedComponentConfig = {
10 | Comment: {
11 | importSource: '@ant-design/compatible',
12 | },
13 | PageHeader: {
14 | importSource: '@ant-design/pro-layout',
15 | },
16 | BackTop: {
17 | importSource: 'antd',
18 | rename: 'FloatButton.BackTop',
19 | },
20 | };
21 |
22 | module.exports = (file, api, options) => {
23 | const j = api.jscodeshift;
24 | const root = j(file.source);
25 | const antdPkgNames = parseStrToArray(options.antdPkgNames || 'antd');
26 |
27 | // import { Comment } from '@ant-design/compatible'
28 | // import { PageHeader } from '@ant-design/pro-components'
29 | function importDeprecatedComponent(j, root) {
30 | let hasChanged = false;
31 |
32 | // import { Comment, PageHeader } from 'antd';
33 | // import { Comment, PageHeader } from '@forked/antd';
34 | const componentNameList = Object.keys(removedComponentConfig);
35 |
36 | root
37 | .find(j.Identifier)
38 | .filter(
39 | path =>
40 | componentNameList.includes(path.node.name) &&
41 | path.parent.node.type === 'ImportSpecifier' &&
42 | antdPkgNames.includes(path.parent.parent.node.source.value),
43 | )
44 | .forEach(path => {
45 | hasChanged = true;
46 | const importedComponentName = path.parent.node.imported.name;
47 | const antdPkgName = path.parent.parent.node.source.value;
48 |
49 | // remove old imports
50 | const importDeclaration = path.parent.parent.node;
51 | importDeclaration.specifiers = importDeclaration.specifiers.filter(
52 | specifier =>
53 | !specifier.imported ||
54 | specifier.imported.name !== importedComponentName,
55 | );
56 |
57 | // add new import from '@ant-design/compatible'
58 | const localComponentName = path.parent.node.local.name;
59 | const importConfig = removedComponentConfig[importedComponentName];
60 | if (importConfig.rename) {
61 | if (importConfig.rename.includes('.')) {
62 | // `FloatButton.BackTop`
63 | const [
64 | newComponentName,
65 | compoundComponentName,
66 | ] = importConfig.rename.split('.');
67 | // import part
68 | const importedLocalName = addSubmoduleImport(j, root, {
69 | moduleName: importConfig.importSource,
70 | importedName: newComponentName,
71 | before: antdPkgName,
72 | });
73 | // rename part
74 | root
75 | .find(j.JSXElement, {
76 | openingElement: {
77 | name: { name: localComponentName },
78 | },
79 | })
80 | .forEach(path => {
81 | path.node.openingElement.name = j.jsxMemberExpression(
82 | j.jsxIdentifier(importedLocalName),
83 | j.jsxIdentifier(compoundComponentName),
84 | );
85 | });
86 | }
87 | } else {
88 | addSubmoduleImport(j, root, {
89 | moduleName: importConfig.importSource,
90 | importedName: importedComponentName,
91 | localName: localComponentName,
92 | before: antdPkgName,
93 | });
94 | markDependency(importConfig.importSource);
95 | }
96 | });
97 |
98 | return hasChanged;
99 | }
100 |
101 | // step1. import deprecated components from '@ant-design/compatible'
102 | // step2. cleanup antd import if empty
103 | let hasChanged = false;
104 | hasChanged = importDeprecatedComponent(j, root) || hasChanged;
105 |
106 | if (hasChanged) {
107 | antdPkgNames.forEach(antdPkgName => {
108 | removeEmptyModuleImport(j, root, antdPkgName);
109 | });
110 | }
111 |
112 | return hasChanged
113 | ? root.toSource(options.printOptions || printOptions)
114 | : null;
115 | };
116 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | 简体中文
2 |
3 | # Ant Design 5 Codemod
4 |
5 | 一组帮助你升级到 antd 5 的 codemod 脚本集合,基于 [jscodeshift](https://github.com/facebook/jscodeshift) 和 and [postcss](https://github.com/postcss/postcss) 构建。(受 [react-codemod](https://github.com/reactjs/react-codemod) 启发)
6 |
7 | [](https://npmjs.org/package/@ant-design/codemod-v5) [](https://npmjs.org/package/@ant-design/codemod-v5) [](https://github.com/ant-design/codemod-v5/actions/workflows/test.yml)
8 |
9 | ## 使用
10 |
11 | 在运行 codemod 脚本前,请先提交你的本地代码修改。
12 |
13 | ```shell
14 | # 使用 npx 直接运行
15 | npx -p @ant-design/codemod-v5 antd5-codemod src
16 | # 或者使用 pnpm 直接运行
17 | pnpm --package=@ant-design/codemod-v5 dlx antd5-codemod src
18 | ```
19 |
20 | ## Codemod 脚本包括:
21 |
22 | #### `v5-removed-component-migration`
23 |
24 | Replace import for removed component in v5.
25 |
26 | - 将 `Comment` 改为从 `@ant-design/compatible` import.
27 | - 将 `PageHeader` 改为从 `@ant-design/pro-layout` import.
28 | - 从 `FloatButton.BackTop` 组件中导入并使用 `BackTop`.
29 |
30 | ```diff
31 | - import { Avatar, BackTop, Comment, PageHeader } from 'antd';
32 | + import { Comment } from '@ant-design/compatible';
33 | + import { PageHeader } from '@ant-design/pro-layout';
34 | + import { Avatar, FloatButton } from 'antd';
35 |
36 | ReactDOM.render( (
37 |
38 |
null}
41 | title="Title"
42 | subTitle="This is a subtitle"
43 | />
44 | Han Solo}
47 | avatar={}
48 | content={
49 |
50 | We supply a series of design principles, practical patterns and high quality design
51 | resources (Sketch and Axure), to help people create their product prototypes beautifully
52 | and efficiently.
53 |
54 | }
55 | datetime={
56 | 8 hours ago
57 | }
58 | />
59 | -
60 | +
61 |
62 | );
63 | ```
64 |
65 | #### `v5-props-changed-migration`
66 |
67 | 将 v4 中部分 props 用法迁移到 v5 版本.
68 |
69 | ```diff
70 | import { Tag, Modal, Slider } from 'antd';
71 |
72 | const App = () => {
73 | const [visible, setVisible] = useState(false);
74 |
75 | return (
76 | <>
77 | -
80 | + {visible ? : null}
81 |
85 | -
86 | +
87 | >
88 | );
89 | };
90 | ```
91 |
92 | #### `v5-removed-static-method-migration`
93 |
94 | * 替换 `message.warn` 为 `message.warning`。
95 | * 替换 `notification.close` 为 `notification.destroy`。
96 |
97 | ```diff
98 | import { message, notification } from 'antd';
99 |
100 | const App = () => {
101 | const [messageApi, contextHolder] = message.useMessage();
102 | const onClick1 = () => {
103 | - message.warn();
104 | + message.warning();
105 | }
106 | const onClick2 = () => {
107 | - messageApi.warn();
108 | + messageApi.warning();
109 | };
110 |
111 | const [notificationApi] = notification.useNotification();
112 | const onClick3 = () => {
113 | - notification.close();
114 | + notification.destroy();
115 | }
116 | const onClick4 = () => {
117 | - notificationApi.close();
118 | + notificationApi.destroy();
119 | };
120 |
121 | return <>{contextHolder}>;
122 | };
123 | ```
124 |
125 | #### `v5-remove-style-import`
126 |
127 | 注释掉 js 文件中的 antd 样式文件导入。
128 |
129 | ```diff
130 | - import 'antd/es/auto-complete/style';
131 | - import 'antd/lib/button/style/index.less';
132 | - import 'antd/dist/antd.compact.min.css';
133 | + // import 'antd/es/auto-complete/style';
134 | + // import 'antd/lib/button/style/index.less';
135 | + // import 'antd/dist/antd.compact.min.css';
136 | ```
137 |
138 | #### `Remove Antd Less` in less file
139 |
140 | 注释掉 less 文件中的 antd 样式文件导入。
141 |
142 | ```diff
143 | - @import (reference) '~antd/dist/antd.less';
144 | - @import '~antd/es/button/style/index.less';
145 | + /* @import (reference) '~antd/dist/antd.less'; */
146 | + /* @import '~antd/es/button/style/index.less'; */
147 | @import './styles.less';
148 |
149 | body {
150 | font-size: 14px;
151 | }
152 | ```
153 |
154 | ## Development
155 |
156 | ```bash
157 | npm run release
158 | npm publish
159 | ```
160 |
161 | ## License
162 |
163 | MIT
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | English | [简体中文](./README.zh-CN.md)
2 |
3 | # Ant Design v5 Codemod
4 |
5 | A collection of codemod scripts that help upgrade antd v5 using [jscodeshift](https://github.com/facebook/jscodeshift) and [postcss](https://github.com/postcss/postcss).(Inspired by [react-codemod](https://github.com/reactjs/react-codemod))
6 |
7 | [](https://npmjs.org/package/@ant-design/codemod-v5) [](https://npmjs.org/package/@ant-design/codemod-v5) [](https://github.com/ant-design/codemod-v5/actions/workflows/test.yml)
8 |
9 | ## Usage
10 |
11 | Before run codemod scripts, you'd better make sure to commit your local git changes firstly.
12 |
13 | ```shell
14 | # Run directly through npx
15 | npx -p @ant-design/codemod-v5 antd5-codemod src
16 |
17 | # Or run directly through pnpm
18 | pnpm --package=@ant-design/codemod-v5 dlx antd5-codemod src
19 | ```
20 |
21 | ## Codemod scripts introduction
22 |
23 | #### `v5-removed-component-migration`
24 |
25 | Replace import for removed component in v5.
26 |
27 | - Change `Comment` import from `@ant-design/compatible`.
28 | - Change `PageHeader` import from `@ant-design/pro-layout`.
29 | - Use `BackTop` from `FloatButton.BackTop`.
30 |
31 | ```diff
32 | - import { Avatar, BackTop, Comment, PageHeader } from 'antd';
33 | + import { Comment } from '@ant-design/compatible';
34 | + import { PageHeader } from '@ant-design/pro-layout';
35 | + import { Avatar, FloatButton } from 'antd';
36 |
37 | ReactDOM.render( (
38 |
39 |
null}
42 | title="Title"
43 | subTitle="This is a subtitle"
44 | />
45 | Han Solo}
48 | avatar={}
49 | content={
50 |
51 | We supply a series of design principles, practical patterns and high quality design
52 | resources (Sketch and Axure), to help people create their product prototypes beautifully
53 | and efficiently.
54 |
55 | }
56 | datetime={
57 | 8 hours ago
58 | }
59 | />
60 | -
61 | +
62 |
63 | );
64 | ```
65 |
66 | #### `v5-props-changed-migration`
67 |
68 | Change props usage from v4 to v5.
69 |
70 | ```diff
71 | import { Tag, Modal, Slider } from 'antd';
72 |
73 | const App = () => {
74 | const [visible, setVisible] = useState(false);
75 |
76 | return (
77 | <>
78 | -
81 | + {visible ? : null}
82 |
86 | -
87 | +
88 | >
89 | );
90 | };
91 | ```
92 |
93 | #### `v5-removed-static-method-migration`
94 |
95 | * Replace `message.warn` with `message.warning`.
96 | * Replace `notification.close` with `notification.destroy`.
97 |
98 | ```diff
99 | import { message, notification } from 'antd';
100 |
101 | const App = () => {
102 | const [messageApi, contextHolder] = message.useMessage();
103 | const onClick1 = () => {
104 | - message.warn();
105 | + message.warning();
106 | }
107 | const onClick2 = () => {
108 | - messageApi.warn();
109 | + messageApi.warning();
110 | };
111 |
112 | const [notificationApi] = notification.useNotification();
113 | const onClick3 = () => {
114 | - notification.close();
115 | + notification.destroy();
116 | }
117 | const onClick4 = () => {
118 | - notificationApi.close();
119 | + notificationApi.destroy();
120 | };
121 |
122 | return <>{contextHolder}>;
123 | };
124 | ```
125 |
126 | #### `v5-remove-style-import`
127 |
128 | Comment out the style file import from antd (in js file).
129 |
130 | ```diff
131 | - import 'antd/es/auto-complete/style';
132 | - import 'antd/lib/button/style/index.less';
133 | - import 'antd/dist/antd.compact.min.css';
134 | + // import 'antd/es/auto-complete/style';
135 | + // import 'antd/lib/button/style/index.less';
136 | + // import 'antd/dist/antd.compact.min.css';
137 | ```
138 |
139 | #### `Remove Antd Less` in less file
140 |
141 | Comment out the style file import from antd in less file.
142 |
143 | ```diff
144 | - @import (reference) '~antd/dist/antd.less';
145 | - @import '~antd/es/button/style/index.less';
146 | + /* @import (reference) '~antd/dist/antd.less'; */
147 | + /* @import '~antd/es/button/style/index.less'; */
148 | @import './styles.less';
149 |
150 | body {
151 | font-size: 14px;
152 | }
153 | ```
154 |
155 | ## Development
156 |
157 | ```bash
158 | npm run release
159 | npm publish
160 | ```
161 |
162 | ## License
163 |
164 | MIT
165 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
6 | // "lib": [], /* Specify library files to be included in the compilation. */
7 | "allowJs": true /* Allow javascript files to be compiled. */,
8 | // "checkJs": true, /* Report errors in .js files. */
9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
10 | "declaration": true /* Generates corresponding '.d.ts' file. */,
11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
12 | // "sourceMap": true, /* Generates corresponding '.map' file. */
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | "outDir": "lib" /* Redirect output structure to the directory. */,
15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | // "composite": true, /* Enable project compilation */
17 | // "removeComments": true, /* Do not emit comments to output. */
18 | // "noEmit": true, /* Do not emit outputs. */
19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22 |
23 | /* Strict Type-Checking Options */
24 | "strict": true /* Enable all strict type-checking options. */,
25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
26 | // "strictNullChecks": true, /* Enable strict null checks. */
27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
31 |
32 | /* Additional Checks */
33 | // "noUnusedLocals": true, /* Report errors on unused locals. */
34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
37 |
38 | /* Module Resolution Options */
39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
43 | // "typeRoots": [], /* List of folders to include type definitions from. */
44 | // "types": [], /* Type declaration files to be included in compilation. */
45 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
46 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
48 |
49 | /* Source Map Options */
50 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
51 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
54 |
55 | /* Experimental Options */
56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
58 |
59 | /* Advanced Options */
60 | // "declarationDir": "lib" /* Output directory for generated declaration files. */
61 | },
62 | "include": ["./transforms", "less-transforms"],
63 | "exclude": ["less-transforms/__tests__", "__testfixtures__"]
64 | }
65 |
--------------------------------------------------------------------------------
/transforms/v5-removed-static-method-migration.js:
--------------------------------------------------------------------------------
1 | const { parseStrToArray } = require('./utils');
2 | const { printOptions } = require('./utils/config');
3 | const { findAllAssignedNames } = require('./utils/ast');
4 |
5 | const changedApiMap = {
6 | message: {
7 | hook: 'useMessage',
8 | replace: {
9 | 'warn': 'warning',
10 | },
11 | },
12 | notification: {
13 | hook: 'useNotification',
14 | replace: {
15 | 'close': 'destroy',
16 | },
17 | },
18 | };
19 |
20 | module.exports = (file, api, options) => {
21 | const j = api.jscodeshift;
22 | const root = j(file.source);
23 | const antdPkgNames = parseStrToArray(options.antdPkgNames || 'antd');
24 |
25 | function replaceDeconstructionAssignCall(msgApiName, importedName) {
26 | const changedApiConfig = changedApiMap[importedName]?.replace;
27 | if (changedApiConfig) {
28 | Object.entries(changedApiConfig).forEach(([oldName, newName]) => {
29 | // msgApi.warn => msgApi.warning
30 | root.find(j.CallExpression, {
31 | callee: {
32 | type: 'MemberExpression',
33 | object: {
34 | type: 'Identifier',
35 | name: msgApiName,
36 | },
37 | property: {
38 | type: 'Identifier',
39 | name: oldName,
40 | },
41 | },
42 | }).forEach(path => {
43 | path.node.callee.property.name = newName;
44 | });
45 | });
46 | }
47 | }
48 |
49 | function replaceCallWithIndexZero(aliasedApiName, importedName) {
50 | const changedApiConfig = changedApiMap[importedName]?.replace;
51 | if (changedApiConfig) {
52 | Object.entries(changedApiConfig).forEach(([_, newName]) => {
53 | // msgNamespace[0].warn => msgNamespace[0].warning
54 | root.find(j.CallExpression, {
55 | callee: {
56 | type: 'MemberExpression',
57 | object: {
58 | type: 'MemberExpression',
59 | object: {
60 | type: 'Identifier',
61 | name: aliasedApiName,
62 | },
63 | property: {
64 | type: 'NumericLiteral',
65 | value: 0,
66 | },
67 | },
68 | },
69 | }).forEach(path => {
70 | path.node.callee.property.name = newName;
71 | });
72 | });
73 | }
74 | }
75 |
76 | function replaceMessageCall(path, importedName) {
77 | // const [messageApi, contextHolder] = messageAlias;
78 | if (path.node.id.type === 'ArrayPattern') {
79 | const msgApiName = path.node.id.elements[0].name;
80 | // 处理反复 reassign 的场景
81 | const localAssignedNames = findAllAssignedNames(
82 | root, j,
83 | msgApiName,
84 | [msgApiName],
85 | );
86 |
87 | localAssignedNames.forEach(apiName => {
88 | replaceDeconstructionAssignCall(apiName, importedName);
89 | hasChanged = true;
90 | });
91 | } else if (path.node.id.type === 'Identifier') {
92 | // const msg = msg;
93 | // 处理反复 reassign 的场景
94 | const msgName = path.node.id.name;
95 | const localAssignedNames = findAllAssignedNames(
96 | root, j,
97 | msgName,
98 | [msgName],
99 | );
100 |
101 | localAssignedNames.forEach(apiName => {
102 | // const [api] = msg;
103 | root.find(j.VariableDeclarator, {
104 | init: {
105 | type: 'Identifier',
106 | name: apiName,
107 | },
108 | }).forEach(path => {
109 | replaceMessageCall(path, importedName);
110 | hasChanged = true;
111 | });
112 |
113 | replaceCallWithIndexZero(apiName, importedName);
114 | });
115 | }
116 | }
117 |
118 | // rename old Message.warn() calls to Message.warning()
119 | function renameMessageWarnMethodCalls(j, root) {
120 | let hasChanged = false;
121 | root
122 | .find(j.Identifier)
123 | .filter(
124 | path =>
125 | Object.keys(changedApiMap).includes(path.node.name) &&
126 | path.parent.node.type === 'ImportSpecifier' &&
127 | antdPkgNames.includes(path.parent.parent.node.source.value),
128 | )
129 | .forEach(path => {
130 | const importedName = path.parent.node.imported.name;
131 | const localComponentName = path.parent.node.local.name;
132 | // message.warn -> message.warning
133 | replaceDeconstructionAssignCall(localComponentName, importedName);
134 | hasChanged = true;
135 |
136 | // useMessage called()
137 | // const [messageApi, contextHolder] = message.useMessage();
138 | // const msg = message.useMessage();
139 | const hook = changedApiMap[importedName]?.hook;
140 | if (hook) {
141 | root.find(j.VariableDeclarator, {
142 | init: {
143 | type: 'CallExpression',
144 | callee: {
145 | type: 'MemberExpression',
146 | property: {
147 | type: 'Identifier',
148 | name: hook,
149 | },
150 | },
151 | },
152 | }).forEach(path => {
153 | replaceMessageCall(path, importedName);
154 | });
155 | }
156 | });
157 |
158 | return hasChanged;
159 | }
160 |
161 | // step1. // rename old Model.method() calls
162 | // step2. cleanup antd import if empty
163 | let hasChanged = false;
164 | hasChanged = renameMessageWarnMethodCalls(j, root) || hasChanged;
165 |
166 | return hasChanged
167 | ? root.toSource(options.printOptions || printOptions)
168 | : null;
169 | };
170 |
--------------------------------------------------------------------------------
/transforms/utils/index.js:
--------------------------------------------------------------------------------
1 | // some utils extract from https://github.com/reactjs/react-codemod
2 | function insertImportAfter(j, root, { importStatement, afterModule }) {
3 | const firstAfterModuleImport = root
4 | .find(j.ImportDeclaration, {
5 | source: { value: afterModule },
6 | })
7 | .at(0);
8 |
9 | if (firstAfterModuleImport.paths()[0]) {
10 | firstAfterModuleImport.insertAfter(importStatement);
11 | } else {
12 | // 保留首行的注释
13 | // https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md
14 | const firstNode = getFirstNode(j);
15 | const { comments } = firstNode;
16 | if (comments) {
17 | delete firstNode.comments;
18 | importStatement.comments = comments;
19 | }
20 |
21 | // insert `import` at body(0)
22 | root.get().node.program.body.unshift(importStatement);
23 | }
24 | }
25 |
26 | function insertImportBefore(j, root, { importStatement, beforeModule }) {
27 | const firstBeforeModuleImport = root
28 | .find(j.ImportDeclaration, {
29 | source: { value: beforeModule },
30 | })
31 | .at(0);
32 |
33 | const firstNode = getFirstNode(j, root);
34 | if (
35 | (firstBeforeModuleImport.paths()[0] &&
36 | firstBeforeModuleImport.paths()[0].node === firstNode) ||
37 | !firstBeforeModuleImport
38 | ) {
39 | // 保留首行的注释
40 | // https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md
41 | const { comments } = firstNode;
42 | if (comments) {
43 | delete firstNode.comments;
44 | importStatement.comments = comments;
45 | }
46 | }
47 |
48 | if (firstBeforeModuleImport) {
49 | firstBeforeModuleImport.insertBefore(importStatement);
50 | } else {
51 | // insert `import` at body(0)
52 | root.get().node.program.body.unshift(importStatement);
53 | }
54 | }
55 |
56 | function hasSubmoduleImport(j, root, moduleName, submoduleName) {
57 | const collections = root
58 | .find(j.ImportDeclaration, {
59 | source: {
60 | value: moduleName,
61 | },
62 | })
63 | .find(j.ImportSpecifier, {
64 | imported: {
65 | name: submoduleName,
66 | },
67 | });
68 |
69 | const targetImport = collections.paths();
70 |
71 | // local 默认设置为 null
72 | return (
73 | targetImport[0]?.value?.local?.name || targetImport[0]?.value?.imported.name
74 | );
75 | }
76 |
77 | function hasModuleImport(j, root, moduleName) {
78 | return (
79 | root.find(j.ImportDeclaration, {
80 | source: { value: moduleName },
81 | }).length > 0
82 | );
83 | }
84 |
85 | function hasModuleDefaultImport(j, root, pkgName, localModuleName) {
86 | let found = false;
87 | root
88 | .find(j.ImportDeclaration, {
89 | source: { value: pkgName },
90 | })
91 | .forEach(nodePath => {
92 | const defaultImport = nodePath.node.specifiers.filter(
93 | n =>
94 | n.type === 'ImportDefaultSpecifier' &&
95 | n.local.name === localModuleName,
96 | );
97 | if (defaultImport.length) {
98 | found = true;
99 | }
100 | });
101 | return found;
102 | }
103 |
104 | function removeEmptyModuleImport(j, root, moduleName) {
105 | root
106 | .find(j.ImportDeclaration)
107 | .filter(
108 | path =>
109 | path.node.specifiers.length === 0 &&
110 | path.node.source.value === moduleName,
111 | )
112 | .replaceWith();
113 | }
114 |
115 | // Program uses var keywords
116 | function useVar(j, root) {
117 | return root.find(j.VariableDeclaration, { kind: 'const' }).length === 0;
118 | }
119 |
120 | function getFirstNode(j, root) {
121 | return root.find(j.Program).get('body', 0).node;
122 | }
123 |
124 | function addModuleImport(j, root, { pkgName, importSpecifier, before }) {
125 | // if has module imported, just import new submodule from existed
126 | // else just create a new import
127 | if (hasModuleImport(j, root, pkgName)) {
128 | root
129 | .find(j.ImportDeclaration, {
130 | source: { value: pkgName },
131 | })
132 | .at(0)
133 | .replaceWith(({ node }) => {
134 | const mergedImportSpecifiers = node.specifiers
135 | .concat(importSpecifier)
136 | .sort((a, b) => {
137 | if (a.type === 'ImportDefaultSpecifier') {
138 | return -1;
139 | }
140 |
141 | if (b.type === 'ImportDefaultSpecifier') {
142 | return 1;
143 | }
144 |
145 | return a.imported.name.localeCompare(b.imported.name);
146 | });
147 | return j.importDeclaration(mergedImportSpecifiers, j.literal(pkgName));
148 | });
149 | return true;
150 | }
151 |
152 | const importStatement = j.importDeclaration(
153 | [importSpecifier],
154 | j.literal(pkgName),
155 | );
156 |
157 | insertImportBefore(j, root, { importStatement, beforeModule: before });
158 | return true;
159 | }
160 |
161 | // add default import before one module
162 | function addModuleDefaultImport(j, root, { moduleName, localName, before }) {
163 | if (hasModuleDefaultImport(j, root, moduleName, localName)) {
164 | return;
165 | }
166 |
167 | // DefaultImportSpecifier
168 | const importSpecifier = j.importDefaultSpecifier(j.identifier(localName));
169 |
170 | if (
171 | addModuleImport(j, root, { pkgName: moduleName, importSpecifier, before })
172 | ) {
173 | return;
174 | }
175 |
176 | throw new Error(`No ${moduleName} import found!`);
177 | }
178 |
179 | // add submodule import before one module
180 | function addSubmoduleImport(
181 | j,
182 | root,
183 | { moduleName, importedName, localName, before },
184 | ) {
185 | const importedLocalName = hasSubmoduleImport(
186 | j,
187 | root,
188 | moduleName,
189 | importedName,
190 | );
191 | if (importedLocalName) {
192 | return importedLocalName;
193 | }
194 |
195 | const importSpecifier = j.importSpecifier(
196 | j.identifier(importedName),
197 | localName ? j.identifier(localName) : null,
198 | );
199 |
200 | if (
201 | addModuleImport(j, root, { pkgName: moduleName, importSpecifier, before })
202 | ) {
203 | return localName || importedName;
204 | }
205 |
206 | throw new Error(`No ${moduleName} import found!`);
207 | }
208 |
209 | // add style import after one module
210 | function addStyleModuleImport(j, root, { moduleName, after }) {
211 | if (hasModuleImport(j, root, moduleName)) {
212 | return;
213 | }
214 |
215 | const importStatement = j.importDeclaration([], j.literal(moduleName));
216 | insertImportAfter(j, root, { importStatement, afterModule: after });
217 | }
218 |
219 | function parseStrToArray(antdPkgNames) {
220 | return (antdPkgNames || '')
221 | .split(',')
222 | .filter(n => n)
223 | .map(n => n.trim());
224 | }
225 |
226 | module.exports = {
227 | parseStrToArray,
228 | addModuleDefaultImport,
229 | addStyleModuleImport,
230 | addSubmoduleImport,
231 | removeEmptyModuleImport,
232 | useVar,
233 | };
234 |
--------------------------------------------------------------------------------
/bin/cli.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console: 0 */
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const os = require('os');
6 |
7 | const _ = require('lodash');
8 | const chalk = require('chalk');
9 | const isGitClean = require('is-git-clean');
10 | const updateCheck = require('update-check');
11 | const findUp = require('find-up');
12 | const semver = require('semver');
13 | const { run: jscodeshift } = require('jscodeshift/src/Runner');
14 |
15 | const pkg = require('../package.json');
16 | const pkgUpgradeList = require('./upgrade-list');
17 | const { getDependencies } = require('../transforms/utils/marker');
18 | const transformLess = require('../less-transforms');
19 |
20 | // jscodeshift codemod scripts dir
21 | const transformersDir = path.join(__dirname, '../transforms');
22 |
23 | // jscodeshift bin#--ignore-config
24 | const ignoreConfig = path.join(__dirname, './codemod.ignore');
25 |
26 | const transformers = [
27 | 'v5-props-changed-migration',
28 | 'v5-removed-component-migration',
29 | 'v5-remove-style-import',
30 | ];
31 |
32 | const dependencyProperties = [
33 | 'dependencies',
34 | 'devDependencies',
35 | 'clientDependencies',
36 | 'isomorphicDependencies',
37 | 'buildDependencies',
38 | ];
39 |
40 | async function ensureGitClean() {
41 | let clean = false;
42 | try {
43 | clean = await isGitClean();
44 | } catch (err) {
45 | if (
46 | err &&
47 | err.stderr &&
48 | err.stderr.toLowerCase().includes('not a git repository')
49 | ) {
50 | clean = true;
51 | }
52 | }
53 |
54 | if (!clean) {
55 | console.log(chalk.yellow('Sorry that there are still some git changes'));
56 | console.log('\n you must commit or stash them firstly');
57 | process.exit(1);
58 | }
59 | }
60 |
61 | async function checkUpdates() {
62 | let update;
63 | try {
64 | update = await updateCheck(pkg);
65 | } catch (err) {
66 | console.log(chalk.yellow(`Failed to check for updates: ${err}`));
67 | }
68 |
69 | if (update) {
70 | console.log(
71 | chalk.blue(`Latest version is ${update.latest}. Please update firstly`),
72 | );
73 | process.exit(1);
74 | }
75 | }
76 |
77 | function getMaxWorkers(options = {}) {
78 | // limit usage for cpus
79 | return options.cpus || Math.max(2, Math.ceil(os.cpus().length / 3));
80 | }
81 |
82 | function getRunnerArgs(
83 | transformerPath,
84 | parser = 'babylon', // use babylon as default parser
85 | options = {},
86 | ) {
87 | const args = {
88 | verbose: 2,
89 | // limit usage for cpus
90 | cpus: getMaxWorkers(options),
91 | // https://github.com/facebook/jscodeshift/blob/master/src/Runner.js#L255
92 | // https://github.com/facebook/jscodeshift/blob/master/src/Worker.js#L50
93 | babel: false,
94 | parser,
95 | // override default babylon parser config to enable `decorator-legacy`
96 | // https://github.com/facebook/jscodeshift/blob/master/parser/babylon.js
97 | parserConfig: require('./babylon.config.json'),
98 | extensions: ['tsx', 'ts', 'jsx', 'js'].join(','),
99 | transform: transformerPath,
100 | ignorePattern: '**/node_modules',
101 | ignoreConfig,
102 | args: ['antd', '@alipay/bigfish/antd'],
103 | };
104 |
105 | return args;
106 | }
107 |
108 | async function run(filePath, args = {}) {
109 | for (const transformer of transformers) {
110 | await transform(transformer, 'babylon', filePath, args);
111 | }
112 |
113 | await lessTransform(filePath, args);
114 | }
115 |
116 | async function lessTransform(filePath, options) {
117 | const maxWorkers = getMaxWorkers(options);
118 | const dir = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
119 | // less part
120 | // `@antd/xxxx` | `~@antd/xxxx`
121 | return await transformLess(dir, { maxWorkers });
122 | }
123 |
124 | async function transform(transformer, parser, filePath, options) {
125 | console.log(chalk.bgGreen.bold('Transform'), transformer);
126 | const transformerPath = path.join(transformersDir, `${transformer}.js`);
127 |
128 | // pass closet .gitignore to jscodeshift as extra `--ignore-file` option
129 | // const gitignorePath = await findGitIgnore(filePath);
130 |
131 | const args = getRunnerArgs(transformerPath, parser, {
132 | ...options,
133 | // gitignore: gitignorePath,
134 | });
135 |
136 | try {
137 | if (process.env.NODE_ENV === 'local') {
138 | console.log(`Running jscodeshift with: ${JSON.stringify(args)}`);
139 | }
140 |
141 | // js part
142 | await jscodeshift(transformerPath, [ filePath ], args);
143 | } catch (err) {
144 | console.error(err);
145 | if (process.env.NODE_ENV === 'local') {
146 | const errorLogFile = path.join(__dirname, './error.log');
147 | fs.appendFileSync(errorLogFile, err);
148 | fs.appendFileSync(errorLogFile, '\n');
149 | }
150 | }
151 | }
152 |
153 | async function upgradeDetect(targetDir, needProLayout, needCompatible) {
154 | const result = [];
155 | const cwd = path.join(process.cwd(), targetDir);
156 | const { readPackageUp } = await import('read-pkg-up');
157 | const closetPkgJson = await readPackageUp({ cwd });
158 |
159 | let pkgJsonPath;
160 | if (!closetPkgJson) {
161 | pkgJsonPath = "we didn't find your package.json";
162 | // unknown dependency property
163 | result.push(['install', 'antd', pkgUpgradeList.antd]);
164 | if (needProLayout) {
165 | result.push([
166 | 'install',
167 | '@ant-design/pro-layout',
168 | pkgUpgradeList['@ant-design/pro-layout'].version,
169 | ]);
170 | }
171 |
172 | if (needCompatible) {
173 | result.push([
174 | 'install',
175 | '@ant-design/compatible',
176 | pkgUpgradeList['@ant-design/compatible'].version,
177 | ]);
178 | }
179 | } else {
180 | const { packageJson } = closetPkgJson;
181 | pkgJsonPath = closetPkgJson.path;
182 |
183 | // dependencies must be installed or upgraded with correct version
184 | const mustInstallOrUpgradeDeps = ['antd'];
185 | if (needProLayout) {
186 | mustInstallOrUpgradeDeps.push('@ant-design/pro-layout');
187 | }
188 | if (needCompatible) {
189 | mustInstallOrUpgradeDeps.push('@ant-design/compatible');
190 | }
191 |
192 | // handle mustInstallOrUpgradeDeps
193 | mustInstallOrUpgradeDeps.forEach(depName => {
194 | let hasDependency = false;
195 | const expectVersion = pkgUpgradeList[depName].version;
196 | // const upgradePkgDescription = pkgUpgradeList[depName].description;
197 | dependencyProperties.forEach(property => {
198 | const versionRange = _.get(packageJson, `${property}.${depName}`);
199 | // mark dependency installment state
200 | hasDependency = hasDependency || !!versionRange;
201 | // no dependency or improper version dependency
202 | if (versionRange && !semver.satisfies(semver.minVersion(versionRange), expectVersion)) {
203 | result.push(['update', depName, expectVersion, property]);
204 | }
205 | });
206 | if (!hasDependency) {
207 | // unknown dependency property
208 | result.push(['install', depName, pkgUpgradeList[depName].version]);
209 | }
210 | });
211 |
212 | // dependencies must be upgraded to correct version
213 | const mustUpgradeDeps = _.without(
214 | Object.keys(pkgUpgradeList),
215 | ...mustInstallOrUpgradeDeps,
216 | );
217 | mustUpgradeDeps.forEach(depName => {
218 | dependencyProperties.forEach(property => {
219 | const expectVersion = pkgUpgradeList[depName].version;
220 | const versionRange = _.get(packageJson, `${property}.${depName}`);
221 | /**
222 | * we may have dependencies in `package.json`
223 | * make sure that they can `work well` with `antd5`
224 | * so we check dependency's version here
225 | */
226 | if (versionRange && !semver.satisfies(semver.minVersion(versionRange), expectVersion)) {
227 | result.push(['update', depName, expectVersion, property]);
228 | }
229 | });
230 | });
231 | }
232 |
233 | if (!result.length) {
234 | console.log(chalk.green('Checking passed'));
235 | return;
236 | }
237 |
238 | console.log(
239 | chalk.yellow(
240 | "It's recommended to install or upgrade these dependencies to ensure working well with antd v5\n",
241 | ),
242 | );
243 | console.log(`> package.json file: ${pkgJsonPath} \n`);
244 | const dependencies = result.map(
245 | ([operateType, depName, expectVersion, dependencyProperty]) =>
246 | [
247 | _.capitalize(operateType),
248 | `${depName}${expectVersion}`,
249 | dependencyProperty ? `in ${dependencyProperty}` : '',
250 | ].join(' '),
251 | );
252 |
253 | console.log(dependencies.map(n => `* ${n}`).join('\n'));
254 | }
255 |
256 | async function findGitIgnore(targetDir) {
257 | const cwd = path.join(process.cwd(), targetDir);
258 | return await findUp('.gitignore', { cwd });
259 | }
260 |
261 | /**
262 | * options
263 | * --force // force skip git checking (dangerously)
264 | * --cpus=1 // specify cpus cores to use
265 | */
266 |
267 | async function bootstrap() {
268 | const dir = process.argv[2];
269 | // eslint-disable-next-line global-require
270 | const args = require('yargs-parser')(process.argv.slice(3));
271 | if (process.env.NODE_ENV !== 'local') {
272 | // check for updates
273 | await checkUpdates();
274 | // check for git status
275 | if (!args.force) {
276 | await ensureGitClean();
277 | } else {
278 | console.log(
279 | Array(3)
280 | .fill(1)
281 | .map(() =>
282 | chalk.yellow(
283 | 'WARNING: You are trying to skip git status checking, please be careful',
284 | ),
285 | )
286 | .join('\n'),
287 | );
288 | }
289 | }
290 |
291 | // check for `path`
292 | if (!dir || !fs.existsSync(dir)) {
293 | console.log(chalk.yellow('Invalid dir:', dir, ', please pass a valid dir'));
294 | process.exit(1);
295 | }
296 |
297 | await run(dir, args);
298 |
299 | try {
300 | console.log('----------- antd5 dependencies alert -----------\n');
301 | const depsList = await getDependencies();
302 | await upgradeDetect(
303 | dir,
304 | depsList.includes('@ant-design/pro-layout'),
305 | depsList.includes('@ant-design/compatible'),
306 | );
307 | } catch (err) {
308 | console.log('skip summary due to', err);
309 | } finally {
310 | console.log(
311 | `\n----------- Thanks for using @ant-design/codemod ${pkg.version} -----------`,
312 | );
313 | }
314 | }
315 |
316 | module.exports = {
317 | bootstrap,
318 | ensureGitClean,
319 | transform,
320 | run,
321 | getRunnerArgs,
322 | checkUpdates,
323 | };
324 |
--------------------------------------------------------------------------------
/transforms/v5-props-changed-migration.js:
--------------------------------------------------------------------------------
1 | const { printOptions } = require('./utils/config');
2 | const { parseStrToArray } = require('./utils');
3 | const {
4 | createObjectProperty,
5 | createObjectExpression,
6 | getJSXAttributeValue,
7 | findAllAssignedNames,
8 | } = require('./utils/ast');
9 |
10 | const changedComponentPropsMap = {
11 | AutoComplete: {
12 | dropdownClassName: {
13 | action: 'rename',
14 | replacer: 'popupClassName',
15 | },
16 | },
17 | Cascader: {
18 | dropdownClassName: {
19 | action: 'rename',
20 | replacer: 'popupClassName',
21 | },
22 | },
23 | Select: {
24 | dropdownClassName: {
25 | action: 'rename',
26 | replacer: 'popupClassName',
27 | },
28 | },
29 | TreeSelect: {
30 | dropdownClassName: {
31 | action: 'rename',
32 | replacer: 'popupClassName',
33 | },
34 | },
35 | // 处理 compound components: TimePicker.RangePicker
36 | TimePicker: {
37 | dropdownClassName: {
38 | action: 'rename',
39 | replacer: 'popupClassName',
40 | },
41 | },
42 | 'TimePicker.RangePicker': {
43 | dropdownClassName: {
44 | action: 'rename',
45 | replacer: 'popupClassName',
46 | },
47 | },
48 | // 处理 compound components: DatePicker.RangePicker
49 | DatePicker: {
50 | dropdownClassName: {
51 | action: 'rename',
52 | replacer: 'popupClassName',
53 | },
54 | },
55 | 'DatePicker.RangePicker': {
56 | dropdownClassName: {
57 | action: 'rename',
58 | replacer: 'popupClassName',
59 | },
60 | },
61 | Mentions: {
62 | dropdownClassName: {
63 | action: 'rename',
64 | replacer: 'popupClassName',
65 | },
66 | },
67 | Drawer: {
68 | visible: {
69 | action: 'rename',
70 | replacer: 'open',
71 | },
72 | className: {
73 | action: 'rename',
74 | replacer: 'rootClassName',
75 | },
76 | style: {
77 | action: 'rename',
78 | replacer: 'rootStyle',
79 | },
80 | },
81 | Modal: {
82 | visible: {
83 | action: 'rename',
84 | replacer: 'open',
85 | },
86 | },
87 | Dropdown: {
88 | visible: {
89 | action: 'rename',
90 | replacer: 'open',
91 | },
92 | },
93 | Tooltip: {
94 | visible: {
95 | action: 'rename',
96 | replacer: 'open',
97 | },
98 | },
99 | Tag: {
100 | visible: {
101 | action: 'remove',
102 | },
103 | },
104 | Slider: {
105 | tipFormatter: {
106 | action: 'rename',
107 | replacer: 'tooltip.formatter',
108 | },
109 | tooltipPlacement: {
110 | action: 'rename',
111 | replacer: 'tooltip.placement',
112 | },
113 | tooltipVisible: {
114 | action: 'rename',
115 | replacer: 'tooltip.open',
116 | },
117 | },
118 | Table: {
119 | filterDropdownVisible: {
120 | action: 'rename',
121 | replacer: 'filterDropdownOpen',
122 | },
123 | },
124 | };
125 |
126 | module.exports = (file, api, options) => {
127 | const j = api.jscodeshift;
128 | const root = j(file.source);
129 | const antdPkgNames = parseStrToArray(options.antdPkgNames || 'antd');
130 |
131 | // [ [DatePicker], [DatePicker, RangePicker] ]
132 | const componentTuple = Object.keys(changedComponentPropsMap).map(component =>
133 | component.split('.'),
134 | );
135 |
136 | function handlePropsTransform(collection, componentConfig) {
137 | let hasChanged = false;
138 | Object.keys(componentConfig).forEach(propName => {
139 | collection
140 | .find(j.JSXAttribute, {
141 | name: {
142 | type: 'JSXIdentifier',
143 | name: propName,
144 | },
145 | })
146 | .filter(nodePath => {
147 | // 只找第一层的 JSXElement 的 Attributes
148 | return j.JSXOpeningElement.check(nodePath.parent.node)
149 | && collection.paths().includes(nodePath.parent.parent);
150 | })
151 | .forEach(nodePath => {
152 | const { action, replacer } = componentConfig[propName];
153 | if (action === 'rename' && replacer) {
154 | // ->
155 | if (replacer.includes('.')) {
156 | const value = getJSXAttributeValue(j, nodePath);
157 |
158 | // delete origin `props`
159 | nodePath.parent.node.attributes = nodePath.parent.node.attributes.filter(
160 | attr => attr.name.name !== propName,
161 | );
162 |
163 | hasChanged = true;
164 |
165 | const [propKey, propSubKey] = replacer.split('.');
166 | // 检测是否已存在对应的 property 没有则创建一个新的
167 | // 获取 `Tag` 的 props
168 | let existedPropKeyAttr = j(nodePath.parent.parent).find(
169 | j.JSXAttribute,
170 | {
171 | name: {
172 | type: 'JSXIdentifier',
173 | name: propKey,
174 | },
175 | },
176 | );
177 | if (existedPropKeyAttr.length === 0) {
178 | const newPropKeyAttr = j.jsxAttribute(
179 | j.jsxIdentifier(propKey),
180 | j.jsxExpressionContainer(
181 | createObjectExpression(j, {
182 | [propSubKey]: value,
183 | }),
184 | ),
185 | );
186 |
187 | // 给对应 property 新增值
188 | nodePath.parent.node.attributes.push(newPropKeyAttr);
189 | } else {
190 | existedPropKeyAttr
191 | .paths()[0]
192 | .value.value.expression.properties.push(
193 | createObjectProperty(j, propSubKey, value),
194 | );
195 | }
196 | } else {
197 | // ->
198 | nodePath.node.name = replacer;
199 | hasChanged = true;
200 | }
201 | }
202 |
203 | if (action === 'remove') {
204 | //
205 | let value = nodePath.value.value;
206 | //
207 | // 取出来 JSXExpressionContainer 其中值部分
208 | if (nodePath.value?.value?.type === 'JSXExpressionContainer') {
209 | value = nodePath.value.value.expression;
210 | }
211 |
212 | // delete origin `props`
213 | nodePath.parent.node.attributes = nodePath.parent.node.attributes.filter(
214 | attr => attr.name.name !== propName,
215 | );
216 |
217 | hasChanged = true;
218 |
219 | //
220 | // 这种情况直接去掉 visible 即可
221 |
222 | // 有 value 再创建条件语句,没有 value 则默认为 true
223 | // create a conditional expression
224 | const conditionalExpression = value
225 | ? j.conditionalExpression(
226 | value,
227 | // Component Usage JSXElement ``
228 | nodePath.parent.parent.node,
229 | j.nullLiteral(),
230 | )
231 | : null;
232 |
233 | // <>>
234 | //
235 | // return ();
236 | // <- transform to ->
237 | // <>{vi && }>
238 | // {vi && }
239 | // return (vi && )
240 |
241 | if (conditionalExpression) {
242 | // vi(JSXIdentifier) -> `<`(JSXOpeningElement) -> (JSXElement)
243 | // -> `<>>`(JSXFragment) | ``(JSXElement)
244 | if (
245 | ['JSXElement', 'JSXFragment'].includes(
246 | nodePath.parent.parent.parent.node.type,
247 | )
248 | ) {
249 | const index = nodePath.parent.parent.parent.node.children.findIndex(
250 | n => n === nodePath.parent.parent.node,
251 | );
252 | if (index > -1) {
253 | nodePath.parent.parent.parent.node.children.splice(
254 | index,
255 | 1,
256 | // add `{}`
257 | j.jsxExpressionContainer(conditionalExpression),
258 | );
259 | }
260 | } else if (
261 | ['ReturnStatement'].includes(
262 | nodePath.parent.parent.parent.node.type,
263 | )
264 | ) {
265 | nodePath.parent.parent.parent.node.argument = conditionalExpression;
266 | }
267 | }
268 |
269 | // 将 jsx element 转为一个条件表达式,并使用小括号包住
270 | // 最后再检查是不是有一个大的 {} JSXExpressionContainer 包住
271 | }
272 | });
273 | });
274 |
275 | return hasChanged;
276 | }
277 |
278 | // import deprecated components from '@ant-design/compatible'
279 | function handlePropsChanged(j, root) {
280 | let hasChanged = false;
281 |
282 | // import { Tag, Mention } from 'antd';
283 | // import { Form, Mention } from '@forked/antd';
284 | root
285 | .find(j.Identifier)
286 | .filter(
287 | path =>
288 | componentTuple.map(n => n[0]).includes(path.node.name) &&
289 | path.parent.node.type === 'ImportSpecifier' &&
290 | antdPkgNames.includes(path.parent.parent.node.source.value),
291 | )
292 | .forEach(path => {
293 | // import { Tag } from 'antd'
294 | // import { Tag as Tag1 } from 'antd'
295 | const importedComponentName = path.parent.node.imported.name;
296 | const localComponentName = path.parent.node.local.name;
297 |
298 | const componentConfig = changedComponentPropsMap[importedComponentName];
299 |
300 | const nonCompoundComponent = root.findJSXElements(localComponentName);
301 | // 处理非 compound component 部分
302 | if (handlePropsTransform(nonCompoundComponent, componentConfig)) {
303 | hasChanged = true;
304 | }
305 |
306 | // 处理 compound component 部分
307 | const compoundComponentTuple = componentTuple.find(
308 | n => n[0] === importedComponentName && n[1],
309 | );
310 | const [, compoundComponentName] = compoundComponentTuple || [];
311 | if (compoundComponentName) {
312 | //
313 | // JSXMemberExpression
314 | const compoundComponent = root.find(j.JSXElement, {
315 | openingElement: {
316 | name: {
317 | type: 'JSXMemberExpression',
318 | object: {
319 | type: 'JSXIdentifier',
320 | name: localComponentName,
321 | },
322 | property: {
323 | type: 'JSXIdentifier',
324 | name: compoundComponentName,
325 | },
326 | },
327 | },
328 | });
329 | if (handlePropsTransform(compoundComponent, componentConfig)) {
330 | hasChanged = true;
331 | }
332 |
333 | // const { RangePicker } = DatePicker;
334 | root
335 | .find(j.VariableDeclarator, {
336 | init: {
337 | type: 'Identifier',
338 | name: localComponentName,
339 | },
340 | })
341 | .forEach(path => {
342 | const localComponentNames = path.node.id.properties
343 | .filter(n => n.key.name === compoundComponentName)
344 | // 优先处理解构场景
345 | // key.name: `const { RangePicker } = DatePicker;`
346 | // value.name: `const { RangePicker: RP1 } = DatePicker;`
347 | .map(n => n.value?.name || n.key.name);
348 | localComponentNames.forEach(compoundComponentName => {
349 | // 处理反复 reassign 的场景
350 | const localAssignedNames = findAllAssignedNames(
351 | root, j,
352 | compoundComponentName,
353 | [compoundComponentName],
354 | );
355 |
356 | localAssignedNames.forEach(componentName => {
357 | const compoundComponent = root.findJSXElements(componentName);
358 | if (handlePropsTransform(compoundComponent, componentConfig)) {
359 | hasChanged = true;
360 | }
361 | });
362 | });
363 | });
364 |
365 | // const RangerPicker1 = DatePicker.RangePicker;
366 | // const RangerPicker2 = RangerPicker1;
367 | root
368 | .find(j.VariableDeclarator, {
369 | init: {
370 | type: 'MemberExpression',
371 | object: {
372 | type: 'Identifier',
373 | name: localComponentName,
374 | },
375 | property: {
376 | type: 'Identifier',
377 | name: compoundComponentName,
378 | },
379 | },
380 | })
381 | .forEach(path => {
382 | const localAssignedName = path.node.id.name;
383 |
384 | // 处理反复 reassign 的场景
385 | const localAssignedNames = findAllAssignedNames(
386 | root, j,
387 | localAssignedName,
388 | [localAssignedName],
389 | );
390 |
391 | localAssignedNames.forEach(componentName => {
392 | const compoundComponent = root.findJSXElements(componentName);
393 | if (handlePropsTransform(compoundComponent, componentConfig)) {
394 | hasChanged = true;
395 | }
396 | });
397 | });
398 | }
399 | });
400 |
401 | return hasChanged;
402 | }
403 |
404 | // step1. import deprecated components from '@ant-design/compatible'
405 | // step2. cleanup antd import if empty
406 | let hasChanged = false;
407 | hasChanged = handlePropsChanged(j, root) || hasChanged;
408 |
409 | return hasChanged
410 | ? root.toSource(options.printOptions || printOptions)
411 | : null;
412 | };
413 |
--------------------------------------------------------------------------------