@niko464/eslint-config 0.0.1 → 0.1.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/README.md +57 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +42 -0
- package/dist/no-comments.d.ts +2 -0
- package/dist/no-comments.js +25 -0
- package/dist/no-effect-hooks.d.ts +2 -0
- package/dist/no-effect-hooks.js +63 -0
- package/package.json +38 -5
package/README.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
1
|
# @niko464/eslint-config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared ESLint flat-config blocks for Niko464's TypeScript projects. Each export is an array of config objects. Spread the ones a project needs into its own `eslint.config.*`.
|
|
4
|
+
|
|
5
|
+
| Block | For | What it turns on |
|
|
6
|
+
| -------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `base` | every TypeScript project | `@niko464/no-comments`, and `linterOptions.noInlineConfig: true`, so no `eslint-disable` or inline rule comment has any effect |
|
|
8
|
+
| `react` | React projects | `@niko464/no-effect-hooks`: bans `useEffect`, `useLayoutEffect` and `useInsertionEffect` from `react`, however they are imported |
|
|
9
|
+
| `shadcn` | Tailwind v4 + shadcn/ui projects | [`@shadcn/lint`](https://github.com/shadcn-ui/lint) with all six rules as errors: `no-restyle`, `no-raw-colors`, `no-arbitrary-values`, `no-inline-styles`, `no-unknown-classes`, `require-static-classes` |
|
|
10
|
+
|
|
11
|
+
`@niko464/no-comments` reports every `//`, `/* */` and `/** */` comment except the tool directives: the shebang, `/// <reference`, `@ts-expect-error` and `prettier-ignore`. It has no autofix: what a comment says belongs in a name, a type, a test, `docs/` or an ADR, and only a person or an agent can move it there.
|
|
12
|
+
|
|
13
|
+
`@niko464/no-effect-hooks` leaves `useEffectEvent`, `useSyncExternalStore` and ref callbacks allowed. Its error names the replacements: `useMountEffect`, `useEventListener` or `useInterval` from `@niko464/react-kit`, a `key`, a ref callback, deriving the value during render, or Motion for animation. A case none of these covers becomes a new hook in `@niko464/react-kit`, through a PR.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add -D @niko464/eslint-config eslint typescript-eslint
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
ESLint 9.30 or later is required, and `@typescript-eslint/parser` 8.40 or later (it comes with `typescript-eslint`).
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { base, react, shadcn } from '@niko464/eslint-config';
|
|
27
|
+
import tseslint from 'typescript-eslint';
|
|
28
|
+
|
|
29
|
+
export default [...tseslint.configs.recommended, ...base, ...react, ...shadcn];
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A project without React spreads only `base`. A project without Tailwind or shadcn/ui leaves out `shadcn`.
|
|
33
|
+
|
|
34
|
+
### In a monorepo
|
|
35
|
+
|
|
36
|
+
`defineConfig`'s `extends` scopes a block to part of the repo:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import js from '@eslint/js';
|
|
40
|
+
import { base, react, shadcn } from '@niko464/eslint-config';
|
|
41
|
+
import { defineConfig } from 'eslint/config';
|
|
42
|
+
import tseslint from 'typescript-eslint';
|
|
43
|
+
|
|
44
|
+
export default defineConfig(
|
|
45
|
+
{ ignores: ['dist/'] },
|
|
46
|
+
js.configs.recommended,
|
|
47
|
+
tseslint.configs.recommended,
|
|
48
|
+
{
|
|
49
|
+
files: ['apps/api/**/*.ts'],
|
|
50
|
+
rules: { 'no-restricted-imports': ['error', { patterns: ['@prisma/client'] }] },
|
|
51
|
+
},
|
|
52
|
+
base,
|
|
53
|
+
{ files: ['apps/web/**/*.{ts,tsx}'], extends: [react, shadcn] },
|
|
54
|
+
{ files: ['apps/web/components/ui/**'], rules: { 'shadcn/no-restyle': 'off' } },
|
|
55
|
+
);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- The bans have their own rule IDs, so a project's own `no-restricted-imports` or `no-restricted-syntax` sits next to them without replacing them.
|
|
59
|
+
- With inline config off, every exception lives in `eslint.config.*` as a `files`-scoped block, where review sees it.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { plugin as shadcnPlugin } from '@shadcn/lint';
|
|
2
|
+
import tsParser from '@typescript-eslint/parser';
|
|
3
|
+
import { noComments } from "./no-comments.js";
|
|
4
|
+
import { noEffectHooks } from "./no-effect-hooks.js";
|
|
5
|
+
const plugin = {
|
|
6
|
+
meta: { name: '@niko464/eslint-config' },
|
|
7
|
+
rules: { 'no-comments': noComments, 'no-effect-hooks': noEffectHooks },
|
|
8
|
+
};
|
|
9
|
+
export const base = [
|
|
10
|
+
{
|
|
11
|
+
name: '@niko464/base',
|
|
12
|
+
plugins: { '@niko464': plugin },
|
|
13
|
+
linterOptions: { noInlineConfig: true },
|
|
14
|
+
rules: { '@niko464/no-comments': 'error' },
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
export const react = [
|
|
18
|
+
{
|
|
19
|
+
name: '@niko464/react',
|
|
20
|
+
plugins: { '@niko464': plugin },
|
|
21
|
+
rules: { '@niko464/no-effect-hooks': 'error' },
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
export const shadcn = [
|
|
25
|
+
{
|
|
26
|
+
name: '@niko464/shadcn',
|
|
27
|
+
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
28
|
+
languageOptions: {
|
|
29
|
+
parser: tsParser,
|
|
30
|
+
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
31
|
+
},
|
|
32
|
+
plugins: { shadcn: shadcnPlugin },
|
|
33
|
+
rules: {
|
|
34
|
+
'shadcn/no-restyle': 'error',
|
|
35
|
+
'shadcn/no-raw-colors': 'error',
|
|
36
|
+
'shadcn/no-arbitrary-values': 'error',
|
|
37
|
+
'shadcn/no-inline-styles': 'error',
|
|
38
|
+
'shadcn/no-unknown-classes': 'error',
|
|
39
|
+
'shadcn/require-static-classes': 'error',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const TOOL_DIRECTIVES = [/^\s*@ts-expect-error(?:\s|:|$)/, /^\s*prettier-ignore\s*$/];
|
|
2
|
+
const TRIPLE_SLASH_REFERENCE = /^\/\s*<reference\s/;
|
|
3
|
+
const isToolDirective = (comment) => comment.type === 'Shebang' ||
|
|
4
|
+
(comment.type === 'Line' && TRIPLE_SLASH_REFERENCE.test(comment.value)) ||
|
|
5
|
+
TOOL_DIRECTIVES.some((pattern) => pattern.test(comment.value));
|
|
6
|
+
export const noComments = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
comment: 'Comments are banned. Move what this comment says into a name, a type or a test, and put a "why" worth keeping in docs/ or an ADR.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
Program() {
|
|
17
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
18
|
+
if (!isToolDirective(comment) && comment.loc) {
|
|
19
|
+
context.report({ loc: comment.loc, messageId: 'comment' });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const EFFECT_HOOKS = new Set(['useEffect', 'useLayoutEffect', 'useInsertionEffect']);
|
|
2
|
+
const keyName = (key, computed) => {
|
|
3
|
+
if (key.type === 'Literal')
|
|
4
|
+
return String(key.value);
|
|
5
|
+
if (key.type === 'Identifier' && !computed)
|
|
6
|
+
return key.name;
|
|
7
|
+
return null;
|
|
8
|
+
};
|
|
9
|
+
export const noEffectHooks = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
effect: '{{name}} is banned. Use useMountEffect, useEventListener or useInterval from @niko464/react-kit, a key, a ref callback, deriving the value during render, or Motion for animation. A case none of these covers becomes a new hook in @niko464/react-kit, through a PR.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const reportIfEffect = (node, name) => {
|
|
19
|
+
if (name !== null && EFFECT_HOOKS.has(name)) {
|
|
20
|
+
context.report({ node, messageId: 'effect', data: { name } });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const reportNamespaceUse = (reference) => {
|
|
24
|
+
const identifier = reference.identifier;
|
|
25
|
+
const parent = identifier.parent;
|
|
26
|
+
if (parent?.type === 'MemberExpression' && parent.object === identifier) {
|
|
27
|
+
reportIfEffect(parent, keyName(parent.property, parent.computed));
|
|
28
|
+
}
|
|
29
|
+
if (parent?.type === 'VariableDeclarator' &&
|
|
30
|
+
parent.init === identifier &&
|
|
31
|
+
parent.id.type === 'ObjectPattern') {
|
|
32
|
+
for (const property of parent.id.properties) {
|
|
33
|
+
if (property.type === 'Property') {
|
|
34
|
+
reportIfEffect(property, keyName(property.key, property.computed));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
ImportDeclaration(node) {
|
|
41
|
+
if (node.source.value !== 'react')
|
|
42
|
+
return;
|
|
43
|
+
for (const specifier of node.specifiers) {
|
|
44
|
+
const imported = specifier.type === 'ImportSpecifier' ? keyName(specifier.imported, false) : 'default';
|
|
45
|
+
if (imported !== 'default') {
|
|
46
|
+
reportIfEffect(specifier, imported);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const variable of context.sourceCode.getDeclaredVariables(specifier)) {
|
|
50
|
+
variable.references.forEach(reportNamespaceUse);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
ExportNamedDeclaration(node) {
|
|
55
|
+
if (node.source?.value !== 'react')
|
|
56
|
+
return;
|
|
57
|
+
for (const specifier of node.specifiers) {
|
|
58
|
+
reportIfEffect(specifier, keyName(specifier.local, false));
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niko464/eslint-config",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"repository": {
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared ESLint flat-config blocks: a comment ban, an effect-hook ban and @shadcn/lint",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Niko464/conventions.git",
|
|
8
|
+
"directory": "packages/eslint-config"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.19"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@shadcn/lint": "0.2.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@typescript-eslint/parser": ">=8.40.0",
|
|
29
|
+
"eslint": "^9.30.0 || ^10.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/estree": "1.0.9",
|
|
33
|
+
"@typescript-eslint/parser": "8.71.1",
|
|
34
|
+
"eslint": "10.12.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|