@polariens/kitsune-lint 1.0.0-rc.3
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 +233 -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 +77 -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 +12 -0
- package/eslint/configs/typescript.mjs +184 -0
- package/eslint/configs/vitest.d.mts +20 -0
- package/eslint/configs/vitest.mjs +62 -0
- package/eslint/configs/vue.d.mts +25 -0
- package/eslint/configs/vue.mjs +71 -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 +20 -0
- package/package.json +117 -0
- package/prettier/.prettierignore +12 -0
- package/prettier/index.d.mts +44 -0
- package/prettier/index.mjs +78 -0
|
@@ -0,0 +1,184 @@
|
|
|
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, rules: extraRules = {} } = options;
|
|
21
|
+
const resolvedFiles = resolveFiles('all', files);
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
ignores: ignores ?? ['**/*.config.{js,mjs,cjs,ts}', ...IGNORE_PATTERNS],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
...eslint.configs.recommended,
|
|
29
|
+
files: resolvedFiles,
|
|
30
|
+
},
|
|
31
|
+
...tseslint.configs.recommended.map((config) => ({
|
|
32
|
+
...config,
|
|
33
|
+
files: resolvedFiles,
|
|
34
|
+
})),
|
|
35
|
+
{
|
|
36
|
+
plugins: {
|
|
37
|
+
kitsune: {
|
|
38
|
+
rules: {
|
|
39
|
+
'no-null-in-types': noNullInTypes,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
files: resolvedFiles,
|
|
46
|
+
name: '@kitsune/typescript/rules',
|
|
47
|
+
rules: {
|
|
48
|
+
// Prefer `===` or `!==` (never == or !=)
|
|
49
|
+
eqeqeq: ['error', 'always'],
|
|
50
|
+
'@typescript-eslint/no-empty-object-type': [
|
|
51
|
+
'error',
|
|
52
|
+
{
|
|
53
|
+
allowObjectTypes: 'never',
|
|
54
|
+
allowInterfaces: 'with-single-extends',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
'@typescript-eslint/naming-convention': [
|
|
58
|
+
'error',
|
|
59
|
+
{ selector: 'default', format: ['camelCase'] },
|
|
60
|
+
{ selector: 'variable', format: ['camelCase'] },
|
|
61
|
+
{ selector: 'function', format: ['camelCase'] },
|
|
62
|
+
{ selector: 'class', format: ['PascalCase'] },
|
|
63
|
+
{
|
|
64
|
+
selector: 'interface',
|
|
65
|
+
format: ['PascalCase'],
|
|
66
|
+
custom: { regex: '^(OAuth|[A-Z][a-z])', match: true },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
selector: 'typeAlias',
|
|
70
|
+
format: ['PascalCase'],
|
|
71
|
+
custom: { regex: '^(OAuth|[A-Z][a-z])', match: true },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
// camelCase for properties of interfaces/types
|
|
75
|
+
selector: 'typeProperty',
|
|
76
|
+
format: ['camelCase'],
|
|
77
|
+
leadingUnderscore: 'allow',
|
|
78
|
+
trailingUnderscore: 'allow',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// camelCase for class members
|
|
82
|
+
selector: 'classProperty',
|
|
83
|
+
format: ['camelCase'],
|
|
84
|
+
leadingUnderscore: 'allow',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
// camelCase for methods of classes
|
|
88
|
+
selector: 'classMethod',
|
|
89
|
+
format: ['camelCase'],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
// PascalCase for parameters of generic types of only one char
|
|
93
|
+
selector: 'typeParameter',
|
|
94
|
+
format: ['PascalCase'],
|
|
95
|
+
custom: { regex: '^[A-Z]$', match: true },
|
|
96
|
+
},
|
|
97
|
+
// PascalCase for enums names
|
|
98
|
+
{ selector: 'enum', format: ['PascalCase'] },
|
|
99
|
+
{
|
|
100
|
+
// UPPER_CASE for enum members
|
|
101
|
+
selector: 'enumMember',
|
|
102
|
+
format: null,
|
|
103
|
+
custom: { regex: `^['"]?[A-Z]+([-_][A-Z]+)*['"]?$`, match: true },
|
|
104
|
+
},
|
|
105
|
+
// camelCase for object literal properties
|
|
106
|
+
{ selector: 'objectLiteralProperty', format: null },
|
|
107
|
+
// camelCase or PascalCase for imports
|
|
108
|
+
{ selector: 'import', format: ['camelCase', 'PascalCase'] },
|
|
109
|
+
],
|
|
110
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
111
|
+
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
|
|
112
|
+
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
|
113
|
+
'@typescript-eslint/no-non-null-assertion': 'warn',
|
|
114
|
+
'@typescript-eslint/array-type': 'error',
|
|
115
|
+
'no-shadow': 'off',
|
|
116
|
+
'@typescript-eslint/no-shadow': 'warn',
|
|
117
|
+
// Block `console.log` only
|
|
118
|
+
'no-console': ['error', { allow: ['warn', 'error', 'info'] }],
|
|
119
|
+
// Disallows concatenation of string literals that can be combined into a single literal (e.g., 'foo' + 'bar' should be 'foobar').
|
|
120
|
+
'no-useless-concat': 'error',
|
|
121
|
+
'no-unused-vars': [
|
|
122
|
+
'error',
|
|
123
|
+
{
|
|
124
|
+
argsIgnorePattern: '^_',
|
|
125
|
+
varsIgnorePattern: '^_',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
'@typescript-eslint/no-unused-vars': [
|
|
129
|
+
'error',
|
|
130
|
+
{
|
|
131
|
+
argsIgnorePattern: '^_',
|
|
132
|
+
varsIgnorePattern: '^_',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
'no-restricted-imports': [
|
|
136
|
+
'error',
|
|
137
|
+
{
|
|
138
|
+
patterns: [
|
|
139
|
+
{
|
|
140
|
+
regex: '^\\.\\.\\/.*',
|
|
141
|
+
message: 'Use o alias @/ ao invés de imports relativos com ../',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
'sort-imports': [
|
|
147
|
+
'error',
|
|
148
|
+
{
|
|
149
|
+
allowSeparatedGroups: true,
|
|
150
|
+
ignoreCase: false,
|
|
151
|
+
ignoreDeclarationSort: true,
|
|
152
|
+
ignoreMemberSort: false,
|
|
153
|
+
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
'no-var': 'error',
|
|
157
|
+
'prefer-const': 'error',
|
|
158
|
+
'prefer-rest-params': 'error',
|
|
159
|
+
'prefer-spread': 'error',
|
|
160
|
+
'no-restricted-syntax': [
|
|
161
|
+
'error',
|
|
162
|
+
{ selector: 'ExportDefaultDeclaration', message: 'Prefer named exports' },
|
|
163
|
+
{ selector: 'ImportDeclaration[specifiers.length = 0]', message: 'Empty imports are not allowed' },
|
|
164
|
+
],
|
|
165
|
+
'no-duplicate-imports': 'error',
|
|
166
|
+
// No null in types/interfaces (allowed only with `Api` term)
|
|
167
|
+
'kitsune/no-null-in-types': [
|
|
168
|
+
'warn',
|
|
169
|
+
{
|
|
170
|
+
skipPattern: ['Api'],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
...extraRules,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
// Disable export/import default in router plugin and config files
|
|
178
|
+
files: ['src/router/*.ts', '**/*.config.{ts,js}', '.*/**/*.{ts,js}'],
|
|
179
|
+
rules: {
|
|
180
|
+
'no-restricted-syntax': 'off',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
@@ -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,62 @@
|
|
|
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
|
+
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]+';
|
|
14
|
+
const GHERKIN_PT_MESSAGE =
|
|
15
|
+
'O título do test() deve seguir o padrão Gherkin em português: "Dado ...\\nQuando ...\\nEntão ..."';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Regras do plugin Vitest para padronização de testes.
|
|
19
|
+
* @param {VitestOptions} [options={}]
|
|
20
|
+
* @returns {Promise<import('eslint').Linter.Config[]>}
|
|
21
|
+
*/
|
|
22
|
+
export async function vitest(options = {}) {
|
|
23
|
+
const {
|
|
24
|
+
files,
|
|
25
|
+
titlePattern = GHERKIN_PT,
|
|
26
|
+
titleMessage = GHERKIN_PT_MESSAGE,
|
|
27
|
+
fn = 'test',
|
|
28
|
+
maxNestedDescribe = 3,
|
|
29
|
+
rules: extraRules = {},
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
const vitestPlugin = await import('@vitest/eslint-plugin').then((m) => m.default ?? m);
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
files: resolveFiles('tests', files),
|
|
37
|
+
name: '@kitsune/vitest/rules',
|
|
38
|
+
plugins: { vitest: vitestPlugin },
|
|
39
|
+
rules: {
|
|
40
|
+
...vitestPlugin.configs.recommended.rules,
|
|
41
|
+
'vitest/consistent-test-filename': 'error',
|
|
42
|
+
'vitest/consistent-test-it': ['error', { fn }],
|
|
43
|
+
'vitest/valid-title': [
|
|
44
|
+
'error',
|
|
45
|
+
{ mustMatch: { [fn]: [titlePattern, titleMessage] } },
|
|
46
|
+
],
|
|
47
|
+
'vitest/require-top-level-describe': 'error',
|
|
48
|
+
'vitest/no-identical-title': 'error',
|
|
49
|
+
'vitest/no-focused-tests': 'error',
|
|
50
|
+
'vitest/no-disabled-tests': 'warn',
|
|
51
|
+
'vitest/no-duplicate-hooks': 'error',
|
|
52
|
+
'vitest/prefer-hooks-on-top': 'error',
|
|
53
|
+
'vitest/prefer-hooks-in-order': 'error',
|
|
54
|
+
'vitest/prefer-to-be': 'error',
|
|
55
|
+
'vitest/prefer-each': 'error',
|
|
56
|
+
'vitest/no-mocks-import': 'off',
|
|
57
|
+
'vitest/max-nested-describe': ['error', { max: maxNestedDescribe }],
|
|
58
|
+
...extraRules,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
@@ -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,71 @@
|
|
|
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
|
+
},
|
|
40
|
+
},
|
|
41
|
+
rules: {
|
|
42
|
+
'no-undef': 'off',
|
|
43
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
44
|
+
'vue/block-lang': ['error', { script: { lang: 'ts' } }],
|
|
45
|
+
'vue/block-order': ['error', { order: ['script', 'template', 'style'] }],
|
|
46
|
+
'vue/block-tag-newline': 'error',
|
|
47
|
+
'vue/component-api-style': ['error', [apiStyle]],
|
|
48
|
+
'vue/define-props-declaration': ['error', 'type-based'],
|
|
49
|
+
'vue/define-emits-declaration': ['error', 'type-based'],
|
|
50
|
+
'vue/no-setup-props-reactivity-loss': 'error',
|
|
51
|
+
'vue/no-undef-properties': 'error',
|
|
52
|
+
'vue/no-unused-emit-declarations': 'error',
|
|
53
|
+
'vue/no-useless-v-bind': 'error',
|
|
54
|
+
'vue/padding-line-between-blocks': ['error', 'always'],
|
|
55
|
+
'vue/no-static-inline-styles': 'error',
|
|
56
|
+
'vue/require-typed-ref': 'error',
|
|
57
|
+
'vue/prop-name-casing': ['error', propNameCasing],
|
|
58
|
+
'vue/slot-name-casing': ['error', slotNameCasing],
|
|
59
|
+
'vue/component-name-in-template-casing': [
|
|
60
|
+
'error',
|
|
61
|
+
componentsNameCasing,
|
|
62
|
+
{
|
|
63
|
+
registeredComponentsOnly: false,
|
|
64
|
+
ignores: componentsNameCasingIgnores,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
...extraRules,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
@@ -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 '@merieli/kitsune-lint/eslint';
|
|
28
|
+
* export default await createKitsuneConfig();
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* import { createKitsuneConfig } from '@merieli/kitsune-lint/eslint';
|
|
32
|
+
* export default await createKitsuneConfig({ vue: true, pinia: true, tests: true, vitest: true });
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* import { createKitsuneConfig } from '@merieli/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[];
|
package/eslint/utils.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const FILE_PATTERNS = {
|
|
2
|
+
all: ['**/*.{vue,js,mjs,cjs,ts}'],
|
|
3
|
+
tests: ['tests/**/*.{js,mjs,cjs,ts}'],
|
|
4
|
+
vue: ['**/*.vue'],
|
|
5
|
+
pinia: ['src/state/**/*.{ts}'],
|
|
6
|
+
configs: ['**/*.config.{js,mjs,cjs,ts}'],
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const IGNORE_PATTERNS = ['coverage', 'dist', 'node_modules'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Mescla file patterns customizados com os padrões.
|
|
13
|
+
* @param {string} key - Chave do FILE_PATTERNS a usar como base
|
|
14
|
+
* @param {string[] | undefined} custom - Patterns adicionais do consumidor
|
|
15
|
+
* @returns {string[]}
|
|
16
|
+
*/
|
|
17
|
+
export function resolveFiles(key, custom) {
|
|
18
|
+
if (custom) return custom;
|
|
19
|
+
return FILE_PATTERNS[key] ?? FILE_PATTERNS.all;
|
|
20
|
+
}
|