Explorar o código

chore: add eslint-plugin-semi-design (#886)

* chore: add eslint-plugin-semi-design

* chore: update yarn.lock

Co-authored-by: shijia.me <[email protected]>
走鹃 %!s(int64=3) %!d(string=hai) anos
pai
achega
bcc3f1953a

+ 2 - 1
.eslintrc.js

@@ -48,7 +48,7 @@ module.exports = {
             parserOptions: {
                 project: ['./tsconfig.eslint.json'],
             },
-            plugins: ['react', 'jest', 'react-hooks', 'import', '@typescript-eslint'],
+            plugins: ['react', 'jest', 'react-hooks', 'import', '@typescript-eslint', 'semi-design'],
             rules: {
                 // 因为历史原因,现有项目基本全部是4个空格
                 indent: 'off',
@@ -79,6 +79,7 @@ module.exports = {
                 'jsx-a11y/no-noninteractive-element-interactions': ['warn'],
                 'jsx-a11y/no-autofocus': ['warn'],
                 'object-curly-spacing': ['error', 'always'],
+                'semi-design/no-import': 'error'
             }
         },
     ],

+ 2 - 1
package.json

@@ -166,6 +166,7 @@
     "eslint-plugin-markdown": "^2.2.1",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react-hooks": "^4.2.0",
+    "eslint-plugin-semi-design": "^0.0.1",
     "fs-extra": "^8.1.0",
     "glob": "^7.1.7",
     "html-webpack-plugin": "^3.2.0",
@@ -196,7 +197,7 @@
     "svgo": "^2.7.0",
     "terser-webpack-plugin": "^4.2.3",
     "ts-loader": "^5.4.5",
-    "typescript": "^4.4.3",
+    "typescript": "4.4.3",
     "webpack": "^4.46.0",
     "webpack-cli": "^3.3.12",
     "webpack-dev-server": "^3.11.2",

+ 6 - 0
packages/semi-eslint-plugin/.eslintrc.json

@@ -0,0 +1,6 @@
+{
+    "parserOptions": {
+        "ecmaVersion": "latest",
+        "sourceType": "module"
+    }
+}

+ 55 - 0
packages/semi-eslint-plugin/README-zh_CN.md

@@ -0,0 +1,55 @@
+# eslint-plugin-semi-design
+
+Semi 仓库使用的 eslint 插件
+
+## eslint 规则
+
+### ✅ 不能在 semi-foundation 里引用 semi-ui
+
+semi-ui 不应该作为 semi-foundation 的依赖。
+
+原因:根据 Semi 的 foundation 和 adapter 设计,foundation 不应依赖 adapter。点击查看 [F/A 设计](https://bytedance.feishu.cn/wiki/wikcnOVYexosCS1Rmvb5qCsWT1f)。
+
+### ✅ 不能在 semi-ui 和 semi-foundation 引用 lodash-es
+
+使用 lodash 而不是 lodash-es。
+
+原因:为了兼容 next,而 lodash-es 只提供了 es module 的产物。
+
+![image](https://user-images.githubusercontent.com/26477537/172051379-30b42f31-b677-43be-982f-1e8f5345cfc9.png)
+
+点击查看[详情](https://github.com/vercel/next.js/issues/2259)。
+
+### ✅ 不能在 semi-ui 或 semi-foundation 使用相对路径引用 pacakges 下的包
+
+monorepo 下各个包之间的 import 请使用包名而不是相对路径。
+
+原因:这两个包在用户项目的安装路径可能不在同一文件夹下,使用相对路径会找不到对应的包。
+
+```javascript
+// ❌ 不推荐
+// semi-ui/input/index.tsx
+import inputFoundation from '../semi-foundation/input/foundation';
+
+// ✅ 推荐
+// semi-ui/input/index.tsx
+import inputFoundation from '@douyinfe/semi-foundation/input/foundation';
+```
+
+### ✅ 不推荐在同个包下使用包名加路径引用其他模块
+
+同一个包 import 请使用相对路径而不是引用包名。
+
+```javascript
+// ❌ 不推荐
+// semi-ui/modal/Modal.tsx
+import { Button } from '@douyinfe/semi-ui';
+
+// ✅ 推荐
+// semi-ui/modal/Modal.tsx
+import Button from '../button';
+```
+
+## 相关资料
+
+- eslint plugin 文档:https://eslint.org/docs/developer-guide/working-with-plugins

+ 55 - 0
packages/semi-eslint-plugin/README.md

@@ -0,0 +1,55 @@
+# eslint-plugin-semi-design
+
+eslint plugin for semi design
+
+## Rules
+
+### ✅ Should not reference semi-ui in semi-foundation
+
+semi-ui should not be used as a dependency of semi-foundation.
+
+Why: According to Semi's foundation and adapter design, foundation should not depend on adapter. Click to view the [F/A design](https://bytedance.feishu.cn/wiki/wikcnOVYexosCS1Rmvb5qCsWT1f).
+
+### ✅ Should not import lodash-es in semi-ui and semi-foundation
+
+Use lodash instead of lodash-es.
+
+Why: In order to be compatible with next, lodash-es only provides the product of es module.
+
+![image](https://user-images.githubusercontent.com/26477537/172051379-30b42f31-b677-43be-982f-1e8f5345cfc9.png)
+
+See more [here](https://github.com/vercel/next.js/issues/2259)。
+
+### ✅ Should not use relative paths to import a package under pacakges in semi-ui or semi-foundation
+
+For imports between packages under monorepo, use package names instead of relative paths.
+
+Why: These two packages may not be in the same folder in the installation path of the user project, and the corresponding package cannot be found using the relative path.
+
+```javascript
+// ❌ Not recommend
+// semi-ui/input/index.tsx
+import inputFoundation from '../semi-foundation/input/foundation';
+
+// ✅ Recommend
+// semi-ui/input/index.tsx
+import inputFoundation from '@douyinfe/semi-foundation/input/foundation';
+```
+
+### ✅ Should not use the package name and path to import other modules under the same package
+When importing the same package, use relative paths instead of referencing the package name.
+
+```javascript
+// ❌ Not recommend
+// semi-ui/modal/Modal.tsx
+import { Button } from '@douyinfe/semi-ui';
+
+// ✅ Recommend
+// semi-ui/modal/Modal.tsx
+import Button from '../button';
+
+```
+
+## Related docs
+
+- eslint plugin doc:https://eslint.org/docs/developer-guide/working-with-plugins

+ 44 - 0
packages/semi-eslint-plugin/__tests__/index.js

@@ -0,0 +1,44 @@
+const rule = require('../lib/rules/index').default;
+const RuleTester = require('eslint').RuleTester;
+const eslintConfig = require('../.eslintrc.json');
+
+const ruleTester = new RuleTester({ parserOptions: eslintConfig.parserOptions });
+const { messages } = rule['no-import'].meta;
+
+ruleTester.run('no-import', rule['no-import'], {
+    valid: [
+        {
+            code: 'var invalidVariable = true',
+        }
+    ],
+    invalid: [
+        {
+            code: "import Input from '@douyinfe/semi-ui'",
+            filename: 'packages/semi-foundation/input/foundation.ts',
+            errors: [{ message: messages.unexpected }]
+        },
+        {
+            code: "import { get } from 'lodash-es'",
+            filename: 'packages/semi-foundation/input/foundation.ts',
+            output: "import { get } from 'lodash'",
+            errors: [{ message: messages.unexpectedLodashES }]
+        },
+        {
+            code: "import get from 'lodash-es/get'",
+            filename: 'packages/semi-ui/input/index.tsx',
+            output: "import get from 'lodash/get'",
+            errors: [{ message: messages.unexpectedLodashES }]
+        },
+        {
+            code: "import inputNumberFoundation from '../../semi-foundation/inputNumber/foundation.ts'",
+            filename: 'packages/semi-ui/inputNumber/index.tsx',
+            output: "import inputNumberFoundation from '@douyinfe/semi-foundation/inputNumber/foundation.ts'",
+            errors: [{ message: messages.unexpectedRelativeImport }]
+        },
+        {
+            code: "import Input from '@douyinfe/semi-ui/input/index.tsx'",
+            filename: 'packages/semi-ui/inputNumber/index.tsx',
+            errors: [{ message: messages.unexpectedImportSelf }]
+        },
+    ]
+});

+ 43 - 0
packages/semi-eslint-plugin/package.json

@@ -0,0 +1,43 @@
+{
+  "name": "eslint-plugin-semi-design",
+  "version": "0.0.1",
+  "description": "semi ui eslint plugin",
+  "keywords": [
+    "semi",
+    "eslint"
+  ],
+  "author": "shijia.me <[email protected]>",
+  "homepage": "https://semi.design",
+  "license": "MIT",
+  "main": "lib/index.js",
+  "directories": {
+    "lib": "lib",
+    "test": "__tests__"
+  },
+  "files": [
+    "lib",
+    "README.md",
+    "README-zh_CN.md"
+  ],
+  "publishConfig": {
+    "registry": "https://registry.npmjs.org"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/DouyinFE/semi-design.git"
+  },
+  "scripts": {
+    "build:lib": "rm -rf lib && tsc",
+    "prepublishOnly": "npm run build:lib",
+    "test": "node __tests__/index.js"
+  },
+  "devDependencies": {
+    "typescript": "^4"
+},
+  "peerDependencies": {
+    "eslint": ">=0.8.0"
+  },
+  "bugs": {
+    "url": "https://github.com/DouyinFE/semi-design/issues"
+  }
+}

+ 5 - 0
packages/semi-eslint-plugin/src/index.ts

@@ -0,0 +1,5 @@
+import rules from './rules';
+
+export {
+    rules
+};

+ 5 - 0
packages/semi-eslint-plugin/src/rules/index.ts

@@ -0,0 +1,5 @@
+import noImport from './no-import';
+
+export default {
+    'no-import': noImport,
+};

+ 91 - 0
packages/semi-eslint-plugin/src/rules/no-import.ts

@@ -0,0 +1,91 @@
+import { Rule } from "eslint";
+
+const SEMI_PACKAGE_REG = /(?<=packages\/|@douyinfe\/|\.\.\/)(semi-[\w-]+)/;
+const RELATIVE_PATH_REG = /(..\/)+semi-[\w-]+/;
+
+const rule: Rule.RuleModule = {
+    meta: {
+        type: "problem",
+        docs: {
+            description: "disable import statement",
+            recommended: true,
+            url: "https://github.com/DouyinFE/semi-design"
+        },
+        fixable: "code",
+        messages: {
+            unexpected: "Unexpected import statement, semi ui should not be used as a dependency of semi foundation",
+            unexpectedLodashES: "Unexpected import statement, please use lodash instead of lodash-es.",
+            unexpectedRelativeImport: "Unexpected import statement, please use module name instead of relative path.",
+            unexpectedImportSelf: 'Unexpected import statement, please use relative paths to import modules in the same package.'
+        },
+        schema: [],
+    },
+    create(context) {
+        return {
+            ImportDeclaration: (node) => {
+                const fileName = context.getFilename();
+                const sourceCode = context.getSourceCode();
+                const importName = node.source.raw;
+                const isFoundationFile = fileName.includes('semi-foundation');
+                const isUIFile = fileName.includes('semi-ui');
+                const importText = sourceCode.getText(node);
+
+                if (isFoundationFile) {
+                    if (importName.includes('semi-ui')) {
+                        context.report({ node, messageId: "unexpected" });
+                    }
+                }
+
+                if (isFoundationFile || isUIFile) {
+                    if (importName.includes('lodash-es')) {
+                        const fixedSource = importText.replace('lodash-es', 'lodash');
+                        context.report({
+                            node,
+                            messageId: "unexpectedLodashES",
+                            fix: (fixer) => {
+                                return fixer.replaceText(node, fixedSource);
+                            }
+                        });
+                    } else if (importName.includes('semi-')) {
+                        if (isImportRelativePackage({ path: importName, fileName })) {
+                            const importPackageName = SEMI_PACKAGE_REG.exec(importName)[0];
+                            const fixedSource = importText.replace(RELATIVE_PATH_REG, `@douyinfe/${importPackageName}`);
+                            context.report({
+                                node,
+                                messageId: "unexpectedRelativeImport",
+                                fix: (fixer) => {
+                                    return fixer.replaceText(node, fixedSource);
+                                }
+                            });
+                        } else if (isImportSelf({ path: importName, fileName })) {
+                            context.report({
+                                node,
+                                messageId: "unexpectedImportSelf",
+                            });
+                        }
+                    }
+                }
+            }
+        };
+    }
+};
+
+function isRelativePath(path: string) {
+    return path.includes('../');
+}
+
+function isImportRelativePackage(options: { path: string, fileName: string }) {
+    const { path, fileName } = options;
+    const currentPackageName = SEMI_PACKAGE_REG.exec(fileName)[0];
+    const importPackageName = SEMI_PACKAGE_REG.exec(path)[0];
+    return currentPackageName !== importPackageName && isRelativePath(path);
+}
+
+function isImportSelf(options: { path: string, fileName: string }) {
+    const { path, fileName } = options;
+    const currentPackageName = SEMI_PACKAGE_REG.exec(fileName)[0];
+    const importPackageName = SEMI_PACKAGE_REG.exec(path)[0];
+    return currentPackageName === importPackageName;
+}
+
+export default rule;

+ 12 - 0
packages/semi-eslint-plugin/tsconfig.json

@@ -0,0 +1,12 @@
+{
+    "compilerOptions": {
+        "target": "ES6",
+        "module": "CommonJS",
+        "baseUrl": "./",
+        "outDir": "./lib",
+        "lib": ["ES2015"],
+        "moduleResolution": "node",
+        "skipLibCheck": true
+    },
+    "include": ["src"]
+}

+ 6 - 1
yarn.lock

@@ -24476,7 +24476,12 @@ typeface-inter@^3.18.1:
   resolved "https://registry.yarnpkg.com/typeface-inter/-/typeface-inter-3.18.1.tgz#24cccdf29923f318589783997be20a662cd3ab9c"
   integrity sha512-c+TBanYFCvmg3j5vPk+zxK4ocMZbPxMEmjnwG7rPQoV87xvQ6b07VbAOC0Va0XBbbZCGw6cWNeFuLeg1YQru3Q==
 
-typescript@^4, typescript@^4.4.3, typescript@^4.4.4:
[email protected]:
+  version "4.4.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
+  integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
+
+typescript@^4, typescript@^4.4.4:
   version "4.6.4"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
   integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==