@polyv/eslint-config 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,35 +1,67 @@
1
1
  {
2
2
  "name": "@polyv/eslint-config",
3
- "version": "0.8.0",
4
- "description": "Standard ESLint configuration for Polyv projects.",
5
- "keywords": [
6
- "polyv",
7
- "eslint",
8
- "config"
9
- ],
10
- "homepage": "https://www.polyv.net/",
3
+ "version": "1.0.0",
4
+ "description": "ESLint 10 flat config for Polyv Mini Program import checks.",
5
+ "type": "module",
11
6
  "license": "MIT",
12
7
  "engines": {
13
- "node": "^20.10.0"
8
+ "node": ">=20.10.0"
14
9
  },
15
- "dependencies": {
16
- "@babel/core": "^7.24.9",
17
- "@babel/eslint-parser": "^7.24.8",
18
- "@typescript-eslint/eslint-plugin": "^7.16.1",
19
- "@typescript-eslint/parser": "^7.16.1",
20
- "@vue/eslint-config-standard": "^8.0.1",
21
- "eslint": "^8.57.0",
22
- "eslint-config-standard": "^17.1.0",
23
- "eslint-import-resolver-typescript": "^3.6.1",
24
- "eslint-plugin-import": "^2.29.1",
25
- "eslint-plugin-n": "^16.6.2",
26
- "eslint-plugin-promise": "^6.6.0",
27
- "eslint-plugin-sonarjs": "^0.25.1",
28
- "eslint-plugin-vue": "^9.27.0",
29
- "typescript": "^5.5.3",
30
- "vue-eslint-parser": "^9.4.3"
10
+ "exports": {
11
+ ".": "./index.mjs",
12
+ "./plugin": "./plugin.mjs",
13
+ "./prettier-config": "./prettier-config.mjs",
14
+ "./configs/common": "./configs/common/index.mjs",
15
+ "./configs/importx": "./configs/importx/index.mjs",
16
+ "./configs/js": "./configs/js/index.mjs",
17
+ "./configs/miniprogram": "./configs/miniprogram/index.mjs",
18
+ "./configs/prettier": "./configs/prettier/index.mjs",
19
+ "./configs/sonarjs": "./configs/sonarjs/index.mjs",
20
+ "./configs/ts": "./configs/ts/index.mjs",
21
+ "./configs/vue2": "./configs/vue2/index.mjs",
22
+ "./rules/explicit-module-boundary-types": "./rules/explicit-module-boundary-types/index.mjs",
23
+ "./rules/no-relative-directory-index-imports": "./rules/no-relative-directory-index-imports/index.mjs",
24
+ "./rules/no-vue-component-variable-name-conflict": "./rules/no-vue-component-variable-name-conflict/index.mjs"
31
25
  },
26
+ "files": [
27
+ "configs",
28
+ "rules",
29
+ "index.mjs",
30
+ "plugin.mjs",
31
+ "prettier-config.mjs",
32
+ "utils.mjs",
33
+ "README.md"
34
+ ],
32
35
  "scripts": {
33
- "print-config": "eslint --print-config ./lib/for-vue.js > config.txt"
36
+ "lint": "eslint .",
37
+ "test": "node --test"
38
+ },
39
+ "peerDependencies": {
40
+ "eslint": "^10.0.0",
41
+ "typescript": ">=4.8.4 <6.1.0"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "typescript": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "dependencies": {
49
+ "@eslint/js": "^10.0.1",
50
+ "@stylistic/eslint-plugin": "^5.10.0",
51
+ "eslint-config-prettier": "^10.1.8",
52
+ "eslint-import-resolver-node": "^0.3.9",
53
+ "eslint-import-resolver-typescript": "^4.4.5",
54
+ "eslint-plugin-import-x": "^4.16.2",
55
+ "eslint-plugin-prettier": "^5.5.6",
56
+ "eslint-plugin-promise": "^7.3.0",
57
+ "eslint-plugin-sonarjs": "^4.0.3",
58
+ "eslint-plugin-vue": "^10.9.2",
59
+ "globals": "^17.6.0",
60
+ "typescript-eslint": "^8.61.0",
61
+ "vue-eslint-parser": "^10.4.1"
62
+ },
63
+ "devDependencies": {
64
+ "eslint": "^10.5.0",
65
+ "typescript": "^5.8.3"
34
66
  }
35
67
  }
package/plugin.mjs ADDED
@@ -0,0 +1,17 @@
1
+ import explicitModuleBoundaryTypes from './rules/explicit-module-boundary-types/index.mjs';
2
+ import noRelativeDirectoryIndexImports from './rules/no-relative-directory-index-imports/index.mjs';
3
+ import noVueComponentVariableNameConflict from './rules/no-vue-component-variable-name-conflict/index.mjs';
4
+
5
+ const plugin = {
6
+ meta: {
7
+ name: '@polyv/eslint-config'
8
+ },
9
+ rules: {
10
+ 'explicit-module-boundary-types': explicitModuleBoundaryTypes,
11
+ 'no-relative-directory-index-imports': noRelativeDirectoryIndexImports,
12
+ 'no-vue-component-variable-name-conflict': noVueComponentVariableNameConflict
13
+ }
14
+ };
15
+
16
+ export const rules = plugin.rules;
17
+ export default plugin;
@@ -0,0 +1,15 @@
1
+ /** @type {import('prettier').Config} */
2
+ const prettierConfig = {
3
+ useTabs: false,
4
+ printWidth: 120,
5
+ tabWidth: 2,
6
+ singleQuote: true,
7
+ semi: true,
8
+ trailingComma: 'all',
9
+ bracketSameLine: false,
10
+ arrowParens: 'avoid',
11
+ quoteProps: 'as-needed',
12
+ singleAttributePerLine: true
13
+ };
14
+
15
+ export default prettierConfig;
@@ -0,0 +1,126 @@
1
+ # explicit-module-boundary-types
2
+
3
+ `polyv/explicit-module-boundary-types` 基于 `@typescript-eslint/explicit-module-boundary-types` 封装,用于要求导出函数显式声明模块边界类型,同时允许指定函数或方法省略返回类型。
4
+
5
+ ## 适用场景
6
+
7
+ Hook 函数经常返回组合对象、响应式引用或多个工具方法。强制写返回类型容易让声明变得冗长,并且可能削弱 TypeScript 对组合返回值的推导体验。
8
+
9
+ 该规则只放开匹配函数或方法的返回类型要求,不会放开参数类型要求,也不会放开未匹配函数的返回类型要求。
10
+
11
+ ## 使用方式
12
+
13
+ 如果已经组合 `polyv.configs.common`,`polyv` plugin 会自动注册:
14
+
15
+ ```javascript
16
+ // eslint.config.mjs
17
+ import polyv from '@polyv/eslint-config';
18
+
19
+ export default [
20
+ ...polyv.configs.common,
21
+ {
22
+ rules: {
23
+ 'polyv/explicit-module-boundary-types': ['error', {
24
+ allowHookReturnTypeInference: true,
25
+ allowedReturnTypeInferencePatterns: [
26
+ '^[A-Za-z_$][\\w$]*(?:Props?|Emits?)$'
27
+ ]
28
+ }]
29
+ }
30
+ }
31
+ ];
32
+ ```
33
+
34
+ TypeScript 项目通常直接使用 `polyv.configs.ts`:
35
+
36
+ ```javascript
37
+ // eslint.config.mjs
38
+ import polyv from '@polyv/eslint-config';
39
+
40
+ export default [
41
+ ...polyv.configs.common,
42
+ ...polyv.configs.ts
43
+ ];
44
+ ```
45
+
46
+ ## 错误示例
47
+
48
+ ```typescript
49
+ // bad.ts
50
+ export function createService() {
51
+ return { value: 1 };
52
+ }
53
+ ```
54
+
55
+ ## 正确示例
56
+
57
+ ```typescript
58
+ // good.ts
59
+ export function createService(): { value: number } {
60
+ return { value: 1 };
61
+ }
62
+
63
+ export function useDemo() {
64
+ return { value: 1 };
65
+ }
66
+ ```
67
+
68
+ ## 返回类型推导
69
+
70
+ 当 `allowHookReturnTypeInference` 为 `true` 时,函数名符合 `useXxx` 会被视为 hook 函数并允许省略返回类型,例如:
71
+
72
+ - `useDemo`
73
+ - `useUserInfo`
74
+ - `use2dCanvas`
75
+
76
+ 也可以通过 `allowedReturnTypeInferencePatterns` 配置允许省略返回类型的函数或方法名正则,例如:
77
+
78
+ ```javascript
79
+ // eslint.config.mjs
80
+ {
81
+ rules: {
82
+ 'polyv/explicit-module-boundary-types': ['error', {
83
+ allowedReturnTypeInferencePatterns: [
84
+ '^[A-Za-z_$][\\w$]*(?:Props?|Emits?)$'
85
+ ]
86
+ }]
87
+ }
88
+ }
89
+ ```
90
+
91
+ 场景配置也可以通过 settings 注入同名配置;settings 会和规则 options 合并:
92
+
93
+ ```javascript
94
+ // eslint.config.mjs
95
+ {
96
+ settings: {
97
+ 'polyv/explicit-module-boundary-types': {
98
+ allowedReturnTypeInferencePatterns: [
99
+ '^[A-Za-z_$][\\w$]*(?:Props?|Emits?)$'
100
+ ]
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ 该规则不会根据文件路径放开整个文件,因此 `hooks/` 目录里的普通导出函数仍然需要声明返回类型。
107
+
108
+ ## 规则选项
109
+
110
+ 该规则继承 `@typescript-eslint/explicit-module-boundary-types` 的原有选项,并额外支持:
111
+
112
+ - `allowHookReturnTypeInference`:是否允许 `useXxx` hook 函数省略模块边界返回类型,默认值为 `false`。
113
+ - `allowedReturnTypeInferencePatterns`:允许省略模块边界返回类型的函数或方法名正则列表,默认值为 `[]`。
114
+
115
+ ## Settings
116
+
117
+ 该规则会读取 `settings['polyv/explicit-module-boundary-types']`,目前支持:
118
+
119
+ - `allowHookReturnTypeInference`
120
+ - `allowedReturnTypeInferencePatterns`
121
+
122
+ 当 settings 和规则 options 都配置了 `allowedReturnTypeInferencePatterns` 时,两者会合并生效。
123
+
124
+ ## 自动修复
125
+
126
+ 该规则不提供自动修复。返回类型需要根据实际 API 边界手动补充。
@@ -0,0 +1,171 @@
1
+ import tseslint from 'typescript-eslint';
2
+
3
+ const baseRule = tseslint.plugin.rules['explicit-module-boundary-types'];
4
+ const baseOptionsSchema = baseRule.meta.schema[0];
5
+ const settingsKey = 'polyv/explicit-module-boundary-types';
6
+
7
+ function getKeyName(key) {
8
+ if (!key) {
9
+ return null;
10
+ }
11
+
12
+ if (key.type === 'Identifier') {
13
+ return key.name;
14
+ }
15
+
16
+ if (key.type === 'Literal' && typeof key.value === 'string') {
17
+ return key.value;
18
+ }
19
+
20
+ return null;
21
+ }
22
+
23
+ function getFunctionName(node) {
24
+ if (node.id?.type === 'Identifier') {
25
+ return node.id.name;
26
+ }
27
+
28
+ const parent = node.parent;
29
+ if (!parent) {
30
+ return null;
31
+ }
32
+
33
+ if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
34
+ return parent.id.name;
35
+ }
36
+
37
+ if (
38
+ parent.type === 'MethodDefinition' ||
39
+ parent.type === 'Property' ||
40
+ parent.type === 'PropertyDefinition' ||
41
+ parent.type === 'AccessorProperty' ||
42
+ parent.type === 'TSAbstractMethodDefinition'
43
+ ) {
44
+ return getKeyName(parent.key);
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ function isHookName(name) {
51
+ return /^use[A-Z0-9]/.test(name);
52
+ }
53
+
54
+ function createAllowedReturnTypeInferencePatterns(patterns = []) {
55
+ return patterns.map((pattern) => new RegExp(pattern));
56
+ }
57
+
58
+ function getAllowedReturnTypeInferencePatterns(...patternGroups) {
59
+ return patternGroups.flatMap((patterns) => (
60
+ Array.isArray(patterns)
61
+ ? patterns.filter((pattern) => typeof pattern === 'string')
62
+ : []
63
+ ));
64
+ }
65
+
66
+ function getRuleSettings(context) {
67
+ const settings = context.settings?.[settingsKey];
68
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
69
+ return {};
70
+ }
71
+
72
+ return settings;
73
+ }
74
+
75
+ function isAllowedReturnTypeInferenceName(name, patterns) {
76
+ return patterns.some((pattern) => pattern.test(name));
77
+ }
78
+
79
+ function shouldIgnoreMissingReturnType(descriptor, options) {
80
+ if (descriptor?.messageId !== 'missingReturnType') {
81
+ return false;
82
+ }
83
+
84
+ const functionName = getFunctionName(descriptor.node);
85
+ if (!functionName) {
86
+ return false;
87
+ }
88
+
89
+ return (
90
+ (options.allowHookReturnTypeInference && isHookName(functionName)) ||
91
+ isAllowedReturnTypeInferenceName(functionName, options.allowedReturnTypeInferencePatterns)
92
+ );
93
+ }
94
+
95
+ function createBaseContext(context, baseOptions, options) {
96
+ const baseContext = Object.create(context);
97
+
98
+ Object.defineProperties(baseContext, {
99
+ options: {
100
+ value: [baseOptions]
101
+ },
102
+ report: {
103
+ value(descriptor, ...args) {
104
+ if (args.length === 0 && shouldIgnoreMissingReturnType(descriptor, options)) {
105
+ return;
106
+ }
107
+
108
+ return context.report(descriptor, ...args);
109
+ }
110
+ }
111
+ });
112
+
113
+ return baseContext;
114
+ }
115
+
116
+ export default {
117
+ ...baseRule,
118
+ meta: {
119
+ ...baseRule.meta,
120
+ docs: {
121
+ ...baseRule.meta.docs,
122
+ description: '要求导出函数显式声明模块边界类型,并允许指定函数或方法省略返回类型'
123
+ },
124
+ schema: [{
125
+ ...baseOptionsSchema,
126
+ properties: {
127
+ ...baseOptionsSchema.properties,
128
+ allowHookReturnTypeInference: {
129
+ type: 'boolean',
130
+ description: '是否允许 useXxx hook 函数省略模块边界返回类型。'
131
+ },
132
+ allowedReturnTypeInferencePatterns: {
133
+ type: 'array',
134
+ description: '允许省略模块边界返回类型的函数或方法名正则列表。',
135
+ items: {
136
+ type: 'string'
137
+ }
138
+ }
139
+ }
140
+ }]
141
+ },
142
+ defaultOptions: [{
143
+ ...baseRule.defaultOptions[0],
144
+ allowHookReturnTypeInference: false,
145
+ allowedReturnTypeInferencePatterns: []
146
+ }],
147
+ create(context) {
148
+ const settings = getRuleSettings(context);
149
+ const ruleOptions = context.options[0] ?? {};
150
+ const options = {
151
+ ...baseRule.defaultOptions[0],
152
+ allowHookReturnTypeInference: false,
153
+ ...settings,
154
+ ...ruleOptions,
155
+ allowedReturnTypeInferencePatterns: getAllowedReturnTypeInferencePatterns(
156
+ settings.allowedReturnTypeInferencePatterns,
157
+ ruleOptions.allowedReturnTypeInferencePatterns
158
+ )
159
+ };
160
+ const {
161
+ allowHookReturnTypeInference,
162
+ allowedReturnTypeInferencePatterns,
163
+ ...baseOptions
164
+ } = options;
165
+
166
+ return baseRule.create(createBaseContext(context, baseOptions, {
167
+ allowHookReturnTypeInference,
168
+ allowedReturnTypeInferencePatterns: createAllowedReturnTypeInferencePatterns(allowedReturnTypeInferencePatterns)
169
+ }));
170
+ }
171
+ };
@@ -0,0 +1,87 @@
1
+ # no-relative-directory-index-imports
2
+
3
+ `polyv/no-relative-directory-index-imports` 用于禁止依赖相对目录的 `index` 文件兜底解析。
4
+
5
+ ## 适用场景
6
+
7
+ 微信小程序 npm 构建后,不保证把相对目录路径自动解析到该目录下的 `index` 文件。业务代码如果写成 `./helper`、`.`、`..` 这类目录入口,构建后可能出现运行时路径解析失败。
8
+
9
+ ## 使用方式
10
+
11
+ 如果已经组合 `polyv.configs.common`,`polyv` plugin 会自动注册:
12
+
13
+ ```javascript
14
+ // eslint.config.mjs
15
+ import polyv from '@polyv/eslint-config';
16
+
17
+ export default [
18
+ ...polyv.configs.common,
19
+ {
20
+ rules: {
21
+ 'polyv/no-relative-directory-index-imports': 'error'
22
+ }
23
+ }
24
+ ];
25
+ ```
26
+
27
+ 小程序项目通常直接使用 `polyv.configs.miniprogram`:
28
+
29
+ ```javascript
30
+ // eslint.config.mjs
31
+ import polyv from '@polyv/eslint-config';
32
+
33
+ export default [
34
+ ...polyv.configs.common,
35
+ ...polyv.configs.miniprogram
36
+ ];
37
+ ```
38
+
39
+ ## 错误示例
40
+
41
+ ```javascript
42
+ // bad.js
43
+ import helper from './helper';
44
+ import current from '.';
45
+ import parent from '..';
46
+ const utils = require('./utils');
47
+ ```
48
+
49
+ ## 正确示例
50
+
51
+ ```javascript
52
+ // good.js
53
+ import helper from './helper/index';
54
+ import current from './index';
55
+ import parent from '../index';
56
+ const utils = require('./utils/index');
57
+ ```
58
+
59
+ ## 自动修复
60
+
61
+ 该规则支持自动修复,会把相对目录入口补成显式 `index` 路径。
62
+
63
+ 例如:
64
+
65
+ ```javascript
66
+ // bad.js
67
+ import helper from './helper';
68
+ ```
69
+
70
+ 会修复为:
71
+
72
+ ```javascript
73
+ // good.js
74
+ import helper from './helper/index';
75
+ ```
76
+
77
+ ## 检查范围
78
+
79
+ 该规则会检查:
80
+
81
+ - `import ... from '...'`
82
+ - `export ... from '...'`
83
+ - `export * from '...'`
84
+ - `import('...')`
85
+ - `require('...')`
86
+
87
+ 该规则不会处理已经带扩展名的路径,也不会处理已经显式写出 `index` 的路径。
@@ -0,0 +1,142 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const fileExtensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json'];
5
+ const indexExtensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
6
+
7
+ function isRelativeSpecifier(specifier) {
8
+ return specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../');
9
+ }
10
+
11
+ function hasExplicitIndex(specifier) {
12
+ return /(?:^|\/)index$/.test(specifier);
13
+ }
14
+
15
+ function hasExplicitExtension(specifier) {
16
+ return Boolean(path.posix.extname(specifier));
17
+ }
18
+
19
+ function hasMatchingFile(resolvedPath) {
20
+ return fileExtensions.some((extension) => fs.existsSync(`${resolvedPath}${extension}`));
21
+ }
22
+
23
+ function hasDirectoryIndex(resolvedPath) {
24
+ if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isDirectory()) {
25
+ return false;
26
+ }
27
+
28
+ return indexExtensions.some((extension) => fs.existsSync(path.join(resolvedPath, `index${extension}`)));
29
+ }
30
+
31
+ function getFilename(context) {
32
+ return context.filename || context.getFilename?.();
33
+ }
34
+
35
+ function toExplicitIndexSpecifier(specifier) {
36
+ if (specifier === '.') {
37
+ return './index';
38
+ }
39
+
40
+ if (specifier === '..') {
41
+ return '../index';
42
+ }
43
+
44
+ return specifier.endsWith('/') ? `${specifier}index` : `${specifier}/index`;
45
+ }
46
+
47
+ function shouldReport(context, specifier) {
48
+ if (!isRelativeSpecifier(specifier) || hasExplicitIndex(specifier) || hasExplicitExtension(specifier)) {
49
+ return false;
50
+ }
51
+
52
+ const filename = getFilename(context);
53
+ if (!filename || filename === '<input>') {
54
+ return false;
55
+ }
56
+
57
+ const resolvedPath = path.resolve(path.dirname(filename), specifier);
58
+ return !hasMatchingFile(resolvedPath) && hasDirectoryIndex(resolvedPath);
59
+ }
60
+
61
+ function reportSpecifier(context, node, specifier) {
62
+ if (!shouldReport(context, specifier)) {
63
+ return;
64
+ }
65
+
66
+ context.report({
67
+ node,
68
+ messageId: 'noRelativeDirectoryIndexImports',
69
+ data: {
70
+ specifier,
71
+ indexSpecifier: toExplicitIndexSpecifier(specifier)
72
+ },
73
+ fix(fixer) {
74
+ if (!node.range || typeof node.value !== 'string') {
75
+ return null;
76
+ }
77
+
78
+ const sourceCode = context.sourceCode || context.getSourceCode();
79
+ const raw = sourceCode.getText(node);
80
+ const quote = raw[0] === '"' ? '"' : "'";
81
+ return fixer.replaceText(node, `${quote}${toExplicitIndexSpecifier(specifier)}${quote}`);
82
+ }
83
+ });
84
+ }
85
+
86
+ function checkLiteralSource(context, sourceNode) {
87
+ if (sourceNode && sourceNode.type === 'Literal' && typeof sourceNode.value === 'string') {
88
+ reportSpecifier(context, sourceNode, sourceNode.value);
89
+ }
90
+ }
91
+
92
+ function checkCallExpression(context, node) {
93
+ if (
94
+ node.arguments.length !== 1 ||
95
+ !node.arguments[0] ||
96
+ node.arguments[0].type !== 'Literal' ||
97
+ typeof node.arguments[0].value !== 'string'
98
+ ) {
99
+ return;
100
+ }
101
+
102
+ const isRequireCall = node.callee.type === 'Identifier' && node.callee.name === 'require';
103
+ const isLegacyDynamicImport = node.callee.type === 'Import';
104
+ if (!isRequireCall && !isLegacyDynamicImport) {
105
+ return;
106
+ }
107
+
108
+ reportSpecifier(context, node.arguments[0], node.arguments[0].value);
109
+ }
110
+
111
+ export default {
112
+ meta: {
113
+ type: 'problem',
114
+ docs: {
115
+ description: '禁止依赖相对目录的 index 文件兜底解析,避免微信小程序 npm 构建后的运行时解析失败'
116
+ },
117
+ fixable: 'code',
118
+ schema: [],
119
+ messages: {
120
+ noRelativeDirectoryIndexImports: '微信小程序 npm 构建后不保证把目录路径 "{{specifier}}" 解析到 "{{indexSpecifier}}",请显式引入 "{{indexSpecifier}}"。'
121
+ }
122
+ },
123
+ create(context) {
124
+ return {
125
+ ImportDeclaration(node) {
126
+ checkLiteralSource(context, node.source);
127
+ },
128
+ ExportAllDeclaration(node) {
129
+ checkLiteralSource(context, node.source);
130
+ },
131
+ ExportNamedDeclaration(node) {
132
+ checkLiteralSource(context, node.source);
133
+ },
134
+ ImportExpression(node) {
135
+ checkLiteralSource(context, node.source);
136
+ },
137
+ CallExpression(node) {
138
+ checkCallExpression(context, node);
139
+ }
140
+ };
141
+ }
142
+ };