@polariens/kitsune-lint 1.0.0-rc.10
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/README.md +268 -0
- package/bin/copy-prettierignore.mjs +30 -0
- package/eslint/configs/base.d.mts +10 -0
- package/eslint/configs/base.mjs +28 -0
- package/eslint/configs/clean-code.d.mts +20 -0
- package/eslint/configs/clean-code.mjs +102 -0
- package/eslint/configs/pinia.d.mts +10 -0
- package/eslint/configs/pinia.mjs +36 -0
- package/eslint/configs/security.d.mts +12 -0
- package/eslint/configs/security.mjs +46 -0
- package/eslint/configs/tests.d.mts +10 -0
- package/eslint/configs/tests.mjs +40 -0
- package/eslint/configs/typescript.d.mts +14 -0
- package/eslint/configs/typescript.mjs +216 -0
- package/eslint/configs/vitest.d.mts +20 -0
- package/eslint/configs/vitest.mjs +76 -0
- package/eslint/configs/vue.d.mts +25 -0
- package/eslint/configs/vue.mjs +73 -0
- package/eslint/index.d.mts +51 -0
- package/eslint/index.mjs +85 -0
- package/eslint/rules/no-null-in-types.mjs +117 -0
- package/eslint/utils.d.mts +7 -0
- package/eslint/utils.mjs +33 -0
- package/package.json +119 -0
- package/prettier/.prettierignore +14 -0
- package/prettier/index.d.mts +50 -0
- package/prettier/index.mjs +77 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import tseslint from 'typescript-eslint';
|
|
3
|
+
|
|
4
|
+
import noNullInTypes from '../rules/no-null-in-types.mjs';
|
|
5
|
+
import { IGNORE_PATTERNS, resolveFiles } from '../utils.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} TypescriptOptions
|
|
9
|
+
* @property {string[]} [files] - File patterns override
|
|
10
|
+
* @property {string[]} [ignores] - Patterns a ignorar
|
|
11
|
+
* @property {Record<string, unknown>} [rules] - Regras adicionais ou overrides
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuração TypeScript com regras recomendadas e naming conventions.
|
|
16
|
+
* @param {TypescriptOptions} [options={}]
|
|
17
|
+
* @returns {import('eslint').Linter.Config[]}
|
|
18
|
+
*/
|
|
19
|
+
export function typescript(options = {}) {
|
|
20
|
+
const { files, ignores = [], replaceIgnores, rules: extraRules = {} } = options;
|
|
21
|
+
const resolvedFiles = resolveFiles('all', files);
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
ignores: replaceIgnores ?? [
|
|
26
|
+
'**/*.config.{js,mjs,cjs,ts}',
|
|
27
|
+
'**/.prettierrc.*',
|
|
28
|
+
'**/_/*',
|
|
29
|
+
'**/dist/',
|
|
30
|
+
'**/.vitepress/',
|
|
31
|
+
'.rules/',
|
|
32
|
+
'.idea',
|
|
33
|
+
'.npm',
|
|
34
|
+
'.github',
|
|
35
|
+
'.vscode',
|
|
36
|
+
'.quasar',
|
|
37
|
+
'.coverage',
|
|
38
|
+
'.husky',
|
|
39
|
+
...IGNORE_PATTERNS,
|
|
40
|
+
...ignores,
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
...eslint.configs.recommended,
|
|
45
|
+
files: resolvedFiles,
|
|
46
|
+
},
|
|
47
|
+
...tseslint.configs.recommended.map((config) => ({
|
|
48
|
+
...config,
|
|
49
|
+
files: resolvedFiles,
|
|
50
|
+
})),
|
|
51
|
+
{
|
|
52
|
+
plugins: {
|
|
53
|
+
kitsune: {
|
|
54
|
+
rules: {
|
|
55
|
+
'no-null-in-types': noNullInTypes,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
files: resolvedFiles,
|
|
62
|
+
name: '@kitsune/typescript/rules',
|
|
63
|
+
plugins: {
|
|
64
|
+
'@typescript-eslint': tseslint.plugin,
|
|
65
|
+
},
|
|
66
|
+
languageOptions: {
|
|
67
|
+
parser: tseslint.parser,
|
|
68
|
+
parserOptions: {
|
|
69
|
+
projectService: true,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
rules: {
|
|
73
|
+
// Prefer `===` or `!==` (never == or !=)
|
|
74
|
+
eqeqeq: ['error', 'always'],
|
|
75
|
+
'@typescript-eslint/no-empty-object-type': [
|
|
76
|
+
'error',
|
|
77
|
+
{
|
|
78
|
+
allowObjectTypes: 'never',
|
|
79
|
+
allowInterfaces: 'with-single-extends',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
'@typescript-eslint/naming-convention': [
|
|
83
|
+
'error',
|
|
84
|
+
{ selector: 'default', format: ['camelCase'] },
|
|
85
|
+
{ selector: 'variable', format: ['camelCase', 'UPPER_CASE'] },
|
|
86
|
+
{ selector: 'function', format: ['camelCase'] },
|
|
87
|
+
{ selector: 'class', format: ['PascalCase'] },
|
|
88
|
+
{
|
|
89
|
+
selector: 'interface',
|
|
90
|
+
format: ['PascalCase'],
|
|
91
|
+
custom: { regex: '^(?!I[A-Z])(?!.*Interface$)(OAuth|[A-Z][a-z]).*$', match: true },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
selector: 'typeAlias',
|
|
95
|
+
format: ['PascalCase'],
|
|
96
|
+
custom: { regex: '^(?!I[A-Z])(?!.*Type$)(OAuth|[A-Z][a-z]).*$', match: true },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
// camelCase for properties of interfaces/types
|
|
100
|
+
selector: 'typeProperty',
|
|
101
|
+
format: ['camelCase'],
|
|
102
|
+
leadingUnderscore: 'allow',
|
|
103
|
+
trailingUnderscore: 'allow',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
// camelCase for class members
|
|
107
|
+
selector: 'classProperty',
|
|
108
|
+
format: ['camelCase'],
|
|
109
|
+
leadingUnderscore: 'allow',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
// camelCase for methods of classes
|
|
113
|
+
selector: 'classMethod',
|
|
114
|
+
format: ['camelCase'],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
// PascalCase for parameters of generic types of only one char
|
|
118
|
+
selector: 'typeParameter',
|
|
119
|
+
format: ['PascalCase'],
|
|
120
|
+
custom: { regex: '^[A-Z]$', match: true },
|
|
121
|
+
},
|
|
122
|
+
// PascalCase for enums names
|
|
123
|
+
{ selector: 'enum', format: ['PascalCase'] },
|
|
124
|
+
{
|
|
125
|
+
// UPPER_CASE for enum members
|
|
126
|
+
selector: 'enumMember',
|
|
127
|
+
format: null,
|
|
128
|
+
custom: { regex: `^['"]?[A-Z]+([-_][A-Z]+)*['"]?$`, match: true },
|
|
129
|
+
},
|
|
130
|
+
// camelCase for object literal properties
|
|
131
|
+
{ selector: 'objectLiteralProperty', format: null },
|
|
132
|
+
// camelCase or PascalCase for imports
|
|
133
|
+
{ selector: 'import', format: ['camelCase', 'PascalCase'] },
|
|
134
|
+
],
|
|
135
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
136
|
+
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
|
|
137
|
+
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
|
138
|
+
'@typescript-eslint/no-non-null-assertion': 'warn',
|
|
139
|
+
'@typescript-eslint/array-type': 'error',
|
|
140
|
+
'no-shadow': 'off',
|
|
141
|
+
'@typescript-eslint/no-shadow': 'warn',
|
|
142
|
+
// Block `console.log` only
|
|
143
|
+
'no-console': ['error', { allow: ['warn', 'error', 'info'] }],
|
|
144
|
+
// Disallows concatenation of string literals that can be combined into a single literal (e.g., 'foo' + 'bar' should be 'foobar').
|
|
145
|
+
'no-useless-concat': 'error',
|
|
146
|
+
'no-unused-vars': [
|
|
147
|
+
'error',
|
|
148
|
+
{
|
|
149
|
+
argsIgnorePattern: '^_',
|
|
150
|
+
varsIgnorePattern: '^_',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
'@typescript-eslint/no-unused-vars': [
|
|
154
|
+
'error',
|
|
155
|
+
{
|
|
156
|
+
argsIgnorePattern: '^_',
|
|
157
|
+
varsIgnorePattern: '^_',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
'no-restricted-imports': [
|
|
161
|
+
'error',
|
|
162
|
+
{
|
|
163
|
+
patterns: [
|
|
164
|
+
{
|
|
165
|
+
regex: '^\\.\\.\\/.*',
|
|
166
|
+
message: 'Use o alias @/ ou #/ ao invés de imports relativos com ../',
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
'sort-imports': [
|
|
172
|
+
'error',
|
|
173
|
+
{
|
|
174
|
+
allowSeparatedGroups: true,
|
|
175
|
+
ignoreCase: false,
|
|
176
|
+
ignoreDeclarationSort: true,
|
|
177
|
+
ignoreMemberSort: false,
|
|
178
|
+
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
'no-var': 'error',
|
|
182
|
+
'prefer-const': 'error',
|
|
183
|
+
'prefer-rest-params': 'error',
|
|
184
|
+
'prefer-spread': 'error',
|
|
185
|
+
'no-restricted-syntax': [
|
|
186
|
+
'error',
|
|
187
|
+
{ selector: 'ExportDefaultDeclaration', message: 'Prefer named exports' },
|
|
188
|
+
{ selector: 'ImportDeclaration[specifiers.length = 0]', message: 'Empty imports are not allowed' },
|
|
189
|
+
],
|
|
190
|
+
'no-duplicate-imports': 'error',
|
|
191
|
+
// No null in types/interfaces (allowed only with `Api` term)
|
|
192
|
+
'kitsune/no-null-in-types': [
|
|
193
|
+
'warn',
|
|
194
|
+
{
|
|
195
|
+
skipPattern: ['Api'],
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
...extraRules,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
// Disable naming convention rule for env.d.ts files
|
|
203
|
+
files: ['**/env.d.ts'],
|
|
204
|
+
rules: {
|
|
205
|
+
'@typescript-eslint/naming-convention': 'off',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
// Disable export/import default in router plugin and config files
|
|
210
|
+
files: ['src/router/*.ts', '**/*.d.{ts,js}', '**/*.config.{ts,js}', '.*/**/*.{ts,js}'],
|
|
211
|
+
rules: {
|
|
212
|
+
'no-restricted-syntax': 'off',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Linter } from 'eslint';
|
|
2
|
+
|
|
3
|
+
export type VitestFn = 'test' | 'it';
|
|
4
|
+
|
|
5
|
+
export interface VitestOptions {
|
|
6
|
+
/** File patterns override */
|
|
7
|
+
files?: string[];
|
|
8
|
+
/** Regex para validação de títulos de testes @default Gherkin PT-BR */
|
|
9
|
+
titlePattern?: string;
|
|
10
|
+
/** Mensagem de erro para títulos inválidos */
|
|
11
|
+
titleMessage?: string;
|
|
12
|
+
/** Função de teste preferida @default 'test' */
|
|
13
|
+
fn?: VitestFn;
|
|
14
|
+
/** Máximo de describe aninhados @default 3 */
|
|
15
|
+
maxNestedDescribe?: number;
|
|
16
|
+
/** Regras adicionais ou overrides */
|
|
17
|
+
rules?: Partial<Linter.RulesRecord>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export declare function vitest(options?: VitestOptions): Promise<Linter.Config[]>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { resolveFiles } from '../utils.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} VitestOptions
|
|
5
|
+
* @property {string[]} [files] - File patterns override
|
|
6
|
+
* @property {string} [titlePattern] - Regex para validação de títulos de testes
|
|
7
|
+
* @property {string} [titleMessage] - Mensagem de erro para títulos inválidos
|
|
8
|
+
* @property {'test' | 'it'} [fn='test'] - Função de teste preferida
|
|
9
|
+
* @property {number} [maxNestedDescribe=3] - Máximo de describe aninhados
|
|
10
|
+
* @property {Record<string, unknown>} [rules] - Regras adicionais ou overrides
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Padrões de título de teste em português.
|
|
14
|
+
// Gherkin padrão (Dado, Quando, Então) e também "Deve ...".
|
|
15
|
+
const GHERKIN_PT = '^Dado[^\\n]+(?:\\n\\s*(?:E|Mas)\\b[^\\n]+)*\\n\\s*Quando[^\\n]+(?:\\n\\s*(?:E|Mas)\\b[^\\n]+)*\\n\\s*Então[^\\n]+';
|
|
16
|
+
const DEVE_PT = '^Deve\\s.+$';
|
|
17
|
+
// Mensagem combinada para ambos os padrões.
|
|
18
|
+
const TITLE_PATTERN_MESSAGE =
|
|
19
|
+
'O título do test() deve seguir o padrão Gherkin em português ("Dado ...\\nQuando ...\\nEntão ...") ou iniciar com "Deve ..."';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Regras do plugin Vitest para padronização de testes.
|
|
23
|
+
* @param {VitestOptions} [options={}]
|
|
24
|
+
* @returns {Promise<import('eslint').Linter.Config[]>}
|
|
25
|
+
*/
|
|
26
|
+
export async function vitest(options = {}) {
|
|
27
|
+
const {
|
|
28
|
+
files,
|
|
29
|
+
// Aceita string ou array de strings para múltiplos padrões.
|
|
30
|
+
titlePattern = [GHERKIN_PT, DEVE_PT],
|
|
31
|
+
titleMessage = TITLE_PATTERN_MESSAGE,
|
|
32
|
+
fn = 'test',
|
|
33
|
+
maxNestedDescribe = 3,
|
|
34
|
+
rules: extraRules = {},
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
const vitestPlugin = await import('@vitest/eslint-plugin').then((m) => m.default ?? m);
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
files: resolveFiles('tests', files),
|
|
42
|
+
name: '@kitsune/vitest/rules',
|
|
43
|
+
plugins: { vitest: vitestPlugin },
|
|
44
|
+
rules: {
|
|
45
|
+
...vitestPlugin.configs.recommended.rules,
|
|
46
|
+
'vitest/consistent-test-filename': 'error',
|
|
47
|
+
'vitest/consistent-test-it': ['error', { fn }],
|
|
48
|
+
// Constrói o padrão combinado (string ou array) para a regra.
|
|
49
|
+
'vitest/valid-title': [
|
|
50
|
+
'error',
|
|
51
|
+
{
|
|
52
|
+
mustMatch: {
|
|
53
|
+
[fn]: [
|
|
54
|
+
// Se for array, une com "|" para regex alternativo.
|
|
55
|
+
Array.isArray(titlePattern) ? titlePattern.join('|') : titlePattern,
|
|
56
|
+
titleMessage,
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
'vitest/require-top-level-describe': 'error',
|
|
62
|
+
'vitest/no-identical-title': 'error',
|
|
63
|
+
'vitest/no-focused-tests': 'error',
|
|
64
|
+
'vitest/no-disabled-tests': 'warn',
|
|
65
|
+
'vitest/no-duplicate-hooks': 'error',
|
|
66
|
+
'vitest/prefer-hooks-on-top': 'error',
|
|
67
|
+
'vitest/prefer-hooks-in-order': 'error',
|
|
68
|
+
'vitest/prefer-to-be': 'error',
|
|
69
|
+
'vitest/prefer-each': 'error',
|
|
70
|
+
'vitest/no-mocks-import': 'off',
|
|
71
|
+
'vitest/max-nested-describe': ['error', { max: maxNestedDescribe }],
|
|
72
|
+
...extraRules,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Linter } from 'eslint';
|
|
2
|
+
|
|
3
|
+
export type VueApiStyle = 'script-setup' | 'composition' | 'options';
|
|
4
|
+
|
|
5
|
+
export type NameCasing = 'camelCase' | 'PascalCase' | 'kebab-case' | 'snake_case';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export interface VueOptions {
|
|
9
|
+
/** File patterns override */
|
|
10
|
+
files?: string[];
|
|
11
|
+
/** Estilo de API Vue preferido @default 'script-setup' */
|
|
12
|
+
apiStyle?: VueApiStyle;
|
|
13
|
+
/** Nomeclatura para nomes de componentes @default 'PascalCase' */
|
|
14
|
+
componentsNameCasing?: NameCasing;
|
|
15
|
+
/** Padrões de componentes ignorados para validação de nomenclatura @default 'PascalCase' */
|
|
16
|
+
componentsNameCasingIgnores?: string[];
|
|
17
|
+
/** Nomeclatura para nomes de propriedades @default 'camelCase' */
|
|
18
|
+
propNameCasing?: NameCasing;
|
|
19
|
+
/** Nomeclatura para nomes de slots @default 'kebab-case' */
|
|
20
|
+
slotNameCasing?: NameCasing;
|
|
21
|
+
/** Regras adicionais ou overrides */
|
|
22
|
+
rules?: Partial<Linter.RulesRecord>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare function vue(options?: VueOptions): Linter.Config[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pluginVue from 'eslint-plugin-vue';
|
|
2
|
+
import tseslint from 'typescript-eslint';
|
|
3
|
+
import vueParser from 'vue-eslint-parser';
|
|
4
|
+
|
|
5
|
+
import { resolveFiles } from '../utils.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} VueOptions
|
|
9
|
+
* @property {string[]} [files] - File patterns override
|
|
10
|
+
* @property {'script-setup' | 'composition' | 'options'} [apiStyle='script-setup']
|
|
11
|
+
* @property {Record<string, unknown>} [rules] - Regras adicionais ou overrides
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Regras para projetos Vue 3 com TypeScript.
|
|
16
|
+
* @param {VueOptions} [options={}]
|
|
17
|
+
* @returns {import('eslint').Linter.Config[]}
|
|
18
|
+
*/
|
|
19
|
+
export function vue(options = {}) {
|
|
20
|
+
const {
|
|
21
|
+
files,
|
|
22
|
+
apiStyle = 'script-setup',
|
|
23
|
+
rules: extraRules = {},
|
|
24
|
+
componentsNameCasing = 'PascalCase',
|
|
25
|
+
componentsNameCasingIgnores = [],
|
|
26
|
+
propNameCasing = 'camelCase',
|
|
27
|
+
slotNameCasing = 'kebab-case',
|
|
28
|
+
} = options;
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
...pluginVue.configs['flat/recommended'],
|
|
32
|
+
{
|
|
33
|
+
files: resolveFiles('vue', files),
|
|
34
|
+
name: '@kitsune/vue/rules',
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parser: vueParser,
|
|
37
|
+
parserOptions: {
|
|
38
|
+
parser: tseslint.parser,
|
|
39
|
+
extraFileExtensions: ['.vue'],
|
|
40
|
+
projectService: true,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
rules: {
|
|
44
|
+
'no-undef': 'off',
|
|
45
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
46
|
+
'vue/block-lang': ['error', { script: { lang: 'ts' } }],
|
|
47
|
+
'vue/block-order': ['error', { order: ['script', 'template', 'style'] }],
|
|
48
|
+
'vue/block-tag-newline': 'error',
|
|
49
|
+
'vue/component-api-style': ['error', [apiStyle]],
|
|
50
|
+
'vue/define-props-declaration': ['error', 'type-based'],
|
|
51
|
+
'vue/define-emits-declaration': ['error', 'type-based'],
|
|
52
|
+
'vue/no-setup-props-reactivity-loss': 'error',
|
|
53
|
+
'vue/no-undef-properties': 'error',
|
|
54
|
+
'vue/no-unused-emit-declarations': 'error',
|
|
55
|
+
'vue/no-useless-v-bind': 'error',
|
|
56
|
+
'vue/padding-line-between-blocks': ['error', 'always'],
|
|
57
|
+
'vue/no-static-inline-styles': 'error',
|
|
58
|
+
'vue/require-typed-ref': 'error',
|
|
59
|
+
'vue/prop-name-casing': ['error', propNameCasing],
|
|
60
|
+
'vue/slot-name-casing': ['error', slotNameCasing],
|
|
61
|
+
'vue/component-name-in-template-casing': [
|
|
62
|
+
'error',
|
|
63
|
+
componentsNameCasing,
|
|
64
|
+
{
|
|
65
|
+
registeredComponentsOnly: false,
|
|
66
|
+
ignores: componentsNameCasingIgnores,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
...extraRules,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Linter } from 'eslint';
|
|
2
|
+
|
|
3
|
+
import type { BaseOptions } from './configs/base.mjs';
|
|
4
|
+
import type { CleanCodeOptions } from './configs/clean-code.mjs';
|
|
5
|
+
import type { PiniaOptions } from './configs/pinia.mjs';
|
|
6
|
+
import type { SecurityOptions } from './configs/security.mjs';
|
|
7
|
+
import type { TestsOptions } from './configs/tests.mjs';
|
|
8
|
+
import type { TypescriptOptions } from './configs/typescript.mjs';
|
|
9
|
+
import type { VitestOptions } from './configs/vitest.mjs';
|
|
10
|
+
import type { VueOptions } from './configs/vue.mjs';
|
|
11
|
+
|
|
12
|
+
export type { BaseOptions } from './configs/base.mjs';
|
|
13
|
+
export type { CleanCodeOptions } from './configs/clean-code.mjs';
|
|
14
|
+
export type { PiniaOptions } from './configs/pinia.mjs';
|
|
15
|
+
export type { SecurityOptions } from './configs/security.mjs';
|
|
16
|
+
export type { TestsOptions } from './configs/tests.mjs';
|
|
17
|
+
export type { TypescriptOptions } from './configs/typescript.mjs';
|
|
18
|
+
export type { VitestFn, VitestOptions } from './configs/vitest.mjs';
|
|
19
|
+
export type { VueApiStyle, VueOptions } from './configs/vue.mjs';
|
|
20
|
+
|
|
21
|
+
export { base } from './configs/base.mjs';
|
|
22
|
+
export { cleanCode } from './configs/clean-code.mjs';
|
|
23
|
+
export { pinia } from './configs/pinia.mjs';
|
|
24
|
+
export { security } from './configs/security.mjs';
|
|
25
|
+
export { tests } from './configs/tests.mjs';
|
|
26
|
+
export { typescript } from './configs/typescript.mjs';
|
|
27
|
+
export { vitest } from './configs/vitest.mjs';
|
|
28
|
+
export { vue } from './configs/vue.mjs';
|
|
29
|
+
|
|
30
|
+
export interface CreateKitsuneConfigOptions {
|
|
31
|
+
/** Configuração base (globals do ambiente) @default true */
|
|
32
|
+
base?: BaseOptions | boolean;
|
|
33
|
+
/** Regras TypeScript + naming conventions @default true */
|
|
34
|
+
typescript?: TypescriptOptions | boolean;
|
|
35
|
+
/** Regras de segurança (eval, XSS, injection) @default true */
|
|
36
|
+
security?: SecurityOptions | boolean;
|
|
37
|
+
/** Regras de clean code (SRP, legibilidade, imutabilidade) @default true */
|
|
38
|
+
cleanCode?: CleanCodeOptions | boolean;
|
|
39
|
+
/** Regras Vue 3 (script setup, block order) @default false */
|
|
40
|
+
vue?: VueOptions | boolean;
|
|
41
|
+
/** Regras Pinia (naming, organização de stores) @default false */
|
|
42
|
+
pinia?: PiniaOptions | boolean;
|
|
43
|
+
/** Relaxamentos para arquivos de teste @default false */
|
|
44
|
+
tests?: TestsOptions | boolean;
|
|
45
|
+
/** Regras Vitest (plugin vitest) @default false */
|
|
46
|
+
vitest?: VitestOptions | boolean;
|
|
47
|
+
/** Configs adicionais a incluir no final (ex: eslint-config-prettier) */
|
|
48
|
+
extend?: Linter.Config[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export declare function createKitsuneConfig(options?: CreateKitsuneConfigOptions): Promise<Linter.Config[]>;
|
package/eslint/index.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export { base } from './configs/base.mjs';
|
|
2
|
+
export { cleanCode } from './configs/clean-code.mjs';
|
|
3
|
+
export { pinia } from './configs/pinia.mjs';
|
|
4
|
+
export { security } from './configs/security.mjs';
|
|
5
|
+
export { tests } from './configs/tests.mjs';
|
|
6
|
+
export { typescript } from './configs/typescript.mjs';
|
|
7
|
+
export { vitest } from './configs/vitest.mjs';
|
|
8
|
+
export { vue } from './configs/vue.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} CreateKitsuneConfigOptions
|
|
12
|
+
* @property {import('./configs/base.mjs').BaseOptions | boolean} [base=true]
|
|
13
|
+
* @property {import('./configs/typescript.mjs').TypescriptOptions | boolean} [typescript=true]
|
|
14
|
+
* @property {import('./configs/security.mjs').SecurityOptions | boolean} [security=true]
|
|
15
|
+
* @property {import('./configs/clean-code.mjs').CleanCodeOptions | boolean} [cleanCode=true]
|
|
16
|
+
* @property {import('./configs/vue.mjs').VueOptions | boolean} [vue=false]
|
|
17
|
+
* @property {import('./configs/pinia.mjs').PiniaOptions | boolean} [pinia=false]
|
|
18
|
+
* @property {import('./configs/tests.mjs').TestsOptions | boolean} [tests=false]
|
|
19
|
+
* @property {import('./configs/vitest.mjs').VitestOptions | boolean} [vitest=false]
|
|
20
|
+
* @property {import('eslint').Linter.Config[]} [extend] - Configs adicionais a incluir no final
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Factory que compõe uma configuração ESLint completa a partir de módulos selecionados.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { createKitsuneConfig } from '@polariens/kitsune-lint/eslint';
|
|
28
|
+
* export default await createKitsuneConfig();
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* import { createKitsuneConfig } from '@polariens/kitsune-lint/eslint';
|
|
32
|
+
* export default await createKitsuneConfig({ vue: true, pinia: true, tests: true, vitest: true });
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* import { createKitsuneConfig } from '@polariens/kitsune-lint/eslint';
|
|
36
|
+
* export default await createKitsuneConfig({
|
|
37
|
+
* base: { environment: 'node' },
|
|
38
|
+
* cleanCode: { maxDepth: 3, maxParams: 3, complexity: 10 },
|
|
39
|
+
* vue: { apiStyle: 'composition' },
|
|
40
|
+
* pinia: true,
|
|
41
|
+
* tests: true,
|
|
42
|
+
* vitest: true,
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* @param {CreateKitsuneConfigOptions} [options={}]
|
|
46
|
+
* @returns {Promise<import('eslint').Linter.Config[]>}
|
|
47
|
+
*/
|
|
48
|
+
export async function createKitsuneConfig(options = {}) {
|
|
49
|
+
const {
|
|
50
|
+
base: baseOpt = true,
|
|
51
|
+
typescript: tsOpt = true,
|
|
52
|
+
security: secOpt = true,
|
|
53
|
+
cleanCode: cleanOpt = true,
|
|
54
|
+
vue: vueOpt = false,
|
|
55
|
+
pinia: piniaOpt = false,
|
|
56
|
+
tests: testsOpt = false,
|
|
57
|
+
vitest: vitestOpt = false,
|
|
58
|
+
extend = [],
|
|
59
|
+
} = options;
|
|
60
|
+
|
|
61
|
+
const configs = [];
|
|
62
|
+
|
|
63
|
+
const moduleMap = [
|
|
64
|
+
{ opt: baseOpt, name: 'base', file: 'base' },
|
|
65
|
+
{ opt: tsOpt, name: 'typescript', file: 'typescript' },
|
|
66
|
+
{ opt: secOpt, name: 'security', file: 'security' },
|
|
67
|
+
{ opt: cleanOpt, name: 'cleanCode', file: 'clean-code' },
|
|
68
|
+
{ opt: vueOpt, name: 'vue', file: 'vue' },
|
|
69
|
+
{ opt: piniaOpt, name: 'pinia', file: 'pinia' },
|
|
70
|
+
{ opt: testsOpt, name: 'tests', file: 'tests' },
|
|
71
|
+
{ opt: vitestOpt, name: 'vitest', file: 'vitest' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const { opt, name, file } of moduleMap) {
|
|
75
|
+
if (opt) {
|
|
76
|
+
const mod = await import(`./configs/${file}.mjs`);
|
|
77
|
+
const configArr = await mod[name](opt === true ? {} : opt);
|
|
78
|
+
configs.push(...configArr);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
configs.push(...extend);
|
|
83
|
+
|
|
84
|
+
return configs;
|
|
85
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule: disallow `null` in interfaces/types unless
|
|
3
|
+
* the interface/type name contains a specific string defined via
|
|
4
|
+
* configuration. Default is "Api".
|
|
5
|
+
*
|
|
6
|
+
* @file
|
|
7
|
+
*/
|
|
8
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description:
|
|
15
|
+
'Disallow `null` in interfaces/types unless the name contains a configured string.',
|
|
16
|
+
category: 'Possible Errors',
|
|
17
|
+
recommended: false,
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
noNull:
|
|
21
|
+
'Type "{{name}}" uses `null`; only interfaces/types whose name contains "{{skipPattern}}" may contain `null`.',
|
|
22
|
+
},
|
|
23
|
+
schema: [
|
|
24
|
+
{
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
skipPattern: {
|
|
28
|
+
oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }],
|
|
29
|
+
description:
|
|
30
|
+
'String or array of strings that, if present in the type name, skips the null check.',
|
|
31
|
+
default: 'Api',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
create(context) {
|
|
40
|
+
const options = context.options?.[0] ?? {};
|
|
41
|
+
let skipPatterns = options.skipPattern ?? 'Api';
|
|
42
|
+
if (!Array.isArray(skipPatterns)) {
|
|
43
|
+
skipPatterns = [skipPatterns];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detects if a type node contains a `null` literal.
|
|
48
|
+
*
|
|
49
|
+
* @param {TSESTree.TSType} node
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
function containsNull(node) {
|
|
53
|
+
if (!node) return false;
|
|
54
|
+
|
|
55
|
+
switch (node.type) {
|
|
56
|
+
case AST_NODE_TYPES.TSNullKeyword:
|
|
57
|
+
return true;
|
|
58
|
+
case AST_NODE_TYPES.TSUnionType:
|
|
59
|
+
return node.types.some(containsNull);
|
|
60
|
+
case AST_NODE_TYPES.TSTypeAnnotation:
|
|
61
|
+
return containsNull(node.typeAnnotation);
|
|
62
|
+
case AST_NODE_TYPES.TSTypeLiteral:
|
|
63
|
+
// Check members only for object literal types
|
|
64
|
+
return node.members.some(
|
|
65
|
+
(m) =>
|
|
66
|
+
m.type === AST_NODE_TYPES.TSPropertySignature &&
|
|
67
|
+
containsNull(m.typeAnnotation?.typeAnnotation),
|
|
68
|
+
);
|
|
69
|
+
default:
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handles property signatures inside interfaces or type literals.
|
|
76
|
+
*
|
|
77
|
+
* @param {TSESTree.TSPropertySignature} prop
|
|
78
|
+
* @param {string} typeName
|
|
79
|
+
*/
|
|
80
|
+
function checkProperty(prop, typeName) {
|
|
81
|
+
const typeNode = prop.typeAnnotation?.typeAnnotation;
|
|
82
|
+
if (typeNode && containsNull(typeNode)) {
|
|
83
|
+
context.report({
|
|
84
|
+
node: prop,
|
|
85
|
+
messageId: 'noNull',
|
|
86
|
+
data: { name: typeName, skipPattern: skipPatterns.join(', ') },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
TSInterfaceDeclaration(node) {
|
|
93
|
+
const name = node.id?.name;
|
|
94
|
+
if (!name || skipPatterns.some((pattern) => name.includes(pattern))) return; // skip based on config
|
|
95
|
+
|
|
96
|
+
node.body.body.forEach((member) => {
|
|
97
|
+
if (member.type === AST_NODE_TYPES.TSPropertySignature) {
|
|
98
|
+
checkProperty(member, name);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
TSTypeAliasDeclaration(node) {
|
|
104
|
+
const name = node.id?.name;
|
|
105
|
+
if (!name || skipPatterns.some((pattern) => name.includes(pattern))) return; // skip based on config
|
|
106
|
+
|
|
107
|
+
if (node.typeAnnotation.type === 'TSTypeLiteral') {
|
|
108
|
+
node.typeAnnotation.members.forEach((member) => {
|
|
109
|
+
if (member.type === AST_NODE_TYPES.TSPropertySignature) {
|
|
110
|
+
checkProperty(member, name);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type FilePatternKey = 'all' | 'tests' | 'vue' | 'pinia' | 'configs';
|
|
2
|
+
|
|
3
|
+
export declare const FILE_PATTERNS: Record<FilePatternKey, string[]>;
|
|
4
|
+
|
|
5
|
+
export declare const IGNORE_PATTERNS: string[];
|
|
6
|
+
|
|
7
|
+
export declare function resolveFiles(key: FilePatternKey, custom?: string[]): string[];
|