Repository navigation
Expand file tree
/
Copy patheslint.config.js
More file actions
185 lines (171 loc) · 6.96 KB
/
Copy patheslint.config.js
File metadata and controls
185 lines (171 loc) · 6.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import js from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin';
import betterTailwind from 'eslint-plugin-better-tailwindcss';
import { getDefaultSelectors } from 'eslint-plugin-better-tailwindcss/api/defaults';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import { defineConfig, globalIgnores } from 'eslint/config';
import globals from 'globals';
import tseslint from 'typescript-eslint';
// Custom rules
import jsxSiblingBlankLine from './eslint/rules/jsx-sibling-blank-line.js';
import maxComponentLines from './eslint/rules/max-component-lines.js';
import noConditionalMessageId from './eslint/rules/no-conditional-message-id.js';
/*
A component that hands classes to a part of itself takes them in an object
prop, as `labelProps={{ className: '...' }}`. The plugin reads class and
className attributes alone, so those lists are neither wrapped nor checked
until the shape is named here.
*/
const CLASSNAME_IN_PROPS_OBJECT = [
{
kind: 'attribute',
name: '.*Props$',
match: [{ type: 'objectValues', path: '^className$' }],
},
];
const MULTILINE_STATEMENTS = [
'multiline-const',
'multiline-let',
'multiline-var',
'multiline-using',
'multiline-expression',
'multiline-export',
'multiline-type',
'multiline-return',
];
export default defineConfig([
globalIgnores(['**/dist', '**/coverage', '**/.turbo', '**/routeTree.gen.ts']),
{
files: ['**/*.{ts,tsx}'],
extends: [js.configs.recommended, tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2022,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'better-tailwindcss': betterTailwind,
'@stylistic': stylistic,
},
settings: {
'better-tailwindcss': {
entryPoint: 'packages/ui/styles/index.css',
selectors: [...getDefaultSelectors(), ...CLASSNAME_IN_PROPS_OBJECT],
},
},
rules: {
...reactHooks.configs.recommended.rules,
curly: ['error', 'all'],
'nonblock-statement-body-position': ['error', 'below'],
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
// Runtime namespaces stay banned; `declare global` augmentations of library types are allowed.
'@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }],
'@stylistic/padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: '*', next: 'return' },
{ blankLine: 'always', prev: ['block', 'block-like'], next: '*' },
{ blankLine: 'always', prev: '*', next: ['block', 'block-like'] },
// A statement spanning several lines gets a blank line on both sides.
{ blankLine: 'always', prev: MULTILINE_STATEMENTS, next: '*' },
{ blankLine: 'always', prev: '*', next: MULTILINE_STATEMENTS },
],
// `@ui/*` and `@api/*` are path aliases for package internals. An app
// reaching for them bypasses the exports map and pulls in more than it asked for.
/*
Arbitrary values bypass the design system. A colour, radius, shadow or
spacing written inline is invisible to the token set, so the week the
palette lands it has to be found by hand rather than changed in one
file. Add a token instead.
*/
/*
A class list longer than the line is read by scrolling, and its diff
shows the whole list rather than the state that changed. Wrapped by
variant group, both read at a glance. Autofixable, so it costs nothing.
*/
'better-tailwindcss/enforce-consistent-line-wrapping': [
'error',
{ printWidth: 100, group: 'newLine', preferSingleLine: true, strictness: 'loose' },
],
'better-tailwindcss/no-restricted-classes': [
'error',
{
restrict: [
/*
Colours and shadows, off for now. Turn this back on once the
design team's palette lands — until then components sometimes
need a value the provisional token set has no name for.
(^|:) so an arbitrary variant like has-[>svg]: is not mistaken
for an arbitrary value.
*/
// {
// pattern: String.raw`(^|:)(bg|text|border|ring|outline|divide|fill|stroke|shadow|from|via|to|accent|caret|decoration)-\[`,
// message: 'Hardcoded value. Use a token from packages/ui/styles/theme.css.',
// },
{
// (?!var\() lets a class reference a runtime custom property,
// like the values Radix sets on a popover.
pattern: String.raw`(^|:)rounded(-(s|e|t|r|b|l|ss|se|ee|es|tl|tr|br|bl))?-\[(?!var\()`,
message: 'Hardcoded radius. Use rounded-xs through rounded-2xl, or rounded-full.',
},
{
// Only the rhythm is held to the scale. A size, a limit or an offset is
// set against the viewport, the content or a percentage, none of which
// the scale can express, so those take an arbitrary value instead.
pattern: String.raw`(^|:)(p|px|py|pt|pr|pb|pl|ps|pe|m|mx|my|mt|mr|mb|ml|ms|me|gap|gap-x|gap-y|space-x|space-y)-\[(?!var\()`,
message:
'Hardcoded spacing. The scale is driven by --spacing; add a token if none fits.',
},
{
pattern: String.raw`(^|:)(text|leading|tracking|font)-\[(?!var\()`,
message: 'Hardcoded type value. Use the --text-* and --font-weight-* tokens.',
},
],
},
],
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@ui/*', '@api/*', '@config/*'],
message:
'Apps import package subpaths (@irene/ui/...). @ui, @api and @config are for package internals.',
},
],
},
],
},
},
{
// The rules written for this repo, in eslint/rules.
files: ['**/*.{ts,tsx}'],
plugins: {
irene: {
rules: {
'jsx-sibling-blank-line': jsxSiblingBlankLine,
'max-component-lines': maxComponentLines,
'no-conditional-message-id': noConditionalMessageId,
},
},
},
rules: {
// Element children written on their own lines read as one block without a blank line between them.
'irene/jsx-sibling-blank-line': 'error',
// A message id chosen by a ternary hides both ids from the reader and the extractor.
'irene/no-conditional-message-id': 'error',
},
},
{
// A component past 350 lines of code does too much; the error says how to split it.
files: ['**/*.tsx'],
ignores: ['**/*.test.tsx', '**/*.stories.tsx'],
rules: { 'irene/max-component-lines': ['error', { max: 350 }] },
},
{
// Package internals are the one place those aliases belong.
files: ['packages/**/*.{ts,tsx}'],
rules: { 'no-restricted-imports': 'off' },
},
]);